import * as THREE from 'three/webgpu';
import { GLTFToZincJSLoader } from './loaders/GLTFToZincJSLoader';
import { OBJLoader } from './loaders/OBJLoader';
import { PrimitivesLoader } from './loaders/primitivesLoader';
import { STLLoader } from './loaders/STLLoader';
import { Geometry } from './primitives/geometry';
import { Glyphset } from './primitives/glyphset';
import { Lines } from './primitives/lines';
import { Pointset } from './primitives/pointset';
import { TextureSlides } from './primitives/textureSlides';
import { TubeLines } from './primitives/tubeLines';
import { createNewURL, isRegionGroup } from './utilities';
import Zinc from './zinc';
/**
* A helper class to help with reading / importing primitives and
* settings into a {@link Scene}.
*
* @class
* @param {Object} containerIn - Container to create the renderer on.
* @author Alan Wu
* @return {SceneLoader}
*/
const SceneLoader = function (sceneIn) {
const scene = sceneIn;
this.toBeDownloaded = 0;
this.progressMap = {};
let viewLoaded = false;
let errorDownload = false;
let generation = 0;
const primitivesLoader = new PrimitivesLoader();
/**
* Cancel all pending downloads, objects from the cancelled downloads
* will be discarded instead of being added into the scene once they
* arrive.
*/
this.cancelPendingLoads = () => {
generation++;
this.toBeDownloaded = 0;
this.progressMap = {};
viewLoaded = false;
errorDownload = false;
};
//Return a function which returns true if the loads requested before
//this call have been cancelled.
const createStaleCheck = () => {
const requestGeneration = generation;
return () => requestGeneration !== generation;
};
/**
* 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 = () => {
let totalSize = 0;
let totalLoaded = 0;
let unknownFound = false;
for (const key in this.progressMap) {
const progress = this.progressMap[key];
totalSize += progress[1];
totalLoaded += progress[0];
if (progress[1] == 0) unknownFound = true;
}
if (unknownFound) {
totalSize = 0;
}
return [totalSize, totalLoaded, errorDownload];
};
//Stores the current progress of downloads
this.onProgress = (id) => {
return (xhr) => {
this.progressMap[id] = [xhr.loaded, xhr.total];
};
};
this.onError = (finishCallback) => {
const isStale = createStaleCheck();
return (xhr) => {
if (isStale()) return;
this.toBeDownloaded = this.toBeDownloaded - 1;
errorDownload = true;
console.error(
`There is an issue with external resource ${xhr?.responseURL ? ': ' + xhr?.responseURL : ''}.`,
);
const payload = {
type: 'Error',
xhr,
};
if (finishCallback) {
finishCallback(payload);
}
};
};
let loadMultipleViews = (referenceURL, views) => {
const defaultView = views.Default;
if (views.Inline) {
scene.setupMultipleViews(defaultView, views.Entries);
} else {
const promises = [];
for (const [key, value] of Object.entries(views.Entries)) {
if (referenceURL) {
const newURL = createNewURL(value, referenceURL);
promises.push(
new Promise((resolve, reject) => {
// Add parameters if we are sent them
fetch(newURL)
.then((response) => response.json())
.then((data) => resolve({ key: key, data: data }))
.catch((data) => reject(data));
}),
);
}
}
Promise.all(promises).then((values) => {
const entries = {};
values.forEach((entry) => {
entries[entry.key] = entry.data;
});
scene.setupMultipleViews(defaultView, entries);
let zincCameraControls = scene.getZincCameraControls();
if (zincCameraControls) zincCameraControls.setCurrentViewport(defaultView);
viewLoaded = true;
});
}
};
/**
* Load the viewport from an external location provided by the url.
* @param {String} URL - address to the file containing viewport information.
*/
this.loadViewURL = (url, finishCallback) => {
this.toBeDownloaded += 1;
const requestURL = url;
const isStale = createStaleCheck();
fetch(requestURL)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((viewData) => {
if (isStale()) return;
scene.setupMultipleViews('default', { default: viewData });
scene.resetView();
viewLoaded = true;
--this.toBeDownloaded;
if (finishCallback !== undefined && typeof finishCallback === 'function') {
finishCallback();
}
})
.catch((error) => {
console.error(`Fetch failed for URL: ${requestURL}`, error);
this.onError(finishCallback)({ responseURL: requestURL });
});
};
/**
* 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 = (
region,
urls,
colours,
opacities,
timeEnabled,
morphColour,
finishCallback,
) => {
const number = urls.length;
this.toBeDownloaded += number;
for (let i = 0; i < number; i++) {
const filename = urls[i];
let colour = Zinc.defaultMaterialColor;
let opacity = Zinc.defaultOpacity;
if (colours != undefined && colours[i] != undefined) colour = colours[i] ? true : false;
if (opacities != undefined && opacities[i] != undefined) opacity = opacities[i];
let localTimeEnabled = 0;
if (timeEnabled != undefined && timeEnabled[i] != undefined)
localTimeEnabled = timeEnabled[i] ? true : false;
let localMorphColour = false;
if (morphColour != undefined && morphColour[i] != undefined)
localMorphColour = morphColour[i] ? true : false;
primitivesLoader.load(
filename,
meshloader(
region,
colour,
opacity,
localTimeEnabled,
localMorphColour,
undefined,
undefined,
undefined,
undefined,
finishCallback,
),
this.onProgress(filename),
this.onError(finishCallback),
);
}
};
/**
* 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.SceneLoader.#loadMetadataURL} instead.
*
* @param {String} URL - address to the file containing viewport and model information.
* @deprecated
*/
this.loadFromViewURL = (targetRegion, jsonFilePrefix, finishCallback) => {
const requestURL = jsonFilePrefix + '_view.json';
const isStale = createStaleCheck();
fetch(requestURL)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((viewData) => {
if (isStale()) return;
scene.loadView(viewData);
const urls = [];
const filename_prefix = jsonFilePrefix + '_';
for (let i = 0; i < viewData.numberOfResources; i++) {
const filename = filename_prefix + (i + 1) + '.json';
urls.push(filename);
}
this.loadModelsURL(
targetRegion,
urls,
viewData.colour,
viewData.opacity,
viewData.timeEnabled,
viewData.morphColour,
finishCallback,
);
})
.catch((error) => {
console.error(`Failed to load view data from: ${requestURL}`, error);
});
};
//Internal loader for a regular zinc geometry.
const linesloader = (
region,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
lod,
tubeLines,
finishCallback,
) => {
const isStale = createStaleCheck();
return (geometry, materials) => {
if (isStale()) {
geometry.dispose();
return;
}
const newLines = tubeLines ? new TubeLines() : new Lines();
let material = undefined;
if (materials && materials[0]) {
material = new THREE.LineBasicMaterial({ color: materials[0].color.clone() });
if (1.0 > materials[0].opacity) {
material.transparent = true;
}
material.opacity = materials[0].opacity;
material.vertexColors = materials[0].vertexColors;
}
let options = {};
options.localTimeEnabled = localTimeEnabled;
options.localMorphColour = localMorphColour;
if (newLines) {
newLines.createLineSegment(geometry, material, options);
newLines.setName(groupName);
newLines.setAnatomicalId(anatomicalId);
newLines.setRenderOrder(renderOrder);
region.addZincObject(newLines);
newLines.setDuration(scene.getDuration());
if (lod && lod.levels) {
for (const [key, value] of Object.entries(lod.levels)) {
newLines.addLOD(primitivesLoader, key, value.URL, value.Index, lod.preload);
}
}
}
--this.toBeDownloaded;
geometry.dispose();
if (finishCallback != undefined && typeof finishCallback == 'function')
finishCallback(newLines);
};
};
/**
* Load lines into this scene object.
*
* @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 = (
region,
url,
timeEnabled,
morphColour,
groupName,
finishCallback,
options,
) => {
let localTimeEnabled = 0;
this.toBeDownloaded += 1;
let isInline = options?.isInline ? options.isInline : false;
let anatomicalId = options?.anatomicalId ? options.anatomicalId : undefined;
let renderOrder = options && 'renderOrder' in options ? options.renderOrder : undefined;
if (timeEnabled != undefined) localTimeEnabled = timeEnabled ? true : false;
let localMorphColour = 0;
if (morphColour != undefined) localMorphColour = morphColour ? true : false;
let tubeLines = options.tubeLines && !localTimeEnabled && !localMorphColour;
if (isInline) {
let object = primitivesLoader.parse(url);
linesloader(
region,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
options.lod,
tubeLines,
finishCallback,
)(object.geometry, object.materials);
} else {
primitivesLoader.load(
url,
linesloader(
region,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
options.lod,
tubeLines,
finishCallback,
),
this.onProgress(url),
this.onError(finishCallback),
options.loaderOptions,
);
}
};
const glyphsetloader = (region, glyphurl, groupName, finishCallback, options) => {
const isStale = createStaleCheck();
return (data) => {
if (isStale()) return;
let glyphsetData = data;
if (typeof glyphsetData === 'string' || glyphsetData instanceof String) {
glyphsetData = JSON.parse(data);
}
let isInline = options && options.isInline ? options.isInline : undefined;
let anatomicalId = options && options.anatomicalId ? options.anatomicalId : undefined;
let displayLabels = options?.displayLabels;
if (displayLabels === undefined) {
displayLabels = true;
}
let renderOrder = options && 'renderOrder' in options ? options.renderOrder : undefined;
const newGlyphset = new Glyphset();
newGlyphset.setDuration(scene.getDuration());
newGlyphset.groupName = groupName;
let myCallback = () => {
if (isStale()) return;
--this.toBeDownloaded;
if (finishCallback != undefined && typeof finishCallback == 'function')
finishCallback(newGlyphset);
};
++this.toBeDownloaded;
if (isInline) {
newGlyphset.load(glyphsetData, glyphurl, myCallback, isInline, displayLabels);
} else {
newGlyphset.load(glyphsetData, glyphurl, myCallback, isInline, displayLabels);
}
newGlyphset.setAnatomicalId(anatomicalId);
newGlyphset.setRenderOrder(renderOrder);
region.addZincObject(newGlyphset);
};
};
//Load a glyphset into this scene.
const onLoadGlyphsetReady = (region, xmlhttp, glyphurl, groupName, finishCallback, options) => {
return () => {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
let glyphsetData = JSON.parse(xmlhttp.responseText);
if (Array.isArray(glyphsetData)) {
const index = options?.loaderOptions?.index;
if (index !== undefined) {
if (glyphsetData.length > index) {
glyphsetData = glyphsetData[index];
}
}
}
glyphsetloader(region, glyphurl, groupName, finishCallback, options)(glyphsetData);
}
};
};
/**
* 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 = (region, metaurl, glyphurl, groupName, finishCallback, options) => {
const isInline = options && options.isInline ? options.isInline : false;
if (isInline) {
glyphsetloader(region, glyphurl, groupName, finishCallback, options)(metaurl);
} else {
primitivesLoader.load(
metaurl,
glyphsetloader(region, glyphurl, groupName, finishCallback, options),
this.onProgress(metaurl),
this.onError(finishCallback),
options.loaderOptions,
);
/*
const xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = onLoadGlyphsetReady(region, xmlhttp, glyphurl,
groupName, finishCallback, options);
xmlhttp.open("GET", metaurl, true);
xmlhttp.send();*/
}
};
//Internal loader for zinc pointset.
const pointsetloader = (
region,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
finishCallback,
) => {
const isStale = createStaleCheck();
return (geometry, materials) => {
if (isStale()) {
geometry.dispose();
return;
}
const newPointset = new Pointset();
let material = new THREE.PointsMaterial({
alphaTest: 0.05,
size: 10,
sizeAttenuation: false,
});
if (materials && materials[0]) {
if (1.0 > materials[0].opacity) {
material.transparent = true;
}
material.opacity = materials[0].opacity;
material.color = materials[0].color;
material.vertexColors = false;
}
let options = {};
options.localTimeEnabled = localTimeEnabled;
options.localMorphColour = localMorphColour;
if (newPointset) {
newPointset.createMesh(geometry, material, options);
newPointset.setName(groupName);
newPointset.setAnatomicalId(anatomicalId);
region.addZincObject(newPointset);
newPointset.setDuration(scene.getDuration());
newPointset.setRenderOrder(renderOrder);
}
geometry.dispose();
--this.toBeDownloaded;
if (finishCallback != undefined && typeof finishCallback == 'function')
finishCallback(newPointset);
};
};
/**
* 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 = (region, url, groupName, finishCallback) => {
this.toBeDownloaded += 1;
const colour = Zinc.defaultMaterialColor;
const opacity = Zinc.defaultOpacity;
const loader = new STLLoader();
loader.crossOrigin = 'Anonymous';
loader.load(
url,
meshloader(
region,
colour,
opacity,
false,
false,
groupName,
undefined,
undefined,
{},
finishCallback,
),
this.onProgress(url),
this.onError(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 = (region, url, groupName, finishCallback) => {
this.toBeDownloaded += 1;
const colour = Zinc.defaultMaterialColor;
const opacity = Zinc.defaultOpacity;
const loader = new OBJLoader();
loader.crossOrigin = 'Anonymous';
loader.load(
url,
objloader(
region,
colour,
opacity,
false,
false,
groupName,
undefined,
undefined,
{},
finishCallback,
),
this.onProgress(url),
this.onError(finishCallback),
);
};
/**
* Load a geometry into this scene, this is a subsequent called from
* {@link Zinc.Scene#loadMetadataURL}, although it can be used to read
* in geometry into the scene externally.
*
* @param {String} url - regular json model file providing geometry.
* @param {Boolean} timeEnabled - Indicate if geometry morphing is enabled.
* @param {Boolean} morphColour - Indicate if color morphing is enabled.
* @param {STRING} groupName - name to assign the geometry's groupname to.
* @param {STRING} fileFormat - name supported formats are STL, OBJ and JSON.
* @param {Function} finishCallback - Callback function which will be called
* once the geometry is succssfully loaded in.
*/
const loadSurfaceURL = (
region,
url,
timeEnabled,
morphColour,
groupName,
finishCallback,
options,
) => {
this.toBeDownloaded += 1;
const colour = Zinc.defaultMaterialColor;
const opacity = Zinc.defaultOpacity;
let localTimeEnabled = 0;
let isInline = options && options.isInline ? options.isInline : false;
let fileFormat = options && options.fileFormat ? options.fileFormat : undefined;
let anatomicalId = options && options.anatomicalId ? options.anatomicalId : undefined;
let renderOrder = options && 'renderOrder' in options ? options.renderOrder : undefined;
if (timeEnabled != undefined) localTimeEnabled = timeEnabled ? true : false;
let localMorphColour = 0;
if (morphColour != undefined) localMorphColour = morphColour ? true : false;
let loader = primitivesLoader;
if (fileFormat == 'STL' || fileFormat == 'OBJ') {
const typedLoader = fileFormat == 'STL' ? meshloader : objloader;
loader = fileFormat == 'STL' ? new STLLoader() : new OBJLoader();
loader.crossOrigin = 'Anonymous';
loader.load(
url,
typedLoader(
region,
colour,
opacity,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
options ? options : {},
finishCallback,
),
this.onProgress(url),
this.onError(finishCallback),
);
return;
}
if (isInline) {
const object = primitivesLoader.parse(url);
meshloader(
region,
colour,
opacity,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
options,
finishCallback,
)(object.geometry, object.materials);
} else {
loader.crossOrigin = 'Anonymous';
primitivesLoader.load(
url,
meshloader(
region,
colour,
opacity,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
options,
finishCallback,
),
this.onProgress(url),
this.onError(finishCallback),
options.loaderOptions,
);
}
};
//Object to keep track of number of items downloaded and when all items are downloaded
//allCompletedCallback is called
const metaFinishCallback = function (numberOfDownloaded, finishCallback, allCompletedCallback) {
let downloadedItem = 0;
return (zincObject) => {
downloadedItem = downloadedItem + 1;
if (zincObject && finishCallback != undefined && typeof finishCallback == 'function') {
finishCallback(zincObject);
}
if (downloadedItem == numberOfDownloaded) {
if (viewLoaded === false) scene.viewAll();
if (allCompletedCallback != undefined && typeof allCompletedCallback == 'function') {
allCompletedCallback();
let zincCameraControls = scene.getZincCameraControls();
if (zincCameraControls) {
zincCameraControls.calculateMaxAllowedDistance(scene);
}
}
}
};
};
/**
* Load a pointset into this scene object.
*
* @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 = (
region,
url,
timeEnabled,
morphColour,
groupName,
finishCallback,
options,
) => {
let localTimeEnabled = 0;
this.toBeDownloaded += 1;
if (timeEnabled != undefined) localTimeEnabled = timeEnabled ? true : false;
let localMorphColour = 0;
if (morphColour != undefined) localMorphColour = morphColour ? true : false;
let isInline = options && options.isInline ? options.isInline : false;
let anatomicalId = options && options.anatomicalId ? options.anatomicalId : undefined;
let renderOrder = options && 'renderOrder' in options ? options.renderOrder : undefined;
if (isInline) {
const object = primitivesLoader.parse(url);
pointsetloader(
region,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
finishCallback,
)(object.geometry, object.materials);
} else {
primitivesLoader.load(
url,
pointsetloader(
region,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
finishCallback,
),
this.onProgress(url),
this.onError(finishCallback),
options.loaderOptions,
);
}
};
const loadTexture = (region, referenceURL, textureData, groupName, finishCallback, options) => {
let isInline = options && options.isInline ? options.isInline : undefined;
let anatomicalId = options && options.anatomicalId ? options.anatomicalId : undefined;
let renderOrder = options && 'renderOrder' in options ? options.renderOrder : undefined;
let newTexture = undefined;
if (textureData) {
if (referenceURL && textureData.images && textureData.images.source) {
const source = textureData.images.source;
for (let i = 0; i < source.length; i++) {
const newURL = createNewURL(source[i], referenceURL);
textureData.images.source[i] = newURL;
}
}
if (textureData.type === 'slides') {
newTexture = new TextureSlides();
}
if (newTexture) {
newTexture.groupName = groupName;
const isStale = createStaleCheck();
let myCallback = () => {
if (isStale()) {
newTexture.dispose();
return;
}
//Add zincObject after it has sort out all the required download
region.addZincObject(newTexture);
--this.toBeDownloaded;
if (finishCallback != undefined && typeof finishCallback == 'function')
finishCallback(newTexture);
};
++this.toBeDownloaded;
newTexture.load(textureData, myCallback, isInline);
newTexture.setAnatomicalId(anatomicalId);
newTexture.setRenderOrder(renderOrder);
}
}
};
/**
* Load a texture into this scene object.
*
* @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.loadTextureURL = (region, url, groupName, finishCallback, options) => {
const isInline = options && options.isInline ? options.isInline : false;
if (isInline) {
loadTexture(region, undefined, url, groupName, finishCallback, options);
} else {
const requestURL = url;
const isStale = createStaleCheck();
fetch(requestURL)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const referenceURL = response.url || new URL(requestURL).href;
return response.json().then((textureData) => ({ textureData, referenceURL }));
})
.then(({ textureData, referenceURL }) => {
if (isStale()) return;
loadTexture(region, referenceURL, textureData, groupName, finishCallback, options);
})
.catch((error) => {
console.error(`Fetch failed for texture URL: ${requestURL}`, error);
});
}
};
/**
* Add a user provided {THREE.Geometry} into the scene as zinc geometry.
*
* @param {Three.Geometry} geometry - The threejs geometry to be added as {@link Zinc.Geometry}.
* @param {THREE.Color} color - Colour to be assigned to this geometry, overrided if materialIn is provided.
* @param {Number} opacity - Opacity to be set for this geometry, overrided if materialIn is provided.
* @param {Boolean} localTimeEnabled - Set this to true if morph geometry is present, overrided if materialIn is provided.
* @param {Boolean} localMorphColour - Set this to true if morph colour is present, overrided if materialIn is provided.
* @param {Boolean} external - Set this to true if morph geometry is present, overrided if materialIn is provided.
* @param {Function} finishCallback - Callback once the geometry has been added succssfully.
* @param {THREE.Material} materialIn - Material to be set for this geometry if it is present.
*
* @returns {Zinc.Geometry}
*/
const addZincGeometry = (
region,
geometryIn,
colour,
opacity,
localTimeEnabled,
localMorphColour,
finishCallback,
materialIn,
groupName,
renderOrder,
anatomicalId,
) => {
let options = {};
options.colour = colour;
options.opacity = opacity;
options.localTimeEnabled = localTimeEnabled;
options.localMorphColour = localMorphColour;
const newGeometry = new Geometry();
newGeometry.createMesh(geometryIn, materialIn, options);
if (newGeometry.getMorph()) {
newGeometry.setName(groupName);
newGeometry.setRenderOrder(renderOrder);
newGeometry.setAnatomicalId(anatomicalId);
if (region) region.addZincObject(newGeometry);
newGeometry.setDuration(scene.getDuration());
if (finishCallback != undefined && typeof finishCallback == 'function')
finishCallback(newGeometry);
if (newGeometry.videoHandler) {
scene.setVideoHandler(newGeometry.videoHandler);
}
return newGeometry;
}
return undefined;
};
//Internal loader for a regular zinc geometry.
const meshloader = (
region,
colour,
opacity,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
options,
finishCallback,
) => {
const isStale = createStaleCheck();
return (geometry, materials) => {
if (isStale()) {
geometry.dispose();
return;
}
let material = undefined;
if (materials && materials[0]) {
material = materials[0];
}
const zincGeometry = addZincGeometry(
region,
geometry,
colour,
opacity,
localTimeEnabled,
localMorphColour,
undefined,
material,
groupName,
renderOrder,
anatomicalId,
);
if (options?.lod?.levels) {
for (const [key, value] of Object.entries(options.lod.levels)) {
zincGeometry.addLOD(primitivesLoader, key, value.URL, value.Index, options.lod.preload);
}
}
--this.toBeDownloaded;
geometry.dispose();
if (finishCallback != undefined && typeof finishCallback == 'function') {
finishCallback(zincGeometry);
}
};
};
//Internal loader for OBJ files, OBJLoader returns a group and each mesh
//in it is added as a separate zinc geometry.
const objloader = (
region,
colour,
opacity,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
options,
finishCallback,
) => {
const isStale = createStaleCheck();
const onMeshLoaded = meshloader(
region,
colour,
opacity,
localTimeEnabled,
localMorphColour,
groupName,
anatomicalId,
renderOrder,
options,
finishCallback,
);
return (group) => {
const meshes = [];
group.traverse((child) => {
if (child.isMesh && child.geometry) {
meshes.push(child);
}
if (child.material) {
const materials = Array.isArray(child.material) ? child.material : [child.material];
materials.forEach((material) => material.dispose());
}
});
if (isStale()) {
meshes.forEach((mesh) => mesh.geometry.dispose());
return;
}
if (meshes.length === 0) {
--this.toBeDownloaded;
return;
}
//meshloader decrements the counter once per mesh
this.toBeDownloaded += meshes.length - 1;
meshes.forEach((mesh) => onMeshLoaded(mesh.geometry));
};
};
//Turn ISO 8601 duration string into an array.
const parseDuration = (durationString) => {
const regex =
/P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
const [, years, months, weeks, days, hours, mins, secs] = durationString.match(regex);
return {
years: years,
months: months,
weeks: weeks,
days: days,
hours: hours,
mins: mins,
secs: secs,
};
};
//Load settings from metadata item.
this.loadSettings = (item) => {
if (item) {
//duration uses the ISO 8601 standard - PnYnMnDTnHnMnS
if (item.Duration) {
const duration = parseDuration(item.Duration);
scene.setDurationFromObject(duration);
}
if (item.OriginalDuration) {
const duration = parseDuration(item.OriginalDuration);
scene.setOriginalDurationFromObject(duration);
}
if (item.TimeStamps) {
for (const key in item.TimeStamps) {
const time = parseDuration(item.TimeStamps[key]);
scene.addMetadataTimeStamp(key, time);
}
}
}
};
//Function to process each of the graphical metadata item except for view and
//settings.
const readPrimitivesItem = (region, referenceURL, item, order, finishCallback) => {
if (item) {
let newURL = undefined;
let isInline = false;
if (item.URL) {
//Convert it into an array
newURL = item.URL;
if (referenceURL) newURL = createNewURL(newURL, referenceURL);
} else if (item.Inline) {
newURL = item.Inline.URL;
isInline = true;
}
const lod = {};
if (item.LOD && item.LOD.Levels) {
lod.preload = item.LOD.Preload ? true : false;
lod.levels = {};
for (const [key, value] of Object.entries(item.LOD.Levels)) {
lod.levels[key] = {};
lod.levels[key]['URL'] = createNewURL(value.URL, referenceURL);
lod.levels[key]['Index'] = value.Index;
}
}
let groupName = item.GroupName;
if (groupName === undefined || groupName === '') {
groupName = '_Unnamed';
}
let options = {
loaderOptions: {
index: item.Index,
},
isInline: isInline,
fileFormat: item.FileFormat,
anatomicalId: item.AnatomicalId,
compression: item.compression,
tubeLines: item.tubeLines,
lod: lod,
renderOrder: order,
};
switch (item.Type) {
case 'Surfaces':
loadSurfaceURL(
region,
newURL,
item.MorphVertices,
item.MorphColours,
groupName,
finishCallback,
options,
);
break;
case 'Glyph':
let newGeometryURL = undefined;
if (!isInline) {
newGeometryURL = item.GlyphGeometriesURL;
newGeometryURL = createNewURL(item.GlyphGeometriesURL, referenceURL);
} else {
newGeometryURL = item.Inline.GlyphGeometriesURL;
}
options.displayLabels = item.DisplayLabels;
if (options.displayLabels === undefined) {
options.displayLabels = true;
}
options.loaderOptions.isGlyphsets = true;
this.loadGlyphsetURL(region, newURL, newGeometryURL, groupName, finishCallback, options);
break;
case 'Points':
this.loadPointsetURL(
region,
newURL,
item.MorphVertices,
item.MorphColours,
groupName,
finishCallback,
options,
);
break;
case 'Lines':
this.loadLinesURL(
region,
newURL,
item.MorphVertices,
item.MorphColours,
groupName,
finishCallback,
options,
);
break;
case 'Texture':
this.loadTextureURL(region, newURL, groupName, finishCallback, options);
break;
default:
break;
}
}
};
//Function to read the view item first
const readViewAndSettingsItem = (referenceURL, item, finishCallback) => {
if (item) {
let newURL = undefined;
let isInline = false;
if (item.URL) {
newURL = item.URL;
if (referenceURL) newURL = createNewURL(item.URL, referenceURL);
} else if (item.Inline) {
newURL = item.Inline.URL;
isInline = true;
}
switch (item.Type) {
case 'View':
if (isInline) {
scene.setupMultipleViews('default', { default: newURL });
viewLoaded = true;
if (finishCallback != undefined && typeof finishCallback == 'function')
finishCallback();
} else this.loadViewURL(newURL, finishCallback);
break;
case 'Settings':
this.loadSettings(item);
break;
default:
break;
}
}
};
/**
* Load GLTF into this scene object.
*
* @param {String} url - URL to the GLTF file
* @param {Function} finishCallback - Callback function which will be called
* once the glyphset is succssfully load in.
*/
this.loadGLTF = (region, url, finishCallback, allCompletedCallback, options) => {
const loader = new GLTFToZincJSLoader();
loader.load(
scene,
region,
url,
finishCallback,
allCompletedCallback,
options,
createStaleCheck(),
);
};
let loadRegions = (currentRegion, referenceURL, regions, callback) => {
if (regions.Primitives) {
regions.Primitives.forEach((primitive) => {
let order = 1;
if (primitive.Order) order = primitive.Order;
readPrimitivesItem(currentRegion, referenceURL, primitive, order, callback);
});
}
if (regions.Transformation) {
currentRegion.setTransformation(regions.Transformation);
}
if (regions.Children) {
for (const [regionName, value] of Object.entries(regions.Children)) {
const childRegion = currentRegion.findOrCreateChildFromPath(regionName);
if (childRegion) {
loadRegions(childRegion, referenceURL, value, callback);
}
}
}
};
let supplementPrimitivesArray = (array, options) => {
let newArray = array;
const include = options?.enabled?.include;
const exclude = options?.enabled?.exclude;
const tubeLines = options?.tubeLines;
if (include?.length || exclude?.length) {
if (include) {
newArray = array.filter((item) => {
if (item.Type === 'View') {
return true;
}
for (let i = 0; i < include.length; i++) {
if (isRegionGroup(item.RegionPath, item.GroupName, include[i])) {
return true;
}
}
return false;
});
}
if (exclude) {
newArray = newArray.filter((item) => {
if (item.Type === 'View') {
return true;
}
for (let i = 0; i < exclude.length; i++) {
if (isRegionGroup(item.RegionPath, item.GroupName, exclude[i])) {
return false;
}
}
return true;
});
}
}
if (tubeLines) {
newArray.forEach((item) => {
if (item.Type === 'Lines') {
item.tubeLines = true;
}
});
}
return newArray;
};
let supplementMetadataInArray = (metadata, options) => {
if (Array.isArray(metadata)) {
return supplementPrimitivesArray(metadata, options);
}
return metadata;
};
let getNumberOfDownloadsInArray = (array, includeViews) => {
if (Array.isArray(array)) {
let count = 0;
for (let i = 0; i < array.length; i++) {
if (
array[i].Type &&
((includeViews && array[i].Type === 'View') ||
array[i].Type === 'Surfaces' ||
array[i].Type === 'Glyph' ||
array[i].Type === 'Points' ||
array[i].Type === 'Lines' ||
array[i].Type === 'Texture')
) {
count++;
}
}
return count;
}
return 0;
};
let getNumberOfObjectsInRegions = (regionJson) => {
let counts = regionJson.Primitives
? getNumberOfDownloadsInArray(regionJson.Primitives, false)
: 0;
if (regionJson.Children) {
Object.values(regionJson.Children).forEach((childRegion) => {
counts += getNumberOfObjectsInRegions(childRegion);
});
}
return counts;
};
let getNumberOfObjects = (metadata) => {
if (Array.isArray(metadata)) {
return getNumberOfDownloadsInArray(metadata, true);
} else if (typeof metadata === 'object' && metadata !== null) {
if (metadata.Version === '2.0') {
return getNumberOfObjectsInRegions(metadata.Regions);
}
}
};
let readVersionOneRegionPath = (region, referenceURL, item, order, callback) => {
let targetRegion = region;
if (item.RegionPath && item.RegionPath !== '') {
targetRegion = region.findOrCreateChildFromPath(item.RegionPath);
}
//Render order is set to i * 2 to account for front and back rendering
readPrimitivesItem(targetRegion, referenceURL, item, order * 2, callback);
};
let loadVersionOne = (
targetRegion,
metadata,
referenceURL,
finishCallback,
allCompletedCallback,
options,
) => {
const filteredMetada = supplementMetadataInArray(metadata, options);
let numberOfObjects = getNumberOfObjects(filteredMetada);
// view file does not receive callback
let callback = new metaFinishCallback(numberOfObjects, finishCallback, allCompletedCallback);
// Prioritise the view file and settings before loading anything else
for (let i = 0; i < filteredMetada.length; i++)
readViewAndSettingsItem(referenceURL, filteredMetada[i], callback);
for (let i = 0; i < filteredMetada.length; i++) {
readVersionOneRegionPath(targetRegion, referenceURL, filteredMetada[i], i, callback);
}
};
let loadVersionTwo = (
targetRegion,
metadata,
referenceURL,
finishCallback,
allCompletedCallback,
) => {
let numberOfObjects = getNumberOfObjects(metadata);
// view file does not receive callback
let callback = new metaFinishCallback(numberOfObjects, finishCallback, allCompletedCallback);
if (metadata.Settings) this.loadSettings(metadata.Settings);
if (metadata.Views) loadMultipleViews(referenceURL, metadata.Views, referenceURL);
if (metadata.Regions) loadRegions(targetRegion, referenceURL, metadata.Regions, callback);
};
/**
* 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 metadata file
* @param {Function} finishCallback - Callback function which will be called
* @param {options} Optional settings, it can be used to ignore some regions/groups
* in the metadata file. Only supports version 1 at this moment.
* for each glyphset and geometry that has been written in.
*/
this.loadMetadataURL = (targetRegion, url, finishCallback, allCompletedCallback, options) => {
const requestURL = url;
const isStale = createStaleCheck();
fetch(requestURL)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const referenceURL = response.url || new URL(requestURL).href;
return response.json().then((metadata) => ({ metadata, referenceURL }));
})
.then(({ metadata, referenceURL }) => {
if (isStale()) return;
scene.resetMetadata();
scene.resetDuration();
viewLoaded = false;
if (Array.isArray(metadata)) {
loadVersionOne(
targetRegion,
metadata,
referenceURL,
finishCallback,
allCompletedCallback,
options,
);
} else if (typeof metadata === 'object' && metadata !== null) {
if (metadata.Version === '2.0') {
loadVersionTwo(
targetRegion,
metadata,
referenceURL,
finishCallback,
allCompletedCallback,
);
}
}
})
.catch((error) => {
console.error(`Fetch failed for URL: ${requestURL}`, error);
});
};
};
export { SceneLoader };