pxlNav/core/FileIO.js

// Core File IO Helper Utilities
// -- -- -- --

/**
 * @namespace pxlFile
 * @description File handling and asset loading
 */

import {
  Vector2,
  Vector3,
  Color,
  Group,
  Mesh,
  InstancedMesh,
  DoubleSide,
  FrontSide,
  BackSide,
  DynamicDrawUsage,
  Matrix4,
  Quaternion,
  Euler,
  Box3,
  ShaderMaterial,
  LinearSRGBColorSpace,
  SRGBColorSpace,
  NoColorSpace,
  LOD,
  SpotLight
} from "../../libs/three/three.module.min.js";
import { FBXLoader } from "../../libs/three/FBXLoader.js";
import { VERBOSE_LEVEL } from "./Enums.js";

export class FileIO{
  constructor( folderDict={}){
    this.pxlTimer=null;
    this.pxlUtils=null;
    this.pxlQuality=null;
    this.pxlEnums=null;
    this.pxlVideo=null;
    this.pxlCamera=null;
    this.pxlAutoCam=null;
    this.pxlUser=null;
    this.pxlEnv=null;
    this.pxlAnim=null;
    this.pxlDevice=null;
    this.pxlShaders=null;
    this.pxlColliders=null;

    this.verbose = false;

    this.pxlOptions={};
    
    // Turn on Verbose Logging to Javascript Console
    this.runDebugger = false;

    this.assetRoot = this.findInDict( folderDict, "assetRoot", "images/assets/" );
    this.guiRoot = this.findInDict( folderDict, "guiRoot", "images/GUI/" );
    this.roomRoot = this.findInDict( folderDict, "roomRoot", "images/rooms/" );
    this.vidRoot = this.findInDict( folderDict, "vidRoot", "images/screenContent/" );
    
    this.workerList = [];

    // No need for these to be in a Consts.js file yet
    this.oneTwoPFour = 1.0/2.4;
    this.twelvePNineTwoDiv = 1.0/12.92;
    this.onePOFiveFiveDiv = 1.0/1.055;
  }
  
  setDependencies( pxlNav ){
    this.pxlTimer=pxlNav.pxlTimer;
    this.pxlUtils=pxlNav.pxlUtils;
    this.pxlQuality=pxlNav.pxlQuality;
    this.pxlEnums=pxlNav.pxlEnums;
    this.pxlVideo=pxlNav.pxlVideo;
    this.pxlCamera=pxlNav.pxlCamera;
    this.pxlAutoCam=pxlNav.pxlAutoCam;
    this.pxlUser=pxlNav.pxlUser;
    this.pxlEnv=pxlNav.pxlEnv;
    this.pxlAnim=pxlNav.pxlAnim;
    this.pxlDevice=pxlNav.pxlDevice;
    this.pxlShaders=pxlNav.pxlShaders;
    this.pxlColliders=pxlNav.pxlColliders;
    this.pxlOptions=pxlNav.pxlOptions;

    this.verbose = this.pxlOptions.verbose;
  }

  log(...logger){
    if( this.runDebugger ||
       this.verbose >= this.pxlEnums.VERBOSE_LEVEL.DEBUG
    ){
      console.log("---");
      logger.forEach( (l)=>{ console.log(l); } );
      //logger= logger.length<2 ? logger[0] : logger;
      //console.log(logger);
    }
  }
  
  toggleDebugger( val=null ){
    if( !val ){
      val = !this.runDebugger;
    }
    this.runDebugger = val;
  }

  findInDict( dict, key, def ){
    if( dict.hasOwnProperty(key) ){
      return dict[key];
    }else{
      dict[key]=def;
      return def;
    }
  }

  // -- -- --

  convertVertColor( meshObj, space=this.pxlEnums.COLOR_SHIFT.KEEP ){
    if (meshObj.geometry && meshObj.geometry.attributes && meshObj.geometry.attributes.color) {
      let colors = meshObj.geometry.attributes.color;
      for( let x=0; x<colors.count; ++x ){
        let color = new Color().fromBufferAttribute(colors, x);
        this.pxlUtils.convertColor(color, space);
        colors.setXYZ(x, color.r, color.g, color.b);
      }
      colors.needsUpdate = true;
    }
  }

  // -- -- --

  // Check for UserData on the Mesh
  // envObj = Environment Class Object
  // envScene = Environment ThreeJS Scene Object
  // mesh = Current Object To Check For UserData Entries
  checkForUserData( envObj, envScene, mesh ){
    if( mesh.hasOwnProperty("userData") ){
      if( mesh.hasOwnProperty("material") ){
        if( mesh.userData.hasOwnProperty("doubleSided") && mesh.userData.doubleSided ){
          mesh.material.side=DoubleSide;
        }else{
          mesh.material.side=FrontSide;
        }
      }

      // Add to Glow render pass; ran by blurComposer
      if( mesh.userData.hasOwnProperty("GlowPass") && mesh.userData.GlowPass ){
        if( !envObj.geoList['GlowPass'] ){
          envObj.geoList['GlowPass'] = [];
        }
        mesh.layers.set( this.pxlEnums.RENDER_LAYER.GLOW )
        envObj.geoList['GlowPass'].push(mesh);
      }
      if( mesh.userData.hasOwnProperty("GlowPassMask") && mesh.userData.GlowPass ){
        if( !envObj.geoList['GlowPassMask'] ){
          envObj.geoList['GlowPassMask'] = [];
        }
        mesh.layers.set( this.pxlEnums.RENDER_LAYER.GLOW_MASK )
        envObj.geoList['GlowPassMask'].push(mesh);
      }
      
      if( mesh.userData.hasOwnProperty("castShadow") && mesh.userData.castShadow ){
        mesh.castShadow=true;
      }
      if( mesh.userData.hasOwnProperty("receiveShadow") && mesh.userData.receiveShadow ){
        mesh.receiveShadow=true;
      }
      if( mesh.userData.hasOwnProperty("Shader") && mesh.userData.Shader!="" ){
        let toShader = mesh.userData.Shader.trim()
        if( !envObj.shaderGeoList ) {
          envObj.shaderGeoList={};
        }
        envObj.shaderGeoList[mesh.name]=mesh;
      }
      
      if( mesh.userData.hasOwnProperty("Emitter") && mesh.userData.Emitter != "" ){
        if( !envObj.emitterList ) {
          envObj.emitterList={};
        }
        if( !envObj.emitterList[mesh.userData.Emitter] ) {
          envObj.emitterList[mesh.userData.Emitter]={};
          envObj.emitterList[mesh.userData.Emitter]['Emitter']=[];
          envObj.emitterList[mesh.userData.Emitter]['Particles']=[];
        }
        envObj.emitterList[mesh.userData.Emitter]['Emitter'].push( mesh );
      }
      
      //Hoverable Object
      if( mesh.userData.hasOwnProperty("Hover") && mesh.userData.Hover ){
        envObj.hasHoverables=true;
        envObj.hoverableList.push(mesh);
      }
      if( mesh.userData.hasOwnProperty("Click") && mesh.userData.Click ){
        envObj.hasClickables=true;
        envObj.clickableList.push(mesh);
      }
      
      this.checkObjectInstancing( envObj, envScene, mesh );
      
      // The Scripted UserData system needs fixing...
      //   Nested Objects should be handled in the main loader
      //   Objects in general should get Parent Transforms by default
      //     Meaning no reason for Scripted outside of storing the group for future use
      if( mesh.userData.hasOwnProperty("Scripted") ){
        //envScene.add( mesh );
        
        if( !envObj.geoList.hasOwnProperty('Scripted') ){
          envObj.geoList['Scripted']={}
        }
        envObj.geoList['Scripted'][mesh.name]=mesh;
        
        let scriptChildren = mesh.children;
        scriptChildren.forEach( (g)=>{
          if( g.name.includes("Geo") ){
            let pos = mesh.position;
            let rot = mesh.rotation;
            let sc = mesh.scale;
            g.position.set( pos.x, pos.y, pos.z )
            g.rotation.set( rot.x, rot.y, rot.z )
            g.scale.set( sc.x, sc.y, sc.z )
            //g.rotation = mesh.rotation.clone() 
            //g.scale  = mesh.scale.clone()
            //g.material.side=FrontSide;
            //mesh.add(g);
            
            g.updateMatrix();
            
            // Check for child UserData
            let gChildren = g.children
            gChildren.forEach( (c)=>{
              this.checkForUserData( envObj, envScene, c )
              if( c.type == "Group" ){
              // g.add(c);
                c.position.set( pos.x+c.position.x, pos.y+c.position.y, pos.z+c.position.z )
                c.rotation.set( rot.x, rot.y, rot.z )
                c.scale.set( sc.x, sc.y, sc.z )
              }
              c.updateMatrix();
            });
            
            
          }else if( g.name.includes("Colliders") ){
            let pos = mesh.position;
            let rot = mesh.rotation;
            let sc = mesh.scale;
            //g.position.set( pos.x, pos.y, pos.z )
            //g.rotation.set( rot.x, rot.y, rot.z )
            //g.scale.set( sc.x, sc.y, sc.z )
            g.position.set( pos.x, pos.y, pos.z )
            g.rotation.set( rot.x, rot.y, rot.z )
            g.scale.set( sc.x, sc.y, sc.z )
            g.updateMatrix();
            g.visible=false;
            //mesh.add(g);
          }
        });
      }
    }
  }

  // -- -- --
  
  checkForMeshSettings( meshObject, baseMaterial ){
    if( baseMaterial.hasOwnProperty("meshSettings") ){
      if( baseMaterial.meshSettings.hasOwnProperty("renderOrder") ){
        meshObject.renderOrder = baseMaterial.meshSettings.renderOrder;
      }
    }
  }

  // -- -- --
  
