src/viewer/scene/nodes/Node.js
import {utils} from '../utils.js';
import {Component} from '../Component.js';
import {math} from '../math/math.js';
const angleAxis = math.vec4(4);
const q1 = math.vec4();
const q2 = math.vec4();
const xAxis = math.vec3([1, 0, 0]);
const yAxis = math.vec3([0, 1, 0]);
const zAxis = math.vec3([0, 0, 1]);
const veca = math.vec3(3);
const vecb = math.vec3(3);
const identityMat = math.identityMat4();
/**
* @desc An {@link Entity} that is a scene graph node that can have child Nodes and {@link Mesh}es.
*
* ## Usage
*
* The example below is the same as the one given for {@link Mesh}, since the two classes work together. In this example,
* we'll create a scene graph in which a root Node represents a group and the {@link Mesh}s are leaves. Since Node
* implements {@link Entity}, we can designate the root Node as a model, causing it to be registered by its ID in {@link Scene#models}.
*
* Since {@link Mesh} also implements {@link Entity}, we can designate the leaf {@link Mesh}es as objects, causing them to
* be registered by their IDs in {@link Scene#objects}.
*
* We can then find those {@link Entity} types in {@link Scene#models} and {@link Scene#objects}.
*
* We can also update properties of our object-Meshes via calls to {@link Scene#setObjectsHighlighted} etc.
*
* [[Run this example](/examples/index.html#sceneRepresentation_SceneGraph)]
*
* ````javascript
* import {Viewer, Mesh, Node, PhongMaterial} from "xeokit-sdk.es.js";
*
* const viewer = new Viewer({
* canvasId: "myCanvas"
* });
*
* viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
* viewer.scene.camera.look = [0, -5.75, 0];
* viewer.scene.camera.up = [0.37, 0.91, -0.11];
*
* new Node(viewer.scene, {
* id: "table",
* isModel: true, // <---------- Node represents a model, so is registered by ID in viewer.scene.models
* rotation: [0, 50, 0],
* position: [0, 0, 0],
* scale: [1, 1, 1],
*
* children: [
*
* new Mesh(viewer.scene, { // Red table leg
* id: "redLeg",
* isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
* position: [-4, -6, -4],
* scale: [1, 3, 1],
* rotation: [0, 0, 0],
* material: new PhongMaterial(viewer.scene, {
* diffuse: [1, 0.3, 0.3]
* })
* }),
*
* new Mesh(viewer.scene, { // Green table leg
* id: "greenLeg",
* isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
* position: [4, -6, -4],
* scale: [1, 3, 1],
* rotation: [0, 0, 0],
* material: new PhongMaterial(viewer.scene, {
* diffuse: [0.3, 1.0, 0.3]
* })
* }),
*
* new Mesh(viewer.scene, {// Blue table leg
* id: "blueLeg",
* isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
* position: [4, -6, 4],
* scale: [1, 3, 1],
* rotation: [0, 0, 0],
* material: new PhongMaterial(viewer.scene, {
* diffuse: [0.3, 0.3, 1.0]
* })
* }),
*
* new Mesh(viewer.scene, { // Yellow table leg
* id: "yellowLeg",
* isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
* position: [-4, -6, 4],
* scale: [1, 3, 1],
* rotation: [0, 0, 0],
* material: new PhongMaterial(viewer.scene, {
* diffuse: [1.0, 1.0, 0.0]
* })
* }),
*
* new Mesh(viewer.scene, { // Purple table top
* id: "tableTop",
* isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
* position: [0, -3, 0],
* scale: [6, 0.5, 6],
* rotation: [0, 0, 0],
* material: new PhongMaterial(viewer.scene, {
* diffuse: [1.0, 0.3, 1.0]
* })
* })
* ]
* });
*
* // Find Nodes and Meshes by their IDs
*
* var table = viewer.scene.models["table"]; // Since table Node has isModel == true
*
* var redLeg = viewer.scene.objects["redLeg"]; // Since the Meshes have isObject == true
* var greenLeg = viewer.scene.objects["greenLeg"];
* var blueLeg = viewer.scene.objects["blueLeg"];
*
* // Highlight one of the table leg Meshes
*
* viewer.scene.setObjectsHighlighted(["redLeg"], true); // Since the Meshes have isObject == true
*
* // Periodically update transforms on our Nodes and Meshes
*
* viewer.scene.on("tick", function () {
*
* // Rotate legs
* redLeg.rotateY(0.5);
* greenLeg.rotateY(0.5);
* blueLeg.rotateY(0.5);
*
* // Rotate table
* table.rotateY(0.5);
* table.rotateX(0.3);
* });
* ````
*
* ## Metadata
*
* As mentioned, we can also associate {@link MetaModel}s and {@link MetaObject}s with our Nodes and {@link Mesh}es,
* within a {@link MetaScene}. See {@link MetaScene} for an example.
*
* @implements {Entity}
*/
class Node extends Component {
/**
* @constructor
* @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
* @param {*} [cfg] Configs
* @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted.
* @param {Boolean} [cfg.isModel] Specify ````true```` if this Mesh represents a model, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and may also have a corresponding {@link MetaModel} with matching {@link MetaModel#id}, registered by that ID in {@link MetaScene#metaModels}.
* @param {Boolean} [cfg.isObject] Specify ````true```` if this Mesh represents an object, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#objects} and may also have a corresponding {@link MetaObject} with matching {@link MetaObject#id}, registered by that ID in {@link MetaScene#metaObjects}.
* @param {Node} [cfg.parent] The parent Node.
* @param {Number[]} [cfg.origin] World-space origin for this Node.
* @param {Number[]} [cfg.rtcCenter] Deprecated - renamed to ````origin````.
* @param {Number[]} [cfg.position=[0,0,0]] Local 3D position.
* @param {Number[]} [cfg.scale=[1,1,1]] Local scale.
* @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
* @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] Local modelling transform matrix. Overrides the position, scale and rotation parameters.
* @param {Number[]} [cfg.offset=[0,0,0]] World-space 3D translation offset. Translates the Node in World space, after modelling transforms.
* @param {Boolean} [cfg.visible=true] Indicates if the Node is initially visible.
* @param {Boolean} [cfg.culled=false] Indicates if the Node is initially culled from view.
* @param {Boolean} [cfg.pickable=true] Indicates if the Node is initially pickable.
* @param {Boolean} [cfg.clippable=true] Indicates if the Node is initially clippable.
* @param {Boolean} [cfg.collidable=true] Indicates if the Node is initially included in boundary calculations.
* @param {Boolean} [cfg.castsShadow=true] Indicates if the Node initially casts shadows.
* @param {Boolean} [cfg.receivesShadow=true] Indicates if the Node initially receives shadows.
* @param {Boolean} [cfg.xrayed=false] Indicates if the Node is initially xrayed.
* @param {Boolean} [cfg.highlighted=false] Indicates if the Node is initially highlighted.
* @param {Boolean} [cfg.selected=false] Indicates if the Mesh is initially selected.
* @param {Boolean} [cfg.edges=false] Indicates if the Node's edges are initially emphasized.
* @param {Number[]} [cfg.colorize=[1.0,1.0,1.0]] Node's initial RGB colorize color, multiplies by the rendered fragment colors.
* @param {Number} [cfg.opacity=1.0] Node's initial opacity factor, multiplies by the rendered fragment alpha.
* @param {Array} [cfg.children] Child Nodes or {@link Mesh}es to add initially. Children must be in the same {@link Scene} and will be removed first from whatever parents they may already have.
* @param {Boolean} [cfg.inheritStates=true] Indicates if children given to this constructor should inherit rendering state from this parent as they are added. Rendering state includes {@link Node#visible}, {@link Node#culled}, {@link Node#pickable}, {@link Node#clippable}, {@link Node#castsShadow}, {@link Node#receivesShadow}, {@link Node#selected}, {@link Node#highlighted}, {@link Node#colorize} and {@link Node#opacity}.
*/
constructor(owner, cfg = {}) {
super(owner, cfg);
this._parentNode = null;
this._children = [];
this._aabb = null;
this._aabbDirty = true;
this.scene._aabbDirty = true;
this._numTriangles = 0;
this._scale = math.vec3();
this._quaternion = math.identityQuaternion();
this._rotation = math.vec3();
this._position = math.vec3();
this._offset = math.vec3();
this._localMatrix = math.identityMat4();
this._worldMatrix = math.identityMat4();
this._localMatrixDirty = true;
this._worldMatrixDirty = true;
if (cfg.matrix) {
this.matrix = cfg.matrix;
} else {
this.scale = cfg.scale;
this.position = cfg.position;
if (cfg.quaternion) {
} else {
this.rotation = cfg.rotation;
}
}
this._isModel = cfg.isModel;
if (this._isModel) {
this.scene._registerModel(this);
}
this._isObject = cfg.isObject;
if (this._isObject) {
this.scene._registerObject(this);
}
this.origin = cfg.origin;
this.visible = cfg.visible;
this.culled = cfg.culled;
this.pickable = cfg.pickable;
this.clippable = cfg.clippable;
this.collidable = cfg.collidable;
this.castsShadow = cfg.castsShadow;
this.receivesShadow = cfg.receivesShadow;
this.xrayed = cfg.xrayed;
this.highlighted = cfg.highlighted;
this.selected = cfg.selected;
this.edges = cfg.edges;
this.colorize = cfg.colorize;
this.opacity = cfg.opacity;
this.offset = cfg.offset;
// Add children, which inherit state from this Node
if (cfg.children) {
const children = cfg.children;
for (let i = 0, len = children.length; i < len; i++) {
this.addChild(children[i], cfg.inheritStates);
}
}
if (cfg.parentId) {
const parentNode = this.scene.components[cfg.parentId];
if (!parentNode) {
this.error("Parent not found: '" + cfg.parentId + "'");
} else if (!parentNode.isNode) {
this.error("Parent is not a Node: '" + cfg.parentId + "'");
} else {
parentNode.addChild(this);
}
} else if (cfg.parent) {
if (!cfg.parent.isNode) {
this.error("Parent is not a Node");
}
cfg.parent.addChild(this);
}
}
//------------------------------------------------------------------------------------------------------------------
// Entity members
//------------------------------------------------------------------------------------------------------------------
/**
* Returns true to indicate that this Component is an Entity.
* @type {Boolean}
*/
get isEntity() {
return true;
}
/**
* Returns ````true```` if this Mesh represents a model.
*
* When this returns ````true````, the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and
* may also have a corresponding {@link MetaModel}.
*
* @type {Boolean}
*/
get isModel() {
return this._isModel;
}
/**
* Returns ````true```` if this Node represents an object.
*
* When ````true```` the Node will be registered by {@link Node#id} in
* {@link Scene#objects} and may also have a {@link MetaObject} with matching {@link MetaObject#id}.
*
* @type {Boolean}
* @abstract
*/
get isObject() {
return this._isObject;
}
/**
* Gets the Node's World-space 3D axis-aligned bounding box.
*
* Represented by a six-element Float64Array containing the min/max extents of the
* axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.
*
* @type {Number[]}
*/
get aabb() {
if (this._aabbDirty) {
this._updateAABB();
}
return this._aabb;
}
/**
* Sets the World-space origin for this Node.
*
* @type {Float64Array}
*/
set origin(origin) {
if (origin) {
if (!this._origin) {
this._origin = math.vec3();
}
this._origin.set(origin);
} else {
if (this._origin) {
this._origin = null;
}
}
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].origin = origin;
}
this.glRedraw();
}
/**
* Gets the World-space origin for this Node.
*
* @type {Float64Array}
*/
get origin() {
return this._origin;
}
/**
* Sets the World-space origin for this Node.
*
* Deprecated and replaced by {@link Node#origin}.
*
* @deprecated
* @type {Float64Array}
*/
set rtcCenter(rtcCenter) {
this.origin = rtcCenter;
}
/**
* Gets the World-space origin for this Node.
*
* Deprecated and replaced by {@link Node#origin}.
*
* @deprecated
* @type {Float64Array}
*/
get rtcCenter() {
return this.origin;
}
/**
* The number of triangles in this Node.
*
* @type {Number}
*/
get numTriangles() {
return this._numTriangles;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are visible.
*
* Only rendered both {@link Node#visible} is ````true```` and {@link Node#culled} is ````false````.
*
* When {@link Node#isObject} and {@link Node#visible} are both ````true```` the Node will be
* registered by {@link Node#id} in {@link Scene#visibleObjects}.
*
* @type {Boolean}
*/
set visible(visible) {
visible = visible !== false;
this._visible = visible;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].visible = visible;
}
if (this._isObject) {
this.scene._objectVisibilityUpdated(this, visible);
}
}
/**
* Gets if this Node is visible.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* When {@link Node#isObject} and {@link Node#visible} are both ````true```` the Node will be
* registered by {@link Node#id} in {@link Scene#visibleObjects}.
*
* @type {Boolean}
*/
get visible() {
return this._visible;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are xrayed.
*
* When {@link Node#isObject} and {@link Node#xrayed} are both ````true```` the Node will be
* registered by {@link Node#id} in {@link Scene#xrayedObjects}.
*
* @type {Boolean}
*/
set xrayed(xrayed) {
xrayed = !!xrayed;
this._xrayed = xrayed;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].xrayed = xrayed;
}
if (this._isObject) {
this.scene._objectXRayedUpdated(this, xrayed);
}
}
/**
* Gets if this Node is xrayed.
*
* When {@link Node#isObject} and {@link Node#xrayed} are both ````true```` the Node will be
* registered by {@link Node#id} in {@link Scene#xrayedObjects}.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get xrayed() {
return this._xrayed;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are highlighted.
*
* When {@link Node#isObject} and {@link Node#highlighted} are both ````true```` the Node will be
* registered by {@link Node#id} in {@link Scene#highlightedObjects}.
*
* @type {Boolean}
*/
set highlighted(highlighted) {
highlighted = !!highlighted;
this._highlighted = highlighted;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].highlighted = highlighted;
}
if (this._isObject) {
this.scene._objectHighlightedUpdated(this, highlighted);
}
}
/**
* Gets if this Node is highlighted.
*
* When {@link Node#isObject} and {@link Node#highlighted} are both ````true```` the Node will be
* registered by {@link Node#id} in {@link Scene#highlightedObjects}.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get highlighted() {
return this._highlighted;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are selected.
*
* When {@link Node#isObject} and {@link Node#selected} are both ````true```` the Node will be
* registered by {@link Node#id} in {@link Scene#selectedObjects}.
*
* @type {Boolean}
*/
set selected(selected) {
selected = !!selected;
this._selected = selected;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].selected = selected;
}
if (this._isObject) {
this.scene._objectSelectedUpdated(this, selected);
}
}
/**
* Gets if this Node is selected.
*
* When {@link Node#isObject} and {@link Node#selected} are both ````true```` the Node will be
* registered by {@link Node#id} in {@link Scene#selectedObjects}.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get selected() {
return this._selected;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are edge-enhanced.
*
* @type {Boolean}
*/
set edges(edges) {
edges = !!edges;
this._edges = edges;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].edges = edges;
}
}
/**
* Gets if this Node's edges are enhanced.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get edges() {
return this._edges;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are culled.
*
* @type {Boolean}
*/
set culled(culled) {
culled = !!culled;
this._culled = culled;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].culled = culled;
}
}
/**
* Gets if this Node is culled.
*
* @type {Boolean}
*/
get culled() {
return this._culled;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are clippable.
*
* Clipping is done by the {@link SectionPlane}s in {@link Scene#clips}.
*
* @type {Boolean}
*/
set clippable(clippable) {
clippable = clippable !== false;
this._clippable = clippable;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].clippable = clippable;
}
}
/**
* Gets if this Node is clippable.
*
* Clipping is done by the {@link SectionPlane}s in {@link Scene#clips}.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get clippable() {
return this._clippable;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are included in boundary calculations.
*
* @type {Boolean}
*/
set collidable(collidable) {
collidable = collidable !== false;
this._collidable = collidable;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].collidable = collidable;
}
}
/**
* Gets if this Node is included in boundary calculations.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get collidable() {
return this._collidable;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es are pickable.
*
* Picking is done via calls to {@link Scene#pick}.
*
* @type {Boolean}
*/
set pickable(pickable) {
pickable = pickable !== false;
this._pickable = pickable;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].pickable = pickable;
}
}
/**
* Gets if to this Node is pickable.
*
* Picking is done via calls to {@link Scene#pick}.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get pickable() {
return this._pickable;
}
/**
* Sets the RGB colorize color for this Node and all child Nodes and {@link Mesh}es}.
*
* Multiplies by rendered fragment colors.
*
* Each element of the color is in range ````[0..1]````.
*
* @type {Number[]}
*/
set colorize(rgb) {
let colorize = this._colorize;
if (!colorize) {
colorize = this._colorize = new Float32Array(4);
colorize[3] = 1.0;
}
if (rgb) {
colorize[0] = rgb[0];
colorize[1] = rgb[1];
colorize[2] = rgb[2];
} else {
colorize[0] = 1;
colorize[1] = 1;
colorize[2] = 1;
}
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].colorize = colorize;
}
if (this._isObject) {
const colorized = (!!rgb);
this.scene._objectColorizeUpdated(this, colorized);
}
}
/**
* Gets the RGB colorize color for this Node.
*
* Each element of the color is in range ````[0..1]````.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Number[]}
*/
get colorize() {
return this._colorize.slice(0, 3);
}
/**
* Sets the opacity factor for this Node and all child Nodes and {@link Mesh}es.
*
* This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
*
* @type {Number}
*/
set opacity(opacity) {
let colorize = this._colorize;
if (!colorize) {
colorize = this._colorize = new Float32Array(4);
colorize[0] = 1;
colorize[1] = 1;
colorize[2] = 1;
}
colorize[3] = opacity !== null && opacity !== undefined ? opacity : 1.0;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].opacity = opacity;
}
if (this._isObject) {
const opacityUpdated = (opacity !== null && opacity !== undefined);
this.scene._objectOpacityUpdated(this, opacityUpdated);
}
}
/**
* Gets this Node's opacity factor.
*
* This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Number}
*/
get opacity() {
return this._colorize[3];
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es cast shadows.
*
* @type {Boolean}
*/
set castsShadow(castsShadow) {
castsShadow = !!castsShadow;
this._castsShadow = castsShadow;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].castsShadow = castsShadow;
}
}
/**
* Gets if this Node casts shadows.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get castsShadow() {
return this._castsShadow;
}
/**
* Sets if this Node and all child Nodes and {@link Mesh}es can have shadows cast upon them.
*
* @type {Boolean}
*/
set receivesShadow(receivesShadow) {
receivesShadow = !!receivesShadow;
this._receivesShadow = receivesShadow;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].receivesShadow = receivesShadow;
}
}
/**
* Whether or not to this Node can have shadows cast upon it.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Boolean}
*/
get receivesShadow() {
return this._receivesShadow;
}
/**
* Gets if this Node can have Scalable Ambient Obscurance (SAO) applied to it.
*
* SAO is configured by {@link SAO}.
*
* @type {Boolean}
* @abstract
*/
get saoEnabled() {
return false; // TODO: Support SAO on Nodes
}
/**
* Sets the 3D World-space offset for this Node and all child Nodes and {@link Mesh}es}.
*
* The offset dynamically translates those components in World-space.
*
* Default value is ````[0, 0, 0]````.
*
* Note that child Nodes and {@link Mesh}es may subsequently be given different values for this property.
*
* @type {Number[]}
*/
set offset(offset) {
if (offset) {
this._offset[0] = offset[0];
this._offset[1] = offset[1];
this._offset[2] = offset[2];
} else {
this._offset[0] = 0;
this._offset[1] = 0;
this._offset[2] = 0;
}
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i].offset = this._offset;
}
if (this._isObject) {
this.scene._objectOffsetUpdated(this, offset);
}
}
/**
* Gets the Node's 3D World-space offset.
*
* Default value is ````[0, 0, 0]````.
*
* Child Nodes and {@link Mesh}es may have different values for this property.
*
* @type {Number[]}
*/
get offset() {
return this._offset;
}
//------------------------------------------------------------------------------------------------------------------
// Node members
//------------------------------------------------------------------------------------------------------------------
/**
* Returns true to indicate that this Component is a Node.
* @type {Boolean}
*/
get isNode() {
return true;
}
_setLocalMatrixDirty() {
this._localMatrixDirty = true;
this._setWorldMatrixDirty();
}
_setWorldMatrixDirty() {
this._worldMatrixDirty = true;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i]._setWorldMatrixDirty();
}
}
_buildWorldMatrix() {
const localMatrix = this.matrix;
if (!this._parentNode) {
for (let i = 0, len = localMatrix.length; i < len; i++) {
this._worldMatrix[i] = localMatrix[i];
}
} else {
math.mulMat4(this._parentNode.worldMatrix, localMatrix, this._worldMatrix);
}
this._worldMatrixDirty = false;
}
_setSubtreeAABBsDirty(node) {
node._aabbDirty = true;
if (node._children) {
for (let i = 0, len = node._children.length; i < len; i++) {
this._setSubtreeAABBsDirty(node._children[i]);
}
}
}
_setAABBDirty() {
this._setSubtreeAABBsDirty(this);
if (this.collidable) {
for (let node = this; node; node = node._parentNode) {
node._aabbDirty = true;
}
}
}
_updateAABB() {
this.scene._aabbDirty = true;
if (!this._aabb) {
this._aabb = math.AABB3();
}
if (this._buildAABB) {
this._buildAABB(this.worldMatrix, this._aabb); // Mesh or VBOSceneModel
} else { // Node | Node | Model
math.collapseAABB3(this._aabb);
let node;
for (let i = 0, len = this._children.length; i < len; i++) {
node = this._children[i];
if (!node.collidable) {
continue;
}
math.expandAABB3(this._aabb, node.aabb);
}
}
this._aabbDirty = false;
}
/**
* Adds a child Node or {@link Mesh}.
*
* The child must be a Node or {@link Mesh} in the same {@link Scene}.
*
* If the child already has a parent, will be removed from that parent first.
*
* Does nothing if already a child.
*
* @param {Node|Mesh|String} child Instance or ID of the child to add.
* @param [inheritStates=false] Indicates if the child should inherit rendering states from this parent as it is added. Rendering state includes {@link Node#visible}, {@link Node#culled}, {@link Node#pickable}, {@link Node#clippable}, {@link Node#castsShadow}, {@link Node#receivesShadow}, {@link Node#selected}, {@link Node#highlighted}, {@link Node#colorize} and {@link Node#opacity}.
* @returns {Node|Mesh} The child.
*/
addChild(child, inheritStates) {
if (utils.isNumeric(child) || utils.isString(child)) {
const nodeId = child;
child = this.scene.component[nodeId];
if (!child) {
this.warn("Component not found: " + utils.inQuotes(nodeId));
return;
}
if (!child.isNode && !child.isMesh) {
this.error("Not a Node or Mesh: " + nodeId);
return;
}
} else {
if (!child.isNode && !child.isMesh) {
this.error("Not a Node or Mesh: " + child.id);
return;
}
if (child._parentNode) {
if (child._parentNode.id === this.id) {
this.warn("Already a child: " + child.id);
return;
}
child._parentNode.removeChild(child);
}
}
const id = child.id;
if (child.scene.id !== this.scene.id) {
this.error("Child not in same Scene: " + child.id);
return;
}
this._children.push(child);
child._parentNode = this;
if (!!inheritStates) {
child.visible = this.visible;
child.culled = this.culled;
child.xrayed = this.xrayed;
child.highlited = this.highlighted;
child.selected = this.selected;
child.edges = this.edges;
child.clippable = this.clippable;
child.pickable = this.pickable;
child.collidable = this.collidable;
child.castsShadow = this.castsShadow;
child.receivesShadow = this.receivesShadow;
child.colorize = this.colorize;
child.opacity = this.opacity;
child.offset = this.offset;
}
child._setWorldMatrixDirty();
child._setAABBDirty();
this._numTriangles += child.numTriangles;
return child;
}
/**
* Removes the given child Node or {@link Mesh}.
*
* @param {Node|Mesh} child Child to remove.
*/
removeChild(child) {
for (let i = 0, len = this._children.length; i < len; i++) {
if (this._children[i].id === child.id) {
child._parentNode = null;
this._children = this._children.splice(i, 1);
child._setWorldMatrixDirty();
child._setAABBDirty();
this._setAABBDirty();
this._numTriangles -= child.numTriangles;
return;
}
}
}
/**
* Removes all child Nodes and {@link Mesh}es.
*/
removeChildren() {
let child;
for (let i = 0, len = this._children.length; i < len; i++) {
child = this._children[i];
child._parentNode = null;
child._setWorldMatrixDirty();
child._setAABBDirty();
this._numTriangles -= child.numTriangles;
}
this._children = [];
this._setAABBDirty();
}
/**
* Number of child Nodes or {@link Mesh}es.
*
* @type {Number}
*/
get numChildren() {
return this._children.length;
}
/**
* Array of child Nodes or {@link Mesh}es.
*
* @type {Array}
*/
get children() {
return this._children;
}
/**
* The parent Node.
*
* The parent Node may also be set by passing the Node to the parent's {@link Node#addChild} method.
*
* @type {Node}
*/
set parent(node) {
if (utils.isNumeric(node) || utils.isString(node)) {
const nodeId = node;
node = this.scene.components[nodeId];
if (!node) {
this.warn("Node not found: " + utils.inQuotes(nodeId));
return;
}
if (!node.isNode) {
this.error("Not a Node: " + node.id);
return;
}
}
if (node.scene.id !== this.scene.id) {
this.error("Node not in same Scene: " + node.id);
return;
}
if (this._parentNode && this._parentNode.id === node.id) {
this.warn("Already a child of Node: " + node.id);
return;
}
node.addChild(this);
}
/**
* The parent Node.
*
* @type {Node}
*/
get parent() {
return this._parentNode;
}
/**
* Sets the Node's local translation.
*
* Default value is ````[0,0,0]````.
*
* @type {Number[]}
*/
set position(value) {
this._position.set(value || [0, 0, 0]);
this._setLocalMatrixDirty();
this._setAABBDirty();
this.glRedraw();
}
/**
* Gets the Node's local translation.
*
* Default value is ````[0,0,0]````.
*
* @type {Number[]}
*/
get position() {
return this._position;
}
/**
* Sets the Node's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
*
* Default value is ````[0,0,0]````.
*
* @type {Number[]}
*/
set rotation(value) {
this._rotation.set(value || [0, 0, 0]);
math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion);
this._setLocalMatrixDirty();
this._setAABBDirty();
this.glRedraw();
}
/**
* Gets the Node's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
*
* Default value is ````[0,0,0]````.
*
* @type {Number[]}
*/
get rotation() {
return this._rotation;
}
/**
* Sets the Node's local rotation quaternion.
*
* Default value is ````[0,0,0,1]````.
*
* @type {Number[]}
*/
set quaternion(value) {
this._quaternion.set(value || [0, 0, 0, 1]);
math.quaternionToEuler(this._quaternion, "XYZ", this._rotation);
this._setLocalMatrixDirty();
this._setAABBDirty();
this.glRedraw();
}
/**
* Gets the Node's local rotation quaternion.
*
* Default value is ````[0,0,0,1]````.
*
* @type {Number[]}
*/
get quaternion() {
return this._quaternion;
}
/**
* Sets the Node's local scale.
*
* Default value is ````[1,1,1]````.
*
* @type {Number[]}
*/
set scale(value) {
this._scale.set(value || [1, 1, 1]);
this._setLocalMatrixDirty();
this._setAABBDirty();
this.glRedraw();
}
/**
* Gets the Node's local scale.
*
* Default value is ````[1,1,1]````.
*
* @type {Number[]}
*/
get scale() {
return this._scale;
}
/**
* Sets the Node's local modeling transform matrix.
*
* Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
*
* @type {Number[]}
*/
set matrix(value) {
if (!this._localMatrix) {
this._localMatrix = math.identityMat4();
}
this._localMatrix.set(value || identityMat);
math.decomposeMat4(this._localMatrix, this._position, this._quaternion, this._scale);
this._localMatrixDirty = false;
this._setWorldMatrixDirty();
this._setAABBDirty();
this.glRedraw();
}
/**
* Gets the Node's local modeling transform matrix.
*
* Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
*
* @type {Number[]}
*/
get matrix() {
if (this._localMatrixDirty) {
if (!this._localMatrix) {
this._localMatrix = math.identityMat4();
}
math.composeMat4(this._position, this._quaternion, this._scale, this._localMatrix);
this._localMatrixDirty = false;
}
return this._localMatrix;
}
/**
* Gets the Node's World matrix.
*
* @property worldMatrix
* @type {Number[]}
*/
get worldMatrix() {
if (this._worldMatrixDirty) {
this._buildWorldMatrix();
}
return this._worldMatrix;
}
/**
* Rotates the Node about the given local axis by the given increment.
*
* @param {Number[]} axis Local axis about which to rotate.
* @param {Number} angle Angle increment in degrees.
*/
rotate(axis, angle) {
angleAxis[0] = axis[0];
angleAxis[1] = axis[1];
angleAxis[2] = axis[2];
angleAxis[3] = angle * math.DEGTORAD;
math.angleAxisToQuaternion(angleAxis, q1);
math.mulQuaternions(this.quaternion, q1, q2);
this.quaternion = q2;
this._setLocalMatrixDirty();
this._setAABBDirty();
this.glRedraw();
return this;
}
/**
* Rotates the Node about the given World-space axis by the given increment.
*
* @param {Number[]} axis Local axis about which to rotate.
* @param {Number} angle Angle increment in degrees.
*/
rotateOnWorldAxis(axis, angle) {
angleAxis[0] = axis[0];
angleAxis[1] = axis[1];
angleAxis[2] = axis[2];
angleAxis[3] = angle * math.DEGTORAD;
math.angleAxisToQuaternion(angleAxis, q1);
math.mulQuaternions(q1, this.quaternion, q1);
//this.quaternion.premultiply(q1);
return this;
}
/**
* Rotates the Node about the local X-axis by the given increment.
*
* @param {Number} angle Angle increment in degrees.
*/
rotateX(angle) {
return this.rotate(xAxis, angle);
}
/**
* Rotates the Node about the local Y-axis by the given increment.
*
* @param {Number} angle Angle increment in degrees.
*/
rotateY(angle) {
return this.rotate(yAxis, angle);
}
/**
* Rotates the Node about the local Z-axis by the given increment.
*
* @param {Number} angle Angle increment in degrees.
*/
rotateZ(angle) {
return this.rotate(zAxis, angle);
}
/**
* Translates the Node along local space vector by the given increment.
*
* @param {Number[]} axis Normalized local space 3D vector along which to translate.
* @param {Number} distance Distance to translate along the vector.
*/
translate(axis, distance) {
math.vec3ApplyQuaternion(this.quaternion, axis, veca);
math.mulVec3Scalar(veca, distance, vecb);
math.addVec3(this.position, vecb, this.position);
this._setLocalMatrixDirty();
this._setAABBDirty();
this.glRedraw();
return this;
}
/**
* Translates the Node along the local X-axis by the given increment.
*
* @param {Number} distance Distance to translate along the X-axis.
*/
translateX(distance) {
return this.translate(xAxis, distance);
}
/**
* Translates the Node along the local Y-axis by the given increment.
*
* @param {Number} distance Distance to translate along the Y-axis.
*/
translateY(distance) {
return this.translate(yAxis, distance);
}
/**
* Translates the Node along the local Z-axis by the given increment.
*
* @param {Number} distance Distance to translate along the Z-axis.
*/
translateZ(distance) {
return this.translate(zAxis, distance);
}
//------------------------------------------------------------------------------------------------------------------
// Component members
//------------------------------------------------------------------------------------------------------------------
/**
@private
*/
get type() {
return "Node";
}
/**
* Destroys this Node.
*/
destroy() {
super.destroy();
if (this._parentNode) {
this._parentNode.removeChild(this);
}
if (this._isObject) {
this.scene._deregisterObject(this);
if (this._visible) {
this.scene._objectVisibilityUpdated(this, false, false);
}
if (this._xrayed) {
this.scene._objectXRayedUpdated(this, false, false);
}
if (this._selected) {
this.scene._objectSelectedUpdated(this, false, false);
}
if (this._highlighted) {
this.scene._objectHighlightedUpdated(this, false, false);
}
this.scene._objectColorizeUpdated(this, false);
this.scene._objectOpacityUpdated(this, false);
if (this.offset.some((v) => v !== 0))
this.scene._objectOffsetUpdated(this, false);
}
if (this._isModel) {
this.scene._deregisterModel(this);
}
if (this._children.length) {
// Clone the _children before iterating, so our children don't mess us up when calling removeChild().
const tempChildList = this._children.splice();
let child;
for (let i = 0, len = tempChildList.length; i < len; i++) {
child = tempChildList[i];
child.destroy();
}
}
this._children = [];
this._setAABBDirty();
this.scene._aabbDirty = true;
}
}
export {Node};