Source: utilities.js

const THREE = require('three');
const THREEGeometry = require('./three/Geometry').Geometry;
const SpriteText = require('three-spritetext').default;

function resolveURL(url) {
	let actualURL = url;
	const prefix = (require("./zinc").modelPrefix);
	
	if (prefix) {
		if (prefix[prefix.length -1] != '/')
			prefix = prefix + '/';
		const r = new RegExp('^(?:[a-z]+:)?//', 'i');
		if (!r.test(url)) {
			actualURL =  prefix + url;
		}
	}
	
	return actualURL;
}

function createNewURL(target, reference) {
  const getNewURL = (target, reference) => {
    try {
      let newURL = (new URL(target, reference)).href;
      //Make sure the target url does not contain parameters
      if (target && target.split("?").length < 2) {
        const paramsStrings = reference.split("?");
        //There are parameters, add them to the target
        if (paramsStrings.length === 2) {
          newURL = newURL + "?" + paramsStrings[1];
        }
      }
      return newURL;
    } catch {
      console.error(`There is an issue creting the url link with: ${target}.` );
    }
  }
  if (!Array.isArray(target)) {
    return getNewURL(target, reference);
  } else {
    const urls = [];
    target.forEach((url) => {
      urls.push(getNewURL(url, reference));
    });
    return urls;
  }
}

/*
 * Calculate the bounding box of a mesh, values will be
 * set for cachedBox, b1, v1 and v2 and they need to be
 * defined. 
 */
function getBoundingBox(mesh, cachedBox, b1, v1, v2) {
  let influences = mesh.morphTargetInfluences;
  let attributes = undefined;
  if (mesh.geometry)
    attributes = mesh.geometry.morphAttributes;
  let found = false;
  if (influences && attributes && attributes.position) {
    v1.set(0.0, 0.0, 0.0);
    v2.set(0.0, 0.0, 0.0);
    for (let i = 0; i < influences.length; i++) {
      if (influences[i] > 0) {
        found = true;
        b1.setFromArray(attributes.position[i].array);
        v1.add(b1.min.multiplyScalar(influences[i]));
        v2.add(b1.max.multiplyScalar(influences[i]));
      }
    }
    if (found) {
      cachedBox.set(v1, v2);
    }
  }
  if (!found) {
    cachedBox.setFromBufferAttribute(
      mesh.geometry.attributes.position);
  }
  mesh.updateWorldMatrix(true, true);
  cachedBox.applyMatrix4(mesh.matrixWorld);
}


//Convenient function
function loadExternalFile(url, data, callback, errorCallback) {
    // Set up an asynchronous request
    const request = new XMLHttpRequest();
    request.open('GET', resolveURL(url), true);

    // Hook the event that gets called as the request progresses
    request.onreadystatechange = () => {
        // If the request is "DONE" (completed or failed)
        if (request.readyState == 4) {
            // If we got HTTP status 200 (OK)
            if (request.status == 200) {
                callback(request.responseText, data)
            } else { // Failed
                errorCallback(url);
            }
        }
    };

    request.send(null);    
}

function loadExternalFiles(urls, callback, errorCallback) {
    const numUrls = urls.length;
    let numComplete = 0;
    const result = [];

    // Callback for a single file
    function partialCallback(text, urlIndex) {
        result[urlIndex] = text;
        numComplete++;

        // When all files have downloaded
        if (numComplete == numUrls) {
            callback(result);
        }
    }

    for (let i = 0; i < numUrls; i++) {
    	loadExternalFile(urls[i], i, partialCallback, errorCallback);
    }
}


//Get the colours at index
exports.getColorsRGB = (colors, index) => {
    const index_in_colors = Math.floor(index/3);
    const remainder = index%3;
    let hex_value = 0;
    if (remainder == 0)
    {
        hex_value = colors[index_in_colors].r;
    }
    else if (remainder == 1)
    {
        hex_value = colors[index_in_colors].g;
    }
    else if (remainder == 2)
    {
        hex_value = colors[index_in_colors].b;
    }
    const mycolor = new THREE.Color(hex_value);
    return [mycolor.r, mycolor.g, mycolor.b];
}

exports.updateMorphColorAttribute = function(targetGeometry, morph) {
  if (morph && targetGeometry && targetGeometry.morphAttributes &&
    targetGeometry.morphAttributes[ "color" ]) {
    const morphColors = targetGeometry.morphAttributes[ "color" ];
    const influences = morph.morphTargetInfluences;
    const length = influences.length;
    targetGeometry.deleteAttribute( 'morphColor0' );
    targetGeometry.deleteAttribute( 'morphColor1' );
    let bound = 0;
    let morphArray = [];
    for (let i = 0; (1 > bound) || (i < length); i++) {
      if (influences[i] > 0) {
        bound++;
        morphArray.push([i, influences[i]]);
      }
    }
    if (morphArray.length == 2) {
      targetGeometry.setAttribute('morphColor0', morphColors[ morphArray[0][0] ] );
      targetGeometry.setAttribute('morphColor1', morphColors[ morphArray[1][0] ] );
    } else if (morphArray.length == 1) {
      targetGeometry.setAttribute('morphColor0', morphColors[ morphArray[0][0] ] );
      targetGeometry.setAttribute('morphColor1', morphColors[ morphArray[0][0] ] );
    }
  }
}


