/**
An **STLModel** is a {{#crossLink "Model"}}{{/crossLink}} that's loaded from an <a href="https://en.wikipedia.org/wiki/STL_(file_format)">STL</a> file.
<a href="../../examples/#importing_stl_shapes"><img src="../../../assets/images/screenshots/STLModel.png"></img></a>
## Overview
* An <a href="https://en.wikipedia.org/wiki/STL_(file_format)">STL</a> (“StereoLithography”) file is a triangular representation of a 3-dimensional surface geometry. The surface is
tessellated logically into a series of triangles. Each facet is described by a perpendicular
direction and three points representing the vertices (corners) of the triangle.
* An STLModel is a container of {{#crossLink "Component"}}Components{{/crossLink}} that loads itself from an STL file.
* It begins loading as soon as you set its {{#crossLink "STLModel/src:property"}}{{/crossLink}}
property to the location of a valid STL file.
* You can set {{#crossLink "STLModel/src:property"}}{{/crossLink}} to a new file path at any time, which causes
the STLModel to clear itself and load components from the new file.
* For binary STL, has the option to create a separate {{#crossLink "Mesh"}}{{/crossLink}} for each group of faces
that share the same vertex colors. This allows us to treat STL models as parts assemblies.
* Can be configured to automatically smooth STL models by converting their face-oriented normals to vertex-oriented.
STLModel inherits these capabilities from its {{#crossLink "Group"}}{{/crossLink}} base class:
* Allows you to access and manipulate the {{#crossLink "Meshes"}}{{/crossLink}} within it.
* Can be transformed as a unit within World-space.
* Can be a child within a parent {{#crossLink "Group"}}{{/crossLink}}.
* Provides its World-space axis-aligned and object-aligned boundaries.
## Examples
* [Simple shapes with smoothing](../../examples/#importing_stl_shapes)
* [F1 concept car with smoothing](../../examples/#importing_stl_F1Concept)
* [Models within an object hierarchy](../../examples/#objects_hierarchy_models)
## Usage
* [Loading STL](#loading-stl)
* [Parsing STL](#parsing-stl)
* [Options](#options)
* [Smoothing Normals](#smoothing-normals)
* [Finding loaded Meshes](#finding-loaded-meshes)
* [Transforming an STLModel](#transforming-a-gltfmodel)
* [Getting the World-space boundary of an STLModel](#getting-the-world-space-boundary-of-an-stlmodel)
* [Clearing an STLModel](#clearing-a-gltfmodel)
* [Destroying an STLModel](#destroying-a-gltfmodel)
### Loading STL
Load an STL file by creating an STLModel:
````javascript
var model = new xeogl.STLModel({
id: "myModel",
src: "models/stl/F1Concept.stl",
// Some example loading options (see "Options" below)
smoothNormals: true,
smoothNormalsAngleThreshold: 45
});
````
An STLModel prefixes its own ID to those of its components. The ID is optional, but in this example we're providing our own custom ID.
The STLModel begins loading the STL file immediately.
To bind a callback to be notified when the file has loaded (which fires immediately if already loaded):
````javascript
model.on("loaded", function() {
// STLModel has loaded!
});
````
You can also bind a callback to fire if loading fails:
````javascript
model.on("error", function(msg) {
// Error occurred
});
````
To switch to a different STL file, you can dynamically update {{#crossLink "STLModel/src:property"}}{{/crossLink}}:
````javascript
model.src = "models/stl/F1Concept.stl"
````
That will apply whatever options were specified to the constructor.
### Parsing STL
If we have STL data in memory, then we can parse it directly into an existing STLModel instance using the
static {{#crossLink "STLModel/parse:method"}}{{/crossLink}} method:
````javascript
xeogl.STLModel.parse(model, stlData, {
// Some example parsing options (see "Options" below)
smoothNormals: true,
smoothNormalsAngleThreshold: 45,
combineGeometry: true,
quantizeGeometry: true
});
````
That's asynchronous because STL is self-contained and does not need to load any external assets.
### Options
The following options may be specified when loading or parsing STL:
| Option | Type | Range | Default Value | Description |
|:--------:|:----:|:-----:|:-------------:|:-----:|:-----------:|
| quantizeGeometry | Boolean | | true | When true, quantizes geometry to reduce memory and GPU bus usage (see {{#crossLink "Geometry"}}{{/crossLink}}). |
| combineGeometry | Boolean | | true | When true, internally combines geometry vertex buffers to improve rendering performance (see {{#crossLink "Geometry"}}{{/crossLink}}). |
| smoothNormals | Boolean | | false | When true, automatically converts face-oriented normals to vertex normals for a smooth appearance. See [Smoothing Normals](#smoothing-normals). |
| smoothNormalsAngleThreshold | Number (degrees) | [0..180] | 20 | See [Smoothing Normals](#smoothing-normals). |
| backfaces | Boolean | | true | When true, allows visible backfaces, wherever specified in the STL. When false, ignores backfaces. |
| ghosted | Boolean | | false | When true, ghosts all the model's Meshes (see {{#crossLink "Mesh"}}{{/crossLink}} and {{#crossLink "EmphasisMaterial"}}{{/crossLink}}). |
| outlined | Boolean | | false | When true, outlines all the model's Meshes (see {{#crossLink "Mesh"}}{{/crossLink}} and {{#crossLink "OutlineMaterial"}}{{/crossLink}}). |
| highlighted | Boolean | | false | When true, highlights all the model's Meshes (see {{#crossLink "Mesh"}}{{/crossLink}} and {{#crossLink "EmphasisMaterial"}}{{/crossLink}}). |
| selected | Boolean | | false | When true, renders all the model's Meshes as selected (see {{#crossLink "Mesh"}}{{/crossLink}} and {{#crossLink "EmphasisMaterial"}}{{/crossLink}}). |
| edges | Boolean | | false | When true, emphasizes the edges on all the model's Meshes (see {{#crossLink "Mesh"}}{{/crossLink}} and {{#crossLink "EdgeMaterial"}}{{/crossLink}}). |
| edgeThreshold | Number | [0..180] | 2 | When ghosting, this is the threshold angle between normals of adjacent triangles, below which their shared wireframe edge is not drawn. |
| splitMeshes | Boolean | | true | When true, creates a separate {{#crossLink "Mesh"}}{{/crossLink}} for each group of faces that share the same vertex colors. Only works with binary STL.| |
### Smoothing Normals
As mentioned above, providing a ````smoothNormals```` flag to the constructor gives our STLModel a smooth appearance. Triangles in STL
are disjoint, where each triangle has its own separate vertex positions, normals and (optionally) colors. This means that you can
have gaps between triangles. Normals for each triangle are perpendicular to the triangle's surface, which gives the model a faceted appearance by default.
The ```smoothNormals``` flag causes the STLModel to recalculate its normals, so that each normal's direction is the average
of the orientations of the triangles adjacent to its vertex. When smoothing, each vertex normal is set to the average of the
orientations of all other triangles that have a vertex at the same position, excluding those triangles whose direction deviates from
the direction of the vertice's triangle by a threshold given in ````smoothNormalsAngleThreshold````. This makes
smoothing robust for hard edges, which you can see on the cylindrical objects in one of the examples:
<a href="../../examples/#importing_stl_shapes"><img src="../../../assets/images/screenshots/STLModelHardEdges.png"></img></a>
Note how the rim is smooth, yet the there is still a sharp edge adjacent to the flat portions.
### Finding STLModels in Scenes
Our STLModel will now be registered by ID on its Scene, so we can now find it like this:
````javascript
var scene = xeogl.getDefaultScene();
model = scene.models["myModel"];
````
That's assuming that we've created the STLModel in the default xeogl Scene, which we're doing in these examples.
We can also get all the STLModels in a Scene, using the Scene's {{#crossLink "Scene/types:property"}}{{/crossLink}} map:
````javascript
var scene = xeogl.getDefaultScene();
var stlModels = scene.types["xeogl.STLModel"];
model = stlModels["myModel"];
````
### Finding loaded Meshes
Once the STLModel has loaded, its {{#crossLink "Scene"}}{{/crossLink}} will contain various components that represent the elements of the STL file.
We'll now access some of those components by ID, to query and update them programmatically.
Let's highlight an {{#crossLink "Object"}}{{/crossLink}} in our STLModel:
````javascript
scene.omponents["myModel#1"].highlighted = true;
````
An STLModel also has an ID map of the components within it. Let's highlight another {{#crossLink "Object"}}{{/crossLink}} via the STLModel:
````javascript
model.components["myModel#2"].highlighted = true;
````
An STLModel also has a map containing just the {{#crossLink "Objects"}}Objects{{/crossLink}}:
````javascript
model.objects["myModel#3"].highlighted = true;
````
TODO: ID format description
### Transforming an STLModel
An STLModel lets us transform its Meshes as a group:
```` Javascript
var model = new xeogl.STLModel({
src: "models/stl/F1Concept.stl",
position: [-35, 0, 0],
rotation: [0, 45, 0],
scale: [0.5, 0.5, 0.5]
});
model.position = [-20, 0, 0];
````
### Getting the World-space boundary of an STLModel
Get the World-space axis-aligned boundary like this:
```` Javascript
model.on("boundary", function() {
var aabb = model.aabb; // [xmin, ymin,zmin,xmax,ymax, zmax]
//...
});
````
We can also subscribe to changes to that boundary, which will happen whenever
* the STLModel's {{#crossLink "Transform"}}{{/crossLink}} is updated,
* components are added or removed, or
* the STLModel is reloaded from a different source,
* the {{#crossLink "Geometry"}}Geometries{{/crossLink}} or {{#crossLink "Transform"}}Transforms{{/crossLink}} of its {{#crossLink "Mesh"}}Meshes{{/crossLink}} are updated.
````javascript
model.on("boundary", function() {
var aabb = model.aabb; // [xmin, ymin,zmin,xmax,ymax, zmax]
});
````
### Clearing an STLModel
```` Javascript
model.clear();
````
### Destroying an STLModel
```` Javascript
model.destroy();
````
@class STLModel
@module xeogl
@submodule models
@constructor
@param [scene] {Scene} Parent {{#crossLink "Scene"}}Scene{{/crossLink}} - creates this STLModel in the default
{{#crossLink "Scene"}}Scene{{/crossLink}} when omitted.
@param [cfg] {*} Configs
@param [cfg.id] {String} Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}},
generated automatically when omitted.
@param [cfg.entityType] {String} Optional entity classification when using within a semantic data model. See the {{#crossLink "Object"}}{{/crossLink}} documentation for usage.
@param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this STLModel.
@param [cfg.src] {String} Path to an STL file. You can set this to a new file path at any time, which will cause the
@param [cfg.quantizeGeometry=true] {Boolean} When true, quantizes geometry to reduce memory and GPU bus usage.
@param [cfg.combineGeometry=true] {Boolean} When true, combines geometry vertex buffers to improve rendering performance.
@param [cfg.smoothNormals=false] {Boolean} When true, automatically converts face-oriented normals to vertex normals for a smooth appearance - see <a href="#smoothing-normals">Smoothing Normals</a>.
@param [cfg.smoothNormalsAngleThreshold=20] {Number} See <a href="#smoothing-normals">Smoothing Normals</a>.
@param [cfg.backfaces=false] {Boolean} When true, allows visible backfaces, wherever specified in the STL. When false, ignores backfaces.
@param [cfg.ghosted=false] {Boolean} When true, sets all the Model's Meshes initially ghosted.
@param [cfg.highlighted=false] {Boolean} When true, sets all the Model's Meshes initially highlighted.
@param [cfg.outline=false] {Boolean} When true, sets all the Model's Meshes initially outlined.
@param [cfg.edgeThreshold=2] {Number} When ghosting, this is the threshold angle between normals of adjacent triangles, below which their shared wireframe edge is not drawn.
@param [cfg.transform] {Number|String|Transform} A Local-to-World-space (modelling) {{#crossLink "Transform"}}{{/crossLink}} to attach to this STLModel.
Must be within the same {{#crossLink "Scene"}}{{/crossLink}} as this STLModel. Internally, the given
{{#crossLink "Transform"}}{{/crossLink}} will be inserted above each top-most {{#crossLink "Transform"}}Transform{{/crossLink}}
that the STLModel attaches to its {{#crossLink "Mesh"}}Meshes{{/crossLink}}.
@param [cfg.splitMeshes=true] {Boolean} When true, creates a separate {{#crossLink "Mesh"}}{{/crossLink}} for each group of faces that share the same vertex colors. Only works with binary STL.|
@param [cfg.position=[0,0,0]] {Float32Array} The STLModel's local 3D position.
@param [cfg.scale=[1,1,1]] {Float32Array} The STLModel's local scale.
@param [cfg.rotation=[0,0,0]] {Float32Array} The STLModel's local rotation, as Euler angles given in degrees.
@param [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] {Float32Array} The STLModel's local transform matrix. Overrides the position, scale and rotation parameters.
@extends Model
*/
{
xeogl.STLModel = class xeoglSTLModel extends xeogl.Model {
init(cfg) {
super.init(cfg);
this._src = null;
this._options = {
combineGeometry: cfg.combineGeometry !== false,
quantizeGeometry: cfg.quantizeGeometry !== false,
edgeThreshold: cfg.edgeThreshold,
splitMeshes: cfg.splitMeshes,
smoothNormals: cfg.smoothNormals,
smoothNormalsAngleThreshold: cfg.smoothNormalsAngleThreshold
};
this.src = cfg.src;
}
/**
Path to an STL file.
You can set this to a new file path at any time (except while loading), which will cause the STLModel to load components from
the new file (after first destroying any components loaded from a previous file path).
Fires a {{#crossLink "STLModel/loaded:event"}}{{/crossLink}} event when the STL has loaded.
@property src
@type String
*/
set src(value) {
if (!value) {
return;
}
if (!xeogl._isString(value)) {
this.error("Value for 'src' should be a string");
return;
}
if (value === this._src) { // Already loaded this STLModel
/**
Fired whenever this STLModel has finished loading components from the STL file
specified by {{#crossLink "STLModel/src:property"}}{{/crossLink}}.
@event loaded
*/
this.fire("loaded", true, true);
return;
}
this.clear();
this._src = value;
xeogl.STLModel.load(this, this._src, this._options);
}
get source() {
return this._src;
}
destroy() {
this.destroyAll();
super.destroy();
}
/**
* Loads STL from a URL into a {{#crossLink "Model"}}{{/crossLink}}.
*
* @method load
* @static
* @param {Model} model Model to load into.
* @param {String} src Path to STL file.
* @param {Object} options Loading options.
* @param {Function} [ok] Completion callback.
* @param {Function} [error] Error callback.
*/
static load(model, src, options, ok, error) {
var spinner = model.scene.canvas.spinner;
spinner.processes++;
load(model, src, options, function () {
spinner.processes--;
xeogl.scheduleTask(function () {
model.fire("loaded", true, true);
});
if (ok) {
ok();
}
},
function (msg) {
spinner.processes--;
model.error(msg);
if (error) {
error(msg);
}
/**
Fired whenever this STLModel fails to load the STL file
specified by {{#crossLink "STLModel/src:property"}}{{/crossLink}}.
@event error
@param msg {String} Description of the error
*/
model.fire("error", msg);
});
}
/**
* Parses STL into a {{#crossLink "Model"}}{{/crossLink}}.
*
* @method parse
* @static
* @param {Model} model Model to parse into.
* @param {ArrayBuffer} data The STL data.
* @param {Object} [options] Parsing options
* @param {String} [options.basePath] Base path path to find external resources on, if any.
* @param {String} [options.loadBuffer] Callback to load buffer files.
*/
static parse(model, data, options) {
options = options || {};
var spinner = model.scene.canvas.spinner;
spinner.processes++;
parse(data, "", options, model, function () {
spinner.processes--;
model.fire("loaded", true, true);
},
function (msg) {
spinner.processes--;
model.error(msg);
model.fire("error", msg);
});
}
};
var load = (function () {
function loadData(src, ok, error) {
var request = new XMLHttpRequest();
request.overrideMimeType("application/json");
request.open('GET', src, true);
request.responseType = 'arraybuffer';
request.onreadystatechange = function () {
if (request.readyState == 4 && request.status == "200") {
ok(request.response, this);
}
};
request.send(null);
}
return function (model, src, options, ok, error) {
loadData(src, function (data) { // OK
parse(data, model, options);
ok();
},
error);
};
})();
function parse(data, model, options) {
var entityCount = 0;
function isBinary(data) {
var reader = new DataView(data);
var numFaces = reader.getUint32(80, true);
var faceSize = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 );
var numExpectedBytes = 80 + ( 32 / 8 ) + ( numFaces * faceSize );
if (numExpectedBytes === reader.byteLength) {
return true;
}
var solid = [115, 111, 108, 105, 100];
for (var i = 0; i < 5; i++) {
if (solid[i] != reader.getUint8(i, false)) {
return true;
}
}
return false;
}
function parseBinary(data, model, options) {
var autoVertexNormals = options.autoVertexNormals;
var reader = new DataView(data);
var faces = reader.getUint32(80, true);
var r;
var g;
var b;
var hasColors = false;
var colors;
var defaultR;
var defaultG;
var defaultB;
var lastR = null;
var lastG = null;
var lastB = null;
var newMesh = false;
var alpha;
var indices;
var geometry;
var mesh;
for (var index = 0; index < 80 - 10; index++) {
if (( reader.getUint32(index, false) == 0x434F4C4F /*COLO*/ ) &&
( reader.getUint8(index + 4) == 0x52 /*'R'*/ ) &&
( reader.getUint8(index + 5) == 0x3D /*'='*/ )) {
hasColors = true;
colors = [];
defaultR = reader.getUint8(index + 6) / 255;
defaultG = reader.getUint8(index + 7) / 255;
defaultB = reader.getUint8(index + 8) / 255;
alpha = reader.getUint8(index + 9) / 255;
}
}
var material = new xeogl.MetallicMaterial(model, { // Share material with all meshes
roughness: 0.5
});
// var material = new xeogl.PhongMaterial(model, { // Share material with all meshes
// diffuse: [0.4, 0.4, 0.4],
// reflectivity: 1,
// specular: [0.5, 0.5, 1.0]
// });
model._addComponent(material);
var dataOffset = 84;
var faceLength = 12 * 4 + 2;
var positions = [];
var normals = [];
var splitMeshes = options.splitMeshes;
for (var face = 0; face < faces; face++) {
var start = dataOffset + face * faceLength;
var normalX = reader.getFloat32(start, true);
var normalY = reader.getFloat32(start + 4, true);
var normalZ = reader.getFloat32(start + 8, true);
if (hasColors) {
var packedColor = reader.getUint16(start + 48, true);
if (( packedColor & 0x8000 ) === 0) {
r = ( packedColor & 0x1F ) / 31;
g = ( ( packedColor >> 5 ) & 0x1F ) / 31;
b = ( ( packedColor >> 10 ) & 0x1F ) / 31;
} else {
r = defaultR;
g = defaultG;
b = defaultB;
}
if (splitMeshes && r !== lastR || g !== lastG || b !== lastB) {
if (lastR !== null) {
newMesh = true;
}
lastR = r;
lastG = g;
lastB = b;
}
}
for (var i = 1; i <= 3; i++) {
var vertexstart = start + i * 12;
positions.push(reader.getFloat32(vertexstart, true));
positions.push(reader.getFloat32(vertexstart + 4, true));
positions.push(reader.getFloat32(vertexstart + 8, true));
if (!autoVertexNormals) {
normals.push(normalX, normalY, normalZ);
}
if (hasColors) {
colors.push(r, g, b, 1); // TODO: handle alpha
}
}
if (splitMeshes && newMesh) {
addMesh(model, positions, normals, colors, material, options);
positions = [];
normals = [];
colors = colors ? [] : null;
newMesh = false;
}
}
if (positions.length > 0) {
addMesh(model, positions, normals, colors, material, options);
}
}
function parseASCII(data, model, options) {
var faceRegex = /facet([\s\S]*?)endfacet/g;
var faceCounter = 0;
var floatRegex = /[\s]+([+-]?(?:\d+.\d+|\d+.|\d+|.\d+)(?:[eE][+-]?\d+)?)/.source;
var vertexRegex = new RegExp('vertex' + floatRegex + floatRegex + floatRegex, 'g');
var normalRegex = new RegExp('normal' + floatRegex + floatRegex + floatRegex, 'g');
var positions = [];
var normals = [];
var colors = null;
var normalx;
var normaly;
var normalz;
var result;
var verticesPerFace;
var normalsPerFace;
var text;
while (( result = faceRegex.exec(data) ) !== null) {
verticesPerFace = 0;
normalsPerFace = 0;
text = result[0];
while (( result = normalRegex.exec(text) ) !== null) {
normalx = parseFloat(result[1]);
normaly = parseFloat(result[2]);
normalz = parseFloat(result[3]);
normalsPerFace++;
}
while (( result = vertexRegex.exec(text) ) !== null) {
positions.push(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]));
normals.push(normalx, normaly, normalz);
verticesPerFace++;
}
if (normalsPerFace !== 1) {
model.error("Error in normal of face " + faceCounter);
}
if (verticesPerFace !== 3) {
model.error("Error in positions of face " + faceCounter);
}
faceCounter++;
}
var material = new xeogl.MetallicMaterial(model, {
roughness: 0.5
});
// var material = new xeogl.PhongMaterial(model, {
// diffuse: [0.4, 0.4, 0.4],
// reflectivity: 1,
// specular: [0.5, 0.5, 1.0]
// });
model._addComponent(material);
addMesh(model, positions, normals, colors, material, options);
}
function addMesh(model, positions, normals, colors, material, options) {
var indices = new Int32Array(positions.length / 3);
for (var ni = 0, len = indices.length; ni < len; ni++) {
indices[ni] = ni;
}
normals = normals && normals.length > 0 ? normals : null;
colors = colors && colors.length > 0 ? colors : null;
if (options.smoothNormals) {
xeogl.math.faceToVertexNormals(positions, normals, options);
}
var geometry = new xeogl.Geometry(model, {
primitive: "triangles",
positions: positions,
normals: normals,
// autoVertexNormals: !normals,
colors: colors,
indices: indices
});
var mesh = new xeogl.Mesh(model, {
id: model.id + "#" + entityCount++,
geometry: geometry,
material: material
});
model._addComponent(geometry);
model.addChild(mesh);
}
function ensureString(buffer) {
if (typeof buffer !== 'string') {
return decodeText(new Uint8Array(buffer));
}
return buffer;
}
function ensureBinary(buffer) {
if (typeof buffer === 'string') {
var arrayBuffer = new Uint8Array(buffer.length);
for (var i = 0; i < buffer.length; i++) {
arrayBuffer[i] = buffer.charCodeAt(i) & 0xff; // implicitly assumes little-endian
}
return arrayBuffer.buffer || arrayBuffer;
} else {
return buffer;
}
}
function decodeText(array) {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder().decode(array);
}
var s = '';
for (var i = 0, il = array.length; i < il; i++) {
s += String.fromCharCode(array[i]); // Implicitly assumes little-endian.
}
return decodeURIComponent(escape(s));
}
var binData = ensureBinary(data);
return isBinary(binData) ? parseBinary(binData, model, options) : parseASCII(ensureString(data), model, options);
}
}