Source: utilities.js

import * as THREE from 'three/webgpu';
import discPNG from './assets/disc.png';
import { requestRenderAll } from './renderRequests';
import { createTextSprite } from './textSprite';

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

/*
 * Compute the world matrix of an object from its ancestors and return it.
 * three.js only recomputes world matrices flagged by matrixWorldNeedsUpdate,
 * which objects with matrixAutoUpdate set to false (zinc meshes and region
 * groups) never set, they would otherwise stay stale until the next render.
 */
function updateWorldMatrixFromAncestors(object) {
  if (object.parent) {
    updateWorldMatrixFromAncestors(object.parent);
  }
  if (object.matrixAutoUpdate) {
    object.updateMatrix();
  }
  if (object.matrixWorldAutoUpdate !== false) {
    if (object.parent) {
      object.matrixWorld.multiplyMatrices(object.parent.matrixWorld, object.matrix);
    } else {
      object.matrixWorld.copy(object.matrix);
    }
  }
  return object.matrixWorld;
}

/*
 * 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);
  }
  cachedBox.applyMatrix4(updateWorldMatrixFromAncestors(mesh));
}

//Convenient function
function loadExternalFile(url, data, callback, errorCallback) {
  // Set up an asynchronous request
  fetch(url)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.text();
    })
    .then((responseText) => {
      callback(responseText, data);
    })
    .catch((error) => {
      console.error(`Fetch string payload retrieval failed for: ${requestURL}`, error);
      errorCallback(url);
    });
}

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
const 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];
};

//morphColor0/morphColor1 attribute -> morph colour target last copied into it
const morphColorSources = new WeakMap();

const 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;
    //Find the first two morph targets with non-zero influence
    let found = 0;
    let index0 = 0;
    let index1 = 0;
    let weight0 = 0;
    let weight1 = 0;
    for (let i = 0; i < length && found < 2; i++) {
      if (influences[i] > 0) {
        if (found === 0) {
          index0 = index1 = i;
          weight0 = influences[i];
        } else {
          index1 = i;
          weight1 = influences[i];
        }
        found++;
      }
    }
    //morphColorMix (0 = fully morphColor0, 1 = fully morphColor1) drives the
    //TSL colorNode set up by applyMorphColorNode - keep it in sync with
    //whichever two morph targets currently have non-zero influence.
    let mix = 0;
    if (found >= 2) {
      const total = weight0 + weight1;
      mix = total > 0 ? weight1 / total : 0;
    }
    //Keep morphColor0/morphColor1 as two persistent attributes and mutate
    //their contents in place (array.set + needsUpdate) rather than
    //swapping which BufferAttribute object is bound under those names
    //every call. WebGPU's pipeline is built once against whichever
    //attribute object first occupies a given slot; a brand new object
    //bound in later (as deleteAttribute/setAttribute would do every frame)
    //isn't guaranteed to be picked up by an already-built pipeline, which
    //silently renders black. Mutating a single long-lived attribute is the
    //standard, well tested update path both the WebGL and WebGPU
    //backends use everywhere else.
    let attribute0 = targetGeometry.getAttribute('morphColor0');
    let attribute1 = targetGeometry.getAttribute('morphColor1');
    if (!attribute0 || !attribute1) {
      attribute0 = new THREE.Float32BufferAttribute(
        new Float32Array(morphColors[0].array.length),
        morphColors[0].itemSize,
      );
      attribute1 = new THREE.Float32BufferAttribute(
        new Float32Array(morphColors[0].array.length),
        morphColors[0].itemSize,
      );
      attribute0.setUsage(THREE.DynamicDrawUsage);
      attribute1.setUsage(THREE.DynamicDrawUsage);
      targetGeometry.setAttribute('morphColor0', attribute0);
      targetGeometry.setAttribute('morphColor1', attribute1);
    }
    //Only copy and upload when the pair of keyframes changes, between
    //keyframes only morphColorMix changes.
    const source0 = morphColors[index0];
    const source1 = morphColors[index1];
    if (morphColorSources.get(attribute0) !== source0) {
      attribute0.array.set(source0.array);
      attribute0.needsUpdate = true;
      morphColorSources.set(attribute0, source0);
    }
    if (morphColorSources.get(attribute1) !== source1) {
      attribute1.array.set(source1.array);
      attribute1.needsUpdate = true;
      morphColorSources.set(attribute1, source1);
    }
    const morphColorMix =
      morph.material &&
      morph.material.userData &&
      morph.material.userData.uniforms &&
      morph.material.userData.uniforms.morphColorMix;
    if (morphColorMix) {
      morphColorMix.value = mix;
    }
  }
};

const toBufferGeometry = (geometryIn) => {
  const geometry = geometryIn.clone();
  geometry.colorsNeedUpdate = true;
  geometry.computeBoundingBox();
  geometry.computeBoundingSphere();
  if (geometryIn._video) geometry._video = geometryIn._video;
  return geometry;
};

/**
 * 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.
 */
const 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;
};

const 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(),
      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;
}

/*
 * WebGPURenderer converts 16 bit index buffers to 32 bit and keeps the
 * converted array, doubling the memory used by indices. WebGPU supports 16
 * bit indices natively, three.js only converts arrays whose constructor is
 * exactly Uint16Array while the index format is chosen with
 * instanceof Uint16Array. Indices are viewed through this subclass so they
 * stay 16 bit, the values and getX() are unchanged.
 */
class IndexUint16Array extends Uint16Array {}

function keepIndexType(geometry) {
  const index = geometry?.index;
  if (index && index.array.constructor === Uint16Array) {
    const array = index.array;
    //A view of the same buffer, no copy is made
    index.array = new IndexUint16Array(array.buffer, array.byteOffset, array.length);
  }
}