  canAppendChildren( envObj, mesh ){
    if( mesh.type != "Group" ){
      return false;
    }
    
    let ret = true;
    //if( mesh.hasOwnProperty("userData") && mesh.userData.hasOwnProperty("Scripted") && mesh.userData.Scripted ){
    if( envObj.geoList.hasOwnProperty('Scripted') && envObj.geoList.Scripted.hasOwnProperty(mesh.name) ){
      //console.log(envObj.geoList.Scripted, envObj.geoList.Scripted.hasOwnProperty(mesh.name), mesh.name)
      ret=false;
    }
    return ret;
  }
  
  canAddToScene( envObj, mesh ){
    let valid = true;
    if( mesh.hasOwnProperty("userData")
        && mesh.userData.hasOwnProperty("Instance")
        && envObj.hasOwnProperty("baseInstancesNames")
        && envObj.baseInstancesNames.hasOwnProperty(mesh.userData.Instance) ){
      valid = false;
    }
    return valid;
  }
  
  checkIsGlassLiquid( envObj, envScene, mesh, matList ){
    let isGlass = false;
    if( mesh.userData.hasOwnProperty("isGlass") && mesh.userData.isGlass ){
      isGlass=true;
    }
    if( mesh.userData.hasOwnProperty("isLiquid") && mesh.userData.isLiquid ){
      isGlass=true;
    }

    if( !isGlass ){
      return false;
    }
    
    this.log("Glass Detected - ",mesh.name);


    if(!envObj.glassGroup){
      let glassGrp=new Group();
      envObj.glassGroup=glassGrp;
      envScene.add(glassGrp);
    }


    let cBackMat=mesh.material.clone();
    mesh.material=cBackMat;
    mesh.material.transparent=true;
    mesh.material.opacity=.5;
    mesh.material.shininess=20;
    mesh.material.specular=mesh.material.color.clone();
    mesh.material.specular.r = mesh.material.specular.r*.5 +.1;
    mesh.material.specular.g = mesh.material.specular.g*.5 +.1;
    mesh.material.specular.b = mesh.material.specular.b*.5 +.1;
    
    //mesh.material.side=FrontSide;
    //mesh.material.depthTest=true;
    mesh.material.side=BackSide;
    mesh.material.depthWrite=false;
    mesh.matrixAutoUpdate=false;
    mesh.renderOrder = 1 + envObj.glassList.length;
    envObj.glassList.push(mesh);
    envObj.glassGroup.add(mesh);
    
    let cFrontGeo = mesh.geometry.clone()
    let cFrontMat= mesh.material.clone();
    cFrontMat.copy( mesh.material );
    let cFrontMesh = new Mesh( cFrontGeo, cFrontMat );
    cFrontMesh.name = mesh.name+"_Front";
    cFrontMesh.material.shininess=40;
    cFrontMesh.material.side=FrontSide;
    cFrontMesh.matrixAutoUpdate=false;
    cFrontMesh.renderOrder = 1 + envObj.glassList.length;
    
    
    let curPos=mesh.position;
    let curRot=mesh.rotation;
    let curScale=mesh.scale;
    
    cFrontMesh.rotation.set(curRot.x,curRot.y,curRot.z);
    cFrontMesh.position.set(curPos.x,curPos.y,curPos.z);
    cFrontMesh.scale.set(curScale.x,curScale.y,curScale.z);
    cFrontMesh.updateMatrix();
    

    mesh.parent.add( cFrontMesh )
    envObj.glassList.push(cFrontMesh);
    envObj.glassGroup.add(cFrontMesh);


    return isGlass;
  }
  
  // -- -- --

  // ## Make Instancing actually Instance
  //   Instancing is currently not actually instancing
  //   Object is cloned, raising ram/vram usage
  checkObjectInstancing( envObj, envScene, mesh ){

    if( !envObj.hasOwnProperty("baseInstancesNames") || !envObj.hasOwnProperty("baseInstancesList") ){
      return false;
    }

    if( mesh.hasOwnProperty("userData")
        && mesh.userData.hasOwnProperty("Instance")
        && envObj.baseInstancesList.hasOwnProperty(mesh.userData.Instance) ){
          
          let name = mesh.name;
          this.log("Generate Instance - ",name);
          
          if( !envObj.geoList.hasOwnProperty("InstanceObjects") ){
            envObj.geoList['InstanceObjects']={};
          }
          
          // -- -- --

          // Regex attribute checks --
          
          // Instance Geometry Object for LOD level N
          let lodInstanceRegex = new RegExp( /(instlod)\d+/, "i" );
          let lodNumberRegex = new RegExp( /\d+/, "i" );

          // Instance Switch Distance for LOD level N
          let lodDistRegex = new RegExp( /(instlod)\d+(dist)/, "i" );

          // Instance Mesh Point Skip Rate for LOD level N
          //   If no child mesh with lod level found,
          //     It will auto skip verts to spawn at to help with performance
          let lodSkipRegex = new RegExp( /(instlod)\d+(skip)/, "i" );

          // -- -- --

          // LOD data objects

          let lodMeshes = { 'lod0' : 
            { 
              'mesh': mesh, 
              'dist': 1 
            }
          };

          let instObjectData = {
            'mesh' : mesh,
            'instMesh' : null,
            'dist' : 1,
            'skip' : 0
          }

          // -- -- --

          // Base variables

          let hasLods = false;
          let hasInstSettings = {};
          let instanceBaseObject = null;
          let foundInstLODLevels = {};

          // -- -- --

          // Check for LOD

          try{
            let userDataKeys = Object.keys( mesh.userData );

            userDataKeys.forEach( ( dataKey )=>{
              let isInstanceLODLevel = lodInstanceRegex.test (dataKey );
              if( isInstanceLODLevel ){
                let curLevelNumber = lodNumberRegex.exec( dataKey );
                if( !curLevelNumber || curLevelNumber.length < 1 ){
                  return;
                }
                curLevelNumber = parseInt( curLevelNumber[0] );

                let levelData = {};
                if( foundInstLODLevels.hasOwnProperty( curLevelNumber ) ){
                  levelData = foundInstLODLevels[ curLevelNumber ];
                }else{
                  foundInstLODLevels[ curLevelNumber ] = Object.assign( {}, instObjectData );
                  levelData = foundInstLODLevels[ curLevelNumber ];
                }

                if( lodDistRegex.test( dataKey ) ){
                  let curDist = mesh.userData[ dataKey ];
                  levelData['dist'] = curDist;
                }else if( lodSkipRegex.test( dataKey ) ){
                  let curSkip = mesh.userData[ dataKey ];
                  levelData['skip'] = curSkip;
                }else{
                  levelData['instMesh'] = mesh.userData[ dataKey ];
                }
              }
            });
          }catch(e){
            if( this.verbose >= this.pxlEnums.VERBOSE_LEVEL.ERROR ){
              console.error(e);
            }
          }


          // -- -- --


          // Check for Child LODs levels
          let lodChildren = mesh.children;
          
          for( let x=0; x<lodChildren.length; ++x ){ 
            if( !lodChildren[x].hasOwnProperty("userData") ){
              continue;
            }
            let curUserData = lodChildren[x].userData;
            for( let c=0; c<curUserData.length; ++c ){
              let dataKey = curUserData[c];
              let curKey = lodInstanceRegex.test( dataKey );
              let curDataObj = Object.assign( {}, instObjectData );
              if( curKey ){
                let curLevelNumber = lodNumberRegex.exec( dataKey );
                let onParent = foundInstLODLevels.hasOwnProperty( curLevelNumber );
                if( onParent ){
                  curDataObj = foundInstLODLevels[ curLevelNumber ];
                }
              }
              if( lodDistRegex.test( dataKey ) ){
                let curDist = curUserData[ dataKey ];
                curDataObj['dist'] = curDist;
              }else if( lodSkipRegex.test( dataKey ) ){
                let curSkip = curUserData[ dataKey ];
                curDataObj['skip'] = curSkip;
              }else{
                curDataObj['instMesh'] = curUserData[ dataKey ];
              } 
            }
          }

          // -- -- --

          let lodKeysList = Object.keys( lodMeshes );

          try{
            if( hasLods ){
              instanceBaseObject = new LOD();
              instanceBaseObject.name = name + "_LOD_TOPLVL";
              instanceBaseObject.userData = Object.assign( {},  mesh.userData );
              instanceBaseObject.userData['pxlInstanceToMesh'] = true;
              envObj.geoList['InstanceObjects'][name] = instanceBaseObject;
              envObj.lodList.push( instanceBaseObject );
              mesh.parent.add(instanceBaseObject);
            }

            for( let x=0; x<lodKeysList.length; ++x ){
              let curMesh = lodMeshes[lodKeysList[x]].mesh;
              let curPos = curMesh.position;
              let curRot = curMesh.rotation;
              let curScale = curMesh.scale;
              
              let instBase = envObj.baseInstancesList[ curMesh.userData.Instance ];

              if( curMesh.type == "Mesh" ){
                let matrix = new Matrix4();
                let position = new Vector3();
                let normal = new Vector3();
                let quaternion = new Quaternion();
                let scale = new Vector3(1, 1, 1);
                const hasColor = curMesh.geometry.attributes.hasOwnProperty("color");
                let userDataKeys = Object.keys( curMesh.userData );
                let userDataKeysLower = userDataKeys.map( (c)=> c.toLowerCase() );

                let hasFitScale = false;
                let minScale = 0;
                let maxScale = 1;
                if( userDataKeysLower.includes("minscale") ){
                  hasFitScale = true;
                  minScale = curMesh.userData[ userDataKeys[ userDataKeysLower.indexOf("minscale") ] ];
                }
                if( userDataKeysLower.includes("maxscale") ){
                  hasFitScale = true;
                  maxScale = curMesh.userData[ userDataKeys[ userDataKeysLower.indexOf("maxscale") ] ];
                }

                // Prevent dupelicate instances
                //   Verts are split, so neighboring polygons have stacked vertices
                //     'entry' checks those dupes
                let pointRecorder = {};
                let instanceMatricies = [];
                for (let x = 0; x < curMesh.geometry.attributes.position.count; ++x) {
                  position.fromBufferAttribute(curMesh.geometry.attributes.position, x);
                  let entry = position.toArray();

                  // Flatten array elements to 0.01 precision joined by ","
                  entry = this.pxlUtils.flattenArrayToStr( entry );

                  if( !pointRecorder.hasOwnProperty(entry) ){
                    normal.fromBufferAttribute(curMesh.geometry.attributes.normal, x);
                    let randomRot = new Euler( 0,Math.random() * 2 * Math.PI, 0);
                    quaternion.setFromEuler(randomRot);
                    
                    let curInstScale = scale;
                    if( hasColor ){
                      let curScalar = curMesh.geometry.attributes.color.getX(x);
                      if( hasFitScale ){
                        // Scale the object based on object parameter `minScale` & `maxScale`
                        curScalar = minScale + (maxScale - minScale) * curScalar;
                      }
                      curInstScale = new Vector3(curScalar, curScalar, curScalar);
                    }
                    matrix.compose(position, quaternion, curInstScale);
                    instanceMatricies.push( matrix.clone() );
                    pointRecorder[entry]=true;
                  }
                }
                if( instanceMatricies.length > 0 ){
                  let instancedMesh = new InstancedMesh(instBase.geometry, instBase.material, instanceMatricies.length);
                  instancedMesh.instanceMatrix.setUsage(DynamicDrawUsage);

                  instancedMesh.userData = Object.assign( {},  curMesh.userData );
                  instancedMesh.userData['pxlInstanceToMesh'] = true;

                  /*if( instBase.material.hasOwnProperty("curMeshSettings") ){
                    if( instBase.material.curMeshSettings.hasOwnProperty("renderOrder") ){
                      instancedMesh.renderOrder = instBase.material.curMeshSettings.renderOrder;
                    }
                  }*/
                  this.checkForMeshSettings( instancedMesh, instBase.material );
                  
                  for (let x = 0; x < instanceMatricies.length; ++x) {
                    instancedMesh.setMatrixAt( x, instanceMatricies[x] );
                  }


                  instancedMesh.visible = true;
                  instancedMesh.position.set(curPos.x,curPos.y,curPos.z);
                  instancedMesh.rotation.set(curRot.x,curRot.y,curRot.z);
                  instancedMesh.scale.set(curScale.x,curScale.y,curScale.z);

                  instancedMesh.updateMatrix();


                  if( hasLods ){

                    instancedMesh.name = name + "Geo_" + lodKeysList[x];
                    let curDist = lodMeshes[lodKeysList[x]].dist;
                    instanceBaseObject.addLevel(instancedMesh, curDist);

                  }else{

                    instancedMesh.name = name + "Geo";
                    envObj.geoList['InstanceObjects'][name] = instancedMesh;
                    curMesh.parent.add(instancedMesh);
                  }
                  curMesh.visible = false;
                  curMesh.parent.remove(curMesh);
                }
              }else{
                // Clone the base instance; single instance
                let instancedMesh = new InstancedMesh(instBase.geometry, instBase.material, 1);
                instancedMesh.instanceMatrix.setUsage(DynamicDrawUsage);

                instancedMesh.userData = Object.assign( {},  curMesh.userData );
                instancedMesh.userData['pxlInstanceToMesh'] = false;

                this.checkForMeshSettings( instancedMesh, instBase.material );


                let altInstPlacement = false;
                if( curMesh.userData.hasOwnProperty("fixInstMatrix") ){
                  altInstPlacement = !!curMesh.userData.fixInstMatrix;
                }

                if( altInstPlacement ){
                  instancedMesh.rotation.set(curRot.x,curRot.y,curRot.z);
                  instancedMesh.position.set(curPos.x,curPos.y,curPos.z);
                  instancedMesh.scale.set(curScale.x,curScale.y,curScale.z);
                }else{
                  const matrix = new Matrix4();
                  matrix.compose(curPos, new Quaternion().setFromEuler(curRot), curScale);
                  instancedMesh.setMatrixAt(0, matrix);
                }

                instancedMesh.visible=true;
                instancedMesh.updateMatrix();

                if( hasLods ){

                  instancedMesh.name = name + "Geo_" + lodKeysList[x];
                  let curDist = lodMeshes[lodKeysList[x]].dist;
                  instanceBaseObject.addLevel(instancedMesh, curDist);

                }else{

                  instancedMesh.name = name + "Geo";
                  envObj.geoList['InstanceObjects'][name] = instancedMesh;
                  curMesh.parent.add(instancedMesh);
                }
                
                curMesh.visible=false;
                curMesh.parent.remove(curMesh);

              }
            }
          }catch(e){
            if( this.verbose >= this.pxlEnums.VERBOSE_LEVEL.ERROR ){
              console.error(e);
            }
          }

          if( this.verbose >= this.pxlEnums.VERBOSE_LEVEL.DEBUG && hasLods ){
            let status = "Failed";
            let createdLods = -1;
            let lodKeys = [];
            if( instanceBaseObject?.levels ){
              status = "Created";
              createdLods = instanceBaseObject.levels;
              lodKeys = Object.keys( createdLods );
            }

            let printText = "Instance LODs " + status + " - " + name;
            this.log( printText );
            if( lodKeys.length > 0 ){
              printText = "LOD Levels - " + createdLods;
              this.log( printText );
              this.log( instanceBaseObject );
            }
          }

          /*
          // Dupe the base object; single dupe
          
          let dupe=instBase.clone();//new Mesh(instBase.geometry.clone());
          dupe.rotation.set(curRot.x,curRot.y,curRot.z);
          dupe.position.set(curPos.x,curPos.y,curPos.z);
          dupe.scale.set(curScale.x,curScale.y,curScale.z);
           
          dupe.visible=true;
          dupe.updateMatrix();
          
          let curSide = FrontSide;
          if(instBase.userData.doubleSided){
            curSide=DoubleSide;
          }
          dupe.material.side=curSide;
          dupe.name = name+"Geo";
            
          envObj.geoList['InstanceObjects'][name]=dupe;
          mesh.parent.add( dupe );
          */
    }
  }
  
  
  
