Source: TestObjects.js

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

/**
 * CS559 3D World Framework Code
 *
 * Test Objects - these examples are for use in testing the framework
 * and are less generally useful
 *
 * @module TestObjects 
 */

// we need to have the BaseClass definition
import { GrObject } from "./GrObject.js";

// a global variable to keep track of how many objects we create
// this allows us to give unique names
let testobjsctr = 0;

import * as T from "../CS559-Three/build/three.module.js";

function degreesToRadians(deg) {
  return (deg * Math.PI) / 180;
}

/**
 * A simple object that is like a dump truck (with a hinge), but just made of
 * boxes.
 * A simple way to test a parametric object
 * 
 * It's also a simple example of a hierarchical object
 */
export class HingeCube extends GrObject {
  constructor() {
    const group = new T.Group();
    const geometry = new T.BoxGeometry(1, 0.5, 1);

    const mesh1 = new T.Mesh(
      geometry,
      new T.MeshStandardMaterial({ color: 0xa0a000 })
    );
    mesh1.position.y = 0.25;

    const mesh2 = new T.Mesh(
      geometry,
      new T.MeshStandardMaterial({ color: 0xffff00 })
    );
    mesh2.position.y = 0.25;
    mesh2.position.z = 0.5;

    // set group with origin at pivot point
    group.add(mesh1);
    const g2 = new T.Group();
    g2.position.set(0, 0.5, -0.5);
    g2.add(mesh2);
    group.add(g2);

    super(`HingeCube-${testobjsctr++}`, group, [
      ["x", -5, 5, 2],
      ["z", -5, 5, 2],
      ["theta", -180, 180, 0],
      ["tilt", 0, 90, 0]
    ]);

    this.group = group;
    this.mesh1 = mesh1;
    this.mesh2 = mesh2;
    this.g2 = g2;
  }

  update(paramValues) {
    this.group.position.x = paramValues[0];
    this.group.position.z = paramValues[1];
    this.group.rotation.y = degreesToRadians(paramValues[2]);
    this.g2.rotation.x = degreesToRadians(-paramValues[3]);
  }
}

// for faking deferred loading
// from https://flaviocopes.com/javascript-sleep/
const sleep = milliseconds => {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
};

/**
 * test for an object that is created slowly (like loading an OBJ)
 *
 * the catch is that we need to have an object to install in the world
 * (since we can't defer that), but we don't have "the" object
 *
 * the trick: make a Group - when the deferred object finally arrives,
 * stick it in the group
 *
 * here, we fake OBJ loading with sleep
 */
export class DelayTest extends GrObject {
  constructor() {
    const group = new T.Group();
    super("Delay-Test", group);
    this.group = group;
    // use sleep, rather than OBJ loader
    sleep(1500).then(function() {
      group.add(
        new T.Mesh(
          new T.TorusKnotGeometry(),
          new T.MeshStandardMaterial({ color: "red" })
        )
      );
    });
  }
}

/**
 * Better delayed object - put a proxy object in its place, and then remove it
 */
export class BetterDelayTest extends GrObject {
  constructor() {
    const group = new T.Group();
    super("Delay-Test", group);
    this.group = group;
    // make a cube that will be there temporarily
    const tempCube = new T.Mesh(
      new T.BoxGeometry(),
      new T.MeshStandardMaterial()
    );
    group.add(tempCube);
    // use sleep, rather than OBJ loader
    sleep(2000).then(function() {
      group.remove(tempCube);
      group.add(
        new T.Mesh(
          new T.TorusKnotGeometry(),
          new T.MeshStandardMaterial({ color: "purple" })
        )
      );
    });
  }
}

/**
 * test for changing an object's material after some delay
 */
export class MaterialDelayTest extends GrObject {
  constructor() {
    const group = new T.Group();
    super("Delay-Test", group);

    this.material = new T.MeshStandardMaterial({ color: "white" });
    this.geometry = new T.TorusGeometry();
    this.mesh = new T.Mesh(this.geometry, this.material);

    group.add(this.mesh);
    group.position.x = -3;

    const self = this;

    // use sleep, rather than OBJ loader
    sleep(1000).then(function() {
      // note: we can't use "this" because this isn't lexically scoped
      self.material.setValues({ color: "red" });
      self.material.needsUpdate = true;
    });
  }
}

export class CheckSign extends GrObject {
  /**
   *
   * @param {Object} props
   * @param {number} [props.checks=4] - number of squares per side
   * @param {string} [props.colortype="vertex"] - vertex,face,none
   * @param {number} [props.x]
   * @param {number} [props.y]
   * @param {number} [props.z]
   * @param {number} [props.scale=1]
   * @param {THREE.Color | string | Number} [props.materialcolor]
   */
  constructor(props = {}) {
    const group = new T.Group();
    super("CheckSign1", group);

    // let geometry = new T.Geometry();
    const geometry = new T.BufferGeometry();

    const nchecks = props.checks ?? 4;
    const nverts = nchecks + 1;
    const scale = props.scale > 0.0001 ? props.scale : 1; // disallow 0

    let colortype;
    switch (props.colortype && props.colortype[0]) {
      case "v":
        colortype = T.VertexColors;
        break;
      case "f":
        colortype = T.FaceColors;
        break;
      case "n":
        colortype = T.NoColors;
        break;
      default:
        console.log(`no or bad colortype - assuming vertex`);
        colortype = T.VertexColors;
    }

    const vertexIndex = []

    for (let i = 0; i < nverts + 1; i++) {
      for (let j = 0; j < nverts; j++) {
        vertexIndex.push([i, j, 0]);
      }
    }

    const vertices = []
    const colors = []

    for (let i = 0; i < nchecks; i++) {
      for (let j = 0; j < nchecks; j++) {
        vertices.push(...vertexIndex[i * nverts + j])
        vertices.push(...vertexIndex[i * nverts + j + 1])
        vertices.push(...vertexIndex[(i + 1) * nverts + j])

        vertices.push(...vertexIndex[i * nverts + j + 1])
        vertices.push(...vertexIndex[(i + 1) * nverts + j + 1])
        vertices.push(...vertexIndex[(i + 1) * nverts + j])

        const faceColor1 = (new T.Color('red')).toArray()
        
        colors.push(...faceColor1);
        colors.push(...faceColor1);
        colors.push(...faceColor1);

        colors.push(1, 0, 0);
        colors.push(1, 1, 1);
        colors.push(0, 0, 1);
      }
    }

    geometry.setAttribute('position', new T.BufferAttribute(Float32Array.from(vertices), 3))
    geometry.setAttribute('color', new T.BufferAttribute(Float32Array.from(colors), 3))

    geometry.computeVertexNormals();

    const materialProps = { 
      side: T.DoubleSide, 
      vertexColors: colortype 
    };
    if (props.materialcolor) materialProps["color"] = props.materialcolor;
    const material = new T.MeshStandardMaterial(materialProps);

    const mesh = new T.Mesh(geometry, material);
    // center at 0,0
    mesh.scale.set(scale, scale, scale);
    // warning - scale does not affect translation!
    mesh.translateX(scale * (-nchecks / 2));
    mesh.translateY(scale * (-nchecks / 2));

    group.add(mesh);

    group.position.x = Number(props.x) || 0;
    group.position.y = Number(props.y) || 0;
    group.position.z = Number(props.z) || 0;
  }
}