Browse Source

2020 update

Nikolay Suslov 3 years ago
parent
commit
e8a047aaf8
83 changed files with 9556 additions and 2111 deletions
  1. 14 0
      public/core/app.js
  2. 21 5
      public/core/vwf.js
  3. 11 2
      public/core/vwf/model/javascript.js
  4. 1 1
      public/core/vwf/utility/logger.js
  5. 10 4
      public/core/vwf/utility/utility.js
  6. 1 2
      public/core/vwf/view/document.js
  7. BIN
      public/defaults/assets/textures/concrete.png
  8. BIN
      public/defaults/assets/textures/rock.png
  9. 19 1
      public/defaults/proxy/aframe/aentity.js
  10. 16 1
      public/defaults/proxy/aframe/aentity.vwf.json
  11. 65 6
      public/defaults/proxy/aframe/ascene.js
  12. 5 2
      public/defaults/proxy/aframe/avatar.js
  13. 16 16
      public/defaults/proxy/aframe/gearvrcontroller.js
  14. 2 1
      public/defaults/proxy/aframe/raycasterComponent.vwf.json
  15. 65 21
      public/defaults/proxy/aframe/xrcontroller.js
  16. 30 3
      public/defaults/proxy/aframe/xrcontroller.vwf.json
  17. 15 1
      public/defaults/proxy/animation/animation.js
  18. 3 1
      public/defaults/proxy/animation/animation.vwf.json
  19. 4 4
      public/defaults/proxy/animation/animationNode.js
  20. 2 2
      public/defaults/proxy/animation/animationNode.vwf.json
  21. 7 7
      public/defaults/proxy/objects/cursorVisual.js
  22. 69 0
      public/defaults/proxy/objects/gui/button.js
  23. 16 0
      public/defaults/proxy/objects/gui/button.vwf.json
  24. 104 0
      public/defaults/proxy/objects/gui/frame3D.js
  25. 13 0
      public/defaults/proxy/objects/gui/frame3D.vwf.json
  26. 19 6
      public/defaults/proxy/objects/legoboost.js
  27. 1 1
      public/defaults/worlds/aframe/index.vwf.json
  28. 0 10
      public/defaults/worlds/aframe2/index.vwf.json
  29. 0 0
      public/defaults/worlds/minimal/appui.js
  30. 7 0
      public/defaults/worlds/minimal/index.vwf.config.json
  31. 97 0
      public/defaults/worlds/minimal/index.vwf.js
  32. 18 0
      public/defaults/worlds/minimal/index.vwf.json
  33. 14 0
      public/defaults/worlds/minimal/info.json
  34. 22 0
      public/defaults/worlds/minimal/minimal.js
  35. BIN
      public/defaults/worlds/minimal/webimg.jpg
  36. 10 2
      public/defaults/worlds/paint/controller.js
  37. 4 2
      public/defaults/worlds/paint/index.vwf.json
  38. 0 10
      public/defaults/worlds/pure/index.vwf.config.json
  39. 0 48
      public/defaults/worlds/pure/index.vwf.js
  40. 0 16
      public/defaults/worlds/pure/index.vwf.json
  41. 0 14
      public/defaults/worlds/pure/info.json
  42. 198 0
      public/defaults/worlds/rubik/appui.js
  43. 351 0
      public/defaults/worlds/rubik/cubeModel.js
  44. 73 0
      public/defaults/worlds/rubik/cubeModel.vwf.json
  45. 273 0
      public/defaults/worlds/rubik/cubeletModel.js
  46. 33 0
      public/defaults/worlds/rubik/cubeletModel.vwf.json
  47. 18 0
      public/defaults/worlds/rubik/index.vwf.config.json
  48. 173 0
      public/defaults/worlds/rubik/index.vwf.json
  49. 14 0
      public/defaults/worlds/rubik/info.json
  50. 92 0
      public/defaults/worlds/rubik/robot.js
  51. 19 0
      public/defaults/worlds/rubik/robot.vwf.json
  52. 170 0
      public/defaults/worlds/rubik/rubik.js
  53. BIN
      public/defaults/worlds/rubik/webimg.jpg
  54. 10 0
      public/drivers/model/aframe.js
  55. 5 5
      public/drivers/model/aframe/addon/TransformControls.js
  56. 136 37
      public/drivers/model/aframe/addon/aframe-components.js
  57. 1159 1860
      public/drivers/model/aframe/aframe-master.js
  58. 2 0
      public/drivers/model/aframe/aframe-master.js.map
  59. 0 0
      public/drivers/model/aframe/aframe-master.min.js
  60. 0 0
      public/drivers/model/aframe/aframe-master.min.js.map
  61. 14 1
      public/drivers/model/aframeComponent.js
  62. BIN
      public/drivers/model/rubik/assets/back.png
  63. BIN
      public/drivers/model/rubik/assets/front.png
  64. BIN
      public/drivers/model/rubik/assets/left.png
  65. BIN
      public/drivers/model/rubik/assets/old/back.png
  66. BIN
      public/drivers/model/rubik/assets/old/front.png
  67. BIN
      public/drivers/model/rubik/assets/old/left.png
  68. BIN
      public/drivers/model/rubik/assets/old/right.png
  69. BIN
      public/drivers/model/rubik/assets/right.png
  70. 103 0
      public/drivers/model/rubik/lib/colors.js
  71. 1077 0
      public/drivers/model/rubik/lib/cubelets.js
  72. 2109 0
      public/drivers/model/rubik/lib/cubes.js
  73. 247 0
      public/drivers/model/rubik/lib/directions.js
  74. 102 0
      public/drivers/model/rubik/lib/folds.js
  75. 395 0
      public/drivers/model/rubik/lib/groups.js
  76. 93 0
      public/drivers/model/rubik/lib/queues.js
  77. 751 0
      public/drivers/model/rubik/lib/skip.js
  78. 438 0
      public/drivers/model/rubik/lib/slices.js
  79. 210 0
      public/drivers/model/rubik/lib/twists.js
  80. 461 0
      public/drivers/model/rubik/rubik.js
  81. 48 2
      public/drivers/view/aframe.js
  82. 2 1
      public/drivers/view/editor.js
  83. 79 16
      public/drivers/view/webrtc/adapter-latest.js

+ 14 - 0
public/core/app.js