 // -- -- -- -- -- -- -- -- -- -- -- -- -- //
 // -- -- -- -- -- -- -- -- -- -- -- -- -- //
 // -- -- -- -- -- -- -- -- -- -- -- -- -- //
  
  // TODO : Apparently traversing FBX objects with no children on a leaf breaks
  //   Oh well... Just get it done
  
  // Currently only used for loading core scene assets;
  //   Initial Camera Position, Avatar Geometry, Items
  // TODO : Convert into a set of agregate lists, to allow for non-scene structure settings.
  //          If a material has transparency & reflective settings, should be flagged "glass"  
  //            Even if its not in the "Scene" group.
  loadSceneFBX(objPath, imgPath, transList, verboseLoading,meshKey,addToScene){
    if(meshKey!=''){ // Prep for IsLoaded checks
      this.pxlEnv.geoLoadListComplete=0;
      this.pxlEnv.geoLoadList[meshKey]=0;
    }
    let addedGlow=0;
    let pingCount=0;
    
    var fbxLoader=new FBXLoader();
    fbxLoader.load( objPath, (curFbx)=>{
      let groups=curFbx.children;
      let groupTypes={};
      let groupNames=[];

      groups.forEach( (c,x) =>{ 
        let curName = c.name.toLowerCase();
        groupNames.push( curName ); 
        groupTypes[ curName ]=x; 
      });

      groupNames.forEach( (curName) =>{
        if(curName.includes('camera')){
          let ch=groups[groupTypes[curName]].children;
          ch.forEach( (c,x)=>{
            c.matrixAutoUpdate=false;
            if(c.name.includes("position")){
              let toPos=c.position.clone();
              this.pxlCamera.cameraPrevPos=toPos.clone();
              this.pxlCamera.camera.position.copy(toPos);
              this.pxlCamera.cameraPos.copy(toPos);
              this.pxlCamera.camera.updateMatrixWorld();
              this.pxlCamera.cameraBooted=true;
              this.pxlEnv.camInitPos=toPos;
              this.pxlEnv.camThumbPos=this.pxlEnv.camThumbPos.clone().add( toPos.clone() );
            }else if(c.name.includes("lookat")){
              let toLookAt=c.position.clone();
              this.pxlCamera.cameraAimTarget.position.copy( toLookAt );
              this.pxlCamera.camera.lookAt(toLookAt);
              this.pxlCamera.camera.updateMatrixWorld();
              this.pxlCamera.cameraPrevLookAt=new Vector3();
              this.pxlEnv.camInitLookAt=toLookAt;
              this.pxlEnv.camThumbLookAt=this.pxlEnv.camThumbLookAt.clone().add( toLookAt.clone() );
            }else if(c.name.includes("returnposition")){
              let toPos=c.position.clone();
              this.pxlEnv.camReturnPos=toPos;
            }else if(c.name.includes("returnlookat")){
              let toPos=c.position.clone();
              this.pxlEnv.camReturnLookAt=toPos;
            }
          });
          this.pxlDevice.touchMouseData.initialQuat=this.pxlCamera.camera.quaternion.clone();
        }
        
        if(curName.includes('Items')){
          let ch=groups[groupTypes[curName]].children;
          let baseMtl=null;
          let lowGravMtl=null;
          let lizardKingMtl=null;
          let infinityZoomMtl=null;
          let starFieldMtl=null;
          while(ch.length>0){
            let g=ch.pop();
            //ch.push(...c.children);
            if(g.type == "Group"){
              let curChildren=g.children;
              curChildren.forEach( (c)=>{
                if(c.name.includes("Item")){
                  if( g.name.includes("LowGravity")){
                    if(lowGravMtl==null){
                      lowGravMtl=new ShaderMaterial({
                        uniforms:{
                          color:{value : c.material.emissive.clone() },
                          alphaMap:{type:'t',value : c.material.map },
                          cloudNoise:{type : 't',value : this.pxlEnv.cloud3dTexture},
                          time:{ value:this.pxlTimer.msRunner },
                          intensity: {  type:"f", value: 1.5 },
                          rate: { type:"f", value: this.pxlUser.itemRotateRate }
                        },
                        vertexShader:this.pxlShaders.objects.itemVert(),
                        fragmentShader:this.pxlShaders.objects.itemFrag(),
                        transparent:true,
                        side:DoubleSide,
                        depthTest:true,
                        depthWrite:false,
                      });
                    }
                    c.material=lowGravMtl;
                  }else if( g.name.includes("LizardKing") ){
                    if( lizardKingMtl==null ){
                      lizardKingMtl= c.material.clone();
                      lizardKingMtl.emissiveMap=lizardKingMtl.map;
                      lizardKingMtl.emissive=new Color( 0x808080 );
                    }
                    c.material=lizardKingMtl;
                  }else if( g.name.includes("StarField") ){
                    //c.material.emissiveMap=c.material.map;
                    //c.material.emissive=new Color( 0x808080 );
                  }else if( g.name.includes("InfinityZoom") ){
                    if(infinityZoomMtl==null){
                      infinityZoomMtl=new ShaderMaterial({
                        uniforms:{
                          color:{value : c.material.map },
                          cloudNoise:{type : 't',value : this.pxlEnv.cloud3dTexture},
                          time:{ value:this.pxlTimer.msRunner },
                          intensity: {  type:"f", value: 1.0 },
                          rate: { type:"f", value: this.pxlUser.itemRotateRate }
                        },
                        vertexShader:this.pxlShaders.core.defaultVert(),
                        fragmentShader:this.pxlShaders.objects.itemZoomFrag(),
                        transparent:true,
                        side:DoubleSide,
                        depthTest:true,
                        depthWrite:true,
                      });
                    }
                    c.material=infinityZoomMtl;
                  }
                  this.pxlUser.itemList[g.name]=c;
                }else if(c.name.includes("ItemBase")){
                  if(baseMtl==null){
                    baseMtl=new ShaderMaterial({
                      uniforms:{
                        color:{value : c.material.emissive.clone() },
                        alphaMap:{type:'t',value : c.material.map },
                        cloudNoise:{type : 't',value : this.pxlEnv.cloud3dTexture},
                        time:{ value:this.pxlTimer.msRunner },
                        intensity: {  type:"f", value: 1.5 },
                        rate: { type:"f", value: this.pxlUser.itemBaseRotateRate }
                      },
                      vertexShader:this.pxlShaders.objects.itemBaseVert(),
                      fragmentShader:this.pxlShaders.objects.itemBaseFrag(),
                      transparent:true,
                      side:DoubleSide,
                      depthTest:true,
                      depthWrite:false,
                    });
                  }
                  c.material=baseMtl;
                  this.pxlUser.itemBaseList.push(c);
                }
              });
              
              addToScene[0].add(g);
              this.pxlUser.itemGroupList[g.name]=g;
              this.pxlUser.itemListNames.push(g.name);
            }
          }
        }
      });
      
      if(meshKey!=''){
        this.pxlEnv.geoList[meshKey]=curFbx;
        this.pxlEnv.geoLoadList[meshKey]=1;
      }
      this.pxlEnv.geoLoadList[meshKey]=1;

    }, undefined, (err)=>{
      if(meshKey!=''){
        this.pxlEnv.geoLoadList[meshKey]=1;
      }
    });
    
    return fbxLoader;
  }