/*
 * Temporarily view the indices of the object and its descendants as plain
 * Uint16Array, for code which checks the exact array type such as
 * GLTFExporter. Returns a function which undoes the change.
 */
function useStandardIndexType(object) {
  const changed = [];
  object.traverse((child) => {
    const index = child.geometry?.index;
    if (index && index.array instanceof IndexUint16Array) {
      const array = index.array;
      index.array = new Uint16Array(array.buffer, array.byteOffset, array.length);
      changed.push([index, array, index.array]);
    }
  });
  return () => {
    changed.forEach(([index, original, replacement]) => {
      //Only restore if nothing else has replaced the array since
      if (index.array === replacement) {
        index.array = original;
      }
    });
  };
}

//Shared by all pointsets
let circularTexture = undefined;

function getCircularTexture() {
  if (circularTexture) {
    return circularTexture;
  }
  const image = new Image();
  const texture = new THREE.Texture();
  texture.image = image;
  //Only upload once the image is available
  image.onload = () => {
    texture.needsUpdate = true;
    requestRenderAll();
  };
  image.src = discPNG;
  if (image.complete && image.naturalWidth > 0) {
    texture.needsUpdate = true;
  }
  circularTexture = texture;
  return texture;
}

/*
 * Create a text sprite, e.g. marker numbers. The canvas resolution is chosen
 * from the size of the text on screen, pixel is no longer used.
 */
function createNewSpriteText(text, height, colour, font, pixel, weight) {
  const sprite = createTextSprite(text, height, colour, font, weight);
  //Only discard fully transparent pixels to keep the smooth edges
  sprite.material.alphaTest = 0.05;
  sprite.material.transparent = true;
  sprite.material.depthWrite = false;
  sprite.material.depthTest = false;
  sprite.center.set(0.5, -1.2);
  sprite.renderOrder = 10001;
  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;
}

function removeVertexAtIndex(geometry, index, maintainLength) {
  const positionAttribute = geometry.getAttribute('position');
  const normalAttribute = geometry.getAttribute('normal'); // Also update other attributes

  if (!positionAttribute) return;

  const itemSize = positionAttribute.itemSize; // e.g., 3 for x, y, z
  const start = index * itemSize;
  const deleteCount = itemSize;
  let removed = false;

  // Helper to remove elements from a typed array
  const removeElements = (attribute, name) => {
    const array = attribute.array;
    if (array.length < start + deleteCount) {
      return false;
    }
    if (maintainLength) {
      //Shift the remaining values in place and clear the end, this keeps
      //the attribute and its GPU buffer.
      array.copyWithin(start, start + deleteCount);
      array.fill(0, array.length - deleteCount);
      attribute.needsUpdate = true;
    } else {
      const newArray = new array.constructor(array.length - deleteCount);
      newArray.set(array.subarray(0, start));
      newArray.set(array.subarray(start + deleteCount), start);
      geometry.setAttribute(name, new THREE.BufferAttribute(newArray, itemSize));
      geometry.getAttribute(name).needsUpdate = true;
    }
    return true;
  };

  removed = removeElements(positionAttribute, 'position');
  if (normalAttribute) removeElements(normalAttribute, 'normal');
  // Repeat for 'uv', 'color', etc.

  // If the geometry is indexed, the index buffer will also need adjustment
  if (geometry.index !== null) {
    // This is more complex, typically easier to work with non-indexed geometry when modifying vertices
    console.warn('Removing vertices from indexed geometry requires index buffer recalculation.');
  }

  return removed;
}

function copyVector2sArray(attribute, vectors) {
  const array = attribute.array;
  let offset = 0;

  for (let i = 0; i < vectors.length; i++) {
    const v = vectors[i];
    array[offset++] = v.x;
    array[offset++] = v.y;
  }

  attribute.needsUpdate = true;
  return attribute;
}

function copyVector3sArray(attribute, vectors) {
  const array = attribute.array;
  let offset = 0;

  for (let i = 0; i < vectors.length; i++) {
    const v = vectors[i];
    array[offset++] = v.x;
    array[offset++] = v.y;
    array[offset++] = v.z;
  }

  attribute.needsUpdate = true;
  return attribute;
}

function copyVector4sArray(attribute, vectors) {
  const array = attribute.array;
  let offset = 0;

  for (let i = 0; i < vectors.length; i++) {
    const v = vectors[i];
    array[offset++] = v.x;
    array[offset++] = v.y;
    array[offset++] = v.z;
    array[offset++] = v.w;
  }

  attribute.needsUpdate = true;
  return attribute;
}

function copyColorsArray(attribute, colors) {
  const array = attribute.array;
  let offset = 0;

  for (let i = 0; i < colors.length; i++) {
    const c = colors[i];
    array[offset++] = c.r;
    array[offset++] = c.g;
    array[offset++] = c.b;
  }

  attribute.needsUpdate = true;
  return attribute;
}

export {
  copyColorsArray,
  copyVector2sArray,
  copyVector3sArray,
  copyVector4sArray,
  createBufferGeometry,
  createNewSpriteText,
  createNewURL,
  getBoundingBox,
  getCircularTexture,
  keepIndexType,
  updateWorldMatrixFromAncestors,
  useStandardIndexType,
  IndexUint16Array,
  getColorsRGB,
  isRegionGroup,
  loadExternalFile,
  loadExternalFiles,
  mergeGeometries,
  mergeVertices,
  PhongToToon,
  removeVertexAtIndex,
  updateMorphColorAttribute,
  toBufferGeometry,
};