const { Group, Matrix4 } = require('three');
const Pointset = require('./primitives/pointset').Pointset;
const Lines = require('./primitives/lines').Lines;
const Lines2 = require('./primitives/lines2').Lines2;
const Geometry = require('./primitives/geometry').Geometry;
const THREE = require('three');
let uniqueiId = 0;
const getUniqueId = function () {
return "re" + uniqueiId++;
}
/**
* Provides a hierachical structure to objects, Each region
* may contain multiple child regions and {@link ZincObject}.
*
* @class
* @author Alan Wu
* @return {Region}
*/
let Region = function (parentIn, sceneIn) {
let parent = parentIn;
let group = new Group();
group.matrixAutoUpdate = false;
group.userData = this;
let children = [];
let name = "";
let zincObjects = [];
let scene = sceneIn;
const tMatrix = new Matrix4();
let duration = 3000;
tMatrix.set(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
this.pickableUpdateRequired = true;
this.isRegion = true;
this.uuid = getUniqueId();
/**
* Hide all primitives belong to this region.
*/
this.hideAllPrimitives = () => {
children.forEach(child => child.hideAllPrimitives());
zincObjects.forEach(zincObject => zincObject.setVisibility(false));
}
/**
* Show all primitives belong to this region.
*/
this.showAllPrimitives = () => {
children.forEach(child => child.showAllPrimitives());
zincObjects.forEach(zincObject => zincObject.setVisibility(true));
}
/**
* Set the visibility and propagate it down the hierarchies
* depending on the flag.
*
* @param {Boolean} flag - A flag indicating either the visibilty to be on/off.
*/
this.setVisibility = (flag) => {
if (flag != group.visible) {
group.visible = flag;
this.pickableUpdateRequired = true;
}
}
/**
* Get the visibility of the region and its children.
*
* @return {Boolean}
*/
this.getVisibility = () => {
return group.visible;
}
/**
* Get the {THREE.Group} containing all child regions and their
* primitives.
*
* @return {THREE.Group}
*/
this.getGroup = () => {
return group;
}
/**
* Set the transformation with a {THREE.Matrix4} matrix, this will affect
* all primitives in this and its child regions
*
* @param {THREE.Matrix4} transformation - The transformation matrix
* used for the transformation.
*/
this.setTransformation = transformation => {
tMatrix.set(...transformation);
group.matrix.copy(tMatrix);
group.updateMatrixWorld();
}
/**
* Set the name of this region.
*
* @param {String} nameIn - Name to be set for this region. It must be defined
* and non-empty.
*/
this.setName = (nameIn) => {
if (nameIn && nameIn !== "") {
name = nameIn;
}
}
/**
* Get the name of this region.
*
* @return {String}
*/
this.getName = () => {
return name;
}
/**
* Get the parent region.
*
* @return {Region}
*/
this.getParent = () => {
return parent;
}
/**
* Get the array of each hierarachy from the root region to this region.
*
* @return {Array}
*/
this.getFullSeparatedPath = () => {
const paths = [];
if (name !== "") {
paths.push(name);
for (let p = parent; p !== undefined;) {
const parentName = p.getName();
if (parentName !== "") {
paths.unshift(parentName);
}
p = p.getParent();
}
}
return paths;
}
/**
* Get the full paths from the root region to this region.
*
* @return {String}
*/
this.getFullPath = () => {
const paths = this.getFullSeparatedPath();
if (paths.length > 0) {
let fullPath = paths.shift();
paths.forEach(path => {
fullPath = fullPath.concat("/", path);
});
return fullPath;
}
return "";
}
/**
* Create a new child region with the provided name.
* @param {String} nameIn - Name to be set for the new child region.
*
* @return {Region}
*/
this.createChild = (nameIn) => {
let childRegion = new Region(this, scene);
childRegion.setName(nameIn);
children.push(childRegion);
group.add(childRegion.getGroup());
return childRegion;
}
/**
* Get the child region with matching childName.
* @param {String} childName - Name to be matched.
*
* @return {Region}
*/
this.getChildWithName = childName => {
if (childName) {
const lowerChildName = childName.toLowerCase();
for (let i = 0; i < children.length; i++) {
if (children[i].getName().toLowerCase() === lowerChildName)
return children[i];
}
}
return undefined;
}
/**
* Find a child region using the path array.
* @param {Array} pathArray - Array containing regions' name at each
* hierarchy to match.
*
* @return {Region}
*/
this.findChildFromSeparatedPath = pathArray => {
if (pathArray && pathArray.length > 0) {
if (pathArray[0] === "") {
pathArray.shift();
}
}
if (pathArray && pathArray.length > 0) {
const childRegion = this.getChildWithName(pathArray[0]);
if (childRegion) {
pathArray.shift();
return childRegion.findChildFromSeparatedPath(pathArray);
} else {
return undefined;
}
}
return this;
}
/**
* Find the region using the provided relative path.
*
* @param {String} path - Relative paths from this region
* to the child region.
*
* @return {Region}
*/
this.findChildFromPath = (path) => {
const pathArray = path.split("/");
return this.findChildFromSeparatedPath(pathArray);
}
/**
* Create a new child using the path array. All required new regions
* down the path will be created.
*
* @param {Array} pathArray - Array containing regions' name, new regions
* will be created along the path if not found.
*
* @return {Region}
*/
this.createChildFromSeparatedPath = pathArray => {
if (pathArray.length > 0) {
if (pathArray[0] === "") {
pathArray.shift();
}
}
if (pathArray.length > 0) {
let childRegion = this.getChildWithName(pathArray[0]);
if (!childRegion) {
childRegion = this.createChild(pathArray[0]);
}
pathArray.shift();
return childRegion.createChildFromSeparatedPath(pathArray);
}
return this;
}
/**
* Create a new child using the path. All required new regions
* down the path will be created.
*
* @param {String} path - Relative paths from the region
* to the child region.
*
* @return {Region}
*/
this.createChildFromPath = (path) => {
const pathArray = path.split("/");
return this.createChildFromSeparatedPath(pathArray);
}
/**
* Return existing region if it exists, otherwise, create a new
* region with the provided path.
*
* @param {String} path - Relative paths from the region
* to the child region.
*
* @return {Region}
*/
this.findOrCreateChildFromPath = (path) => {
let childRegion = this.findChildFromPath(path);
if (!childRegion) {
childRegion = this.createChildFromPath(path);
}
return childRegion;
}
/**
* Add a zinc object into this region, the morph will be added
* to the group.
*
* @param {ZincObject} zincObject - Zinc object to be added into
* this region.
*/
this.addZincObject = zincObject => {
if (zincObject) {
zincObject.setRegion(this);
group.add(zincObject.getGroup());
zincObjects.push(zincObject);
this.pickableUpdateRequired = true;
if (scene) {
scene.triggerObjectAddedCallback(zincObject);
}
}
}
/**
* Remove a ZincObject from this region if it presents. This will eventually
* destroy the object and free up the memory.
*
* @param {ZincObject} zincObject - object to be removed from this region.
*/
this.removeZincObject = zincObject => {
for (let i = 0; i < zincObjects.length; i++) {
if (zincObject === zincObjects[i]) {
group.remove(zincObject.getGroup());
zincObjects.splice(i, 1);
if (scene) {
scene.triggerObjectRemovedCallback(zincObject);
}
zincObject.dispose();
this.pickableUpdateRequired = true;
return;
}
}
}
/**
* Return true if pickable objects require an update.
*
* @param {Boolean} transverse - Check child regions as well
* if this is set to true.
*
* @return {Boolean}
*/
this.checkPickableUpdateRequred = (transverse) => {
if (this.pickableUpdateRequired) return true;
if (transverse) {
let flag = false;
for (let i = 0; i < children.length; i++) {
flag = children[i].checkPickableUpdateRequred(transverse);
if (flag) return true;
}
}
return false;
}
/**
* Get all pickable objects.
*/
this.getPickableThreeJSObjects = (objectsList, transverse) => {
if (group.visible) {
zincObjects.forEach(zincObject => {
if (zincObject.getGroup() && zincObject.getGroup().visible) {
let marker = zincObject.marker;
if (marker && marker.isEnabled()) {
objectsList.push(marker.getMorph());
}
objectsList.push(zincObject.getGroup());
}
});
if (transverse) {
children.forEach(childRegion => {
childRegion.getPickableThreeJSObjects(objectsList, transverse);
});
}
this.pickableUpdateRequired = false;
}
return objectsList;
}
/**
* Set the default duration value for all zinc objects
* that are to be loaded into this region.
*
* @param {Number} durationIn - duration of the scene.
*/
this.setDuration = durationIn => {
duration = durationIn;
zincObjects.forEach(zincObject => zincObject.setDuration(durationIn));
children.forEach(childRegion => childRegion.setDuration(durationIn));
}
/**
* Get the default duration value.
* returns {Number}
*/
this.getDuration = () => {
return duration;
}
/**
* Get the bounding box of all the object in this and child regions only.
* Do not include the matrix transformation here, it is done at the primitives
* level.
*
* @returns {THREE.Box3}
*/
this.getBoundingBox = transverse => {
let boundingBox1 = undefined, boundingBox2 = undefined;
zincObjects.forEach(zincObject => {
boundingBox2 = zincObject.getBoundingBox();
if (boundingBox2) {
if (boundingBox1 == undefined) {
boundingBox1 = boundingBox2.clone();
} else {
boundingBox1.union(boundingBox2);
}
}
});
if (transverse) {
children.forEach(childRegion => {
boundingBox2 = childRegion.getBoundingBox(transverse);
if (boundingBox2) {
if (boundingBox1 == undefined) {
boundingBox1 = boundingBox2.clone();
} else {
boundingBox1.union(boundingBox2);
}
}
});
}
return boundingBox1;
}
/**
* Clear and dispose all objects belong to this region.
*
* @param {Boolean} transverse - Clear and dispose child regions as well
* if this is set to true.
*/
this.clear = transverse => {
if (transverse) {
children.forEach(childRegion => childRegion.clear(transverse));
}
zincObjects.forEach(zincObject => {
group.remove(zincObject.getGroup());
zincObject.dispose();
});
children = [];
zincObjects = [];
}
/**
* Check if a zincObject is a member of this region.
*
* @param {ZincObject} zincObject - The ZincObject to be checked.
* @param {Boolean} transverse - Also check the child regions.
*
* @return {Boolean}
*/
this.objectIsInRegion = (zincObject, transverse) => {
for (let i = 0; i < zincObjects.length; i++) {
if (zincObject === zincObjects[i]) {
return true;
}
}
if (transverse) {
for (let i = 0; i < children.length; i++) {
if (children[i].objectIsInRegion(zincObject, transverse))
return true;
}
}
return false;
}
/**
* A function which iterates through the list of geometries and call the callback
* function with the geometries as the argument.
*
* @param {Function} callbackFunction - Callback function with the geometry
* as an argument.
* @param {Boolean} transverse - Also perform the same callback function for
* all child regions if this is set to be true.
*/
this.forEachGeometry = (callbackFunction, transverse) => {
zincObjects.forEach(zincObject => {
if (zincObject.isGeometry)
callbackFunction(zincObject);
});
if (transverse)
children.forEach(childRegion => childRegion.forEachGeometry(
callbackFunction, transverse));
}
/**
* A function which iterates through the list of glyphsets and call the callback
* function with the glyphset as the argument.
*
* @param {Function} callbackFunction - Callback function with the glyphset
* as an argument.
* @param {Boolean} transverse - Also perform the same callback function for
* all child regions if this is set to be true.
*/
this.forEachGlyphset = (callbackFunction, transverse) => {
zincObjects.forEach(zincObject => {
if (zincObject.isGlyphset)
callbackFunction(zincObject);
});
if (transverse)
children.forEach(childRegion => childRegion.forEachGlyphset(
callbackFunction, transverse));
}
/**
* A function which iterates through the list of pointsets and call the callback
* function with the pointset as the argument.
*
* @param {Function} callbackFunction - Callback function with the pointset
* as an argument.
* @param {Boolean} transverse - Also perform the same callback function for
* all child regions if this is set to be true.
*/
this.forEachPointset = (callbackFunction, transverse) => {
zincObjects.forEach(zincObject => {
if (zincObject.isPointset)
callbackFunction(zincObject);
});
if (transverse)
children.forEach(childRegion => childRegion.forEachPointset(
callbackFunction, transverse));
}
/**
* A function which iterates through the list of lines and call the callback
* function with the lines as the argument.
*
* @param {Function} callbackFunction - Callback function with the lines
* as an argument.
* @param {Boolean} transverse - Also perform the same callback function for
* all child regions if this is set to be true.
*/
this.forEachLine = (callbackFunction, transverse) => {
zincObjects.forEach(zincObject => {
if (zincObject.isLines)
callbackFunction(zincObject);
});
if (transverse)
children.forEach(childRegion => childRegion.forEachLine(
callbackFunction, transverse));
}
this.findObjectsWithAnatomicalId = (anatomicalId, transverse) => {
zincObjects.forEach(zincObject => {
if (zincObject.anatomicalId === anatomicalId)
objectsArray.push(zincObject);
});
if (transverse) {
children.forEach(childRegion => {
let childObjects = childRegion.findObjectsWithAnatomicalId(anatomicalId, transverse);
objectsArray.push(...childObjects);
});
}
return objectsArray;
}
/**
* Find and return all zinc objects in this and child regions with
* the matching GroupName.
*
* @param {String} groupName - Groupname to match with.
* @param {Boolean} transverse - Also look for the object with groupName
* in child regions if set to true.
* @returns {Array}
*/
this.findObjectsWithGroupName = (groupName, transverse) => {
const objectsArray = [];
zincObjects.forEach(zincObject => {
const lowerObjectName = zincObject.groupName ? zincObject.groupName.toLowerCase() : zincObject.groupName;
const lowerGroupName = groupName ? groupName.toLowerCase() : groupName;
if (lowerObjectName === lowerGroupName)
objectsArray.push(zincObject);
});
if (transverse) {
children.forEach(childRegion => {
let childObjects = childRegion.findObjectsWithGroupName(groupName, transverse);
objectsArray.push(...childObjects);
});
}
return objectsArray;
}
/**
* Find and return all geometries in this and child regions with
* the matching GroupName.
*
* @param {String} groupName - Groupname to match with.
* @param {Boolean} transverse - Also look for the object with groupName
* in child regions if set to true.
* @returns {Array}
*/
this.findGeometriesWithGroupName = (groupName, transverse) => {
const primitivesArray = this.findObjectsWithGroupName(groupName, transverse);
const geometriesArray = primitivesArray.filter(primitive => primitive.isGeometry);
return geometriesArray;
}
/**
* Find and return all pointsets in this and child regions with
* the matching groupName.
*
* @param {String} groupName - Groupname to match with.
* @param {Boolean} transverse - Also look for the object with groupName
* in child regions if set to true.
* @returns {Array}
*/
this.findPointsetsWithGroupName = (groupName, transverse) => {
const primitivesArray = this.findObjectsWithGroupName(groupName, transverse);
const pointsetsArray = primitivesArray.filter(primitive => primitive.isPointset);
return pointsetsArray;
}
/**
* Find and return all glyphsets in this and child regions with
* the matching groupName.
*
* @param {String} groupName - Groupname to match with.
* @param {Boolean} transverse - Also look for the object with groupName
* in child regions if set to true.
* @returns {Array}
*/
this.findGlyphsetsWithGroupName = (groupName, transverse) => {
const primitivesArray = this.findObjectsWithGroupName(groupName, transverse);
const glyphsetsArray = primitivesArray.filter(primitive => primitive.isGlyphset);
return glyphsetsArray;
}
/**
* Find and return all lines in this and child regions with
* the matching groupName.
*
* @param {String} groupName - Groupname to match with.
* @param {Boolean} transverse - Also look for the object with groupName
* in child regions if set to true.
* @returns {Array}
*/
this.findLinesWithGroupName = (groupName, transverse) => {
const primitivesArray = this.findObjectsWithGroupName(groupName, transverse);
const linesArray = primitivesArray.filter(primitive => primitive.isLines);
return linesArray;
}
/**
* Get all zinc objects in this region.
*
* @param {Boolean} transverse - Include zinc objects in child regions if this is
* set to true.
* @returns {Array}
*/
this.getAllObjects = transverse => {
const objectsArray = [...zincObjects];
if (transverse) {
children.forEach(childRegion => {
let childObjects = childRegion.getAllObjects(transverse);
objectsArray.push(...childObjects);
});
}
return objectsArray;
}
/**
* Get all child regions.
*
* @param {Boolean} transverse - Include all regions which are descendants of
* this reigon when this is set to true.
* @returns {Array}
*/
this.getChildRegions = transverse => {
const objectsArray = [...children];
if (transverse) {
children.forEach(childRegion => {
const childObjects = childRegion.getChildRegions(transverse);
objectsArray.push(...childObjects);
});
}
return objectsArray;
}
/**
* Get the current time of the region.
* Return -1 if no graphics in the region.
*
* @return {Number}
*/
this.getCurrentTime = () => {
if (zincObjects[0] != undefined) {
return zincObjects[0].getCurrentTime();
} else {
for (let i = 0; i < children.length; i++) {
const time = children[i].getCurrentTime();
if (time !== -1)
return time;
}
}
return -1;
}
/**
* Set the current time of all the objects of this region.
*
* @param {Number} time - Value to set the time to.
* @param {Boolean} transverse - Set the time for chidl regions if
* this is set to true.
*/
this.setMorphTime = (time, transverse) => {
zincObjects.forEach(zincObject => {
zincObject.setMorphTime(time);
});
if (transverse) {
children.forEach(childRegion => {
childRegion.setMorphTime(time);
});
}
}
/**
* Check if any object in this region is time varying.
*
* @return {Boolean}
*/
this.isTimeVarying = () => {
for (let i = 0; i < zincObjects.length; i++) {
if (zincObjects[i].isTimeVarying()) {
return true;
}
}
for (let i = 0; i < children.length; i++) {
if (children[i].isTimeVarying()) {
return true;
}
}
return false;
}
/**
* Update geometries and glyphsets based on the calculated time.
* @private
*/
this.renderGeometries = (playRate, delta, playAnimation, cameraControls, options, transverse) => {
// Let video dictates the progress if one is present
const allObjects = this.getAllObjects(transverse);
allObjects.forEach(zincObject => {
zincObject.render(playRate * delta, playAnimation, cameraControls, options);
});
//process markers visibility and size, as long as there are more than
//one entry in markersList is greater than 1, markers have been enabled.
if (options && (playAnimation === false) &&
options.markerCluster?.markerUpdateRequired) {
/**
const markerDepths = Object.values(options.markersList)
.map((marker) => marker.ndc.z);
if (markerDepths.length > 1) {
const min = Math.min(...markerDepths);
const max = Math.max(...markerDepths);
allObjects.forEach(zincObject => {
zincObject.processMarkerVisual(min, max);
});
}
*/
options.markerCluster.calculate();
}
}
/**
* Update geometries and glyphsets based on the calculated time.
*/
this.createPoints = ( groupName, coords, labels, colour ) => {
let isNew = false;
const zincObjects = this.findObjectsWithGroupName(groupName, false);
const index = zincObjects.findIndex((zincObject) => zincObject.isPointset);
const pointset = index > -1 ? zincObjects[index] : new Pointset();
pointset.addPoints(coords, labels, colour);
if (index === -1) {
pointset.setName(groupName);
this.addZincObject(pointset);
isNew = true;
} else {
this.pickableUpdateRequired = true;
}
return { zincObject: pointset, isNew };
}
/**
* Update geometries and glyphsets based on the calculated time.
*/
this.createLines = ( groupName, coords, colour ) => {
let isNew = false;
const zincObjects = this.findObjectsWithGroupName(groupName, false);
const index = zincObjects.findIndex((zincObject) => zincObject.isLines);
const lines = index > -1 ? zincObjects[index] : new Lines2();
lines.addLines(coords, colour);
if (index === -1) {
lines.setName(groupName);
this.addZincObject(lines);
isNew = true;
} else {
this.pickableUpdateRequired = true;
}
return { zincObject: lines, isNew };
}
/**
* Add a new geometry
*/
this.createGeometryFromThreeJSGeometry = (
groupName, geometry, colour, opacity, visibility, renderOrder) => {
const zincGeometry = new Geometry();
const material = new THREE.MeshPhongMaterial({
color : colour,
morphTargets : false,
morphNormals : false,
transparent : true,
opacity : opacity,
side : THREE.DoubleSide
});
zincGeometry.createMesh(
geometry,
material,
{localTimeEnabled: false, localMorphColour: false,},
);
if (zincGeometry.getMorph()) {
zincGeometry.setVisibility(false);
zincGeometry.setName(groupName);
zincGeometry.setRenderOrder(renderOrder);
this.addZincObject(zincGeometry);
return zincGeometry;
}
return undefined;
}
}
exports.Region = Region;