src/viewer/scene/model/SceneModelEntity.js
import {ENTITY_FLAGS} from './ENTITY_FLAGS.js';
import {math} from "../math/math.js";
const tempFloatRGB = new Float32Array([0, 0, 0]);
const tempIntRGB = new Uint16Array([0, 0, 0]);
const tempOBB3a = math.OBB3();
/**
* An entity within a {@link SceneModel}
*
* * Created with {@link SceneModel#createEntity}
* * Stored by ID in {@link SceneModel#entities}
* * Has one or more {@link SceneModelMesh}es
*
* @implements {Entity}
*/
export class SceneModelEntity {
/**
* @private
*/
constructor(model, isObject, id, meshes, flags, lodCullable) {
this._isObject = isObject;
/**
* The {@link Scene} to which this SceneModelEntity belongs.
*/
this.scene = model.scene;
/**
* The {@link SceneModel} to which this SceneModelEntity belongs.
*/
this.model = model;
/**
* The {@link SceneModelMesh}es belonging to this SceneModelEntity.
*
* * These are created with {@link SceneModel#createMesh} and registered in {@ilnk SceneModel#meshes}
* * Each SceneModelMesh belongs to one SceneModelEntity
*/
this.meshes = meshes;
this._numPrimitives = 0;
for (let i = 0, len = this.meshes.length; i < len; i++) { // TODO: tidier way? Refactor?
const mesh = this.meshes[i];
mesh.parent = this;
mesh.entity = this;
this._numPrimitives += mesh.numPrimitives;
}
/**
* The unique ID of this SceneModelEntity.
*/
this.id = id;
/**
* The original system ID of this SceneModelEntity.
*/
this.originalSystemId = math.unglobalizeObjectId(model.id, id);
this._flags = flags;
this._aabb = math.AABB3();
this._aabbDirty = true;
this._offset = math.vec3();
this._colorizeUpdated = false;
this._opacityUpdated = false;
this._lodCullable = (!!lodCullable);
this._culled = false;
this._culledVFC = false;
this._culledLOD = false;
if (this._isObject) {
model.scene._registerObject(this);
}
}
_transformDirty() {
this._aabbDirty = true;
this.model._transformDirty();
}
_sceneModelDirty() { // Called by SceneModel when SceneModel's matrix is updated
this._aabbDirty = true;
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._sceneModelDirty();
}
}
/**
* World-space 3D axis-aligned bounding box (AABB) of this SceneModelEntity.
*
* Represented by a six-element Float64Array containing the min/max extents of the
* axis-aligned volume, ie. ````[xmin, ymin, zmin, xmax, ymax, zmax]````.
*
* @type {Float64Array}
*/
get aabb() {
if (this._aabbDirty) {
math.collapseAABB3(this._aabb);
for (let i = 0, len = this.meshes.length; i < len; i++) {
math.expandAABB3(this._aabb, this.meshes[i].aabb);
}
this._aabbDirty = false;
}
// if (this._aabbDirty) {
// math.AABB3ToOBB3(this._aabb, tempOBB3a);
// math.transformOBB3(this.model.matrix, tempOBB3a);
// math.OBB3ToAABB3(tempOBB3a, this._worldAABB);
// this._worldAABB[0] += this._offset[0];
// this._worldAABB[1] += this._offset[1];
// this._worldAABB[2] += this._offset[2];
// this._worldAABB[3] += this._offset[0];
// this._worldAABB[4] += this._offset[1];
// this._worldAABB[5] += this._offset[2];
// this._aabbDirty = false;
// }
return this._aabb;
}
get isEntity() {
return true;
}
/**
* Returns false to indicate that this Entity subtype is not a model.
* @returns {boolean}
*/
get isModel() {
return false;
}
/**
* Returns ````true```` if this SceneModelEntity represents an object.
*
* When this is ````true````, the SceneModelEntity will be registered by {@link SceneModelEntity#id}
* in {@link Scene#objects} and may also have a corresponding {@link MetaObject}.
*
* @type {Boolean}
*/
get isObject() {
return this._isObject;
}
get numPrimitives() {
return this._numPrimitives;
}
/**
* The approximate number of triangles in this SceneModelEntity.
*
* @type {Number}
*/
get numTriangles() {
return this._numPrimitives;
}
/**
* Gets if this SceneModelEntity is visible.
*
* Only rendered when {@link SceneModelEntity#visible} is ````true````
* and {@link SceneModelEntity#culled} is ````false````.
*
* When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#visible} are
* both ````true```` the SceneModelEntity will be registered
* by {@link SceneModelEntity#id} in {@link Scene#visibleObjects}.
*
* @type {Boolean}
*/
get visible() {
return this._getFlag(ENTITY_FLAGS.VISIBLE);
}
/**
* Sets if this SceneModelEntity is visible.
*
* Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````.
*
* When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#visible} are
* both ````true```` the SceneModelEntity will be
* registered by {@link SceneModelEntity#id} in {@link Scene#visibleObjects}.
*
* @type {Boolean}
*/
set visible(visible) {
if (!!(this._flags & ENTITY_FLAGS.VISIBLE) === visible) {
return; // Redundant update
}
if (visible) {
this._flags = this._flags | ENTITY_FLAGS.VISIBLE;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.VISIBLE;
}
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setVisible(this._flags);
}
if (this._isObject) {
this.model.scene._objectVisibilityUpdated(this);
}
this.model.glRedraw();
}
/**
* Gets if this SceneModelEntity is highlighted.
*
* When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#highlighted} are both ````true```` the SceneModelEntity will be
* registered by {@link SceneModelEntity#id} in {@link Scene#highlightedObjects}.
*
* @type {Boolean}
*/
get highlighted() {
return this._getFlag(ENTITY_FLAGS.HIGHLIGHTED);
}
/**
* Sets if this SceneModelEntity is highlighted.
*
* When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#highlighted} are both ````true```` the SceneModelEntity will be
* registered by {@link SceneModelEntity#id} in {@link Scene#highlightedObjects}.
*
* @type {Boolean}
*/
set highlighted(highlighted) {
if (!!(this._flags & ENTITY_FLAGS.HIGHLIGHTED) === highlighted) {
return; // Redundant update
}
if (highlighted) {
this._flags = this._flags | ENTITY_FLAGS.HIGHLIGHTED;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.HIGHLIGHTED;
}
for (var i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setHighlighted(this._flags);
}
if (this._isObject) {
this.model.scene._objectHighlightedUpdated(this);
}
this.model.glRedraw();
}
/**
* Gets if this SceneModelEntity is xrayed.
*
* When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#xrayed} are both ````true``` the SceneModelEntity will be
* registered by {@link SceneModelEntity#id} in {@link Scene#xrayedObjects}.
*
* @type {Boolean}
*/
get xrayed() {
return this._getFlag(ENTITY_FLAGS.XRAYED);
}
/**
* Sets if this SceneModelEntity is xrayed.
*
* When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#xrayed} are both ````true``` the SceneModelEntity will be
* registered by {@link SceneModelEntity#id} in {@link Scene#xrayedObjects}.
*
* @type {Boolean}
*/
set xrayed(xrayed) {
if (!!(this._flags & ENTITY_FLAGS.XRAYED) === xrayed) {
return; // Redundant update
}
if (xrayed) {
this._flags = this._flags | ENTITY_FLAGS.XRAYED;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.XRAYED;
}
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setXRayed(this._flags);
}
if (this._isObject) {
this.model.scene._objectXRayedUpdated(this);
}
this.model.glRedraw();
}
/**
* Gets if this SceneModelEntity is selected.
*
* When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#selected} are both ````true``` the SceneModelEntity will be
* registered by {@link SceneModelEntity#id} in {@link Scene#selectedObjects}.
*
* @type {Boolean}
*/
get selected() {
return this._getFlag(ENTITY_FLAGS.SELECTED);
}
/**
* Gets if this SceneModelEntity is selected.
*
* When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#selected} are both ````true``` the SceneModelEntity will be
* registered by {@link SceneModelEntity#id} in {@link Scene#selectedObjects}.
*
* @type {Boolean}
*/
set selected(selected) {
if (!!(this._flags & ENTITY_FLAGS.SELECTED) === selected) {
return; // Redundant update
}
if (selected) {
this._flags = this._flags | ENTITY_FLAGS.SELECTED;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.SELECTED;
}
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setSelected(this._flags);
}
if (this._isObject) {
this.model.scene._objectSelectedUpdated(this);
}
this.model.glRedraw();
}
/**
* Gets if this SceneModelEntity's edges are enhanced.
*
* @type {Boolean}
*/
get edges() {
return this._getFlag(ENTITY_FLAGS.EDGES);
}
/**
* Sets if this SceneModelEntity's edges are enhanced.
*
* @type {Boolean}
*/
set edges(edges) {
if (!!(this._flags & ENTITY_FLAGS.EDGES) === edges) {
return; // Redundant update
}
if (edges) {
this._flags = this._flags | ENTITY_FLAGS.EDGES;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.EDGES;
}
for (var i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setEdges(this._flags);
}
this.model.glRedraw();
}
get culledVFC() {
return !!(this._culledVFC);
}
set culledVFC(culled) {
this._culledVFC = culled;
this._setCulled();
}
get culledLOD() {
return !!(this._culledLOD);
}
set culledLOD(culled) {
this._culledLOD = culled;
this._setCulled();
}
/**
* Gets if this SceneModelEntity is culled.
*
* Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````.
*
* @type {Boolean}
*/
get culled() {
return !!(this._culled);
// return this._getFlag(ENTITY_FLAGS.CULLED);
}
/**
* Sets if this SceneModelEntity is culled.
*
* Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````.
*
* @type {Boolean}
*/
set culled(culled) {
this._culled = culled;
this._setCulled();
}
_setCulled() {
let culled = !!(this._culled) || !!(this._culledLOD && this._lodCullable) || !!(this._culledVFC);
if (!!(this._flags & ENTITY_FLAGS.CULLED) === culled) {
return; // Redundant update
}
if (culled) {
this._flags = this._flags | ENTITY_FLAGS.CULLED;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.CULLED;
}
for (var i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setCulled(this._flags);
}
this.model.glRedraw();
}
/**
* Gets if this SceneModelEntity is clippable.
*
* Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
*
* @type {Boolean}
*/
get clippable() {
return this._getFlag(ENTITY_FLAGS.CLIPPABLE);
}
/**
* Sets if this SceneModelEntity is clippable.
*
* Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
*
* @type {Boolean}
*/
set clippable(clippable) {
if ((!!(this._flags & ENTITY_FLAGS.CLIPPABLE)) === clippable) {
return; // Redundant update
}
if (clippable) {
this._flags = this._flags | ENTITY_FLAGS.CLIPPABLE;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.CLIPPABLE;
}
for (var i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setClippable(this._flags);
}
this.model.glRedraw();
}
/**
* Gets if this SceneModelEntity is included in boundary calculations.
*
* @type {Boolean}
*/
get collidable() {
return this._getFlag(ENTITY_FLAGS.COLLIDABLE);
}
/**
* Sets if this SceneModelEntity is included in boundary calculations.
*
* @type {Boolean}
*/
set collidable(collidable) {
if (!!(this._flags & ENTITY_FLAGS.COLLIDABLE) === collidable) {
return; // Redundant update
}
if (collidable) {
this._flags = this._flags | ENTITY_FLAGS.COLLIDABLE;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.COLLIDABLE;
}
for (var i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setCollidable(this._flags);
}
}
/**
* Gets if this SceneModelEntity is pickable.
*
* Picking is done via calls to {@link Scene#pick}.
*
* @type {Boolean}
*/
get pickable() {
return this._getFlag(ENTITY_FLAGS.PICKABLE);
}
/**
* Sets if this SceneModelEntity is pickable.
*
* Picking is done via calls to {@link Scene#pick}.
*
* @type {Boolean}
*/
set pickable(pickable) {
if (!!(this._flags & ENTITY_FLAGS.PICKABLE) === pickable) {
return; // Redundant update
}
if (pickable) {
this._flags = this._flags | ENTITY_FLAGS.PICKABLE;
} else {
this._flags = this._flags & ~ENTITY_FLAGS.PICKABLE;
}
for (var i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setPickable(this._flags);
}
}
/**
* Gets the SceneModelEntity's RGB colorize color, multiplies by the SceneModelEntity's rendered fragment colors.
*
* Each element of the color is in range ````[0..1]````.
*
* @type {Number[]}
*/
get colorize() { // [0..1, 0..1, 0..1]
if (this.meshes.length === 0) {
return null;
}
const colorize = this.meshes[0]._colorize;
tempFloatRGB[0] = colorize[0] / 255.0; // Unquantize
tempFloatRGB[1] = colorize[1] / 255.0;
tempFloatRGB[2] = colorize[2] / 255.0;
return tempFloatRGB;
}
/**
* Sets the SceneModelEntity's RGB colorize color, multiplies by the SceneModelEntity's rendered fragment colors.
*
* Each element of the color is in range ````[0..1]````.
*
* @type {Number[]}
*/
set colorize(color) { // [0..1, 0..1, 0..1]
if (color) {
tempIntRGB[0] = Math.floor(color[0] * 255.0); // Quantize
tempIntRGB[1] = Math.floor(color[1] * 255.0);
tempIntRGB[2] = Math.floor(color[2] * 255.0);
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setColorize(tempIntRGB);
}
} else {
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setColorize(null);
}
}
if (this._isObject) {
const colorized = (!!color);
this.scene._objectColorizeUpdated(this, colorized);
this._colorizeUpdated = colorized;
}
this.model.glRedraw();
}
/**
* Gets the SceneModelEntity's opacity factor.
*
* This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
*
* @type {Number}
*/
get opacity() {
if (this.meshes.length > 0) {
return (this.meshes[0]._colorize[3] / 255.0);
} else {
return 1.0;
}
}
/**
* Sets the SceneModelEntity's opacity factor.
*
* This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
*
* @type {Number}
*/
set opacity(opacity) {
if (this.meshes.length === 0) {
return;
}
const opacityUpdated = (opacity !== null && opacity !== undefined);
const lastOpacityQuantized = this.meshes[0]._colorize[3];
let opacityQuantized = 255;
if (opacityUpdated) {
if (opacity < 0) {
opacity = 0;
} else if (opacity > 1) {
opacity = 1;
}
opacityQuantized = Math.floor(opacity * 255.0); // Quantize
if (lastOpacityQuantized === opacityQuantized) {
return;
}
} else {
opacityQuantized = 255.0;
if (lastOpacityQuantized === opacityQuantized) {
return;
}
}
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._setOpacity(opacityQuantized, this._flags);
}
if (this._isObject) {
this.scene._objectOpacityUpdated(this, opacityUpdated);
this._opacityUpdated = opacityUpdated;
}
this.model.glRedraw();
}
/**
* Gets the SceneModelEntity's 3D World-space offset.
*
* Default value is ````[0,0,0]````.
*
* @type {Number[]}
*/
get offset() {
return this._offset;
}
/**
* Sets the SceneModelEntity's 3D World-space offset.
*
* Default value is ````[0,0,0]````.
*
* @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.meshes.length; i < len; i++) {
this.meshes[i]._setOffset(this._offset);
}
this._aabbDirty = true;
this.model._aabbDirty = true;
this.scene._aabbDirty = true;
this.scene._objectOffsetUpdated(this, offset);
this.model.glRedraw();
}
get saoEnabled() {
return this.model.saoEnabled;
}
getEachVertex(callback) {
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i].getEachVertex(callback)
}
}
getEachIndex(callback) {
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i].getEachIndex(callback)
}
}
/**
* Returns the volume of this SceneModelEntity.
*
* Only works when {@link Scene.readableGeometryEnabled | Scene.readableGeometryEnabled} is `true` and the
* SceneModelEntity contains solid triangle meshes; returns `0` otherwise.
*
* @returns {number}
*/
get volume() {
let volume = 0;
for (let i = 0, len = this.meshes.length; i < len; i++) {
const meshVolume = this.meshes[i].volume;
if (meshVolume < 0) {
return -1;
}
volume += meshVolume;
}
return volume;
}
/**
* Returns the surface area of this SceneModelEntity.
*
* Only works when {@link Scene.readableGeometryEnabled | Scene.readableGeometryEnabled} is `true` and the
* SceneModelEntity contains triangle meshes; returns `0` otherwise.
*
* @returns {number}
*/
get surfaceArea() {
let surfaceArea = 0;
for (let i = 0, len = this.meshes.length; i < len; i++) {
const meshSurfaceArea = this.meshes[i].surfaceArea;
if (meshSurfaceArea >= 0) {
surfaceArea += meshSurfaceArea;
}
}
return surfaceArea > 0 ? surfaceArea : -1;
}
_getFlag(flag) {
return !!(this._flags & flag);
}
_finalize() {
const scene = this.model.scene;
if (this._isObject) {
if (this.visible) {
scene._objectVisibilityUpdated(this);
}
if (this.highlighted) {
scene._objectHighlightedUpdated(this);
}
if (this.xrayed) {
scene._objectXRayedUpdated(this);
}
if (this.selected) {
scene._objectSelectedUpdated(this);
}
}
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._finalize(this._flags);
}
}
_finalize2() {
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._finalize2();
}
}
_destroy() {
const scene = this.model.scene;
if (this._isObject) {
scene._deregisterObject(this);
if (this.visible) {
scene._deRegisterVisibleObject(this);
}
if (this.xrayed) {
scene._deRegisterXRayedObject(this);
}
if (this.selected) {
scene._deRegisterSelectedObject(this);
}
if (this.highlighted) {
scene._deRegisterHighlightedObject(this);
}
if (this._colorizeUpdated) {
this.scene._deRegisterColorizedObject(this);
}
if (this._opacityUpdated) {
this.scene._deRegisterOpacityObject(this);
}
if (this._offset && (this._offset[0] !== 0 || this._offset[1] !== 0 || this._offset[2] !== 0)) {
this.scene._deRegisterOffsetObject(this);
}
}
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i]._destroy();
}
scene._aabbDirty = true;
}
}