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 creating 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; 
  }
}

/**
 * Merges a set of attributes into a single instance. All attributes must have compatible properties and types.
 * Instances of {@link InterleavedBufferAttribute} are not supported.
 *
 * @param {Array<BufferAttribute>} attributes - The attributes to merge.
 * @return {?BufferAttribute} The merged attribute. Returns `null` if the merge does not succeed.
 */
function mergeAttributes( attributes ) {

  let TypedArray;
  let itemSize;
  let normalized;
  let gpuType = - 1;
  let arrayLength = 0;

  for ( let i = 0; i < attributes.length; ++ i ) {

    const attribute = attributes[ i ];

    if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;
    if ( TypedArray !== attribute.array.constructor ) {

      console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.' );
      return null;

    }

    if ( itemSize === undefined ) itemSize = attribute.itemSize;
    if ( itemSize !== attribute.itemSize ) {

      console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.' );
      return null;

    }

    if ( normalized === undefined ) normalized = attribute.normalized;
    if ( normalized !== attribute.normalized ) {

      console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.' );
      return null;

    }

    if ( gpuType === - 1 ) gpuType = attribute.gpuType;
    if ( gpuType !== attribute.gpuType ) {

      console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.gpuType must be consistent across matching attributes.' );
      return null;

    }

    arrayLength += attribute.count * itemSize;

  }

  const array = new TypedArray( arrayLength );
  const result = new THREE.BufferAttribute( array, itemSize, normalized );
  let offset = 0;

  for ( let i = 0; i < attributes.length; ++ i ) {

    const attribute = attributes[ i ];
    if ( attribute.isInterleavedBufferAttribute ) {

      const tupleOffset = offset / itemSize;
      for ( let j = 0, l = attribute.count; j < l; j ++ ) {

        for ( let c = 0; c < itemSize; c ++ ) {

          const value = attribute.getComponent( j, c );
          result.setComponent( j + tupleOffset, c, value );

        }

      }

    } else {

      array.set( attribute.array, offset );

    }

    offset += attribute.count * itemSize;

  }

  if ( gpuType !== undefined ) {

    result.gpuType = gpuType;

  }

  return result;

}

/**
 * Merges a set of geometries into a single instance. All geometries must have compatible attributes.
 *
 * @param {Array<BufferGeometry>} geometries - The geometries to merge.
 * @param {boolean} [useGroups=false] - Whether to use groups or not.
 * @return {?BufferGeometry} The merged geometry. Returns `null` if the merge does not succeed.
 */
exports.mergeGeometries = ( geometries, useGroups = false ) => {

  const isIndexed = geometries[ 0 ].index !== null;

  const attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) );
  const morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) );

  const attributes = {};
  const morphAttributes = {};

  const morphTargetsRelative = geometries[ 0 ].morphTargetsRelative;

  const mergedGeometry = new THREE.BufferGeometry();

  let offset = 0;

  for ( let i = 0; i < geometries.length; ++ i ) {

    const geometry = geometries[ i ];
    let attributesCount = 0;

    // ensure that all geometries are indexed, or none

    if ( isIndexed !== ( geometry.index !== null ) ) {

      console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.' );
      return null;

    }

    // gather attributes, exit early if they're different

    for ( const name in geometry.attributes ) {

      if ( ! attributesUsed.has( name ) ) {

        console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.' );
        return null;

      }

      if ( attributes[ name ] === undefined ) attributes[ name ] = [];

      attributes[ name ].push( geometry.attributes[ name ] );

      attributesCount ++;

    }

    // ensure geometries have the same number of attributes

    if ( attributesCount !== attributesUsed.size ) {

      console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.' );
      return null;

    }

    // gather morph attributes, exit early if they're different

    if ( morphTargetsRelative !== geometry.morphTargetsRelative ) {

      console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.' );
      return null;

    }

    for ( const name in geometry.morphAttributes ) {

      if ( ! morphAttributesUsed.has( name ) ) {

        console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '.  .morphAttributes must be consistent throughout all geometries.' );
        return null;

      }

      if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = [];

      morphAttributes[ name ].push( geometry.morphAttributes[ name ] );

    }

    if ( useGroups ) {

      let count;

      if ( isIndexed ) {

        count = geometry.index.count;

      } else if ( geometry.attributes.position !== undefined ) {

        count = geometry.attributes.position.count;

      } else {

        console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute' );
        return null;

      }

      mergedGeometry.addGroup( offset, count, i );

      offset += count;

    }

  }

  // merge indices

  if ( isIndexed ) {

    let indexOffset = 0;
    const mergedIndex = [];

    for ( let i = 0; i < geometries.length; ++ i ) {

      const index = geometries[ i ].index;

      for ( let j = 0; j < index.count; ++ j ) {

        mergedIndex.push( index.getX( j ) + indexOffset );

      }

      indexOffset += geometries[ i ].attributes.position.count;

    }

    mergedGeometry.setIndex( mergedIndex );

  }

  // merge attributes

  for ( const name in attributes ) {

    const mergedAttribute = mergeAttributes( attributes[ name ] );

    if ( ! mergedAttribute ) {

      console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' attribute.' );
      return null;

    }

    mergedGeometry.setAttribute( name, mergedAttribute );

  }

  // merge morph attributes

  for ( const name in morphAttributes ) {

    const numMorphTargets = morphAttributes[ name ][ 0 ].length;

    if ( numMorphTargets === 0 ) break;

    mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};
    mergedGeometry.morphAttributes[ name ] = [];

    for ( let i = 0; i < numMorphTargets; ++ i ) {

      const morphAttributesToMerge = [];

      for ( let j = 0; j < morphAttributes[ name ].length; ++ j ) {

        morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] );

      }

      const mergedMorphAttribute = mergeAttributes( morphAttributesToMerge );

      if ( ! mergedMorphAttribute ) {

        console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' morphAttribute.' );
        return null;

      }

      mergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute );

    }

  }

  return mergedGeometry;

}

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.fontFace = font;
  sprite.fontSize = pixel;
  sprite.fontWeight = weight;
  sprite.material.map.generateMipmaps = false;
  sprite.material.map.anisotropy = 4;
  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;
}

/*
 * Check if the compare path match with the region or/and group.
 * comparePath should be in the form of regionPath/Group.
 * * can be used as wildcard.
 * comparePath will be used to compare both region and group if it
 * is a single string without /
 */
function isRegionGroup(regionPath, groupName, comparePath) {
  if (comparePath) {
    const region = regionPath ? regionPath : "";
    const group = groupName ? groupName : "";
    const n = comparePath.lastIndexOf('/');
    if (n > -1) {
      let r = undefined;
      let g = undefined;
      r = comparePath.substring(0, n);
      g = comparePath.substring(n + 1);
      if (r === "*" || r === "**" || r.toLowerCase() === region.toLowerCase()) {
        if (g === "*" || g === "**" || g.toLowerCase() === group.toLowerCase()) {
          return true;
        }
      }
    } else {
      //one single value if one of the region / group matches
      if (region.toLowerCase() === comparePath.toLowerCase() ||
        group.toLowerCase() === comparePath.toLowerCase()) {
        return true;
      }
    }
  }

  return false;
}

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;
exports.isRegionGroup = isRegionGroup;