import * as THREE from 'three/webgpu';
import { CameraControls, StereoEffect } from './controls';
import { LineSegments } from './three/line/LineSegments';
import { MarkerCluster } from './primitives/markerCluster';
import { Minimap } from './minimap';
import { Region } from './region';
import { InstancedPoints } from './three/InstancedPoints';
import { SceneExporter } from './sceneExporter';
import { SceneLoader } from './sceneLoader';
import { Viewport } from './controls';
import { createPointQuadGeometry } from './primitives/pointset';
import { createInstancedPointsMaterial } from './tsl/pointsMaterial';
import { createBufferGeometry, createNewSpriteText, getCircularTexture } from './utilities';
let uniqueiId = 0;
const getUniqueId = function () {
return 'sc' + uniqueiId++;
};
const defaultMetadata = function () {
return {
Duration: '6 secs',
OriginalDuration: '-',
TimeStamps: {},
};
};
const defaultDuration = 6000;
/**
* A Scene contains {@link Region},and
* {@link CameraControls} which controls the viewport and additional features.
* It is the main object used for controlling what is and what is not displayed
* on the renderer.
*
* @class
* @param {Object} containerIn - Container to create the renderer on.
* @author Alan Wu
* @return {Scene}
*/
const Scene = function (containerIn, rendererIn) {
const container = containerIn;
let cameraHelper = undefined;
let videoHandler = undefined;
//Set when something has changed and the scene needs to be drawn,
//used for render on demand.
let needsRender = true;
//Set when the camera has been updated on the current frame
let cameraUpdated = false;
//Options passed to the objects on every frame
const renderOptions = {};
//Values of the display properties on the last frame, changing them
//requires the scene to be drawn.
const lastDisplayState = {
displayMarkers: undefined,
displayMinimap: undefined,
displayMiniAxes: undefined,
playAnimation: undefined,
};
let sceneLoader = new SceneLoader(this);
let minimap = undefined;
let zincObjectAddedCallbacks = {};
let zincObjectAddedCallbacks_id = 0;
let zincObjectRemovedCallbacks = {};
let zincObjectRemovedCallbacks_id = 0;
const scene = new THREE.Scene();
const miniAxesScene = new THREE.Scene();
const rootRegion = new Region(undefined, this);
scene.add(rootRegion.getGroup());
const tempGroup = new THREE.Group();
scene.add(tempGroup);
/**
* A {@link THREE.DirectionalLight} object for controlling lighting of this scene.
*/
this.directionalLight = undefined;
/**
* a {@link THREE.AmbientLight} for controlling the ambient lighting of this scene.
*/
this.ambient = undefined;
this.camera = undefined;
let duration = 6000;
let zincCameraControls = undefined;
this.sceneName = undefined;
let stereoEffectFlag = false;
let stereoEffect = undefined;
this.autoClearFlag = true;
this.displayMarkers = false;
this.displayMinimap = false;
this.displayMiniAxes = false;
this.minimapScissor = {
x_offset: 16,
y_offset: 16,
width: 128,
height: 128,
align: 'top-right',
updateRequired: true,
};
let scissor = { x: 0, y: 0 };
let metadata = defaultMetadata();
let _markerTarget = new THREE.Vector2();
let pickableObjectsList = [];
this.forcePickableObjectsUpdate = false;
this.uuid = getUniqueId();
let markerCluster = new MarkerCluster(this);
markerCluster.disable();
scene.add(markerCluster.group);
let axisDisplay = {
main: [],
mini: [],
};
const _v3 = new THREE.Vector3(0, 0, 0);
const getDrawingWidth = () => {
if (container)
if (typeof container.clientWidth !== 'undefined') return container.clientWidth;
else return container.width;
return 0;
};
const getDrawingHeight = () => {
if (container)
if (typeof container.clientHeight !== 'undefined') return container.clientHeight;
else return container.height;
return 0;
};
/**
* This function returns a three component array, which contains
* [totalsize, totalLoaded and errorDownload] of all the downloads happening
* in this scene.
* @returns {Array}
*/
this.getDownloadProgress = () => {
return sceneLoader.getDownloadProgress();
};
//called from Renderer when panel has been resized
this.onWindowResize = () => {
needsRender = true;
const wHeight = getDrawingHeight();
this.camera.aspect = getDrawingWidth() / wHeight;
this.camera.updateProjectionMatrix();
this.minimapScissor.updateRequired = true;
zincCameraControls.onResize();
zincCameraControls.calculateHeightPerPixelAtZeroDepth(wHeight);
};
/**
* Reset the viewport of this scene to its original state.
*/
this.resetView = () => {
needsRender = true;
this.onWindowResize();
zincCameraControls.resetView();
};
/**
* Set the zoom level by unit scroll rate
*/
this.changeZoomByScrollRateUnit = (unit) => {
needsRender = true;
zincCameraControls.changeZoomByScrollRateUnit(unit);
};
//Setup the camera for this scene, it also initialise the lighting
const setupCamera = () => {
this.camera = new THREE.PerspectiveCamera(
40,
getDrawingWidth() / getDrawingHeight(),
0.0,
10.0,
);
this.ambient = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(this.ambient);
this.directionalLight = new THREE.DirectionalLight(0xffffff, 0.7 * Math.PI);
scene.add(this.directionalLight);
zincCameraControls = new CameraControls(this.camera, rendererIn.domElement, rendererIn, this);
zincCameraControls.setDirectionalLight(this.directionalLight);
zincCameraControls.resetView();
minimap = new Minimap(this);
};
setupCamera();
/**
* Load the viewport Data from the argument {@link Zinc.Viewport} and set it as
* the default viewport of this scene.
*
* @param {Zinc.Viewport} viewData - Viewport data to be loaded.
*/
this.loadView = (settings) => {
needsRender = true;
const viewPort = new Viewport();
viewPort.setFromObject(settings);
zincCameraControls.setCurrentCameraSettings(viewPort);
return true;
};
/**
* Set up multiple views.
*
* @param {Zinc.Viewport} viewData - Viewport data to be loaded.
*/
this.setupMultipleViews = (defaultView, entries) => {
for (const [name, settings] of Object.entries(entries)) {
const viewport = new Viewport();
viewport.setFromObject(settings);
zincCameraControls.addViewport(name, viewport);
}
zincCameraControls.setDefaultViewport(defaultView);
};
/**
* Get the bounding box of all the object in this scene only.
*
* @returns {THREE.Box3}
*/
this.getBoundingBox = () => {
return rootRegion.getBoundingBox(true);
};
/**
* Adjust the viewport to display the desired volume provided by the bounding box.
*
* @param {THREE.Box3} boundingBox - The bounding box which describes the volume of
* which we the viewport should be displaying.
*/
this.viewAllWithBoundingBox = (boundingBox) => {
needsRender = true;
if (boundingBox) {
const viewport = zincCameraControls.getViewportFromBoundingBox(boundingBox, 1.0);
zincCameraControls.setCurrentCameraSettings(viewport);
zincCameraControls.calculateHeightPerPixelAtZeroDepth(getDrawingHeight());
markerCluster.markerUpdateRequired = true;
}
};
/**
* Adjust zoom distance to include all primitives in scene only.
*/
this.viewAll = () => {
needsRender = true;
const boundingBox = this.getBoundingBox();
this.viewAllWithBoundingBox(boundingBox);
markerCluster.markerUpdateRequired = true;
};
/**
* 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.
*/
this.forEachGeometry = (callbackFunction) => {
rootRegion.forEachGeometry(callbackFunction, true);
};
/**
* 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.
*/
this.forEachGlyphset = (callbackFunction) => {
rootRegion.forEachGlyphset(callbackFunction, true);
};
/**
* 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.
*/
this.forEachPointset = (callbackFunction) => {
rootRegion.forEachPointset(callbackFunction, true);
};
/**
* 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.
*/
this.forEachLine = (callbackFunction) => {
rootRegion.forEachLine(callbackFunction, true);
};
/**
* Find and return all geometries in this scene with the matching GroupName.
*
* @param {String} GroupName - Groupname to match with.
* @returns {Array}
*/
this.findGeometriesWithGroupName = (GroupName) => {
return rootRegion.findGeometriesWithGroupName(GroupName, true);
};
/**
* Find and return all pointsets in this scene with the matching GroupName.
*
* @param {String} GroupName - Groupname to match with.
* @returns {Array}
*/
this.findPointsetsWithGroupName = (GroupName) => {
return rootRegion.findPointsetsWithGroupName(GroupName, true);
};
/**
* Find and return all glyphsets in this scene with the matching GroupName.
*
* @param {String} GroupName - Groupname to match with.
* @returns {Array}
*/
this.findGlyphsetsWithGroupName = (GroupName) => {
return rootRegion.findGlyphsetsWithGroupName(GroupName, true);
};
/**
* Find and return all lines in this scene with the matching GroupName.
*
* @param {String} GroupName - Groupname to match with.
* @returns {Array}
*/
this.findLinesWithGroupName = (GroupName) => {
return rootRegion.findLinesWithGroupName(GroupName, true);
};
/**
* Find a list of objects with the specified name, this will
* tranverse through the region tree to find all child objects
* with matching name.
*
* @param {String} GroupName - Groupname to match with.
* @returns {Array}
*/
this.findObjectsWithGroupName = (GroupName) => {
return rootRegion.findObjectsWithGroupName(GroupName, true);
};
this.findObjectsWithAnatomicalId = (anatomicalId) => {
return rootRegion.findObjectsWithAnatomicalId(anatomicalId, true);
};
/**
* Get the bounding box of all zinc objects in list.
*
* @param {Array} objectsArray - Groupname to match with.
* @returns {THREE.Box3}
*/
this.getBoundingBoxOfZincObjects = (objectsArray) => {
let boundingBox = undefined;
for (let i = 0; i < objectsArray.length; i++) {
let box = objectsArray[i].getBoundingBox();
if (box) {
if (!boundingBox) boundingBox = box;
else boundingBox.union(box);
}
}
return boundingBox;
};
/**
* Convert the vector3 into screen coordinates.
*
* @param {THREE.Vector3} point - Vector 3 containing the point to convert,
* this vector will be overwritten with the returned value.
* @param {Array} objectsArray - Groupname to match with.
* @returns {THREE.Vector3}
*/
this.vectorToScreenXY = (point) => {
point.project(this.camera);
let width = getDrawingWidth();
let height = getDrawingHeight();
let widthHalf = width / 2;
let heightHalf = height / 2;
point.x = point.x * widthHalf + widthHalf;
point.y = -(point.y * heightHalf) + heightHalf;
return point;
};
/**
* Get the screen coordinate of the centroid of provided list of objects.
*
* @param {Array} zincObjects - List of {@link ZincObject}.
* @returns {THREE.Vector3}
*/
this.getObjectsScreenXY = (zincObjects) => {
if (zincObjects && zincObjects.length > 0) {
let boundingBox = this.getBoundingBoxOfZincObjects(zincObjects);
const center = new THREE.Vector3();
boundingBox.getCenter(center);
return this.vectorToScreenXY(center);
}
return undefined;
};
/**
* Get the screen coordinate of the centroid of all objects
* in scene with the provided name.
*
* @param {String} name - List of {@link ZincObject}.
* @returns {THREE.Vector3}
*/
this.getNamedObjectsScreenXY = (name) => {
let zincObjects = this.findObjectsWithGroupName(name);
return this.getObjectsScreenXY(zincObjects);
};
/**
* Add zinc object into the root {@link Region} of sfcene.
*
* @param {ZincObject} - zinc object ot be added.
* @returns {THREE.Vector3}
*/
this.addZincObject = (zincObject) => {
needsRender = true;
if (zincObject) {
rootRegion.addZincObject(zincObject);
if (zincCameraControls) zincCameraControls.calculateMaxAllowedDistance(this);
}
};
/**
* Load a glyphset into this scene object.
*
* @param {String} metaurl - Provide informations such as transformations, colours
* and others for each of the glyph in the glyphsset.
* @param {String} glyphurl - regular json model file providing geometry of the glyph.
* @param {String} groupName - name to assign the glyphset's groupname to.
* @param {Function} finishCallback - Callback function which will be called
* once the glyphset is succssfully load in.
*/
this.loadGlyphsetURL = (metaurl, glyphurl, groupName, finishCallback) => {
sceneLoader.loadGlyphsetURL(rootRegion, metaurl, glyphurl, groupName, finishCallback);
};
/**
* Load a pointset into this scene object.
*
* @param {String} metaurl - Provide informations such as transformations, colours
* and others for each of the glyph in the glyphsset.
* @param {Boolean} timeEnabled - Indicate if morphing is enabled.
* @param {Boolean} morphColour - Indicate if color morphing is enabled.
* @param {STRING} groupName - name to assign the pointset's groupname to.
* @param {Function} finishCallback - Callback function which will be called
* once the glyphset is succssfully load in.
*/
this.loadPointsetURL = (url, timeEnabled, morphColour, groupName, finishCallback) => {
sceneLoader.loadPointsetURL(
rootRegion,
url,
timeEnabled,
morphColour,
groupName,
finishCallback,
);
};
/**
* Load lines into this scene object.
*
* @param {String} metaurl - Provide informations such as transformations, colours
* and others for each of the glyph in the glyphsset.
* @param {Boolean} timeEnabled - Indicate if morphing is enabled.
* @param {Boolean} morphColour - Indicate if color morphing is enabled.
* @param {STRING} groupName - name to assign the pointset's groupname to.
* @param {Function} finishCallback - Callback function which will be called
* once the glyphset is succssfully load in.
*/
this.loadLinesURL = (url, timeEnabled, morphColour, groupName, finishCallback) => {
sceneLoader.loadLinesURL(rootRegion, url, timeEnabled, morphColour, groupName, finishCallback);
};
/**
* Read a STL file into this scene, the geometry will be presented as
* {@link Zinc.Geometry}.
*
* @param {STRING} url - location to the STL file.
* @param {STRING} groupName - name to assign the geometry's groupname to.
* @param {Function} finishCallback - Callback function which will be called
* once the STL geometry is succssfully loaded.
*/
this.loadSTL = (url, groupName, finishCallback) => {
sceneLoader.loadSTL(rootRegion, url, groupName, finishCallback);
};
/**
* Read a OBJ file into this scene, the geometry will be presented as
* {@link Zinc.Geometry}.
*
* @param {STRING} url - location to the STL file.
* @param {STRING} groupName - name to assign the geometry's groupname to.
* @param {Function} finishCallback - Callback function which will be called
* once the OBJ geometry is succssfully loaded.
*/
this.loadOBJ = (url, groupName, finishCallback) => {
sceneLoader.loadOBJ(rootRegion, url, groupName, finishCallback);
};
/**
* Load a metadata file from the provided URL into this scene. Once
* succssful scene proceeds to read each items into scene for visualisations.
*
* @param {String} url - Location of the metafile
* @param {Function} finishCallback - Callback function which will be called
* for each glyphset and geometry that has been written in.
* @param {options} Optional settings, it can be used to ignore some regions/groups
* in the metadata file. Only supports version 1 at this moment.
*/
this.loadMetadataURL = (url, finishCallback, allCompletedCallback, options) => {
sceneLoader.loadMetadataURL(rootRegion, url, finishCallback, allCompletedCallback, options);
};
/**
* Load a legacy model(s) format with the provided URLs and parameters. This only loads the geometry
* without any of the metadata. Therefore, extra parameters should be provided.
*
* @deprecated
*/
this.loadModelsURL = (urls, colours, opacities, timeEnabled, morphColour, finishCallback) => {
sceneLoader.loadModelsURL(
rootRegion.urls,
colours,
opacities,
timeEnabled,
morphColour,
finishCallback,
);
};
/**
* Load the viewport from an external location provided by the url.
* @param {String} URL - address to the file containing viewport information.
*/
this.loadViewURL = (url) => {
sceneLoader.loadViewURL(url);
};
/**
* Load a legacy file format containing the viewport and its meta file from an external
* location provided by the url. Use the new metadata format with
* {@link Zinc.Scene#loadMetadataURL} instead.
*
* @param {String} URL - address to the file containing viewport and model information.
* @deprecated
*/
this.loadFromViewURL = (jsonFilePrefix, finishCallback) => {
sceneLoader.loadFromViewURL(jsonFilePrefix, finishCallback);
};
/**
* Load GLTF into this scene object.
*/
this.loadGLTF = (url, finishCallback, allCompletedCallback, options) => {
sceneLoader.loadGLTF(rootRegion, url, finishCallback, allCompletedCallback, options);
};
//Update the directional light for this scene.
this.updateDirectionalLight = () => {
needsRender = true;
zincCameraControls.updateDirectionalLight();
};
/**
* Add any {THREE.Object} into this scene.
* @param {THREE.Object} object - to be addded into this scene.
*/
this.addObject = (object) => {
needsRender = true;
scene.add(object);
};
/**
* Remove any {THREE.Object} from this scene.
* @param {THREE.Object} object - to be removed from this scene.
*/
this.removeObject = (object) => {
needsRender = true;
scene.remove(object);
};
/**
* Get the current time of the scene.
* @return {Number}
*/
this.getCurrentTime = () => {
if (videoHandler != undefined) {
return videoHandler.getCurrentTime(duration);
}
const time = rootRegion.getCurrentTime();
if (time !== -1) return time;
return 0;
};
/**
* Set the current time of all the geometries and glyphsets of this scene.
* @param {Number} time - Value to set the time to.
*/
this.setMorphsTime = (time) => {
needsRender = true;
if (videoHandler != undefined) {
videoHandler.setMorphTime(time, duration);
}
rootRegion.setMorphTime(time, true);
};
/**
* Check if any object in this scene is time varying.
*
* @return {Boolean}
*/
this.isTimeVarying = () => {
if (videoHandler && videoHandler.video && !videoHandler.video.error) {
return true;
}
return rootRegion.isTimeVarying();
};
/**
* Update geometries and glyphsets based on the calculated time.
* @private
*/
this.renderGeometries = (playRate, delta, playAnimation, sharedCamera, forceUpdate) => {
//Anything requested since the last frame
let changed = needsRender;
needsRender = false;
cameraUpdated = false;
//These are plain properties, check if they have changed before
//the objects are updated so markers are toggled on this frame
if (
lastDisplayState.displayMarkers !== this.displayMarkers ||
lastDisplayState.displayMinimap !== this.displayMinimap ||
lastDisplayState.displayMiniAxes !== this.displayMiniAxes ||
lastDisplayState.playAnimation !== playAnimation
) {
lastDisplayState.displayMarkers = this.displayMarkers;
lastDisplayState.displayMinimap = this.displayMinimap;
lastDisplayState.displayMiniAxes = this.displayMiniAxes;
lastDisplayState.playAnimation = playAnimation;
changed = true;
}
//Additional active scenes are displayed with the current scene's camera
const cameraControls = sharedCamera ? sharedCamera.controls : zincCameraControls;
//Reused on every frame to avoid allocations
const options = renderOptions;
options.camera = cameraControls;
//Global markers flag, marker can be set at individual zinc object level
//overriding this flag.
options.displayMarkers = this.displayMarkers;
options.markerCluster = markerCluster;
options.markersList = markerCluster.markers;
options.ndcToBeUpdated = sharedCamera ? sharedCamera.updated : false;
//Always set marker cluster update required when playAnimation is true
//to make sure it is updated when it stops
if (playAnimation) {
options.markerCluster.markerUpdateRequired = true;
}
if (videoHandler) {
if (videoHandler.isReadyToPlay()) {
if (playAnimation) {
videoHandler.video.play();
} else {
videoHandler.video.pause();
}
const currentTime =
(videoHandler.video.currentTime / videoHandler.getVideoDuration()) * duration;
if (0 == sceneLoader.toBeDownloaded) {
if (!sharedCamera) {
zincCameraControls.setTime(currentTime);
options.ndcToBeUpdated = zincCameraControls.update(0);
if (options.ndcToBeUpdated) {
zincCameraControls.calculateHeightPerPixelAtZeroDepth(getDrawingHeight());
}
}
rootRegion.setMorphTime(currentTime, true);
rootRegion.renderGeometries(0, 0, playAnimation, cameraControls, options, true);
} else if (!sharedCamera && zincCameraControls.update(0)) {
changed = true;
cameraUpdated = true;
}
//console.log(videoHandler.video.currentTime / videoHandler.getVideoDuration() * 6000);
}
//The video texture changes while playing or loading
if (playAnimation || !videoHandler.isReadyToPlay()) changed = true;
} else {
if (0 == sceneLoader.toBeDownloaded) {
if (!sharedCamera) {
options.ndcToBeUpdated = zincCameraControls.update(delta);
if (options.ndcToBeUpdated) {
zincCameraControls.calculateHeightPerPixelAtZeroDepth(getDrawingHeight());
}
}
//Walking every object is costly for large scenes, only do it when
//something may have changed, LOD and markers depend on the camera
if (
forceUpdate ||
changed ||
options.ndcToBeUpdated ||
playAnimation ||
//Clusters are recalculated during the walk
(markerCluster.isEnabled() && markerCluster.markerUpdateRequired)
) {
rootRegion.renderGeometries(playRate, delta, playAnimation, cameraControls, options, true);
}
} else if (!sharedCamera && zincCameraControls.update(0)) {
changed = true;
cameraUpdated = true;
}
}
//The flag is only cleared when the minimap is drawn
if (this.minimapScissor.updateRequired && (this.displayMinimap || this.displayMiniAxes)) {
changed = true;
}
if (!sharedCamera && options.ndcToBeUpdated) cameraUpdated = true;
if (
options.ndcToBeUpdated ||
(playAnimation && this.isTimeVarying()) ||
//Objects appear as they are loaded
sceneLoader.toBeDownloaded > 0 ||
//Clusters are recalculated on a later frame
(markerCluster.isEnabled() && markerCluster.markerUpdateRequired)
) {
changed = true;
}
//Changes made during this frame, e.g. LOD switching
if (needsRender) {
changed = true;
needsRender = false;
}
return changed;
};
/**
* Request this scene to be drawn on the next frame, this is only
* required when render on demand is enabled on the renderer.
* See {@link Renderer#setRenderOnDemand}.
*/
this.invalidate = () => {
needsRender = true;
};
/**
* Check if the camera of this scene has been updated during the last
* call to renderGeometries.
*
* @return {Boolean}
* @private
*/
this.isCameraUpdated = () => {
return cameraUpdated;
};
/**
* Return the internal {THREE.Scene}.
* @return {THREE.Scene}
*/
this.getThreeJSScene = () => {
return scene;
};
this.setVideoHandler = (videoHandlerIn) => {
needsRender = true;
if (!videoHandler) videoHandler = videoHandlerIn;
};
/**
* Set a group of scenes into this parent scene. This group of
* scenes will also be rendered when this scene is rendered.
* @private
*/
this.setAdditionalScenesGroup = (scenesGroup) => {
scene.add(scenesGroup);
};
let getWindowsPosition = (
align,
x_offset,
y_offset,
width,
height,
renderer_width,
renderer_height,
) => {
let x = 0;
let y = 0;
if (align.includes('top')) {
y = renderer_height - height - y_offset;
} else if (align.includes('bottom')) {
y = y_offset;
} else {
y = Math.floor((renderer_height - height) / 2.0);
}
if (align.includes('left')) {
x = x_offset;
} else if (align.includes('right')) {
x = renderer_width - x_offset - width;
} else {
x = Math.floor((renderer_width - width) / 2.0);
}
return { x: x, y: y };
};
const renderMinimap = (renderer) => {
if (this.displayMinimap || this.displayMiniAxes) {
renderer.setScissorTest(true);
renderer.getSize(_markerTarget);
if (this.minimapScissor.updateRequired) {
scissor = getWindowsPosition(
this.minimapScissor.align,
this.minimapScissor.x_offset,
this.minimapScissor.y_offset,
this.minimapScissor.width,
this.minimapScissor.height,
_markerTarget.x,
_markerTarget.y,
);
this.minimapScissor.updateRequired = false;
}
renderer.setScissor(
scissor.x,
scissor.y,
this.minimapScissor.width,
this.minimapScissor.height,
);
renderer.setViewport(
scissor.x,
scissor.y,
this.minimapScissor.width,
this.minimapScissor.height,
);
minimap.updateCamera();
if (this.displayMiniAxes) {
renderer.render(miniAxesScene, minimap.camera);
} else {
scene.add(minimap.mask);
renderer.render(scene, minimap.camera);
scene.remove(minimap.mask);
}
renderer.setScissorTest(false);
renderer.setViewport(0, 0, _markerTarget.x, _markerTarget.y);
}
};
/**
* Render the scene.
* @private
*/
this.render = (renderer) => {
if (this.autoClearFlag) renderer.clear();
if (stereoEffectFlag && stereoEffect) {
stereoEffect.render(scene, this.camera);
} else {
renderer.render(scene, this.camera);
renderMinimap(renderer);
}
};
/**
* Enable or disable interactive control, this is on by default.
*
* @param {Boolean} flag - Indicate either interactive control
* should be enabled or disabled.
*/
this.setInteractiveControlEnable = (flag) => {
if (flag == true) zincCameraControls.enable();
else zincCameraControls.disable();
};
/**
* Get the camera control of this scene.
* @return {Zinc.CameraControls}
*/
this.getZincCameraControls = () => {
return zincCameraControls;
};
/**
* Get the internal {THREE.Scene}.
* @return {THREE.Scene}
*/
this.getThreeJSScene = () => {
return scene;
};
/**
* Set the default duration value for geometries and glyphsets
* that are to be loaded into this scene.
* @param {Number} durationIn - duration of the scene.
*/
this.setDuration = (durationIn) => {
needsRender = true;
rootRegion.setDuration(durationIn);
duration = durationIn;
zincCameraControls.setPathDuration(durationIn);
sceneLoader.duration = durationIn;
};
/**
* Get the default duration value.
* @return {Number}
*/
this.getDuration = () => {
return duration;
};
/**
* Enable or disable stereo effect of this scene.
* @param {Boolean} stereoFlag - Indicate either stereo effect control
* should be enabled or disabled.
*/
this.setStereoEffectEnable = (stereoFlag) => {
needsRender = true;
if (stereoFlag == true) {
if (!stereoEffect) {
stereoEffect = new StereoEffect(rendererIn);
}
}
rendererIn.setSize(getDrawingWidth(), getDrawingHeight());
this.camera.updateProjectionMatrix();
stereoEffectFlag = stereoFlag;
};
/**
* Check rather object is in scene.
*
* @return {Boolean}
*/
this.objectIsInScene = (zincObject) => {
return rootRegion.objectIsInRegion(zincObject, true);
};
/**
* Rotate the camera view to view the entirety of the
* bounding box with a smooth transition within the providied
* transitionTime.
*
* @param {THREE.Box3} boundingBox - the bounding box to target
* @param {Number} transitionTime - Duration to perform the transition.
*/
this.alignBoundingBoxToCameraView = (boundingBox, transitionTime) => {
needsRender = true;
if (boundingBox) {
boundingBox.getCenter(_v3);
const viewport = this.getZincCameraControls().getCurrentViewport();
const target = new THREE.Vector3(
viewport.targetPosition[0],
viewport.targetPosition[1],
viewport.targetPosition[2],
);
const eyePosition = new THREE.Vector3(
viewport.eyePosition[0],
viewport.eyePosition[1],
viewport.eyePosition[2],
);
const newVec1 = new THREE.Vector3();
const newVec2 = new THREE.Vector3();
newVec1.subVectors(target, eyePosition).normalize();
newVec2.subVectors(target, _v3).normalize();
const newVec3 = new THREE.Vector3();
newVec3.crossVectors(newVec1, newVec2);
const angle = newVec1.angleTo(newVec2);
if (transitionTime > 0) {
this.getZincCameraControls().rotateCameraTransition(newVec3, angle, transitionTime);
this.getZincCameraControls().enableCameraTransition();
} else {
this.getZincCameraControls().rotateAboutLookAtpoint(newVec3, angle);
}
markerCluster.markerUpdateRequired = true;
}
};
/**
* Translate the camera view to the center of the
* bounding box with a smooth transition within the providied
* transitionTime.
*
* @param {THREE.Box3} boundingBox - the bounding box to target
* @param {Number} transitionTime - Duration to perform the transition.
*/
this.translateBoundingBoxToCameraView = (boundingBox, scaleRadius, transitionTime) => {
if (boundingBox) {
const oldViewport = this.getZincCameraControls().getCurrentViewport();
const viewport = this.getZincCameraControls().getViewportFromBoundingBox(
boundingBox,
scaleRadius,
);
if (transitionTime > 0) {
this.getZincCameraControls().cameraTransition(oldViewport, viewport, transitionTime);
this.getZincCameraControls().enableCameraTransition();
}
markerCluster.markerUpdateRequired = true;
}
};
/**
* Transition the camera into viewing the zinc object with a
* smooth transition within the providied transitionTime.
*
* @param {ZincObject} zincObject - the bounding box to target
* @param {Number} transitionTime - Duration to perform the transition.
*/
this.alignObjectToCameraView = (zincObject, transitionTime) => {
if (this.objectIsInScene(zincObject)) {
const boundingBox = zincObject.getBoundingBox();
this.alignBoundingBoxToCameraView(boundingBox, transitionTime);
}
};
/**
* Set the camera to point to the centroid of the zinc object.
*
* @param {ZincObject} zincObject - the bounding box to target
*/
this.setCameraTargetToObject = (zincObject) => {
needsRender = true;
if (this.objectIsInScene(zincObject)) {
const boundingBox = zincObject.getBoundingBox();
const viewport = this.getZincCameraControls().getCurrentViewport();
boundingBox.getCenter(_v3);
const target = new THREE.Vector3(
viewport.targetPosition[0],
viewport.targetPosition[1],
viewport.targetPosition[2],
);
const eyePosition = new THREE.Vector3(
viewport.eyePosition[0],
viewport.eyePosition[1],
viewport.eyePosition[2],
);
const newVec1 = new THREE.Vector3();
const newVec2 = new THREE.Vector3();
newVec1.subVectors(eyePosition, target);
newVec2.addVectors(_v3, newVec1);
viewport.eyePosition[0] = newVec2.x;
viewport.eyePosition[1] = newVec2.y;
viewport.eyePosition[2] = newVec2.z;
viewport.targetPosition[0] = _v3.x;
viewport.targetPosition[1] = _v3.y;
viewport.targetPosition[2] = _v3.z;
this.getZincCameraControls().setCurrentCameraSettings(viewport);
markerCluster.markerUpdateRequired = true;
}
};
/**
* Check if stereo effect is enabled.
* @returns {Boolean}
*/
this.isStereoEffectEnable = () => {
return stereoEffectFlag;
};
/**
* Remove a ZincObject from this scene if it presents. This will eventually
* destroy the object and free up the memory.
* @param {Zinc.Object} zincObject - object to be removed from this scene.
*/
this.removeZincObject = (zincObject) => {
needsRender = true;
rootRegion.removeZincObject(zincObject);
if (zincCameraControls) {
zincCameraControls.calculateMaxAllowedDistance(this);
}
markerCluster.markerUpdateRequired = true;
};
/**
* Update pickable objects list
*/
this.updatePickableThreeJSObjects = () => {
pickableObjectsList.length = 0;
if (markerCluster.isEnabled) {
pickableObjectsList.push(markerCluster.group);
}
rootRegion.getPickableThreeJSObjects(pickableObjectsList, true);
this.forcePickableObjectsUpdate = false;
};
/**
* Get all pickable objects.
*/
this.getPickableThreeJSObjects = () => {
//The list will only be updated if changes have been made
//in region or a flag has been raise
if (this.forcePickableObjectsUpdate || rootRegion.checkPickableUpdateRequred(true)) {
this.updatePickableThreeJSObjects();
}
return pickableObjectsList;
};
/**
* Get the Normalised coordinates on minimap if mouse event is
* inside the minimap
*/
this.getNormalisedMinimapCoordinates = (renderer, event) => {
if (this.displayMinimap) {
const target = new THREE.Vector2();
renderer.getSize(target);
let offsetY = target.y - event.clientY;
if (
scissor.x + this.minimapScissor.width > event.clientX &&
event.clientX > scissor.x &&
scissor.y + this.minimapScissor.height > offsetY &&
offsetY > scissor.y
) {
let x = ((event.clientX - scissor.x) / this.minimapScissor.width) * 2.0 - 1.0;
let y = ((offsetY - scissor.y) / this.minimapScissor.height) * 2.0 - 1.0;
return { x: x, y: y };
}
}
return undefined;
};
/**
* Get the coordinates difference of the current viewing
* point and projected coordinates.
*/
this.getMinimapDiffFromNormalised = (x, y) => {
if (minimap) return minimap.getDiffFromNormalised(x, y);
return undefined;
};
/**
* Check if the renderer is running on the WebGL 2 fallback backend.
*
* @return {Boolean}
*/
this.isWebGL2 = () => {
return rendererIn?.backend?.isWebGLBackend === true;
};
/**
* Check if the renderer is running on the WebGPU backend.
*
* @return {Boolean}
*/
this.isWebGPU = () => {
return rendererIn?.backend?.isWebGPUBackend === true;
};
/**
* Get the THREE.WebGPURenderer this scene is being drawn with, for code
* that needs to dispatch its own GPU work (e.g. a TSL compute pass).
*
* @return {THREE.WebGPURenderer}
*/
this.getRenderer = () => {
return rendererIn;
};
/**
* Remove all objects that are created with ZincJS APIs and it will free the memory allocated.
* This does not remove obejcts that are added using the addObject APIs.
*/
this.clearAll = () => {
needsRender = true;
markerCluster.clear();
rootRegion.clear(true);
this.clearZincObjectAddedCallbacks();
this.clearZincObjectRemovedCallbacks();
sceneLoader.cancelPendingLoads();
if (zincCameraControls) {
zincCameraControls.calculateMaxAllowedDistance(this);
}
markerCluster.markerUpdateRequired = true;
};
/**
* All time stamp to the metadata TimeStamps field.
*/
this.addMetadataTimeStamp = (key, time) => {
metadata['TimeStamps'][key] = convertDurationObjectTomSec(time);
};
/**
* Get a specific metadata field.
*/
this.getMetadataTag = (key) => {
return metadata[key];
};
/**
* Get all metadata set for the scene.
*/
this.getMetadata = () => {
return metadata;
};
/**
* Set a specific metadata field.
*/
this.setMetadataTag = (key, value) => {
metadata[key] = value;
};
/**
* Remove a specific metadata field.
*/
this.removeMetadataTag = (key) => {
delete metadata[key];
};
/**
* Reset all metadata fields to original value.
*/
this.resetMetadata = () => {
metadata = defaultMetadata();
};
/**
* Reset duration of scene to default value.
*/
this.resetDuration = () => {
this.setDuration(defaultDuration);
};
// Turn the object into a readable string {years: years,months: months,
// weeks: weeks, days: days, hours: hours, mins: mins, secs: secs }
const convertDurationObjectToString = (duration) => {
return [
...(duration.years ? [`${duration.years}years`] : []),
...(duration.months ? [`${duration.months}months`] : []),
...(duration.weeks ? [`${duration.weeks}weeks`] : []),
...(duration.days ? [`${duration.days}days`] : []),
...(duration.hours ? [`${duration.hours}hours`] : []),
...(duration.mins ? [`${duration.mins}mins`] : []),
...(duration.secs ? [`${duration.secs}secs`] : []),
].join(' ');
};
// Turn the object into a number representing milliesecond {years: years,months: months,
// weeks: weeks, days: days, hours: hours, mins: mins, secs: secs }
const convertDurationObjectTomSec = (duration) => {
return duration.years
? duration.years * 31536000000
: 0 + duration.months
? duration.months * 2592000000
: 0 + duration.weeks
? duration.weeks * 604800000
: 0 + duration.days
? duration.days * 86400000
: 0 + duration.hours
? duration.hours * 3600000
: 0 + duration.mins
? duration.mins * 60000
: 0 + duration.secs
? duration.secs * 1000
: 0;
};
// Set the readable duration and timer using an object
// with the following format {years: years,months: months, weeks: weeks, days: days,
// hours: hours, mins: mins, secs: secs }
this.setDurationFromObject = (duration) => {
const string = convertDurationObjectToString(duration);
const millisec = convertDurationObjectTomSec(duration);
this.setMetadataTag('Duration', string);
this.setDuration(millisec);
};
// Set the readable original duration using an object
// with the following format {years: years,months: months, weeks: weeks, days: days,
// hours: hours, mins: mins, secs: secs }
this.setOriginalDurationFromObject = (duration) => {
const string = convertDurationObjectToString(duration);
this.setMetadataTag('OriginalDuration', string);
};
/**
* Export the scene in GLTF format, it can either return it in
* string or binary form.
*
* @param {Boolean} binary - Indicate it should be exported as binary or
* text.
*
* @return {Promise} The exported data if the promise resolve successfully
*/
this.exportGLTF = (binary) => {
const exporter = new SceneExporter(this);
return exporter.exportGLTF(binary);
};
/**
* Get the root region of the scene.
*
* @return {Region} Return the root region of the scene
*/
this.getRootRegion = () => {
return rootRegion;
};
/**
* Create points in region specified in the path
*
*/
this.createLines = (regionPath, groupName, coords, colour) => {
let region = rootRegion.findChildFromPath(regionPath);
if (region === undefined) {
region = rootRegion.createChildFromPath(regionPath);
}
return region.createLines(groupName, coords, colour);
};
/**
* Create points in region specified in the path
*
*/
this.createPoints = (regionPath, groupName, coords, labels, colour) => {
let region = rootRegion.findChildFromPath(regionPath);
if (region === undefined) {
region = rootRegion.createChildFromPath(regionPath);
}
return region.createPoints(groupName, coords, labels, colour);
};
/**
* Add a callback function which will be called everytime zinc object is added.
* @param {Function} callbackFunction - callbackFunction to be added.
*
* @return {Number}
*/
this.addZincObjectAddedCallbacks = (callbackFunction) => {
zincObjectAddedCallbacks_id = zincObjectAddedCallbacks_id + 1;
zincObjectAddedCallbacks[zincObjectAddedCallbacks_id] = callbackFunction;
return zincObjectAddedCallbacks_id;
};
/**
* Add a callback function which will be called everytime zinc object is removed.
* @param {Function} callbackFunction - callbackFunction to be added.
*
* @return {Number}
*/
this.addZincObjectRemovedCallbacks = (callbackFunction) => {
zincObjectRemovedCallbacks_id = zincObjectRemovedCallbacks_id + 1;
zincObjectRemovedCallbacks[zincObjectRemovedCallbacks_id] = callbackFunction;
return zincObjectRemovedCallbacks_id;
};
/**
* Remove a callback function that is previously added to the scene.
* @param {Number} id - identifier of the previously added callback function.
*/
this.removeZincObjectAddedCallbacks = (id) => {
if (id in zincObjectAddedCallbacks_id) {
delete zincObjectAddedCallbacks[id];
}
};
/**
* Remove a callback function that is previously added to the scene.
* @param {Number} id - identifier of the previously added callback function.
*/
this.removeZincObjectRemovedCallbacks = (id) => {
if (id in zincObjectRemovedCallbacks_id) {
delete zincObjectRemovedCallbacks[id];
}
};
/**
* Clear all zinc object callback function
*/
this.clearZincObjectAddedCallbacks = () => {
zincObjectAddedCallbacks = {};
zincObjectAddedCallbacks_id = 0;
};
/**
* Clear all zinc object callback function
*/
this.clearZincObjectRemovedCallbacks = () => {
zincObjectRemovedCallbacks = {};
zincObjectRemovedCallbacks_id = 0;
};
/**
* Used to trigger zinc object added callback
*/
this.triggerObjectAddedCallback = (zincObject) => {
for (let key in zincObjectAddedCallbacks) {
if (zincObjectAddedCallbacks.hasOwnProperty(key)) {
zincObjectAddedCallbacks[key](zincObject);
}
}
};
/**
* Used to trigger zinc object removed callback
*/
this.triggerObjectRemovedCallback = (zincObject) => {
for (let key in zincObjectRemovedCallbacks) {
if (zincObjectRemovedCallbacks.hasOwnProperty(key)) {
zincObjectRemovedCallbacks[key](zincObject);
}
}
};
/*
* Add temporary points to the scene which can be removed
* with clearTemporaryPrimitives method.
*/
this.addTemporaryPoints = (coords, colour) => {
needsRender = true;
const geometry = createPointQuadGeometry(coords.length);
const material = createInstancedPointsMaterial(getCircularTexture());
material.color.set(colour);
material.size = 15;
material.userData.uniforms.pointSize.value = 15;
material.alphaTest = 0.5;
const point = new InstancedPoints(geometry, material, coords.length);
const instancePosition = geometry.getAttribute('instancePosition');
//Share the array with the instance positions
point.pointPositions = instancePosition.array;
coords.forEach((coord, index) => {
instancePosition.setXYZ(index, coord[0], coord[1], coord[2]);
});
instancePosition.needsUpdate = true;
tempGroup.add(point);
return point;
};
/*
* Add temporary lines to the scene which can be removed
* with clearTemporaryPrimitives method.
*/
this.addTemporaryLines = (coords, colour) => {
needsRender = true;
const geometry = createBufferGeometry(coords.length, coords);
const material = new THREE.LineBasicMaterial({ color: colour });
const line = new LineSegments(geometry, material);
tempGroup.add(line);
return line;
};
/*
* Display frustum
*/
this.enableFrustumDisplay = () => {
needsRender = true;
if (this.camera && !cameraHelper) {
cameraHelper = new THREE.CameraHelper(this.camera);
scene.add(cameraHelper);
}
};
/*
* Hide frustum
*/
this.disableFrustumDisplay = () => {
needsRender = true;
if (cameraHelper) {
scene.remove(cameraHelper);
cameraHelper.dispose();
cameraHelper = undefined;
}
};
/*
* Remove object from temporary objects list
*/
this.removeTemporaryPrimitive = (object) => {
needsRender = true;
tempGroup.remove(object);
object.geometry.dispose();
object.material.dispose();
};
/*
* Remove all temporary primitives.
* Return number of primitives removed;
*/
this.clearTemporaryPrimitives = () => {
needsRender = true;
let i = 0;
const children = tempGroup.children;
children.forEach((child) => {
child.geometry.dispose();
child.material.dispose();
i++;
});
tempGroup.clear();
return i;
};
/*
* Create primitive based on the bounding box of scene and
* add to specify region and group name.
*/
this.addBoundingBoxPrimitive = (
regionPath,
group,
colour,
opacity,
visibility,
boundingBox = undefined,
) => {
needsRender = true;
let region = rootRegion.findChildFromPath(regionPath);
if (region === undefined) {
region = rootRegion.createChildFromPath(regionPath);
}
const box = boundingBox ? boundingBox : this.getBoundingBox();
_v3.set(0.0, 0.0, 0.0);
const dim = _v3.subVectors(box.max, box.min);
const boxGeo = new THREE.BoxGeometry(dim.x, dim.y, dim.z);
const primitive = region.createGeometryFromThreeJSGeometry(
group,
boxGeo,
colour,
opacity,
visibility,
10000,
);
dim.addVectors(box.min, box.max).multiplyScalar(0.5);
primitive.setPosition(dim.x, dim.y, dim.z);
return primitive;
};
/*
* Create primitive based on the bounding box of scene and
* add to specify region and group name.
*/
this.addSlicesPrimitive = (
regionPath,
groups,
colours,
opacity,
visibility,
boundingBox = undefined,
) => {
needsRender = true;
if (groups && groups.length >= 3 && colours && colours.length >= 3) {
let region = rootRegion.findChildFromPath(regionPath);
if (region === undefined) {
region = rootRegion.createChildFromPath(regionPath);
}
const box = boundingBox ? boundingBox : this.getBoundingBox();
_v3.set(0.0, 0.0, 0.0);
const dim = _v3.subVectors(box.max, box.min);
const directions = ['x', 'y', 'z'];
const primitives = [];
let index = 0;
directions.forEach((direction) => {
let planeGeo = undefined;
switch (direction) {
//YZ plane
case 'x':
planeGeo = new THREE.PlaneGeometry(dim.z, dim.y);
planeGeo.rotateY(Math.PI / 2);
// code block
break;
//XZ plane
case 'y':
planeGeo = new THREE.PlaneGeometry(dim.x, dim.z);
planeGeo.rotateX(Math.PI / 2);
// code block
break;
//XY plane
case 'z':
planeGeo = new THREE.PlaneGeometry(dim.x, dim.y);
// code block
break;
default:
break;
}
const primitive = region.createGeometryFromThreeJSGeometry(
groups[index],
planeGeo,
colours[index],
opacity,
visibility,
10001,
);
primitives.push(primitive);
index++;
});
dim.addVectors(box.min, box.max).multiplyScalar(0.5);
primitives.forEach((primitive) => {
primitive.setPosition(dim.x, dim.y, dim.z);
});
return primitives;
}
};
/*
* Enable marker cluster to work with markers
*/
this.enableMarkerCluster = (flag) => {
needsRender = true;
if (flag) {
markerCluster.markerUpdateRequired = true;
markerCluster.enable();
} else {
markerCluster.markerUpdateRequired = false;
markerCluster.disable();
}
this.forcePickableObjectsUpdate = true;
};
/*
* Destory static axis display object
*/
this.destroyAxisDisplay = () => {
needsRender = true;
this.displayMiniAxes = false;
if (axisDisplay.main) {
this.enableAxisDisplay(false, false);
axisDisplay.main.forEach((axis) => {
if (axis.dispose) {
axis.dispose();
}
});
}
if (axisDisplay.mini) {
this.enableAxisDisplay(false, true);
axisDisplay.mini.forEach((axis) => {
if (axis.dispose) {
axis.dispose();
}
});
}
axisDisplay = {
main: [],
mini: [],
};
};
/*
* Create static axis display object
*/
this.createAxisDisplay = (fitBoundingBox = false) => {
needsRender = true;
this.destroyAxisDisplay();
const XYZ = [
{
name: 'x',
dir: new THREE.Vector3(1, 0, 0),
colour: 'red',
hex: 0xff5555,
},
{
name: 'y',
dir: new THREE.Vector3(0, 1, 0),
colour: 'green',
hex: 0x55ff55,
},
{
name: 'z',
dir: new THREE.Vector3(0, 0, 1),
colour: 'blue',
hex: 0x5555ff,
},
];
const boundingBox = this.getBoundingBox();
const size = boundingBox.min.distanceTo(boundingBox.max);
let origin = new THREE.Vector3(0, 0, 0);
if (fitBoundingBox) {
origin.copy(boundingBox.min);
}
XYZ.forEach((xyzObj) => {
const arrowHelper = new THREE.ArrowHelper(xyzObj.dir, origin, size, xyzObj.hex);
axisDisplay.main.push(arrowHelper);
const miniArrowHelper = new THREE.ArrowHelper(
xyzObj.dir,
boundingBox.getCenter(_v3),
size / 2,
xyzObj.hex,
);
axisDisplay.mini.push(miniArrowHelper);
/*
const miniLabel = createNewSpriteText(xyzObj.name, 0.1, xyzObj.colour, "Asap", 120, 400);
const miniPosition = xyzObj.dir.clone().multiplyScalar(size/2).add(_v3[xyzObj.name]);
miniLabel.position.set(miniPosition.x, miniPosition.y, miniPosition.z);
axisDisplay.mini.push(miniLabel);
*/
const axesLabel = createNewSpriteText(xyzObj.name, 0.036, xyzObj.colour, 'Asap', 120, 700);
const position = xyzObj.dir.clone().multiplyScalar(size).add(origin);
axesLabel.position.set(position.x, position.y, position.z);
axisDisplay.main.push(axesLabel);
});
};
/*
* Enable static axis display, createAxisDisplay must be called
* before the axis can be display.
*/
this.enableAxisDisplay = (enable, miniaxes = false) => {
needsRender = true;
if (miniaxes && axisDisplay?.mini?.length) {
this.displayMiniAxes = enable;
axisDisplay.mini.forEach((axis) => {
enable ? miniAxesScene.add(axis) : miniAxesScene.remove(axis);
});
} else if (!miniaxes && axisDisplay.main) {
axisDisplay.main.forEach((axis) => {
enable ? scene.add(axis) : scene.remove(axis);
});
}
};
};
export { Scene };