src/plugins/LASLoaderPlugin/LASLoaderPlugin.js
import {utils} from "../../viewer/scene/utils.js";
import {SceneModel} from "../../viewer/scene/model/index.js";
import {Plugin} from "../../viewer/Plugin.js";
import {LASDefaultDataSource} from "./LASDefaultDataSource.js";
import {math} from "../../viewer/index.js";
import {parse} from '@loaders.gl/core';
import {LASLoader} from '@loaders.gl/las/dist/esm/las-loader.js';
import {loadLASHeader} from "./loadLASHeader";
const MAX_VERTICES = 500000; // TODO: Rough estimate
/**
* {@link Viewer} plugin that loads lidar point cloud geometry from LAS files.
*
* <a href="https://xeokit.github.io/xeokit-sdk/examples/index.html#loading_LASLoaderPlugin_Autzen"><img src="https://xeokit.github.io/xeokit-sdk/assets/images/autzen.png"></a>
*
* [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#loading_LASLoaderPlugin_Autzen)]
*
* ## Summary
*
* * Loads [LAS Formats](https://www.asprs.org/divisions-committees/lidar-division/laser-las-file-format-exchange-activities) up to v1.3 from both *.las* and *.laz* files. It does not support LAS v1.4.
* * Loads lidar point cloud positions, colors and intensities.
* * Supports 32 and 64-bit positions.
* * Supports 8 and 16-bit color depths.
* * Option to load every *n* points.
* * Does not (yet) load [point classifications](https://www.usna.edu/Users/oceano/pguth/md_help/html/las_format_classification_codes.htm).
*
* ## Performance
*
* If you need faster loading, consider pre-converting your LAS files to XKT format using [xeokit-convert](https://github.com/xeokit/xeokit-convert), then loading them
* with {@link XKTLoaderPlugin}.
*
* ## Scene and metadata representation
*
* When LASLoaderPlugin loads a LAS file, it creates two {@link Entity}s, a {@link MetaModel} and a {@link MetaObject}.
*
* The first Entity represents the file as a model within the Viewer's {@link Scene}. To indicate that it represents a model,
* this Entity will have {@link Entity#isModel} set ````true```` and will be registered by {@link Entity#id} in {@link Scene#models}.
*
* The second Entity represents the the point cloud itself, as an object within the Scene. To indicate that it
* represents an object, this Entity will have {@link Entity#isObject} set ````true```` and will be registered
* by {@link Entity#id} in {@link Scene#objects}.
*
* The MetaModel registers the LAS file as a model within the Viewer's {@link MetaScene}. The MetaModel will be registered
* by {@link MetaModel#id} in {@link MetaScene#metaModels} .
*
* Finally, the MetaObject registers the point cloud as an object within the {@link MetaScene}. The MetaObject will be registered
* by {@link MetaObject#id} in {@link MetaScene#metaObjects}.
*
* ## Usage
*
* In the example below we'll load the Autzen model from
* a [LAS file](/assets/models/las/). Once the model has
* loaded, we'll then find its {@link MetaModel}, and the {@link MetaObject} and {@link Entity} that represent its point cloud.
*
* * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#loading_LASLoaderPlugin_Autzen)]
*
* ````javascript
* import {Viewer, LASLoaderPlugin} from "xeokit-sdk.es.js";
*
* const viewer = new Viewer({
* canvasId: "myCanvas",
* transparent: true
* });
*
* viewer.camera.eye = [-2.56, 8.38, 8.27];
* viewer.camera.look = [13.44, 3.31, -14.83];
* viewer.camera.up = [0.10, 0.98, -0.14];
*
* const lasLoader = new LASLoaderPlugin(viewer, {
* colorDepth: 8, // Default
* fp64: false, // Default
* skip: 1 // Default
* });
*
* const modelEntity = lasLoader.load({
* id: "myModel",
* src: "../assets/models/las/autzen.laz"
* });
*
* modelEntity.on("loaded", () => {
*
* const metaModel = viewer.metaScene.metaModels[modelEntity.id];
* const pointCloudMetaObject = metaModel.rootMetaObject;
*
* const pointCloudEntity = viewer.scene.objects[pointCloudMetaObject.id];
*
* //...
* });
* ````
*
* ## Transforming
*
* We have the option to rotate, scale and translate each LAS model as we load it.
*
* In the example below, we'll scale our model to half its size, rotate it 90 degrees about its local X-axis, then
* position it at [1842022, 10, -5173301] within xeokit's world coordinate system.
*
* ````javascript
* const modelEntity = lasLoader.load({
* id: "myModel",
* src: "../assets/models/las/autzen.laz"
* rotation: [90,0,0],
* scale: [0.5, 0.5, 0.5],
* origin: [1842022, 10, -5173301]
* });
* ````
*
* ## Configuring a custom data source
*
* By default, LASLoaderPlugin will load LAS files over HTTP.
*
* In the example below, we'll customize the way LASLoaderPlugin loads the files by configuring it with our own data source
* object. For simplicity, our custom data source example also uses HTTP, using a couple of xeokit utility functions.
*
* ````javascript
* import {utils} from "xeokit-sdk.es.js";
*
* class MyDataSource {
*
* constructor() {
* }
*
* // Gets the contents of the given LAS file in an arraybuffer
* getLAS(src, ok, error) {
* utils.loadArraybuffer(src,
* (arraybuffer) => {
* ok(arraybuffer);
* },
* (errMsg) => {
* error(errMsg);
* });
* }
* }
*
* const lasLoader = new LASLoaderPlugin(viewer, {
* dataSource: new MyDataSource()
* });
*
* const modelEntity = lasLoader.load({
* id: "myModel",
* src: "../assets/models/las/autzen.laz"
* });
* ````
*
* ## Showing a LAS/LAZ model in TreeViewPlugin
*
* We can use the `load()` method's `elementId` parameter
* to make LASLoaderPlugin load the entire model into a single Entity that gets this ID.
*
* In conjunction with that parameter, we can then use the `load()` method's `metaModelJSON` parameter to create a MetaModel that
* contains a MetaObject that corresponds to that Entity.
*
* When we've done that, then xeokit's {@link TreeViewPlugin} is able to have a node that represents the model and controls
* the visibility of that Entity (ie. to control the visibility of the entire model).
*
* The snippet below shows how this is done.
*
* ````javascript
* import {Viewer, LASLoaderPlugin, NavCubePlugin, TreeViewPlugin} from "../../dist/xeokit-sdk.es.js";
*
* const viewer = new Viewer({
* canvasId: "myCanvas",
* transparent: true
* });
*
* new TreeViewPlugin(viewer, {
* containerElement: document.getElementById("treeViewContainer"),
* hierarchy: "containment"
* });
*
* const lasLoader = new LASLoaderPlugin(viewer);
*
* const sceneModel = lasLoader.load({ // Creates a SceneModel with ID "myScanModel"
* id: "myScanModel",
* src: "../../assets/models/las/Nalls_Pumpkin_Hill.laz",
* rotation: [-90, 0, 0],
*
* entityId: "3toKckUfH2jBmd$7uhJHa4", // Creates an Entity with this ID
*
* metaModelJSON: { // Creates a MetaModel with ID "myScanModel"
* "metaObjects": [
* {
* "id": "3toKckUfH2jBmd$7uhJHa6", // Creates this MetaObject with this ID
* "name": "My Project",
* "type": "Default",
* "parent": null
* },
* {
* "id": "3toKckUfH2jBmd$7uhJHa4", // Creates this MetaObject with this ID
* "name": "My Scan",
* "type": "Default",
* "parent": "3toKckUfH2jBmd$7uhJHa6"
* }
* ]
* }
* });
* ````
*
* @class LASLoaderPlugin
* @since 2.0.17
*/
class LASLoaderPlugin extends Plugin {
/**
* @constructor
*
* @param {Viewer} viewer The Viewer.
* @param {Object} cfg Plugin configuration.
* @param {String} [cfg.id="lasLoader"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
* @param {Object} [cfg.dataSource] A custom data source through which the LASLoaderPlugin can load model and metadata files. Defaults to an instance of {@link LASDefaultDataSource}, which loads over HTTP.
* @param {Number} [cfg.skip=1] Configures LASLoaderPlugin to load every **n** points.
* @param {Number} [cfg.fp64=false] Configures if LASLoaderPlugin assumes that LAS positions are stored in 64-bit floats instead of 32-bit.
* @param {Number} [cfg.colorDepth=8] Configures whether LASLoaderPlugin assumes that LAS colors are encoded using 8 or 16 bits. Accepted values are 8, 16 an "auto".
* @param {Boolean} [cfg.center=false] Whether to center the LAS points. Applied before "rotateX", "rotate" and "transform".
* @param {Boolean} [cfg.rotateX=false] Whether to rotate the LAS point positions 90 degrees. Applied after "center".
* @param {Number[]} [cfg.rotate=[0,0,0]] Rotations to immediately apply to the LAS points, given as Euler angles in degrees, for each of the X, Y and Z axis. Rotation is applied after "center" and "rotateX".
* @param {Number[]} [cfg.transform] 4x4 transform matrix to immediately apply to the LAS points. This is applied after "center", "rotateX" and "rotate". Typically used instead of "rotateX" and "rotate".
*/
constructor(viewer, cfg = {}) {
super("lasLoader", viewer, cfg);
this.dataSource = cfg.dataSource;
this.skip = cfg.skip;
this.fp64 = cfg.fp64;
this.colorDepth = cfg.colorDepth;
this.center = cfg.center;
this.rotate = cfg.rotate;
this.rotateX = cfg.rotateX;
this.transform = cfg.transform;
}
/**
* Gets the custom data source through which the LASLoaderPlugin can load LAS files.
*
* Default value is {@link LASDefaultDataSource}, which loads via HTTP.
*
* @type {Object}
*/
get dataSource() {
return this._dataSource;
}
/**
* Sets a custom data source through which the LASLoaderPlugin can load LAS files.
*
* Default value is {@link LASDefaultDataSource}, which loads via HTTP.
*
* @type {Object}
*/
set dataSource(value) {
this._dataSource = value || new LASDefaultDataSource();
}
/**
* When LASLoaderPlugin is configured to load every **n** points, returns the value of **n**.
*
* Default value is ````1````.
*
* @returns {Number} The **n**th point that LASLoaderPlugin will read.
*/
get skip() {
return this._skip;
}
/**
* Configures LASLoaderPlugin to load every **n** points.
*
* Default value is ````1````.
*
* @param {Number} value The **n**th point that LASLoaderPlugin will read.
*/
set skip(value) {
this._skip = value || 1;
}
/**
* Gets if LASLoaderPlugin assumes that LAS positions are stored in 64-bit floats instead of 32-bit.
*
* Default value is ````false````.
*
* @returns {Boolean} True if LASLoaderPlugin assumes that positions are stored in 64-bit floats instead of 32-bit.
*/
get fp64() {
return this._fp64;
}
/**
* Configures if LASLoaderPlugin assumes that LAS positions are stored in 64-bit floats instead of 32-bit.
*
* Default value is ````false````.
*
* @param {Boolean} value True if LASLoaderPlugin assumes that positions are stored in 64-bit floats instead of 32-bit.
*/
set fp64(value) {
this._fp64 = !!value;
}
/**
* Gets whether LASLoaderPlugin assumes that LAS colors are encoded using 8 or 16 bits.
*
* Default value is ````8````.
*
* Note: LAS specification recommends 16 bits.
*
* @returns {Number|String} Possible returned values are 8, 16 and "auto".
*/
get colorDepth() {
return this._colorDepth;
}
/**
* Configures whether LASLoaderPlugin assumes that LAS colors are encoded using 8 or 16 bits.
*
* Default value is ````8````.
*
* Note: LAS specification recommends 16 bits.
*
* @param {Number|String} value Valid values are 8, 16 and "auto".
*/
set colorDepth(value) {
this._colorDepth = value || "auto";
}
/**
* Gets if LASLoaderPlugin immediately centers LAS positions.
*
* If this is ````true```` then centering is the first thing that happens to LAS positions as they are loaded.
*
* Default value is ````false````.
*
* @returns {Boolean} True if LASLoaderPlugin immediately centers LAS positions.
*/
get center() {
return this._center;
}
/**
* Configures if LASLoaderPlugin immediately centers LAS positions.
*
* If this is ````true```` then centering is the first thing that happens to LAS positions as they are loaded.
*
* Default value is ````false````.
*
* @param {Boolean} value True if LASLoaderPlugin immediately centers LAS positions.
*/
set center(value) {
this._center = !!value;
}
/**
* Gets the current transformation to apply to LAS positions as they are loaded.
*
* If this is ````true````, then LAS positions will be transformed after they are centered and rotated.
*
* Default value is null.
*
* @returns {Number[]|null} A 16-element array containing a 4x4 transformation matrix.
*/
get transform() {
return this._transform;
}
/**
* Sets the current transformation to apply to LAS positions as they are loaded.
*
* If this is ````true````, then LAS positions will be transformed after they are centered and rotated.
*
* Default value is null.
*
* @param {Number[]|null} transform A 16-element array containing a 4x4 transformation matrix.
*/
set transform(transform) {
this._transform = transform;
}
/**
* Gets the current rotations to apply to LAS positions as they are loaded.
*
* Rotations are an array of three Euler angles in degrees, for each of the X, Y and Z axis, applied in that order.
*
* Default value is null.
*
* @returns {Number[]|null} If defined, an array of three Euler angles in degrees, for each of the X, Y and Z axis. Null if undefined.
*/
get rotate() {
return this._rotate;
}
/**
* Sets the current rotations to apply to LAS positions as they are loaded.
*
* Rotations are an array of three Euler angles in degrees, for each of the X, Y and Z axis, applied in that order.
*
* Default value is null.
*
* @param {Number[]|null} rotate Array of three Euler angles in degrees, for each of the X, Y and Z axis.
*/
set rotate(rotate) {
this._rotate = rotate;
}
/**
* Gets if LAS positions are rotated 90 degrees about X as they are loaded.
*
* Default value is ````false````.
*
* @returns {*}
*/
get rotateX() {
return this._rotateX;
}
/**
* Sets if LAS positions are rotated 90 degrees about X as they are loaded.
*
* Default value is ````false````.
*
* @param rotateX
*/
set rotateX(rotateX) {
this._rotateX = rotateX;
}
/**
* Loads an ````LAS```` model into this LASLoaderPlugin's {@link Viewer}.
*
* @param {*} params Loading parameters.
* @param {String} [params.id] ID to assign to the root {@link Entity#id}, unique among all components in the Viewer's {@link Scene}, generated automatically by default.
* @param {String} [params.src] Path to a LAS file, as an alternative to the ````las```` parameter.
* @param {ArrayBuffer} [params.las] The LAS file data, as an alternative to the ````src```` parameter.
* @param {Boolean} [params.loadMetadata=true] Whether to load metadata for the LAS model.
* @param {Number[]} [params.origin=[0,0,0]] The model's World-space double-precision 3D origin. Use this to position the model within xeokit's World coordinate system, using double-precision coordinates.
* @param {Number[]} [params.position=[0,0,0]] The model single-precision 3D position, relative to the ````origin```` parameter.
* @param {Number[]} [params.scale=[1,1,1]] The model's scale.
* @param {Number[]} [params.rotation=[0,0,0]] The model's orientation, given as Euler angles in degrees, for each of the X, Y and Z axis.
* @param {Number[]} [params.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] The model's world transform matrix. Overrides the position, scale and rotation parameters. Relative to ````origin````.
* @param {Object} [params.stats] Collects model statistics.
* @returns {Entity} Entity representing the model, which will have {@link Entity#isModel} set ````true```` and will be registered by {@link Entity#id} in {@link Scene#models}.
*/
load(params = {}) {
if (params.id && this.viewer.scene.components[params.id]) {
this.error("Component with this ID already exists in viewer: " + params.id + " - will autogenerate this ID");
delete params.id;
}
const sceneModel = new SceneModel(this.viewer.scene, utils.apply(params, {
maxGeometryBatchSize: MAX_VERTICES,
isModel: true
}));
if (!params.src && !params.las) {
this.error("load() param expected: src or las");
return sceneModel; // Return new empty model
}
const options = {
las: {
skip: this._skip,
fp64: this._fp64,
colorDepth: this._colorDepth
}
};
if (params.src) {
this._loadModel(params.src, params, options, sceneModel);
} else {
const spinner = this.viewer.scene.canvas.spinner;
spinner.processes++;
this._parseModel(params.las, params, options, sceneModel).then(() => {
spinner.processes--;
}, (errMsg) => {
spinner.processes--;
this.error(errMsg);
sceneModel.fire("error", errMsg);
});
}
return sceneModel;
}
_loadModel(src, params, options, sceneModel) {
const spinner = this.viewer.scene.canvas.spinner;
spinner.processes++;
this._dataSource.getLAS(params.src, (arrayBuffer) => {
this._parseModel(arrayBuffer, params, options, sceneModel).then(() => {
spinner.processes--;
}, (errMsg) => {
spinner.processes--;
this.error(errMsg);
sceneModel.fire("error", errMsg);
});
},
(errMsg) => {
spinner.processes--;
this.error(errMsg);
sceneModel.fire("error", errMsg);
});
}
_parseModel(arrayBuffer, params, options, sceneModel) {
const readPositions = (attributesPosition) => {
const positionsValue = attributesPosition.value;
if (this._center) {
const centerPos = math.vec3();
const numPoints = positionsValue.length;
for (let i = 0, len = positionsValue.length; i < len; i += 3) {
centerPos[0] += positionsValue[i + 0];
centerPos[1] += positionsValue[i + 1];
centerPos[2] += positionsValue[i + 2];
}
centerPos[0] /= numPoints;
centerPos[1] /= numPoints;
centerPos[2] /= numPoints;
for (let i = 0, len = positionsValue.length; i < len; i += 3) {
positionsValue[i + 0] -= centerPos[0];
positionsValue[i + 1] -= centerPos[1];
positionsValue[i + 2] -= centerPos[2];
}
}
if (this._rotateX) {
if (positionsValue) {
for (let i = 0, len = positionsValue.length; i < len; i += 3) {
const temp = positionsValue[i + 1];
positionsValue[i + 1] = positionsValue[i + 2];
positionsValue[i + 2] = temp;
}
}
}
if (this._rotate) {
const quaternion = math.identityQuaternion();
const mat = math.mat4();
math.eulerToQuaternion(this._rotate, "XYZ", quaternion);
math.quaternionToRotationMat4(quaternion, mat);
const pos = math.vec3();
for (let i = 0, len = positionsValue.length; i < len; i += 3) {
pos[0] = positionsValue[i + 0];
pos[1] = positionsValue[i + 1];
pos[2] = positionsValue[i + 2];
math.transformPoint3(mat, pos, pos);
positionsValue[i + 0] = pos[0];
positionsValue[i + 1] = pos[1];
positionsValue[i + 2] = pos[2];
}
}
if (this._transform) {
const mat = math.mat4(this._transform);
const pos = math.vec3();
for (let i = 0, len = positionsValue.length; i < len; i += 3) {
pos[0] = positionsValue[i + 0];
pos[1] = positionsValue[i + 1];
pos[2] = positionsValue[i + 2];
math.transformPoint3(mat, pos, pos);
positionsValue[i + 0] = pos[0];
positionsValue[i + 1] = pos[1];
positionsValue[i + 2] = pos[2];
}
}
return positionsValue;
}
function readColorsAndIntensities(attributesColor, attributesIntensity) {
const colors = attributesColor.value;
const colorSize = attributesColor.size;
const intensities = attributesIntensity.value;
const colorsCompressedSize = intensities.length * 4;
const colorsCompressed = new Uint8Array(colorsCompressedSize);
for (let i = 0, j = 0, k = 0, len = intensities.length; i < len; i++, k += colorSize, j += 4) {
colorsCompressed[j + 0] = colors[k + 0];
colorsCompressed[j + 1] = colors[k + 1];
colorsCompressed[j + 2] = colors[k + 2];
colorsCompressed[j + 3] = Math.round((intensities[i] / 65536) * 255);
}
return colorsCompressed;
}
function readIntensities(attributesIntensity) {
const intensities = attributesIntensity.value;
const colorsCompressedSize = intensities.length * 4;
const colorsCompressed = new Uint8Array(colorsCompressedSize);
for (let i = 0, j = 0, k = 0, len = intensities.length; i < len; i++, k += 3, j += 4) {
colorsCompressed[j + 0] = 0;
colorsCompressed[j + 1] = 0;
colorsCompressed[j + 2] = 0;
colorsCompressed[j + 3] = Math.round((intensities[i] / 65536) * 255);
}
return colorsCompressed;
}
return new Promise((resolve, reject) => {
if (sceneModel.destroyed) {
reject();
return;
}
const stats = params.stats || {};
stats.sourceFormat = "LAS";
stats.schemaVersion = "";
stats.title = "";
stats.author = "";
stats.created = "";
stats.numMetaObjects = 0;
stats.numPropertySets = 0;
stats.numObjects = 0;
stats.numGeometries = 0;
stats.numTriangles = 0;
stats.numVertices = 0;
try {
const lasHeader = loadLASHeader(arrayBuffer);
parse(arrayBuffer, LASLoader, options).then((parsedData) => {
const attributes = parsedData.attributes;
const loaderData = parsedData.loaderData;
const pointsFormatId = loaderData.pointsFormatId !== undefined ? loaderData.pointsFormatId : -1;
if (!attributes.POSITION) {
sceneModel.finalize();
reject("No positions found in file");
return;
}
let positionsValue
let colorsCompressed;
switch (pointsFormatId) {
case 0:
positionsValue = readPositions(attributes.POSITION);
colorsCompressed = readIntensities(attributes.intensity);
break;
case 1:
if (!attributes.intensity) {
sceneModel.finalize();
reject("No positions found in file");
return;
}
positionsValue = readPositions(attributes.POSITION);
colorsCompressed = readIntensities(attributes.intensity);
break;
case 2:
if (!attributes.intensity) {
sceneModel.finalize();
reject("No positions found in file");
return;
}
positionsValue = readPositions(attributes.POSITION);
colorsCompressed = readColorsAndIntensities(attributes.COLOR_0, attributes.intensity);
break;
case 3:
if (!attributes.intensity) {
sceneModel.finalize();
reject("No positions found in file");
return;
}
positionsValue = readPositions(attributes.POSITION);
colorsCompressed = readColorsAndIntensities(attributes.COLOR_0, attributes.intensity);
break;
}
const pointsChunks = chunkArray(positionsValue, MAX_VERTICES * 3);
const colorsChunks = chunkArray(colorsCompressed, MAX_VERTICES * 4);
const meshIds = [];
for (let i = 0, len = pointsChunks.length; i < len; i++) {
const meshId = `pointsMesh${i}`;
meshIds.push(meshId);
sceneModel.createMesh({
id: meshId,
primitive: "points",
positions: pointsChunks[i],
colorsCompressed: (i < colorsChunks.length) ? colorsChunks[i] : null
});
}
/*
const pointsChunks = chunkArray(positionsValue, MAX_VERTICES * 3);
const colorsChunks = chunkArray(colorsCompressed, MAX_VERTICES * 4);
const meshIds = [];
for (let i = 0, len = pointsChunks.length; i < len; i++) {
const geometryId = `geometryMesh${i}`;
const meshId = `pointsMesh${i}`;
meshIds.push(meshId);
sceneModel.createGeometry({
id: geometryId,
primitive: "points",
positions: pointsChunks[i],
colorsCompressed: (i < colorsChunks.length) ? colorsChunks[i] : null
});
sceneModel.createMesh({
id: meshId,
geometryId
});
}
*/
const pointsObjectId = params.entityId || math.createUUID();
sceneModel.createEntity({
id: pointsObjectId,
meshIds,
isObject: true
});
sceneModel.finalize();
if (params.metaModelJSON) {
const metaModelId = sceneModel.id;
this.viewer.metaScene.createMetaModel(metaModelId, params.metaModelJSON, options);
} else if (params.loadMetadata !== false) {
const rootMetaObjectId = math.createUUID();
const metadata = {
projectId: "",
author: "",
createdAt: "",
schema: "",
creatingApplication: "",
metaObjects: [
{
id: rootMetaObjectId,
name: "Model",
type: "Model"
},
{
id: pointsObjectId,
name: "PointCloud (LAS)",
type: "PointCloud",
parent: rootMetaObjectId,
attributes: lasHeader || {}
}
],
propertySets: []
};
const metaModelId = sceneModel.id;
this.viewer.metaScene.createMetaModel(metaModelId, metadata, options);
}
sceneModel.scene.once("tick", () => {
if (sceneModel.destroyed) {
return;
}
sceneModel.scene.fire("modelLoaded", sceneModel.id); // FIXME: Assumes listeners know order of these two events
sceneModel.fire("loaded", true, false); // Don't forget the event, for late subscribers
});
resolve();
});
} catch (e) {
sceneModel.finalize();
reject(e);
}
});
}
}
function chunkArray(array, chunkSize) {
if (chunkSize >= array.length) {
return [array];
}
let result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
export {LASLoaderPlugin};