Reference Source

src/plugins/STLLoaderPlugin/STLSceneGraphLoader.js

import {Mesh} from "../../viewer/scene/mesh/Mesh.js";
import {ReadableGeometry} from "../../viewer/scene/geometry/ReadableGeometry.js";
import {MetallicMaterial} from "../../viewer/scene/materials/MetallicMaterial.js";
import {math} from "../../viewer/scene/math/math.js";
import {worldToRTCPositions} from "../../viewer/scene/math/rtcCoords.js";
import {core} from "../../viewer/scene/core.js";

const tempVec3a = math.vec3();

/**
 * @private
 */
class STLSceneGraphLoader {

    load(plugin, modelNode, src, options, ok, error) {

        options = options || {};

        const spinner = plugin.viewer.scene.canvas.spinner;
        spinner.processes++;

        plugin.dataSource.getSTL(src, function (data) { // OK
                parse(plugin, modelNode, data, options);
                try {
                    const binData = ensureBinary(data);
                    if (isBinary(binData)) {
                        parseBinary(plugin, binData, modelNode, options);
                    } else {
                        parseASCII(plugin, ensureString(data), modelNode, options);
                    }
                    spinner.processes--;
                    core.scheduleTask(function () {
                        modelNode.fire("loaded", true, false);
                    });
                    if (ok) {
                        ok();
                    }
                } catch (e) {
                    spinner.processes--;
                    plugin.error(e);
                    if (error) {
                        error(e);
                    }
                    modelNode.fire("error", e);
                }
            },
            function (msg) {
                spinner.processes--;
                plugin.error(msg);
                if (error) {
                    error(msg);
                }
                modelNode.fire("error", msg);
            });
    }

    parse(plugin, modelNode, data, options) {
        const spinner = plugin.viewer.scene.canvas.spinner;
        spinner.processes++;
        try {
            const binData = ensureBinary(data);
            if (isBinary(binData)) {
                parseBinary(plugin, binData, modelNode, options);
            } else {
                parseASCII(plugin, ensureString(data), modelNode, options);
            }
            spinner.processes--;
            core.scheduleTask(function () {
                modelNode.fire("loaded", true, false);
            });
        } catch (e) {
            spinner.processes--;
            modelNode.fire("error", e);
        }
    }
}

function parse(plugin, modelNode, data, options) {
    try {
        const binData = ensureBinary(data);
        if (isBinary(binData)) {
            parseBinary(plugin, binData, modelNode, options);
        } else {
            parseASCII(plugin, ensureString(data), modelNode, options);
        }
    } catch (e) {
        modelNode.fire("error", e);
    }
}

function isBinary(data) {
    const reader = new DataView(data);
    const numFaces = reader.getUint32(80, true);
    const faceSize = (32 / 8 * 3) + ((32 / 8 * 3) * 3) + (16 / 8);
    const numExpectedBytes = 80 + (32 / 8) + (numFaces * faceSize);
    if (numExpectedBytes === reader.byteLength) {
        return true;
    }
    const 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(plugin, data, modelNode, options) {
    const reader = new DataView(data);
    const faces = reader.getUint32(80, true);
    let r;
    let g;
    let b;
    let hasColors = false;
    let colors;
    let defaultR;
    let defaultG;
    let defaultB;
    let lastR = null;
    let lastG = null;
    let lastB = null;
    let newMesh = false;
    let alpha;
    for (let 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;
        }
    }
    const material = new MetallicMaterial(modelNode, { // Share material with all meshes
        roughness: 0.5
    });
    // var material = new PhongMaterial(modelNode, { // Share material with all meshes
    //     diffuse: [0.4, 0.4, 0.4],
    //     reflectivity: 1,
    //     specular: [0.5, 0.5, 1.0]
    // });
    let dataOffset = 84;
    let faceLength = 12 * 4 + 2;
    let positions = [];
    let normals = [];
    let splitMeshes = options.splitMeshes;
    for (let face = 0; face < faces; face++) {
        let start = dataOffset + face * faceLength;
        let normalX = reader.getFloat32(start, true);
        let normalY = reader.getFloat32(start + 4, true);
        let normalZ = reader.getFloat32(start + 8, true);
        if (hasColors) {
            let 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 (let i = 1; i <= 3; i++) {
            let vertexstart = start + i * 12;
            positions.push(reader.getFloat32(vertexstart, true));
            positions.push(reader.getFloat32(vertexstart + 4, true));
            positions.push(reader.getFloat32(vertexstart + 8, true));
            normals.push(normalX, normalY, normalZ);
            if (hasColors) {
                colors.push(r, g, b, 1); // TODO: handle alpha
            }
        }
        if (splitMeshes && newMesh) {
            addMesh(modelNode, positions, normals, colors, material, options);
            positions = [];
            normals = [];
            colors = colors ? [] : null;
            newMesh = false;
        }
    }
    if (positions.length > 0) {
        addMesh(modelNode, positions, normals, colors, material, options);
    }
}

function parseASCII(plugin, data, modelNode, options) {
    const faceRegex = /facet([\s\S]*?)endfacet/g;
    let faceCounter = 0;
    const floatRegex = /[\s]+([+-]?(?:\d+.\d+|\d+.|\d+|.\d+)(?:[eE][+-]?\d+)?)/.source;
    const vertexRegex = new RegExp('vertex' + floatRegex + floatRegex + floatRegex, 'g');
    const normalRegex = new RegExp('normal' + floatRegex + floatRegex + floatRegex, 'g');
    const positions = [];
    const normals = [];
    const colors = null;
    let normalx;
    let normaly;
    let normalz;
    let result;
    let verticesPerFace;
    let normalsPerFace;
    let 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) {
            plugin.error("Error in normal of face " + faceCounter);
        }
        if (verticesPerFace !== 3) {
            plugin.error("Error in positions of face " + faceCounter);
        }
        faceCounter++;
    }
    const material = new MetallicMaterial(modelNode, {
        roughness: 0.5
    });
    // var material = new PhongMaterial(modelNode, {
    //     diffuse: [0.4, 0.4, 0.4],
    //     reflectivity: 1,
    //     specular: [0.5, 0.5, 1.0]
    // });
    addMesh(modelNode, positions, normals, colors, material, options);
}

function addMesh(modelNode, positions, normals, colors, material, options) {

    const indices = new Int32Array(positions.length / 3);
    for (let 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) {
        math.faceToVertexNormals(positions, normals, options);
    }

    const origin = tempVec3a;

    worldToRTCPositions(positions, positions, origin);

    const geometry = new ReadableGeometry(modelNode, {
        primitive: "triangles",
        positions: positions,
        normals: normals,
        colors: colors,
        indices: indices
    });

    const mesh = new Mesh(modelNode, {
        origin: (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0) ? origin : null,
        geometry: geometry,
        material: material,
        edges: options.edges
    });

    modelNode.addChild(mesh);
}

function ensureString(buffer) {
    if (typeof buffer !== 'string') {
        return decodeText(new Uint8Array(buffer));
    }
    return buffer;
}

function ensureBinary(buffer) {
    if (typeof buffer === 'string') {
        const arrayBuffer = new Uint8Array(buffer.length);
        for (let 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);
    }
    let s = '';
    for (let i = 0, il = array.length; i < il; i++) {
        s += String.fromCharCode(array[i]); // Implicitly assumes little-endian.
    }
    return decodeURIComponent(escape(s));
}

export {STLSceneGraphLoader}