 // -- -- -- -- -- -- -- -- -- -- -- -- -- //
 // -- -- -- -- -- -- -- -- -- -- -- -- -- //
 // -- -- -- -- -- -- -- -- -- -- -- -- -- //
 
 
 // -- -- -- -- -- -- -- -- -- -- //
 // Environment FBX Loader        //
 // -- -- -- -- -- -- -- -- -- -- //

  loadRoomFBX( envObj, fbxPath = null, meshKey = null, enableLogging = false ){
    if( enableLogging ){
      this.runDebugger = true;
    }else{
      this.runDebugger = false;
    }
    if(meshKey==null){ // Prep for IsLoaded checks
        meshKey = envObj.getName();
    }
    if( !fbxPath ){
      fbxPath = envObj.sceneFile;
    }

    let objPath = envObj.sceneFile ;
    let materialList = envObj.materialList ;

    this.pxlEnv.geoLoadListComplete=0;
    this.pxlEnv.geoLoadList[meshKey]=0;

    let addedGlow=0;
    let envScene=envObj.scene;
    
    // TODO : Do new FBXLoader objects really need to be created?
    //          Sounds like the potential for a memory leak if not handled correctly
    var fbxLoader=new FBXLoader();
    fbxLoader.load( objPath, (curFbx)=>{
      //envScene.add(curFbx);
      let groups=curFbx.children;

      if( groups.length == 1 ){
        let curGroup = groups[0];
        let isMainScene = curGroup.name.toLowerCase().includes("scene");
        if( !isMainScene ){
          groups = curGroup.children;
          console.warn("-- pxlFile.loadRoomFBX() - Top level Environment Group not found --\n Attempting to use the first child group as FBX Scene\n Make sure your top group holds all sub-groups.");
        }
      }

      let groupTypes={};
      let groupNames=[];
      
      groups.forEach( (c,x)=>{
        let curName=c.name.split("_")[0].toLowerCase();
        groupNames.push(curName);
        groupTypes[curName]=x;
      });

      // Verbose Logging Step Count
      let count=0;
      /* - Used as -
        count++;
        this.log("Step Count - ",count);
      */
      
      // @ Loaded Scene File - Environment Group; 'camera'
      if(groupNames.indexOf('camera')>-1){
        let ch=[];
        this.log("Camera - ",groups[groupTypes['camera']]);
        
        let defaultCamLocation = envObj.defaultCamLocation;
        if( groups[groupTypes['camera']].userData.hasOwnProperty("DefaultCam") ){
          defaultCamLocation = groups[groupTypes['camera']].userData.DefaultCam;
          envObj.defaultCamLocation = defaultCamLocation;
        }

        let rootCamObjects = false;
        let checkGroups = groups[groupTypes['camera']].children;
        checkGroups.forEach( (c,x)=>{
          let curName = c.name.toLowerCase();
          if( curName.includes("position") || curName.includes("lookat") || curName.includes("returnposition") || curName.includes("returnlookat") ){
            ch.push(c);
            rootCamObjects=true;
          }else{
            if( c.children.length > 0 ){
              ch.push(...c.children);
            }
          }
        });

        //envObj.geoList['camera']=groups[groupTypes['camera']];
        ch.forEach( (c,x)=>{
          c.matrixAutoUpdate=false;
          let parentName = c.parent.name.toLowerCase();
          if( parentName == groups[groupTypes['camera']].name.toLowerCase() ){
            parentName = "default";
          }
          
          if( !envObj.camLocation.hasOwnProperty(parentName) ){
            envObj.camLocation[parentName]={};
            envObj.camLocation[parentName]["position"]=new Vector3( 0, 0, -10 );
            envObj.camLocation[parentName]["lookat"]=new Vector3( 0, 0, 0 );
          }
          let curName = c.name.toLowerCase();
          if(curName.includes("positionmobile")){
            let toPos=c.position.clone();
            envObj.cameraBooted=true;
            envObj.camInitPos=toPos;
            envObj.camLocation[parentName]["positionmobile"]=toPos;
          }else if(curName.includes("lookatmobile")){
            let toPos=c.position.clone();
            envObj.camInitLookAt=toPos;
            envObj.camLocation[parentName]["lookatmobile"]=toPos;
          }else if(curName.includes("position")){
            let toPos=c.position.clone();
            envObj.cameraBooted=true;
            envObj.camInitPos=toPos;
            envObj.camLocation[parentName]["position"]=toPos;
          }else if(curName.includes("lookat")){
            let toPos=c.position.clone();
            envObj.camInitLookAt=toPos;
            envObj.camLocation[parentName]["lookat"]=toPos;
          }else if(curName.includes("returnposition")){
            let toPos=c.position.clone();
            envObj.camReturnPos=toPos;
            envObj.camLocation[parentName]["returnposition"]=toPos;
          }else if(curName.includes("returnlookat")){
            let toPos=c.position.clone();
            envObj.camReturnLookAt=toPos;
            envObj.camLocation[parentName]["returnlookat"]=toPos;
          }
        });


        // Check for missing Mobile Camera Position/lookat
        let locationList = Object.keys( envObj.camLocation );
        locationList.forEach( (c)=>{
          let curLoc = envObj.camLocation[c];
          if( !curLoc.hasOwnProperty("positionmobile") ){
            curLoc["positionmobile"] = curLoc["position"];
          }
          if( !curLoc.hasOwnProperty("lookatmobile") ){
            curLoc["lookatmobile"] = curLoc["lookat"];
          }
        });

        //this.pxlDevice.touchMouseData.initialQuat=envObj.camera.quaternion.clone();
      }
      
      // @ Loaded Scene File - Environment Group; 'AutoCamPaths'
      if(groupNames.indexOf('AutoCamPaths')>-1){
        let ch=groups[groupTypes['AutoCamPaths']].children;
        this.log("AutoCamPaths - ",groups[groupTypes['AutoCamPaths']]);
        
        this.pxlAutoCam.autoCamPaths[ envObj.getName() ]=[];
        while(ch.length>0){
          let g=ch.pop();
          //ch.push(...c.children);
          if(g.type == "Group"){
            let autoPathDict={};
            let curChildren=g.children;
            curChildren.forEach( (c)=>{
              c.matrixAutoUpdate=false;
              autoPathDict[ c.name ] = c;
            });
            g.visible=false;
            g.matrixAutoUpdate=false;
            envScene.add(g);
            this.pxlAutoCam.autoCamPaths[ envObj.getName() ].push( autoPathDict );
          }
        }
      }

      // @ Loaded Scene File - Environment Group; 'Instances'
      if(groupNames.indexOf('instances')>-1 && this.pxlQuality.detailLimit == 0){
        let ch=[...groups[groupTypes['instances']].children];
        this.log("Instances - ",groups[groupTypes['instances']]);
        
        if( ch.length > 0 ){
          if( !envObj.hasOwnProperty("baseInstancesNames") ){
            envObj.baseInstancesNames = [];
          }
          if( !envObj.hasOwnProperty("baseInstancesList") ){
            envObj.baseInstancesList = {};
          }
          let chList = []
          ch.forEach( (c,x)=>{
            this.checkForUserData( envObj, envScene, c );

            if( materialList.hasOwnProperty( c.name ) ){
              let curMap = null;
              if( c.material.map ){
                curMap = c.material.map;
              }
              c.material= materialList[ c.name ];
              this.checkForMeshSettings( c, c.material );

              if( curMap ){
                if( c.material.uniforms.hasOwnProperty("diffuse") ){
                  c.material.uniforms.diffuse.value = curMap;
                }
                if( c.material.hasOwnProperty("emissiveMap") ){
                  c.material.emissiveMap=curMap;
                  if( c.material.emissive.r>0 ){
                    c.material.emissiveIntensity=c.material.emissive.r;
                  }
                }
              }
              c.matrixAutoUpdate=false;
            }else{
              if( c.material.map && !c.material.emissiveMap && c.material.color.r>0 ){
                let curMap = c.material.map;
                c.material.emissiveMap=curMap;
                c.material.emissiveIntensity=c.material.color.r*.4;
                c.material.emissive= c.material.color.clone();
                
              }
              
            }


            envObj.baseInstancesNames.push( c );
            envObj.baseInstancesList[c.name]=c;
          });
        }
      }
      
      
      // @ Loaded Scene File - Environment Group; 'Lights'
      if(groupNames.indexOf('lights')>-1){
        let ch=groups[groupTypes['lights']].children;
        this.log("Lights - ",groups[groupTypes['lights']]);
        
        while(ch.length>0){
          let c=ch.pop();
          ch.push(...c.children);
          
          if( c.type.includes("Light") ){
            if( !envObj.hasOwnProperty('lightList') ){
                envObj['lightList']={};
            }
            if( !envObj.geoList.hasOwnProperty('lights') ){
                envObj.geoList['lights']=[];
            }

            if( c.type == "PointLight" ){

              let lightTypeSplit = c.name.split("_");
              let lightType = lightTypeSplit[0];

              /*
              // Still working out the kinks for Spot Lights
              //
              if( lightType.toLowerCase() == "spot" ){
                try{
                // Spot Light --

                let spotLightShape = {
                  'color': 0xFFFFFF,
                  'intensity': 1,
                  'distance': 10,
                  'angle': Math.PI/3,
                  'penumbra': 0,
                  'decay': 2
                }
                let newLightData = Object.assign( {}, spotLightShape );
                let spotLight = new SpotLight( newLightData.color,
                    newLightData.intensity,
                    newLightData.distance, 
                    newLightData.angle, 
                    newLightData.penumbra, 
                    newLightData.decay
                  );
                spotLight.name = c.name;
                spotLight.userData = Object.assign( {}, c.userData );

                spotLight.rotation.set( c.rotation.x, c.rotation.y, c.rotation.z );
                spotLight.position.set( c.position.x, c.position.y, c.position.z );

                spotLight.matrixAutoUpdate=false;

                // -- -- --

                if( !envObj.lightList.hasOwnProperty( c.type ) ){
                  envObj.lightList[ c.type ] = [];
                }

                envObj.lightList[ c.type ].push( spotLight );
                envObj.geoList['lights'].push( spotLight );
                envScene.add( spotLight );

                c.parent.remove( c );

                }catch(e){
                  console.log(e);
                }
              }else{
              */
                // Point Light --

                c.decay = Math.max( 1.1, Math.min( 3.0, c.intensity ));
                //c.decay = 2;
                c.distance = c.distance == 0 ?  1000 * c.intensity : c.distance;
              //}
            }

            


            if( !envObj.lightList.hasOwnProperty( c.type ) ){
              envObj.lightList[ c.type ] = [];
            }
            
            envObj.lightList[ c.type ].push( c );
            envObj.geoList['lights'].push( c );
            
            c.matrixAutoUpdate=false;
            envScene.add( c );
          }
        }
      }
      

      // Merged Geo, faster but more polies
      // @ Loaded Scene File - Environment Group; 'MainScene'
      if(groupNames.includes('scene') || groupNames.includes('mainscene')){
        let groupId = groupTypes['scene'] || groupTypes['mainscene'];
        let ch = [...groups[groupId].children];
        this.log("MainScene - ",groups[groupId]);

        let curObjId = -1;
        while(ch.length>0){
          //curObjId++;
          
          /*if( curObjId >= ch.length ){
            break;
          }*/

          let c=ch.pop();
          
          this.log( "Cur Object - ", c.name );
          this.checkForUserData( envObj, envScene, c );

          if(c.isMesh){
            if( c.userData.hasOwnProperty("Show") && (!c.userData.Show || c.userData.Show == 0) ){
              c.visible = false;
            }
            
            c.layers.set( this.pxlEnums.RENDER_LAYER.SCENE );
            envObj.geoList[c.name]=c;
              
            let curSide = FrontSide;
            // @ FBX - User Data; boolean, 'doubleSided'
            if(c.userData.hasOwnProperty("doubleSided") || c.userData.hasOwnProperty("DoubleSided") ){
              curSide=DoubleSide;
            }

            if(c.userData.hasOwnProperty("scripted") || c.userData.hasOwnProperty("Scripted") ){
              if( !envObj.geoList.hasOwnProperty('Scripted') ){
                envObj.geoList['Scripted']={};
              }
              envObj.geoList['Scripted'][c.name]=c;
            }
            
            // Custom material shader was added to this object, apply it
            if( materialList.hasOwnProperty( c.name ) ){
              let curMap = null;
              if( c.material.map ){
                curMap = c.material.map;
              }
              c.material= materialList[ c.name ];

              this.checkForMeshSettings( c, c.material );

              if( curMap ){
                if( c.material.uniforms.hasOwnProperty("diffuse") ){
                  c.material.uniforms.diffuse.value = curMap;
                }
                if( c.material.hasOwnProperty("emissiveMap") ){
                  c.material.emissiveMap=curMap;
                  if( c.material.emissive.r>0 ){
                    c.material.emissiveIntensity=c.material.emissive.r;
                  }
                }
              }
              c.matrixAutoUpdate=false;
              //c.geometry.computeVertexNormals();
              //c.material.shading = SmoothShading;
            }else{
              let curMatList = c.material;
              if( !Array.isArray(c.material) ){
                curMatList = [ c.material ];
              }

              if( !this.checkIsGlassLiquid( envObj, envScene, c, curMatList ) ){
                curMatList.forEach( (m)=>{
                  if( m.map && !m.emissiveMap && m.emissive.r>0 ){
                    m.emissiveMap=m.map;
                    m.emissiveIntensity=m.emissive.r;
                    m.emissive=new Color( 0xFFFFFF );
                  }
                  m.side=curSide;
                  //m.depthWrite=true;
                  //m.depthTest=true;
                  //m.shading = SmoothShading;
                });
              }

              //c.geometry.computeVertexNormals();
              c.matrixAutoUpdate=false;
            }
           
              
          }else{ // Current Object isn't a mesh geometry
            if( c.type.includes("Light") ){
              if( !envObj.lightList.hasOwnProperty( c.type ) ){
                envObj.lightList[ c.type ] = [];
              }
              envObj.lightList[ c.type ].push( c );
            }else if( c.type == "Group" ){
              let addChildren = true;
              
              if(c.userData.hasOwnProperty("scripted") || c.userData.hasOwnProperty("Scripted") ){
                if( !envObj.geoList.hasOwnProperty('Scripted') ){
                  envObj.geoList['Scripted']={};
                }
                envObj.geoList['Scripted'][c.name]=c;
              }
              
              if( addChildren ){
                ch.push( ...c.children );
              }
            }
          }
        }
        envScene.add( ...groups[groupId].children );
      }
      
      
      // ## Restricted to only pxlNav's build
      // @ Loaded Scene File - Environment Group; 'Glass'
      if(groupNames.indexOf('glass')>-1){
        let ch=groups[groupTypes['glass']].children;
        this.log("Glass - ",groups[groupTypes['glass']]);
        
        if( ch.length > 0 ){
            if(!envObj.glassGroup){
                let glassGrp=new Group();
                envObj.glassGroup=glassGrp;
                envScene.add(glassGrp);
            }
            while(ch.length>0){
                let c=ch.pop();
                ch.push(...c.children);
                if(c.isMesh){
            
                    this.checkForUserData( envObj, envScene, c );
          
                    let cBackMat=c.material.clone();
                    c.material=cBackMat;
                    c.material.transparent=true;
                    c.material.opacity=.5;
                    c.material.shininess=20;
                    c.material.specular=c.material.color.clone();
                    c.material.specular.r = c.material.specular.r*.5 +.1;
                    c.material.specular.g = c.material.specular.g*.5 +.1;
                    c.material.specular.b = c.material.specular.b*.5 +.1;
                    
                    //c.material.side=FrontSide;
                    //c.material.depthTest=true;
                    c.material.side=BackSide;
                    c.material.depthWrite=false;
                    c.matrixAutoUpdate=false;
                    c.renderOrder = 1;
                    envObj.glassList.push(c);
                    envObj.glassGroup.add(c);
                    
                    let cFrontGeo = c.geometry.clone()
                    let cFrontMat= c.material.clone();
                    cFrontMat.copy( c.material );
                    let cFrontMesh = new Mesh( cFrontGeo, cFrontMat );
                    cFrontMesh.material.shininess=40;
                    cFrontMesh.material.side=FrontSide;
                    cFrontMesh.matrixAutoUpdate=false;
                    cFrontMesh.renderOrder = 2;
                    
                    
                    let curPos=c.position;
                    let curRot=c.rotation;
                    let curScale=c.scale;
                    
                    cFrontMesh.rotation.set(curRot.x,curRot.y,curRot.z);
                    cFrontMesh.position.set(curPos.x,curPos.y,curPos.z);
                    cFrontMesh.scale.set(curScale.x,curScale.y,curScale.z);
                    cFrontMesh.updateMatrix();
                    
                    envScene.add( cFrontMesh )
                    envObj.glassList.push(cFrontMesh);
                    envObj.glassGroup.add(cFrontMesh);
                }
            }
        }
      }
      

      // @ Loaded Scene File - Environment Group; 'Colliders'
      if(groupNames.indexOf('colliders')>-1){
        let colliderParent=groups[groupTypes['colliders']];
        this.log("Colliders - ",groups[groupTypes['colliders']]);
        
        let colliderGroups=colliderParent.children;
        envObj.collidersExist=colliderGroups.length>0;

        for(let x=0; x<colliderGroups.length; ++x){
          let pName=colliderGroups[x].name.toLowerCase();

          let curChildren=colliderGroups[x].children;
          while(curChildren.length>0){
            let child=curChildren.pop();
            curChildren.push(...child.children);
            if(child.isMesh){
              child.visible=false;

              if( pName == "colliderwalls"){ // Movement limiting walls
                envObj.antiColliderActive=true;
                envObj.antiColliderList.push( child );

              // TODO : ColliderTops are implemented in `Ground` colliders to a degree.
              //          Full removal of the `ColliderTops` collider group is pending
              }else if( pName == "collidertops"){ // Top surface of wall, standable top
                envObj.antiColliderTopActive=true;
                envObj.antiColliderTopList.push( child );

              }else{ // `Ground`, `RoomWarp`, & All Other Colliders

                if( pName == "roomwarpzone"){
                  envObj.hasRoomWarp=true;
                  envObj.roomWarp.push(child);
                }

                envObj.colliderActive=true;
                envObj.colliderList.push( child );
              }

              child.matrixAutoUpdate=false;
              envScene.add(child);
              envObj.geoList[child.name]=child;
            }
          }
        }

        // -- -- --

        // Parse grid Vertex-Faces for collision detection
        //   Prep barycentric coordinate dependency values
        //     Vert-Edge lengths, Edge Dot Products, Vert-Face areas & data
        if( envObj.hasColliderType( this.pxlEnums.COLLIDER_TYPE.FLOOR ) ){
          this.pxlColliders.prepColliders( envObj, this.pxlEnums.COLLIDER_TYPE.FLOOR );
        }else if( envObj.hasColliderType( this.pxlEnums.COLLIDER_TYPE.WALL ) ){
          this.pxlColliders.prepColliders( envObj, this.pxlEnums.COLLIDER_TYPE.WALL );
        }else if( envObj.hasColliderType( this.pxlEnums.COLLIDER_TYPE.WALL_TOP ) ){
          this.pxlColliders.prepColliders( envObj, this.pxlEnums.COLLIDER_TYPE.WALL_TOP );
        }else if( envObj.hasColliderType( this.pxlEnums.COLLIDER_TYPE.ROOM ) ){
          this.pxlColliders.prepColliders( envObj, this.pxlEnums.COLLIDER_TYPE.ROOM );
        }
      }
      
      
      // @ Loaded Scene File - Environment Group; 'PortalExit'
      if(groupNames.indexOf('portalexit')>-1){
        let ch=groups[groupTypes['portalexit']].children;
        this.log("PortalExit - ",groups[groupTypes['portalexit']]);
        
        while(ch.length>0){
          let c=ch.pop();
          c.matrixAutoUpdate=false;
          envObj.hasPortalExit=true;
          envObj.portalList[c.name]=c;
        }
      }  
      
      // @ Loaded Scene File - Environment Group; 'FlatColor'
      if(groupNames.indexOf('flatcolor')>-1){
        let ch=groups[groupTypes['flatcolor']].children;
        this.log("FlatColor - ",groups[groupTypes['flatcolor']]);
        
        while(ch.length>0){
          let c=ch.pop();
          ch.push(...c.children);
          if(c.isMesh){
            
            this.checkForUserData( envObj, envScene, c );
          
            let mtl=new MeshBasicMaterial({
              color:c.material.color.clone()
            });
            //mtl.side=DoubleSide;  
            mtl.side=FrontSide;
            mtl.flatShading=true;
            c.material=mtl;
            c.layers.set( this.pxlEnums.RENDER_LAYER.SCENE );
            c.matrixAutoUpdate=false;
            envScene.add(c);
            //addToScene[1].add(c.clone());
          }
        }
      }
      
      
      


      // @ Loaded Scene File - Environment Group; 'LambertColor'
      if(groupNames.indexOf('lambertcolor')>-1){
        let ch=groups[groupTypes['lambertcolor']].children;
        this.log("LambertColor - ",groups[groupTypes['lambertcolor']]);
        
        while(ch.length>0){
          let c=ch.pop();
          ch.push(...c.children);
          if(c.isMesh){
            
            this.checkForUserData( envObj, envScene, c );
          
            let mtl=new MeshLambertMaterial();
            if(c.material.map){
              let mtlMap=c.material.map.clone();
              mtl.map=mtlMap;
              //mtl.color=new Color( 0x888888 );
              mtl.emissiveMap=mtlMap;
              mtl.emissiveIntensity=.5;
              c.material=mtl;
            }else{
              mtl.color=c.material.color.clone();
              mtl.emissive=c.material.emissive.clone();
              //mtl.side=DoubleSide;
              mtl.side=FrontSide;
              mtl.flatShading=true;
              c.material=mtl;
            }
            
            c.layers.set( this.pxlEnums.RENDER_LAYER.SCENE );
            c.matrixAutoUpdate=false;
            envScene.add(c);
            //addToScene[1].add(c.clone());
          }
        }
      }
       
            
      // @ Loaded Scene File - Environment Group; 'Sky'
      if(groupNames.indexOf('sky')>-1){
        let ch=groups[groupTypes['sky']].children;
        this.log("Sky - ",groups[groupTypes['sky']]);
        
        while(ch.length>0){
          let c=ch.pop();
          ch.push(...c.children);
          if(c.isMesh){
            let curMat = null;
            if( materialList.hasOwnProperty( c.name ) ){
              curMat = materialList[ c.name ];
              this.checkForMeshSettings( c, curMat );
            }else{
              curMat=new ShaderMaterial({
                uniforms:{
                  diffuse: { type:"t",value:c.material.map },
                  noiseTexture: { type:"t",value:null },
                  fogColor:{ value: envScene.fog.color },
                  time:{ value:this.pxlTimer.msRunner },
                  camNear:{ type:"f", value: envObj.camera.near },
                  camFar:{ type:"f", value: envObj.camera.far*.85 },
                  resUV: { value: this.pxlDevice.screenRes },
                },
                vertexShader:this.pxlShaders.scene.skyObjectVert(),
                fragmentShader:this.pxlShaders.scene.skyObjectFrag( this.pxlOptions.skyHaze )
              });
            }
            //c.geometry.computeVertexNormals();
            c.material = curMat;
            c.matrixAutoUpdate = false;
            c.frustumCulled = false;
            c.layers.set( this.pxlEnums.RENDER_LAYER.SKY )
            c.material.depthTest=true;
            c.material.depthWrite=false;

            if( c.material?.uniforms.hasOwnProperty("noiseTexture") && envObj?.cloud3dTexture ){
              c.material.uniforms.noiseTexture.value = envObj.cloud3dTexture;
            }

            // Currently unused as the vapor sky object shader no longer has horizon detection
            //   It's a desired effect as an option, but not currently implemented internally
            // Accessible via the `pxlRoom.materialList` object
            //   Simply add a texture uniform of `depthTexture` to your custom shader
            // TODO : Expand this to allow for depthMap derived fog / env effects in the sky
            if( c.material?.uniforms.hasOwnProperty("depthTexture") && envScene?.renderTarget?.depthTexture ){
              c.material.uniforms.envDiffuse.value = envScene.renderTarget.depthTexture;
            }

            envObj.geoList[ c.name ] = c;
            envObj.materialList[ c.name ] = curMat;
            //envObj.shaderGeoList[c.name]=c;
            envScene.add(c);
          }
        }
      }
       

      // Shader Overrides
      // @ Loaded Scene File - Environment Group; 'AnimatedTextures'
      if(groupNames.indexOf('animatedtextures')>-1){
        let ch=groups[groupTypes['animatedtextures']].children;
        this.log("AnimatedTextures - ",groups[groupTypes['animatedtextures']]);
        
        while(ch.length>0){
          let c=ch.pop();
          ch.push(...c.children);
          if(c.isMesh){
            
            this.checkForUserData( envObj, envScene, c );
          
            let uValues={
                time:{ value:this.pxlTimer.msRunner },
                glowTexture: { type:"t",value:c.material.map },
                cloudNoise:{type : 't',value : this.pxlEnv.cloud3dTexture},
                glowColor: { value: new Vector3( .01,.35,.55 ) },
                intensity: { type:"f", value: .35 },
                rate: { type:"f", value: 2.0 },
                freq: { type:"f", value: 1.0 }
              };
            let vertShader=this.pxlShaders.animated.animTextureVert();
            let fragShader=this.pxlShaders.animated.animTextureFrag();
            
            let curMat=new ShaderMaterial({
              uniforms:uValues,
              vertexShader:vertShader,
              fragmentShader:fragShader,
              transparent:true,
              //depthTest:true,
              //depthWrite:true,
              //side:DoubleSide
              side:FrontSide
            });
            
            //c.geometry.computeVertexNormals();

            c.material=curMat;
            
            c.matrixAutoUpdate=false;
            envScene.add(c);
          }
        }
      }
      
      
      // @ Loaded Scene File - Environment Group; 'ScrollingTextures'
      if(groupNames.indexOf('scrollingtextures')>-1){
        let ch=groups[groupTypes['scrollingtextures']].children;
        this.log("ScrollingTextures - ",groups[groupTypes['scrollingtextures']]);
        
        let scrollScreenSeed=1;
        while(ch.length>0){
          scrollScreenSeed+=1;
          let c=ch.pop();
          ch.push(...c.children);
          if(c.isMesh){
            
            this.checkForUserData( envObj, envScene, c );
          
            let name=c.name;
            let speed=0.05;
            if(name.indexOf("_")>-1){
              speed=name.split("_")[1];
              speed=parseInt(speed)*.01;
            }
            let curMat=new ShaderMaterial({
              uniforms:{
                scrollTexture:{type : 't',value:c.material.map},
                //cloudNoise:{type : 't',value : this.cloud3dTexture},
                time:{ value:this.pxlTimer.msRunner },
                speed: { value: speed },
                seed:{ type:'f',value:scrollScreenSeed*1.1423 },
                boostPerc: { value: 1.0 },
              },
              vertexShader:this.pxlShaders.animated.scrollingTextureVert(),
              fragmentShader:this.pxlShaders.animated.scrollingTextureFrag(),
              transparent:true,
              //side:DoubleSide,
              side:FrontSide,
              //depthTest:true,
              //depthWrite:true
            });
            
            //c.geometry.computeVertexNormals();
            c.material=curMat;
            
            c.matrixAutoUpdate=false;
            envScene.add(c);
          }
        }
      }
      
      
      
      // @ Loaded Scene File - Environment Group; 'UserScreens'
      // TODO : Update to read mask from material
      //          Less magic number reliance
      if(groupNames.indexOf('userscreens')>-1){
        let ch=groups[groupTypes['userscreens']].children;
        this.log("UserScreens - ",groups[groupTypes['userscreens']]);
        
        let userScreenSeed=0;
        // Run the mask layers outside shader calculations
        let maskArray=[ new Vector3(1,0,0), new Vector3(0,1,0), new Vector3(0,0,1) ]
        // Non-included masks
        // TODO : Add method to pass materials through to UserScreens from textureList{}
        let maskPaths=[this.assetRoot+"DJ_Vector_Masks_1.jpg", this.assetRoot+"DJ_Vector_Masks_2.jpg", this.assetRoot+"DJ_Vector_Masks_3.jpg"];
        let modMaskId=0;
        let modPathId=0;
        let modMax=maskArray.length;
        while(ch.length>0){
          let c=ch.pop();
          ch.push(...c.children);
          if(c.isMesh){
            let curMat=new ShaderMaterial({
              uniforms:{
                camExists:{ type:'f',value:0.0 },
                time:{ value:this.pxlTimer.msRunner },
                seed:{ type:'f',value:userScreenSeed*1.1423 },
                alpha:{ type:'f',value:1.0 },
                boostPerc: { value: envObj.userScreenIntensity },
                scale:{ value:new Vector2(100,100)},
                ratio:{ value:new Vector2(1,1)},
                videoFeed:{type : 't',value:null},
                cloudNoise:{type : 't',value : this.pxlEnv.cloud3dTexture},
                maskChannel:{value : maskArray[modMaskId] },
                maskMap:{type : 't',value : this.pxlUtils.loadTexture( maskPaths[modPathId] ) },
              },
              vertexShader:envObj.userScreenVert,
              fragmentShader:envObj.userScreenFrag,
              transparent:true,
              //side:DoubleSide,
              side:FrontSide,
              //depthTest:true,
              //depthWrite:true
            });
                        
            //c.geometry.computeVertexNormals();
            c.material=curMat;
            
            c.matrixAutoUpdate=false;
            envObj.pxlEnv.camScreenData.screenGeoList.push(c)
            
            envScene.add(c);
                        
            userScreenSeed += 1;
            modMaskId = userScreenSeed % modMax;
            modPathId = Math.floor(userScreenSeed/3) % modMax;
          }
        }
      }
      
      
      // @ Loaded Scene File - Environment Group; 'Items'
      if(groupNames.indexOf('items')>-1){
        let ch=groups[groupTypes['items']].children;
        this.log("Items - ",groups[groupTypes['items']]);
        
        while(ch.length>0){
          let g=ch.pop();
          //ch.push(...c.children);
          // ## Set up Environment Assets for Item List
          if(g.type == "Group"){
            let curChildren=g.children;
            if( curChildren.length > 0 ){
              curChildren.forEach( (c)=>{
                if(c.name.includes("Item")){
                  let curMat=new ShaderMaterial({
                      uniforms:{
                          color:{value : c.material.emissive.clone() },
                          alphaMap:{type:'t',value : c.material.map },
                          cloudNoise:{type : 't',value : this.cloud3dTexture},
                          time:{ value:this.pxlTimer.msRunner },
                          intensity: {  type:"f", value: 1.5 },
                          rate: { type:"f", value: this.pxlUser.itemRotateRate }
                      },
                      vertexShader:this.pxlShaders.objects.itemVert(),
                      fragmentShader:this.pxlShaders.objects.itemFrag(),
                      transparent:true,
                      side:DoubleSide,
                      depthTest:true,
                      depthWrite:false,
                  });
                  c.material=curMat;
                  this.pxlUser.itemList[g.name]=c;
                }else if(c.name.toLowerCase().includes("base")){
                  let curMat=new ShaderMaterial({
                      uniforms:{
                          color:{value : c.material.emissive.clone() },
                          alphaMap:{type:'t',value : c.material.map },
                          cloudNoise:{type : 't',value : this.cloud3dTexture},
                          time:{ value:this.pxlTimer.msRunner },
                          intensity: {  type:"f", value: 1.5 },
                          rate: { type:"f", value: this.pxlUser.itemBaseRotateRate }
                      },
                      vertexShader:this.pxlShaders.objects.itemBaseVert(),
                      fragmentShader:this.pxlShaders.objects.itemBaseFrag(),
                      transparent:true,
                      side:DoubleSide,
                      depthTest:true,
                      depthWrite:false,
                  });
                  c.material=curMat;
                  this.pxlUser.itemBaseList.push(c);
                }
              });
              
              envScene.add(g);
              this.pxlUser.itemGroupList[g.name]=g;
              this.pxlUser.itemListNames.push(g.name);
            }
          }
        }
      }
      
      
      // @ Loaded Scene File - Environment Group; 'Scripted'
      if(groupNames.includes('scripted')){
        let ch=groups[groupTypes['scripted']].children;
        this.log("Scripted - ",groups[groupTypes['scripted']]);
        
        while(ch.length>0){
          let c=ch.pop();
          if(c.isMesh){
              envObj.geoList[c.name]=c;
              envScene.add(c);
          }
        }
      }
      

      // @ Loaded Scene File - Environment Group; 'Clickable'
      if(groupNames.includes('clickable')){
        let colliderParent=groups[groupTypes['clickable']];
        this.log("Clickable - ",groups[groupTypes['clickable']]);
        
        let colliderGroups=colliderParent.children;
        for(let x=0; x<colliderGroups.length; ++x){
          let pName=colliderGroups[x].name;
          let curChildren=colliderGroups[x].children;
          while(curChildren.length>0){
            let child=curChildren.pop();
            curChildren.push(...child.children);
            if(child.isMesh){
              let mtl=new MeshBasicMaterial();
              mtl.color=new Color( 0xffffff );
              //mtl.map=child.material.map.clone()
              child.material.emissive=new Color( 0x444444 );
              child.material.emissiveMap=child.material.map;
            
              child.matrixAutoUpdate=false;
            
              if(!envObj.objectClickableObjectList[child.name]){
                envObj.objectClickableObjectList[child.name]={};
              }
              envObj.objectClickableObjectList[child.name][pName]=child;
              envObj.objectClickable.push(child);
              envScene.add(child);
              
              if(pName=="Hover"){
                child.visible=false;
              }
            }
          }
        }
      }
      
      // @ Loaded Scene File - Environment Group; 'Markers'
      if(groupNames.includes('markers')){
        let ch=groups[groupTypes['markers']].children;
        this.log("Markers - ",groups[groupTypes['markers']]);
        
        while(ch.length>0){
          let c=ch.pop();
          envObj.marker[ c.name ]=c.position;
        }
      }

      this.pxlEnv.geoList[meshKey]=curFbx;
      this.pxlEnv.geoLoadList[meshKey]=1;

      envObj.fbxPostLoad();
      
      this.runDebugger = false;
    }, null, (err)=>{
      if(meshKey!=''){
        this.pxlEnv.geoLoadList[meshKey]=1;
      }
      this.log("Error Loading FBX");
      this.log(err);
      
      this.runDebugger = false;
    });
    
    return fbxLoader;
  }