exports.toBufferGeometry = (geometryIn, options) => {
  let geometry = undefined;
  if (geometryIn instanceof THREEGeometry) {
    if (options.localTimeEnabled && !geometryIn.morphNormalsReady && 
      (geometryIn.morphNormals == undefined || geometryIn.morphNormals.length == 0))
      geometryIn.computeMorphNormals();
    geometry = geometryIn.toIndexedBufferGeometry();
    if (options.localMorphColour) {
      copyMorphColorsToIndexedBufferGeometry(geometryIn, geometry);
    }
  } else if (geometryIn instanceof THREE.BufferGeometry) {
    geometry = geometryIn.clone();
  }
  geometry.colorsNeedUpdate = true;
  geometry.computeBoundingBox();
  geometry.computeBoundingSphere();
  if (geometryIn._video)
    geometry._video = geometryIn._video;
  return geometry;
}

exports.copyMorphColorsToBufferGeometry = (geometry, bufferGeometry) => {
  if (geometry && geometry.morphColors && geometry.morphColors.length > 0 ) {
    let array = [];
    let morphColors = geometry.morphColors;
    const getColorsRGB = require("./utilities").getColorsRGB;
    for ( var i = 0, l = morphColors.length; i < l; i ++ ) {
      let morphColor = morphColors[ i ];
      let colorArray = [];
      for ( var j = 0; j < geometry.faces.length; j ++ ) {
        let face = geometry.faces[j];
        let color = getColorsRGB(morphColor.colors, face.a);
        colorArray.push(color[0], color[1], color[2]);
        color = getColorsRGB(morphColor.colors, face.b);
        colorArray.push(color[0], color[1], color[2]);
        color = getColorsRGB(morphColor.colors, face.c);
        colorArray.push(color[0], color[1], color[2]);
      }
      var attribute = new THREE.Float32BufferAttribute( geometry.faces.length * 3 * 3, 3 );
      attribute.name = morphColor.name;
      array.push( attribute.copyArray( colorArray ) );
    }
    bufferGeometry.morphAttributes[ "color" ] = array; 
  }
}


const copyMorphColorsToIndexedBufferGeometry = (geometry, bufferGeometry) => {
  if (geometry && geometry.morphColors && geometry.morphColors.length > 0 ) {
    let array = [];
    let morphColors = geometry.morphColors;
    const getColorsRGB = require("./utilities").getColorsRGB;
    for ( let i = 0, l = morphColors.length; i < l; i ++ ) {
      const morphColor = morphColors[ i ];
      const colorArray = [];
      for ( let j = 0; j < morphColor.colors.length * 3; j ++ ) {
        let color = getColorsRGB(morphColor.colors, j);
        colorArray.push(color[0], color[1], color[2]);
      }
      const attribute = new THREE.Float32BufferAttribute( colorArray, 3 );
      attribute.name = morphColor.name;
      array.push( attribute );
    }
    bufferGeometry.morphAttributes[ "color" ] = array; 
  }
}