@@ -1464,6 +1464,15 @@ class App {
       if (fileConf) {
         let config = JSON.parse(fileConf);
         vwfApp.conf = config
+      } else {
+        vwfApp.conf = 
+          {
+            "info":{
+              "title": "LCS Application"
+            },
+            "model": {},
+            "view": {}
+          }
       }
 
       let infoFile = val['info_json'];
@@ -1502,6 +1511,11 @@ class App {
       //Load vwf_view Document
       let dbPath = _app.helpers.appName + '_js';
       vwfApp.doc = res[0][dbPath];
+
+      //Load user model Document
+      // let modelDBPath = 'modelDriver_js';
+      // vwfApp.modelDoc = res[0][modelDBPath];
+
       //Load libs for selected config drivers
       let libs = app.getLibsForConfig(vwfApp.conf);
       if(libs.length !== 0)

+ 21 - 5
public/core/vwf.js

@@ -5114,8 +5114,17 @@ class VWF {
                 //    });
 
             } else {
-                let worldName = dbName.split('/')[1];
-                let fileName = dbName.split('/')[2] + '_json';
+                var worldName = dbName.split('/')[1];
+                var fileName = dbName.split('/')[2];
+                 //+ '_json';
+
+                if(!fileName) {
+                    worldName = self.helpers.appPath
+                    fileName = dbName + '_json';
+                    } else {
+                    fileName = fileName + '_json';
+                    }
+
                 let dbNode = _LCSDB.user(_LCS_WORLD_USER.pub).get('worlds').path(worldName).get(fileName);
 
                 let nodeProm = new Promise(res => dbNode.once(res))
@@ -5182,8 +5191,7 @@ class VWF {
 
 
 
-            let worldName = dbName.split('/')[1];
-
+            var worldName = dbName.split('/')[1];
             //let userDB = _LCSDB.user(_LCS_WORLD_USER.pub);  
 
 
@@ -5204,7 +5212,15 @@ class VWF {
                 // });
 
             } else {
-                let fileName = dbName.split('/')[2]; //dbName.replace(worldName + '/', "");
+                var fileName = dbName.split('/')[2]; //dbName.replace(worldName + '/', "");
+
+                if(!fileName) {
+                    worldName = self.helpers.appPath
+                    fileName = dbName;
+                    } else {
+                    fileName = fileName;
+                    }
+
 
                 let dbNode = _LCSDB.user(_LCS_WORLD_USER.pub).get('worlds').path(worldName).get(fileName);
                 let nodeProm = new Promise(res => dbNode.once(res))

+ 11 - 2
public/core/vwf/model/javascript.js

@@ -347,6 +347,13 @@ class VWFJavaScript extends Fabric {
                 }
             } );
 
+            Object.defineProperty( node, "randomHash", { // "this" is node
+                value: function() {
+                    let random = self.kernel.random( this.id )
+                    return Crypto.MD5(JSON.stringify(random)).toString().substring(0, 16)
+                }
+            } );
+
             // Define the "time", "client", and "moniker" properties.
 
             Object.defineProperty( node, "time", {  // TODO: only define on shared "node" prototype?
@@ -1308,8 +1315,10 @@ future.hasOwnProperty( eventName ) ||  // TODO: calculate so that properties tak
             // On read, return a function that calls `kernel.callMethod` when invoked.
 
             get: function() {  // `this` is the container
-                var node = this.node || this;  // the node via node.methods.node, or just node
+                
                 return function( /* parameter1, parameter2, ... */ ) {  // `this` is the container
+                    let node = this.node || this;  // the node via node.methods.node, or just node
+
                     var argumentsKernel =  VWFJavaScript.parametersKernelFromJS.call( self, arguments );
                     var resultKernel = self.kernel.callMethod( node.id, methodName, argumentsKernel,
                         node.private.when, node.private.callback );
@@ -1320,7 +1329,7 @@ future.hasOwnProperty( eventName ) ||  // TODO: calculate so that properties tak
             // On write, update the method body. `unsettable` methods don't accept writes.
 
             set: unsettable ? undefined : function( value ) {  // `this` is the container
-                var node = this.node || this;  // the node via node.methods.node, or just node
+                let node = this.node || this;  // the node via node.methods.node, or just node
                 self.kernel.setMethod( node.id, methodName,
                     VWFJavaScript.handlerFromFunction( value, vwf.configuration[ "preserve-script-closures" ] ) );
             },

+ 1 - 1
public/core/vwf/utility/logger.js

@@ -36,7 +36,7 @@ Virtual World Framework Apache 2.0 license  (https://github.com/NikolaySuslov/li
             this.label = undefined
             this.context = undefined
     
-            this.level = this.WARN
+            this.level = { name: "warn", number: this.WARN };//this.WARN
     
 
         }

+ 10 - 4
public/core/vwf/utility/utility.js

@@ -61,11 +61,12 @@ class Utility {
 
                 // Convert typed arrays to regular arrays.
 
-                if(object instanceof THREE.Vector2 || object instanceof THREE.Vector3 || object instanceof THREE.Vector4){
+                if ((window.THREE === !undefined) &&
+                    (object instanceof THREE.Vector2 || object instanceof THREE.Vector3 || object instanceof THREE.Vector4)) {
                     return AFRAME.utils.coordinates.stringify(object)
                 } else {
-                return isArraylike( object ) ?
-                    Array.prototype.slice.call( object ) : object;
+                    return isArraylike(object) ?
+                        Array.prototype.slice.call(object) : object;
                 }
 
                 
@@ -233,7 +234,12 @@ class Utility {
             // https://github.com/eriwen/javascript-stacktrace sniffs the browser type from the
             // exception this way.
 
-            if ( error.arguments && error.stack ) { // Chrome
+            if ( error instanceof ReferenceError) {
+
+                return "\n  " + error.stack;
+                
+            }
+            else if ( error.arguments && error.stack ) { // Chrome
 
                 return "\n  " + error.stack;
 

+ 1 - 2
public/core/vwf/view/document.js

@@ -59,7 +59,7 @@ class VWFDocument extends Fabric {
                 childSource, childType, childURI, childName);
             }
             
-            this.userView = new res.default;
+            this.doc = new res.default;
             callback(true);
           })
         } else {
@@ -68,7 +68,6 @@ class VWFDocument extends Fabric {
           }, 0);
 
         }
-
         }
 
       },

BIN
public/defaults/assets/textures/concrete.png


BIN
public/defaults/assets/textures/rock.png


+ 19 - 1
public/defaults/proxy/aframe/aentity.js

@@ -295,7 +295,7 @@ this.createEditTool = function() {
 this.globalToLocalRotation = function(aQ, order){
 
     let ord = order ? order: 'XYZ';
-    let q = this.localQuaternion().inverse().multiply(aQ); //new THREE.Quaternion().setFromEuler(euler)
+    let q = this.localQuaternion().invert().multiply(aQ); //new THREE.Quaternion().setFromEuler(euler)
     let localEuler = new THREE.Euler().setFromQuaternion(q, ord);
     return [
         THREE.Math.radToDeg(localEuler.x),
@@ -325,3 +325,21 @@ this.placeInFrontOf =  function(nodeID, dist) {
     this.rotation = rotation;
 
 }
+
+
+this.doButtonTriggerdownAction = function(button){
+    //do button action
+    console.log('TriggerdownAction form: ', button)
+}
+
+this.triggerdownAction = function(){
+}
+
+this.triggerupAction = function(){
+}
+
+this.mousedownAction = function(){
+}
+
+this.mouseupAction = function(){  
+}

+ 16 - 1
public/defaults/proxy/aframe/aentity.vwf.json

@@ -93,6 +93,12 @@
         "distance"
       ]
     },
+    "applyMatrix":{
+      "parameters": [
+        "value"
+      ]
+    },
+    "getMatrix": {},
     "worldRotation": {},
     "worldPosition": {},
     "worldScale": {},
@@ -155,7 +161,16 @@
       "parameters": [
         "value"
       ]
-    }
+    },
+    "doButtonTriggerdownAction":{
+      "parameters": [
+        "value"
+      ]
+    },
+    "triggerdownAction":{},
+    "triggerupAction":{},
+    "mousedownAction":{},
+    "mouseupAction":{}
   },
   "scripts": {
     "source": "aentity.js"

+ 65 - 6
public/defaults/proxy/aframe/ascene.js

@@ -1,9 +1,17 @@
 this.initialize = function () {
-    this.future(3).clientWatch();
+
+    if(Object.getPrototypeOf(this).id.includes('ascene.vwf')){
+        console.log("Initialize of Scene...", this.id);
+        this.future(3).clientWatch();
+    } else {
+        console.log("Initialize proto Scene..", this.id);
+    }
+   
     //this.createDefaultXRCostume();
 };
 
 this.clientWatch = function () {
+
     var self = this;
 
     if (this.children.length !== 0) {
@@ -1090,25 +1098,76 @@ this.getDefaultXRCostume = function(){
         "properties": {
             displayName: "defaultXRCostume",
             "position": "0 0 0",
-            "avatarColor": myColor
+            "avatarColor": myColor,
+            "mousedown_state": false,
+            "triggerdown_state": false
             // "height": 0.01,
             // "width": 0.01,
             // "depth": 1
         },
         "methods":{
-            triggerdown:{
-                body:'\/\/do on trigger down \n this.cursorVisual.color = "red"',
+            mousedownAction:{
+                body:`
+                this.mousedown_state = true;
+                    if(elID){
+                        //let node = this.findNodeByID(elID);
+                        vwf.callMethod(elID, "mousedownAction",[])
+                    }
+                `,
+                parameters:["point", "elID"],
+                type: "application/javascript"
+            },
+            mouseupAction:{
+                body:`
+                    if(elID){
+                        //let node = this.findNodeByID(elID);
+                        vwf.callMethod(elID, "mouseupAction",[])
+                    }
+                this.mousedown_state = false;
+                `,
+                parameters:["point", "elID"],
                 type: "application/javascript"
             },
-            triggerup:{
+            triggerdownAction:{
+                body:`
+                //do on trigger down
+                this.triggerdown_state = true;
+                this.cursorVisual.color = "red";
+
+                if(elID){
+                    //let node = this.findNodeByID(elID);
+                    let pointData = AFRAME.utils.coordinates.parse(point);
+                    vwf.callMethod(elID, "triggerdownAction",[pointData])
+                }
+                `,
+                parameters:["point", "elID"],
+                type: "application/javascript"
+            },
+            triggerupAction:{
                 body:`
                 //do on trigger up
                 this.cursorVisual.color = this.cursorVisual.avatarColor;
+                if(elID){
+                    //let node = this.findNodeByID(elID);
+                    let pointData = AFRAME.utils.coordinates.parse(point);
+                    vwf.callMethod(elID, "triggerupAction",[pointData])
+                }
+                this.triggerdown_state = false;
                 `,
+                parameters:["point", "elID"],
                 type: "application/javascript"
             },
             onMove:{
-                body:'\/\/do on move \n ',
+                body:`
+                 if(this.mousedown_state || this.triggerdown_state){
+                    if(idata){
+                        //console.log('Move POINT: ', idata.point, + ' on ' + idata.elID);
+                        let point = AFRAME.utils.coordinates.parse(idata.point);
+                        vwf.callMethod(idata.elID, "moveAction",[point])
+                    }
+                }
+                `,
+                parameters:["idata"],
                 type: "application/javascript"
             }
         },

+ 5 - 2
public/defaults/proxy/aframe/avatar.js

@@ -101,8 +101,11 @@ this.createAvatarBody = function (nodeDef, modelSrc) {
                 //this.myHead.myCursor.line.color = myColor;
 
                 let cursorEl = this.getScene()['mouse-' + this.parent.id.slice(7)];
-                cursorEl.xrnode.controller.cursorVisual.avatarColor = myColor;
-                cursorEl.xrnode.controller.cursorVisual.color = myColor;
+                if(cursorEl){
+                    cursorEl.xrnode.controller.cursorVisual.avatarColor = myColor;
+                    cursorEl.xrnode.controller.cursorVisual.color = myColor;
+                }
+                
                 `
 
             }

+ 16 - 16
public/defaults/proxy/aframe/gearvrcontroller.js

@@ -41,22 +41,22 @@ this.simpleDef = {
                         objects: ".gearvrhit"
                     }
                 },
-                "myRayCaster": {
-                    "extends": "proxy/aframe/aentity.vwf",
-                    "properties": {},
-                    "children": {
-                        "raycaster": {
-                            "extends": "proxy/aframe/raycasterComponent.vwf",
-                            "type": "component",
-                            "properties": {
-                                recursive: false,
-                                interval: 10,
-                                far: 0.5,
-                                objects: ".gearvrcontroller"
-                            }
-                        }
-                    }
-                }
+                // "myRayCaster": {
+                //     "extends": "proxy/aframe/aentity.vwf",
+                //     "properties": {},
+                //     "children": {
+                //         "raycaster": {
+                //             "extends": "proxy/aframe/raycasterComponent.vwf",
+                //             "type": "component",
+                //             "properties": {
+                //                 recursive: false,
+                //                 interval: 10,
+                //                 far: 0.5,
+                //                 objects: ".gearvrcontroller"
+                //             }
+                //         }
+                //     }
+                // }
                 // "rotationText": {
                 //     "extends": "proxy/aframe/atext.vwf",
                 //     "properties":{

+ 2 - 1
public/defaults/proxy/aframe/raycasterComponent.vwf.json

@@ -17,6 +17,7 @@
       "parameters": [
         "nodeID"
     ]
-    }
+    },
+    "getIntersectedElement":{}
   }
 }

+ 65 - 21
public/defaults/proxy/aframe/xrcontroller.js

@@ -39,20 +39,24 @@ this.createController = function (pos, modelSrc) {
         }
     }
 
-    let myRayCaster = {
-                "extends": "proxy/aframe/raycasterComponent.vwf",
-                "type": "component",
-                "properties": {
-                    recursive: false,
-                    interval: 0,
-                    far: 10,
-                    objects: ".intersectable",
-                    showLine: false
-                }
-            }
+    // let myRayCaster = {
+    //             "extends": "proxy/aframe/raycasterComponent.vwf",
+    //             "type": "component",
+    //             "properties": {
+    //                 recursive: false,
+    //                 interval: 0,
+    //                 far: 10,
+    //                 objects: ".intersectable",
+    //                 showLine: false
+    //             }
+    //         }
         
     
-    this.children.create( "raycaster", myRayCaster );        
+    // this.children.create( "raycaster", myRayCaster );
+
+    this.createLocalRaycaster();
+
+    
     this.children.create( "interpolation", interpolation );
     this.children.create("xrnode", newNode, function(child){
         if(child) {
@@ -64,11 +68,11 @@ this.createController = function (pos, modelSrc) {
 
 }
 
-this.moveVRController = function(){
+this.moveVRController = function(idata){
 
     let controller = this.xrnode.controller;
     if(controller){
-        controller.onMove();
+        controller.onMove(idata);
     }
 
     // let point = this.raycaster.getIntersectionPoint();
@@ -112,19 +116,49 @@ this.initialize = function() {
    // this.future(0).update();
 }
 
-this.triggerdown = function() {
+this.mousedown = function(point, elID) {
+    let controller = this.xrnode.controller;
+    if(controller){
+        this.showHandSelection(point);
+        controller.mousedownAction(point, elID);
+    }
+    //this.xrnode.controller.pointer.material.color = 'red'
+ }
+
+ this.mouseup = function(point, elID) {
+    let controller = this.xrnode.controller;
+    if(controller){
+        this.resetHandSelection();
+        controller.mouseupAction(point, elID);
+    }
+    //this.xrnode.controller.pointer.material.color = 'green'
+ }
+
+//  this.triggerup = function() {
+//     let controller = this.xrnode.controller;
+//     if(controller){
+//         controller.triggerup();
+//     }
+//     //this.xrnode.controller.pointer.material.color = 'green'
+//  }
+
+this.triggerdown = function(point, elID) {
     let controller = this.xrnode.controller;
+
     if(controller){
-        controller.triggerdown();
+        this.showHandSelection(point);
+        controller.triggerdownAction(point, elID);
     }
     //this.xrnode.controller.pointer.material.color = 'red'
  }
 
- this.triggerup = function() {
+ this.triggerup = function(point, elID) {
     let controller = this.xrnode.controller;
     if(controller){
-        controller.triggerup();
+        this.resetHandSelection();
+        controller.triggerupAction(point, elID);
     }
+    
     //this.xrnode.controller.pointer.material.color = 'green'
  }
 
@@ -199,13 +233,23 @@ this.setControllerNode = function(modelSrc){
 
 this.showHandSelection = function (point) { 
 
+    //let data = this.raycaster.getIntersectionPoint();
+    if(point){  
     let end = this.xrnode.controller.cursorVisual.worldToLocal(point);
     //this.xrnode.controller.line.end = end;
     this.xrnode.controller.cursorVisual.end = end;
-
+}
 }
 
 this.resetHandSelection = function () { 
     //this.xrnode.controller.line.end = "0 0 -3";
-    this.xrnode.controller.cursorVisual.end = "0 0 -0.2";
-}
+    if(this.xrnode.controller.cursorVisual){
+        this.xrnode.controller.cursorVisual.end = "0 0 -0.2"
+
+    }
+    }
+
+
+this.createLocalRaycaster = function () {
+    //only on view
+ }

+ 30 - 3
public/defaults/proxy/aframe/xrcontroller.vwf.json

@@ -29,8 +29,30 @@
                 "rotation"
             ]
         },
-        "triggerdown": {},
-        "triggerup": {},
+        "triggerdown": {
+            "parameters": [
+                "point",
+                "elID"
+            ]
+        },
+        "triggerup": {
+            "parameters": [
+                "point",
+                "elID"
+            ]
+        },
+        "mousedown": {
+            "parameters": [
+                "point",
+                "elID"
+            ]
+        },
+        "mouseup": {
+            "parameters": [
+                "point",
+                "elID"
+            ]
+        },
         "checkDefaultXRCostume":{},
         "setControllerNode":
         {
@@ -39,7 +61,12 @@
             ]
         },
         "saveToScene":{},
-        "moveVRController":{}
+        "moveVRController":{
+            "parameters": [
+                "idata"
+            ]
+        },
+        "createLocalRaycaster":{}
     },
     "scripts": {
         "source": "xrcontroller.js"

+ 15 - 1
public/defaults/proxy/animation/animation.js

@@ -164,7 +164,8 @@ this.animationFrame_get = function () {
 }
 
 //methods
-this.animationPlay = function (startTime, stopTime) {
+this.animationPlay = function (startTime, stopTime, cb) {
+
     if (!isNaN(stopTime)) {
         this.animationStopTime = stopTime;
     }
@@ -172,6 +173,7 @@ this.animationPlay = function (startTime, stopTime) {
         this.animationStartTime = startTime;
     }
     this.animationPlaying = true;
+    this.animationStoppedCallback = cb;
 }
 
 this.animationPause = function () {
@@ -293,3 +295,15 @@ this.initialize = function () {
     }
 
 } //@ sourceURL=http://vwf.example.com/animation.vwf/scripts~initialize
+
+this.animationStopped = function(){
+    //console.log("Animation stopped");
+
+    if(this.animationStoppedCallback){
+        let data = this.animationStoppedCallback.split(':');
+        let args = data ? JSON.parse(data[1]) : [];
+        vwf.callMethod(this.id, data[0], args);
+    }
+    this.animationStoppedCallback = null;
+    
+}

+ 3 - 1
public/defaults/proxy/animation/animation.vwf.json

@@ -17,6 +17,7 @@
       "set": "this.animationPlaying_set(value)",
       "get": "return this.animationPlaying_get()"
     },
+    "animationStoppedCallback": null,
     "animationTimeUpdated": null,
     "animationStartSIM": null,
     "animationPauseSIM": null,
@@ -53,7 +54,8 @@
     "animationPlay": {
       "parameters": [
         "startTime",
-        "stopTime"
+        "stopTime",
+        "cb"
       ]
     },
     "animationPause": {},

+ 4 - 4
public/defaults/proxy/animation/animationNode.js

@@ -49,14 +49,14 @@ this.translateTo = function(translation, duration){
     } //@ sourceURL=node3.animation.translateTo.vwf
 }
 
-this.rotateBy = function(rotation, duration, frame) {
+this.rotateBy = function(rotation, duration, cb, frame) {
     let rotationValue = this.translationFromValue(rotation);
     let deltaQuaternion = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
        (THREE.Math.degToRad(rotationValue.x)),
        (THREE.Math.degToRad(rotationValue.y)),
        (THREE.Math.degToRad(rotationValue.z)), 'XYZ'
      ));
-     this.quaterniateBy( deltaQuaternion, duration, frame ); //@ sourceURL=node3.animation.rotateBy.vwf
+     this.quaterniateBy( deltaQuaternion, duration, cb, frame ); //@ sourceURL=node3.animation.rotateBy.vwf
 }
 
 this.rotateTo = function(rotation, duration){
@@ -69,7 +69,7 @@ this.rotateTo = function(rotation, duration){
     this.quaterniateTo( stopQuaternion, duration ); //@ sourceURL=node3.animation.rotateTo.vwf
 }
 
-this.quaterniateBy = function(quaternion, duration, frame) {
+this.quaterniateBy = function(quaternion, duration, cb, frame) {
 
       this.startQuaternionSIM = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
           (THREE.Math.degToRad(this.rotation.x)),
@@ -95,7 +95,7 @@ this.quaterniateBy = function(quaternion, duration, frame) {
           let interp = e.setFromQuaternion(q, 'XYZ');
           this.rotation = [THREE.Math.radToDeg(interp.x), THREE.Math.radToDeg(interp.y), THREE.Math.radToDeg(interp.z)];
         }
-        this.animationPlay(0, duration);
+        this.animationPlay(0, duration, cb);
       }
       else {
         let eE = new THREE.Euler();

+ 2 - 2
public/defaults/proxy/animation/animationNode.vwf.json

@@ -18,7 +18,7 @@
   },
   "rotateBy":{
     "parameters": [
-        "rotation", "duration", "frame"
+        "rotation", "duration", "cb", "frame"
     ]
   },
   "rotateTo":{
@@ -28,7 +28,7 @@
   },
   "quaterniateBy":{
     "parameters": [
-        "quaternion", "duration", "frame"
+        "quaternion", "duration", "cb", "frame"
     ]
   },
   "quaterniateTo":{

+ 7 - 7
public/defaults/proxy/objects/cursorVisual.js

@@ -2,10 +2,7 @@ this.createVisual = function () {
 
     let parent = this.parent;
 
-    let p1 = [
-        { x: 0, y: 0, z: 0 },
-        { x: 0, y: 0, z: -0.2}
-    ]
+    let p1 = [{"x":0,"y":0,"z":-0.1},{"x":0,"y":0,"z":-0.2}]
 
     let visNode = {
         "extends": "proxy/aframe/aentity.vwf",
@@ -14,10 +11,11 @@ this.createVisual = function () {
             "cone":{
                 "extends": "proxy/aframe/acone.vwf",
                 "properties":{
-                    "height": 0.4,
+                    "height": 0.2,
                     "radius-bottom": 0.01,
                     "radius-top": 0.001,
-                    "rotation": [-90, 0, 0]
+                    "rotation": [-90, 0, 0],
+                    "position": [0, 0, -0.1]
                 },
                 "children": {
                     "material": {
@@ -40,7 +38,7 @@ this.createVisual = function () {
                         "properties": {
                             "color": this.color,
                             "path": p1,
-                            "width": this.width,
+                            "width": 0.08, //this.width
                             "taper": true,
                             "transparent": true,
                             "opacity": 0.6
@@ -67,8 +65,10 @@ this.end_get = function () {
 this.color_set = function (value) {
     //this.avatarColor = value;
     this.color = value;
+    if(this.visualNode){
     this.visualNode.p1.linepath.color = value;
     this.visualNode.cone.material.color = value;
+    }
 }
 
 this.color_get = function () {

+ 69 - 0
public/defaults/proxy/objects/gui/button.js

@@ -0,0 +1,69 @@
+this.init = function(){
+
+
+    this.height = this.height ? this.height : 0.3;
+    this.width = this.width? this.width : 0.4;
+    this.class = "clickable intersectable";
+
+    this.baseColor = this.baseColor ? this.baseColor : 'white';
+    this.hoverColor = this.hoverColor ? this.hoverColor : 'green';
+    this.clickColor = this.clickColor ? this.clickColor : 'blue';
+
+    let material = {
+        "extends": "proxy/aframe/aMaterialComponent.vwf",
+        "type": "component",
+        "properties": {
+            "transprent": true,
+            "opacity": 0.3,
+            "color": "white",
+            "side": "double"
+        }
+    }
+
+    let cursorListener = {
+        "extends": "proxy/aframe/app-raycaster-listener-component.vwf",
+        "type": "component"
+      }
+
+      let raycasterListener = {
+        "extends": "proxy/aframe/app-raycaster-listener-component.vwf",
+        "type": "component"
+      }
+
+      this.children.create('material', material);
+      this.children.create('cursor-listener', cursorListener);
+      this.children.create('raycaster-listener', raycasterListener);
+
+
+}
+
+this.intersectEventMethod = function(){
+    this.material.opacity = 0.6;
+    this.material.color = this.hoverColor
+}
+
+this.clearIntersectEventMethod = function(){
+    this.material.opacity = 0.3;
+    this.material.color = this.baseColor
+}
+
+this.mousedownAction = function(){
+    this.triggerdownAction();
+}
+
+this.mouseupAction = function(){
+    this.triggerupAction();
+
+}
+
+this.triggerdownAction = function(){
+    this.material.color = this.clickColor;
+
+    let target = this.getScene().findNode(this.target);
+    target.doButtonTriggerdownAction(this.id);
+}
+
+this.triggerupAction = function(){
+    this.material.color = this.baseColor;
+    
+}

+ 16 - 0
public/defaults/proxy/objects/gui/button.vwf.json

@@ -0,0 +1,16 @@
+{
+    "extends": "proxy/aframe/aplane.vwf",
+    "properties": {
+        "target": null,
+        "hoverColor": {
+            "set": "this.hoverColor = value",
+            "get": "return this.baseColor"
+            }
+    },
+    "methods": {
+        "init": {}
+    },
+    "scripts": {
+        "source": "button.js"
+    }
+}

+ 104 - 0
public/defaults/proxy/objects/gui/frame3D.js

@@ -0,0 +1,104 @@
+this.initialize = function () {
+
+    if (Object.getPrototypeOf(this).id.includes('frame3D.vwf')) {
+        this.createVisual();
+    } else {
+        console.log("Initialize proto..", this.id);
+    }
+}
+
+this.createVisual = function () {
+
+    let parent = this.parent;
+
+    let visNode = {
+        "extends": "proxy/aframe/aentity.vwf",
+        "properties": {},
+        "children": {
+            "rotatePlane": {
+                "extends": "proxy/aframe/aplane.vwf",
+                "properties": {
+                    "count": 0,
+                    "height": 0.4,
+                    "width": 3,
+                    "class": "clickable intersectable"
+                },
+                "children": {
+                    "material": {
+                        "extends": "proxy/aframe/aMaterialComponent.vwf",
+                        "type": "component",
+                        "properties": {
+                            "color": "white",
+                            "side": "double"
+                            //"transparent": true,
+                            //"opacity": 0.6
+                        }
+                    },
+                    "cursor-listener": {
+                        "extends": "proxy/aframe/app-cursor-listener-component.vwf",
+                        "type": "component"
+                    },
+                    "raycaster-listener": {
+                        "extends": "proxy/aframe/app-raycaster-listener-component.vwf",
+                        "type": "component"
+                    }
+
+                },
+                "methods":{
+                    "moveAction":{
+                        "parameters":["point"],
+                        "type": "application/javascript",
+                        "body": `
+                            let contentsID = this.parent.parent.contentsID;
+                            console.log('Start: ', this.dragStart, ' x: ', point.x);
+                            if(contentsID){
+                                let scene = this.getScene();
+                                let contents = scene.findNode(contentsID);
+                                let rotation = contents.rotation.clone();
+
+                                let offset = this.dragStart ? (point.x - this.dragStart.x)*10 : 0
+
+                                contents.rotation = [rotation.x, rotation.y+offset, rotation.z]
+                                
+                                //contents.rotateBy([0,point.x, 0]);
+                                
+                            }
+                            
+                        `
+                    },
+                    "triggerdownAction":{
+                        "body": `
+                            this.material.color = 'blue';
+                            this.dragStart = point;
+                            `,
+                        parameters:["point"],
+                          "type": "application/javascript"
+                        },
+                        "triggerupAction":{
+                            "body": `
+                                this.material.color = 'grey';
+                                this.dragStart = {x:0, y:0, z:0};
+                                `,
+                                parameters:["point"],
+                              "type": "application/javascript"
+                            }
+                }
+            }
+        }
+    }
+
+    this.children.create("visualNode", visNode);
+
+
+}
+
+
+
+
+// this.intersectEventMethod = function () {
+
+// }
+
+// this.clearIntersectEventMethod = function () {
+
+// }

+ 13 - 0
public/defaults/proxy/objects/gui/frame3D.vwf.json

@@ -0,0 +1,13 @@
+{
+    "extends": "proxy/aframe/aentity.vwf",
+    "properties": {
+        "contentsID":null
+    },
+    "methods": {
+        "createVisual": {},
+        "initialize":{}
+    },
+    "scripts": {
+        "source": "frame3D.js"
+    }
+}

+ 19 - 6
public/defaults/proxy/objects/legoboost.js

@@ -7,7 +7,7 @@ this.initialize = function () {
 
 this.createVisual = function () {
 
-    let motorNode = function (position, rotation) {
+    let motorNode = function (motorName, position, rotation) {
 
         let rot = rotation ? rotation : [0, 0, 0];
 
@@ -35,8 +35,8 @@ this.createVisual = function () {
                     "extends": "proxy/aframe/abox.vwf",
                     "properties": {
                         "height": 0.4,
-                        "width": 0.05,
-                        "depth": 0.4
+                        "width": 0.4,
+                        "depth": 0.05
                     },
                     "children": {
                         "material": {
@@ -45,6 +45,18 @@ this.createVisual = function () {
                             "properties": {
                                 "color": "orange"
                             }
+                        },
+                        "label":{
+                            "extends": "proxy/aframe/atext.vwf",
+                            "properties": {
+                                "displayName": motorName,
+                                "color": "black",
+                                "value": motorName,
+                                "side": "double",
+                                "position": [-0.02,0,0.07],
+                                "rotation": [0,0,0],
+                                "scale":[0.5,0.5,0.5]
+                            }
                         }
                     }
                 }
@@ -71,9 +83,10 @@ this.createVisual = function () {
 
                 }
             },
-            "motorA": motorNode([0.3, 0, 0]),
-            "motorB": motorNode([-0.3, 0, 0]),
-            "motorC": motorNode([0, 0, 0.3], [0, 90, 0])
+            "motorA": motorNode('A', [0.3, 0, 0], [0, 90, 0]),
+            "motorB": motorNode('B', [-0.3, 0, 0], [0, -90, 0]),
+            "motorC": motorNode('C', [0, 0, 0.3], [0, 0, 0]),
+            "motorD": motorNode('D', [0, 0, -0.3], [0, 180, 0])
         }
     }
 

+ 1 - 1
public/defaults/worlds/aframe/index.vwf.json

@@ -5,7 +5,7 @@
   },
   "methods": {
     "initialize": {
-      "body": "    var runBox = vwf.find(\"\", \"/sphere/box2\")[0];\n    console.log(runBox);\n    vwf.callMethod(runBox, \"run\");\n",
+      "body": "var runBox = vwf.find(\"\", \"/sphere/box2\")[0];\n    console.log(runBox);\n    vwf.callMethod(runBox, \"run\");\n",
       "type": "application/javascript"
     }
   },

+ 0 - 10
public/defaults/worlds/aframe2/index.vwf.json

@@ -260,16 +260,6 @@
             }
           },
           "children": {
-            "raycaster": {
-              "extends": "proxy/aframe/raycasterComponent.vwf",
-              "type": "component",
-              "properties": {
-                "recursive": false,
-                "interval": 10,
-                "far": 2,
-                "objects": ".intersectable"
-              }
-            },
             "material": {
               "extends": "proxy/aframe/aMaterialComponent.vwf",
               "properties": {

+ 0 - 0
public/defaults/worlds/pure/appui.js → public/defaults/worlds/minimal/appui.js


+ 7 - 0
public/defaults/worlds/minimal/index.vwf.config.json

@@ -0,0 +1,7 @@
+{
+  "info":{
+    "title": "Minimal App"
+  },
+  "model": {},
+  "view": {}
+}

+ 97 - 0
public/defaults/worlds/minimal/index.vwf.js

@@ -0,0 +1,97 @@
+import { h, text,patch } from "$host/lib/ui/superfine.js"
+
+class UserView {
+
+    constructor(view) {
+        this.view = view;
+        this.init();
+    }
+    
+
+    init() {
+        let self = this;
+
+        vwf_view.initializedNode = function (nodeID, childID, childExtendsID, childImplementsIDs,
+            childSource, childType, childIndex, childName) {
+            
+            if (childID == vwf_view.kernel.application()) {
+
+                ["time", "clicks", "random"].forEach(name => {
+
+                    let el = document.createElement(name);
+                    el.setAttribute("id", name);
+                    document.querySelector("body").appendChild(el);
+
+                })
+
+                self.satTime(0);
+                self.satClicks(0);
+                self.satRandom(0);
+
+            }
+        }
+
+        vwf_view.satProperty = function (nodeID, propertyName, propertyValue) {
+
+            if (propertyValue === undefined || propertyValue == null) {
+                return;
+            }
+            //let el = document.querySelector("[id='" + nodeID + "']");
+
+            if (!document.getElementById("time"))
+                 return
+
+
+            if (propertyName == 'timeCount') {
+                self.satTime(propertyValue);
+            }
+
+            if(propertyName == 'clicks'){
+                self.satClicks(propertyValue)
+            }
+
+            if(propertyName == 'randomNumber'){
+                self.satRandom(propertyValue);
+
+                //update body color
+                let randomColor = Math.floor(parseFloat(propertyValue)*16777215).toString(16);
+                document.body.style.backgroundColor = "#" + randomColor;
+            }
+
+        }
+    }
+
+    satTime(state) {
+        patch(
+            document.getElementById("time"), 
+            h("time", {style: "position: absolute; top: 100px; margin-left: 20px;" }, [
+                    h("h2", {}, text('Time: ')), 
+                    h("h1", {}, text(Math.floor(state)))
+        ]))
+    }
+
+    satClicks(state) {
+        patch(
+          document.getElementById("clicks"),
+          h("clicks", {style: "position: absolute; top: 240px; margin-left: 20px;"}, [
+            h("h2", {}, text('Clicks: ')), 
+            h("h1", {}, text(state)),
+            h("button", { onclick: () => vwf_view.kernel.callMethod(vwf.application(), "incClicks") }, text("+")),
+            h("button", { onclick: () => vwf_view.kernel.callMethod(vwf.application(), "decClicks") }, text("-"))
+          ])
+        )
+    }
+
+    satRandom(state) {
+        patch(
+          document.getElementById("random"),
+          h("random", {style: "position: absolute; top: 400px; margin-left: 20px;"}, [
+            h("h2", {}, text('Random number: ')), 
+            h("h1", {}, text(state)),
+            h("button", { onclick: () => vwf_view.kernel.callMethod(vwf.application(), "getRandom") }, text("Generate"))
+          ])
+        )
+    }
+}
+
+export {UserView as default}

+ 18 - 0
public/defaults/worlds/minimal/index.vwf.json

@@ -0,0 +1,18 @@
+{
+  "extends": "proxy/node.vwf",
+  "properties": {
+    "timeCount": 0,
+    "clicks": 0,
+    "randomNumber": null
+  },
+  "methods":{
+    "initialize": {},
+    "run": {},
+    "incClicks": {},
+    "decClicks": {},
+    "getRandom": {}
+  },
+  "scripts":{
+    "source": "minimal.js"
+  }
+}

+ 14 - 0
public/defaults/worlds/minimal/info.json

@@ -0,0 +1,14 @@
+{
+    "info": {
+        "en": {
+            "title": "Minimal app",
+            "imgUrl": "/defaults/worlds/minimal/webimg.jpg",
+            "text": "Minimal app example"
+        },
+        "ru": {
+            "title": "Minimal app",
+            "imgUrl": "/defaults/worlds/minimal/webimg.jpg",
+            "text": "Minimal app example"
+        }
+    }
+}

+ 22 - 0
public/defaults/worlds/minimal/minimal.js

@@ -0,0 +1,22 @@
+this.initialize = function () {
+    this.run();
+}
+
+this.run = function () {
+    this.timeCount = this.time;
+    console.log(this.timeCount);
+    this.future(1).run();
+}
+
+this.incClicks = function () {
+    this.clicks = this.clicks + 1
+}
+
+this.decClicks = function () {
+    this.clicks = this.clicks - 1
+}
+
+this.getRandom = function () {
+    this.randomNumber = this.random();
+}
+

BIN
public/defaults/worlds/minimal/webimg.jpg


+ 10 - 2
public/defaults/worlds/paint/controller.js

@@ -1,4 +1,4 @@
-this.triggerdown = function(){
+this.triggerdownAction = function(){
     let scene = this.getScene();
     this.pointer.material.color = "white";
     this.penDown = true;
@@ -6,7 +6,7 @@ this.triggerdown = function(){
     scene.createDrawNode(scene.drawBox, this.penName, "#f9f9f9", 0.007, "0 0 0");
 }
 
-this.triggerup = function(){
+this.triggerupAction = function(){
     this.pointer.material.color = "green";
     this.penDown = false;
 }
@@ -23,3 +23,11 @@ this.onMove = function(){
     
 }
 
+this.mouseupAction = function(){
+
+}
+
+this.mousedownAction = function(){
+    
+}
+

+ 4 - 2
public/defaults/worlds/paint/index.vwf.json

@@ -143,8 +143,10 @@
         "penName": "drawNode"
       },
       "methods": {
-        "triggerdown": {},
-        "triggerup": {},
+        "triggerdownAction": {},
+        "triggerupAction": {},
+        "mousedownAction": {},
+        "mouseupAction": {},
         "onMove": {}
       },
       "scripts":{

+ 0 - 10
public/defaults/worlds/pure/index.vwf.config.json

@@ -1,10 +0,0 @@
-{
-  "info":{
-    "title": "Pure App"
-  },
-  "model": {
-  },
-  "view": {
-    "/drivers/view/editor": null
-  }
-}

+ 0 - 48
public/defaults/worlds/pure/index.vwf.js

@@ -1,48 +0,0 @@
-import { h, text,patch } from "$host/lib/ui/superfine.js"
-
-class UserView {
-
-    constructor(view) {
-        this.view = view;
-        this.init();
-    }
-    
-
-    init() {
-        let self = this;
-
-        vwf_view.initializedNode = function (nodeID, childID, childExtendsID, childImplementsIDs,
-            childSource, childType, childIndex, childName) {
-            
-            if (childID == vwf_view.kernel.application()) {
-                let el = document.createElement("pure");
-                el.setAttribute("id", childID);
-                document.querySelector("body").appendChild(el);
-            }
-        }
-
-        vwf_view.satProperty = function (nodeID, propertyName, propertyValue) {
-
-            if (propertyValue === undefined || propertyValue == null) {
-                return;
-            }
-            let el = document.querySelector("[id='" + nodeID + "']");
-
-            if (!el)
-                return
-
-            if (propertyName == 'pure') {
-                self.updatePure(propertyValue, el)
-            }
-        }
-    }
-
-    updatePure(state, el) {
-        patch(
-            el, h("pure", {style: "position: absolute; top: 100px;" }, [
-                    h("h1", {}, text(state))
-            ]));
-    }
-}
-
-export {UserView as default}

+ 0 - 16
public/defaults/worlds/pure/index.vwf.json

@@ -1,16 +0,0 @@
-{
-  "extends": "proxy/node.vwf",
-  "properties": {
-    "pure": 1
-  },
-  "methods":{
-    "initialize": {
-			"body": "this.run();\n",
-			"type": "application/javascript"
-		},
-    "run": {
-      "body": "this.pure = this.time; \n console.log(this.pure);\n this.future(1).run(); \n ",
-			"type": "application/javascript"
-    }
-  }
-}

+ 0 - 14
public/defaults/worlds/pure/info.json

@@ -1,14 +0,0 @@
-{
-    "info": {
-        "en": {
-            "title": "Pure app",
-            "imgUrl": "/defaults/assets/webimg.jpg",
-            "text": "Pure app example"
-        },
-        "ru": {
-            "title": "Pure app",
-            "imgUrl": "/defaults/assets/webimg.jpg",
-            "text": "Pure app example"
-        }
-    }
-}

+ 198 - 0
public/defaults/worlds/rubik/appui.js

@@ -0,0 +1,198 @@
+//-----App ui-----
+
+function createApp() {
+
+    let self = this
+
+    if(!window._rubik){
+        window._rubik = {
+            command: 'Ff'
+        };
+    }
+   
+    
+
+    function makeRobotButtons() {
+        let nodeNames = ['Left', 'Right', 'Back', 'Front' ];
+        return nodeNames.map(el => {
+            return self.widgets.gridListItem({
+                imgSrc: "/drivers/model/rubik/assets/" + el.toLowerCase() + ".png",
+                imgSize: "30px",
+                styleClass:"", //"createListItem",
+                title: el,
+                onclickfunc: function () {
+                    
+                    window._LegoView.changeDeviceID(el.toLowerCase());
+
+                    var status = document.querySelector('#currentRobotID');
+                    status._val = _LegoView.device.id    
+                    // status.$components = [
+                    //     {
+                    //         $type: "h3",
+                    //         $text: _LegoView.device.id      
+                    //     }
+                    // ] 
+
+                    //let sceneID = vwf.find("", "/")[0];
+                    //let boostID = _LegoView.device.id; //_LegoView.isConnected() ? _LegoView.device.id : 'none';
+                    //vwf_view.kernel.callMethod(sceneID, "createLegoBoost", [boostID]);
+                }
+            })
+        })
+    }
+
+    function doFun() {
+        let sceneID = vwf.find("", "/")[0];
+        vwf_view.kernel.callMethod(sceneID, "doOnRubik", [window._rubik.command]);
+    }
+
+    return {
+        $cell: true,
+        $type: "div",
+        class: "propGrid max-width mdc-layout-grid mdc-layout-grid--align-left",
+        $components: [
+            {
+                $cell: true,
+                $type: "div",
+                class: "mdc-layout-grid__inner",
+                $components: [
+                    {
+                        $cell: true,
+                        $type: "div",
+                        class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                        $components: [
+                            {
+                                $cell: true,
+                                $type: "button",
+                                class: "mdc-button mdc-button--raised",
+                                $text: "Create Rubik",
+                                onclick: function (e) {
+                                    let sceneID = vwf.find("", "/")[0];
+                                    let rubikID = "rubik-" + _app.helpers.randId();
+                                    vwf_view.kernel.callMethod(sceneID, "createRubik", [rubikID, false]);
+                                }
+
+                            }
+                        ]
+                    },
+                    {
+                        $type: "div",
+                        class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                        $components: [
+                            {
+                                $type: "h2",
+                                $text: "Rubik Robot"
+                            }
+
+                        ]
+                    },
+                    {
+                        $cell: true,
+                        $type: "div",
+                        class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                        $components: [
+                            {
+                                $cell: true,
+                                $type: "button",
+                                class: "mdc-button mdc-button--raised",
+                                $text: "Create Rubik & Robot",
+                                onclick: function (e) {
+                                    let sceneID = vwf.find("", "/")[0];
+                                    let rubikID = "rubik-" + _app.helpers.randId();
+                                    vwf_view.kernel.callMethod(sceneID, "createRubik", [rubikID, true]);
+                                }
+
+                            }
+                        ]
+                    },
+                    {
+                        $type: "div",
+                        class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                        $components: [
+                            {
+                                $type: "div",
+                                class: "mdc-layout-grid__inner",
+                                $components: makeRobotButtons()
+                            }
+                        ]
+                    },
+                    
+                    {
+                        $cell: true,
+                        $type: "div",
+                        class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                        id: "currentRobotID",
+                        _val: "none",
+                        $components: [
+                            {
+                                $type: "h3",
+                                $text: _LegoView.device.id 
+                            }
+                        ],
+                        $update: function(){
+                            this.$components = [
+                                {
+                                    $type: "h3",
+                                    $text: this._val   
+                                }
+                            ]
+                        }
+                        
+                    },
+                    {
+                        $cell: true,
+                        $type: "div",
+                        class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                        $components: [
+                            self.widgets.buttonRaised(
+                                {
+                                    "label": _LegoView.isConnected() ? "Disconnect" : "Connect",
+                                    "onclick": function (e) {
+                                        if (!_LegoView.isConnected()) {
+                                            this.$text = 'Disconnect';
+                                            _LegoView.connect();
+                                        } else {
+                                            this.$text = 'Connect';
+                                            _LegoView.disconnect();
+                                        }
+                                    }
+                                }
+                            )
+                        ]
+                    },
+
+                    {
+                        $cell: true,
+                        $type: "div",
+                        class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                        $components: [
+                            self.widgets.inputTextFieldOutlined({
+                                "id": 'commandt',
+                                "label": "Command: ",
+                                "value": window._rubik.command,
+                                "change": function (e) {
+                                    window._rubik.command = this.value;
+                                }
+                            })
+
+                        ]
+                    },
+                    {
+                        $cell: true,
+                        $type: "div",
+                        class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                        $components: [
+                            self.widgets.buttonRaised(
+                                {
+                                    "label": "DO",
+                                    "onclick": doFun
+                                })
+                        ]
+                    },
+                    _app.widgets.divider
+
+                ]
+            }
+        ]
+    }
+}

+ 351 - 0
public/defaults/worlds/rubik/cubeModel.js

@@ -0,0 +1,351 @@
+this.initialize = function(){
+    
+    if(Object.getPrototypeOf(this).id == 'cubeModel.vwf'){
+       
+        console.log("Initialize of child..", this.id);
+        this.getCubelets();
+        this.twistLoop();
+        // this.cubeModel = new Cube(this.id);
+        // this.cubeModel.nodeID = this.id;
+    } else {
+        console.log("Initialize proto..", this.id);
+    }
+}
+
+this.cubeID_set = function(value){
+    this.cubeID = value;
+    //this.initializeCubelets();
+}
+
+this.cubeID_get = function(){
+    // if(!this.cubeModel){
+    //     this.cubeModel = new Cube(value);
+    //     this.cubeModel.nodeID = this.id;
+    // }
+    return this.cubeID
+}
+
+this.initializeCubelets = function(){
+
+    let that = this;
+    let cubeModelID = this.getCubeModelID();
+    let cubelets = this.getCubelets();
+    cubelets.forEach(el=>{
+       
+        let distance = 1.1;
+        let position = [el.addressX*distance, el.addressY*distance, el.addressZ*distance];
+        let cubeletID = el.id;
+
+        let cubelet = {
+            "extends": "cubeletModel.vwf",
+            "properties":{
+                "cubeletID": cubeletID,
+                "cubeID": cubeModelID,
+                "cubeNodeID": this.id,
+                "size": 1
+            },
+            "children":{
+                "wrapper":{
+                    "extends": "proxy/aframe/aentity.vwf",
+                    "properties":{
+                    "position": position
+                    }
+                },
+                "interpolation":
+                {
+                    "extends": "proxy/aframe/interpolation-component.vwf",
+                    "type": "component",
+                    "properties": {
+                        "enabled": true
+                    }
+                }
+      }
+            }
+        
+    
+        this.cubelets.children.create(cubeletID, cubelet, function(child){
+            that.setCubeletID(cubeletID, child.id);
+            child.initCubeletFaces();
+        })
+    })
+
+    
+}
+
+this.addGUI = function(){
+    let half = 2;
+
+    let faces = {
+        "front":{
+            "rotation": [0,0,0],
+            "position": [0,0,half]
+        },
+        "up":{
+            "rotation": [-90,0,0],
+            "position": [0,half,0]
+        },
+        "right":{
+            "rotation": [0,90,0],
+            "position": [half,0,0]
+        },
+        "down":{
+            "rotation": [90,0,0],
+            "position": [0,-half,0]
+        },
+        "left":{
+            "rotation": [0,-90,0],
+            "position": [-half,0,0]
+        },
+        "back":{
+            "rotation": [0,180,0],
+            "position": [0,0,-half]
+        }
+    }
+
+
+
+    let guiSize = 1.2;
+    let split = 0.2;
+    let height  = 0.6
+
+    Object.keys(faces).forEach(key=>{
+        let data = faces[key];
+
+        let buttosnGUI = {}
+        let buttons = [
+            {   
+                "command": key.toUpperCase(),
+                "position":[split, 0, 0]
+            },
+            {   
+                "command": key,
+                "position":[-split, 0, 0]
+            }
+        ]
+
+        buttons.forEach((b,i) => {
+
+        let rotateButton = {
+            "extends": "proxy/aframe/aplane.vwf",
+            "properties": {
+                "displayName": b["command"] + '-button-' + i,
+                //"radius": 0.15,
+                "height": height,
+                "width": 0.4,
+                "position": b["position"],
+                "class": "clickable intersectable"
+            },
+            "methods": {
+                "intersectEventMethod": {
+                  "body": `
+                    console.log('I was intersected at point: ', point);//
+                    this.material.opacity = 0.6;
+                    this.material.color = 'green'
+                    `,
+                    "parameters": [
+                        "point"
+                      ],
+                  "type": "application/javascript"
+                },
+                "clearIntersectEventMethod": {
+                    "body": `
+                    console.log('Clear intersection');
+                    this.material.opacity = 0.3;
+                    this.material.color = 'white'
+                    `,
+                  "type": "application/javascript"
+                },
+                "mousedownAction":{
+                    "body": `
+                        this.triggerdownAction();
+                        `,
+                      "type": "application/javascript"
+                    },
+                    "mouseupAction":{
+                        "body": `
+                        this.triggerupAction();
+                            `,
+                          "type": "application/javascript"
+                        },
+                "triggerdownAction":{
+                "body": `
+                    this.material.color = 'blue';
+                    let command = this.displayName.charAt(0);
+                    this.parent.parent.parent.parent.do(command);
+                    `,
+                  "type": "application/javascript"
+                },
+                "triggerupAction":{
+                    "body": `
+                        this.material.color = 'green';
+                        `,
+                      "type": "application/javascript"
+                    }
+            },
+            "children":{
+                "cursor-listener": {
+                    "extends": "proxy/aframe/app-cursor-listener-component.vwf",
+                    "type": "component"
+                },
+                "raycaster-listener": {
+                    "extends": "proxy/aframe/app-raycaster-listener-component.vwf",
+                    "type": "component"
+                  },
+                "material": {
+                    "extends": "proxy/aframe/aMaterialComponent.vwf",
+                    "type": "component",
+                    "properties": {
+                        "transprent": true,
+                        "opacity": 0.3,
+                        "color": "white"
+                    }
+                }
+            }
+        }
+
+            buttosnGUI[i] = rotateButton
+        })
+
+        let face = //getFace(faces[key]);
+        {
+            "extends": "proxy/aframe/aentity.vwf",
+            "properties": {
+                "displayName": key,
+                "rotation": data.rotation,
+                "position": data.position,
+                "width": 1,
+                "height": 1
+            },
+            "children":{
+                "rotateButtons":{
+                    "extends": "proxy/aframe/aentity.vwf",
+                    "children": buttosnGUI
+                },
+                "label":{
+                    "extends": "proxy/aframe/atext.vwf",
+                    "properties": {
+                        "displayName": key,
+                        "color": "black",
+                        "value": key,
+                        "side": "double",
+                        "opacity": 0.6,
+                        "position": [-0.3,0,0.01]
+                    }
+                }
+                // "material": {
+                //     "extends": "proxy/aframe/aMaterialComponent.vwf",
+                //     "type": "component",
+                //     "properties": {
+                //         "side": "double",
+                //         "transprent": true,
+                //         "opacity": 0.6,
+                //         "color": "white"
+                //     }
+                // }
+            }
+        }
+
+        //let color = cubeletModel[key].color.hex;
+        //face.children.material.properties.color = color;
+        this.gui.children.create(key, face)
+    })
+
+}
+
+
+this.addTwistKey = function(key){
+    this.twistQueue.push(key);
+    this.cubeModel.twistQueue.add( key )
+}
+//cube.twist( cube.twistQueue.do() )
+
+
+this.rotateFront = function(){
+    debugger;
+    this.twistQueue.push('f');
+    this.cubeModel.twistQueue.add('f');
+    this.cubeModel.twist( this.cubeModel.twistQueue.do() );
+}
+
+this.do = function(key){
+
+    this.twistQueue = this.twistQueue.concat([key]);
+    this.twistQueueHistory = this.twistQueueHistory.concat([key]);
+    this.twistAction(key);
+    // this.cubeModel.twistQueue.add(key);
+    // this.cubeModel.twist( this.cubeModel.twistQueue.do() );
+}
+
+this.twistLoop = function(){
+    this.future(1).twistLoop();
+    let isCubeTweening = this.isCubeTweening();
+    //console.log(this.isCubeTweening());
+
+    if(!isCubeTweening){
+        this.progressQueue();
+    }
+}
+
+this.getRobot = function(){
+
+    return this.getScene()[this.robotID]
+
+}
+
+this.doButtonTriggerdownAction = function(buttonID){
+
+    let button = this.getScene().findNodeByID(buttonID);
+    let buttonName = button.displayName;
+
+    if(buttonName == "noRobotButton"){
+        console.log("SWITCH NO ROBOT!");
+        this.withRobot = this.withRobot ? false : true;
+
+       
+        if(this.withRobot){
+            button.baseColor = 'yellow';
+        } else {
+            button.baseColor = 'red';
+        }
+    }
+
+    if(buttonName == "editCubeButton"){
+
+        console.log("EDIT CUBE!");
+        this.editCube = this.editCube ? false : true;
+
+        this.editRubik();
+    }
+
+    if(buttonName == "currentCubeButton"){
+
+        console.log("MAKE CURRENT CUBE!");
+        this.getScene().currentCube = this.displayName;
+
+    }
+
+}
+
+
+this.editRubik = function(){
+
+    if(!this.gizmo && this.editCube){
+
+        let gizmoNode =
+    {
+        "extends": "proxy/aframe/gizmoComponent.vwf",
+        "type": "component",
+        "properties":
+        {
+            "mode": "rotate"
+        }
+    }
+    this.children.create("gizmo", gizmoNode);
+} 
+
+    if(this.gizmo && !this.editCube){
+        this.children.delete(this.gizmo)
+    }
+
+
+}

+ 73 - 0
public/defaults/worlds/rubik/cubeModel.vwf.json

@@ -0,0 +1,73 @@
+{
+    "extends": "proxy/aframe/aentity.vwf",
+    "properties": {
+        "cubeID": {
+            "set": "this.cubeID_set(value)",
+            "get": "return this.cubeID_get()"
+        },
+        "robotID": null,
+        "withRobot": false,
+        "editCube": false,
+        "twistQueue": [],
+        "twistQueueHistory": [],
+        "actions":null
+    },
+    "methods": {
+        "initialize": {},
+        "cubeID_set": {
+            "parameters": [
+                "value"
+            ]
+        },
+        "cubeID_get": {},
+        "initializeCubelets": {},
+        "rotateFront": {},
+        "addGUI": {},
+        "inspect":{},
+        "getCubeModel": {},
+        "getCubelets": {},
+        "getCubeModelID": {},
+        "getCubelet": {
+            "parameters": [
+                "value"
+            ]
+        },
+        "do": {
+            "parameters": [
+                "value"
+            ]
+        },
+        "twistAction": {
+            "parameters": [
+                "value"
+            ]
+        },
+        "setCubeletID": {
+            "parameters": [
+                "cubeletID",
+                "nodeID"
+            ]
+        },
+        "cubeletsRemap": {
+            "parameters": [
+                "cubeletID", "cubeCallback"
+            ]
+        },
+        "twistLoop":{},
+        "isCubeTweening":{},
+        "progressQueue":{},
+        "undo":{},
+        "getRobot":{},
+        "doButtonTriggerdownAction":{
+            "parameters": [
+                "buttonID"
+            ]
+        },
+        "editRubik":{
+            
+        }
+    },
+    "scripts": {
+        "source": "cubeModel.js"
+    }
+}

+ 273 - 0
public/defaults/worlds/rubik/cubeletModel.js

@@ -0,0 +1,273 @@
+this.initialize = function(){}
+this.getCube = function(){
+
+    return this.parent.parent
+
+}
+
+this.initCubeletFaces = function(){
+
+    let cube = this.getCube();
+    let cubeletModel = cube.getCubelet(this.cubeletID);
+
+    //let cubeModel = cube.cubeModel;
+    //let cubeletModel = cubeModel.cubelets[this.cubleteID];
+    //cubeletModel.nodeID = this.id;
+
+    let half = this.size / 2;
+
+    //MISTAKE in UP/DOWN position (in original code)
+
+    let faces = {
+        "front":{
+            "faceID": "0",
+            "rotation": [0,0,0],
+            "position": [0,0,half]
+        },
+        "up":{
+            "faceID": "1",
+            "rotation": [-90,0,0],
+            "position": [0,half,0]
+        },
+        "right":{
+            "faceID": "2",
+            "rotation": [0,90,0],
+            "position": [half,0,0]
+        },
+        "down":{
+            "faceID": "3",
+            "rotation": [90,0,0],
+            "position": [0,-half,0]
+        },
+        "left":{
+            "faceID": "4",
+            "rotation": [0,-90,0],
+            "position": [-half,0,0]
+        },
+        "back":{
+            "faceID": "5",
+            "rotation": [0,180,0],
+            "position": [0,0,-half]
+        }
+    }
+
+    Object.keys(faces).forEach(key=>{
+        let data = faces[key];
+        let face = //getFace(faces[key]);
+        {
+            "extends": "proxy/aframe/aplane.vwf",
+            "properties": {
+                "faceID": data.faceID,
+                "rotation": data.rotation,
+                "position": data.position
+            },
+            "children":{
+                "label":{
+                    "extends": "proxy/aframe/atext.vwf",
+                    "properties": {
+                        "displayName": key,
+                        "color": "black",
+                        "value": this.cubeletID,
+                        "side": "double",
+                        "position": [-0.3,0,0.01]
+                    }
+                },
+                "material": {
+                    "extends": "proxy/aframe/aMaterialComponent.vwf",
+                    "type": "component",
+                    "properties": {
+                        "side": "double",
+                        "color": cubeletModel.faces[data.faceID].color.hex
+                    }
+                }
+            }
+        }
+
+        //let color = cubeletModel[key].color.hex;
+        //face.children.material.properties.color = color;
+        this.wrapper.children.create(key, face)
+    })
+
+    
+
+}
+
+
+// this.rotateCublet = function(data){
+
+// //     this.rotateBy([0,0,90],0.5);
+// // this.parent["1"].rotateBy([0,0,90],0.5);
+// // this.parent["2"].rotateBy([0,0,90],0.5);
+// // this.parent["3"].rotateBy([0,0,90],0.5);
+// // this.parent["4"].rotateBy([0,0,90],0.5);
+
+//     this.rotation = data;
+//     let k = this.getMatrix().clone();
+//     let t = this.wrapper.getMatrix().clone();
+//     t.premultiply(k);
+//     console.log(t);
+//     let p = new THREE.Vector3();
+//     let q = new THREE.Quaternion();
+//     let s = new THREE.Vector3();
+//     t.decompose( p, q, s );
+//     let angle = (new THREE.Euler()).setFromQuaternion(q, 'XYZ');
+//     let rotation = (new THREE.Vector3(THREE.Math.radToDeg(angle.x),
+//                                 THREE.Math.radToDeg(angle.y), THREE.Math.radToDeg(angle.z)));
+//     console.log(p, rotation, s);
+
+//     this.wrapper.position = p;
+//     this.wrapper.rotation = rotation;
+
+//     this.rotation = [0,0,0];
+    
+//     }
+
+    this.rotateCubelet = function(rotation, speed, cubeCallback) {
+
+        if(cubeCallback){
+            this.rotateBy(rotation, speed, 'cubeletOnStopRotation:[' + this.cubeletID + ',"' + cubeCallback +'"]')
+        } else {
+            this.rotateBy(rotation, speed, "cubeletOnStopRotation:[" + this.cubeletID + "]" )
+        }
+        
+    }
+
+    this.cubeletOnStopRotation = function(cubeletID, cubeCallback){
+
+        let cube = this.getCube();
+        cube.cubeletsRemap(cubeletID, cubeCallback);
+        
+        if(cubeCallback){
+            if(cube.robotID && cube.withRobot){
+                let robot = cube.getRobot();
+                robot.rotateFace(cubeCallback);
+            }
+        }
+        
+
+    }
+
+    // this.cubeletOnStopRotation = function(cubeletID, cubeCallback){
+    //     debugger;
+    //     let threshold = 0.001
+    //     //  Here's some complexity.
+	// 			//  We need to support partial rotations of arbitrary degrees
+	// 			//  yet ensure our internal model is always in a valid state.
+	// 			//  This means only remapping the Cubelet when it makes sense
+	// 			//  and also remapping the Cube if this Cubelet is allowed to do so.
+    //             let myCube = this.getCube();
+    //             let cube = myCube.cubeModel;
+    //             let cubelet = cube.cubelets[cubeletID];
+                
+
+	// 			var 
+	// 			xRemaps = cubelet.x.divide( 90 ).round()
+	// 				.subtract( cubelet.xPrevious.divide( 90 ).round() )
+	// 				.absolute(),
+	// 			yRemaps = cubelet.y.divide( 90 ).round()
+	// 				.subtract( cubelet.yPrevious.divide( 90 ).round() )
+	// 				.absolute(),
+	// 			zRemaps = cubelet.z.divide( 90 ).round()
+	// 				.subtract( cubelet.zPrevious.divide( 90 ).round() )
+	// 				.absolute()
+
+	// 			if( Cube.verbosity >= 0.9 ){
+
+	// 				console.log( 'Cublet #'+ ( cubelet.id < 10 ? '0'+ cubelet.id : cubelet.id ), 
+	// 					' |  xRemaps:', xRemaps, ' yRemaps:', yRemaps, ' zRemaps:', zRemaps,
+	// 					' |  xPrev:', cubelet.xPrevious, ' x:', cubelet.x,
+	// 					' |  yPrev:', cubelet.yPrevious, ' y:', cubelet.y,
+	// 					' |  zPrev:', cubelet.zPrevious, ' z:', cubelet.z )
+	// 			}
+
+
+	// 			if( xRemaps ){
+					
+	// 				while( xRemaps -- ){
+
+	// 					if( cubelet.x < cubelet.xPrevious ) cubelet.faces = [ cubelet.up, cubelet.back, cubelet.right, cubelet.front, cubelet.left, cubelet.down ]
+	// 					else cubelet.faces = [ cubelet.down, cubelet.front, cubelet.right, cubelet.back, cubelet.left, cubelet.up ]
+	// 					cubelet.map()
+	// 					if( cubeCallback !== undefined ){
+
+    //                         let swapMap = Cube.swapMaps[cubeCallback];
+    //                         let swap = cubelet.cube.cubelets.slice();
+    //                         swapMap.forEach(el=>{
+    //                             cube.cubelets[el[0]] = swap[el[1]]
+    //                         })
+	// 						//cubeCallback( cubelet.cube.cubelets.slice())
+	// 						cubelet.cube.map()
+	// 					}
+	// 				}
+	// 				cubelet.xPrevious = cubelet.x
+	// 			}
+	// 			if( cubelet.x.modulo( 90 ).absolute() < threshold ){
+
+	// 				cubelet.x = 0
+	// 				cubelet.xPrevious = cubelet.x
+	// 				cubelet.isEngagedX = false
+	// 			}
+				
+
+	// 			if( yRemaps ){
+					
+	// 				while( yRemaps -- ){
+
+	// 					if( cubelet.y < cubelet.yPrevious ) cubelet.faces = [ cubelet.left, cubelet.up, cubelet.front, cubelet.down, cubelet.back, cubelet.right ]
+	// 					else cubelet.faces = [ cubelet.right, cubelet.up, cubelet.back, cubelet.down, cubelet.front, cubelet.left ]
+	// 					cubelet.map()
+	// 					if( cubeCallback !== undefined ){
+
+    //                         let swapMap = Cube.swapMaps[cubeCallback];
+    //                         let swap = cubelet.cube.cubelets.slice();
+    //                         swapMap.forEach(el=>{
+    //                             cube.cubelets[el[0]] = swap[el[1]]
+    //                         })
+	// 						//cubeCallback( cubelet.cube.cubelets.slice())
+	// 						cubelet.cube.map()
+	// 					}
+	// 				}
+	// 				cubelet.yPrevious = cubelet.y
+	// 			}
+	// 			if( cubelet.y.modulo( 90 ).absolute() < threshold ){
+
+	// 				cubelet.y = 0
+	// 				cubelet.yPrevious = cubelet.y
+	// 				cubelet.isEngagedY = false
+	// 			}
+
+
+	// 			if( zRemaps ){
+					
+	// 				while( zRemaps -- ){
+
+	// 					if( cubelet.z < cubelet.zPrevious ) cubelet.faces = [ cubelet.front, cubelet.right, cubelet.down, cubelet.left, cubelet.up, cubelet.back ]
+	// 					else cubelet.faces = [ cubelet.front, cubelet.left, cubelet.up, cubelet.right, cubelet.down, cubelet.back ]
+	// 					cubelet.map()
+	// 					if( cubeCallback !== undefined ){
+    //                 //debugger;
+    //                         let swapMap = Cube.swapMaps[cubeCallback];
+    //                         let swap = cubelet.cube.cubelets.slice();
+    //                         swapMap.forEach(el=>{
+    //                             cube.cubelets[el[0]] = swap[el[1]]
+    //                         })
+	// 						//cubeCallback( cubelet.cube.cubelets.slice())
+	// 						cubelet.cube.map()
+	// 					}
+	// 				}
+	// 				cubelet.zPrevious = cubelet.z
+	// 			}
+	// 			if( cubelet.z.modulo( 90 ).absolute() < threshold ){
+
+	// 				cubelet.z = 0
+	// 				cubelet.zPrevious = cubelet.z
+	// 				cubelet.isEngagedZ = false
+	// 			}
+
+
+	// 			//  Phew! Now we can turn off the tweening flag.
+
+	// 			cubelet.isTweening = false
+
+
+    // }

+ 33 - 0
public/defaults/worlds/rubik/cubeletModel.vwf.json

@@ -0,0 +1,33 @@
+{
+    "extends": "proxy/aframe/aentity.vwf",
+    "properties": {
+        "cubeletID": null,
+        "cubeNodeID": null,
+        "cubeID": null,
+        "size": null,
+        "address": null
+    },
+    "methods": {
+        "initialize":{},
+        "initCubeletFaces":{},
+        "getCube":{},
+        "rotateCubelet":{
+            "parameters":[
+                "rotation", "speed", "cubeCallback"
+            ]
+        },
+        "cubeletOnStopRotation":{
+            "parameters":[
+                "cubelet", "cubeCallback"
+            ]
+        },
+        "setNodeID":{
+            "parameters":[
+                "cubeletID"
+            ]
+        }
+    },
+    "scripts": {
+        "source": "cubeletModel.js"
+    }
+}

+ 18 - 0
public/defaults/worlds/rubik/index.vwf.config.json

@@ -0,0 +1,18 @@
+{
+    "info":{
+      "title": "Rubik App"
+    },
+    "model": {
+      "/drivers/model/aframe": {},
+      "/drivers/model/aframeComponent": null,
+      "/drivers/model/rubik/rubik": null,
+      "/drivers/model/lego-boost": null
+    },
+    "view": {
+      "/drivers/view/aframe": null,
+      "/drivers/view/aframeComponent": null,
+      "/drivers/view/editor": null,
+      "/drivers/view/lego-boost": null,
+      "/drivers/view/webrtc": null
+    }
+  }

+ 173 - 0
public/defaults/worlds/rubik/index.vwf.json

@@ -0,0 +1,173 @@
+{
+  "extends": "proxy/aframe/ascene.vwf",
+  "properties":{
+    "sampleCube": false,
+    "currentCube": null,
+    "currentCommand": null
+  },
+  "methods":{
+    "initialize":{},
+    "createRubik":{
+      "parameters":[
+        "id",
+        "robot"
+      ]
+    },
+    "doOnRubik":{
+      "parameters":[
+        "command"
+      ]
+    }
+  },
+  "children": {
+    "floorTexture": {
+    "extends": "proxy/aframe/a-asset-image-item.vwf",
+    "properties": {
+        "itemID": "bg2",
+        "itemSrc": "/defaults/assets/checker.jpg"
+    }
+  },
+  "skyTexture": {
+    "extends": "proxy/aframe/a-asset-image-item.vwf",
+    "properties": {
+        "itemID": "sky",
+        "itemSrc": "/defaults/assets/skyes/sky3.jpg"
+    }
+  },
+  "screen": {
+    "extends": "proxy/aframe/aplane.vwf",
+    "properties": {
+      "height": "7",
+      "width": "9",
+      "rotation": [
+        10,
+       -16,
+        2
+      ],
+      "position":[10,5.5,-12]
+    },
+    "children": {
+      "material": {
+        "extends": "proxy/aframe/aMaterialComponent.vwf",
+        "properties": {
+          "wireframe": false,
+          "src": "#",
+          "color": "black"
+        }
+      }
+    }
+  },  
+  "screen2": {
+    "extends": "proxy/aframe/aplane.vwf",
+    "properties": {
+      "height": "8",
+      "width": "10",
+      "rotation": [
+        10,
+       -16,
+        2
+      ],
+      "position":[10,5.5,-12.01]
+    },
+    "children": {
+      "material": {
+        "extends": "proxy/aframe/aMaterialComponent.vwf",
+        "properties": {
+          "wireframe": false,
+          "color": "#b56e3f"
+        }
+      }
+    }
+  },
+    "groundPlane": {
+      "extends": "proxy/aframe/aplane.vwf",
+      "properties": {
+        "height": "50",
+        "width": "50",
+        "rotation": [
+          -90,
+          0,
+          0
+        ]
+      },
+      "children": {
+        "material": {
+          "extends": "proxy/aframe/aMaterialComponent.vwf",
+          "properties": {
+            "wireframe": false,
+            "src": "#bg2",
+            "repeat": "10 10"
+          }
+        }
+      }
+    },
+    "myLight": {
+      "extends": "proxy/aframe/alight.vwf",
+      "properties": {
+        "type": "ambient",
+        "color": "#dddddd"
+      }
+    },
+    "myLight2": {
+      "extends": "proxy/aframe/alight.vwf",
+      "properties": {
+        "type": "directional",
+        "color": "#FFF",
+        "intensity": 0.6,
+        "position": [0,10,3]
+      }
+    },
+    "sky": {
+      "extends": "proxy/aframe/asky.vwf",
+      "children": {
+        "material": {
+          "extends": "proxy/aframe/aMaterialComponent.vwf",
+          "properties": {
+            "color": "#ECECEC",
+            "src": "#sky",
+            "fog": false,
+            "side": "back"
+          }
+        }
+      }
+    },
+    "spaceText": {
+      "extends": "proxy/aframe/atext.vwf",
+      "properties": {
+        "value": "Rubik's Cube app",
+        "color": "white",
+        "position": [-4,5.5,-7],
+        "scale": [3,3,3]
+      }
+    },
+    "box": {
+      "extends": "proxy/aframe/abox.vwf",
+      "properties": {
+        "position": [
+          0,
+          0,
+          -3
+        ],
+        "rotation": [
+          0,
+          0,
+          0
+        ],
+        "depth": "3",
+        "height": "0.2",
+        "width": "3"
+      },
+      "children": {
+        "material": {
+          "extends": "proxy/aframe/aMaterialComponent.vwf",
+          "properties": {
+            "color": "#0c2913"
+          }
+        }
+      }
+    }
+  },
+  "scripts":{
+    "source": "rubik.js"
+  }
+}

+ 14 - 0
public/defaults/worlds/rubik/info.json

@@ -0,0 +1,14 @@
+{
+    "info": {
+        "en": {
+            "title": "Rubik app",
+            "imgUrl": "/defaults/worlds/rubik/webimg.jpg",
+            "text": "Rubik"
+        },
+        "ru": {
+            "title": "Rubik",
+            "imgUrl": "/defaults/worlds/rubik/webimg.jpg",
+            "text": "Rubik"
+        }
+    }
+}

+ 92 - 0
public/defaults/worlds/rubik/robot.js

@@ -0,0 +1,92 @@
+this.initialize = function(){}
+
+this.initRobot = function(){
+
+    let nodes = {
+        'left': {
+            position: [-1,0,0]
+        },
+        'right': {
+            position: [1,0,0]
+        },
+        'front': {
+            position: [0,0,1]
+        },
+        'back': {
+            position: [0,0,-1]
+        }
+
+    };
+
+    Object.keys(nodes).forEach(el=>{
+
+        let legoBoostNode = {
+            "extends": "proxy/objects/legoboost.vwf",
+            "properties": {
+                "boostID": el,
+                "position": nodes[el].position,
+                "displayName": el,
+                "tracking": false
+            }
+        }
+        this.children.create(el, legoBoostNode, function( child ) {
+            child.createVisual();
+            child.trackLego();
+        })
+    
+    })
+
+
+}
+
+this.rotateFace = function(faceID){
+
+    let direction = (faceID == faceID.toLowerCase()) ? -1 : 1;
+
+    let angle = 90;
+    //let dutyCycle = 80  * direction;
+
+    let robotMap = {
+
+        'l': {
+            'robot': 'left',
+            'motor': 'B',
+            'dutyCycle': 70,
+            'direction': -1*direction
+        },
+        'd': {
+            'robot': 'left',
+            'motor': 'C',
+            'dutyCycle': 100,
+            'direction': -1*direction
+        },
+        'b': {
+            'robot': 'back',
+            'motor': 'A',
+            'dutyCycle': 70,
+            'direction': direction
+        },
+        'r': {
+            'robot': 'right',
+            'motor': 'A',
+            'dutyCycle': 70,
+            'direction': direction
+        },
+        'u': {
+            'robot': 'back',
+            'motor': 'C',
+            'dutyCycle': 100,
+            'direction': direction
+        },
+        'f': {
+            'robot': 'front',
+            'motor': 'A',
+            'dutyCycle': 70,
+            'direction': direction
+        }
+    } 
+
+    let robot = robotMap[faceID.toLowerCase()];
+    this[robot.robot].setMotorAngle(robot.motor, angle, robot.dutyCycle*robot.direction, 'sync');
+
+}

+ 19 - 0
public/defaults/worlds/rubik/robot.vwf.json

@@ -0,0 +1,19 @@
+{
+    "extends": "proxy/aframe/aentity.vwf",
+    "properties": {
+        "cubeID": null
+    },
+    "methods": {
+        "initialize":{},
+        "initRobot":{},
+        "getCube":{},
+        "rotateFace":{
+            "parameters":[
+                "faceID"
+            ]
+        }
+    },
+    "scripts": {
+        "source": "robot.js"
+    }
+}

+ 170 - 0
public/defaults/worlds/rubik/rubik.js

@@ -0,0 +1,170 @@
+this.initialize = function () {
+    //console.log(vwf_view.doc.cube.calc());
+
+    // if(Object.getPrototypeOf(this).id.includes('ascene.vwf')){
+    //     debugger;
+    //     console.log("Initialize of Scene...", this.id);
+    //     let rubikID = "rubik-" + this.randomHash();
+    //     this.createRubik(rubikID);
+    // } else {
+    //     console.log("Initialize proto Scene..", this.id);
+    // }
+}
+
+this.createRubik = function (id, robot) {
+
+    this.currentCube = id;
+
+    let cubeContainer = {
+        "extends": "proxy/aframe/aentity.vwf",
+        "properties": {
+            "displayName": 'container-' + id,
+            "target": id,
+            "position": [0,2.7,-7.5]
+        },
+        "methods":{
+            "setupGUI":{
+            "body": `
+            this.gui.noRobotButton.init();
+            this.gui.editCubeButton.init();
+            this.gui.currentCubeButton.init();
+            `,
+          "type": "application/javascript"
+            }
+        },
+        "children": {
+            "gui": {
+                "extends": "proxy/aframe/aentity.vwf",
+                "properties": {
+                    "position": [3,0,0]
+
+                },
+                "children": {
+                    "noRobotButton":{
+                           "extends": "proxy/objects/gui/button.vwf",
+                            "properties": {
+                                "target": id,
+                                "displayName": "noRobotButton",
+                                "position": [-6, -1, 1],
+                                "clickColor": "blue",
+                                "baseColor": "red",
+                                "height": 0.5,
+                                "width": 0.6
+                            }
+                    },
+                    "editCubeButton":{
+                        "extends": "proxy/objects/gui/button.vwf",
+                         "properties": {
+                             "target": id,
+                             "displayName": "editCubeButton",
+                             "position": [-6, 0, 1],
+                             "clickColor": "blue",
+                             "baseColor": "green",
+                             "height": 0.5,
+                             "width": 0.6
+                         }
+                 },
+                 "currentCubeButton":{
+                    "extends": "proxy/objects/gui/button.vwf",
+                     "properties": {
+                         "target": id,
+                         "displayName": "currentCubeButton",
+                         "position": [-6, 1, 1],
+                         "clickColor": "blue",
+                         "baseColor": "white",
+                         "height": 0.8,
+                         "width": 0.8
+                     }
+             }
+                    // "material": {
+                    //     "extends": "proxy/aframe/aMaterialComponent.vwf",
+                    //     "type": "component",
+                    //     "properties": {
+                    //         "color": "white",
+                    //         "transparent": true,
+                    //         "opacity": 0.5
+                    //     }
+                    // }
+                }
+            }
+        }
+    }
+
+    
+
+    let cube = {
+        "extends": "cubeModel.vwf",
+        "properties": {
+            "cubeID": id,
+            "displayName": id,
+            "rotation": [35, -35, 0],
+            "twistQueue": [],
+            "twistQueueHistory": []
+        },
+        "children": {
+            "cubelets": {
+                "extends": "proxy/aframe/aentity.vwf"
+            },
+            "gui": {
+                "extends": "proxy/aframe/aentity.vwf"
+            },
+            "interpolation":
+            {
+                "extends": "proxy/aframe/interpolation-component.vwf",
+                "type": "component",
+                "properties": {
+                    "enabled": true
+                }
+            }
+        }
+    }
+
+    this.children.create('container-' + id, cubeContainer,function (box){
+
+
+        box.children.create(id, cube, function (child) {   
+        child.initializeCubelets();
+        child.addGUI();
+
+        if(robot){
+            child.robotID = 'robot-' + id;
+        }
+
+        box.setupGUI();
+
+    });
+
+    // let frame3D = {
+    //     "extends": "proxy/objects/gui/frame3D.vwf",
+    //     "properties": {
+    //         "contentsID": id,
+    //         "position": [0, -2.5, 3]
+    //     }
+    // }
+    //    box.children.create(id + '-frame3D', frame3D)
+
+
+} )
+
+if (robot) {
+
+    let nodeName = 'robot-' + id;
+    let cube = {
+        "extends": "robot.vwf",
+        "properties": {
+            "cubeID": id,
+            "position": [0,0.5,-3],
+            "visible": false
+        }
+    }
+
+    this.children.create(nodeName, cube, function (child) {
+        child.initRobot();
+    });
+}
+}
+
+this.doOnRubik = function(command){
+    let rubik = this.findNode(this.currentCube);
+    rubik.do(command);
+}

BIN
public/defaults/worlds/rubik/webimg.jpg


+ 10 - 0
public/drivers/model/aframe.js

@@ -546,6 +546,16 @@ class AFrameModel extends Fabric {
                         return localPoint
 
                     }
+                    if (methodName == 'applyMatrix') {
+                        let fromNode = this.state.nodes[methodParameters[0]];
+                        let matrix = fromNode.aframeObj.object3D.matrix;
+                        node.aframeObj.object3D.applyMatrix(matrix);
+                    }
+
+                    if (methodName == 'getMatrix') {
+                        node.aframeObj.object3D.updateMatrix( true );
+                        return node.aframeObj.object3D.matrix
+                    }
                     //.worldToLocal
 
                 }

+ 5 - 5
public/drivers/model/aframe/addon/TransformControls.js

@@ -225,8 +225,8 @@ THREE.TransformControls = function ( camera, domElement ) {
 
 			this.object.matrixWorld.decompose( worldPosition, worldQuaternion, worldScale );
 
-			parentQuaternionInv.copy( parentQuaternion ).inverse();
-			worldQuaternionInv.copy( worldQuaternion ).inverse();
+			parentQuaternionInv.copy( parentQuaternion ).invert();
+			worldQuaternionInv.copy( worldQuaternion ).invert();
 
 		}
 
@@ -383,7 +383,7 @@ THREE.TransformControls = function ( camera, domElement ) {
 
 				if ( space === 'local' ) {
 
-					object.position.applyQuaternion( _tempQuaternion.copy( quaternionStart ).inverse() );
+					object.position.applyQuaternion( _tempQuaternion.copy( quaternionStart ).invert() );
 
 					if ( axis.search( 'X' ) !== - 1 ) {
 
@@ -1297,7 +1297,7 @@ THREE.TransformControlsGizmo = function () {
 					handle.position.copy( this.worldPositionStart );
 					handle.quaternion.copy( this.worldQuaternionStart );
 					tempVector.set( 1e-10, 1e-10, 1e-10 ).add( this.worldPositionStart ).sub( this.worldPosition ).multiplyScalar( - 1 );
-					tempVector.applyQuaternion( this.worldQuaternionStart.clone().inverse() );
+					tempVector.applyQuaternion( this.worldQuaternionStart.clone().invert() );
 					handle.scale.copy( tempVector );
 					handle.visible = this.dragging;
 
@@ -1480,7 +1480,7 @@ THREE.TransformControlsGizmo = function () {
 				// Align handles to current local or world rotation
 
 				tempQuaternion2.copy( quaternion );
-				alignVector.copy( this.eye ).applyQuaternion( tempQuaternion.copy( quaternion ).inverse() );
+				alignVector.copy( this.eye ).applyQuaternion( tempQuaternion.copy( quaternion ).invert() );
 
 				if ( handle.name.search( "E" ) !== - 1 ) {
 

+ 136 - 37
public/drivers/model/aframe/addon/aframe-components.js

@@ -40,15 +40,53 @@ AFRAME.registerComponent('desktop-controls', {
     let controllerID = 'mouse-' + vwf_view.kernel.moniker();
 
     this.domElement.addEventListener('mousedown', function (e) {
-        if (e.button == 1) {
-            vwf_view.kernel.callMethod(controllerID, "triggerdown", []);
-          }
+        
+        if(!this.xrcontroller){
+            this.xrcontroller = document.querySelector('#'+ controllerID);
+        }
+
+        if(this.xrcontroller) {
+
+            let intersection = this.xrcontroller.components.raycaster.intersections[0];
+            let point = intersection ? intersection.point : null;
+            let elID = intersection ? intersection.object.el.id : null;
+            if(point) console.log('Point to: ', point);
+    
+            if (e.button == 1) {
+                vwf_view.kernel.callMethod(controllerID, "triggerdown", [point, elID]);
+              }
+    
+              if (e.button == 0) {
+                vwf_view.kernel.callMethod(controllerID, "mousedown", [point, elID]);
+              }  
+
+        }
+ 
             
     });
 
     this.domElement.addEventListener('mouseup', function (e) {
+
+        if(!this.xrcontroller){
+            this.xrcontroller = document.querySelector('#'+ controllerID);
+        }
+
+        if(this.xrcontroller) {
+
+            let intersection = this.xrcontroller.components.raycaster.intersections[0];
+            let point = intersection ? intersection.point : null;
+            let elID = intersection ? intersection.object.el.id : null;
+            if(point) console.log('Point to: ', point);
+    
+
+
         if (e.button == 1) {
-            vwf_view.kernel.callMethod(controllerID, "triggerup", []);
+            vwf_view.kernel.callMethod(controllerID, "triggerup", [point, elID]);
+        }
+        if (e.button == 0) {
+            vwf_view.kernel.callMethod(controllerID, "mouseup", [point, elID]);
+          }   
+
         }
     });
 
@@ -67,6 +105,29 @@ AFRAME.registerComponent('desktop-controls', {
             this.raycaster.setFromCamera( this.mouse, this.camera );
             this.handDirection.copy(this.raycaster.ray.direction);
             this.el.object3D.lookAt(this.handDirection.negate());
+
+            if(!self.xrcontroller){
+                self.xrcontroller = document.querySelector('#'+ controllerID);
+            }
+
+            if(self.xrcontroller){
+                let intersection = this.xrcontroller.components.raycaster.intersections[0];
+                let point = intersection ? intersection.point : null;
+                let elID = intersection ? intersection.object.el.id : null;
+                if(point) {
+                //console.log('Point to: ', point, ' intersect ', elID);
+                self.intersectionData = {
+                    point: point,
+                    elID: elID
+                } 
+            }
+            else {
+                self.intersectionData = null;
+                
+            }
+
+
+            }
             
          }
             
@@ -439,33 +500,36 @@ AFRAME.registerComponent('cursor-listener', {
             //vwf_view.kernel.fireEvent(evt.detail.target.id, "clickEvent")
         });
 
-        this.el.addEventListener('mousedown', function (evt) {
-            console.log('mousedown at: ', evt.detail.intersection.point);
-            if (evt.detail.cursorEl.id.includes(vwf_view.kernel.moniker())) {
-
-                let point = evt.detail.intersection.point;
-                // let locPoint = new THREE.Vector3();
-                // locPoint.copy(evt.detail.intersection.point);
-                // self.el.object3D.parent.worldToLocal(locPoint);
-                // let point = AFRAME.utils.coordinates.stringify(locPoint);
-                vwf_view.kernel.callMethod('mouse-'+vwf_view.kernel.moniker(), "showHandSelection", [point]);
-                vwf_view.kernel.fireEvent(evt.detail.intersection.object.el.id, "mousedownEvent", [point]);
-            }
-
-        })
+        // this.el.addEventListener('mousedown', function (evt) {
+        //     console.log('mousedown at: ', evt.detail.intersection.point);
+        //     if (evt.detail.cursorEl.id.includes(vwf_view.kernel.moniker())) {
+
+        //         let point = evt.detail.intersection.point;
+        //         // let locPoint = new THREE.Vector3();
+        //         // locPoint.copy(evt.detail.intersection.point);
+        //         // self.el.object3D.parent.worldToLocal(locPoint);
+        //         // let point = AFRAME.utils.coordinates.stringify(locPoint);
+        //         //vwf_view.kernel.callMethod('mouse-'+vwf_view.kernel.moniker(), "showHandSelection", [point]);
+        //         vwf_view.kernel.fireEvent(evt.detail.intersection.object.el.id, "mousedownEvent", [point]);
+                
 
-        this.el.addEventListener('mouseup', function (evt) {
-            let intersection =  evt.detail.intersection;
-            if(intersection)
-            {
-                console.log('mouseup at: ', evt.detail.intersection.point);
-                vwf_view.kernel.fireEvent(evt.detail.intersection.object.el.id, "mouseupEvent", [evt.detail.intersection.point]);
-            } else {
-                console.log('mouseup');
-            }
-            vwf_view.kernel.callMethod('mouse-'+vwf_view.kernel.moniker(), "resetHandSelection", []);
+        //     }
+
+        // })
+
+        // this.el.addEventListener('mouseup', function (evt) {
+        //     let intersection =  evt.detail.intersection;
+        //     if(intersection)
+        //     {
+        //         console.log('mouseup at: ', evt.detail.intersection.point);
+        //         vwf_view.kernel.fireEvent(evt.detail.intersection.object.el.id, "mouseupEvent", [evt.detail.intersection.point]);
+        //     } else {
+        //         console.log('mouseup');
+        //         //vwf_view.kernel.fireEvent(evt.detail.intersection.object.el.id, "mouseupEvent", []);
+        //     }
+        //     //vwf_view.kernel.callMethod('mouse-'+vwf_view.kernel.moniker(), "resetHandSelection", []);
              
-        })
+        // })
 
     }
 });
@@ -510,8 +574,9 @@ AFRAME.registerComponent('raycaster-listener', {
 
                 } else {
                     let point = evt.detail.getIntersection(evt.target).point;
-                    console.log('I was intersected at: ', evt.target, ' point: ', point);//evt.detail.getIntersection().point);
-                    vwf.callMethod(evt.target.id, "intersectEventMethod", [point]);
+                    //console.log('I was intersected at: ', evt.target, ' point: ', point);//evt.detail.getIntersection().point);
+                    vwf_view.kernel.fireEvent(evt.target.id, "intersectEvent", [point]);
+                    //vwf.callMethod(evt.target.id, "intersectEventMethod", [point]);
                 }
 
                 self.casters[evt.target.id] = evt.target;
@@ -528,9 +593,10 @@ AFRAME.registerComponent('raycaster-listener', {
 
             } else {
                 if (self.intersected) {
-                    console.log('Clear intersection');
+                    //console.log('Clear intersection');
                     if (Object.entries(self.casters).length == 1 && (self.casters[evt.target.id] !== undefined)) {
-                        vwf.callMethod(evt.target.id, "clearIntersectEventMethod", [])
+                        vwf_view.kernel.fireEvent(evt.target.id, "clearIntersectEvent")
+                        //vwf.callMethod(evt.target.id, "clearIntersectEventMethod", [])
                     }
                     delete self.casters[evt.target.id]
                 } else { }
@@ -732,11 +798,22 @@ AFRAME.registerComponent('gearvrcontrol', {
         var controllerID = 'gearvr-' + vwf_view.kernel.moniker();
 
         this.el.addEventListener('triggerdown', function (event) {
-            vwf_view.kernel.callMethod(controllerID, "triggerdown", []);
+
+            if(!self.xrcontroller){
+                self.xrcontroller = document.querySelector('#'+ self.controllerID);
+            }
+    
+    
+            let intersection = self.xrcontroller.components.raycaster.intersections[0];
+            let point = intersection ? intersection.point : null;
+            let elID = intersection ? intersection.object.el.id : null;
+            if(point) console.log('Point to: ', point);
+
+            vwf_view.kernel.callMethod(controllerID, "triggerdown", [point, elID]);
         });
 
         this.el.addEventListener('triggerup', function (event) {
-            vwf_view.kernel.callMethod(controllerID, "triggerup", []);
+            vwf_view.kernel.callMethod(controllerID, "triggerup", [point, elID]);
         });
 
          //X-buttorn Pressed 
@@ -785,11 +862,33 @@ AFRAME.registerComponent('xrcontroller', {
         this.controllerID = 'xrcontroller-' + this.hand + '-' + vwf_view.kernel.moniker();
 
         this.el.addEventListener('triggerdown', function (event) { //pointdown 'triggerdown'
-            vwf_view.kernel.callMethod(self.controllerID, "triggerdown", []);
+
+        if(!self.xrcontroller){
+            self.xrcontroller = document.querySelector('#'+ self.controllerID);
+        }
+
+
+        let intersection = self.xrcontroller.components.raycaster.intersections[0];
+        let point = intersection ? intersection.point : null;
+        let elID = intersection ? intersection.object.el.id : null;
+        if(point) console.log('Point to: ', point);
+
+            vwf_view.kernel.callMethod(self.controllerID, "triggerdown", [point, elID]);
             //this.emit('teleportstart');
         });
         this.el.addEventListener('triggerup', function (event) { //pointup 'triggerup'
-            vwf_view.kernel.callMethod(self.controllerID, "triggerup", []);
+
+        if(!self.xrcontroller){
+            self.xrcontroller = document.querySelector('#'+ self.controllerID);
+        }
+
+
+        let intersection = self.xrcontroller.components.raycaster.intersections[0];
+        let point = intersection ? intersection.point : null;
+        let elID = intersection ? intersection.object.el.id : null;
+        if(point) console.log('Point to: ', point);
+
+            vwf_view.kernel.callMethod(self.controllerID, "triggerup", [point, elID]);
             //this.emit('teleportend');
         });
 

File diff suppressed because it is too large
+ 1159 - 1860
public/drivers/model/aframe/aframe-master.js


File diff suppressed because it is too large
+ 2 - 0
public/drivers/model/aframe/aframe-master.js.map


File diff suppressed because it is too large
+ 0 - 0
public/drivers/model/aframe/aframe-master.min.js


File diff suppressed because it is too large
+ 0 - 0
public/drivers/model/aframe/aframe-master.min.js.map


+ 14 - 1
public/drivers/model/aframeComponent.js

@@ -1126,6 +1126,18 @@ class AFrameComponentModel extends Fabric {
                     let aframeObject = node.aframeObj;
                     let parentNodeAF = aframeObject.el;
 
+                    if(methodName == "getIntersectedElement") {
+
+                        let comp = parentNodeAF.components['raycaster'];
+                        if (comp.intersectedEls.length>0){
+                            //console.log(comp.intersectedEls);
+                        
+                        let intersecedObjID = comp.intersectedEls[0].id
+                        return intersecedObjID
+                        }
+                        return undefined
+                    }
+
                     if(methodName == "getIntersectionPoint") {
                         //let nodes = vwf.models["/drivers/model/aframe"].model.state.nodes;
                         let comp = parentNodeAF.components['raycaster'];
@@ -1134,9 +1146,10 @@ class AFrameComponentModel extends Fabric {
                             //console.log(comp.intersectedEls);
                         
                         let intersecedObj = comp.intersectedEls[0]; //objID ? nodes[objID] : nodes[comp.intersectedEls[0].id];
+                        //comp.checkIntersections();
                         let intersection = comp.getIntersection(intersecedObj);
                         if(intersection)
-                            return intersection.point
+                            return {id: intersecedObj.id, point: intersection.point}
 
                         }
                         return undefined

BIN
public/drivers/model/rubik/assets/back.png


BIN
public/drivers/model/rubik/assets/front.png


BIN
public/drivers/model/rubik/assets/left.png


BIN
public/drivers/model/rubik/assets/old/back.png


BIN
public/drivers/model/rubik/assets/old/front.png


BIN
public/drivers/model/rubik/assets/old/left.png


BIN
public/drivers/model/rubik/assets/old/right.png


BIN
public/drivers/model/rubik/assets/right.png


+ 103 - 0
public/drivers/model/rubik/lib/colors.js

@@ -0,0 +1,103 @@
+/*
+
+
+	COLORS
+
+	Here's a little bootstrapping to create our global Color constants.
+	At first it seemed like overkill, but then as the solvers and inspectors
+	moved forward having these objects available became highly desirable.
+	Sure, ES5 doesn't really have constants but the all-caps alerts you
+	to the fact that them thar variables ought not to be messed with.
+
+
+*/
+
+
+
+
+
+
+
+
+
+
+
+
+export function Color( name, initial, hex, styleF, styleB ){
+
+	this.name    = name
+	this.initial = initial
+	this.hex     = hex
+	this.styleF  = styleF
+	this.styleB  = styleB
+}
+
+
+//  Global constants to describe sticker colors.
+
+
+globalThis.W = globalThis.WHITE = new Color(
+
+	'white',
+	'W',
+	'#FFF',
+	'font-weight: bold; color: #888',
+	'background-color: #F3F3F3; color: rgba( 0, 0, 0, 0.5 )'
+)
+
+globalThis.O = globalThis.ORANGE = new Color(
+
+	'orange',
+	'O',
+	'#F60',
+	'font-weight: bold; color: #F60',
+	'background-color: #F60; color: rgba( 255, 255, 255, 0.9 )'
+)
+
+globalThis.B = globalThis.BLUE = new Color(
+
+	'blue',
+	'B',
+	'#00D',
+	'font-weight: bold; color: #00D',
+	'background-color: #00D; color: rgba( 255, 255, 255, 0.9 )'
+)
+
+globalThis.R = globalThis.RED = new Color(
+
+	'red',
+	'R',
+	'#F00',
+	'font-weight: bold; color: #F00',
+	'background-color: #F00; color: rgba( 255, 255, 255, 0.9 )'
+)
+
+globalThis.G = globalThis.GREEN = new Color(
+
+	'green',
+	'G',
+	'#0A0',
+	'font-weight: bold; color: #0A0',
+	'background-color: #0A0; color: rgba( 255, 255, 255, 0.9 )'
+)
+
+globalThis.Y = globalThis.YELLOW = new Color(
+
+	'yellow',
+	'Y',
+	'#FE0',
+	'font-weight: bold; color: #ED0',
+	'background-color: #FE0; color: rgba( 0, 0, 0, 0.5 )'
+)
+
+globalThis.COLORLESS = new Color(
+
+	'NA',
+	'X',
+	'#DDD',
+	'color: #EEE',
+	'color: #DDD'
+)
+
+
+

+ 1077 - 0
public/drivers/model/rubik/lib/cubelets.js

@@ -0,0 +1,1077 @@
+/*
+
+
+	CUBELETS
+
+	Faces are mapped in a clockwise spiral from Front to Back:
+
+
+                  Back
+                   5
+              -----------
+            /    Up     /|
+           /     1     / |
+           -----------  Right
+          |           |  2
+    Left  |   Front   |  .
+     4    |     0     | /
+          |           |/
+           -----------
+               Down
+                3
+
+	
+	The faces[] Array is mapped to names for convenience:
+
+	  this.faces[ 0 ] === this.front
+	  this.faces[ 1 ] === this.up
+	  this.faces[ 2 ] === this.right
+	  this.faces[ 3 ] === this.down
+	  this.faces[ 4 ] === this.left
+	  this.faces[ 5 ] === this.back
+	
+	
+	Each Cubelet has an Index which is assigned during Cube creation
+	and an Address which changes as the Cubelet changes location.
+	Additionally an AddressX, AddressY, and AddressZ are calculated 
+	from the Address and represent the Cubelet's location relative
+	to the Cube's core with integer values ranging from -1 to +1.
+	For an overview of the Cubelet's data from the browser's console:
+
+	  this.inspect()
+
+
+
+
+*/
+
+export function Cubelet( cube, id, colors ){
+
+
+	//  Our Cube can directly address its Cubelet children,
+	//  only fair the Cubelet can address their parent Cube!
+
+	this.cube = cube
+	
+
+	//  Our Cubelet's ID is its unique number on the Cube.
+	//  Each Cube has Cubletes numbered 0 through 26.
+	//  Even if we're debugging (and not attached to an actual Cube)
+	//  we need an ID number for later below
+	//  when we derive positions and rotations for the Cubelet faces.
+
+	this.id = id || 0
+
+
+	//  Our Cubelet's address is its current location on the Cube.
+	//  When the Cubelet is initialized its ID and address are the same.
+	//  This method will also set the X, Y, and Z components of the
+	//  Cubelet's address on the Cube.
+
+	this.setAddress( this.id )
+
+
+	//  We're going to build Cubelets that are 140 pixels square.
+	//  Yup. This size is hardwired in Cube.
+	//  It is also hard-wired into the CSS, but we can't simply
+	//  grab the style.getBoundingClientRect() value because 
+	//  that's a 2D measurement -- doesn't account for pos and rot.
+	
+	this.size = cube.cubeletSize || 140
+
+
+	//  Now we can find our Cubelet's X, Y, and Z position in space.
+	//  We only need this momentarily to create our Object3D so
+	//  there's no need to attach these properties to our Cubelet object.
+
+	var
+	x = this.addressX * this.size,
+	y = this.addressY * this.size,
+	z = this.addressZ * this.size
+
+
+	//  For convenience here are maps for rotating and positioning
+	//  the Cubelet face wall into place.
+
+	var	
+	half = this.size / 2,
+	rotations = [
+
+		[   0,   0, 0 ],//  Front
+		[ -90,   0, 0 ],//  Up
+		[   0,  90, 0 ],//  Right
+		[  90,   0, 0 ],//  Down
+		[   0, -90, 0 ],//  Left
+		[   0, 180, 0 ] //  Back
+	],
+	positions = [
+
+		[  0,     0,    half ],//  Front
+		[  0,    -half, 0    ],//  Up
+		[  half,  0,    0    ],//  Right
+		[  0,     half, 0    ],//  Down
+		[ -half,  0,    0    ],//  Left
+		[  0,     0,   -half ] //  Back
+	]
+
+
+	//  Our anchor only achieves rotation during a tween animation.
+	//  It is then immediately reset to rotation( 0, 0, 0 )
+	// (and its rotation information is applied to the wrapper at that moment)
+	//  and thus can be used as a reliable anchor in space repeatedly.
+
+
+	///////VWF_VIEW//////
+	// this.anchor = new THREE.Object3D()
+	// this.anchor.name = 'anchor-' + this.id
+	// if( this.cube )	this.cube.threeObject.add( this.anchor )
+	// else scene.add( this.anchor )
+	// 
+	// 
+	//
+	// if( erno.renderMode === 'css' ){
+	
+	// 	var domElement = document.createElement( 'div' )
+	// 	domElement.classList.add( 'cubelet' )
+	// 	domElement.classList.add( 'cubeletId-'+ this.id )
+	// 	this.wrapper = new THREE.CSS3DObject( domElement )
+	// }
+	// else if( erno.renderMode === 'svg' ){
+		
+	// 	this.wrapper = new THREE.Object3D()
+
+
+	// 	//  Create this Cubelet's plastic shell.
+
+	// 	this.plastic = new THREE.Mesh( 
+
+	// 		new THREE.CubeGeometry( cube.cubeletSize, cube.cubeletSize, cube.cubeletSize ),
+	// 		new THREE.MeshBasicMaterial({ color: 0xFFFFFF, vertexColors: THREE.FaceColors })
+	// 	)
+	// 	this.plastic.position.set( x, y, z )
+	// 	this.wrapper.add( this.plastic )
+
+
+	// 	//  Wireframe!
+
+	// 	this.wireframe = new THREE.Mesh( 
+
+	// 		new THREE.CubeGeometry( cube.cubeletSize, cube.cubeletSize, cube.cubeletSize ),
+	// 		new THREE.MeshBasicMaterial({ color: 0x00CCFF, wireframe: true })
+	// 	)
+	// 	this.wireframe.position.set( x, y, z )
+	// 	this.wrapper.add( this.wireframe )
+	// }
+	// this.wrapper.name = 'wrapper-' + this.id
+	// this.wrapper.position.set( x, y, z )
+	// this.anchor.add( this.wrapper )
+	//////////
+
+
+
+
+
+	//@@@  QUICK HACK TO FORCE A CUBE TO APPEAR FOR HIT TESTING WITH RAYCASTING!!!
+
+	/*if( this.plastic === undefined ){
+	
+		this.plastic = new THREE.Mesh( 
+
+			new THREE.CubeGeometry( cube.cubeletSize, cube.cubeletSize, cube.cubeletSize ),
+			new THREE.MeshBasicMaterial({ color: 0xFF00FF, vertexColors: THREE.FaceColors })
+		)
+		this.plastic.position.set( x, y, z )
+		this.wrapper.add( this.plastic )
+	}*/
+
+
+
+
+
+
+
+
+	//  We're about to loop through our colors[] Array
+	//  to build the six faces of our Cubelet.
+	//  Here's our overhead for that:
+
+	var extrovertedFaces = 0
+	if( colors === undefined ) colors = [ W, O,  ,  , G, ]
+	this.faces = []
+
+
+	//  Now let's map one color per side based on colors[].
+	//  Undefined values are allowed (and anticipated).
+	//  We need to loop through the colors[] Array "manually"
+	//  because Array.forEach() would skip the undefined entries.
+
+	for( var i = 0; i < 6; i ++ ){
+
+
+		//  Before we create our face's THREE object
+		//  we need to know where it should be positioned and rotated.
+		// (This is based on our above positions and rotations map.)
+
+		var
+		color  = colors[ i ] || COLORLESS
+		
+
+		//  Each face is an object and keeps track of its original ID number
+		// (which is important because its address will change with each rotation)
+		//  its current color, and so on.
+
+		this.faces[ i ] = {}
+		this.faces[ i ].id = i
+		this.faces[ i ].color = color
+		
+
+		//  We're going to keep track of what face was what at the moment of initialization,
+		//  mostly for solving purposes.
+		//  This is particularly useful for Striegel's solver
+		//  which requires an UP normal.
+
+		this.faces[ i ].normal = Direction.getNameById( i )
+
+		////////VWF_VIEW//////
+		// if( erno.renderMode === 'css' ){
+
+
+		// 	//  FACE CONTAINER.
+		// 	//  This face of our Cubelet needs a DOM element for all the
+		// 	//  related DOM elements to be attached to.
+
+		// 	var faceElement = document.createElement( 'div' )
+		// 	faceElement.classList.add( 'face' )
+		// 	faceElement.classList.add( 'face'+ Direction.getNameById( i ).capitalize() )
+		// 	this.wrapper.element.appendChild( faceElement )
+
+
+		// 	//  WIREFRAME.
+
+		// 	var wireframeElement = document.createElement( 'div' )
+		// 	wireframeElement.classList.add( 'wireframe' )
+		// 	faceElement.appendChild( wireframeElement )
+
+
+		// 	//  CUBELET ID.
+		// 	//  For debugging we want the ability to display this Cubelet's ID number
+		// 	//  with an underline (to make numbers like 6 and 9 legible upside-down).
+
+		// 	var idElement = document.createElement( 'div' )
+		// 	idElement.classList.add( 'id' )
+		// 	faceElement.appendChild( idElement )
+			
+		// 	var underlineElement = document.createElement( 'span' )
+		// 	underlineElement.classList.add( 'underline' )
+		// 	underlineElement.innerText = this.id
+		// 	idElement.appendChild( underlineElement )
+		// }
+
+
+		// //  INTROVERTED FACES.
+		// //  If this face has no color sticker then it must be interior to the Cube.
+		// //  That means in a normal state (no twisting happening) it is entirely hidden.
+
+		// if( color === COLORLESS ){
+
+		// 	if( erno.renderMode === 'css' ) faceElement.classList.add( 'faceIntroverted' )
+		// 	else {
+
+		// 		this.plastic.geometry.faces[ i ].color.setHex( 0x000000 )
+		// 		this.plastic.geometry.colorsNeedUpdate = true
+		// 	}
+		// }
+
+
+		// //  EXTROVERTED FACES.
+		// //  But if this face does have a color then we need to
+		// //  create a sticker with that color
+		// //  and also allow text to be placed on it.
+
+		// else {
+
+
+		// 	//  We're going to use the number of exposed sides
+		// 	//  to determine below what 'type' of Cubelet this is:
+		// 	//  Core, Center, Edge, or Corner.
+
+		// 	extrovertedFaces ++
+
+
+		// 	if( erno.renderMode === 'css' ){	
+
+		// 		faceElement.classList.add( 'faceExtroverted' )
+
+
+		// 		//  STICKER.
+		// 		//  You know, the color part that makes the Cube
+		// 		//  the most frustrating toy ever.
+
+		// 		var stickerElement = document.createElement( 'div' )
+		// 		stickerElement.classList.add( 'sticker' )			
+		// 		stickerElement.style.backgroundColor = color.hex
+		// 		faceElement.appendChild( stickerElement )
+
+
+		// 		//  TEXT.
+		// 		//  One character per face, mostly for our branding.
+
+		// 		var textElement = document.createElement( 'div' )
+		// 		textElement.classList.add( 'text' )
+		// 		textElement.innerText = i
+		// 		this.faces[ i ].text = textElement
+		// 		faceElement.appendChild( textElement )
+		// 	}
+		// 	else {
+
+		// 		/*
+		// 		var sticker = new THREE.Mesh( 
+
+		// 			new THREE.PlaneGeometry( cube.cubeletSize, cube.cubeletSize ),
+		// 			new THREE.MeshBasicMaterial({ color: color })
+		// 		)
+		// 		sticker.material.opacity = 0.7
+		// 		sticker.overdraw = true
+		// 		sticker.position.set( faceX,  faceY,  faceZ  )
+		// 		sticker.rotation.set( faceXR, faceYR, faceZR )
+		// 		this.faces[ i ].sticker = sticker
+		// 		this.wrapper.add( this.faces[ i ].sticker )
+		// 		*/
+		// 		//console.log(colorNameToHex( color ))
+		// 		//console.log(' ' )
+		// 		//console.log(this.plastic.geometry.faces[ i ].color)
+		// 		//console.log(colorNameToDecimal( color ))
+
+		// 		//var j = [ 1, 0, 2, 3, 4, 5 ][ i ]
+		// 		var j = [ 3,3,3,3,3,3 ][ i ]
+
+
+		// 		this.plastic.geometry.faces[ j ].color.setHex( colorNameToDecimal( color ))
+		// 		//this.plastic.geometry.faces[ i ].color.setHex( 0xFF00FF )
+		// 		this.plastic.geometry.colorsNeedUpdate = true
+		// 		//console.log(this.plastic.geometry.faces[ i ].color)
+		// 		//console.log(' ' )
+		// 	}
+		// }
+		/////////////
+
+	}
+
+
+	//  Now that we've run through our colors[] Array
+	//  and counted the number of extroverted sides
+	//  we can determine what 'type' of Cubelet this is.
+
+	this.type = [
+
+		'core',
+		'center',
+		'edge',
+		'corner'
+
+	][ extrovertedFaces ]
+
+
+	//  Mapping the Cubelet will setup all of our convenience shortcuts
+	//  like "this.front.color" and "this.left.text" for example.
+
+	this.map()
+
+
+	//  If this happens to be our logo-bearing Cubelet
+	//  we had better attach the logo to it!
+
+	if( this.front.color && this.front.color.name === 'white' && this.type === 'center' ){
+
+		////VWF_VIEW////
+		//if( erno.renderMode === 'css' ) stickerElement.classList.add( 'stickerLogo' )
+		////
+	}
+
+
+	//  We need to know if we're "engaged" on an axis 
+	//  which at first seems indentical to isTweening,
+	//  until you consider partial rotations. 
+
+	this.isTweening = true
+	this.isEngagedX = false
+	this.isEngagedY = false
+	this.isEngagedZ = false
+
+
+	//  Remember our separation of state code and visual code?
+	//  Well here's some slightly (though not entirely!) redundant
+	//  rotation tracking. 
+	//  It's actually this that makes partial rotations possible...
+
+	this.x = this.xPrevious = 0
+	this.y = this.yPrevious = 0
+	this.z = this.zPrevious = 0
+
+
+	//  These will perform their actions, of course,
+	//  but also setup their own boolean toggles.
+
+	////VWF_VIEW
+	// this.show()
+	// this.showPlastics()
+	// this.showIntroverts()
+	// this.showStickers()
+	// this.hideIds()
+	// this.hideTexts()
+	// this.hideWireframes()
+	///////
+
+
+	//  During a rotation animation this Cubelet marks itself as 
+	//  this.isTweening = true. 
+	//  Very useful. Let's try it out.
+
+	this.isTweening = false
+
+
+	//  Some fun tweenable properties.
+
+	this.opacity = 1
+	this.radius  = 0
+}
+
+
+
+
+
+
+
+
+globalThis.setupTasks = globalThis.setupTasks || []
+globalThis.setupTasks.push( function(){
+
+
+	//  Let's add some functionality to Cubelet's prototype
+	//  via the augment() function from Skip.js.
+	//  By adding to Cubelet's prototype and not the Cubelet constructor
+	//  we're keeping instances of Cubelet super clean and light.
+
+	globalThis.augment( Cubelet, {
+
+
+		//  Convience accessors for the Cubelet's faces.
+		//  What color is the left face? this.left() !!
+
+		map: function(){
+
+			this.front  = this.faces[ 0 ]
+			this.up     = this.faces[ 1 ]
+			this.right  = this.faces[ 2 ]
+			this.down   = this.faces[ 3 ]
+			this.left   = this.faces[ 4 ]
+			this.back   = this.faces[ 5 ]
+			this.colors = 
+
+				( this.faces[ 0 ].color ? this.faces[ 0 ].color.initial : '-' ) +
+				( this.faces[ 1 ].color ? this.faces[ 1 ].color.initial : '-' ) +
+				( this.faces[ 2 ].color ? this.faces[ 2 ].color.initial : '-' ) +
+				( this.faces[ 3 ].color ? this.faces[ 3 ].color.initial : '-' ) +
+				( this.faces[ 4 ].color ? this.faces[ 4 ].color.initial : '-' ) +
+				( this.faces[ 5 ].color ? this.faces[ 5 ].color.initial : '-' )
+		},
+
+
+		//  Aside from initialization this function will be called 
+		//  by the Cube during remapping.
+		//  The raw address is an integer from 0 through 26
+		//  mapped to the Cube in the same fashion as this.id.
+		//  The X, Y, and Z components each range from -1 through +1
+		//  where (0, 0, 0) is the Cube's core.
+
+		setAddress: function( address ){
+
+			this.address  = address || 0
+			this.addressX = address.modulo( 3 ).subtract( 1 )
+			this.addressY = address.modulo( 9 ).divide( 3 ).roundDown().subtract( 1 ) * -1
+			this.addressZ = address.divide( 9 ).roundDown().subtract( 1 ) * -1
+		},
+
+
+		//  Full inspection of the Cublet's faces
+		//  using the convenience accessors from above.
+
+		inspect: function( face ){			
+
+			if( face !== undefined ){
+
+				
+				//  Just a particular face's color -- called by Slice's inspector.
+				
+				return this[ face ].color || '!'
+			}
+			else {
+				
+
+				//  Full on ASCII-art inspection mode -- with console colors!
+
+				var
+				that    = this,
+				id      = this.id,
+				address = this.address,
+				type    = this.type,
+				color   = this.cube.color,				
+				LEFT    = 0,
+				CENTER  = 1,
+				getColorName = function( face, justification, minimumLength ){
+
+					var colorName = that[ face ].color.name.toUpperCase()
+					
+					if( justification !== undefined && minimumLength !== undefined ){
+
+						if( justification === CENTER ) colorName = colorName.justifyCenter( minimumLength )
+						else if( justification === LEFT ) colorName = colorName.justifyLeft( minimumLength )
+					}
+					return colorName
+				}
+
+				if( id < 10 ) id = '0' + id
+				if( address < 10 ) address = '0' + address
+				console.log(
+
+					'\n    ID         '+ id +
+					'\n    Type       '+ type.toUpperCase() +'\n'+
+
+					'\n    Address    '+ address +
+					'\n    Address X  '+ this.addressX.toSignedString() +
+					'\n    Address Y  '+ this.addressY.toSignedString() +
+					'\n    Address Z  '+ this.addressZ.toSignedString() +'\n'+
+
+					'\n    Engaged X  '+ this.isEngagedX +
+					'\n    Engaged Y  '+ this.isEngagedY +
+					'\n    Engaged Z  '+ this.isEngagedZ +
+					'\n    Tweening   '+ this.isTweening +'\n'+
+					
+					'\n%c 0  Front      '+ getColorName( 'front', LEFT, 7 ) +'%c'+
+					'\n%c 1  Up         '+ getColorName( 'up',    LEFT, 7 ) +'%c'+
+					'\n%c 2  Right      '+ getColorName( 'right', LEFT, 7 ) +'%c'+
+					'\n%c 3  Down       '+ getColorName( 'down',  LEFT, 7 ) +'%c'+
+					'\n%c 4  Left       '+ getColorName( 'left',  LEFT, 7 ) +'%c'+
+					'\n%c 5  Back       '+ getColorName( 'back',  LEFT, 7 ) +'%c\n' +
+
+					'\n              -----------  %cback%c'+
+					'\n            /    %cup%c     /|  %c5%c'+
+					'\n           /     %c1%c     / | %c'+ getColorName( 'back' ) +'%c'+
+					'\n          /%c'+ getColorName( 'up', CENTER, 11 ) +'%c/  |'+
+					'\n  %cleft%c    -----------   %cright%c'+
+					'\n   %c4%c     |           |   %c2%c'+
+					'\n%c'+ getColorName( 'left', CENTER, 8 ) +'%c |   %cfront%c   |  %c'+ getColorName( 'right' ) +'%c'+
+					'\n         |     %c0%c     |  /'+
+					'\n         |%c'+ getColorName( 'front', CENTER, 11 ) +'%c| /'+
+					'\n         |           |/'+
+					'\n          -----------'+
+					'\n               %cdown%c'+
+					'\n                %c3%c'+
+					'\n           %c'+ getColorName( 'down', CENTER, 11 ) +'%c\n',
+
+					this.front.color.styleB, '',
+					this.up.color.styleB,    '',
+					this.right.color.styleB, '',
+					this.down.color.styleB,  '',
+					this.left.color.styleB,  '',
+					this.back.color.styleB,  '',
+
+					this.back.color.styleF,  '',
+					this.up.color.styleF,    '',
+					this.back.color.styleF,  '',
+					this.up.color.styleF,    '',
+					this.back.color.styleF,  '',
+					this.up.color.styleF,    '',
+					this.left.color.styleF,  '',
+					this.right.color.styleF, '',
+					this.left.color.styleF,  '',
+					this.right.color.styleF, '',
+					this.left.color.styleF,  '',
+					this.front.color.styleF, '',
+					this.right.color.styleF, '',
+					this.front.color.styleF, '',
+					this.front.color.styleF, '',
+					this.down.color.styleF,  '',
+					this.down.color.styleF,  '',
+					this.down.color.styleF,  ''
+				)
+			}
+		},
+
+
+
+
+		//  Does this Cubelet contain a certain color?
+		//  If so, return a String decribing what face that color is on.
+		//  Otherwise return false.
+
+		hasColor: function( color ){
+
+			var i, face
+			
+			for( i = 0; i < 6; i ++ ){
+
+				if( this.faces[ i ].color === color ){
+					
+					face = i
+					break
+				}
+			}
+			if( face !== undefined ){
+
+				return [
+
+					'front',
+					'up',
+					'right',
+					'down',
+					'left',
+					'back'
+
+				][ face ]
+			}
+			else return false
+		},
+
+
+		//  Similar to above, but accepts an arbitrary number of colors.
+		//  This function implies AND rather than OR, XOR, etc.
+
+		hasColors: function(){
+
+			var 
+			cubelet = this,
+			result  = true,
+			colors  = Array.prototype.slice.call( arguments )
+			
+			colors.forEach( function( color ){
+
+				result = result && !!cubelet.hasColor( color )
+			})
+			return result
+		},
+
+
+
+
+		//  We can rotate this Cublet on the X, Y, and Z axes
+		//  both clockwise and anticlockwise.
+
+		rotate: function( rotation, degrees, cubeCallback, local ){
+
+			var 
+			cubelet = this,
+			cube    = this.cube,
+			xTarget = 0,
+			yTarget = 0,
+			zTarget = 0,			
+			rotationUpperCase = rotation.toUpperCase(),
+			threshold = 0.001
+
+
+			//  We need to signal to the world that we cannot accept more rotation() commands.
+			//  This will also cause all Groups (and Slices) containing this Cubelet
+			//  to refuse all twist() commands until further notice.
+
+			this.isTweening = true
+
+
+			//  Logically rotating our Cubelet is a matter of swapping the order
+			//  of the faces in the this.faces Array. The order is interpreted as:
+			//  Front, Up, Right, Down, Left, Back.
+
+			if( rotationUpperCase === 'X' ){
+
+				cubelet.isEngagedX = true
+				if( rotation === 'X' ) xTarget = degrees
+				else xTarget = -degrees
+			}
+			else if( rotationUpperCase === 'Y' ){
+
+				cubelet.isEngagedY = true
+				if( rotation === 'Y' ) yTarget = degrees
+				else yTarget = -degrees
+			}
+			else if( rotationUpperCase === 'Z' ){
+
+				cubelet.isEngagedZ = true
+				if( rotation === 'Z' ) zTarget = degrees
+				else zTarget = -degrees
+			}
+
+
+			//  At every steps let's try to keep our values tidy.
+
+			this.x += xTarget.round()
+			this.y += yTarget.round()
+			this.z += zTarget.round()
+
+
+			//  Our Cube's twistDuration is the amount of time (in miliseconds)
+			//  that it should take to rotate 90 dgrees.
+			//  We're going to scale that to match whatever number of degrees we're actually rotating:
+
+			let twistDurationScaled = 0.5
+			// var 
+			// twistDuration = this.cube !== undefined ? this.cube.twistDuration : SECOND,
+			// twistDurationScaled = Math.max(degrees.absolute().scale( 0, 90, 0, twistDuration ), 250)
+
+
+			//  And now for the rotation tween itself...
+			//  It feels very wrong to me that we're going to invert the coordinate space here
+			//  but that's how the cookie crumbles. Sorry. 
+
+			if(!local){
+				this.cube.kernel.callMethod(this.nodeID, "rotateCubelet", [
+					[
+					-xTarget,
+					-yTarget,
+					-zTarget
+				], twistDurationScaled, cubeCallback])
+			} else {
+				this.cube.remap(this.id, cubeCallback);
+				//this.isTweening = false
+			}
+
+			// new TWEEN.Tween( this.anchor.rotation )
+			// .to({
+
+			// 	x: -xTarget.degreesToRadians(),
+			// 	y: -yTarget.degreesToRadians(),
+			// 	z: -zTarget.degreesToRadians()
+			
+			// }, twistDurationScaled )
+			// .easing( TWEEN.Easing.Quartic.Out )
+			// .start()
+			// .onComplete( function(){
+
+
+			// 	//  What dark voodoo is this? 
+			// 	//  Calling window.render() within a tween onComplete()?
+			// 	//  I noticed that when switching between tabs,
+			// 	//  or after putting the machine to sleep then waking it,
+			// 	//  the tweened bits would be totally f'd
+			// 	//  yet their X, Y, Z rotation values were as expected.
+			// 	//  Calling window.render() is a dirty way to update 
+			// 	//  all of the *matrices* and keeps the world tidy!
+				
+			// 	render()
+
+
+			// 	//  We need to reset our anchor's rotation to ( 0, 0, 0 )
+			// 	//  so that we never lose our anchor's orientation relative to the cube itself.
+			// 	//  First thing to do is apply our anchor's matrix 
+			// 	//  to the matrix of our visual wrapper:
+
+			// 	cubelet.wrapper.applyMatrix( cubelet.anchor.matrix )
+
+
+			// 	//  And now that we've retained that rotation information
+			// 	//  we can safely reset the anchor's rotation:
+
+			// 	cubelet.anchor.rotation.set( 0, 0, 0 )
+
+
+			// 	// //  Here's some complexity.
+			// 	// //  We need to support partial rotations of arbitrary degrees
+			// 	// //  yet ensure our internal model is always in a valid state.
+			// 	// //  This means only remapping the Cubelet when it makes sense
+			// 	// //  and also remapping the Cube if this Cubelet is allowed to do so.
+
+			// 	// var 
+			// 	// xRemaps = cubelet.x.divide( 90 ).round()
+			// 	// 	.subtract( cubelet.xPrevious.divide( 90 ).round() )
+			// 	// 	.absolute(),
+			// 	// yRemaps = cubelet.y.divide( 90 ).round()
+			// 	// 	.subtract( cubelet.yPrevious.divide( 90 ).round() )
+			// 	// 	.absolute(),
+			// 	// zRemaps = cubelet.z.divide( 90 ).round()
+			// 	// 	.subtract( cubelet.zPrevious.divide( 90 ).round() )
+			// 	// 	.absolute()
+
+			// 	// if( erno.verbosity >= 0.9 ){
+
+			// 	// 	console.log( 'Cublet #'+ ( cubelet.id < 10 ? '0'+ cubelet.id : cubelet.id ), 
+			// 	// 		' |  xRemaps:', xRemaps, ' yRemaps:', yRemaps, ' zRemaps:', zRemaps,
+			// 	// 		' |  xPrev:', cubelet.xPrevious, ' x:', cubelet.x,
+			// 	// 		' |  yPrev:', cubelet.yPrevious, ' y:', cubelet.y,
+			// 	// 		' |  zPrev:', cubelet.zPrevious, ' z:', cubelet.z )
+			// 	// }
+
+
+			// 	// if( xRemaps ){
+					
+			// 	// 	while( xRemaps -- ){
+
+			// 	// 		if( cubelet.x < cubelet.xPrevious ) cubelet.faces = [ cubelet.up, cubelet.back, cubelet.right, cubelet.front, cubelet.left, cubelet.down ]
+			// 	// 		else cubelet.faces = [ cubelet.down, cubelet.front, cubelet.right, cubelet.back, cubelet.left, cubelet.up ]
+			// 	// 		cubelet.map()
+			// 	// 		if( cubeCallback !== undefined ){
+
+			// 	// 			cubeCallback( cubelet.cube.cubelets.slice())
+			// 	// 			cubelet.cube.map()
+			// 	// 		}
+			// 	// 	}
+			// 	// 	cubelet.xPrevious = cubelet.x
+			// 	// }
+			// 	// if( cubelet.x.modulo( 90 ).absolute() < threshold ){
+
+			// 	// 	cubelet.x = 0
+			// 	// 	cubelet.xPrevious = cubelet.x
+			// 	// 	cubelet.isEngagedX = false
+			// 	// }
+				
+
+			// 	// if( yRemaps ){
+					
+			// 	// 	while( yRemaps -- ){
+
+			// 	// 		if( cubelet.y < cubelet.yPrevious ) cubelet.faces = [ cubelet.left, cubelet.up, cubelet.front, cubelet.down, cubelet.back, cubelet.right ]
+			// 	// 		else cubelet.faces = [ cubelet.right, cubelet.up, cubelet.back, cubelet.down, cubelet.front, cubelet.left ]
+			// 	// 		cubelet.map()
+			// 	// 		if( cubeCallback !== undefined ){
+
+			// 	// 			cubeCallback( cubelet.cube.cubelets.slice())
+			// 	// 			cubelet.cube.map()
+			// 	// 		}
+			// 	// 	}
+			// 	// 	cubelet.yPrevious = cubelet.y
+			// 	// }
+			// 	// if( cubelet.y.modulo( 90 ).absolute() < threshold ){
+
+			// 	// 	cubelet.y = 0
+			// 	// 	cubelet.yPrevious = cubelet.y
+			// 	// 	cubelet.isEngagedY = false
+			// 	// }
+
+
+			// 	// if( zRemaps ){
+					
+			// 	// 	while( zRemaps -- ){
+
+			// 	// 		if( cubelet.z < cubelet.zPrevious ) cubelet.faces = [ cubelet.front, cubelet.right, cubelet.down, cubelet.left, cubelet.up, cubelet.back ]
+			// 	// 		else cubelet.faces = [ cubelet.front, cubelet.left, cubelet.up, cubelet.right, cubelet.down, cubelet.back ]
+			// 	// 		cubelet.map()
+			// 	// 		if( cubeCallback !== undefined ){
+
+			// 	// 			cubeCallback( cubelet.cube.cubelets.slice())
+			// 	// 			cubelet.cube.map()
+			// 	// 		}
+			// 	// 	}
+			// 	// 	cubelet.zPrevious = cubelet.z
+			// 	// }
+			// 	// if( cubelet.z.modulo( 90 ).absolute() < threshold ){
+
+			// 	// 	cubelet.z = 0
+			// 	// 	cubelet.zPrevious = cubelet.z
+			// 	// 	cubelet.isEngagedZ = false
+			// 	// }
+
+
+			// 	// //  Phew! Now we can turn off the tweening flag.
+
+			// 	// cubelet.isTweening = false
+			// })
+		},
+
+
+
+
+		//  Visual switches.
+
+		show: function(){
+
+			$( '.cubeletId-'+ this.id ).show()
+			this.showing = true
+		},
+		hide: function(){
+
+			$( '.cubeletId-'+ this.id ).hide()
+			this.showing = false
+		},
+		showPlastics: function(){
+
+			if( erno.renderMode === 'css' ) $( '.cubeletId-'+ this.id +' .face' ).removeClass( 'faceTransparent' )
+			else this.plastic.material.opacity = 1
+			this.showingPlastics = true
+		},
+		hidePlastics: function(){
+
+			if( erno.renderMode === 'css' ) $( '.cubeletId-'+ this.id +' .face' ).addClass( 'faceTransparent' )
+			else this.plastic.material.opacity = 0
+			this.showingPlastics = false
+		},
+		showExtroverts: function(){
+
+			$( '.cubeletId-'+ this.id + ' .faceExtroverted' ).show()
+			this.showingExtroverts = true
+		},
+		hideExtroverts: function(){
+
+			$( '.cubeletId-'+ this.id + ' .faceExtroverted' ).hide()
+			this.showingExtroverts = false
+		},
+		showIntroverts: function(){
+
+			$( '.cubeletId-'+ this.id + ' .faceIntroverted' ).show()
+			this.showingIntroverts = true
+		},
+		hideIntroverts: function(){
+
+			$( '.cubeletId-'+ this.id + ' .faceIntroverted' ).hide()
+			this.showingIntroverts = false
+		},
+		showStickers: function(){
+
+			if( erno.renderMode === 'css' ) $( '.cubeletId-'+ this.id + ' .sticker' ).show()
+			else this.faces.forEach( function( face ){
+
+				if( face.sticker ) face.sticker.material.opacity = 1
+			})
+			this.showingStickers = true
+		},
+		hideStickers: function(){
+
+			if( erno.renderMode === 'css' ) $( '.cubeletId-'+ this.id + ' .sticker' ).hide()
+			else this.faces.forEach( function( face ){
+
+				if( face.sticker ) face.sticker.material.opacity = 0
+			})
+			this.showingStickers = false
+		},
+		showWireframes: function(){
+
+			if( erno.renderMode === 'css' ) $( '.cubeletId-'+ this.id + ' .wireframe' ).show()
+			else this.wireframe.material.opacity = 1
+			this.showingWireframes = true
+		},
+		hideWireframes: function(){
+
+			if( erno.renderMode === 'css' ) $( '.cubeletId-'+ this.id + ' .wireframe' ).hide()
+			else this.wireframe.material.opacity = 0
+			this.showingWireframes = false
+		},
+		showIds: function(){
+
+			$( '.cubeletId-'+ this.id + ' .id' ).show()
+			this.showingIds = true
+		},
+		hideIds: function(){
+
+			$( '.cubeletId-'+ this.id + ' .id' ).hide()
+			this.showingIds = false
+		},
+		showTexts: function(){
+
+			$( '.cubeletId-'+ this.id + ' .text' ).show()
+			this.showingTexts = true
+		},
+		hideTexts: function(){
+
+			$( '.cubeletId-'+ this.id + ' .text' ).hide()
+			this.showingTexts = false
+		},
+
+
+
+
+		getOpacity: function(){
+
+			return this.opacity
+		},
+		setOpacity: function( opacityTarget, onComplete ){
+
+			if( this.opacityTween ) this.opacityTween.stop()
+			if( opacityTarget === undefined ) opacityTarget = 1
+			if( opacityTarget !== this.opacity ){
+
+				var 
+				that = this,
+				tweenDuration = ( opacityTarget - this.opacity ).absolute().scale( 0, 1, 0, SECOND )
+
+				this.opacityTween = new TWEEN.Tween({ opacity: this.opacity })
+				.to({
+
+					opacity: opacityTarget
+				
+				}, tweenDuration )
+				.easing( TWEEN.Easing.Quadratic.InOut )
+				.onUpdate( function(){
+
+					$( '.cubeletId-'+ that.id ).css( 'opacity', this.opacity )
+					that.opacity = this.opacity//opacityTarget
+				})
+				.onComplete( function(){
+
+					if( onComplete instanceof Function ) onComplete()
+				})
+				.start()
+			}
+		},
+		getStickersOpacity: function( value ){
+
+			return $( '.cubeletId-'+ this.id + ' .sticker' ).css( 'opacity' )
+		},
+		setStickersOpacity: function( value ){
+
+			if( value === undefined ) value = 0.2
+			$( '.cubeletId-'+ this.id + ' .sticker' ).css( 'opacity', value )
+		},
+		getRadius: function(){
+
+			return this.radius
+		},
+		setRadius: function( radius, onComplete ){
+
+
+			//  @@
+			//  It's a shame that we can't do this whilst tweening
+			//  but it's because the current implementation is altering the actual X, Y, Z
+			//  rather than the actual radius. Can fix later.
+
+			//  Current may produce unexpected results while shuffling. For example:
+			//    cube.corners.setRadius( 90 )
+			//  may cause only 4 corners instead of 6 to setRadius()
+			//  because one side is probably engaged in a twist tween.
+
+			if( this.isTweening === false ){
+	
+				radius = radius || 0
+				if( this.radius === undefined ) this.radius = 0
+				if( this.radius !== radius ){
+
+
+					//  Here's some extra cuteness to make the tween's duration
+					//  proportional to the distance traveled.
+
+					var tweenDuration = ( this.radius - radius ).absolute().scale( 0, 100, 0, SECOND )
+
+
+					//  We need a "that = this" in order to set this.radius = radius
+					//  from inside the anonymous onComplete() function below. 
+
+					var that = this
+					new TWEEN.Tween( this.wrapper.position )
+					.to({
+
+						x: this.addressX.multiply( this.size + radius ),
+						y: this.addressY.multiply( this.size + radius ),
+						z: this.addressZ.multiply( this.size + radius )
+					
+					}, tweenDuration )
+					.easing( TWEEN.Easing.Quartic.Out )	
+					.onComplete( function(){
+
+						that.radius = radius
+						if( onComplete instanceof Function ) onComplete()
+					})
+					.start()
+				}
+			}
+		}
+	
+
+
+
+	})
+})

+ 2109 - 0
public/drivers/model/rubik/lib/cubes.js

@@ -0,0 +1,2109 @@
+/*
+
+
+	CUBES
+
+	A Cube is composed of 27 Cubelets (3x3x3 grid) numbered 0 through 26.
+	Cubelets are numbered beginning from the top-left-forward corner of the 
+	Cube and proceeding left to right, top to bottom, forward to back:
+     
+
+             ----------------------- 
+           /   18      19      20  /|
+          /                       / |
+         /   9      10       11  / 20
+        /                       /   |
+       /   0       1       2   / 11 |
+       -----------------------     23
+      |                       |2    |
+      |   0       1       2   |  14 |
+      |                       |    26
+      |                       |5    |
+      |   3       4       5   |  17 /
+      |                       |    /
+      |                       |8  /
+      |   6       7       8   |  /
+      |                       | /
+       ----------------------- 
+
+
+
+	Portions of the Cube are grouped (Groups):
+
+	  this.core
+	  this.centers
+	  this.edges
+	  this.corners
+	  this.crosses
+	
+
+
+	Portions of the Cube are grouped and rotatable (Slices):
+
+	Rotatable around the Z axis:
+	  this.front
+	  this.standing
+	  this.back
+
+	Rotatable around the X axis:
+	  this.left
+	  this.middle
+	  this.right
+
+	Rotatable around the Y axis:
+	  this.up
+	  this.equator
+	  this.down
+
+
+
+	A Cube may be inspected through its Faces (see Slices for more 
+	information on Faces vs Slices). From the browser's JavaScript console:
+
+	  this.inspect()
+
+	This will reveal each Face's Cubelet indexes and colors using the Face's
+	compact inspection mode. The non-compact mode may be accessed by passing
+	a non-false value as an argument:
+
+	  this.inspect( true )
+
+
+
+
+*/
+
+
+
+
+
+
+
+
+export function Cube( id, preset ){
+
+
+
+	//  Important for working around lexical closures in things like
+	//  forEach() or setTimeout(), etc which change the scope of "this".
+
+	var cube = this
+
+	this.id = id;
+	//  Some important booleans.
+
+	this.isReady     = true
+	this.isShuffling = false
+	this.isRotating  = false
+	this.isSolving   = false
+
+
+	//  Every fire of this.loop() will attempt to complete our tasks
+	//  which can only be run if this.isReady === true.
+
+	this.taskQueue = new Queue()
+
+
+	//  We need the ability to gang up twist commands.
+	//  Every fire of this.loop() will attempt to empty it.
+
+	this.twistQueue = new Queue( Twist.validate )
+
+
+	//  How long should a Cube.twist() take?
+
+	this.twistDuration = SECOND
+
+
+	//  If we shuffle, how shall we do it?
+	
+	this.shuffleMethod = this.PRESERVE_LOGO
+
+
+	//  Size matters? Cubelets will attempt to read these values.
+
+	this.size = 420
+	this.cubeletSize = 140
+
+
+
+
+	//  We need to create and setup a new CSS3 Object
+	//  to represent our Cube. 
+	//  THREE will take care of attaching it to the DOM, etc.
+
+
+	///////VWF_VIEW////
+	// if( erno.renderMode === 'css' ){
+	
+	// 	this.domElement = document.createElement( 'div' )
+	// 	this.domElement.classList.add( 'cube' )
+	// 	this.threeObject = new THREE.CSS3DObject( this.domElement )
+	// }
+	// else if( erno.renderMode === 'svg' ){
+
+	// 	this.threeObject = new THREE.Object3D()
+	// }
+	// this.threeObject.rotation.set(
+
+	// 	(  25 ).degreesToRadians(), 
+	// 	( -30 ).degreesToRadians(),
+	// 	0
+	// )
+	// scene.add( this.threeObject )
+	/////
+
+
+	//  If we enable Auto-Rotate then the cube will spin (not twist!) in space
+	//  by adding the following values to the Three object on each frame.
+
+	this.rotationDeltaX = 0.1
+	this.rotationDeltaY = 0.15
+	this.rotationDeltaZ = 0
+
+
+
+
+	//  Here's the first big map we've come across in the program so far. 
+	//  Imagine you're looking at the Cube straight on so you only see the front face.
+	//  We're going to map that front face from left to right (3), and top to bottom (3): 
+	//  that's 3 x 3 = 9 Cubelets.
+	//  But then behind the Front slice we also have a Standing slice (9) and Back slice (9),
+	//  so that's going to be 27 Cubelets in total to create a Cube.
+
+	this.cubelets = []
+	;([
+
+		//  Front slice
+
+		[ W, O,  ,  , G,   ],    [ W, O,  ,  ,  ,   ],    [ W, O, B,  ,  ,   ],//   0,  1,  2
+		[ W,  ,  ,  , G,   ],    [ W,  ,  ,  ,  ,   ],    [ W,  , B,  ,  ,   ],//   3,  4,  5
+		[ W,  ,  , R, G,   ],    [ W,  ,  , R,  ,   ],    [ W,  , B, R,  ,   ],//   6,  7,  8
+
+
+		//  Standing slice
+
+		[  , O,  ,  , G,   ],    [  , O,  ,  ,  ,   ],    [  , O, B,  ,  ,   ],//   9, 10, 11
+		[  ,  ,  ,  , G,   ],    [  ,  ,  ,  ,  ,   ],    [  ,  , B,  ,  ,   ],//  12, XX, 14
+		[  ,  ,  , R, G,   ],    [  ,  ,  , R,  ,   ],    [  ,  , B, R,  ,   ],//  15, 16, 17
+
+
+		//  Back slice
+
+		[  , O,  ,  , G, Y ],    [  , O,  ,  ,  , Y ],    [  , O, B,  ,  , Y ],//  18, 19, 20
+		[  ,  ,  ,  , G, Y ],    [  ,  ,  ,  ,  , Y ],    [  ,  , B,  ,  , Y ],//  21, 22, 23
+		[  ,  ,  , R, G, Y ],    [  ,  ,  , R,  , Y ],    [  ,  , B, R,  , Y ] //  24, 25, 26
+
+	]).forEach( function( cubeletColorMap, cubeletId ){
+
+		cube.cubelets.push( new Cubelet( cube, cubeletId, cubeletColorMap ))
+	})
+
+
+	//  Mapping the Cube creates all the convenience shortcuts
+	//  that we will need later. (Demonstrated immediately below!)
+
+	this.map()
+
+
+	//  Now that we have mapped faces we can create faceLabels
+
+	/////////VWF_VIEW
+	// if( erno.renderMode === 'css' ){
+
+	// 	this.faces.forEach( function( face, i ){
+
+	// 		var labelElement = document.createElement( 'div' )
+	// 		labelElement.classList.add( 'faceLabel' )
+	// 		labelElement.classList.add( 'face'+ face.face.capitalize() )
+	// 		labelElement.innerHTML = face.face.toUpperCase()
+	// 		cube.domElement.appendChild( labelElement )
+	// 	})
+	// }
+	///////////
+
+	//  We need to map our folds separately from Cube.map()
+	//  because we only want folds mapped at creation time.
+	//  Remapping folds with each Cube.twist() would get weird...
+
+	this.folds = [
+
+		new Fold( this.front, this.right ),
+		new Fold( this.left,  this.up    ),
+		new Fold( this.down,  this.back  )
+	]
+
+
+	//  Enable some "Hero" text for this Cube.
+	
+	/////VWF_VIEW
+	// if( erno.renderMode === 'css' ){
+
+	// 	this.setText( 'BEYONDRUBIKs  CUBE', 0 )
+	// 	this.setText( 'BEYONDRUBIKs  CUBE', 1 )
+	// 	this.setText( 'BEYONDRUBIKs  CUBE', 2 )
+	// }
+	/////////
+
+	//  Shall we load some presets here?
+
+	// preset = 'preset' + preset.capitalize()
+	// if( this[ preset ] instanceof Function === false ) preset = 'presetBling'
+	// this[ preset ]()
+
+
+	//  Get ready for major loop-age.
+	//  Our Cube checks these booleans at roughly 60fps.
+
+	// setInterval( cube.loop, 16 )
+
+
+	//  Enable key commands for our Cube.
+
+
+	/////VWF_VIEW/////
+	// $( document ).keypress( function( event ){
+
+	// 	if( $( 'input:focus, textarea:focus' ).length === 0 ){
+			
+	// 		var key = String.fromCharCode( event.which )
+	// 		if( 'XxRrMmLlYyUuEeDdZzFfSsBb'.indexOf( key ) >= 0 ) cube.twistQueue.add( key )
+	// 	}
+	// })
+	////////
+
+
+}
+
+
+
+globalThis.setupTasks = globalThis.setupTasks || []
+globalThis.setupTasks.push( function(){
+
+	Cube.swapMaps = {
+		'f':[
+			[0,2],
+			[1,5],
+			[2,8],
+			[3,1],
+			[5,7],
+			[6,0],
+			[7,3],
+			[8,6]
+		],
+		'F':[
+			[0,6],
+			[1,3],
+			[2,0],
+			[3,7],
+			[5,1],
+			[6,8],
+			[7,5],
+			[8,2]
+		],
+		'b':[
+			[18,24],
+			[19,21],
+			[20,18],
+			[21,25],
+			[23,19],
+			[24,26],
+			[25,23],
+			[26,20]
+		],
+		'B':[
+			[18,20],
+			[19,23],
+			[20,26],
+			[21,19],
+			[23,25],
+			[24,18],
+			[25,21],
+			[26,24]
+		],
+		'd':[
+			[6,8],
+			[7,17],
+			[8,26],
+			[15,7],
+			[17,25],
+			[24,6],
+			[25,15],
+			[26,24]
+		],
+		'D':[
+			[6,24],
+			[7,15],
+			[8,6],
+			[15,25],
+			[17,7],
+			[24,26],
+			[25,17],
+			[26,8]
+		],
+		'u':[
+			[18,20],
+			[19,11],
+			[20,2],
+			[9,19],
+			[11,1],
+			[0,18],
+			[1,9],
+			[2,0]
+		],
+		'U':[
+			[18,0],
+			[19,9],
+			[20,18],
+			[9,1],
+			[11,19],
+			[0,2],
+			[1,11],
+			[2,20]
+		],
+		'l':[
+			[18,0],
+			[9,3],
+			[0,6],
+			[21,9],
+			[3,15],
+			[24,18],
+			[15,21],
+			[6,24]
+		],
+		'L':[
+			[18,24],
+			[9,21],
+			[0,18],
+			[21,15],
+			[3,9],
+			[24,6],
+			[15,3],
+			[6,0]
+		],
+		'r':[
+			[2,20],
+			[11,23],
+			[20,26],
+			[5,11],
+			[23,17],
+			[8,2],
+			[17,5],
+			[26,8]
+		],
+		'R':[
+			[2,8],
+			[11,5],
+			[20,2],
+			[5,17],
+			[23,11],
+			[8,26],
+			[17,23],
+			[26,20]
+		]
+	}
+
+	Cube.verbosity = 0.5;
+	Cube.prototype = Object.create( Group.prototype )
+	Cube.prototype.constructor = Cube
+
+	forceAugment( Cube, {
+
+
+		//  A Rubik's Cube is composed of 27 cubelets arranged 3 x 3 x 3.
+		//  We need a map that relates these 27 locations to the 27 cubelets
+		//  such that we can ask questions like:
+		//  What colors are on the Front face of the cube? Etc.
+
+		map: function(){
+				
+			var that = this, i
+
+
+			//  Groups are simple collections of Cubelets.
+			//  Their position and rotation is irrelevant. 
+
+			this.core    = new Group()
+			this.centers = new Group()
+			this.edges   = new Group()
+			this.corners = new Group()
+			this.crosses = new Group()
+			this.cubelets.forEach( function( cubelet, index ){
+
+				if( cubelet.type === 'core'   ) that.core.add( cubelet )
+				if( cubelet.type === 'center' ) that.centers.add( cubelet )
+				if( cubelet.type === 'edge'   ) that.edges.add( cubelet )
+				if( cubelet.type === 'corner' ) that.corners.add( cubelet )
+				if( cubelet.type === 'center' || cubelet.type === 'edge' ) that.crosses.add( cubelet )
+			})
+
+
+			//  Slices that can rotate about the X-axis:
+
+			this.left = new Slice(
+
+				this.cubelets[ 24 ], this.cubelets[ 21 ], this.cubelets[ 18 ],
+				this.cubelets[ 15 ], this.cubelets[ 12 ], this.cubelets[  9 ],
+				this.cubelets[  6 ], this.cubelets[  3 ], this.cubelets[  0 ]
+			)
+			this.left.name = 'left'
+			this.middle = new Slice(
+
+				this.cubelets[ 25 ], this.cubelets[ 22 ], this.cubelets[ 19 ],
+				this.cubelets[ 16 ], this.cubelets[ 13 ], this.cubelets[ 10 ],
+				this.cubelets[  7 ], this.cubelets[  4 ], this.cubelets[  1 ]
+			)
+			this.middle.name = 'middle'
+			this.right = new Slice(
+
+				this.cubelets[  2 ], this.cubelets[ 11 ], this.cubelets[ 20 ],
+				this.cubelets[  5 ], this.cubelets[ 14 ], this.cubelets[ 23 ],
+				this.cubelets[  8 ], this.cubelets[ 17 ], this.cubelets[ 26 ]
+			)
+			this.right.name = 'right'
+
+
+			//  Slices that can rotate about the Y-axis:
+
+			this.up = new Slice(
+
+				this.cubelets[ 18 ], this.cubelets[ 19 ], this.cubelets[ 20 ],
+				this.cubelets[  9 ], this.cubelets[ 10 ], this.cubelets[ 11 ],
+				this.cubelets[  0 ], this.cubelets[  1 ], this.cubelets[  2 ]
+			)
+			this.up.name = 'up'
+			this.equator = new Slice(
+
+				this.cubelets[ 21 ], this.cubelets[ 22 ], this.cubelets[ 23 ],
+				this.cubelets[ 12 ], this.cubelets[ 13 ], this.cubelets[ 14 ],
+				this.cubelets[  3 ], this.cubelets[  4 ], this.cubelets[  5 ]
+			)
+			this.equator.name = 'equator'
+			this.down = new Slice(
+
+				this.cubelets[  8 ], this.cubelets[ 17 ], this.cubelets[ 26 ],
+				this.cubelets[  7 ], this.cubelets[ 16 ], this.cubelets[ 25 ],
+				this.cubelets[  6 ], this.cubelets[ 15 ], this.cubelets[ 24 ]
+			)
+			this.down.name = 'down'
+
+
+			//  Slices are Groups with purpose; they are rotate-able!
+			//  These are Slices that can rotate about the Z-axis:
+
+			this.front = new Slice(
+
+				this.cubelets[  0 ], this.cubelets[  1 ], this.cubelets[  2 ],
+				this.cubelets[  3 ], this.cubelets[  4 ], this.cubelets[  5 ],
+				this.cubelets[  6 ], this.cubelets[  7 ], this.cubelets[  8 ]
+			)
+			this.front.name = 'front'
+			this.standing = new Slice(
+
+				this.cubelets[  9 ], this.cubelets[ 10 ], this.cubelets[ 11 ],
+				this.cubelets[ 12 ], this.cubelets[ 13 ], this.cubelets[ 14 ],
+				this.cubelets[ 15 ], this.cubelets[ 16 ], this.cubelets[ 17 ]
+			)
+			this.standing.name = 'standing'
+			this.back = new Slice(
+
+				this.cubelets[ 26 ], this.cubelets[ 23 ], this.cubelets[ 20 ],
+				this.cubelets[ 25 ], this.cubelets[ 22 ], this.cubelets[ 19 ],
+				this.cubelets[ 24 ], this.cubelets[ 21 ], this.cubelets[ 18 ]
+			)
+			this.back.name = 'back'
+
+
+			//  Faces .... special kind of Slice!
+
+			this.faces = [ this.front, this.up, this.right, this.down, this.left, this.back ]
+
+
+			//  Good to let each Cubelet know where it exists
+			//  in relationship to our full Cube.
+
+			for( i = 0; i < this.cubelets.length; i ++ ){
+
+				this.cubelets[ i ].setAddress( i );
+
+				//vwf_model
+				// if(that.kernel && this.cubelets[ i ].nodeID){
+				// 	that.kernel.setProperty(this.cubelets[ i ].nodeID, "address", this.cubelets[ i ].address);
+				// }
+			}
+		},
+
+
+
+
+		//  We can read and write text to the Cube.
+		//  This is handled by Folds which are composed of two Faces.
+
+		getText: function( fold ){
+
+			if( fold === undefined ){
+
+				return [
+
+					this.folds[ 0 ].getText(),
+					this.folds[ 1 ].getText(),
+					this.folds[ 2 ].getText()
+				]
+			}
+			else if( isNumeric( fold ) && fold >= 0 && fold <= 2 ){
+
+				return this.folds[ fold ].getText()
+			}
+		},
+		setText: function( text, fold ){
+
+			if( fold === undefined ){
+
+				this.folds[ 0 ].setText( text )
+				this.folds[ 1 ].setText( text )
+				this.folds[ 2 ].setText( text )
+			}
+			else if( isNumeric( fold ) && fold >= 0 && fold <= 2 ){
+
+				this.folds[ fold ].setText( text )
+			}
+		},
+
+
+
+
+		//  We'll inspect the Cube by specifically inspecting the Faces.
+		//  Bear in mind this is merely one way to think about the Cube
+		//  and does require some redundancy in terms of Cubelet indexes.
+		//  Here we'll default to 'compact' mode in order to give the
+		//  full Cube overview in the least amount of space. 
+
+		inspect: function( compact, side ){
+
+			compact = !compact
+
+			this.front.inspect( compact, side )
+			this.up.inspect(    compact, side )
+			this.right.inspect( compact, side )
+			this.down.inspect(  compact, side )
+			this.left.inspect(  compact, side )
+			this.back.inspect(  compact, side )
+		},
+
+
+
+
+		solve: function(){
+
+			this.isSolving = true
+		},
+		isSolved: function(){
+
+			return (
+
+				this.front.isSolved( FRONT ) &&
+				this.up.isSolved(    UP    ) &&
+				this.right.isSolved( RIGHT ) &&
+				this.down.isSolved(  DOWN  ) &&
+				this.left.isSolved(  LEFT  ) &&
+				this.back.isSolved(  BACK  )
+			)
+		},
+
+
+		remap: function(cubeletID, cubeCallback){
+
+			// let cubeletID = methodParameters[0];
+			// let cubeCallback = methodParameters[1]
+
+
+				let threshold = 0.001
+				//  Here's some complexity.
+						//  We need to support partial rotations of arbitrary degrees
+						//  yet ensure our internal model is always in a valid state.
+						//  This means only remapping the Cubelet when it makes sense
+						//  and also remapping the Cube if this Cubelet is allowed to do so.
+						//let myCube = node.cube;
+						let cube = this; //node.cube;
+						let cubelet = cube.cubelets.filter(el=> el.id == cubeletID)[0];
+						
+		
+						var 
+						xRemaps = cubelet.x.divide( 90 ).round()
+							.subtract( cubelet.xPrevious.divide( 90 ).round() )
+							.absolute(),
+						yRemaps = cubelet.y.divide( 90 ).round()
+							.subtract( cubelet.yPrevious.divide( 90 ).round() )
+							.absolute(),
+						zRemaps = cubelet.z.divide( 90 ).round()
+							.subtract( cubelet.zPrevious.divide( 90 ).round() )
+							.absolute()
+		
+						if( Cube.verbosity >= 0.9 ){
+		
+							console.log( 'Cublet #'+ ( cubelet.id < 10 ? '0'+ cubelet.id : cubelet.id ), 
+								' |  xRemaps:', xRemaps, ' yRemaps:', yRemaps, ' zRemaps:', zRemaps,
+								' |  xPrev:', cubelet.xPrevious, ' x:', cubelet.x,
+								' |  yPrev:', cubelet.yPrevious, ' y:', cubelet.y,
+								' |  zPrev:', cubelet.zPrevious, ' z:', cubelet.z )
+						}
+		
+		
+						if( xRemaps ){
+							
+							while( xRemaps -- ){
+		
+								if( cubelet.x < cubelet.xPrevious ) cubelet.faces = [ cubelet.up, cubelet.back, cubelet.right, cubelet.front, cubelet.left, cubelet.down ]
+								else cubelet.faces = [ cubelet.down, cubelet.front, cubelet.right, cubelet.back, cubelet.left, cubelet.up ]
+								cubelet.map()
+								if( cubeCallback !== undefined ){
+		
+									let swapMap = Cube.swapMaps[cubeCallback];
+									let swap = cubelet.cube.cubelets.slice();
+									swapMap.forEach(el=>{
+										cube.cubelets[el[0]] = swap[el[1]]
+									})
+									//cubeCallback( cubelet.cube.cubelets.slice())
+									cubelet.cube.map()
+								}
+							}
+							cubelet.xPrevious = cubelet.x
+						}
+						if( cubelet.x.modulo( 90 ).absolute() < threshold ){
+		
+							cubelet.x = 0
+							cubelet.xPrevious = cubelet.x
+							cubelet.isEngagedX = false
+						}
+						
+		
+						if( yRemaps ){
+							
+							while( yRemaps -- ){
+		
+								if( cubelet.y < cubelet.yPrevious ) cubelet.faces = [ cubelet.left, cubelet.up, cubelet.front, cubelet.down, cubelet.back, cubelet.right ]
+								else cubelet.faces = [ cubelet.right, cubelet.up, cubelet.back, cubelet.down, cubelet.front, cubelet.left ]
+								cubelet.map()
+								if( cubeCallback !== undefined ){
+		
+									let swapMap = Cube.swapMaps[cubeCallback];
+									let swap = cubelet.cube.cubelets.slice();
+									swapMap.forEach(el=>{
+										cube.cubelets[el[0]] = swap[el[1]]
+									})
+									//cubeCallback( cubelet.cube.cubelets.slice())
+									cubelet.cube.map()
+								}
+							}
+							cubelet.yPrevious = cubelet.y
+						}
+						if( cubelet.y.modulo( 90 ).absolute() < threshold ){
+		
+							cubelet.y = 0
+							cubelet.yPrevious = cubelet.y
+							cubelet.isEngagedY = false
+						}
+		
+		
+						if( zRemaps ){
+							
+							while( zRemaps -- ){
+		
+								if( cubelet.z < cubelet.zPrevious ) cubelet.faces = [ cubelet.front, cubelet.right, cubelet.down, cubelet.left, cubelet.up, cubelet.back ]
+								else cubelet.faces = [ cubelet.front, cubelet.left, cubelet.up, cubelet.right, cubelet.down, cubelet.back ]
+								cubelet.map()
+								if( cubeCallback !== undefined ){
+							//debugger;
+									let swapMap = Cube.swapMaps[cubeCallback];
+									let swap = cubelet.cube.cubelets.slice();
+									swapMap.forEach(el=>{
+										cube.cubelets[el[0]] = swap[el[1]]
+									})
+									//cubeCallback( cubelet.cube.cubelets.slice())
+									cubelet.cube.map()
+								}
+							}
+							cubelet.zPrevious = cubelet.z
+						}
+						if( cubelet.z.modulo( 90 ).absolute() < threshold ){
+		
+							cubelet.z = 0
+							cubelet.zPrevious = cubelet.z
+							cubelet.isEngagedZ = false
+						}
+		
+		
+						//  Phew! Now we can turn off the tweening flag.
+		
+						cubelet.isTweening = false
+
+		},
+
+		twist: function( twist, local ){
+
+			let cube = this;
+			var onTwistComplete
+
+			let command = twist.command;
+			console.log(cube.isTweening());
+			if( twist instanceof Twist && !cube.isTweening() ){
+
+				
+				let degrees = twist.degrees
+				if( Cube.verbosity >= 0.8 ){
+	
+					console.log( 
+
+						'Executing a twist command to rotate the '+ 
+						 twist.group +' '+ twist.wise +' by',
+						 twist.degrees, 'degrees.'
+					)
+				}
+
+
+				//  X-axis rotations
+		
+				if( command === 'X' && !cube.isEngagedY() && !cube.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets = [
+
+							swap[  6 ], swap[  7 ], swap[  8 ],
+							swap[ 15 ], swap[ 16 ], swap[ 17 ],
+							swap[ 24 ], swap[ 25 ], swap[ 26 ],
+
+							swap[  3 ], swap[  4 ], swap[  5 ],
+							swap[ 12 ], swap[ 13 ], swap[ 14 ],
+							swap[ 21 ], swap[ 22 ], swap[ 23 ],
+
+							swap[  0 ], swap[  1 ], swap[  2 ],
+							swap[  9 ], swap[ 10 ], swap[ 11 ],
+							swap[ 18 ], swap[ 19 ], swap[ 20 ]
+						]
+					}
+					if( degrees === undefined ) degrees = cube.getDistanceToPeg( 'X' )
+					cube.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.cubelets.length - 1 ) cubelet.rotate( 'X', degrees, onTwistComplete )
+						else cubelet.rotate( 'X', degrees )
+					})
+				}
+				else if( command === 'x' && !cube.isEngagedY() && !cube.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets = [
+
+							swap[ 18 ], swap[ 19 ], swap[ 20 ],
+							swap[  9 ], swap[ 10 ], swap[ 11 ],
+							swap[  0 ], swap[  1 ], swap[  2 ],
+
+							swap[ 21 ], swap[ 22 ], swap[ 23 ],
+							swap[ 12 ], swap[ 13 ], swap[ 14 ],
+							swap[  3 ], swap[  4 ], swap[  5 ],
+
+							swap[ 24 ], swap[ 25 ], swap[ 26 ],
+							swap[ 15 ], swap[ 16 ], swap[ 17 ],
+							swap[  6 ], swap[  7 ], swap[  8 ]
+						]
+					}
+					if( degrees === undefined ) degrees = cube.getDistanceToPeg( 'x' )
+					cube.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.cubelets.length - 1 ) cubelet.rotate( 'x', degrees, onTwistComplete )
+						else cubelet.rotate( 'x', degrees )
+					})
+				}
+				else if( command === 'R' && !cube.right.isEngagedY() && !cube.right.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  2 ] = swap[  8 ]
+						cube.cubelets[ 11 ] = swap[  5 ]
+						cube.cubelets[ 20 ] = swap[  2 ]
+						cube.cubelets[  5 ] = swap[ 17 ]
+						cube.cubelets[ 23 ] = swap[ 11 ]
+						cube.cubelets[  8 ] = swap[ 26 ]
+						cube.cubelets[ 17 ] = swap[ 23 ]
+						cube.cubelets[ 26 ] = swap[ 20 ]
+					}
+					if( degrees === undefined ) degrees = cube.right.getDistanceToPeg( 'X' )
+					cube.right.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.right.cubelets.length - 1 ) {
+							cubelet.rotate( 'X', degrees, 'R', local )
+						} else {
+							cubelet.rotate( 'X', degrees, undefined, local )
+						}
+					})
+				}
+				else if( command === 'r' && !cube.right.isEngagedY() && !cube.right.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  2 ] = swap[ 20 ]
+						cube.cubelets[ 11 ] = swap[ 23 ]
+						cube.cubelets[ 20 ] = swap[ 26 ]
+						cube.cubelets[  5 ] = swap[ 11 ]
+						cube.cubelets[ 23 ] = swap[ 17 ]
+						cube.cubelets[  8 ] = swap[  2 ]
+						cube.cubelets[ 17 ] = swap[  5 ]
+						cube.cubelets[ 26 ] = swap[  8 ]
+					}
+					if( degrees === undefined ) degrees = cube.right.getDistanceToPeg( 'x' )
+					cube.right.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.right.cubelets.length - 1 ) {
+							cubelet.rotate( 'x', degrees, 'r', local )
+						} else {
+							cubelet.rotate( 'x', degrees, undefined, local)
+						}
+					})
+				}
+				else if( command === 'M' && !cube.middle.isEngagedY() && !cube.middle.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  1 ] = swap[ 19 ]
+						cube.cubelets[ 10 ] = swap[ 22 ]
+						cube.cubelets[ 19 ] = swap[ 25 ]
+						cube.cubelets[  4 ] = swap[ 10 ]
+						cube.cubelets[ 22 ] = swap[ 16 ]
+						cube.cubelets[  7 ] = swap[  1 ]
+						cube.cubelets[ 16 ] = swap[  4 ]
+						cube.cubelets[ 25 ] = swap[  7 ]
+					}
+					if( degrees === undefined ) degrees = cube.middle.getDistanceToPeg( 'x' )
+					cube.middle.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.middle.cubelets.length - 1 ) cubelet.rotate( 'x', degrees, onTwistComplete )
+						else cubelet.rotate( 'x', degrees )
+					})
+				}
+				else if( command === 'm' && !cube.middle.isEngagedY() && !cube.middle.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  1 ] = swap[  7 ]
+						cube.cubelets[ 10 ] = swap[  4 ]
+						cube.cubelets[ 19 ] = swap[  1 ]
+						cube.cubelets[  4 ] = swap[ 16 ]
+						cube.cubelets[ 22 ] = swap[ 10 ]
+						cube.cubelets[  7 ] = swap[ 25 ]
+						cube.cubelets[ 16 ] = swap[ 22 ]
+						cube.cubelets[ 25 ] = swap[ 19 ]
+					}
+					if( degrees === undefined ) degrees = cube.middle.getDistanceToPeg( 'X' )
+					cube.middle.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.middle.cubelets.length - 1 ) cubelet.rotate( 'X', degrees, onTwistComplete )
+						else cubelet.rotate( 'X', degrees, onTwistComplete )
+					})
+				}
+				else if( command === 'L' && !cube.left.isEngagedY() && !cube.left.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[ 18 ] = swap[ 24 ]
+						cube.cubelets[  9 ] = swap[ 21 ]
+						cube.cubelets[  0 ] = swap[ 18 ]
+						cube.cubelets[ 21 ] = swap[ 15 ]
+						cube.cubelets[  3 ] = swap[  9 ]
+						cube.cubelets[ 24 ] = swap[  6 ]
+						cube.cubelets[ 15 ] = swap[  3 ]
+						cube.cubelets[  6 ] = swap[  0 ]
+					}
+					if( degrees === undefined ) degrees = cube.left.getDistanceToPeg( 'x' )
+					cube.left.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.left.cubelets.length - 1 ) {
+							cubelet.rotate( 'x', degrees, 'L', local )
+						} else {
+							cubelet.rotate( 'x', degrees, undefined, local )
+						}
+					})
+				}
+				else if( command === 'l' && !cube.left.isEngagedY() && !cube.left.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[ 18 ] = swap[  0 ]
+						cube.cubelets[  9 ] = swap[  3 ]
+						cube.cubelets[  0 ] = swap[  6 ]
+						cube.cubelets[ 21 ] = swap[  9 ]
+						cube.cubelets[  3 ] = swap[ 15 ]
+						cube.cubelets[ 24 ] = swap[ 18 ]
+						cube.cubelets[ 15 ] = swap[ 21 ]
+						cube.cubelets[  6 ] = swap[ 24 ]
+					}
+					if( degrees === undefined ) degrees = cube.left.getDistanceToPeg( 'X' )
+					cube.left.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.left.cubelets.length - 1 ) {
+							cubelet.rotate( 'X', degrees, 'l', local )
+						} else {
+							cubelet.rotate( 'X', degrees, undefined, local )
+						}
+					})
+				}
+				
+
+				//  Y-axis rotations
+		
+				if( command === 'Y' && !cube.isEngagedX() && !cube.isEngagedZ() ){
+			
+					onTwistComplete = function( swap ){
+
+						cube.cubelets = [
+
+							swap[  2 ], swap[ 11 ], swap[ 20 ],
+							swap[  5 ], swap[ 14 ], swap[ 23 ],
+							swap[  8 ], swap[ 17 ], swap[ 26 ],
+
+							swap[  1 ], swap[ 10 ], swap[ 19 ],
+							swap[  4 ], swap[ 13 ], swap[ 22 ],
+							swap[  7 ], swap[ 16 ], swap[ 25 ],
+
+							swap[  0 ], swap[  9 ], swap[ 18 ],
+							swap[  3 ], swap[ 12 ], swap[ 21 ],
+							swap[  6 ], swap[ 15 ], swap[ 24 ]
+						]
+					}
+					if( degrees === undefined ) degrees = cube.getDistanceToPeg( 'Y' )
+					cube.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.cubelets.length - 1 ) cubelet.rotate( 'Y', degrees, onTwistComplete )
+						else cubelet.rotate( 'Y', degrees )
+					})
+				}
+				else if( command === 'y' && !cube.isEngagedX() && !cube.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets = [
+
+							swap[ 18 ], swap[  9 ], swap[  0 ],
+							swap[ 21 ], swap[ 12 ], swap[  3 ],
+							swap[ 24 ], swap[ 15 ], swap[  6 ],
+
+							swap[ 19 ], swap[ 10 ], swap[  1 ],
+							swap[ 22 ], swap[ 13 ], swap[  4 ],
+							swap[ 25 ], swap[ 16 ], swap[  7 ],
+
+							swap[ 20 ], swap[ 11 ], swap[  2 ],
+							swap[ 23 ], swap[ 14 ], swap[  5 ],
+							swap[ 26 ], swap[ 17 ], swap[  8 ]
+						]
+					}
+					if( degrees === undefined ) degrees = cube.getDistanceToPeg( 'y' )
+					cube.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.cubelets.length - 1 ) cubelet.rotate( 'y', degrees, onTwistComplete )
+						else cubelet.rotate( 'y', degrees )
+					})
+				}
+				else if( command === 'U' && !cube.up.isEngagedX() && !cube.up.isEngagedZ() ){
+					
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[ 18 ] = swap[  0 ]
+						cube.cubelets[ 19 ] = swap[  9 ]
+						cube.cubelets[ 20 ] = swap[ 18 ]
+						cube.cubelets[  9 ] = swap[  1 ]
+						cube.cubelets[ 11 ] = swap[ 19 ]
+						cube.cubelets[  0 ] = swap[  2 ]
+						cube.cubelets[  1 ] = swap[ 11 ]
+						cube.cubelets[  2 ] = swap[ 20 ]
+					}					
+					if( degrees === undefined ) degrees = cube.up.getDistanceToPeg( 'Y' )
+					cube.up.cubelets.forEach( function( cubelet, i ){
+						
+						if( i === cube.up.cubelets.length - 1 ) {
+							cubelet.rotate( 'Y', degrees, 'U', local  )
+						} else { 
+							cubelet.rotate( 'Y', degrees, undefined, local  )
+						}
+					})
+				}
+				else if( command === 'u' && !cube.up.isEngagedX() & !cube.up.isEngagedZ() ){
+				
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[ 18 ] = swap[ 20 ]
+						cube.cubelets[ 19 ] = swap[ 11 ]
+						cube.cubelets[ 20 ] = swap[  2 ]
+						cube.cubelets[  9 ] = swap[ 19 ]
+						cube.cubelets[ 11 ] = swap[  1 ]
+						cube.cubelets[  0 ] = swap[ 18 ]
+						cube.cubelets[  1 ] = swap[  9 ]
+						cube.cubelets[  2 ] = swap[  0 ]
+					}
+					if( degrees === undefined ) degrees = cube.up.getDistanceToPeg( 'y' )
+					cube.up.cubelets.forEach( function( cubelet, i ){
+						
+						if( i === cube.up.cubelets.length - 1 ) {
+							cubelet.rotate( 'y', degrees, 'u', local  )
+						} else {
+							cubelet.rotate( 'y', degrees, undefined, local  )
+						}
+					})
+				}
+				else if( command === 'E' && !cube.equator.isEngagedX() && !cube.equator.isEngagedZ() ){
+					
+					onTwistComplete = function( swap ){
+					
+						cube.cubelets[ 21 ] = swap[ 23 ]
+						cube.cubelets[ 22 ] = swap[ 14 ]
+						cube.cubelets[ 23 ] = swap[  5 ]
+						cube.cubelets[ 12 ] = swap[ 22 ]
+						cube.cubelets[ 14 ] = swap[  4 ]
+						cube.cubelets[  3 ] = swap[ 21 ]
+						cube.cubelets[  4 ] = swap[ 12 ]
+						cube.cubelets[  5 ] = swap[  3 ]
+					}
+					if( degrees === undefined ) degrees = cube.equator.getDistanceToPeg( 'y' )
+					cube.equator.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.equator.cubelets.length - 1 ) cubelet.rotate( 'y', degrees, onTwistComplete )
+						else cubelet.rotate( 'y', degrees )
+					})
+				}
+				else if( command === 'e' && !cube.equator.isEngagedX() && !cube.equator.isEngagedZ() ){
+					
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[ 21 ] = swap[  3 ]
+						cube.cubelets[ 22 ] = swap[ 12 ]
+						cube.cubelets[ 23 ] = swap[ 21 ]
+						cube.cubelets[ 12 ] = swap[  4 ]
+						cube.cubelets[ 14 ] = swap[ 22 ]
+						cube.cubelets[  3 ] = swap[  5 ]
+						cube.cubelets[  4 ] = swap[ 14 ]
+						cube.cubelets[  5 ] = swap[ 23 ]
+					}
+					if( degrees === undefined ) degrees = cube.equator.getDistanceToPeg( 'Y' )
+					cube.equator.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.equator.cubelets.length - 1 ) cubelet.rotate( 'Y', degrees, onTwistComplete )
+						else cubelet.rotate( 'Y', degrees )
+					})
+				}
+				else if( command === 'D' && !cube.down.isEngagedX() && !cube.down.isEngagedZ() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  6 ] = swap[ 24 ]
+						cube.cubelets[  7 ] = swap[ 15 ]
+						cube.cubelets[  8 ] = swap[  6 ]
+						cube.cubelets[ 15 ] = swap[ 25 ]
+						cube.cubelets[ 17 ] = swap[  7 ]
+						cube.cubelets[ 24 ] = swap[ 26 ]
+						cube.cubelets[ 25 ] = swap[ 17 ]
+						cube.cubelets[ 26 ] = swap[  8 ]
+					}
+					if( degrees === undefined ) degrees = cube.down.getDistanceToPeg( 'y' )
+					cube.down.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.down.cubelets.length - 1 ) {
+							cubelet.rotate( 'y', degrees, 'D', local  )
+						} else {
+							cubelet.rotate( 'y', degrees, undefined, local  )
+						}
+					})
+				}
+				else if( command === 'd' && !cube.down.isEngagedX() && !cube.down.isEngagedZ() ){
+					
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  6 ] = swap[  8 ]
+						cube.cubelets[  7 ] = swap[ 17 ]
+						cube.cubelets[  8 ] = swap[ 26 ]
+						cube.cubelets[ 15 ] = swap[  7 ]
+						cube.cubelets[ 17 ] = swap[ 25 ]
+						cube.cubelets[ 24 ] = swap[  6 ]
+						cube.cubelets[ 25 ] = swap[ 15 ]
+						cube.cubelets[ 26 ] = swap[ 24 ]
+					}
+					if( degrees === undefined ) degrees = cube.down.getDistanceToPeg( 'Y' )
+					cube.down.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.down.cubelets.length - 1 ) {
+							cubelet.rotate( 'Y', degrees,  'd', local )
+						} else {
+							cubelet.rotate( 'Y', degrees,  undefined, local )
+						}
+					})
+				}
+
+
+				//  Z-axis rotations
+
+				if( command === 'Z' && !cube.isEngagedX() && !cube.isEngagedY() ){
+			
+					onTwistComplete = function( swap ){
+						
+						cube.cubelets = [
+
+							swap[  6 ], swap[  3 ], swap[  0 ],
+							swap[  7 ], swap[  4 ], swap[  1 ],
+							swap[  8 ], swap[  5 ], swap[  2 ],
+
+							swap[ 15 ], swap[ 12 ], swap[  9 ],
+							swap[ 16 ], swap[ 13 ], swap[ 10 ],
+							swap[ 17 ], swap[ 14 ], swap[ 11 ],
+
+							swap[ 24 ], swap[ 21 ], swap[ 18 ],
+							swap[ 25 ], swap[ 22 ], swap[ 19 ],
+							swap[ 26 ], swap[ 23 ], swap[ 20 ]
+						]
+					}
+					if( degrees === undefined ) degrees = cube.getDistanceToPeg( 'Z' )
+					cube.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.cubelets.length - 1 ) cubelet.rotate( 'Z', degrees, onTwistComplete )
+						else cubelet.rotate( 'Z', degrees )
+					})
+				}
+				else if( command === 'z' && !cube.isEngagedX() && !cube.isEngagedY() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets = [
+
+							swap[  2 ], swap[  5 ], swap[  8 ],
+							swap[  1 ], swap[  4 ], swap[  7 ],
+							swap[  0 ], swap[  3 ], swap[  6 ],
+
+							swap[ 11 ], swap[ 14 ], swap[ 17 ],
+							swap[ 10 ], swap[ 13 ], swap[ 16 ],
+							swap[  9 ], swap[ 12 ], swap[ 15 ],
+
+							swap[ 20 ], swap[ 23 ], swap[ 26 ],
+							swap[ 19 ], swap[ 22 ], swap[ 25 ],
+							swap[ 18 ], swap[ 21 ], swap[ 24 ]
+						]
+					}
+					if( degrees === undefined ) degrees = cube.getDistanceToPeg( 'z' )
+					cube.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.cubelets.length - 1 ) cubelet.rotate( 'z', degrees, onTwistComplete )
+						else cubelet.rotate( 'z', degrees )
+					})
+				}
+				else if( command === 'F' && !cube.front.isEngagedX() && !cube.front.isEngagedY() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  0 ] = swap[  6 ]
+						cube.cubelets[  1 ] = swap[  3 ]
+						cube.cubelets[  2 ] = swap[  0 ]
+						cube.cubelets[  3 ] = swap[  7 ]
+						cube.cubelets[  5 ] = swap[  1 ]
+						cube.cubelets[  6 ] = swap[  8 ]
+						cube.cubelets[  7 ] = swap[  5 ]
+						cube.cubelets[  8 ] = swap[  2 ]
+					}
+					if( degrees === undefined ) degrees = cube.front.getDistanceToPeg( 'Z' )
+					cube.front.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.front.cubelets.length - 1 )
+					 {
+						cubelet.rotate( 'Z', degrees, 'F', local)
+					 } 
+						else  {
+							cubelet.rotate( 'Z', degrees, undefined, local )
+						}
+					})
+				}
+				else if( command === 'f' && !cube.front.isEngagedX() && !cube.front.isEngagedY() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  0 ] = swap[  2 ]
+						cube.cubelets[  1 ] = swap[  5 ]
+						cube.cubelets[  2 ] = swap[  8 ]
+						cube.cubelets[  3 ] = swap[  1 ]
+						cube.cubelets[  5 ] = swap[  7 ]
+						cube.cubelets[  6 ] = swap[  0 ]
+						cube.cubelets[  7 ] = swap[  3 ]
+						cube.cubelets[  8 ] = swap[  6 ]
+					}
+					if( degrees === undefined ) degrees = cube.front.getDistanceToPeg( 'z' )
+					cube.front.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.front.cubelets.length - 1 ) 
+						{
+							cubelet.rotate( 'z', degrees, 'f', local) //onTwistComplete ) //
+						}
+						else {
+							cubelet.rotate( 'z', degrees, undefined, local )
+						}
+					})
+				}
+				else if( command === 'S' && !cube.standing.isEngagedX() && !cube.standing.isEngagedY() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  9 ] = swap[ 15 ]
+						cube.cubelets[ 10 ] = swap[ 12 ]
+						cube.cubelets[ 11 ] = swap[  9 ]
+						cube.cubelets[ 12 ] = swap[ 16 ]
+						cube.cubelets[ 14 ] = swap[ 10 ]
+						cube.cubelets[ 15 ] = swap[ 17 ]
+						cube.cubelets[ 16 ] = swap[ 14 ]
+						cube.cubelets[ 17 ] = swap[ 11 ]
+					}
+					if( degrees === undefined ) degrees = cube.standing.getDistanceToPeg( 'Z' )
+					cube.standing.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.standing.cubelets.length - 1 ) cubelet.rotate( 'Z', degrees, onTwistComplete )
+						else cubelet.rotate( 'Z', degrees )
+					})
+				}
+				else if( command === 's' && !cube.standing.isEngagedX() && !cube.standing.isEngagedY() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[  9 ] = swap[ 11 ]
+						cube.cubelets[ 10 ] = swap[ 14 ]
+						cube.cubelets[ 11 ] = swap[ 17 ]
+						cube.cubelets[ 12 ] = swap[ 10 ]
+						cube.cubelets[ 14 ] = swap[ 16 ]
+						cube.cubelets[ 15 ] = swap[  9 ]
+						cube.cubelets[ 16 ] = swap[ 12 ]
+						cube.cubelets[ 17 ] = swap[ 15 ]
+					}
+					if( degrees === undefined ) degrees = cube.standing.getDistanceToPeg( 'z' )
+					cube.standing.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.standing.cubelets.length - 1 ) cubelet.rotate( 'z', degrees, onTwistComplete )
+						else cubelet.rotate( 'z', degrees )
+					})
+				}
+				else if( command === 'B' && !cube.back.isEngagedX() && !cube.back.isEngagedY() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[ 18 ] = swap[ 20 ]
+						cube.cubelets[ 19 ] = swap[ 23 ]
+						cube.cubelets[ 20 ] = swap[ 26 ]
+						cube.cubelets[ 21 ] = swap[ 19 ]
+						cube.cubelets[ 23 ] = swap[ 25 ]
+						cube.cubelets[ 24 ] = swap[ 18 ]
+						cube.cubelets[ 25 ] = swap[ 21 ]
+						cube.cubelets[ 26 ] = swap[ 24 ]
+					}
+					if( degrees === undefined ) degrees = cube.back.getDistanceToPeg( 'z' )
+					cube.back.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.back.cubelets.length - 1 ) {
+							cubelet.rotate( 'z', degrees, 'B', local  )
+						}
+						else {
+							cubelet.rotate( 'z', degrees, undefined, local  )
+						}
+					})
+				}
+				else if( command === 'b' && !cube.back.isEngagedX() && !cube.back.isEngagedY() ){
+
+					onTwistComplete = function( swap ){
+
+						cube.cubelets[ 18 ] = swap[ 24 ]
+						cube.cubelets[ 19 ] = swap[ 21 ]
+						cube.cubelets[ 20 ] = swap[ 18 ]
+						cube.cubelets[ 21 ] = swap[ 25 ]
+						cube.cubelets[ 23 ] = swap[ 19 ]
+						cube.cubelets[ 24 ] = swap[ 26 ]
+						cube.cubelets[ 25 ] = swap[ 23 ]
+						cube.cubelets[ 26 ] = swap[ 20 ]
+					}
+					if( degrees === undefined ) degrees = cube.back.getDistanceToPeg( 'Z' )
+					cube.back.cubelets.forEach( function( cubelet, i ){
+
+						if( i === cube.back.cubelets.length - 1 ) {
+							cubelet.rotate( 'Z', degrees, 'b', local  ) 
+						} else {
+							cubelet.rotate( 'Z', degrees, undefined, local  )
+						}
+					})
+				}
+
+
+				//@@  COME BACK AND BETTER DOCUMENT WHAT'S HAPPENING HERE!
+
+
+				if( onTwistComplete instanceof Function ){
+
+					twist.completed = Date.now()
+					// $( '#twist' ).text( command ).fadeIn( 50, function(){ 
+
+					// 	var that = this
+					// 	setTimeout( function(){
+
+					// 		$( that ).fadeOut( 500 )
+						
+					// 	}, 50 )
+					// })				
+				}
+				else console.log( '! Received a twist command ('+ command +'), however some of the required Cubelets are currently engaged.' )
+			}
+			else if( Cube.verbosity >= 0.1 ) console.log( '! Received an invalid twist command: '+ command +'.' )
+		},
+
+
+
+
+		showFaceLabels: function(){
+
+			$( '.faceLabel' ).show()
+			this.showingFaceLabels = true
+		},
+		hideFaceLabels: function(){
+
+			$( '.faceLabel' ).hide()
+			this.showingFaceLabels = false
+		},
+
+
+
+
+
+
+		    /////////////////
+		   //             //
+		  //   Presets   //
+		 //             //
+		/////////////////
+
+
+		presetBling: function(){
+
+			var cube = this
+
+			this.threeObject.position.y = -2000
+			new TWEEN.Tween( this.threeObject.position )
+				.to({ 
+					y: 0
+				}, SECOND * 2 )
+				.easing( TWEEN.Easing.Quartic.Out )
+				.start()
+			this.threeObject.rotation.set(
+				
+				( 180 ).degreesToRadians(),
+				( 180 ).degreesToRadians(),
+				(  20 ).degreesToRadians()
+			)
+			new TWEEN.Tween( this.threeObject.rotation )
+				.to({ 
+
+					x: (  25 ).degreesToRadians(), 
+					y: ( -30 ).degreesToRadians(),
+					z: 0
+
+				}, SECOND * 3 )
+				.easing( TWEEN.Easing.Quartic.Out )
+				.onComplete( function(){
+
+					cube.isReady = true
+					updateControls()
+				})
+				.start()
+			this.isReady = false
+
+			
+			//  And we want each Cubelet to begin in an exploded position and tween inward.
+
+			this.cubelets.forEach( function( cubelet ){
+	
+
+				//  We want to start with each Cubelet exploded out away from the Cube center.
+				//  We're reusing the x, y, and z we created far up above to handle Cubelet positions.
+
+				var distance = 1000
+				cubelet.anchor.position.set(
+
+					cubelet.addressX * distance,
+					cubelet.addressY * distance,
+					cubelet.addressZ * distance
+				)
+
+
+				//  Let's vary the arrival time of flying Cubelets based on their type.
+				//  An nice extra little but of sauce!
+
+				var delay
+				if( cubelet.type === 'core'   ) delay = (   0 ).random(  200 )
+				if( cubelet.type === 'center' ) delay = ( 200 ).random(  400 )
+				if( cubelet.type === 'edge'   ) delay = ( 400 ).random(  800 )
+				if( cubelet.type === 'corner' ) delay = ( 800 ).random( 1000 )
+
+
+				new TWEEN.Tween( cubelet.anchor.position )
+					.to({
+
+						x: 0,
+						y: 0,
+						z: 0
+					
+					}, SECOND )
+					.delay( delay ) 
+					.easing( TWEEN.Easing.Quartic.Out )	
+					.onComplete( function(){
+
+						cubelet.isTweening = false
+					})
+					.start()
+				
+				cubelet.isTweening = true
+			})
+			updateControls( this )
+		},
+		presetNormal: function(){
+
+			$( 'body' ).css( 'background-color', '#000' )
+			$( 'body' ).addClass( 'graydient' )
+			setTimeout( function(){ $( '.cubelet' ).removeClass( 'purty' )}, 1 )
+			this.show()
+			this.showIntroverts()
+			this.showPlastics()
+			this.showStickers()
+			this.hideTexts()
+			this.hideWireframes()
+			this.hideIds()
+			this.setOpacity()
+			this.setRadius()
+			updateControls( this )
+		},
+		presetText: function( virgin ){
+
+			$( 'body' ).css( 'background-color', '#F00' )
+			$( 'body' ).removeClass( 'graydient' )
+			setTimeout( function(){ $( '.cubelet' ).removeClass( 'purty' )}, 1 )
+
+			var cube = this
+
+			setTimeout( function(){
+	
+				cube.show()
+				cube.hidePlastics()
+				cube.hideStickers()
+				cube.hideIds()
+				cube.hideIntroverts()
+				cube.showTexts()
+				cube.hideWireframes()
+				cube.setOpacity()
+				updateControls( cube )
+			
+			}, 1 )
+		},
+		presetLogo: function(){
+
+			var cube = this
+
+			this.isReady = false
+			this.presetText()			
+			new TWEEN.Tween( cube.threeObject.rotation )
+			.to({ 
+				x: 0,
+				y: ( -45 ).degreesToRadians(),
+				z: 0
+			}, SECOND * 2 )
+			.easing( TWEEN.Easing.Quartic.Out )
+			.onComplete( function(){
+
+				updateControls( cube )
+				cube.isReady = true
+				cube.twistQueue.add( 'E20d17' )
+			})
+			.start()
+		},
+		presetTextAnimate: function(){//  Specifically for Monica!
+
+			var 
+			delay = 1,//SECOND * 2,
+			twistDurationScaled = Math.max([ (20+90).absolute().scale( 0, 90, 0, cube.twistDuration ), 250 ])
+			_this = this
+
+			cube.shuffleMethod = cube.ALL_SLICES
+			presetHeroic( virgin )
+			setTimeout( function(){ 
+
+				_this.twist( 'E', 20 )
+			}, delay )
+			setTimeout( function(){ 
+
+				_this.twist( 'd', 20 )
+				//$('body').css('background-color', '#000')
+			}, delay + SECOND )
+			setTimeout( function(){
+
+				_this.twist( 'D', 20 + 90 )		
+				_this.isRotating = true
+			}, delay + SECOND * 2 )
+			setTimeout( function(){
+
+				_this.twist( 'e', 20 + 90 )
+				_this.isShuffling = true
+			}, delay + SECOND * 2 + twistDurationScaled + 50 )
+			updateControls( this )
+		},
+		presetWireframe: function( included, excluded ){
+
+			setTimeout( function(){ $( '.cubelet' ).removeClass( 'purty' )}, 1 )
+			this.showIntroverts()
+			if( included === undefined ) included = new Group( this.cubelets )
+			if( excluded === undefined ){
+
+				excluded = new Group( this.cubelets )
+				excluded.remove( included )
+			}						
+			this.show()		
+			excluded.showPlastics()
+			excluded.showStickers()
+			excluded.hideWireframes()
+			included.hidePlastics()
+			included.hideStickers()
+			included.showWireframes()
+			updateControls( this )
+		},
+		presetHighlight: function( included, excluded ){
+
+			if( erno.state === 'setup' ) this.presetBling()
+			if( included === undefined ) included = new Group( this.cubelets )
+			if( excluded === undefined ){
+
+				excluded = new Group( this.cubelets )
+				excluded.remove( included )
+			}
+			excluded.setOpacity( 0.1 )
+			included.setOpacity()
+			updateControls( this )
+		},
+		presetHighlightCore: function(){
+
+			this.presetHighlight( this.core )
+			updateControls( this )
+		},
+		presetHighlightCenters: function(){
+
+			this.presetHighlight( this.centers )
+			updateControls( this )
+		},
+		presetHighlightEdges: function(){
+
+			this.presetHighlight( this.edges )
+			updateControls( this )
+		},
+		presetHighlightCorners: function(){
+
+			this.presetHighlight( this.corners )
+			updateControls( this )
+		},
+		presetHighlightWhite: function(){
+
+			this.presetHighlight( this.hasColor( WHITE ))
+			updateControls( this )
+		},
+		presetPurty: function(){
+
+			this.showIntroverts()
+			setTimeout( function(){ 
+				
+				$( '.cubelet' ).addClass( 'purty' )
+
+			}, 1 )
+			this.threeObject.rotation.set(
+
+				( 35.3).degreesToRadians(),
+				(-45  ).degreesToRadians(),
+				   0
+			)
+			updateControls( this )
+		},
+		presetDemo: function(){
+
+			var 
+			cube  = this,
+			loops = 0,
+			captions = $( '#captions' )
+
+			this.taskQueue.add(
+
+
+				//  Rotation and twist demo.
+
+				function(){
+
+					cube.rotationDeltaX = -0.1
+					cube.rotationDeltaY = 0.15
+					cube.isRotating = true
+					cube.presetNormal()
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.twistQueue.add( 'rdRD'.multiply( 6 ))
+				},
+
+
+				//  Opacity demo.
+				
+				function(){
+
+					cube.back.setOpacity( 0.2 )
+					cube.taskQueue.isReady = false					
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.standing.setOpacity( 0.2 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.twistQueue.add( 'rdRD'.multiply( 3 ))
+				},
+				function(){
+
+					cube.showFaceLabels()
+					cube.twistQueue.add( 'rdRD'.multiply( 3 ))
+				},
+				function(){
+
+					cube.hideFaceLabels()
+					cube.standing.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.back.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+
+
+				//  Radial demo.
+
+				function(){
+
+					cube.down.setRadius( 90 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.equator.setRadius( 90 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.up.setRadius( 90 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.twistQueue.add( 'rdRD'.multiply( 2 ))
+				},
+				function(){
+
+					cube.back.setRadius()
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.standing.setRadius()
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.twistQueue.add( 'rdRD'.multiply( 2 ))
+				},
+				function(){
+
+					var 
+					excluded = new Group( cube.cubelets ),
+					included = cube.hasColors( RED, YELLOW, BLUE )
+
+					excluded.remove( included )
+					excluded.setRadius()
+					excluded.setOpacity( 0.5 )
+					included.setRadius( 120 )
+					included.setOpacity( 1 )
+
+					cube.back.setRadius()
+					cube.showIds()
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, (6).seconds() )
+				},
+				function(){
+
+					cube.twistQueue.add( 'rdRD'.multiply( 2 ))
+				},
+				function(){
+
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, (6).seconds() )
+				},
+				function(){
+
+					cube.setRadius()
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, (3).seconds() )
+				},
+
+
+				//  A cube is made up of cubelets
+				//  and these can be a core or centers, edges, and corners.
+
+				function(){
+					
+					captions.text( 'Core' ).fadeIn()
+					cube.presetHighlightCore()
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )					
+				},
+				function(){
+					
+					cube.showIds()
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, (2).seconds() )	
+				},
+				function(){
+
+					cube.twistQueue.add( 'rdRD'.multiply( 2 ))
+				},
+				function(){
+
+					captions.text( 'Centers' )
+					cube.presetHighlightCenters()
+					cube.twistQueue.add( 'rdRD'.multiply( 4 ))
+				},
+				function(){
+
+					captions.text( 'Edges' )
+					cube.presetHighlightEdges()
+					cube.twistQueue.add( 'rdRD'.multiply( 3 ))
+				},
+				function(){
+
+					captions.text( 'Corners' )
+					cube.presetHighlightCorners()
+					cube.twistQueue.add( 'rdRD'.multiply( 3 ))
+				},
+				function(){
+					
+					captions.fadeOut()
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, (2).seconds() )	
+				},
+
+
+				//  Wireframe demo.
+				
+				function(){
+
+					cube.left.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.left
+						.hidePlastics()
+						.hideStickers()
+						.showWireframes()
+						.showIds()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.middle.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+					
+					cube.middle
+						.hidePlastics()
+						.hideStickers()
+						.showWireframes()
+						.showIds()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.right.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.right
+						.hidePlastics()
+						.hideStickers()
+						.showWireframes()
+						.showIds()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.twistQueue.add( 'rdRD'.multiply( 3 ))
+				},
+
+
+				//  Text demo.
+
+				function(){
+
+					cube.left.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.left
+						.hidePlastics()
+						.hideStickers()
+						.hideWireframes()
+						.hideIds()
+						.showTexts()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.middle.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+					
+					cube.middle
+						.hidePlastics()
+						.hideStickers()
+						.hideWireframes()
+						.hideIds()
+						.showTexts()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.right.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.right
+						.hidePlastics()
+						.hideStickers()
+						.hideWireframes()
+						.hideIds()
+						.showTexts()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.twistQueue.add( 'rdRD'.multiply( 3 ))
+				},
+				function(){
+
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND * 8 )
+				},
+
+
+				//  Return to Normal mode
+
+				function(){
+
+					cube.left.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.left
+						.showPlastics()
+						.showStickers()
+						.hideTexts()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.middle.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+					
+					cube.middle
+						.showPlastics()
+						.showStickers()
+						.hideTexts()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.right.setOpacity( 0 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+				function(){
+
+					cube.right
+						.showPlastics()
+						.showStickers()
+						.hideTexts()
+						.setOpacity( 1 )
+					cube.taskQueue.isReady = false
+					setTimeout( function(){ cube.taskQueue.isReady = true }, SECOND )
+				},
+
+
+				//  Loop it.
+
+				function(){
+
+					loops ++
+					console.log( 'The cuber demo has completed', loops, 'loops.' )
+					cube.twistQueue.history = []//  Lets just kill it outright.
+				}
+			)
+			this.taskQueue.isLooping = true
+			updateControls( this )
+		},
+		presetDemoStop: function(){
+
+			this.taskQueue.isLooping = false
+			this.twistQueue.empty()
+			this.taskQueue.empty()
+			this.isRotating = false
+			updateControls( this )
+		},
+
+
+
+
+
+
+
+
+		//  Shuffle methods.
+
+		PRESERVE_LOGO: 'RrLlUuDdSsBb',            //  Preserve the logo position and rotation.
+		ALL_SLICES:    'RrMmLlUuEeDdFfSsBb',      //  Allow all slices to rotate.
+		EVERYTHING:    'XxRrMmLlYyUuEeDdZzFfSsBb',//  Allow all slices, and also full cube X, Y, and Z rotations.
+
+
+		//  The cube does its own loopage.
+		//  It attempts to execute twists in the twistQueue
+		//  and then tasks in the taskQueue.
+		//  This is how shuffling and solving are handled.
+
+		loop: function(){
+
+			if( cube.isRotating ){
+
+				cube.threeObject.rotation.x += cube.rotationDeltaX.degreesToRadians()
+				cube.threeObject.rotation.y += cube.rotationDeltaY.degreesToRadians()
+				cube.threeObject.rotation.z += cube.rotationDeltaZ.degreesToRadians()
+				updateControls()
+			}
+
+
+			//  If the Cube is "ready"
+			//  and not a single cubelet is currently tweening
+			//  regardless of it's resting state (engagement;
+			//  meanging it could in theory not be tweening but
+			//  has come to rest at where rotation % 90 !== 0.
+
+			if( cube.isReady && !cube.isTweening() ){
+	
+				$( '#cubeIsTweening' ).fadeOut( 100 )
+				if( cube.twistQueue.isReady ){
+
+
+					//  We have zero twists in the queue
+					//  so perhaps we'd like to add some?
+
+					if( cube.twistQueue.future.length === 0 ){
+
+						$( '#cubeHasTwistsQueued' ).fadeOut( 100 )
+
+
+						//  If the Cube ought to be shuffling then
+						//  add a random command to the twist queue.
+
+						if( cube.isShuffling ){
+
+							cube.twistQueue.add( cube.shuffleMethod[ cube.shuffleMethod.length.rand() ])
+						}
+						
+						//  If the cube ought to be solving and a solver exists
+						//  and we're not shuffling, tweening, etc.
+
+						else if( cube.isSolving && window.solver ){
+
+							cube.isSolving = window.solver.consider( cube )
+						}
+
+						//  If we are doing absolutely nothing else
+						//  then we can can try executing a task.
+
+						else if( cube.taskQueue.isReady === true ){
+
+							var task = cube.taskQueue.do()
+							if( task instanceof Function ) task()
+						}					 
+					}
+
+					//  Otherwise, we have some twists in the queue
+					//  and we should put everything else aside and tend to those.
+
+					else {
+						
+						cube.twist( cube.twistQueue.do() )
+						if( cube.twistQueue.future.length > 0 ) $( '#cubeHasTwistsQueued' ).fadeIn( 100 )
+					}
+
+
+				}// cube.twistQueue.isReady
+			}
+			else if( cube.isTweening ){
+
+				$( '#cubeIsTweening' ).fadeIn( 100 )
+			}
+		}// loop: function()
+
+
+
+
+	})
+})
+
+
+

+ 247 - 0
public/drivers/model/rubik/lib/directions.js

@@ -0,0 +1,247 @@
+/*
+
+
+	DIRECTIONS
+
+	We have six Directions which we map in a spiral around a cube: front, up,
+	right, down, left, and back. That's nice on its own but what's important 
+	is the relationships between faces. For example, What's to the left of the
+	Front face? Well that depends on what the Front faace considers "up" to 
+	be. The Direction class handles these relationships and calculates clock-
+	wise and anticlockwise relationships.
+
+
+	                 ------------- 
+	                |             |
+	                |      0      |   opposite
+	                |             |
+	                |    getUp()  |
+	                |             |
+	   ------------- ------------- ------------- 
+	  |             |             |             |
+	  |      3      |             |      1      |
+	  |             |             |             |
+	  |  getLeft()  |    this     |  getRight() |
+	  |             |             |             |
+	   ------------- ------------- ------------- 
+	                |             |
+	                |      2      |
+	                |             |
+	                |  getDown()  |
+	                |             |
+	                 ------------- 
+
+
+	The following equalities demonstrate how Directions operate:
+
+	  FRONT.getOpposite() === BACK
+	  FRONT.getUp() === UP
+	  FRONT.getUp( LEFT ) === LEFT
+	  FRONT.getRight() === RIGHT
+	  FRONT.getRight( DOWN ) === LEFT
+	  FRONT.getClockwise() === RIGHT
+	  FRONT.getClockwise( RIGHT ) === DOWN
+
+	  RIGHT.getOpposite() === LEFT
+	  RIGHT.getUp() === UP
+	  RIGHT.getUp( FRONT ) === FRONT
+	  RIGHT.getRight() === BACK
+	  RIGHT.getRight( DOWN ) === FRONT
+	  RIGHT.getClockwise() === BACK
+	  RIGHT.getClockwise( FRONT ) === UP
+
+
+	Keep in mind that a direction cannot use itself or its opposite as the
+	normalized up vector when seeking a direction!
+
+	  RIGHT.getUp( RIGHT ) === null
+	  RIGHT.getUp( LEFT  ) === null
+
+
+*/
+
+
+
+
+
+
+
+
+export function Direction( id, name ){
+
+	this.id        = id
+	this.name      = name.toLowerCase()
+	this.initial   = name.substr( 0, 1 ).toUpperCase()
+	this.neighbors = []
+	this.opposite  = null
+}
+Direction.prototype.setRelationships = function( up, right, down, left, opposite ){
+
+	this.neighbors = [ up, right, down, left ]
+	this.opposite  = opposite
+}
+
+
+
+
+Direction.getNameById = function( id ){
+
+	return [
+
+		'front',
+		'up',
+		'right',
+		'down',
+		'left',
+		'back'
+
+	][ id ]
+}
+Direction.getIdByName = function( name ){
+
+	return {
+
+		front: 0,
+		up   : 1,
+		right: 2,
+		down : 3,
+		left : 4,
+		back : 5
+
+	}[ name ]
+}
+Direction.getDirectionById = function( id ){
+
+	return [
+
+		FRONT,
+		UP,
+		RIGHT,
+		DOWN,
+		LEFT,
+		BACK
+
+	][ id ]
+}
+Direction.getDirectionByInitial = function( initial ){
+
+	return {
+
+		F: FRONT,
+		U: UP,
+		R: RIGHT,
+		D: DOWN,
+		L: LEFT,
+		B: BACK
+
+	}[ initial.toUpperCase() ]
+}
+Direction.getDirectionByName = function( name ){
+
+	return {
+
+		front: FRONT,
+		up   : UP,
+		right: RIGHT,
+		down : DOWN,
+		left : LEFT,
+		back : BACK
+
+	}[ name.toLowerCase() ]
+}
+
+
+
+
+//  If we're looking at a particular face 
+//  and we designate an adjacet side as up
+//  then we can calculate what adjacent side would appear to be up
+//  if we rotated clockwise or anticlockwise.
+
+Direction.prototype.getRotation = function( vector, from, steps ){
+
+	if( from === undefined ) from = this.neighbors[ 0 ]
+	if( from === this || from === this.opposite ) return null
+	steps = steps === undefined ? 1 : steps.modulo( 4 )
+	for( var i = 0; i < 5; i ++ ){
+
+		if( this.neighbors[ i ] === from ) break
+	}
+	return this.neighbors[ i.add( steps * vector ).modulo( 4 )]
+}
+Direction.prototype.getClockwise = function( from, steps ){
+
+	return this.getRotation( +1, from, steps )
+}
+Direction.prototype.getAnticlockwise = function( from, steps ){
+
+	return this.getRotation( -1, from, steps )
+}
+
+
+//  Similar to above,
+//  if we're looking at a particular face 
+//  and we designate an adjacet side as up
+//  we can state what sides appear to be to the up, right, down, and left
+//  of this face.
+
+Direction.prototype.getDirection = function( direction, up ){
+
+	return this.getRotation( 1, up, direction.id - 1 )
+}
+Direction.prototype.getUp = function( up ){
+
+	return this.getDirection( UP, up )
+}
+Direction.prototype.getRight = function( up ){
+
+	return this.getDirection( RIGHT, up )
+}
+Direction.prototype.getDown = function( up ){
+
+	return this.getDirection( DOWN, up )
+}
+Direction.prototype.getLeft = function( up ){
+
+	return this.getDirection( LEFT, up )
+}
+
+
+
+//  An convenience method that mimics the verbiage
+//  of the getRotation() and getDirection() methods.
+
+Direction.prototype.getOpposite = function(){
+
+	return this.opposite
+}
+
+
+
+
+//  Create facing directions as global constants this way we can access from 
+//  anywhere in any scope without big long variables names full of dots and 
+//  stuff. Sure, ES5 doesn't really have constants but the all-caps alerts you
+//	to the fact that them thar variables ought not to be messed with.
+
+var 
+FRONT = new Direction( 0, 'front' ),
+UP    = new Direction( 1, 'up'    ),
+RIGHT = new Direction( 2, 'right' ),
+DOWN  = new Direction( 3, 'down'  ),
+LEFT  = new Direction( 4, 'left'  ),
+BACK  = new Direction( 5, 'back'  )
+
+
+//  Now that they all exist we can 
+//  establish their relationships to one another.
+
+FRONT.setRelationships( UP,    RIGHT, DOWN,  LEFT,  BACK  )
+UP.setRelationships(    BACK,  RIGHT, FRONT, LEFT,  DOWN  )
+RIGHT.setRelationships( UP,    BACK,  DOWN,  FRONT, LEFT  )
+DOWN.setRelationships(  FRONT, RIGHT, BACK,  LEFT,  UP    )
+LEFT.setRelationships(  UP,    FRONT, DOWN,  BACK,  RIGHT )
+BACK.setRelationships(  UP,    LEFT,  DOWN,  RIGHT, FRONT )
+
+
+

+ 102 - 0
public/drivers/model/rubik/lib/folds.js

@@ -0,0 +1,102 @@
+/*
+
+
+	FOLDS
+
+	Folds are two adjacent Faces joined together, as if one
+	long 6 x 3 strip has been folding down the center and
+	three such shapes together wrap the six sides of the Cube.
+	Currently this is important for text wrapping. And in the
+	future? Who knows. Characters in a String are mapped thus:
+	
+
+               LEFT FACE
+                                         RIGHT FACE
+       -------- -------- -------- 
+      |        |        |        |-------- -------- -------- 
+      |    0   |    1   |    2   |        |        |        |
+      |        |        |        |    3   |    4   |    5   |
+       -------- -------- --------         |        |        |
+      |        |        |        |-------- -------- -------- 
+      |    6   |    7   |    8   |        |        |        |
+      |        |        |        |    9   |   10   |   11   |
+       -------- -------- --------         |        |        |
+      |        |        |        |-------- -------- -------- 
+      |   12   |   13   |   14   |        |        |        |
+      |        |        |        |   15   |   16   |   17   |
+       -------- -------- --------         |        |        |
+                                  -------- -------- -------- 
+
+                                 ^
+                                 |
+
+                             FOLD LINE
+
+
+	Currently Folds are only intended to be created and
+	heroized after the first Cube mapping. After the Cube
+	twists things would get rather weird...
+
+
+*/
+
+
+
+
+
+
+
+
+export function Fold( left, right ){
+
+	this.map = [
+
+		left.northWest[  left.face  ].text,
+		left.north[      left.face  ].text,
+		left.northEast[  left.face  ].text,
+		right.northWest[ right.face ].text,
+		right.north[     right.face ].text,
+		right.northEast[ right.face ].text,
+
+		left.west[       left.face  ].text,
+		left.origin[     left.face  ].text,
+		left.east[       left.face  ].text,
+		right.west[      right.face ].text,
+		right.origin[    right.face ].text,
+		right.east[      right.face ].text,
+
+		left.southWest[  left.face  ].text,
+		left.south[      left.face  ].text,
+		left.southEast[  left.face  ].text,
+		right.southWest[ right.face ].text,
+		right.south[     right.face ].text,
+		right.southEast[ right.face ].text
+	]
+}
+
+
+
+
+Fold.prototype.getText = function(){
+
+	var text = ''
+
+	this.map.forEach( function( element ){
+
+		text += element.innerHTML
+	})
+	return text
+}
+Fold.prototype.setText = function( text ){
+
+	var i
+
+	text = text.justifyLeft( 18 )
+	for( i = 0; i < 18; i ++ ){
+
+		this.map[ i ].innerHTML = text.substr( i, 1 )
+	}
+}
+
+
+

+ 395 - 0
public/drivers/model/rubik/lib/groups.js

@@ -0,0 +1,395 @@
+/*
+
+
+	GROUPS
+
+	Groups are collections of an arbitrary number of Cubelets.
+	They have no concept of Cubelet location or orientation
+	and therefore are not capable of rotation around any axis.
+
+
+*/
+
+
+
+
+
+
+
+
+export function Group(){
+
+	this.cubelets = []
+	this.add( Array.prototype.slice.call( arguments ))
+}
+
+
+
+
+globalThis.setupTasks = globalThis.setupTasks || []
+globalThis.setupTasks.push( function(){
+
+	globalThis.augment( Group, {
+
+		inspect: function( face ){
+
+			this.cubelets.forEach( function( cubelet ){
+
+				cubelet.inspect( face )
+			})
+			return this
+		},
+		add: function(){
+
+			var 
+			cubeletsToAdd = Array.prototype.slice.call( arguments ),
+			that = this
+
+			cubeletsToAdd.forEach( function( cubelet ){
+
+				if( cubelet instanceof Group ) cubelet = cubelet.cubelets
+				if( cubelet instanceof Array ) that.add.apply( that, cubelet )
+				else that.cubelets.push( cubelet )
+			})
+			return this
+		},
+		remove: function( cubeletToRemove ){
+
+			if( cubeletToRemove instanceof Group ) cubeletToRemove = cubeletToRemove.cubelets
+			if( cubeletToRemove instanceof Array ){
+
+				var that = this
+				cubeletToRemove.forEach( function( c ){
+
+					that.remove( c )
+				})
+			}
+			for( var i = this.cubelets.length - 1; i >= 0; i -- ){
+
+				if( this.cubelets[ i ] === cubeletToRemove )
+					this.cubelets.splice( i, 1 )
+			}
+			return this
+		},
+
+
+
+
+		//  Boolean checker.
+		//  Are any Cubelets in this group tweening?
+		//  Engaged on the Z axis? Etc.
+
+		isFlagged: function( property ){
+
+			var count = 0
+			this.cubelets.forEach( function( cubelet ){
+
+				count += cubelet[ property ] ? 1 : 0
+			})
+			return count
+		},
+		isTweening: function(){
+
+			return this.isFlagged( 'isTweening' )
+		},
+		isEngagedX: function(){
+
+			return this.isFlagged( 'isEngagedX' )
+		},
+		isEngagedY: function(){
+
+			return this.isFlagged( 'isEngagedY' )
+		},
+		isEngagedZ: function(){
+
+			return this.isFlagged( 'isEngagedZ' )
+		},
+		isEngaged: function(){
+
+			return this.isEngagedX() + this.isEngagedY() + this.isEngagedZ()
+		},
+
+
+
+
+		//  Search functions.
+		//  What Cubelets in this Group have a particular color?
+		//  How about all of these three colors?
+		//  And index? address? Solver uses these a lot.
+
+		hasProperty: function( property, value ){
+
+			var
+			results = new Group()
+
+			this.cubelets.forEach( function( cubelet ){
+
+				if( cubelet[ property ] === value ) results.add( cubelet )
+			})
+			return results
+		},
+		hasId: function( id ){
+
+			return this.hasProperty( 'id', id ).cubelets[ 0 ]//  expecting a single return!
+		},
+		hasAddress: function( address ){
+
+			return this.hasProperty( 'address', address ).cubelets[ 0 ]//  expecting a single return!
+		},
+		hasType: function( type ){
+
+			return this.hasProperty( 'type', type )
+		},
+		hasColor: function( color ){
+
+			var
+			results = new Group()
+
+			this.cubelets.forEach( function( cubelet ){
+
+				if( cubelet.hasColor( color )) results.add( cubelet )
+			})
+			return results
+		},
+		hasColors: function(){//  this function implies AND rather than OR, XOR, etc.
+
+			var
+			results = new Group(),
+			colors  = Array.prototype.slice.call( arguments )
+
+			this.cubelets.forEach( function( cubelet ){
+
+				if( cubelet.hasColors.apply( cubelet, colors )) results.add( cubelet )
+			})
+			return results
+		},
+
+
+
+
+		//  We needed this business in order to deal with partial rotations.
+		//  A little janky perhaps (grabbing the average, that is)
+		//  but gets the job done and allows us to do getDistanceToPeg() below.
+
+		getAverageRotation: function( axis ){
+
+			var	sum  = 0
+
+			this.cubelets.forEach( function( cubelet ){
+
+				sum += cubelet[ axis.toLowerCase() ]
+			})
+			return sum / this.cubelets.length
+		},
+		getAverageRotationX: function(){
+
+			return this.getAverageRotation( 'x' )
+		},
+		getAverageRotationY: function(){
+
+			return this.getAverageRotation( 'y' )
+		},
+		getAverageRotationZ: function(){
+
+			return this.getAverageRotation( 'z' )
+		},
+
+
+
+
+		//  What rotation degree are we on right now?
+		//  What direction are we spinning from here? (Clockwise or anti?)
+		//  Let's go in that direction until we hit degree % 90 === 0.
+		//  What's the distance from here to there?
+		//  Not the prettiest code I've ever written... Sorry.
+
+		getDistanceToPeg: function( axis ){
+
+			var
+			current   = this.getAverageRotation( axis ),
+			direction = axis.toUpperCase() === axis ? 'clockwise' : 'anticlockwise',
+			distance  = current
+				.add( 90 )
+				.divide( 90 )
+				.roundDown()
+				.multiply( 90 )
+				.subtract( current ),
+			target = current + distance
+
+			if( direction === 'anticlockwise' ){
+
+				distance -= 90
+				if( distance === 0 ) distance -= 90
+				target = current + distance
+			}
+			if( Cube.verbosity >= 0.9 ) console.log( 
+
+				'Average rotation for this group about the '+ axis.toUpperCase() +' axis:', current, 
+				'\nRotation direction:', direction,
+				'\nDistance to next peg:', distance, 
+				'\nTarget rotation:', target
+			)
+
+
+			//  We need to return the absolute() value of the distance
+			//  because the vector (direction) of the twist will be taken into account
+			//  by cube.twist() or whatever else is calling this function.
+
+			return distance.absolute()
+		},
+
+
+
+
+		//  cube.front.isSolved( 'front' )
+		//  cube.front.up.isSolved( 'up' )
+
+		isSolved: function( face ){
+
+			if( face ){
+
+				var
+				faceColors = {},
+				numberOfColors = 0
+
+				if( face instanceof Direction ) face = face.name
+				this.cubelets.forEach( function( cubelet ){
+
+					var color = cubelet[ face ].color.name
+					if( faceColors[ color ] === undefined ){
+						
+						faceColors[ color ] = 1
+						numberOfColors ++
+					}
+					else faceColors[ color ] ++
+				})
+				return numberOfColors === 1 ? true : false
+			}
+			else {
+			
+				console.warn( 'A face [String or Direction] argument must be specified when using Group.isSolved().' )
+				return false
+			}
+		},
+
+
+
+
+		//  Visual switches.
+		//  Take this group and hide all the stickers,
+		//  turn on wireframe mode, etc.
+
+		show: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.show() })
+			return this
+		},
+		hide: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.hide() })
+			return this
+		},
+		showPlastics: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.showPlastics() })
+			return this
+		},
+		hidePlastics: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.hidePlastics() })
+			return this
+		},
+		showExtroverts: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.showExtroverts() })
+			return this
+		},
+		hideExtroverts: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.hideExtroverts() })
+			return this
+		},
+		showIntroverts: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.showIntroverts() })
+			return this
+		},
+		hideIntroverts: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.hideIntroverts() })
+			return this
+		},		
+		showStickers: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.showStickers() })
+			return this
+		},
+		hideStickers: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.hideStickers() })
+			return this
+		},
+		showWireframes: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.showWireframes() })
+			return this
+		},
+		hideWireframes: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.hideWireframes() })
+			return this
+		},
+		showIds: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.showIds() })
+			return this
+		},
+		hideIds: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.hideIds() })
+			return this
+		},
+		showTexts: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.showTexts() })
+			return this
+		},
+		hideTexts: function(){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.hideTexts() })
+			return this
+		},
+
+
+
+
+		getOpacity: function(){
+
+			var avg = 0
+
+			this.cubelets.forEach( function( cubelet ){ avg += cubelet.getOpacity() })
+			return avg / this.cubelets.length
+		},
+		setOpacity: function( opacity, onComplete ){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.setOpacity( opacity, onComplete ) })
+			return this
+		},
+		getRadius: function(){
+
+			var avg = 0
+
+			this.cubelets.forEach( function( cubelet ){ avg += cubelet.getRadius() })
+			return avg / this.cubelets.length
+		},
+		setRadius: function( radius, onComplete ){
+
+			this.cubelets.forEach( function( cubelet ){ cubelet.setRadius( radius, onComplete ) })
+			return this
+		}
+
+
+
+
+	})
+})

+ 93 - 0
public/drivers/model/rubik/lib/queues.js

@@ -0,0 +1,93 @@
+/*
+
+
+	QUEUES
+
+	Queues are glorified Arrays and rather useful for things like our
+	cube.twistQueue, cube.taskQueue, etc. 
+
+
+*/
+
+
+
+
+
+
+
+
+export function Queue( validation ){
+
+
+	//  Do we want to run a validation routine on objects being stored in 
+	//  this Queue? If so you can send the function as an argument to the 
+	//  constructor or create this property later on your own.
+
+	if( validation !== undefined && validation instanceof Function ) this.validate = validation
+
+
+	//  The rest is vanilla.
+
+	this.history = []
+	this.future  = []
+	this.isReady = true
+	this.isLooping = false
+}
+
+
+
+
+//  The idea here with .add() is that .validate() will always return an Array.
+//  The reason for this is that the validator may decide it needs to add more
+//  than one element to the Queue. This allows it to do so.
+
+Queue.prototype.add = function(){
+
+	var 
+	elements = Array.prototype.slice.call( arguments ),
+	_this = this
+
+	if( this.validate !== undefined && this.validate instanceof Function ) elements = this.validate( elements )
+
+	if( elements instanceof Array ){
+	
+		elements.forEach( function( element ){
+
+			_this.future.push( element )
+		})
+	}
+}
+Queue.prototype.empty = function(){
+
+	this.future = []
+}
+Queue.prototype.do = function(){
+
+	if( this.future.length ){
+
+		var element = this.future.shift()
+		this.history.push( element )
+		return element
+	}
+	else if( this.isLooping ){
+
+		this.future  = this.history.slice()
+		this.history = []
+	}
+}
+Queue.prototype.undo = function(){
+
+	if( this.history.length ){
+		
+		var element = this.history.pop()
+		this.future.unshift( element )
+		return element
+	}
+}
+Queue.prototype.redo = function(){
+
+	this.do()
+}
+
+
+

+ 751 - 0
public/drivers/model/rubik/lib/skip.js

@@ -0,0 +1,751 @@
+
+
+//  Skip.js
+//  
+//  Make JavaScript a little warmer, a little fuzzier.
+//  
+//  Author:  Stewart Smith.
+//  Website: http://stewd.io
+//  GitHub:  http://github.com/stewdio
+//  Twitter: http://twitter.com/stewd_io
+
+
+
+
+//  Copyright (C) 2013, Stewart Smith.
+//  
+//  Permission is hereby granted, free of charge, to any person obtaining a	
+//  copy of this software and associated documentation files (the "Software"), 
+//  to deal in the Software without restriction, including without limitation 
+//  the rights to use, copy, modify, merge, publish, distribute, sublicense, 
+//  and/or sell copies of the Software, and to permit persons to whom
+//  the Software is furnished to do so, subject to the following conditions:
+//  
+//  The above copyright notice and this permission notice shall be included 
+//  in all copies or substantial portions of the Software.
+//  
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+//  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+//  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 
+//  ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+//  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+//  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+
+
+export function skipjs(){
+
+
+
+
+	globalThis.SKIP_JS = 20130821.1536
+
+
+
+
+	globalThis.forceAugment = function( type, name, data ){
+
+		var key
+		
+		if( typeof name === 'string' && data ){
+
+			type.prototype[ name ] = data
+		}
+		else if( typeof name === 'object' && !data ){
+
+			for( key in name ) forceAugment( type, key, name[ key ] )
+		}
+	}
+	globalThis.augment = function( type, name, data ){
+
+		var key
+		
+		if( typeof name === 'string' && 
+			type.prototype[ name ] === undefined &&
+			data ){
+
+			forceAugment( type, name, data )
+		}
+		else if( typeof name === 'object' && !data ){
+
+			for( key in name ) augment( type, key, name[ key ] )
+		}
+	}
+	globalThis.forceLearn = function( student, teacher ){
+
+		for( var p in teacher ){
+		
+			if( teacher.hasOwnProperty( p )){
+
+				if( teacher[ p ].constructor === Object ) 
+					student[ p ] = forceLearn( student[ p ], teacher[ p ])
+				else student[ p ] = teacher[ p ]
+			}
+		}
+		return student
+	}
+	globalThis.learn = function( student, teacher ){
+
+		for( var p in teacher ){
+		
+			if( teacher.hasOwnProperty( p ) && student[ p ] === undefined ){
+
+				if( teacher[ p ].constructor === Object ) 
+					student[ p ] = learn( student[ p ], teacher[ p ])
+				else student[ p ] = teacher[ p ]
+			}
+		}
+		return student
+	}
+	globalThis.cascade = function(){
+
+		var i, args = Array.prototype.slice.call( arguments )
+
+		for( i = 0; i < args.length; i ++ )
+			if( args[ i ] !== undefined ) return args[ i ]
+		return false
+	}
+	globalThis.coinFlip = function(){
+
+		return Math.round( Math.random() )
+	}
+	globalThis.isNumeric = function( n ){
+
+		return !isNaN( parseFloat( n )) && isFinite( n )
+	}
+	
+
+
+
+	globalThis.E       = Math.E
+	globalThis.HALF_PI = Math.PI / 2
+	globalThis.PI      = Math.PI
+	globalThis.π       = Math.PI
+	globalThis.TAU     = Math.PI * 2
+	globalThis.SQRT2   = Math.SQRT2
+	globalThis.SQRT1_2 = Math.SQRT1_2
+	globalThis.LN2     = Math.LN2
+	globalThis.LN10    = Math.LN10
+	globalThis.LOG2E   = Math.LOG2E
+	globalThis.LOG10E  = Math.LOG10E
+
+	globalThis.SECOND  = 1000
+	globalThis.MINUTE  = SECOND *  60
+	globalThis.HOUR    = MINUTE *  60
+	globalThis.DAY     = HOUR   *  24
+	globalThis.WEEK    = DAY    *   7
+	globalThis.MONTH   = DAY    *  30.4368499
+	globalThis.YEAR    = DAY    * 365.242199
+	globalThis.DECADE  = YEAR   *  10
+	globalThis.CENTURY = YEAR   * 100
+	globalThis.now     = function(){ return +Date.now() }
+
+	globalThis.MIN = Number.MIN_VALUE
+	globalThis.MAX = Number.MAX_VALUE
+
+
+
+
+	// augment( Array, {
+		
+		
+	// 	distanceTo : function( target ){
+
+	// 		var i, sum = 0
+
+	// 		if( arguments.length > 0 ) 
+	// 			target = Array.prototype.slice.call( arguments )
+	// 		if( this.length === target.length ){
+
+	// 			for( i = 0; i < this.length; i ++ )
+	// 				sum += Math.pow( target[i] - this[i], 2 )
+	// 			return Math.pow( sum, 0.5 )
+	// 		}
+	// 		else return null
+	// 	},
+	// 	first : function(){
+			
+	// 		return this[ 0 ]
+	// 	},
+	// 	last : function(){
+			
+	// 		return this[ this.length - 1 ]
+	// 	},
+	// 	maximum : function(){
+
+	// 		return Math.max.apply( null, this )
+	// 	},
+	// 	middle : function(){
+		
+	// 		return this[ Math.round(( this.length - 1 ) / 2 ) ]
+	// 	},
+	// 	minimum : function(){
+
+	// 		return Math.min.apply( null, this )
+	// 	},
+	// 	indexOf : function( obj, fromIndex ){
+
+	// 		var i, j
+
+	// 		if( fromIndex === null )
+	// 			fromIndex = 0
+	// 		else if( fromIndex < 0 )
+	// 			fromIndex = Math.max( 0, this.length + fromIndex )
+	// 		for( i = fromIndex, j = this.length; i < j; i++ )
+	// 			if( this[i] === obj ) return i
+	// 		return -1//  I'd rather return NaN, but this is more standard.
+	// 	},
+	// 	rand : function(){
+
+	// 		return this[ Math.floor( Math.random() * this.length )]
+	// 	},
+	// 	random : function(){//  Convenience here. Exactly the same as .rand().
+
+	// 		return this[ Math.floor( Math.random() * this.length )]
+	// 	},
+	// 	//  Ran into trouble here with Three.js. Will investigate....
+	// 	/*remove: function( from, to ){
+
+	// 		var rest = this.slice(( to || from ) + 1 || this.length )
+			
+	// 		this.length = from < 0 ? this.length + from : from
+	// 		return this.push.apply( this, rest )
+	// 	},*/
+	// 	shuffle : function(){
+
+	// 		var 
+	// 		copy = this,
+	// 		i = this.length, 
+	// 		j,
+	// 		tempi,
+	// 		tempj
+
+	// 		if( i == 0 ) return false
+	// 		while( -- i ){
+
+	// 			j = Math.floor( Math.random() * ( i + 1 ))
+	// 			tempi = copy[ i ]
+	// 			tempj = copy[ j ]
+	// 			copy[ i ] = tempj
+	// 			copy[ j ] = tempi
+	// 		}
+	// 		return copy
+	// 	},
+	// 	toArray : function(){
+
+	// 		return this
+	// 	},
+	// 	toHtml : function(){
+
+	// 		var i, html = '<ul>'
+
+	// 		for( i = 0; i < this.length; i ++ ){
+
+	// 			if( this[ i ] instanceof Array )
+	// 				html += this[ i ].toHtml()
+	// 			else
+	// 				html += '<li>' + this[ i ] + '</li>'
+	// 		}
+	// 		html += '</ul>'
+	// 		return html
+	// 	},
+	// 	toText : function( depth ){
+
+	// 		var i, indent, text
+
+	// 		depth = cascade( depth, 0 )
+	// 		indent = '\n' + '\t'.multiply( depth )
+	// 		text = ''
+	// 		for( i = 0; i < this.length; i ++ ){
+
+	// 			if( this[ i ] instanceof Array )
+	// 				text += indent + this[ i ].toText( depth + 1 )
+	// 			else
+	// 				text += indent + this[ i ]
+	// 		}
+	// 		return text
+	// 	}
+
+
+	// })
+
+
+
+
+	augment( Number, {
+
+
+		absolute : function(){
+
+			return Math.abs( this )
+		},
+		add : function(){
+			
+			var sum = this
+
+			Array.prototype.slice.call( arguments ).forEach( function( n ){
+
+				sum += n
+			})
+			return sum
+		},
+		arcCosine : function(){
+
+			return Math.acos( this )
+		},
+		arcSine : function(){
+
+			return Math.asin( this )
+		},
+		arcTangent : function(){
+
+			return Math.atan( this )
+		},
+		constrain : function( a, b ){
+
+			var higher, lower, c = this
+
+			b = b || 0
+			higher = Math.max( a, b )
+			lower  = Math.min( a, b )
+			c = Math.min( c, higher )
+			c = Math.max( c, lower  )
+			return c
+		},
+		cosine : function(){
+
+			return Math.cos( this )
+		},
+		degreesToDirection : function(){
+
+			var d = this % 360,
+
+			directions = [ 'N', 'NNE', 'NE', 'NEE', 'E', 'SEE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'SWW', 'W', 'NWW', 'NW', 'NNW', 'N' ]
+			return directions[ this.scale( 0, 360, 0, directions.length - 1 ).round() ]
+		},
+		degreesToRadians : function(){
+
+			return this * Math.PI / 180
+		},
+		divide : function(){
+			
+			var sum = this
+
+			Array.prototype.slice.call( arguments ).forEach( function( n ){
+
+				sum /= n
+			})
+			return sum
+		},
+		isBetween : function( a, b ){
+			
+			var 
+			min = Math.min( a, b ),
+			max = Math.max( a, b )
+			
+			return ( min <= this && this <= max )
+		},
+		lerp : function( a, b ){
+
+			return a + (b - a ) * this
+		},
+		log : function( base ){
+			
+			return Math.log( this ) / ( base === undefined ? 1 : Math.log( base ))
+		},
+		log10 : function(){
+
+			// is this more pragmatic? ---> return ( '' + this.round() ).length;
+			return Math.log( this ) / Math.LN10
+		},
+		maximum : function( n ){
+
+			return Math.max( this, n )
+		},
+		minimum : function( n ){
+
+			return Math.min( this, n )
+		},
+		modulo : function( n ){
+
+			return (( this % n ) + n ) % n
+		},
+		multiply : function(){
+			
+			var sum = this
+
+			Array.prototype.slice.call( arguments ).forEach( function( n ){
+
+				sum *= n
+			})
+			return sum
+		},
+		normalize : function( a, b ){
+
+			if( a == b ) return 1.0
+			return ( this - a ) / ( b - a )
+		},
+		raiseTo : function( exponent ){
+
+			return Math.pow( this, exponent )
+		},
+		radiansToDegrees : function(){
+
+			return this * 180 / Math.PI
+		},
+		rand : function( n ){
+
+			var min, max
+
+			if( n !== undefined ){
+
+				min = Math.min( this, n )
+				max = Math.max( this, n )
+				return min + Math.floor( Math.random() * ( max - min ))
+			}
+			return Math.floor( Math.random() * this )
+		},
+		random : function( n ){
+
+			var min, max
+
+			if( n !== undefined ){
+
+				min = Math.min( this, n )
+				max = Math.max( this, n )
+				return min + Math.random() * ( max - min )
+			}
+			return Math.random() * this
+		},
+		remainder : function( n ){
+
+			return this % n
+		},
+		round : function( decimals ){
+
+			var n  = this
+
+			decimals = decimals || 0
+			n *= Math.pow( 10, decimals )
+			n  = Math.round( n )
+			n /= Math.pow( 10, decimals )
+			return n
+		},
+		roundDown : function(){
+
+			return Math.floor( this )
+		},
+		roundUp : function(){
+
+			return Math.ceil( this )
+		},
+		scale : function( a0, a1, b0, b1 ){
+
+			var phase = this.normalize( a0, a1 )
+
+			if( b0 == b1 ) return b1
+			return b0 + phase * ( b1 - b0 )
+		},
+		sine : function(){
+
+			return Math.sin( this )
+		},
+		subtract : function(){
+			
+			var sum = this
+
+			Array.prototype.slice.call( arguments ).forEach( function( n ){
+
+				sum -= n
+			})
+			return sum
+		},
+		tangent : function(){
+
+			return Math.tan( this )
+		},
+		toArray : function(){
+
+			return [ this.valueOf() ]
+		},
+		toNumber : function(){
+
+			return this.valueOf()
+		},
+		toPaddedString : function( digits, decimals ){
+
+			//  @@ 
+			//  Need to review this later. 
+			//  Mos def not bullet proof and also doesn't handle decimals.
+			
+			var
+			i,
+			stringed = '' + this,
+			padding  = ''
+
+			digits   = digits   || 2
+			decimals = decimals || 0
+			for( i = stringed.length; i < digits; i ++ )
+				padding += '0'
+			// so what about decimals? padding to right of decimal?
+			return padding + stringed
+		},
+		toSignedString : function(){
+
+			var stringed = '' + this
+			
+			if( this >= 0 ) stringed = '+' + stringed
+			return stringed
+		},
+		toString : function(){
+
+			return ''+ this
+		},
+
+
+
+
+		//  Fun with dates:
+
+		//  ((2).months() + (3).weeks() + (5).days() + (9).hours() + MINUTE ).ago().toDate()
+		//  Sat Jun 16 2012 13:03:07 GMT-0400 (EDT)
+
+		//  ((2).months() + (3).weeks() + (5).days() + (9).hours() + MINUTE ).fromNow().toDate()
+		//  Sat Dec 08 2012 00:01:23 GMT-0500 (EST)
+
+		//  (2).days().ago().isBetween( (3).weeks().ago(), (2).hours().ago() )
+		//  true
+		
+		seconds : function(){
+
+			return this * SECOND
+		},
+		minutes : function(){
+
+			return this * MINUTE
+		},
+		hours : function(){
+
+			return this * HOUR
+		},
+		days : function(){
+
+			return this * DAY
+		},
+		weeks : function(){
+
+			return this * WEEK
+		},
+		months : function(){
+
+			return this * MONTH
+		},
+		years : function(){
+
+			return this * YEAR
+		},
+		decades : function(){
+
+			return this * DECADE
+		},
+		centuries : function(){
+
+			return this * CENTURY
+		},
+		ago : function(){
+
+			return +Date.now() - this
+		},
+		fromNow : function(){
+
+			return +Date.now() + this
+		},
+		toDate : function(){
+
+			return new Date( +this )
+		},
+
+
+		//  Both the Unix epoch and the JavaScript epoch
+		//  began on 01 January 1970 at 00:00:00 UTC.
+		//  Unix counts time in SECONDS since then.
+		//  JavaScript counts time in MILLISECONDS since then.
+		//  Also don't forget that these below are in LOCAL timezones!
+
+		unixToYear : function(){
+
+			return ( new Date( this * 1000 )).getFullYear()
+		},
+		yearToUnix : function(){
+
+			//  Pay attention: Every arg is zero-indexed
+			//  except for the day of the month, which is one-indexed!!
+			return ( new Date( this, 0, 1, 0, 0, 0, 0 )).valueOf() / 1000
+		}
+	})
+
+
+
+
+	augment( String, {
+
+
+		capitalize : function(){
+
+			return this.charAt( 0 ).toUpperCase() + this.slice( 1 )//.toLowerCase()
+		},
+		invert: function(){
+
+			var
+			s = '',
+			i
+
+			for( i = 0; i < this.length; i ++ ){
+
+				if( this.charAt( i ) === this.charAt( i ).toUpperCase()) s += this.charAt( i ).toLowerCase()
+				else s += this.charAt( i ).toUpperCase()
+			}
+			return s
+		},
+		isEmpty : function(){
+
+			return this.length === 0 ? true : false
+		},
+		justifyCenter : function( n ){
+
+			var
+			thisLeftLength  = Math.round( this.length / 2 ),
+			thisRightLength = this.length - thisLeftLength,
+			containerLeftLength  = Math.round( n / 2 ),
+			containerRightLength = n - containerLeftLength,
+			padLeftLength  = containerLeftLength  - thisLeftLength,
+			padRightLength = containerRightLength - thisRightLength,
+			centered = this
+
+			if( padLeftLength > 0 ){
+
+				while( padLeftLength -- ) centered = ' ' + centered
+			}
+			else if( padLeftLength < 0 ){
+
+				centered = centered.substr( padLeftLength * -1 )
+			}
+			if( padRightLength > 0 ){
+
+				while( padRightLength -- ) centered += ' '
+			}
+			else if( padRightLength < 0 ){
+
+				centered = centered.substr( 0, centered.length + padRightLength )
+			}
+			return centered
+		},
+		justifyLeft: function( n ){
+
+			var justified = this
+
+			while( justified.length < n ) justified = justified + ' '
+			return justified
+		},
+		justifyRight: function( n ){
+
+			var justified = this
+
+			while( justified.length < n ) justified = ' ' + justified
+			return justified
+		},
+		multiply : function( n ){
+
+			var i, s = ''
+
+			n = cascade( n, 2 )
+			for( i = 0; i < n; i ++ ){
+				s += this
+			}
+			return s
+		},
+		reverse : function(){
+
+			var i, s = ''
+
+			for( i = 0; i < this.length; i ++ ){
+				s = this[ i ] + s
+			}
+			return s
+		},
+		size : function(){
+
+			return this.length
+		},
+		toEntities : function(){
+
+			var i, entities = ''
+
+			for( i = 0; i < this.length; i ++ ){
+				entities += '&#' + this.charCodeAt( i ) + ';'
+			}
+			return entities
+		},
+		toCamelCase : function(){
+			
+			var
+			split  = this.split( /\W+|_+/ ),
+			joined = split[ 0 ],
+			i
+
+			for( i = 1; i < split.length; i ++ )
+				joined += split[ i ].capitalize()
+
+			return joined
+		},
+		directionToDegrees : function(){
+
+			var
+			directions = [ 'N', 'NNE', 'NE', 'NEE', 'E', 'SEE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'SWW', 'W', 'NWW', 'NW', 'NNW', 'N' ],
+			i = directions.indexOf( this.toUpperCase() )
+
+			return i >= 0 ? i.scale( 0, directions.length - 1, 0, 360 ) : Number.NaN
+		},
+		toArray : function(){
+
+			return [ this ]
+		},
+		toNumber : function(){
+
+			return parseFloat( this )
+		},
+		toString : function(){
+
+			return this
+		},
+		toUnderscoreCase : function(){
+			
+			var underscored = this.replace( /[A-Z]+/g, function( $0 ){
+				
+				return '_' + $0
+			})
+
+			if( underscored.charAt( 0 ) === '_' ) underscored = underscored.substr( 1 )
+			return underscored.toLowerCase()
+		},
+		toUnicode : function(){
+
+			var i, u, unicode = ''
+
+			for( i = 0; i < this.length; i ++ ){
+				u = this.charCodeAt( i ).toString( 16 ).toUpperCase()
+				while( u.length < 4 ){
+					u = '0' + u
+				}
+				unicode += '\\u' + u
+			}
+			return unicode
+		}
+	})
+
+
+
+
+}

+ 438 - 0
public/drivers/model/rubik/lib/slices.js

@@ -0,0 +1,438 @@
+/*
+
+
+	SLICES
+
+	Slices are thin layers sliced out of the Cube
+	composed of 9 Cubelets (3x3 grid).
+	The position of these Cubelets can be mapped as follows:
+
+
+       ----------- ----------- ----------- 
+      |           |           |           |
+      | northWest |   north   | northEast |
+      |     0     |     1     |     2     |
+      |           |           |           |
+       ----------- ----------- ----------- 
+      |           |           |           |
+      |    west   |   origin  |    east   |
+      |     3     |     4     |     5     |
+      |           |           |           |
+       ----------- ----------- ----------- 
+      |           |           |           |
+      | southWest |   south   | southEast |
+      |     6     |     7     |     8     |
+      |           |           |           |
+       ----------- ----------- ----------- 
+
+
+
+	The cubelets[] Array is mapped to names for convenience:
+
+	  this.cubelets[ 0 ] === this.northWest
+	  this.cubelets[ 1 ] === this.north
+	  this.cubelets[ 2 ] === this.northEast
+	  this.cubelets[ 3 ] === this.west
+	  this.cubelets[ 4 ] === this.origin
+	  this.cubelets[ 5 ] === this.east
+	  this.cubelets[ 6 ] === this.southWest
+	  this.cubelets[ 7 ] === this.south
+	  this.cubelets[ 8 ] === this.southEast	
+
+
+
+	Portions of Slices can be Grouped:
+
+	Rows and columns as strips (1x3)
+	  this.up
+	  this.equator
+	  this.down
+	  this.left
+	  this.middle
+	  this.right
+
+	Other combinations
+	  this.cross
+	  this.edges
+	  this.ex
+	  this.corners
+	  this.ring
+	  this.dexter
+	  this.sinister
+
+
+
+	A Slice may be inspected from the browser's JavaScript console with: 
+
+	  this.inspect() 
+
+	This will reveal the Slice's Cubelets, their Indexes, and colors. 
+	A compact inspection mode is also available:
+
+	  this.inspect( true )
+
+	This is most useful for Slices that are also Faces. For Slices that are
+	not Faces, or for special cases, it may be useful to send a side
+	argument which is usually by default the Slice's origin's only visible
+	side if it has one. 
+
+	  this.inspect( false, 'up' )
+	  this.inspect( true, 'up' )
+
+
+
+	CUBE FACES vs CUBE SLICES
+
+	All Cube faces are Slices, but not all Slices are Cube faces. 
+	For example, a Cube has 6 faces: front, up, right, down, left, back. 
+	But it also has slices that that cut through the center of the Cube 
+	itself: equator, middle, and standing. When a Slice maps itself it 
+	inspects the faces of the Cubelet in the origin position of the Slice -- 
+	the center piece -- which can either have a single visible face or no 
+	visible face. If it has a visible face then the Slice's face and the 
+	face's direction is in the direction of that Cubelet's visible face. 
+	This seems redundant from the Cube's perspective:
+
+	  cube.front.face === 'front'
+
+	However it becomes valuable from inside a Slice or Fold when a 
+	relationship to the Cube's orientation is not immediately clear:
+
+	  if( this.face === 'front' )...
+
+	Therefore a Slice (s) is also a face if s.face !== undefined.
+
+
+
+
+*/
+
+
+
+
+
+
+
+export function Slice(){
+
+	this.cubelets = Array.prototype.slice.call( arguments )
+	this.map()
+}
+
+
+
+
+globalThis.setupTasks = globalThis.setupTasks || []
+globalThis.setupTasks.push( function(){
+
+	augment( Slice, {
+
+	
+		inspect: function( compact, side ){
+
+			var
+			getColorName = function( cubelet ){
+
+				return cubelet[ side ].color.name.toUpperCase().justifyCenter( 9 )
+			},
+			sideLabel = ''
+
+			if( side === undefined ){
+
+ 				if( this.face !== undefined ) side = this.face
+				else side = 'front'
+			}
+			if( side instanceof Direction ) side = side.name
+			if( side !== this.face ) sideLabel = side + 's'
+			if( compact ){
+
+				console.log(
+
+					'\n' + this.name.capitalize().justifyLeft( 10 ) +
+					'%c '+ this.northWest.id.toPaddedString( 2 ) +' %c '+
+					'%c '+ this.north.id.toPaddedString( 2 ) +' %c '+
+					'%c '+ this.northEast.id.toPaddedString( 2 ) +' %c '+
+					'\n' + sideLabel +'\n'+
+
+					'          %c '+ this.west.id.toPaddedString( 2 ) +' %c '+
+					'%c '+ this.origin.id.toPaddedString( 2 ) +' %c '+
+					'%c '+ this.east.id.toPaddedString( 2 ) +' %c '+
+					'\n\n'+
+					'          %c '+ this.southWest.id.toPaddedString( 2 ) +' %c '+
+					'%c '+ this.south.id.toPaddedString( 2 ) +' %c '+
+					'%c '+ this.southEast.id.toPaddedString( 2 ) +' %c '+
+					'\n',
+
+					this.northWest[ side ].color.styleB, '',
+					this.north[     side ].color.styleB, '',
+					this.northEast[ side ].color.styleB, '',
+					
+					this.west[      side ].color.styleB, '',
+					this.origin[    side ].color.styleB, '',
+					this.east[      side ].color.styleB, '',
+					
+					this.southWest[ side ].color.styleB, '',
+					this.south[     side ].color.styleB, '',
+					this.southEast[ side ].color.styleB, ''
+				)
+			}
+			else {
+
+				console.log(
+
+					'\n          %c           %c %c           %c %c           %c '+
+					'\n'+ this.name.capitalize().justifyLeft( 10 ) +
+					'%c northWest %c '+
+					'%c   north   %c '+
+					'%c northEast %c '+
+					'\n' + sideLabel.justifyLeft( 10 ) +
+					'%c '+ this.northWest.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'%c '+ this.north.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'%c '+ this.northEast.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'\n' +
+					'          %c ' + getColorName( this.northWest ) +' %c '+
+					'%c '+ getColorName( this.north ) +' %c '+
+					'%c '+ getColorName( this.northEast ) +' %c '+
+					'\n          %c           %c %c           %c %c           %c '+
+
+
+					'\n\n          %c           %c %c           %c %c           %c '+
+					'\n          %c    west   %c '+
+					'%c   origin  %c '+
+					'%c    east   %c '+
+					'\n' +
+					'          %c ' + this.west.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'%c '+ this.origin.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'%c '+ this.east.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'\n' +
+					'          %c ' + getColorName( this.west ) +' %c '+
+					'%c '+ getColorName( this.origin ) +' %c '+
+					'%c '+ getColorName( this.east ) +' %c '+
+					'\n          %c           %c %c           %c %c           %c '+
+
+
+					'\n\n          %c           %c %c           %c %c           %c '+
+					'\n          %c southWest %c '+
+					'%c   south   %c '+
+					'%c southEast %c '+
+					'\n' +
+					'          %c ' + this.southWest.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'%c '+ this.south.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'%c '+ this.southEast.id.toPaddedString( 2 ).justifyCenter( 9 ) +' %c '+
+					'\n' +
+					'          %c ' + getColorName( this.southWest ) +' %c '+
+					'%c '+ getColorName( this.south ) +' %c '+
+					'%c '+ getColorName( this.southEast ) +' %c '+
+					'\n          %c           %c %c           %c %c           %c\n',
+
+
+					this.northWest[ side ].color.styleB, '',
+					this.north[     side ].color.styleB, '',
+					this.northEast[ side ].color.styleB, '',
+					this.northWest[ side ].color.styleB, '',
+					this.north[     side ].color.styleB, '',
+					this.northEast[ side ].color.styleB, '',
+					this.northWest[ side ].color.styleB, '',
+					this.north[     side ].color.styleB, '',
+					this.northEast[ side ].color.styleB, '',
+					this.northWest[ side ].color.styleB, '',
+					this.north[     side ].color.styleB, '',
+					this.northEast[ side ].color.styleB, '',
+					this.northWest[ side ].color.styleB, '',
+					this.north[     side ].color.styleB, '',
+					this.northEast[ side ].color.styleB, '',
+					
+					this.west[      side ].color.styleB, '',
+					this.origin[    side ].color.styleB, '',
+					this.east[      side ].color.styleB, '',
+					this.west[      side ].color.styleB, '',
+					this.origin[    side ].color.styleB, '',
+					this.east[      side ].color.styleB, '',
+					this.west[      side ].color.styleB, '',
+					this.origin[    side ].color.styleB, '',
+					this.east[      side ].color.styleB, '',
+					this.west[      side ].color.styleB, '',
+					this.origin[    side ].color.styleB, '',
+					this.east[      side ].color.styleB, '',
+					this.west[      side ].color.styleB, '',
+					this.origin[    side ].color.styleB, '',
+					this.east[      side ].color.styleB, '',
+					
+					this.southWest[ side ].color.styleB, '',
+					this.south[     side ].color.styleB, '',
+					this.southEast[ side ].color.styleB, '',
+					this.southWest[ side ].color.styleB, '',
+					this.south[     side ].color.styleB, '',
+					this.southEast[ side ].color.styleB, '',
+					this.southWest[ side ].color.styleB, '',
+					this.south[     side ].color.styleB, '',
+					this.southEast[ side ].color.styleB, '',
+					this.southWest[ side ].color.styleB, '',
+					this.south[     side ].color.styleB, '',
+					this.southEast[ side ].color.styleB, '',
+					this.southWest[ side ].color.styleB, '',
+					this.south[     side ].color.styleB, '',
+					this.southEast[ side ].color.styleB, ''
+				)
+			}
+		},
+		map: function(){
+
+
+			//  Addressing single Cubelets can best be done by 
+			//  compass notation.
+
+			this.origin    = this.cubelets[ 4 ]
+			this.north     = this.cubelets[ 1 ]
+			this.northEast = this.cubelets[ 2 ]
+			this.east      = this.cubelets[ 5 ]
+			this.southEast = this.cubelets[ 8 ]
+			this.south     = this.cubelets[ 7 ]
+			this.southWest = this.cubelets[ 6 ]
+			this.west      = this.cubelets[ 3 ]
+			this.northWest = this.cubelets[ 0 ]
+
+
+			//  Now that we know what the origin Cubelet is 
+			//  we can determine if this is merely a Slice
+			//  or if it is also a Face.
+			//  If a face we'll know what direction it faces
+			//  and what the color of the face *should* be. 
+
+			for( var i = 0; i < 6; i ++ ){
+
+				if( this.origin.faces[ i ].color && this.origin.faces[ i ].color !== COLORLESS ){
+
+					this.color = this.origin.faces[ i ].color
+					this.face = Direction.getNameById( i )
+					break
+				}
+			}
+
+			
+			//  Addressing orthagonal strips of Cubelets is more easily done by
+			//  cube notation for the X and Y axes.
+		
+			this.up = new Group(
+
+				this.northWest, this.north, this.northEast
+			)
+			this.equator = new Group(
+
+				this.west, this.origin, this.east
+			)
+			this.down = new Group(
+
+				this.southWest, this.south, this.southEast
+			)
+			this.left = new Group(
+
+				this.northWest,
+				this.west,
+				this.southWest
+			)
+			this.middle = new Group(
+
+				this.north,
+				this.origin,
+				this.south
+			)
+			this.right = new Group(
+
+				this.northEast,
+				this.east,
+				this.southEast
+			)
+
+
+			//  If our Slice has only one center piece 
+			// (ie. a Cubelet with only ONE single Sticker)
+			//  then it is a Face -- a special kind of Slice.
+
+			var hasCenter = this.hasType( 'center' )
+			if( hasCenter && hasCenter.cubelets.length === 1 ){
+
+				this.center  = this.hasType( 'center' )//.cubelets[ 0 ]
+				this.corners = new Group( this.hasType( 'corner' ))
+				this.cross   = new Group( this.center, this.hasType( 'edge' ))				
+				this.ex      = new Group( this.center, this.hasType( 'corner' ))
+			}
+
+
+			//  Otherwise our Slice will have multiple center pieces
+			// (again, that means Cubelets with only ONE single Sticker)
+			//  and this is why a Slice's "origin" is NOT the same as
+			//  its "center" or "centers!"
+
+			else {
+
+				this.centers = new Group( this.hasType( 'center' ))
+			}
+			this.edges = new Group( this.hasType( 'edge' ))			
+
+
+			//  I'm still debating whether this should be Sticker-related
+			//  or if it's merely a fun grouping. 
+			//  Writing the solver should clarify this further...
+
+			this.ring = new Group(
+
+				this.northWest, this.north, this.northEast,
+				this.west, this.east,
+				this.southWest, this.south, this.southEast
+			)
+
+
+			//  And finally for the hell of it let's try diagonals via
+			//  Blazon notation:
+
+			this.dexter = new Group(//  From top-left to bottom-right.
+
+				this.northWest,
+				this.origin,
+				this.southEast
+			)
+			this.sinister = new Group(//  From top-right to bottom-left.
+
+				this.northEast,
+				this.origin,
+				this.southWest
+			)
+		},
+
+
+
+
+		//  Given a Cubelet in this Slice,
+		//  what is its compass location?
+
+		getLocation: function( cubelet ){
+
+			if( cubelet === this.origin    ) return 'origin'
+			if( cubelet === this.north     ) return 'north'
+			if( cubelet === this.northEast ) return 'northEast'
+			if( cubelet === this.east      ) return 'east'
+			if( cubelet === this.southEast ) return 'southEast'
+			if( cubelet === this.south     ) return 'south'
+			if( cubelet === this.southWest ) return 'southWest'
+			if( cubelet === this.west      ) return 'west'
+			if( cubelet === this.northWest ) return 'northWest'
+
+			return false
+		}
+
+
+
+
+	})
+
+
+	//  We want Slice to learn from Group
+	//  but we don't want their prototypes to actually be linked.
+	//  Hence we use Skip.js's learn function:
+
+	learn( Slice.prototype, Group.prototype )
+})
+
+
+

+ 210 - 0
public/drivers/model/rubik/lib/twists.js

@@ -0,0 +1,210 @@
+/*
+
+
+	TWISTS
+
+	Why have twist validation code in multiple places when we can create a
+	Twist class here for all?
+
+
+*/
+
+
+
+
+
+
+
+
+export function Twist( command, degrees ){
+
+
+	//  What group of Cubelets do we intend to twist?
+
+	var group = {
+
+		X: 'Cube on X',
+		L: 'Left face',
+		M: 'Middle slice',
+		R: 'Right face',
+
+		Y: 'Cube on Y',
+		U: 'Up face',
+		E: 'Equator slice',
+		D: 'Down face',
+
+		Z: 'Cube on Z',
+		F: 'Front face',
+		S: 'Standing slice',
+		B: 'Back face'
+
+	}[ command.toUpperCase() ]
+
+
+	//  If we've received a valid twist group to operate on
+	//  then we can proceed. Otherwise return false!
+
+	if( group !== undefined ){
+
+
+		//  If our degrees of rotation are negative
+		//  then we need to invert the twist direction
+		// (ie. change clockwise to anticlockwise)
+		//  and take the absolute value of the degrees.
+		//  Remember, it's ok to have degrees === undefined
+		//  which will peg to the nearest degrees % 90 === 0.
+
+		if( degrees != undefined && degrees < 0 ){
+
+			command = command.invert()
+			degrees = degrees.absolute()
+		}
+
+
+		//  Now let's note the absolute direction of the rotation
+		//  as both a number and in English.
+
+		var
+		vector =  0,
+		wise   = 'unwise'
+
+		if( command === command.toUpperCase() ){
+
+			vector =  1
+			wise   = 'clockwise'
+		}
+		else if( command === command.toLowerCase() ){
+
+			vector = -1
+			wise   = 'anticlockwise'
+		}
+
+
+		//  Finally we're ready to package up all the relevant information
+		//  about this particular twist.
+		//  The constructor will return it of course.
+
+		this.command = command //  Twist command
+		this.group   = group   //  Description in English
+		this.degrees = degrees //  Relative degrees (undefined is ok!)
+		this.vector  = vector  //  Absolute degree polarity
+		this.wise    = wise    //  Absolute clock direction in English
+		this.created = Date.now()
+	
+
+		//  Best to leave this as a function rather than a property.
+		//  I mean... imagine call this constructor if it tried to call itself!
+		//  Infinite loopage mess.
+
+		this.getInverse = function(){
+
+			return new Twist( command.invert(), degrees )
+		}
+	}
+	else return false
+}
+
+
+
+
+Twist.validate = function(){
+
+	var 
+	elements = Array.prototype.slice.call( arguments ),
+	element, i,
+	pattern, matches, match, m, head, foot
+
+	for( i = 0; i < elements.length; i ++ ){
+		var lookAhead = undefined;
+		
+		element = elements[ i ]
+		if( i + 1 < elements.length ) lookAhead = elements[ i + 1 ]
+		else lookAhead = undefined
+
+
+		if( element instanceof Twist ){
+
+
+			//  Example usage: 
+			//  cube.twistQueue.add( new Twist( 'U' ))
+			//  cube.twistQueue.add( new Twist( 'U', -17 ))
+			//  AWESOME. Nothing to do here.
+		}
+		else if( typeof element === 'string' ){
+
+			if( element.length === 1 ){
+
+
+				//  Example usage: 
+				//  cube.twistQueue.add( 'U' )
+				//  cube.twistQueue.add( 'U', 45 )
+
+				if( typeof lookAhead === 'number' ){
+
+					 elements[ i ] = new Twist( element, lookAhead )
+				}
+				else elements[ i ] = new Twist( element )
+
+			}
+			else if( element.length > 1 ){
+
+
+				//  Example usage: 
+				//  cube.twistQueue.add( 'UdrLf' )
+				//  cube.twistQueue.add( 'Udr10Lf-30b' )
+				
+				pattern = /(-?\d+|[XLMRYUEDZFSB])/gi
+				matches = element.match( pattern )
+				for( m = 0; m < matches.length; m ++ ){
+
+					match = matches[ m ]
+					if( isNumeric( match )) matches[ m ] = +match
+					else {
+
+						head    = matches.slice( 0, m )
+						foot    = matches.slice( m + 1 )
+						match   = match.split( '' )
+						matches = head.concat( match, foot )
+					}
+				}
+				head = elements.slice( 0, i )
+				foot = elements.slice( i + 1 )				
+				elements = head.concat( matches, foot )
+				i --//  Send it through the loop again to avoid duplicating logic.
+			}
+		}
+		else if( element instanceof Direction ){
+
+
+			//  Example usage: 
+			//  cube.twistQueue.add( FRONT )
+
+			elements[ i ] = element.initial
+			i --//  Send it through the loop again to avoid duplicating logic.
+		}
+		else if( element instanceof Array ){
+
+
+			//  Example usage: 
+			//  cube.twistQueue.add([ ? ])
+
+			head = elements.slice( 0, i )
+			foot = elements.slice( i + 1 )				
+			elements = head.concat( element, foot )
+			i --//  Send it through the loop again to avoid duplicating logic.
+		}
+		else {
+
+
+			//  Whatever this element is, we don't recognize it.
+			//  (Could be a Number that we're discarding on purpose.)
+
+			elements.splice( i, 1 )
+			i --//  Send it through the loop again to avoid duplicating logic.
+		}
+	}
+	return elements
+}
+
+
+

+ 461 - 0
public/drivers/model/rubik/rubik.js

@@ -0,0 +1,461 @@
+/*
+The MIT License (MIT)
+Copyright (c) 2014-2020 Nikolai Suslov and the Krestianstvo.org project contributors. (https://github.com/NikolaySuslov/livecodingspace/blob/master/LICENSE.md)
+
+*/
+
+// VWF & Ohm model driver
+
+import { Fabric } from '/core/vwf/fabric.js';
+
+import {skipjs} from "/drivers/model/rubik/lib/skip.js"
+import {Queue} from "/drivers/model/rubik/lib/queues.js"
+import {Color} from "/drivers/model/rubik/lib/colors.js"
+import {Twist} from "/drivers/model/rubik/lib/twists.js"
+import {Direction} from "/drivers/model/rubik/lib/directions.js"
+import {Fold} from "/drivers/model/rubik/lib/folds.js"
+import {Cubelet} from "/drivers/model/rubik/lib/cubelets.js"
+import {Group} from "/drivers/model/rubik/lib/groups.js"
+import {Slice} from "/drivers/model/rubik/lib/slices.js"
+import {Cube} from "/drivers/model/rubik/lib/cubes.js"
+
+class RubikModel extends Fabric {
+
+    constructor(module) {
+
+        console.log("RUBIK model constructor");
+        super(module, "Model");
+    }
+
+    factory() {
+        let _self_ = this;
+
+        return this.load( this.module, 
+            {
+
+                // == Module Definition ====================================================================
+        
+                // -- pipeline -----------------------------------------------------------------------------
+        
+                // pipeline: [ log ], // vwf <=> log <=> scene
+        
+                // -- initialize ---------------------------------------------------------------------------
+        
+                initialize: function() {
+                    
+                    let self = this;
+
+              
+                skipjs();
+                globalThis.Queue = Queue;
+                globalThis.Color = Color;
+                globalThis.Twist = Twist;
+                globalThis.Direction = Direction;
+                globalThis.Fold = Fold;
+        
+                globalThis.Cubelet = Cubelet;
+                globalThis.Group = Group;
+                globalThis.Slice = Slice;
+        
+                globalThis.Cube = Cube;
+        
+                if( globalThis.setupTasks )	{
+                    globalThis.setupTasks.forEach( function( task ){ task() })
+                }
+                        
+                    
+
+        
+                   this.state = {
+                        nodes: {},
+                        scenes: {},
+                        prototypes: {},
+                        createLocalNode: function (nodeID, childID, childExtendsID, childImplementsIDs,
+                            childSource, childType, childIndex, childName, callback) {
+                            return {
+                                "parentID": nodeID,
+                                "ID": childID,
+                                "extendsID": childExtendsID,
+                                "implementsIDs": childImplementsIDs,
+                                "source": childSource,
+                                "type": childType,
+                                "name": childName,
+                                "prototypes": undefined,
+                            };
+                        },
+                        isCubeNodeComponent: function (prototypes) {
+                            var found = false;
+                            if (prototypes) {
+                                for (var i = 0; i < prototypes.length && !found; i++) {
+                                    found = (prototypes[i] === "cubeModel.vwf");
+                                }
+                            }
+                            return found;
+                        },
+                        isCubeletNodeComponent: function (prototypes) {
+                            var found = false;
+                            if (prototypes) {
+                                for (var i = 0; i < prototypes.length && !found; i++) {
+                                    found = (prototypes[i] === "cubeletModel.vwf");
+                                }
+                            }
+                            return found;
+                        }
+                
+                        
+
+                    };
+        
+                    this.state.kernel = this.kernel.kernel.kernel;
+        
+                    //this.state.kernel = this.kernel.kernel.kernel;
+                    
+                },
+                // == Model API ============================================================================
+        
+                // -- creatingNode -------------------------------------------------------------------------
+        
+                creatingNode: function( nodeID, childID, childExtendsID, childImplementsIDs,
+                    childSource, childType, childIndex, childName, callback /* ( ready ) */ ) {
+        
+                    // If the parent nodeID is 0, this node is attached directly to the root and is therefore either 
+                    // the scene or a prototype.  In either of those cases, save the uri of the new node
+                    var childURI = (nodeID === 0 ? childIndex : undefined);
+                    var appID = this.kernel.application();
+        
+                    // If the node being created is a prototype, construct it and add it to the array of prototypes,
+                    // and then return
+                    var prototypeID = _self_.utility.ifPrototypeGetId(appID, this.state.prototypes, nodeID, childID);
+                    if (prototypeID !== undefined) {
+        
+                        this.state.prototypes[prototypeID] = {
+                            parentID: nodeID,
+                            ID: childID,
+                            extendsID: childExtendsID,
+                            implementsID: childImplementsIDs,
+                            source: childSource,
+                            type: childType,
+                            name: childName
+                        };
+                        return;
+                    }
+        
+                    var protos = _self_.getPrototypes(this.kernel, childExtendsID);
+                    //var kernel = this.kernel.kernel.kernel;
+                    var node;
+        
+                    if (this.state.isCubeNodeComponent(protos)) {
+        
+                        // Create the local copy of the node properties
+                        if (this.state.nodes[childID] === undefined) {
+                            this.state.nodes[childID] = this.state.createLocalNode(nodeID, childID, childExtendsID, childImplementsIDs,
+                                childSource, childType, childIndex, childName, callback);
+                        }
+        
+                        node = this.state.nodes[childID];
+                        node.prototypes = protos;
+        
+                        node.cube = new Cube(childName);
+                        node.cube.nodeID = childID;
+                        node.cube.kernel = this.state.kernel;
+                        // let jsModel = vwf.models['/core/vwf/model/javascript'].model;
+                        // let jsNode = jsModel.nodes[childID];
+                        // jsNode.cubeModel = node.cube;
+
+                        //node.aframeObj = createAFrameObject(node);
+                        //addNodeToHierarchy(node);
+                        //notifyDriverOfPrototypeAndBehaviorProps();
+                    }
+
+                    if (this.state.isCubeletNodeComponent(protos)) {
+        
+                        // Create the local copy of the node properties
+                        if (this.state.nodes[childID] === undefined) {
+                            this.state.nodes[childID] = this.state.createLocalNode(nodeID, childID, childExtendsID, childImplementsIDs,
+                                childSource, childType, childIndex, childName, callback);
+                        }
+        
+                        node = this.state.nodes[childID];
+                        node.prototypes = protos;
+        
+
+                    }
+                },
+        
+                callingMethod: function( nodeID, methodName, methodParameters ) {
+        
+                    //let self = this;
+                    let node = this.state.nodes[nodeID];
+
+                    if (node && node.cube) {
+    
+                    // if (methodName == 'getCubeModel') {
+                    //    return node.cube
+                    // }
+
+                    if(methodName == "inspect"){
+
+                        node.cube.inspect();
+                        console.log(node.cube);
+
+                    }
+                    
+                    if (methodName == 'isCubeTweening') {
+                        return node.cube.isTweening()
+                     }
+
+                    if (methodName == 'getCubeModelID') {
+                        return node.cube.id
+                     }
+
+                    if (methodName == 'getCubelet') {
+
+                        let cubelet = Object.assign({}, node.cube.cubelets[methodParameters[0]]);
+                        cubelet.cube = node.cube.id;
+                        return cubelet
+                        
+                    }
+
+                    if(methodName == "setCubeletID"){
+
+                        node.cube.cubelets[methodParameters[0]].nodeID = methodParameters[1];
+
+                    }
+
+                    if (methodName == 'progressQueue') {
+
+                           if (node.cube.twistQueue.future.length > 0) {
+                                node.cube.twist(node.cube.twistQueue.do());
+                            } 
+
+                    }
+
+                    if (methodName == 'undo') {
+
+                        //TODO:
+
+                 }
+
+                    if (methodName == 'twistAction') {
+                        let key = methodParameters[0];
+
+                        node.cube.twistQueue.add(key);
+
+                            // while (node.cube.twistQueue.future.length > 0) {
+                            //     node.cube.twist(node.cube.twistQueue.do());
+                            // }
+
+                    //node.cube.twist( node.cube.twistQueue.do() );     
+
+                    }
+
+                    if (methodName == 'getCubelets') {
+
+                        var cubelets = [];
+                        node.cube.cubelets.forEach(el=>{
+                            let cubelet = {
+                                id: el.id.toString(),
+                                address: el.address,
+                                addressX: el.addressX,
+                                addressY: el.addressY,
+                                addressZ: el.addressZ
+                            }
+                            cubelets.push(cubelet)
+                        })
+                        return cubelets
+                     }
+
+                }
+
+                if(methodName == "cubeletsRemap"){
+
+                    let cubeletID = methodParameters[0];
+                    let cubeCallback = methodParameters[1]
+                
+                    node.cube.remap(cubeletID, cubeCallback)
+                    
+
+                }
+
+
+                // if (this.state.isCubeletNodeComponent(node.prototypes)){
+
+                   
+                    
+                //     // if (methodName == 'getFaces') {
+
+                //     //     var faces = [];
+                //     //     node.cube.cubelets.forEach(el=>{
+                //     //         let cubelet = {
+                //     //             id: el.id.toString(),
+                //     //             address: el.address,
+                //     //             addressX: el.addressX,
+                //     //             addressY: el.addressY,
+                //     //             addressZ: el.addressZ
+                //     //         }
+                //     //         cubelets.push(cubelet)
+                //     //     })
+                //     //     return cubelets
+                //     // }
+                     
+
+                    
+                // }
+        
+                },
+
+
+                 // -- initializingNode -------------------------------------------------------------------------
+        
+                initializingNode: function( nodeID, childID, childExtendsID, childImplementsIDs,
+                childSource, childType, childIndex, childName ) {
+                let node = this.state.nodes[childID];
+
+                if ( node ) {
+                    if (this.state.isCubeletNodeComponent(node.prototypes)) {
+                            console.log("CUBElet INitializing...");
+                            
+                            let props = this.state.kernel.getProperties(childID);
+                            let nodeCube = this.state.nodes[props.cubeNodeID];
+                            nodeCube.cube.cubelets[childName].nodeID = childID;
+                            
+                            // nodeCube.cube.cubelets[childName].address = props.address;
+                            // nodeCube.cube.cubelets[childName].cubeletID = props.cubeletID;
+                    }
+
+                    if (this.state.isCubeNodeComponent(node.prototypes)) {
+                        console.log("CUBE INitializing...");
+                        let props = this.state.kernel.getProperties(childID);
+                        let history = props.twistQueueHistory;
+                        // let action = history.join('');
+                        // node.cube.twistQueue.add(action);
+                        // node.cube.twist(node.cube.twistQueue.do(), true );
+                        history.forEach(el=>{
+                            node.cube.twistQueue.add(el);
+                        })
+                        
+                        
+                        if(history.length > 0){
+                            while (node.cube.twistQueue.future.length > 0) {
+                                node.cube.twist(node.cube.twistQueue.do(), true );
+                            }
+                            // node.cube.twistQueue.future.map(el=>{
+                            //     node.cube.twist(node.cube.twistQueue.do(), true );
+                            // })
+                            
+                        }
+                        
+                        //node.cube.map();
+                }
+                }
+            },
+        
+                // -- deletingNode -------------------------------------------------------------------------
+        
+                //deletingNode: function( nodeID ) {
+                //},
+        
+                 // -- initializingProperty -----------------------------------------------------------------
+    
+                initializingProperty: function( nodeID, propertyName, propertyValue ) {
+        
+                     var value = undefined;
+                    var node = this.state.nodes[nodeID];
+                    if (node !== undefined) {
+                        value = this.settingProperty(nodeID, propertyName, propertyValue);
+                    }
+                    return value;
+                
+            },
+        
+                // -- creatingProperty ---------------------------------------------------------------------
+        
+                creatingProperty: function (nodeID, propertyName, propertyValue) {
+                    return this.initializingProperty(nodeID, propertyName, propertyValue);
+                },
+        
+        
+                // -- settingProperty ----------------------------------------------------------------------
+        
+                settingProperty: function( nodeID, propertyName, propertyValue ) {
+        
+                    // let node = this.state.nodes[nodeID];
+                    // var value = undefined;
+
+
+                    // if (node) {
+    
+                    //     if (this.state.isCubeletNodeComponent(node.prototypes)) {
+
+                    //        switch ( propertyName ) {
+                         
+                    //             case "cubeletID":
+                    //                 node.nodeID = nodeID;
+                    //                 value = propertyValue          
+                    //                 break;
+        
+                    //             default:
+                    //                 value = undefined;
+                    //                 break;
+                    //         }
+                    //      }
+                    //     }
+        
+                    //  return value;
+        
+                },
+        
+                // -- gettingProperty ----------------------------------------------------------------------
+        
+                gettingProperty: function( nodeID, propertyName, propertyValue ) {
+        
+                    // let node = this.state.nodes[nodeID];
+                    // let value = undefined;
+    
+                    // if (node) {
+    
+                    //     if (this.state.isCubeNodeComponent(node.prototypes)) {
+
+                    //        switch ( propertyName ) {
+                         
+                    //                case "cubeID":
+                    //                            value = node.cube.id;
+                    //                             break;
+
+                    //         }
+                    //      }
+                    // }
+
+        
+                    //  if ( value !== undefined ) {
+                    //     propertyValue = value;
+                    // }
+        
+                    // return value;
+        
+                     
+                }
+        
+            } );
+
+        }
+
+
+    getPrototypes(kernel, extendsID) {
+        var prototypes = [];
+        var id = extendsID;
+    
+        while (id !== undefined) {
+            prototypes.push(id);
+            id = kernel.prototype(id);
+        }
+        return prototypes;
+    }
+    
+
+    }
+
+    export {
+        RubikModel as default
+      }
+    

+ 48 - 2
public/drivers/view/aframe.js

@@ -534,6 +534,41 @@ class AFrameView extends Fabric {
     
                 // if (methodName == "createGooglePoly") {
                 // }
+
+                if (methodName == "createLocalRaycaster") {
+
+                    //var clientThatSatProperty = self.kernel.client();
+                    var me = self.kernel.moniker();
+    
+                    // If the transform property was initially updated by this view....
+                    if (nodeID.includes(me)) {
+                        console.log('Creating raycaster for ME: ', nodeID);
+                        let xrcontroller = document.querySelector('#'+nodeID);
+
+                        xrcontroller.setAttribute('raycaster', {
+                            objects: '.intersectable',
+                            showLine: false,
+                            far: 100,
+                            recursive: false,
+                            interval: 10});
+
+                            // xrcontroller.addEventListener('raycaster-intersection', function (evt) {
+                            //     let int = evt.detail.intersections[0];
+                            //     let idata = {
+                            //         point: int.point,
+                            //         elID: int.object.el.id
+                            //     }
+                            //     console.log(idata)
+                            // })
+                        
+                            // xrcontroller.addEventListener('raycaster-intersection-cleared', function (evt) {
+                            // })
+                    }
+
+                   
+
+
+                }
     
             }
         });
@@ -706,6 +741,7 @@ class AFrameView extends Fabric {
          //let elA = document.querySelector('#avatarControlParent');
          let elA = document.querySelector('#avatarControl');
          let el = document.querySelector(aSelector);
+         let xrController = document.querySelector('#' + avatarName);
          if (el && elA) {
  
             //  let positionC = el.object3D.position.clone();
@@ -747,8 +783,12 @@ class AFrameView extends Fabric {
                  if (distance > delta)
                  {
                     // console.log("position not equal");
+
+                    let idata = el.components["desktop-controls"].intersectionData;
+                    //if(idata) console.log('Point to: ', idata.point, ' intersect ', idata.elID);
+
                     self.kernel.setProperty(avatarName, "position", position);
-                     self.kernel.callMethod(avatarName, "moveVRController",[]);
+                     self.kernel.callMethod(avatarName, "moveVRController",[idata]);
                  }
              }
  
@@ -758,8 +798,13 @@ class AFrameView extends Fabric {
                  if (distance)
                  {
                      //console.log("rotation not equal");
+
+
+                    let idata =  el.components["desktop-controls"].intersectionData;
+                    //if(idata) console.log('Point to: ', idata.point, ' intersect ', idata.elID);
+
                      self.kernel.setProperty(avatarName, "rotation", rotation);
-                     self.kernel.callMethod(avatarName, "moveVRController",[]);
+                     self.kernel.callMethod(avatarName, "moveVRController",[idata]);
 
                      self.kernel.callMethod(avatarID, "moveHead", [headRotation]);
                  }
@@ -1145,6 +1190,7 @@ class AFrameView extends Fabric {
         //     x: 0, y: 0, z: -1
         // });
         el.setAttribute('desktop-controls', {});
+       // el.setAttribute('raycaster', {objects: ".intersectable", far: 1000, showLine: true});
         avatarControl.appendChild(el);
     }
 

+ 2 - 1
public/drivers/view/editor.js

@@ -1471,7 +1471,7 @@ class LCSEditor extends Fabric {
                             'min': sliderProps[m.name].min,
                             'max': sliderProps[m.name].max,
                             'step': sliderProps[m.name].step ? sliderProps[m.name].step : 0.1,
-                            'value': currenValue,
+                            'value': parseInt(currenValue),
                             'init': function () {
     
     
@@ -4029,6 +4029,7 @@ class LCSEditor extends Fabric {
                     implementsIDs: childImplementsIDs,
                     source: childSource,
                     name: childName,
+                    liveBindings: {}
                 };
     
                 if (parent) {

+ 79 - 16
public/drivers/view/webrtc/adapter-latest.js

@@ -12,7 +12,7 @@
 
 var _adapter_factory = require('./adapter_factory.js');
 
-var adapter = (0, _adapter_factory.adapterFactory)({ window: window });
+var adapter = (0, _adapter_factory.adapterFactory)({ window: typeof window === 'undefined' ? undefined : window });
 module.exports = adapter; // this is the difference from adapter_core.
 
 },{"./adapter_factory.js":2}],2:[function(require,module,exports){
@@ -87,6 +87,10 @@ function adapterFactory() {
         logging('Chrome shim is not included in this adapter release.');
         return adapter;
       }
+      if (browserDetails.version === null) {
+        logging('Chrome shim can not determine version, not shimming.');
+        return adapter;
+      }
       logging('adapter.js shimming chrome.');
       // Export to the adapter global object visible in the browser.
       adapter.browserShim = chromeShim;
@@ -124,6 +128,7 @@ function adapterFactory() {
       firefoxShim.shimReceiverGetStats(window);
       firefoxShim.shimRTCDataChannel(window);
       firefoxShim.shimAddTransceiver(window);
+      firefoxShim.shimGetParameters(window);
       firefoxShim.shimCreateOffer(window);
       firefoxShim.shimCreateAnswer(window);
 
@@ -167,6 +172,7 @@ function adapterFactory() {
       safariShim.shimRemoteStreamsAPI(window);
       safariShim.shimTrackEventTransceiver(window);
       safariShim.shimGetUserMedia(window);
+      safariShim.shimAudioContext(window);
 
       commonShim.shimRTCIceCandidate(window);
       commonShim.shimMaxMessageSize(window);
@@ -184,7 +190,6 @@ function adapterFactory() {
 // Browser shims.
 
 },{"./chrome/chrome_shim":3,"./common_shim":6,"./edge/edge_shim":7,"./firefox/firefox_shim":11,"./safari/safari_shim":14,"./utils":15}],3:[function(require,module,exports){
-
 /*
  *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
  *
@@ -942,11 +947,15 @@ function shimPeerConnection(window) {
   };
 }
 
+// Attempt to fix ONN in plan-b mode.
 function fixNegotiationNeeded(window) {
+  var browserDetails = utils.detectBrowser(window);
   utils.wrapPeerConnectionEvent(window, 'negotiationneeded', function (e) {
     var pc = e.target;
-    if (pc.signalingState !== 'stable') {
-      return;
+    if (browserDetails.version < 72 || pc.getConfiguration && pc.getConfiguration().sdpSemantics === 'plan-b') {
+      if (pc.signalingState !== 'stable') {
+        return;
+      }
     }
     return e;
   });
@@ -1537,7 +1546,7 @@ function shimConnectionState(window) {
 }
 
 function removeAllowExtmapMixed(window) {
-  /* remove a=extmap-allow-mixed for Chrome < M71 */
+  /* remove a=extmap-allow-mixed for webrtc.org < M71 */
   if (!window.RTCPeerConnection) {
     return;
   }
@@ -1545,6 +1554,9 @@ function removeAllowExtmapMixed(window) {
   if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) {
     return;
   }
+  if (browserDetails.browser === 'safari' && browserDetails.version >= 605) {
+    return;
+  }
   var nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription;
   window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(desc) {
     if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) {
@@ -1845,6 +1857,7 @@ exports.shimReceiverGetStats = shimReceiverGetStats;
 exports.shimRemoveStream = shimRemoveStream;
 exports.shimRTCDataChannel = shimRTCDataChannel;
 exports.shimAddTransceiver = shimAddTransceiver;
+exports.shimGetParameters = shimGetParameters;
 exports.shimCreateOffer = shimCreateOffer;
 exports.shimCreateAnswer = shimCreateAnswer;
 
@@ -2080,9 +2093,16 @@ function shimAddTransceiver(window) {
         var sender = transceiver.sender;
 
         var params = sender.getParameters();
-        if (!('encodings' in params)) {
+        if (!('encodings' in params) ||
+        // Avoid being fooled by patched getParameters() below.
+        params.encodings.length === 1 && Object.keys(params.encodings[0]).length === 0) {
           params.encodings = initParameters.sendEncodings;
-          this.setParametersPromises.push(sender.setParameters(params).catch(function () {}));
+          sender.sendEncodings = initParameters.sendEncodings;
+          this.setParametersPromises.push(sender.setParameters(params).then(function () {
+            delete sender.sendEncodings;
+          }).catch(function () {
+            delete sender.sendEncodings;
+          }));
         }
       }
       return transceiver;
@@ -2090,6 +2110,22 @@ function shimAddTransceiver(window) {
   }
 }
 
+function shimGetParameters(window) {
+  if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCRtpSender)) {
+    return;
+  }
+  var origGetParameters = window.RTCRtpSender.prototype.getParameters;
+  if (origGetParameters) {
+    window.RTCRtpSender.prototype.getParameters = function getParameters() {
+      var params = origGetParameters.apply(this, arguments);
+      if (!('encodings' in params)) {
+        params.encodings = [].concat(this.sendEncodings || [{}]);
+      }
+      return params;
+    };
+  }
+}
+
 function shimCreateOffer(window) {
   // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
   // Firefox ignores the init sendEncodings options passed to addTransceiver
@@ -2277,6 +2313,7 @@ exports.shimConstraints = shimConstraints;
 exports.shimRTCIceServerUrls = shimRTCIceServerUrls;
 exports.shimTrackEventTransceiver = shimTrackEventTransceiver;
 exports.shimCreateOfferLegacy = shimCreateOfferLegacy;
+exports.shimAudioContext = shimAudioContext;
 
 var _utils = require('../utils');
 
@@ -2510,6 +2547,9 @@ function shimConstraints(constraints) {
 }
 
 function shimRTCIceServerUrls(window) {
+  if (!window.RTCPeerConnection) {
+    return;
+  }
   // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
   var OrigPeerConnection = window.RTCPeerConnection;
   window.RTCPeerConnection = function RTCPeerConnection(pcConfig, pcConstraints) {
@@ -2533,7 +2573,7 @@ function shimRTCIceServerUrls(window) {
   };
   window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
   // wrap static methods. Currently just generateCertificate.
-  if ('generateCertificate' in window.RTCPeerConnection) {
+  if ('generateCertificate' in OrigPeerConnection) {
     Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
       get: function get() {
         return OrigPeerConnection.generateCertificate;
@@ -2611,6 +2651,13 @@ function shimCreateOfferLegacy(window) {
   };
 }
 
+function shimAudioContext(window) {
+  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || window.AudioContext) {
+    return;
+  }
+  window.AudioContext = window.webkitAudioContext;
+}
+
 },{"../utils":15}],15:[function(require,module,exports){
 /*
  *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
@@ -2673,21 +2720,37 @@ function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
     var wrappedCallback = function wrappedCallback(e) {
       var modifiedEvent = wrapper(e);
       if (modifiedEvent) {
-        cb(modifiedEvent);
+        if (cb.handleEvent) {
+          cb.handleEvent(modifiedEvent);
+        } else {
+          cb(modifiedEvent);
+        }
       }
     };
     this._eventMap = this._eventMap || {};
-    this._eventMap[cb] = wrappedCallback;
+    if (!this._eventMap[eventNameToWrap]) {
+      this._eventMap[eventNameToWrap] = new Map();
+    }
+    this._eventMap[eventNameToWrap].set(cb, wrappedCallback);
     return nativeAddEventListener.apply(this, [nativeEventName, wrappedCallback]);
   };
 
   var nativeRemoveEventListener = proto.removeEventListener;
   proto.removeEventListener = function (nativeEventName, cb) {
-    if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[cb]) {
+    if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[eventNameToWrap]) {
+      return nativeRemoveEventListener.apply(this, arguments);
+    }
+    if (!this._eventMap[eventNameToWrap].has(cb)) {
       return nativeRemoveEventListener.apply(this, arguments);
     }
-    var unwrappedCb = this._eventMap[cb];
-    delete this._eventMap[cb];
+    var unwrappedCb = this._eventMap[eventNameToWrap].get(cb);
+    this._eventMap[eventNameToWrap].delete(cb);
+    if (this._eventMap[eventNameToWrap].size === 0) {
+      delete this._eventMap[eventNameToWrap];
+    }
+    if (Object.keys(this._eventMap).length === 0) {
+      delete this._eventMap;
+    }
     return nativeRemoveEventListener.apply(this, [nativeEventName, unwrappedCb]);
   };
 
@@ -2758,10 +2821,7 @@ function deprecated(oldMethod, newMethod) {
  *     properties.
  */
 function detectBrowser(window) {
-  var navigator = window.navigator;
-
   // Returned result object.
-
   var result = { browser: null, version: null };
 
   // Fail early if it's not a browser
@@ -2770,6 +2830,9 @@ function detectBrowser(window) {
     return result;
   }
 
+  var navigator = window.navigator;
+
+
   if (navigator.mozGetUserMedia) {
     // Firefox.
     result.browser = 'firefox';

Some files were not shown because too many files changed in this diff