UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

417 lines (386 loc) 14.3 kB
import { m as macro } from '../../macros2.js'; import BinaryHelper from '../Core/BinaryHelper.js'; import DataAccessHelper from '../Core/DataAccessHelper.js'; import vtkDataArray from '../../Common/Core/DataArray.js'; import vtkMatrixBuilder from '../../Common/Core/MatrixBuilder.js'; import vtkPoints from '../../Common/Core/Points.js'; import vtkMergePoints from '../../Common/DataModel/MergePoints.js'; import vtkPolyData from '../../Common/DataModel/PolyData.js'; import '../Core/DataAccessHelper/LiteHttpDataAccessHelper.js'; // import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // HTTP + zip // import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; // html + base64 + zip // import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // zip const { vtkErrorMacro, vtkWarningMacro } = macro; function parseHeader(headerString) { const headerSubStr = headerString.split(' '); const fieldValues = headerSubStr.filter(e => e.indexOf('=') > -1); const header = {}; for (let i = 0; i < fieldValues.length; ++i) { const fieldValueStr = fieldValues[i]; const fieldValueSubStr = fieldValueStr.split('='); if (fieldValueSubStr.length === 2) { header[fieldValueSubStr[0]] = fieldValueSubStr[1]; } } return header; } function addValuesToArray(src, dst) { for (let i = 0; i < src.length; i++) { dst.push(src[i]); } } // facet normal ni nj nk // outer loop // vertex v1x v1y v1z // vertex v2x v2y v2z // vertex v3x v3y v3z // endloop // endfacet function readTriangle(lines, offset, points, cellArray, cellNormals) { const normalLine = lines[offset]; if (normalLine === undefined) { return -1; } if (normalLine.indexOf('endfacet') !== -1) { return offset + 1; } if (normalLine.indexOf('facet') === -1) { return offset + 1; // Move to next line } let nbVertex = 0; let nbConsumedLines = 2; const firstVertexIndex = points.length / 3; const normal = normalLine.split(/[ \t]+/).filter(i => i).slice(-3).map(Number); addValuesToArray(normal, cellNormals); while (lines[offset + nbConsumedLines].indexOf('vertex') !== -1) { const line = lines[offset + nbConsumedLines]; const coords = line.split(/[ \t]+/).filter(i => i).slice(-3).map(Number); addValuesToArray(coords, points); nbVertex++; nbConsumedLines++; } cellArray.push(nbVertex); for (let i = 0; i < nbVertex; i++) { cellArray.push(firstVertexIndex + i); } while (lines[offset + nbConsumedLines] && lines[offset + nbConsumedLines].indexOf('endfacet') !== -1) { nbConsumedLines++; } // +1 (endfacet) +1 (next facet) return offset + nbConsumedLines + 2; } // ---------------------------------------------------------------------------- // vtkSTLReader methods // ---------------------------------------------------------------------------- function vtkSTLReader(publicAPI, model) { // Set our className model.classHierarchy.push('vtkSTLReader'); // Create default dataAccessHelper if not available if (!model.dataAccessHelper) { model.dataAccessHelper = DataAccessHelper.get('http'); } // Internal method to fetch Array function fetchData(url, option = {}) { const compression = option.compression !== undefined ? option.compression : model.compression; const progressCallback = option.progressCallback !== undefined ? option.progressCallback : model.progressCallback; if (option.binary) { return model.dataAccessHelper.fetchBinary(url, { compression, progressCallback }); } return model.dataAccessHelper.fetchText(publicAPI, url, { compression, progressCallback }); } function removeDuplicateVertices(tolerancePower) { const polydata = model.output[0]; const points = polydata.getPoints().getData(); const faces = polydata.getPolys().getData(); if (!points || !faces) { vtkWarningMacro('No valid polydata.'); return; } const inCellNormals = polydata.getCellData().getNormals(); const inCellScalars = polydata.getCellData().getScalars(); const mergedPoints = vtkPoints.newInstance({ dataType: points.constructor }); mergedPoints.allocate(points.length / 6, 3); let locator = model.locator; if (!locator) { locator = vtkMergePoints.newInstance(); } if (tolerancePower >= 0) { locator.setTolerance(10 ** -tolerancePower); } locator.initPointInsertion(mergedPoints, polydata.getBounds(), Math.floor(points.length / 3 / 2)); const mergedCells = []; const keptCellIds = []; for (let i = 0, cellId = 0; i < faces.length; i += 4, cellId++) { if (faces[i] === 3) { const nodeIds = [0, 0, 0]; for (let j = 0; j < 3; j++) { const pid = faces[i + 1 + j]; const x = [points[pid * 3], points[pid * 3 + 1], points[pid * 3 + 2]]; nodeIds[j] = locator.insertUniquePoint(x).id; } if (nodeIds[0] !== nodeIds[1] && nodeIds[0] !== nodeIds[2] && nodeIds[1] !== nodeIds[2]) { mergedCells.push(3, nodeIds[0], nodeIds[1], nodeIds[2]); keptCellIds.push(cellId); } } } const outCellArray = new faces.constructor(mergedCells); polydata.getPoints().setData(mergedPoints.getData(), 3); polydata.getPolys().setData(outCellArray); if (inCellNormals && inCellNormals.getData()) { const inData = inCellNormals.getData(); const numComp = inCellNormals.getNumberOfComponents(); const outData = new inData.constructor(keptCellIds.length * numComp); for (let i = 0; i < keptCellIds.length; i++) { const srcOffset = keptCellIds[i] * numComp; const dstOffset = i * numComp; for (let c = 0; c < numComp; c++) { outData[dstOffset + c] = inData[srcOffset + c]; } } polydata.getCellData().setNormals(vtkDataArray.newInstance({ name: inCellNormals.getName() || 'Normals', values: outData, numberOfComponents: numComp })); } if (inCellScalars && inCellScalars.getData()) { const inData = inCellScalars.getData(); const numComp = inCellScalars.getNumberOfComponents(); const outData = new inData.constructor(keptCellIds.length * numComp); for (let i = 0; i < keptCellIds.length; i++) { const srcOffset = keptCellIds[i] * numComp; const dstOffset = i * numComp; for (let c = 0; c < numComp; c++) { outData[dstOffset + c] = inData[srcOffset + c]; } } polydata.getCellData().setScalars(vtkDataArray.newInstance({ name: inCellScalars.getName() || 'Attribute', values: outData, numberOfComponents: numComp })); } locator.initialize(); polydata.modified(); } // Set DataSet url publicAPI.setUrl = (url, option = { binary: true }) => { model.url = url; // Remove the file in the URL const path = url.split('/'); path.pop(); model.baseURL = path.join('/'); // Fetch metadata return publicAPI.loadData(option); }; // Fetch the actual data arrays publicAPI.loadData = (option = {}) => fetchData(model.url, option).then(publicAPI.parse); publicAPI.parse = content => { if (typeof content === 'string') { publicAPI.parseAsText(content); } else { publicAPI.parseAsArrayBuffer(content); } }; publicAPI.parseAsArrayBuffer = content => { if (!content) { return; } if (content !== model.parseData) { publicAPI.modified(); } else { return; } model.parseData = content; // ascii/binary detection let isBinary = false; // 80=STL header, 4=uint32 of num of triangles (le) const dview = new DataView(content, 0, 80 + 4); const numTriangles = dview.getUint32(80, true); // 50 bytes per triangle isBinary = 84 + numTriangles * 50 === content.byteLength; // Check if ascii format if (!isBinary) { publicAPI.parseAsText(BinaryHelper.arrayBufferToString(content)); return; } // Binary parsing // Header const headerData = content.slice(0, 80); const headerStr = BinaryHelper.arrayBufferToString(headerData); const header = parseHeader(headerStr); // Data const dataView = new DataView(content, 84); // global.dataview = dataView; const nbFaces = (content.byteLength - 84) / 50; const pointValues = new Float32Array(nbFaces * 9); const normalValues = new Float32Array(nbFaces * 3); const cellValues = new Uint32Array(nbFaces * 4); const cellDataValues = new Uint16Array(nbFaces); let cellOffset = 0; for (let faceIdx = 0; faceIdx < nbFaces; faceIdx++) { const offset = faceIdx * 50; normalValues[faceIdx * 3 + 0] = dataView.getFloat32(offset + 0, true); normalValues[faceIdx * 3 + 1] = dataView.getFloat32(offset + 4, true); normalValues[faceIdx * 3 + 2] = dataView.getFloat32(offset + 8, true); pointValues[faceIdx * 9 + 0] = dataView.getFloat32(offset + 12, true); pointValues[faceIdx * 9 + 1] = dataView.getFloat32(offset + 16, true); pointValues[faceIdx * 9 + 2] = dataView.getFloat32(offset + 20, true); pointValues[faceIdx * 9 + 3] = dataView.getFloat32(offset + 24, true); pointValues[faceIdx * 9 + 4] = dataView.getFloat32(offset + 28, true); pointValues[faceIdx * 9 + 5] = dataView.getFloat32(offset + 32, true); pointValues[faceIdx * 9 + 6] = dataView.getFloat32(offset + 36, true); pointValues[faceIdx * 9 + 7] = dataView.getFloat32(offset + 40, true); pointValues[faceIdx * 9 + 8] = dataView.getFloat32(offset + 44, true); cellValues[cellOffset++] = 3; cellValues[cellOffset++] = faceIdx * 3 + 0; cellValues[cellOffset++] = faceIdx * 3 + 1; cellValues[cellOffset++] = faceIdx * 3 + 2; cellDataValues[faceIdx] = dataView.getUint16(offset + 48, true); } // Rotate points const orientationField = 'SPACE'; if (orientationField in header && header[orientationField] !== 'LPS') { const XYZ = header[orientationField]; const mat4 = new Float32Array(16); mat4[15] = 1; switch (XYZ[0]) { case 'L': mat4[0] = 1; break; case 'R': mat4[0] = -1; break; default: vtkErrorMacro(`Can not convert STL file from ${XYZ} to LPS space: ` + `permutations not supported. Use itk.js STL reader instead.`); return; } switch (XYZ[1]) { case 'P': mat4[5] = 1; break; case 'A': mat4[5] = -1; break; default: vtkErrorMacro(`Can not convert STL file from ${XYZ} to LPS space: ` + `permutations not supported. Use itk.js STL reader instead.`); return; } switch (XYZ[2]) { case 'S': mat4[10] = 1; break; case 'I': mat4[10] = -1; break; default: vtkErrorMacro(`Can not convert STL file from ${XYZ} to LPS space: ` + `permutations not supported. Use itk.js STL reader instead.`); return; } vtkMatrixBuilder.buildFromDegree().setMatrix(mat4).apply(pointValues).apply(normalValues); } const polydata = vtkPolyData.newInstance(); polydata.getPoints().setData(pointValues, 3); polydata.getPolys().setData(cellValues); polydata.getCellData().setScalars(vtkDataArray.newInstance({ name: 'Attribute', values: cellDataValues })); polydata.getCellData().setNormals(vtkDataArray.newInstance({ name: 'Normals', values: normalValues, numberOfComponents: 3 })); // Add new output model.output[0] = polydata; if (model.merging && model.removeDuplicateVertices >= 0) { removeDuplicateVertices(model.removeDuplicateVertices); } }; publicAPI.parseAsText = content => { if (!content) { return; } if (content !== model.parseData) { publicAPI.modified(); } else { return; } model.parseData = content; const lines = content.split('\n'); let offset = 1; const points = []; const cellArray = []; const cellNormals = []; while (offset !== -1) { offset = readTriangle(lines, offset, points, cellArray, cellNormals); } const polydata = vtkPolyData.newInstance(); polydata.getPoints().setData(Float32Array.from(points), 3); polydata.getPolys().setData(Uint32Array.from(cellArray)); polydata.getCellData().setNormals(vtkDataArray.newInstance({ name: 'Normals', values: Float32Array.from(cellNormals), numberOfComponents: 3 })); // Add new output model.output[0] = polydata; if (model.merging && model.removeDuplicateVertices >= 0) { removeDuplicateVertices(model.removeDuplicateVertices); } }; publicAPI.requestData = (inData, outData) => { publicAPI.parse(model.parseData); }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { // baseURL: null, // dataAccessHelper: null, // locator: null, // url: null, merging: true, removeDuplicateVertices: -1 }; // ---------------------------------------------------------------------------- function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues); // Build VTK API macro.obj(publicAPI, model); macro.get(publicAPI, model, ['url', 'baseURL']); macro.setGet(publicAPI, model, ['dataAccessHelper', 'locator', 'merging', 'removeDuplicateVertices']); macro.algo(publicAPI, model, 0, 1); // vtkSTLReader methods vtkSTLReader(publicAPI, model); // To support destructuring if (!model.compression) { model.compression = null; } if (!model.progressCallback) { model.progressCallback = null; } } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkSTLReader'); // ---------------------------------------------------------------------------- var vtkSTLReader$1 = { extend, newInstance }; export { vtkSTLReader$1 as default, extend, newInstance };