  loadAnimFBX( envObj, meshKey, rigPath, animPath, stateConnections ){
    if(meshKey==''){ // Prep for IsLoaded checks
        meshKey = envObj.getName();
    }
    this.pxlEnv.geoLoadListComplete=0;
    this.pxlEnv.geoLoadList[meshKey]=0;

    let addedGlow=0;
    let envScene=envObj.scene;
    // TODO : Do new FBXLoader objects really need to be created?
    //          Sounds like the potential for a memory leak if not handled correctly
    var fbxLoader=new FBXLoader();
    fbxLoader.load( rigPath, (curFbx)=>{

      let groups=curFbx.children;
      let groupTypes={};
      let groupNames=[];
      
      groups.forEach((c,x)=>{ let curName=c.name.split("_")[0]; groupNames.push(curName); groupTypes[curName]=x; });

      curFbx.traverse((c)=>{
        this.checkForUserData( envObj, envScene, c );

        if(c.userData.hasOwnProperty("doubleSided") && c.userData.doubleSided){
          c.material.side=DoubleSide;
        }
        //if(c.material?.map){
          //console.log(c.material.map);
          //c.material.emissiveMap=c.material.map;
          //c.material.emissiveIntensity=c.material.emissive.r;
          //c.material.emissive=new Color( 0xFFFFFF );
          //c.material.map.colorSpace = NoColorSpace;
          //c.material.map.colorSpace = LinearSRGBColorSpace;
          //c.material.map.colorSpace = SRGBColorSpace;
        //}
      });

      this.pxlAnim.initObject( meshKey, curFbx );

      // -- -- --
      
      this.log("Animation FBX - ",groupNames[0]);
      envScene.add(curFbx);
      curFbx.frustumCulled = false;

      var animLoader = new FBXLoader();
      let promisList = [];
      let animKeys = Object.keys( animPath );
      animKeys.forEach( (animKey)=>{
        let curAnimPath = animPath[animKey];
        let animPromise = new Promise((resolve, reject) => {
          animLoader.load( curAnimPath, (animFbx)=>{
            if( animFbx.animations.length == 0 ){
              this.log("No animations found in file", curAnimPath);
              this.log(animFbx);
              resolve();
            }
            this.pxlAnim.addClips( meshKey, animKey, animFbx );
            this.log("Animation Loaded", animKey);
            resolve();
          }, null, (err)=>{
            this.log("Animation Load Error");
            this.log(err);
            reject(err);
          });
        });
        promisList.push( animPromise );
      });

      Promise.all(promisList).then(() => {
        this.pxlAnim.setStateConnections( meshKey, stateConnections );
        //this.pxlEnv.geoList[meshKey]=curFbx;
        envObj.geoList[meshKey]=curFbx;
        this.pxlEnv.geoLoadList[meshKey]=1;
        envObj.animPostLoad(meshKey);
      }).catch((err) => {
        this.log("Error loading animations", err);
      });

      // -- -- --
      /*
      //envObj.geoList[c.name]=c;
      let count=0;
    
      let runner = -1;
      while(runner < groups.length-1){
        runner++;
        let c=groups[runner];
        if(!c){
          this.log("-- Error, Uncaught Animation Child --");
          this.log("Error Entry- '"+runner+"'");
        }
        
        this.checkForUserData( envObj, envScene, c );
        if(c.isMesh){
          if( c.userData.hasOwnProperty("Show") && (!c.userData.Show || c.userData.Show == 0) ){
            c.visible = false;
          }
          
          envObj.geoList[c.name]=c;
            
          let curSide = FrontSide;
          
          if(c.userData.doubleSided){
            curSide=DoubleSide;
          }
          
          if( materialList.hasOwnProperty( c.name ) ){
            c.material= materialList[ c.name ];
            this.checkForMeshSettings( c, c.material );
            c.matrixAutoUpdate=false;
            continue;
          }
          
          if( c.material.map ){
            c.material.emissiveMap=c.material.map;
            if( c.material.emissive.r>0 ){
              c.material.emissiveIntensity=c.material.emissive.r;
            }
            c.material.emissive=new Color( 0xFFFFFF );
            
            if( !c.material.specularMap && c.material.specular.r>0 ){
              c.material.specularMap=c.material.map;
            }
          }else{
            c.material.emissive = c.material.color;
          }
          //c.material.depthWrite=true;
          c.material.side=curSide;
          //c.geometry.computeVertexNormals();
          //c.matrixAutoUpdate=false;
          c.frustumCulled = false;
          c.matrixAutoUpdate=true;
          //envScene.add(c);
            
        }else if( c.type == "Group" ){
          groups.push( ...c.children );
          c.frustumCulled = false;
        }else if(  c.type == "Bone" ){
          
          c.frustumCulled = false;
          groups.push( ...c.children );
        }else{
          this.log("-- Warning, FBX Animation Bypass --");
          this.log("Bypass Name- '"+c.name+"';\nBypass Type- '"+c.type+"'");
        }
      }

      console.log("animLoaded");
      envObj.animPostLoad(meshKey);
      */
    }, null, (err)=>{
      if(meshKey!=''){
        this.pxlEnv.geoLoadList[meshKey]=1;
      }
    });
    

    return fbxLoader;
  }


// -- -- -- -- --


