Source: loaders.js

/*jshint esversion: 11 */
// @ts-check

/**
 * Access to THREE's loaders within the CS559 framework
 * 
 * @module loaders
 */

import * as T from "three";
import { GrObject } from "./GrObject.js";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
import { GrCube } from "./SimpleObjects.js";

/**
 * Rescale an object - assumes that the object is a group with 1 mesh in it
 *
 * @param {T.Object3D} obj
 */
function normObject(obj, scale = 1.0, center = true, ground = true) {
  // since other bounding box things aren't reliable
  const box = new T.Box3();
  box.setFromObject(obj);
  // easier than vector subtract
  const dx = box.max.x - box.min.x;
  const dy = box.max.y - box.min.y;
  const dz = box.max.z - box.min.z;
  const size = Math.max(dx, dy, dz);
  const s = scale / size;
  obj.scale.set(s, s, s);

  if (center) {
    obj.translateX((-s * (box.max.x + box.min.x)) / 2);
    obj.translateZ((-s * (box.max.z + box.min.z)) / 2);
    if (!ground) {
      // only center Y if not grounding
      obj.translateY((-s * (box.max.y + box.min.y)) / 2);
    }
  }
  if (ground) {
    obj.translateY(-box.min.y * s);
  }
}

/**
 * The loaders have optional callbacks that take a GrObject (not an Object3D!)
 * 
 * @callback LoaderCallback
 * @param {GrObject} object
 */

/**
 * A base class of GrObjects loaded from an OBJ file
 * note: this has to deal with the deferred loading
 *
 * Warning: While ObjLoader2 might be better, ObjLoader is simpler
 */
export class ObjGrObject extends GrObject {
  /**
   *
   * @param {Object} params
   * @property {string} params.obj
   * @property {string} [params.mtl]
   * @property {string} [params.texture]
   * @property {string} [params.name]
   * @property {Object} [params.mtloptions]
   * @property {Number} [params.norm] - normalize the object (make the largest dimension this value)
   * @property {Number} [params.x] - initial translate for the group
   * @property {Number} [params.y]
   * @property {Number} [params.z]
   * @property {LoaderCallback} [params.callback]
   */
  constructor(params = {}) {
    if (!params.obj) {
      alert("Bad OBJ object - no obj file given!");
      throw "No OBJ given!";
    }

    const name = params.name || `Objfile(${params.obj})`;
    const objholder = new T.Group();

    super(name, objholder);
    const self = this;

    // if there is a material, load it first, and then have that load the OBJ file
    if (params.mtl) {
      const mtloader = new MTLLoader();

      if (params.mtloptions) {
        mtloader.setMaterialOptions(params.mtloptions);
      }

      // note that the callback then calls the Obj Loader
      mtloader.load(params.mtl, function (myMaterialCreator) {
        myMaterialCreator.preload();

        const objLoader = new OBJLoader();
        objLoader.setMaterials(myMaterialCreator);

        objLoader.load(params.obj, function (obj) {
          if (params.norm) normObject(obj, params.norm);
          objholder.add(obj);
          if (params.callback) params.callback(self);
        });
      });

    } else if (params.texture) {
      let texture = new T.TextureLoader().load(params.texture);
      const objLoader = new OBJLoader();

      objLoader.load(params.obj, function (obj) {
        if (params.norm) normObject(obj, params.norm);
        obj.children[0].material.map = texture;
        objholder.add(obj);
        if (params.callback) params.callback(self);
      });
    } else {
      // no material file, just an obj
      const objLoader = new OBJLoader();

      objLoader.load(params.obj, function (obj) {
        if (params.norm) normObject(obj, params.norm);
        objholder.add(obj);
        if (params.callback) params.callback(self);
      });

    }
    objholder.translateX(Number(params.x) || 0);
    objholder.translateY(Number(params.y) || 0);
    objholder.translateZ(Number(params.z) || 0);
  }
}

/** 
 * load from an FBX file - this is quite simple 
 * it makes a group so it can stick the FBX object in once
 * it is loaded.
 * 
 * Note: if the loading fails, an error is printed in the console
 * and a red cube is made instead of the loaded object. 
 * (added May 2022)
 * */
export class FbxGrObject extends GrObject {
  /**
   *
   * @param {Object} [params]
   * @property {string} params.fbx
   * @property {Number} [params.norm] - normalize the object (make the largest dimension this value)
   * @property {Number} [params.x] - initial translate for the group
   * @property {Number} [params.y]
   * @property {Number} [params.z]
   * @property {String} [params.name]
   * @property {LoaderCallback} [params.callback]
   */
  constructor(params = {}) {
    const name = params.name || `FBXfile(${params.fbx})`;
    const objholder = new T.Group();
    super(name, objholder);
    const self = this;

    const fbx = new FBXLoader();

    fbx.load(params.fbx,
      function (obj) { /* loaded callback */
        if (params.norm) normObject(obj, params.norm);
        objholder.add(obj);
        if (params.callback) params.callback(self);
      },
      undefined, /* progress callback */
      function (error) {  /* error callback */
        console.log(error);
        // put a dummy object in as an error warning
        let tempGr = new GrCube({ color: "red" });
        let obj = tempGr.objects[0];
        if (params.norm) normObject(obj, params.norm);
        objholder.add(obj);
        if (params.callback) params.callback(self);
      }
    );

    objholder.translateX(Number(params.x) || 0);
    objholder.translateY(Number(params.y) || 0);
    objholder.translateZ(Number(params.z) || 0);
  }
}

/**
 * Load from a glTF/GLB file.
 *
 * Note: if loading fails, an error is printed in the console
 * and a red cube is used as an error marker.
 */
export class GltfGrObject extends GrObject {
  /**
   *
   * @param {Object} [params]
   * @property {string} params.gltf - path to a .gltf or .glb file
   * @property {Number} [params.norm] - normalize the object (make the largest dimension this value)
   * @property {Number} [params.x] - initial translate for the group
   * @property {Number} [params.y]
   * @property {Number} [params.z]
   * @property {String} [params.name]
   * @property {LoaderCallback} [params.callback]
   */
  constructor(params = {}) {
    if (!params.gltf) {
      alert("Bad glTF object - no gltf file given!");
      throw "No glTF given!";
    }

    const name = params.name || `glTFfile(${params.gltf})`;
    const objholder = new T.Group();
    super(name, objholder);
    const self = this;

    /** @type {Array<any>} */
    this.animations = [];

    const loader = new GLTFLoader();

    loader.load(
      params.gltf,
      function (gltf) {
        const obj = gltf.scene || new T.Group();
        if (params.norm) normObject(obj, params.norm);
        objholder.add(obj);
        self.animations = gltf.animations || [];
        if (params.callback) params.callback(self);
      },
      undefined,
      function (error) {
        console.log(error);
        // put a dummy object in as an error warning
        let tempGr = new GrCube({ color: "red" });
        let obj = tempGr.objects[0];
        if (params.norm) normObject(obj, params.norm);
        objholder.add(obj);
        if (params.callback) params.callback(self);
      }
    );

    objholder.translateX(Number(params.x) || 0);
    objholder.translateY(Number(params.y) || 0);
    objholder.translateZ(Number(params.z) || 0);
  }
}