import {
BufferAttribute, // three.js 中的 BufferAttribute 类,用于创建和管理缓冲区属性
BufferGeometry, // three.js 中的 BufferGeometry 类,用于表示几何体
Color, // three.js 中的 Color 类,用于表示和操作颜色
FileLoader, // three.js 中的 FileLoader 类,用于加载文件
Float32BufferAttribute, // three.js 中的 Float32BufferAttribute 类,用于创建 32 位浮点数缓冲区属性
Loader, // three.js 中的 Loader 基类,所有加载器的基类
} from "three" // 从 three.js 模块中导入这些类
import * as fflate from "../libs/fflate.module.js" // 导入 fflate 库,用于处理压缩数据
class VTKLoader extends Loader {
constructor(manager) {
super(manager)
}
load(url, onLoad, onProgress, onError) { // 定义 load 方法,用于加载 VTK 文件
const scope = this // 保存当前对象的引用
const loader = new FileLoader(scope.manager) // 创建 FileLoader 实例
loader.setPath(scope.path) // 设置文件路径
loader.setResponseType("arraybuffer") // 设置响应类型为 arraybuffer
loader.setRequestHeader(scope.requestHeader) // 设置请求头
loader.setWithCredentials(scope.withCredentials) // 设置是否携带凭证
loader.load(url, function(text) { // 加载文件并处理响应
try {
onLoad(scope.parse(text)) // 解析文件并调用 onLoad 回调函数
} catch (e) {
if (onError) {
onError(e) // 如果有错误,调用 onError 回调函数
} else {
console.error(e) // 如果没有提供 onError 回调函数,则在控制台输出错误
}
scope.manager.itemError(url) // 通知管理器加载项出错
}
}, onProgress, onError) // 传递进度和错误回调函数
}
parse(data) {// 定义 parse 方法,用于解析 VTK 数据
function parseASCII(data) {
const indices = [] // 存储三角形顶点索引
const positions = [] // 存储顶点坐标
const colors = [] // 存储顶点颜色
const normals = [] // 存储顶点法向量
let result // 临时变量,用于存储正则匹配结果
const patWord = /^[^\d.\s-]+/ // 匹配非数字、空白和负号的字符
const pat3Floats = /(-?\d+.?[\d-+e]*)\s+(-?\d+.?[\d-+e]*)\s+(-?\d+.?[\d-+e]*)/g // 匹配三个浮点数
const patConnectivity = /^(\d+)\s+([\s\d]*)/ // 匹配多边形连接信息
const patPOINTS = /^POINTS / // 匹配 POINTS 标记
const patPOLYGONS = /^POLYGONS / // 匹配 POLYGONS 标记
const patTRIANGLE_STRIPS = /^TRIANGLE_STRIPS / // 匹配 TRIANGLE_STRIPS 标记
const patPOINT_DATA = /^POINT_DATA[ ]+(\d+)/ // 匹配 POINT_DATA 标记
const patCELL_DATA = /^CELL_DATA[ ]+(\d+)/ // 匹配 CELL_DATA 标记
const patCOLOR_SCALARS = /^COLOR_SCALARS[ ]+(\w+)[ ]+3/ // 匹配 COLOR_SCALARS 标记
const patNORMALS = /^NORMALS[ ]+(\w+)[ ]+(\w+)/ // 匹配 NORMALS 标记
let inPointsSection = false // 标记是否在 POINTS 部分
let inPolygonsSection = false // 标记是否在 POLYGONS 部分
let inTriangleStripSection = false // 标记是否在 TRIANGLE_STRIPS 部分
let inPointDataSection = false // 标记是否在 POINT_DATA 部分
let inCellDataSection = false // 标记是否在 CELL_DATA 部分
let inColorSection = false // 标记是否在 COLOR_SCALARS 部分
let inNormalsSection = false // 标记是否在 NORMALS 部分
const color = new Color() // 创建 Color 实例
const lines = data.split("\n") // 按行分割数据
for (const i in lines) { // 遍历每一行
const line = lines[i].trim() // 去掉行首尾空白
if (line.indexOf("DATASET") === 0) { // 如果行以 DATASET 开头
const dataset = line.split(" ")[1] // 获取数据集类型
if (dataset !== "POLYDATA") throw new Error("Unsupported DATASET type: " + dataset) // 如果不是 POLYDATA 类型,抛出错误
} else if (inPointsSection) { // 如果在 POINTS 部分
while ((result = pat3Floats.exec(line)) !== null) { // 匹配三个浮点数
if (patWord.exec(line) !== null) break // 如果匹配到非数字字符,停止匹配
const x = parseFloat(result[1]) // 解析第一个浮点数
const y = parseFloat(result[2]) // 解析第二个浮点数
const z = parseFloat(result[3]) // 解析第三个浮点数
positions.push(x, y, z) // 将坐标添加到 positions 数组
}
} else if (inPolygonsSection) { // 如果在 POLYGONS 部分
if ((result = patConnectivity.exec(line)) !== null) { // 匹配连接信息
const numVertices = parseInt(result[1]) // 获取顶点数
const inds = result[2].split(/\s+/) // 获取顶点索引
if (numVertices >= 3) { // 如果顶点数大于等于 3
const i0 = parseInt(inds[0]) // 第一个顶点索引
let k = 1 // 顶点索引偏移
for (let j = 0; j < numVertices - 2; ++j) { // 遍历多边形
const i1 = parseInt(inds[k]) // 第二个顶点索引
const i2 = parseInt(inds[k + 1]) // 第三个顶点索引
indices.push(i0, i1, i2) // 添加三角形顶点索引
k++
}
}
}
} else if (inTriangleStripSection) { // 如果在 TRIANGLE_STRIPS 部分
if ((result = patConnectivity.exec(line)) !== null) { // 匹配连接信息
const numVertices = parseInt(result[1]) // 获取顶点数
const inds = result[2].split(/\s+/) // 获取顶点索引
if (numVertices >= 3) { // 如果顶点数大于等于 3
for (let j = 0; j < numVertices - 2; j++) { // 遍历三角带
if (j % 2 === 1) { // 如果是奇数索引
const i0 = parseInt(inds[j]) // 第一个顶点索引
const i1 = parseInt(inds[j + 2]) // 第二个顶点索引
const i2 = parseInt(inds[j + 1]) // 第三个顶点索引
indices.push(i0, i1, i2) // 添加三角形顶点索引
} else { // 如果是偶数索引
const i0 = parseInt(inds[j]) // 第一个顶点索引
const i1 = parseInt(inds[j + 1]) // 第二个顶点索引
const i2 = parseInt(inds[j + 2]) // 第三个顶点索引
indices.push(i0, i1, i2) // 添加三角形顶点索引
}
}
}
}
} else if (inPointDataSection || inCellDataSection) { // 如果在 POINT_DATA 或 CELL_DATA 部分
if (inColorSection) { // 如果在 COLOR_SCALARS 部分
while ((result = pat3Floats.exec(line)) !== null) { // 匹配三个浮点数
if (patWord.exec(line) !== null) break // 如果匹配到非数字字符,停止匹配
const r = parseFloat(result[1]) // 解析红色分量
const g = parseFloat(result[2]) // 解析绿色分量
const b = parseFloat(result[3]) // 解析蓝色分量
color.set(r, g, b).convertSRGBToLinear() // 设置颜色并转换为线性颜色空间
colors.push(color.r, color.g, color.b) // 将颜色添加到 colors 数组
}
} else if (inNormalsSection) { // 如果在 NORMALS 部分
while ((result = pat3Floats.exec(line)) !== null) { // 匹配三个浮点数
if (patWord.exec(line) !== null) break // 如果匹配到非数字字符,停止匹配
const nx = parseFloat(result[1]) // 解析法向量的 x 分量
const ny = parseFloat(result[2]) // 解析法向量的 y 分量
const nz = parseFloat(result[3]) // 解析法向量的 z 分量
normals.push(nx, ny, nz) // 将法向量添加到 normals 数组
}
}
}
if (patPOLYGONS.exec(line) !== null) { // 匹配 POLYGONS 标记
inPolygonsSection = true // 设置为 POLYGONS 部分
inPointsSection = false // 取消 POINTS 部分
inTriangleStripSection = false // 取消 TRIANGLE_STRIPS 部分
} else if (patPOINTS.exec(line) !== null) { // 匹配 POINTS 标记
inPolygonsSection = false // 取消 POLYGONS 部分
inPointsSection = true // 设置为 POINTS 部分
inTriangleStripSection = false // 取消 TRIANGLE_STRIPS 部分
} else if (patTRIANGLE_STRIPS.exec(line) !== null) { // 匹配 TRIANGLE_STRIPS 标记
inPolygonsSection = false // 取消 POLYGONS 部分
inPointsSection = false // 取消 POINTS 部分
inTriangleStripSection = true // 设置为 TRIANGLE_STRIPS 部分
} else if (patPOINT_DATA.exec(line) !== null) { // 匹配 POINT_DATA 标记
inPointDataSection = true // 设置为 POINT_DATA 部分
inPointsSection = false // 取消 POINTS 部分
inPolygonsSection = false // 取消 POLYGONS 部分
inTriangleStripSection = false // 取消 TRIANGLE_STRIPS 部分
} else if (patCELL_DATA.exec(line) !== null) { // 匹配 CELL_DATA 标记
inCellDataSection = true // 设置为 CELL_DATA 部分
inPointsSection = false // 取消 POINTS 部分
inPolygonsSection = false // 取消 POLYGONS 部分
inTriangleStripSection = false // 取消 TRIANGLE_STRIPS 部分
} else if (patCOLOR_SCALARS.exec(line) !== null) { // 匹配 COLOR_SCALARS 标记
inColorSection = true // 设置为 COLOR_SCALARS 部分
inNormalsSection = false // 取消 NORMALS 部分
inPointsSection = false // 取消 POINTS 部分
inPolygonsSection = false // 取消 POLYGONS 部分
inTriangleStripSection = false // 取消 TRIANGLE_STRIPS 部分
} else if (patNORMALS.exec(line) !== null) { // 匹配 NORMALS 标记
inNormalsSection = true // 设置为 NORMALS 部分
inColorSection = false // 取消 COLOR_SCALARS 部分
inPointsSection = false // 取消 POINTS 部分
inPolygonsSection = false // 取消 POLYGONS 部分
inTriangleStripSection = false // 取消 TRIANGLE_STRIPS 部分
}
}
let geometry = new BufferGeometry()
geometry.setIndex(indices) // 设置几何体的索引
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3)) // 设置顶点位置属性
if (normals.length === positions.length) {
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3)) // 设置法向量属性
}
if (colors.length !== indices.length) {
if (colors.length === positions.length) {
geometry.setAttribute("color", new Float32BufferAttribute(colors, 3)) // 设置颜色属性
}
} else {
geometry = geometry.toNonIndexed() // 将几何体转换为非索引几何体,即每个面都单独存储顶点数据
const numTriangles = geometry.attributes.position.count / 3 // 计算三角形的数量(顶点数量除以 3)
if (colors.length === (numTriangles * 3)) { // 如果颜色数组的长度等于三角形数量的三倍
const newColors = [] // 创建一个新的数组来存储展开后的颜色数据
for (let i = 0; i < numTriangles; i++) { // 遍历所有的三角形
const r = colors[3 * i + 0] // 获取颜色数组中第 i 个三角形的红色分量
const g = colors[3 * i + 1] // 获取颜色数组中第 i 个三角形的绿色分量
const b = colors[3 * i + 2] // 获取颜色数组中第 i 个三角形的蓝色分量
color.set(r, g, b).convertSRGBToLinear() // 设置颜色并转换为线性颜色空间
newColors.push(color.r, color.g, color.b) // 将线性颜色空间的红色分量添加到新颜色数组中
newColors.push(color.r, color.g, color.b) // 将线性颜色空间的绿色分量添加到新颜色数组中
newColors.push(color.r, color.g, color.b) // 将线性颜色空间的蓝色分量添加到新颜色数组中
}
geometry.setAttribute("color", new Float32BufferAttribute(newColors, 3)) // 设置几何体的颜色属性,使用新的颜色数组
}
}
return geometry // 返回几何体
}
function parseBinary(data) {
const buffer = new Uint8Array(data) // 创建一个 Uint8Array 缓冲区以存储二进制数据
const dataView = new DataView(data) // 创建 DataView 以便更方便地读取二进制数据
let points = [] // 存储顶点位置
let normals = [] // 存储法向量
let indices = [] // 存储顶点索引
let index = 0 // 当前读取位置的索引
// 查找字符串
function findString(buffer, start) {
let index = start // 从指定的起始位置开始
let c = buffer[index] // 读取当前字节
const s = [] // 用于存储字符串的字符
while (c !== 10) { // 遇到换行符结束
s.push(String.fromCharCode(c)) // 将当前字节转换为字符并添加到数组中
index++ // 移动到下一个字节
c = buffer[index] // 读取下一个字节
}
return {
start: start, // 返回起始位置
end: index, // 返回结束位置
next: index + 1, // 返回下一个位置
parsedString: s.join(""), // 返回解析后的字符串
}
}
let state, line
while (true) {
state = findString(buffer, index) // 查找当前索引位置的字符串
line = state.parsedString // 获取解析后的字符串
if (line.indexOf("DATASET") === 0) { // 如果行以 DATASET 开头
const dataset = line.split(" ")[1] // 获取数据集类型
if (dataset !== "POLYDATA") throw new Error("Unsupported DATASET type: " + dataset) // 如果不是 POLYDATA 类型,抛出错误
} else if (line.indexOf("POINTS") === 0) { // 如果行以 POINTS 开头
const numberOfPoints = parseInt(line.split(" ")[1], 10) // 获取顶点数
const count = numberOfPoints * 4 * 3 // 计算数据长度(每个顶点由三个 32 位浮点数组成)
points = new Float32Array(numberOfPoints * 3) // 创建 Float32Array 用于存储顶点位置
let pointIndex = state.next // 获取下一个位置的索引
for (let i = 0; i < numberOfPoints; i++) { // 遍历所有顶点
points[3 * i] = dataView.getFloat32(pointIndex, false) // 读取 x 坐标
points[3 * i + 1] = dataView.getFloat32(pointIndex + 4, false) // 读取 y 坐标
points[3 * i + 2] = dataView.getFloat32(pointIndex + 8, false) // 读取 z 坐标
pointIndex = pointIndex + 12 // 移动到下一个顶点
}
state.next = state.next + count + 1 // 更新下一个位置的索引
} else if (line.indexOf("TRIANGLE_STRIPS") === 0) { // 如果行以 TRIANGLE_STRIPS 开头
const numberOfStrips = parseInt(line.split(" ")[1], 10) // 获取三角带数量
const size = parseInt(line.split(" ")[2], 10) // 获取数据大小
const count = size * 4 // 计算数据长度
indices = new Uint32Array(3 * size - 9 * numberOfStrips) // 创建 Uint32Array 用于存储顶点索引
let indicesIndex = 0 // 索引数组的当前索引
let pointIndex = state.next // 获取下一个位置的索引
for (let i = 0; i < numberOfStrips; i++) { // 遍历所有三角带
const indexCount = dataView.getInt32(pointIndex, false) // 获取当前三角带的顶点数
const strip = [] // 用于存储三角带的顶点索引
pointIndex += 4 // 移动到下一个顶点
for (let s = 0; s < indexCount; s++) { // 遍历三角带中的所有顶点
strip.push(dataView.getInt32(pointIndex, false)) // 读取顶点索引并添加到数组中
pointIndex += 4 // 移动到下一个顶点
}
for (let j = 0; j < indexCount - 2; j++) { // 遍历三角带中的所有三角形
if (j % 2) { // 如果是奇数索引
indices[indicesIndex++] = strip[j] // 添加三角形的第一个顶点索引
indices[indicesIndex++] = strip[j + 2] // 添加三角形的第二个顶点索引
indices[indicesIndex++] = strip[j + 1] // 添加三角形的第三个顶点索引
} else { // 如果是偶数索引
indices[indicesIndex++] = strip[j] // 添加三角形的第一个顶点索引
indices[indicesIndex++] = strip[j + 1] // 添加三角形的第二个顶点索引
indices[indicesIndex++] = strip[j + 2] // 添加三角形的第三个顶点索引
}
}
}
state.next = state.next + count + 1 // 更新下一个位置的索引
} else if (line.indexOf("POLYGONS") === 0) {
const numberOfStrips = parseInt(line.split(" ")[1], 10) // 获取多边形数量
const size = parseInt(line.split(" ")[2], 10) // 获取数据大小
const count = size * 4 // 计算数据长度
indices = new Uint32Array(3 * size - 9 * numberOfStrips) // 创建 Uint32Array 用于存储顶点索引
let indicesIndex = 0 // 索引数组的当前索引
let pointIndex = state.next // 获取下一个位置的索引
for (let i = 0; i < numberOfStrips; i++) { // 遍历所有多边形
const indexCount = dataView.getInt32(pointIndex, false) // 获取当前多边形的顶点数
const strip = [] // 用于存储多边形的顶点索引
pointIndex += 4 // 移动到下一个顶点
for (let s = 0; s < indexCount; s++) { // 遍历多边形中的所有顶点
strip.push(dataView.getInt32(pointIndex, false)) // 读取顶点索引并添加到数组中
pointIndex += 4 // 移动到下一个顶点
}
for (let j = 1; j < indexCount - 1; j++) { // 遍历多边形中的所有三角形
indices[indicesIndex++] = strip[0] // 添加三角形的第一个顶点索引
indices[indicesIndex++] = strip[j] // 添加三角形的第二个顶点索引
indices[indicesIndex++] = strip[j + 1] // 添加三角形的第三个顶点索引
}
}
state.next = state.next + count + 1 // 更新下一个位置的索引
} else if (line.indexOf("POINT_DATA") === 0) {
const numberOfPoints = parseInt(line.split(" ")[1], 10) // 获取顶点数
state = findString(buffer, state.next) // 查找下一个字符串
const count = numberOfPoints * 4 * 3 // 计算数据长度
normals = new Float32Array(numberOfPoints * 3) // 创建 Float32Array 用于存储法向量
let pointIndex = state.next // 获取下一个位置的索引
for (let i = 0; i < numberOfPoints; i++) { // 遍历所有顶点
normals[3 * i] = dataView.getFloat32(pointIndex, false) // 读取法向量的 x 分量
normals[3 * i + 1] = dataView.getFloat32(pointIndex + 4, false) // 读取法向量的 y 分量
normals[3 * i + 2] = dataView.getFloat32(pointIndex + 8, false) // 读取法向量的 z 分量
pointIndex += 12 // 移动到下一个顶点
}
state.next = state.next + count // 更新下一个位置的索引
}
index = state.next // 更新当前索引位置
if (index >= buffer.byteLength) { // 如果索引超过缓冲区长度,结束循环
break
}
}
const geometry = new BufferGeometry() // 创建 BufferGeometry 实例
geometry.setIndex(new BufferAttribute(indices, 1)) // 设置几何体的索引属性
geometry.setAttribute("position", new BufferAttribute(points, 3)) // 设置顶点位置属性
if (normals.length === points.length) {
geometry.setAttribute("normal", new BufferAttribute(normals, 3)) // 设置法向量属性
}
return geometry // 返回几何体
}
// 连接两个 Float32Array
function Float32Concat(first, second) {
const firstLength = first.length, result = new Float32Array(firstLength + second.length) // 创建新数组,长度为两个数组之和
result.set(first) // 设置第一个数组
result.set(second, firstLength) // 设置第二个数组,从第一个数组的末尾开始
return result // 返回连接后的新数组
}
// 连接两个 Int32Array
function Int32Concat(first, second) {
const firstLength = first.length, result = new Int32Array(firstLength + second.length) // 创建新数组,长度为两个数组之和
result.set(first) // 设置第一个数组
result.set(second, firstLength) // 设置第二个数组,从第一个数组的末尾开始
return result // 返回连接后的新数组
}
function parseXML(stringFile) {
// Changes XML to JSON, based on https://davidwalsh.name/convert-xml-json
function xmlToJson(xml) { // 将 XML 转换为 JSON 对象
let obj = {} // 创建空对象
if (xml.nodeType === 1) { // 如果是元素节点
if (xml.attributes) { // 如果有属性
if (xml.attributes.length > 0) {
obj["attributes"] = {} // 创建属性对象
for (let j = 0; j < xml.attributes.length; j++) { // 遍历所有属性
const attribute = xml.attributes.item(j) // 获取属性
obj["attributes"][attribute.nodeName] = attribute.nodeValue.trim() // 将属性添加到对象中
}
}
}
} else if (xml.nodeType === 3) { // 如果是文本节点
obj = xml.nodeValue.trim() // 获取文本值
}
if (xml.hasChildNodes()) { // 如果有子节点
for (let i = 0; i < xml.childNodes.length; i++) { // 遍历所有子节点
const item = xml.childNodes.item(i) // 获取子节点
const nodeName = item.nodeName // 获取节点名称
if (typeof obj[nodeName] === "undefined") {
const tmp = xmlToJson(item) // 递归解析子节点
if (tmp !== "") {
if (Array.isArray(tmp["#text"])) {
tmp["#text"] = tmp["#text"][0] // 可能存在子节点 既有文本又有dom节点的情况
}
obj[nodeName] = tmp // 如果子节点不是空字符串,将其添加到对象中
}
} else {
if (typeof obj[nodeName].push === "undefined") {
const old = obj[nodeName] // 获取旧值
obj[nodeName] = [old] // 将旧值转换为数组
}
const tmp = xmlToJson(item) // 递归解析子节点
if (tmp !== "") {
if (Array.isArray(tmp["#text"])) {
tmp["#text"] = tmp["#text"][0] // 可能存在子节点 既有文本又有dom节点的情况
}
obj[nodeName].push(tmp) // 将子节点添加到数组中
}
}
}
}
return obj // 返回解析后的对象
}
// Taken from Base64-js
function Base64toByteArray(b64) { // 将 Base64 编码的字符串转换为字节数组
const Arr = typeof Uint8Array !== "undefined" ? Uint8Array : Array // 确定使用 Uint8Array 或普通数组
const revLookup = [] // 创建反向查找表
const code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" // Base64 编码字符表
for (let i = 0, l = code.length; i < l; ++i) {
revLookup[code.charCodeAt(i)] = i // 填充反向查找表
}
revLookup["-".charCodeAt(0)] = 62 // 填充特殊字符
revLookup["_".charCodeAt(0)] = 63 // 填充特殊字符
const len = b64.length // 获取 Base64 字符串长度
if (len % 4 > 0) {
throw new Error("Invalid string. Length must be a multiple of 4") // 如果长度不是 4 的倍数,抛出错误
}
const placeHolders = b64[len - 2] === "=" ? 2 : b64[len - 1] === "=" ? 1 : 0 // 计算填充字符的数量
const arr = new Arr(len * 3 / 4 - placeHolders) // 创建字节数组
const l = placeHolders > 0 ? len - 4 : len // 计算有效字符的长度
let L = 0
let i, j
for (i = 0, j = 0; i < l; i += 4, j += 3) { // 遍历 Base64 字符串
const tmp = (revLookup[b64.charCodeAt(i)] << 18) | (revLookup[b64.charCodeAt(i + 1)] << 12) | (revLookup[b64.charCodeAt(i + 2)] << 6) | revLookup[b64.charCodeAt(i + 3)] // 将 4 个 Base64 字符转换为 3 个字节
arr[L++] = (tmp & 0xFF0000) >> 16 // 提取第一个字节
arr[L++] = (tmp & 0xFF00) >> 8 // 提取第二个字节
arr[L++] = tmp & 0xFF // 提取第三个字节
}
if (placeHolders === 2) { // 如果有两个填充字符
const tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4) // 将 2 个 Base64 字符转换为 1 个字节
arr[L++] = tmp & 0xFF // 提取字节
} else if (placeHolders === 1) { // 如果有一个填充字符
const tmp = (revLookup[b64.charCodeAt(i)] << 10) | (revLookup[b64.charCodeAt(i + 1)] << 4) | (revLookup[b64.charCodeAt(i + 2)] >> 2) // 将 3 个 Base64 字符转换为 2 个字节
arr[L++] = (tmp >> 8) & 0xFF // 提取第一个字节
arr[L++] = tmp & 0xFF // 提取第二个字节
}
return arr // 返回字节数组
}
function parseDataArray(ele, compressed) {
let numBytes = 0
if (json.attributes.header_type === "UInt64") { // 根据 header_type 确定字节数
numBytes = 8
} else if (json.attributes.header_type === "UInt32") {
numBytes = 4
}
let txt, content
// Check the format
if (ele.attributes.format === "binary" && compressed) {
if (ele.attributes.type === "Float32") {
txt = new Float32Array() // 创建空的 Float32Array
} else if (ele.attributes.type === "Int32" || ele.attributes.type === "Int64") {
txt = new Int32Array() // 创建空的 Int32Array
}
// 带标头的 VTP 数据具有以下结构:
// [#blocks][#u-size][#p-size][#c-size-1][#c-size-2]...[#c-size-#blocks][DATA]
//
// 每个标记都是一个整数值,其类型由文件顶部的“header_type”指定(如果未指定类型,则为 UInt32)。标记含义如下:
// [#blocks] = 块数
// [#u-size] = 压缩前的块大小
// [#p-size] = 最后部分块的大小(如果不需要,则为零)
// [#c-size-i] = 压缩后块 i 的大小(以字节为单位)
//
// [DATA] 部分连续存储每个附加在一起的块。从数据部分开头到块开头的偏移量
// 根据标头,通过对前面块的压缩块大小求和来计算。
const textNode = ele["#text"]
const rawData = Array.isArray(textNode) ? textNode[0] : textNode // 获取原始数据
const byteData = Base64toByteArray(rawData) // 将 Base64 编码转换为字节数组
// 无论 header_type 如何, 每个数据点由 8 位组成
const dataPointSize = 8 // 数据点大小
let blocks = byteData[0] // 读取压缩块的数量
for (let i = 1; i < numBytes - 1; i++) {
blocks = blocks | (byteData[i] << (i * dataPointSize)) // 计算块数
}
let headerSize = (blocks + 3) * numBytes // 计算头部大小
const padding = ((headerSize % 3) > 0) ? 3 - (headerSize % 3) : 0 // 计算填充量
headerSize = headerSize + padding // 添加填充量到头部大小
const dataOffsets = [] // 存储数据块的偏移量
let currentOffset = headerSize // 当前偏移量
dataOffsets.push(currentOffset) // 添加当前偏移量到数组
// 获取压缩后的块大小。
// c-size-i 之前有三个块,因此我们跳过 3*numBytes
const cSizeStart = 3 * numBytes // 压缩块大小的起始位置
for (let i = 0; i < blocks; i++) {
let currentBlockSize = byteData[i * numBytes + cSizeStart] // 读取当前块的大小
for (let j = 1; j < numBytes - 1; j++) { // 遍历字节以计算当前块的大小
currentBlockSize = currentBlockSize | (byteData[i * numBytes + cSizeStart + j] << (j * dataPointSize))
}
currentOffset = currentOffset + currentBlockSize
dataOffsets.push(currentOffset)
}
for (let i = 0; i < dataOffsets.length - 1; i++) { // 遍历所有数据块的偏移量
const data = fflate.unzlibSync(byteData.slice(dataOffsets[i], dataOffsets[i + 1])) // 解压缩数据块
content = data.buffer // 获取数据缓冲区
if (ele.attributes.type === "Float32") { // 如果数据类型是 Float32
content = new Float32Array(content) // 创建 Float32Array
txt = Float32Concat(txt, content) // 连接浮点数组
} else if (ele.attributes.type === "Int32" || ele.attributes.type === "Int64") { // 如果数据类型是 Int32 或 Int64
content = new Int32Array(content) // 创建 Int32Array
txt = Int32Concat(txt, content) // 连接整数数组
}
}
delete ele["#text"] // 删除元素的文本内容,以便后续处理
if (ele.attributes.type === "Int64") {
if (ele.attributes.format === "binary") {
txt = txt.filter(function(el, idx) {
if (idx % 2 !== 1) return true
})
}
}
} else {
if (ele.attributes.format === "binary" && !compressed) { // 如果数据格式是二进制且未压缩
content = Base64toByteArray(ele["#text"]) // 将 Base64 字符串转换为字节数组
// 未压缩情况下的 VTP 数据具有以下结构:
// [#bytes][DATA]
// 其中“[#bytes]”是一个整数值,指定其后的数据块中的字节数。
content = content.slice(numBytes).buffer // 移除头部的字节数信息
} else {
if (ele["#text"]) { // 如果存在文本内容
content = ele["#text"].split(/\s+/).filter(function(el) { // 分割并过滤空字符串
if (el !== "") return el // 只保留非空字符串
})
} else {
content = new Int32Array(0).buffer // 如果没有文本内容,创建一个空的 Int32Array 缓冲区
}
}
delete ele["#text"] // 删除元素的文本内容,以便后续处理
// Get the content and optimize it
if (ele.attributes.type === "Float32") { // 如果数据类型是 Float32
txt = new Float32Array(content) // 创建一个 Float32Array 来存储数据
} else if (ele.attributes.type === "Int32") { // 如果数据类型是 Int32
txt = new Int32Array(content) // 创建一个 Int32Array 来存储数据
} else if (ele.attributes.type === "Int64") { // 如果数据类型是 Int64
txt = new Int32Array(content) // 创建一个 Int32Array 来存储数据
if (ele.attributes.format === "binary") { // 如果数据格式是二进制
txt = txt.filter(function(el, idx) { // 过滤数据
if (idx % 2 !== 1) return true // 只保留偶数索引的数据
})
}
}
} // endif ( ele.attributes.format === 'binary' && compressed )
return txt // 返回解析后的数据
}
// 主要部分
// 获取 DOM
const dom = new DOMParser().parseFromString(stringFile, "application/xml") // 将 XML 字符串解析为 DOM
// 获取文档根元素
const doc = dom.documentElement
// 将 XML 转换为 JSON
const json = xmlToJson(doc)
let points = [] // 存储顶点位置
let normals = [] // 存储法向量
let indices = [] // 存储顶点索引
if (json.AppendedData) {
const appendedData = json.AppendedData["#text"].slice(1)
const encoding = json.AppendedData.attributes.encoding
const piece = json.PolyData.Piece // 获取 Piece 元素
const sections = ["PointData", "CellData", "Points", "Verts", "Lines", "Strips", "Polys"]
let sectionIndex = 0 // 当前部分的索引
const offsets = sections.map(s => {
const sect = piece[s]
if (sect && sect.DataArray) {
const arr = Array.isArray(sect.DataArray) ? sect.DataArray : [sect.DataArray]
return arr.map(a => a.attributes.offset)
}
return []
}).flat()
for (const sect of sections) {
const section = piece[sect] // 获取当前部分
// 如果存在 DataArray
if (section && section.DataArray) {
if (Array.isArray(section.DataArray)) {
for (const sectionEle of section.DataArray) {
sectionEle["#text"] = appendedData.slice(offsets[sectionIndex], offsets[sectionIndex + 1])
sectionEle.attributes.format = "binary"
sectionIndex++
}
} else {
section.DataArray["#text"] = appendedData.slice(offsets[sectionIndex], offsets[sectionIndex + 1])
section.DataArray.attributes.format = "binary"
sectionIndex++
}
}
}
}
if (json.PolyData) {
const piece = json.PolyData.Piece // 获取 Piece 元素
const compressed = json.attributes.hasOwnProperty("compressor") // 检查是否压缩
// 可以优化
// 遍历各个部分
const sections = ["PointData", "Points", "Strips", "Polys"] // 定义需要处理的部分
let sectionIndex = 0 // 当前部分的索引
const numberOfSections = sections.length // 部分的数量
while (sectionIndex < numberOfSections) { // 遍历所有部分
const section = piece[sections[sectionIndex]] // 获取当前部分
// 如果存在 DataArray
if (section && section.DataArray) {
// 根据 DataArray 的数量处理
let arr
if (Array.isArray(section.DataArray)) { // 如果 DataArray 是数组
arr = section.DataArray
} else {
arr = [section.DataArray] // 如果只有一个 DataArray,将其转换为数组
}
let dataArrayIndex = 0 // 当前 DataArray 的索引
const numberOfDataArrays = arr.length // DataArray 的数量
while (dataArrayIndex < numberOfDataArrays) { // 遍历所有 DataArray
// 解析 DataArray
if (("#text" in arr[dataArrayIndex]) && (arr[dataArrayIndex]["#text"].length > 0)) {
arr[dataArrayIndex].text = parseDataArray(arr[dataArrayIndex], compressed) // 解析 DataArray 的文本内容
}
dataArrayIndex++ // 移动到下一个 DataArray
}
switch (sections[sectionIndex]) {
case "PointData": { // 处理 PointData 部分
const numberOfPoints = parseInt(piece.attributes.NumberOfPoints) // 获取顶点数量
const normalsName = section.attributes.Normals // 获取法向量名称
if (numberOfPoints > 0) { // 如果顶点数量大于 0
for (let i = 0, len = arr.length; i < len; i++) { // 遍历所有 DataArray
if (normalsName === arr[i].attributes.Name) { // 如果名称匹配
const components = arr[i].attributes.NumberOfComponents // 获取分量数量
normals = new Float32Array(numberOfPoints * components) // 创建 Float32Array 存储法向量
normals.set(arr[i].text, 0) // 设置法向量数据
}
}
}
}
break
case "Points": { // 处理 Points 部分
const numberOfPoints = parseInt(piece.attributes.NumberOfPoints) // 获取顶点数量
if (numberOfPoints > 0) { // 如果顶点数量大于 0
const components = section.DataArray.attributes.NumberOfComponents // 获取分量数量
points = new Float32Array(numberOfPoints * components) // 创建 Float32Array 存储顶点位置
points.set(section.DataArray.text, 0) // 设置顶点位置数据
}
}
break
case "Strips": { // 处理 Strips 部分
const numberOfStrips = parseInt(piece.attributes.NumberOfStrips) // 获取三角带数量
if (numberOfStrips > 0) { // 如果三角带数量大于 0
const connectivity = new Int32Array(section.DataArray[0].text.length) // 创建 Int32Array 存储连接信息
const offset = new Int32Array(section.DataArray[1].text.length) // 创建 Int32Array 存储偏移信息
connectivity.set(section.DataArray[0].text, 0) // 设置连接信息数据
offset.set(section.DataArray[1].text, 0) // 设置偏移信息数据
const size = numberOfStrips + connectivity.length // 计算大小
indices = new Uint32Array(3 * size - 9 * numberOfStrips) // 创建 Uint32Array 存储顶点索引
let indicesIndex = 0 // 索引数组的当前索引
for (let i = 0, len = numberOfStrips; i < len; i++) { // 遍历所有三角带
const strip = [] // 用于存储三角带的顶点索引
for (let s = 0, len1 = offset[i], len0 = 0; s < len1 - len0; s++) { // 遍历三角带中的所有顶点
strip.push(connectivity[s]) // 读取顶点索引并添加到数组中
if (i > 0) len0 = offset[i - 1] // 更新偏移量
}
for (let j = 0, len1 = offset[i], len0 = 0; j < len1 - len0 - 2; j++) { // 遍历三角带中的所有三角形
if (j % 2) { // 如果是奇数索引
indices[indicesIndex++] = strip[j] // 添加三角形的第一个顶点索引
indices[indicesIndex++] = strip[j + 2] // 添加三角形的第二个顶点索引
indices[indicesIndex++] = strip[j + 1] // 添加三角形的第三个顶点索引
} else { // 如果是偶数索引
indices[indicesIndex++] = strip[j] // 添加三角形的第一个顶点索引
indices[indicesIndex++] = strip[j + 1] // 添加三角形的第二个顶点索引
indices[indicesIndex++] = strip[j + 2] // 添加三角形的第三个顶点索引
}
if (i > 0) len0 = offset[i - 1] // 更新偏移量
}
}
}
}
break
case "Polys": { // 处理 Polys 部分
const numberOfPolys = parseInt(piece.attributes.NumberOfPolys) // 获取多边形数量
if (numberOfPolys > 0) { // 如果多边形数量大于 0
const connectivity = new Int32Array(section.DataArray[0].text.length) // 创建 Int32Array 存储连接信息
const offset = new Int32Array(section.DataArray[1].text.length) // 创建 Int32Array 存储偏移信息
connectivity.set(section.DataArray[0].text, 0) // 设置连接信息数据
offset.set(section.DataArray[1].text, 0) // 设置偏移信息数据
const size = numberOfPolys + connectivity.length // 计算大小
indices = new Uint32Array(3 * size - 9 * numberOfPolys) // 创建 Uint32Array 存储顶点索引
let indicesIndex = 0, connectivityIndex = 0
let i = 0, len0 = 0
const len = numberOfPolys
while (i < len) { // 遍历所有多边形
const poly = [] // 用于存储多边形的顶点索引
let s = 0
const len1 = offset[i]
while (s < len1 - len0) { // 遍历多边形中的所有顶点
poly.push(connectivity[connectivityIndex++]) // 读取顶点索引并添加到数组中
s++
}
let j = 1
while (j < len1 - len0 - 1) { // 遍历多边形中的所有三角形
indices[indicesIndex++] = poly[0] // 添加三角形的第一个顶点索引
indices[indicesIndex++] = poly[j] // 添加三角形的第二个顶点索引
indices[indicesIndex++] = poly[j + 1] // 添加三角形的第三个顶点索引
j++
}
i++
len0 = offset[i - 1] // 更新偏移量
}
}
}
break
default:
break
}
}
sectionIndex++ // 移动到下一个部分
}
const geometry = new BufferGeometry() // 创建 BufferGeometry 实例
geometry.setIndex(new BufferAttribute(indices, 1)) // 设置几何体的索引属性
geometry.setAttribute("position", new BufferAttribute(points, 3)) // 设置顶点位置属性
if (normals.length === points.length) {
geometry.setAttribute("normal", new BufferAttribute(normals, 3)) // 设置法向量属性
}
return geometry // 返回几何体
} else {
throw new Error("Unsupported DATASET type")
}
}
const textDecoder = new TextDecoder() // 创建 TextDecoder 实例,用于解码文本
// 获取文件的前 5 行,检查是否包含关键字 binary
const meta = textDecoder.decode(new Uint8Array(data, 0, 250)).split("\n")
if (meta[0].indexOf("xml") !== -1) {
return parseXML(textDecoder.decode(data)) // 如果是 XML 文件,调用 parseXML 方法解析
} else if (meta[2].includes("ASCII")) {
return parseASCII(textDecoder.decode(data)) // 如果是 ASCII 文件,调用 parseASCII 方法解析
} else {
return parseBinary(data) // 否则调用 parseBinary 方法解析
}
}
}
export { VTKLoader } // 导出 VTKLoader 类
02-03
2059