  pxlShaderBuilder( customUniforms, vertShader, fragShader, defines=null ){
    var mat;
    var uniforms={
      diffuse:{type:"t",value:null},
      time:{ value:this.pxlTimer.msRunner }
    };
    if(customUniforms!=null){
      uniforms=Object.assign({},uniforms,customUniforms);
    }
    
    let shaderOptions={
      uniforms:uniforms,
      vertexShader:vertShader,
      fragmentShader:fragShader
    }
    if( defines ){
      shaderOptions.defines = defines;
    }
    
    mat=new ShaderMaterial( shaderOptions );
    mat.transparent=true;
    mat.depthTest=true;
    
    return mat;
  }


  ////////////////////////////////////////////////////

  removeChildren(curObj){
    var removedCount=0;
    var children=curObj.children;
    for(var x=0; x<children.length; ++x){
      if(children[x].type == "Group"){
        curObj.remove(children[x]);
        removedCount++;
      }
    }
    return removedCount;
  }

  findMesh(curObj){
    var ret=null;
    var children=curObj.children;
    for(var x=0; x<children.length; ++x){
      if(children[x].type == "Mesh"){
        ret=children[x];
        break;
      }
    }
    return ret;
  }

  // Used mainly for groups of objects rather than calculating bbox for an object itself
  getBBoxCentroid(curObj){
    try{
      var objBBox=new Box3().setFromObject(curObj);
      var min=objBBox.min;
      var max=objBBox.max;
      var objCentroid=new Vector3().addVectors(max,min).multiplyScalar(.5);
      curObj.userData={'bbox':objBBox, 'centroid':objCentroid};
      if(mapBookHelper != null){
        mapBookHelper.update();
      }
    }catch(err){
      if(this.verbose >= VERBOSE_LEVEL.ERROR){
        console.log("- - - - - - - - ERROR - - - - - - - -");
        console.log("     Object does not exist.\n           - Error Info -");
        console.log(err);
        console.log("- - - - - - - - - - - - - - - - - - -");
      }
    }
  }
  
  
    loadScript( url ){
        return new Promise( (resolve, reject)=>{
            let script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = url;
            script.async = true;

            script.onreadystatechange = ()=>{ resolve(true); } ;
            script.onload = ()=>{ resolve(true); } ;

            document.head.appendChild(script);
        });
    }
    
    
    xmlHttp(){
        return window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
    }
    
