/*jshint esversion: 11 */
// @ts-check
/**
* CS559 3D World Framework Code
*
* GrObject: a "thin wrapper" around Three.JS's Object3D to facilitate
* creating UIs and doing animation
*
* @module GrObject
*/
/* students will want to create objects that extend this class */
/*
* This is the main class for the framework. Most of the work involves extending
* the class `GrObject` defined here.
*/
import * as T from "../CS559-Three/build/three.module.js";
/**
* This function converts from the specifications given to the `GrObject`
* constructor into the form used internally. It is the best documentation for
* how those descriptions are interpreted.
*
* when creating an object, a parameter is defined by an array of up to
* 5 things
* name (string)
* min (number)
* max (number)
* initial value (number)
* step size for slider (number)
*
* **Note:** this function is for internal use, but it is exported to convince
* JSDoc to document it.
*
* @param {string|Array} param
*/
export function paramObjFromParam(param) {
const paramObj = {
name: "no name",
min: 0,
max: 1,
initial: 0,
step: 0
};
if (typeof param === "string") {
paramObj.name = param;
} else if (Array.isArray(param)) {
if (param.length > 0) {
paramObj.name = param[0];
}
if (param.length > 1) {
paramObj.min = param[1];
}
if (param.length > 2) {
paramObj.max = param[2];
}
if (param.length > 3) {
paramObj.initial = param[3];
}
if (param.length > 4) {
paramObj.step = param[4];
}
}
// make sure the initial value is legal
if (paramObj.initial < paramObj.min) {
paramObj.initial = paramObj.min;
}
if (paramObj.initial > paramObj.max) {
paramObj.initial = paramObj.max;
}
return paramObj;
}
/**
* @class GrObject
*
* GrObjects have:
* - a name - each object should have a unique name (like an id), but this is not
* enforced
* - parameters (these are things that the user may want to control with sliders)
* - geometry / "Object3D" - they kind of serve like three's groups
* note: animation should not update the parameters
*
* any new object should provide methods for:
* - construction - the constructor needs to call the base class constructor
* and provide the parameters and geometry
* - update - which takes an array of parameters and sets things accordingly
* - stepWorld - which moves the animation ahead a small amount
*
*
* and optionally
* - lookfrom/lookat
*
* Note that a `GrObject` does not add itself to the scene (other things take care
* of that). When the object is added to the world, it's THREE objects are added to
* the `Scene` (the THREE world container).
*
*
*/
export class GrObject {
/**
* The parameter list (if provided) should be either a string
* (with the name of the parameter) or an Array with the first
* value being a string (the name), and the remaining 4 values being
* numbers: min, max, initial value, and step size (all optional).
* @see paramObjFromParam
*
* @param {String} name - unique name for the object
* @param {THREE.Object3D | Array<THREE.Object3D>} objectOrObjects
* @param {Array<string|Array>} [paramInfo] - a list of the parameters for the object
*/
constructor(name, objectOrObjects, paramInfo) {
// simple declarations of defaults so we can easily identify members
/** @type {Array<THREE.Object3D>} */
this.objects = [];
/** @type {Array<Object>} */
this.params = [];
/** @type {String} */
this.name = name;
/** A flag for if this object is ridable - if so, it should be a specific THREE object to
* parent the object to */
/** @type {THREE.Object3D | undefined } */
this.rideable = undefined;
/** the unique ID is a number (non-zero) that comes from the world - set by GrWorld.add*/
/** @type {Number} */
this.id = 0;
/** a flag as to whether this object should be "highlighted" as special - use by the UI */
/** @type {Boolean} */
this.highlighted = false;
// set up the object list
if (Array.isArray(objectOrObjects)) {
// we were given a list - do a deep copy
const objList = this.objects; // deal with the non-lexical this
objectOrObjects.forEach(function(obj) {
objList.push(obj);
});
} else {
// if there is 1 object (there might be zero)
if (objectOrObjects) {
this.objects.push(objectOrObjects);
}
}
// set up the parameters
// we allow specifying parameters in many different ways
// we always convert to lightweight objects
if (paramInfo) {
// Totally OK to have none
const self = this;
paramInfo.forEach(function(param) {
// default values for the parameter in case we don't get any
const paramObj = paramObjFromParam(param);
self.params.push(paramObj);
});
}
}
// methods that must be over-ridden
/**
* Advance the object by an amount of time. Time only flows forward
* so use this to figure out how fast things should move.
* In theory, it is always a "step" (1/60th of a second)
* In the past, so many things were stochastic and only computed the
* delta, that this became the norm (if you need to accumulate time
* you can sum the delta)
* time of day is provided so you can make objects that change over the
* course of the day - it is a number between 0-24 (midnight->midnight)
* it does not necessarily change smoothly.
* Delta is intended to be in "milliseconds" - but it is scaled by the current
* "speed" (and will be zero if time is stopped).
* @param {number} delta
* @param {number} timeOfDay
*/
stepWorld(delta, timeOfDay) {
// by default (base class), does nothing
}
/**
* set the parameter values to new values
* this gets called when the sliders are moved
* @param {Array<Number>} paramValues
*/
update(paramValues) {}
/**
* return a plausible lookfrom/lookat pair to look at this object
* this makes a guess based on the bounding box, but an object may
* want to override to give a better view
*
* Returns an array of 6 numbers (lookfrom X,Y,Z, lookat X, Y, Z)
*
* @returns {Array<Number>}
*/
lookFromLookAt() {
const bbox = new T.Box3();
bbox.setFromObject(this.objects[0]);
const x = (bbox.max.x + bbox.min.x) / 2;
const y = (bbox.max.y + bbox.min.y) / 2;
const z = (bbox.max.z + bbox.min.z) / 2;
// make the box a little bigger to deal with think/small objects
const dx = bbox.max.x - x + 0.05;
const dy = bbox.max.y - y + 0.05;
const dz = bbox.max.z - z + 0.05;
const d = Math.max(dx, dy, dz);
const fx = x + d * 3;
const fy = y + d * 3;
const fz = z + d * 3;
return [fx, fy, fz, x, y, z];
}
/**
* helper method - set the scale of the objects
* note: this sets the scale of all the root level objects
* it doesn't consider what was already there
* also, it is only a uniform method
*
* @param {number} scale=1.0
* @param {number} sy=0
* @param {number} sz=0
*/
setScale(scale = 1.0, sy = 0, sz = 0) {
const syy = sy || scale;
const szz = sz || scale;
this.objects.forEach(e => e.scale.set(scale, syy, szz));
}
/**
* set the position of each (root level) object
*
* @param {number} x
* @param {number} y
* @param {number} z
*/
setPos(x = 0, y = 0, z = 0) {
this.objects.forEach(e => e.position.set(x,y,z));
}
}