exports.mergeVertices = ( geometry, tolerance = 1e-4 ) => {

  tolerance = Math.max( tolerance, Number.EPSILON );

  // Generate an index buffer if the geometry doesn't have one, or optimize it
  // if it's already available.
  var hashToIndex = {};
  var indices = geometry.getIndex();
  var positions = geometry.getAttribute( 'position' );
  var vertexCount = indices ? indices.count : positions.count;

  // next value for triangle indices
  var nextIndex = 0;

  // attributes and new attribute arrays
  var attributeNames = Object.keys( geometry.attributes );
  var attrArrays = {};
  var morphAttrsArrays = {};
  var newIndices = [];
  var getters = [ 'getX', 'getY', 'getZ', 'getW' ];

  // initialize the arrays
  for ( var i = 0, l = attributeNames.length; i < l; i ++ ) {
      var name = attributeNames[ i ];

      attrArrays[ name ] = [];

      var morphAttr = geometry.morphAttributes[ name ];
      if ( morphAttr ) {

          morphAttrsArrays[ name ] = new Array( morphAttr.length ).fill().map( () => [] );

      }

  }

  // convert the error tolerance to an amount of decimal places to truncate to
  var decimalShift = Math.log10( 1 / tolerance );
  var shiftMultiplier = Math.pow( 10, decimalShift );
  for ( var i = 0; i < vertexCount; i ++ ) {

      var index = indices ? indices.getX( i ) : i;

      // Generate a hash for the vertex attributes at the current index 'i'
      var hash = '';
      for ( var j = 0, l = attributeNames.length; j < l; j ++ ) {

          var name = attributeNames[ j ];
          var attribute = geometry.getAttribute( name );
          var itemSize = attribute.itemSize;

          for ( var k = 0; k < itemSize; k ++ ) {

              // double tilde truncates the decimal value
              hash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * shiftMultiplier ) },`;

          }

      }

      // Add another reference to the vertex if it's already
      // used by another index
      if ( hash in hashToIndex ) {

          newIndices.push( hashToIndex[ hash ] );

      } else {

          // copy data to the new index in the attribute arrays
          for ( var j = 0, l = attributeNames.length; j < l; j ++ ) {

              var name = attributeNames[ j ];
              var attribute = geometry.getAttribute( name );
              var morphAttr = geometry.morphAttributes[ name ];
              var itemSize = attribute.itemSize;
              var newarray = attrArrays[ name ];
              var newMorphArrays = morphAttrsArrays[ name ];

              for ( var k = 0; k < itemSize; k ++ ) {

                  var getterFunc = getters[ k ];
                  newarray.push( attribute[ getterFunc ]( index ) );

                  if ( morphAttr ) {

                      for ( var m = 0, ml = morphAttr.length; m < ml; m ++ ) {

                          newMorphArrays[ m ].push( morphAttr[ m ][ getterFunc ]( index ) );

                      }

                  }

              }

          }

          hashToIndex[ hash ] = nextIndex;
          newIndices.push( nextIndex );
          nextIndex ++;

      }

  }

  // Generate typed arrays from new attribute arrays and update
  // the attributeBuffers
  const result = geometry.clone();
  for ( var i = 0, l = attributeNames.length; i < l; i ++ ) {

      var name = attributeNames[ i ];
      var oldAttribute = geometry.getAttribute( name );
      var attribute;

      var buffer = new oldAttribute.array.constructor( attrArrays[ name ] );
      if ( oldAttribute.isInterleavedBufferAttribute ) {

          attribute = new THREE.BufferAttribute( buffer, oldAttribute.itemSize, oldAttribute.itemSize );

      } else {

          attribute = geometry.getAttribute( name ).clone();
          attribute.setArray( buffer );

      }

      result.setAttribute( name, attribute );

      // Update the attribute arrays
      if ( name in morphAttrsArrays ) {

          for ( var j = 0; j < morphAttrsArrays[ name ].length; j ++ ) {

              var morphAttribute = geometry.morphAttributes[ name ][ j ].clone();
              morphAttribute.setArray( new morphAttribute.array.constructor( morphAttrsArrays[ name ][ j ] ) );
              result.morphAttributes[ name ][ j ] = morphAttribute;

          }

      }

  }

  // Generate an index buffer typed array
  var cons = Uint8Array;
  if ( newIndices.length >= Math.pow( 2, 8 ) ) cons = Uint16Array;
  if ( newIndices.length >= Math.pow( 2, 16 ) ) cons = Uint32Array;

  var newIndexBuffer = new cons( newIndices );
  var newIndices = null;
  if ( indices === null ) {

      newIndices = new THREE.BufferAttribute( newIndexBuffer, 1 );

  } else {

      newIndices = geometry.getIndex().clone();
      newIndices.setArray( newIndexBuffer );

  }

  result.setIndex( newIndices );

  return result;
}

function PhongToToon(materialIn) {
	if (materialIn.isMeshPhongMaterial) {
		let material = new THREE.MeshToonMaterial({
			color : materialIn.color.clone(),
			morphTargets : materialIn.morphTargets,
			morphNormals : materialIn.morphNormals,
			vertexColors : materialIn.vertexColors,
			transparent : materialIn.transparent,
			opacity : materialIn.opacity,
			side : materialIn.side
		});
		if (materialIn.map)
			material.map = materialIn.map;
		return material;
	}

	return materialIn;
}

/**
 * Create and return a new buffer geometry with the size of length,
 * and initial coords.
 */
function createBufferGeometry(length, coords) {
  if (coords && (length >= coords.length)) {
    const geometry = new THREE.BufferGeometry()
    const vertices = new Float32Array(length * 3);
    let i = 0;
    coords.forEach(coord => {
      vertices[i++] = coord[0];
      vertices[i++] = coord[1];
      vertices[i++] = coord[2];
    });
    geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
    geometry.setDrawRange(0, coords.length);
    return geometry;
  }
  return undefined;
};

function getCircularTexture() {
  const image = new Image();
  image.src = require("./assets/disc.png");
  const texture = new THREE.Texture();
  texture.image = image;
  texture.needsUpdate = true;
  return texture;
}

function createNewSpriteText(text, height, colour, font, pixel, weight) {
  const sprite = new SpriteText(text, height, colour, font, pixel, weight);
  sprite.material.sizeAttenuation = false;
  sprite.material.alphaTest = 0.5;
  sprite.material.transparent = true;
  sprite.material.depthWrite = false;
  sprite.material.depthTest = false;
  sprite.center.set(0.5, -1.2);
  sprite.renderOrder = 10000;
  return sprite;
}

exports.getBoundingBox = getBoundingBox;
exports.createNewURL = createNewURL;
exports.createBufferGeometry = createBufferGeometry;
exports.getCircularTexture = getCircularTexture;
exports.resolveURL = resolveURL;
exports.loadExternalFile = loadExternalFile;
exports.loadExternalFiles = loadExternalFiles;
exports.PhongToToon = PhongToToon;
exports.createNewSpriteText = createNewSpriteText;