  // ## Added for Google Drive doc reading
  //      Links open within the iframe and fail on header source origin issues
  static getURLContent(url){
    return new Promise( (resolve, reject)=>{
            //let xhrRequest= this.xmlHttp();
            let xhrRequest= window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
      /*xhrRequest.onreadystatechange=(e)=>{
        if(xhrRequest.readyState==4 && xhrRequest.status==200){
          return xhrRequest.responceText;
        }
      };*/
      xhrRequest.open("GET",url);//,false);
      xhrRequest.onload=(e)=>{
        if(xhrRequest.readyState==4 && ( xhrRequest.status>=200 && xhrRequest.status<300 ) ){
          resolve(xhrRequest.response);
        }else{
          resolve(xhrRequest.statusText);
        }
      }
      xhrRequest.onerror=()=>reject(xhrRequest.statusText);
      xhrRequest.send();
    });
  }

  getExternalHTML(url, callback){
    if(!window.XMLHttpRequest){
      window.open( url, '_blank' );
      return;
    }
        let xhrRequest= this.xmlHttp();
    xhrRequest.onreadystatechange=function(){
      if (this.readyState == 4 && this.status == 200) {
        if( callback && typeof( callback ) === 'function' ){
          callback( xhrRequest.responseText );
        }
      }
    }
    xhrRequest.open( 'GET', url, true );
    xhrRequest.responceType='document';
    xhrRequest.send();
  }

  fetchURLStatus(url, cmdRun, resolveDict){
        fetch( url, {
            method: 'HEAD'
        }).then( (resolve) =>{
            cmdRun(resolve.status, resolveDict);
        }).catch( (err)=>{
            cmdRun(404, resolveDict);
        });
  }
    
    
    urlExistsFallback( url ){
        return new Promise( (resolve, reject)=>{
            let xhrRequest= this.xmlHttp();

            xhrRequest.open('HEAD', url, true);
            xhrRequest.send();

            xhrRequest.onreadystatechange=function(){
                if( this.readyState == this.DONE ){
                    resolve( this.status<400 );
                }
            }
            xhrRequest.onerror=(err)=>{
                resolve( false );
            };
            xhrRequest.ontimeout=(err)=>{
                resolve( false );
            };
        });
    }
    
    // Extend browser to handle url requests ( Unused )
    //   While the network layer has been removed,
    //     The primary source of excessive API & server requests,
    //       Retaining the code for future web worker management
    urlExists(url){
        var worker;
        if( false && Worker ){
            worker = new Worker("js/pxlBase/webWorkers/FileWorkerIO.js");  
            //this.workerList.push( worker );
        }
        
        return new Promise( (resolve, reject)=>{
            if( worker ){
                worker.onmessage = function(event) {  
                    resolve( event.data.data );
                };
                worker.onerror = function(event) {  
                    resolve( false );
                };

                worker.postMessage( { type: "urlExists", data: url } );
            }else{
                let resp= this.urlExistsFallback( url ).then( (resp)=>{
                    resolve( resp );
                });
            }
        }).then( (resp)=>{
            if( worker ){
                worker.terminate();
            }
            return resp;
        }).catch( (err)=>{
            if( worker ){
                worker.terminate();
            }
            return false;
        });
    }
}