xl-public-utils 1.0.25 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -970,5 +970,17 @@ declare module "xl-public-utils" {
970
970
  * 获取顶点索引映射
971
971
  */
972
972
  getPointsMapData(polydata: ArrayLike<number>): IndexData;
973
+ /**
974
+ * 加载3D模型文件
975
+ * @param data - 文件路径(字符串)或文件数据(ArrayBuffer)
976
+ * @param name - 文件名,用于推断文件类型(可选)
977
+ * @param typeOverride - 手动指定的文件类型,将覆盖文件名推断的类型(可选)
978
+ * @returns Promise<{ vertices: Float32Array, faces: Uint32Array }> - 包含顶点和面片数据的网格对象
979
+ */
980
+ loadFile(
981
+ data: string | ArrayBuffer,
982
+ name?: string,
983
+ typeOverride?: string
984
+ ): Promise<{ vertices: Float32Array; faces: Uint32Array }>;
973
985
  }
974
986
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xl-public-utils",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
package/src/drcUtils.js CHANGED
@@ -323,35 +323,4 @@ export function enCodeMesh(mesh, byteLength = 14, attr = undefined) {
323
323
  }
324
324
  }
325
325
  return enCodeMeshByVF(vertices, faces, byteLength, attr)
326
- }
327
-
328
- /**
329
- * 初始化Draco文件
330
- */
331
- export function initDrcCoder() {
332
- if (!globalThis.encoderModule) {
333
- const timerEncoder = setInterval(() => {
334
- if (globalThis.DracoEncoderModule) {
335
- globalThis.DracoEncoderModule().then((res) => {
336
- globalThis.encoderModule = res;
337
- });
338
- clearInterval(timerEncoder);
339
- }
340
- }, 50);
341
- }
342
- if (!globalThis.decoderModule) {
343
- const timerDecoder = setInterval(() => {
344
- if (globalThis.DracoDecoderModule) {
345
- globalThis.DracoDecoderModule().then((res) => {
346
- globalThis.decoderModule = res;
347
- function a() {
348
- return res;
349
- }
350
- // show.value = true;
351
- // vtkDracoReader.setDracoDecoder(a);
352
- });
353
- clearInterval(timerDecoder);
354
- }
355
- }, 50);
356
- }
357
326
  }
@@ -0,0 +1,24 @@
1
+ import { decodeDrcBufferArray } from "../../drcUtils.js";
2
+
3
+ /**
4
+ * @typedef {Object} DracoMesh
5
+ * @property {Float32Array} vertices
6
+ * @property {Uint32Array} faces
7
+ */
8
+
9
+ class DRCReader {
10
+ /**
11
+ * 读取并解码Draco压缩的3D模型文件
12
+ * @param {ArrayBuffer} buffer - Draco压缩的3D模型数据
13
+ * @returns {DracoMesh} 解码后的网格数据
14
+ */
15
+ static readArrayBuffer(buffer) {
16
+ const { vertices, faces } = decodeDrcBufferArray(buffer);
17
+ return {
18
+ vertices,
19
+ faces
20
+ };
21
+ }
22
+ }
23
+
24
+ export default DRCReader;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @typedef {Object} Mesh
3
+ * @property {number[]} vertices
4
+ * @property {number[]} colors
5
+ * @property {number[]} texCoords
6
+ * @property {number[]} faces
7
+ * @property {number[]} uvFaces
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} FinalizedMesh
12
+ * @property {Float32Array} vertices
13
+ * @property {Uint32Array} faces
14
+ * @property {Float32Array | null} colors
15
+ * @property {Float32Array | null} texCoords
16
+ * @property {Uint32Array | null} uvFaces
17
+ */
18
+
19
+ class OBJReader {
20
+ static TRI_INDEX = 4294967295;
21
+
22
+ /**
23
+ * @param {string} data
24
+ * @returns {FinalizedMesh | FinalizedMesh[]}
25
+ */
26
+ static importOBJ(data) {
27
+ const meshes = [];
28
+ let currentMesh = {
29
+ vertices: [],
30
+ colors: [],
31
+ texCoords: [],
32
+ faces: [],
33
+ uvFaces: []
34
+ };
35
+
36
+ const lines = data.split('\n');
37
+ const inv255 = 1.0 / 255;
38
+ let nbVertices = 0;
39
+ let nbTexCoords = 0;
40
+
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i].trim();
43
+
44
+ if (line.length === 0) continue;
45
+
46
+ const firstChar = line[0];
47
+
48
+ if (firstChar === 'v') {
49
+ const secondChar = line[1];
50
+
51
+ if (secondChar === ' ') {
52
+ // 顶点坐标
53
+ const split = line.split(/\s+/);
54
+ currentMesh.vertices.push(
55
+ parseFloat(split[1]),
56
+ parseFloat(split[2]),
57
+ parseFloat(split[3])
58
+ );
59
+
60
+ // 顶点颜色(如果存在)
61
+ if (split[4]) {
62
+ currentMesh.colors.push(
63
+ parseFloat(split[4]),
64
+ parseFloat(split[5]),
65
+ parseFloat(split[6])
66
+ );
67
+ }
68
+ nbVertices++;
69
+ } else if (secondChar === 't') {
70
+ // 纹理坐标
71
+ const split = line.split(/\s+/);
72
+ currentMesh.texCoords.push(
73
+ parseFloat(split[1]),
74
+ parseFloat(split[2])
75
+ );
76
+ nbTexCoords++;
77
+ }
78
+ } else if (firstChar === 'f') {
79
+ // 面
80
+ const split = line.split(/\s+/);
81
+ const nbVerts = split.length - 1;
82
+
83
+ if (nbVerts < 3) continue;
84
+
85
+ // 三角化多边形
86
+ const nbPrim = Math.ceil(nbVerts / 2) - 1;
87
+
88
+ for (let j = 0; j < nbPrim; j++) {
89
+ const id1 = j + 1;
90
+ const id2 = j + 2;
91
+ let id3 = nbVerts - id1;
92
+ let id4 = nbVerts - j;
93
+
94
+ if (id3 === id2) {
95
+ id3 = id4;
96
+ id4 = this.TRI_INDEX;
97
+ }
98
+
99
+ const sp1 = split[id1].split('/');
100
+ const sp2 = split[id2].split('/');
101
+ const sp3 = split[id3].split('/');
102
+ const isQuad = id4 !== this.TRI_INDEX;
103
+ const sp4 = isQuad ? split[id4].split('/') : null;
104
+
105
+ let iv1 = parseInt(sp1[0], 10);
106
+ let iv2 = parseInt(sp2[0], 10);
107
+ let iv3 = parseInt(sp3[0], 10);
108
+ let iv4 = isQuad && sp4 ? parseInt(sp4[0], 10) : undefined;
109
+
110
+ // 处理负索引
111
+ iv1 = iv1 < 0 ? iv1 + nbVertices : iv1 - 1;
112
+ iv2 = iv2 < 0 ? iv2 + nbVertices : iv2 - 1;
113
+ iv3 = iv3 < 0 ? iv3 + nbVertices : iv3 - 1;
114
+ if (isQuad && iv4 !== undefined) {
115
+ iv4 = iv4 < 0 ? iv4 + nbVertices : iv4 - 1;
116
+ }
117
+
118
+ // 跳过退化三角形
119
+ if (iv1 === iv2 || iv1 === iv3 || iv2 === iv3) continue;
120
+ if (isQuad && iv4 !== undefined && (iv4 === iv1 || iv4 === iv2 || iv4 === iv3)) continue;
121
+
122
+ currentMesh.faces.push(iv1, iv2, iv3, isQuad && iv4 !== undefined ? iv4 : this.TRI_INDEX);
123
+
124
+ // 处理UV坐标
125
+ if (sp1[1]) {
126
+ let uv1 = parseInt(sp1[1], 10);
127
+ let uv2 = parseInt(sp2[1], 10);
128
+ let uv3 = parseInt(sp3[1], 10);
129
+ let uv4 = isQuad && sp4 && sp4[1] ? parseInt(sp4[1], 10) : undefined;
130
+
131
+ uv1 = uv1 < 0 ? uv1 + nbTexCoords : uv1 - 1;
132
+ uv2 = uv2 < 0 ? uv2 + nbTexCoords : uv2 - 1;
133
+ uv3 = uv3 < 0 ? uv3 + nbTexCoords : uv3 - 1;
134
+ if (isQuad && uv4 !== undefined) uv4 = uv4 < 0 ? uv4 + nbTexCoords : uv4 - 1;
135
+
136
+ currentMesh.uvFaces.push(uv1, uv2, uv3, isQuad && uv4 !== undefined ? uv4 : this.TRI_INDEX);
137
+ }
138
+ }
139
+ } else if (firstChar === '#') {
140
+ // 处理ZBrush风格的顶点颜色
141
+ if (line.startsWith('#MRGB ')) {
142
+ const split = line.split(/\s+/);
143
+ const blockMRGB = split[1];
144
+
145
+ for (let m = 2; m < blockMRGB.length; m += 8) {
146
+ const hex = parseInt(blockMRGB.substr(m, 6), 16);
147
+ currentMesh.colors.push(
148
+ ((hex >> 16) & 0xff) * inv255,
149
+ ((hex >> 8) & 0xff) * inv255,
150
+ (hex & 0xff) * inv255
151
+ );
152
+ }
153
+ }
154
+ } else if (line.startsWith('o ')) {
155
+ // 新对象
156
+ if (meshes.length > 0) {
157
+ meshes.push(this.finalizeMesh(currentMesh));
158
+ currentMesh = {
159
+ vertices: [],
160
+ colors: [],
161
+ texCoords: [],
162
+ faces: [],
163
+ uvFaces: []
164
+ };
165
+ }
166
+ }
167
+ }
168
+
169
+ // 添加最后一个网格
170
+ meshes.push(this.finalizeMesh(currentMesh));
171
+
172
+ return meshes.length === 1 ? meshes[0] : meshes;
173
+ }
174
+
175
+ /**
176
+ * @param {Mesh} mesh
177
+ * @returns {FinalizedMesh}
178
+ */
179
+ static finalizeMesh(mesh) {
180
+ // 将四边形拆分为三角形
181
+ const triangles = [];
182
+
183
+ for (let i = 0; i < mesh.faces.length; i += 4) {
184
+ const v0 = mesh.faces[i];
185
+ const v1 = mesh.faces[i + 1];
186
+ const v2 = mesh.faces[i + 2];
187
+ const v3 = mesh.faces[i + 3];
188
+
189
+ if (v3 === this.TRI_INDEX) {
190
+ // 三角形
191
+ triangles.push(3, v0, v1, v2);
192
+ } else {
193
+ // 四边形:拆分为两个三角形
194
+ triangles.push(3, v0, v1, v2);
195
+ triangles.push(3, v0, v2, v3);
196
+ }
197
+ }
198
+
199
+ return {
200
+ vertices: new Float32Array(mesh.vertices),
201
+ faces: new Uint32Array(triangles),
202
+ colors: mesh.colors.length > 0 ? new Float32Array(mesh.colors) : null,
203
+ texCoords: mesh.texCoords.length > 0 ? new Float32Array(mesh.texCoords) : null,
204
+ uvFaces: null // 注意:拆分后UV坐标也需要相应处理
205
+ };
206
+ }
207
+
208
+ /**
209
+ * @param {string} data
210
+ * @returns {FinalizedMesh | FinalizedMesh[]}
211
+ */
212
+ static readFile(data) {
213
+ return this.importOBJ(data);
214
+ }
215
+ }
216
+
217
+ export default OBJReader;
@@ -0,0 +1,428 @@
1
+ /**
2
+ * @typedef {'char' | 'uchar' | 'int8' | 'uint8' | 'short' | 'ushort' | 'int16' | 'uint16' |
3
+ * 'int' | 'uint' | 'float' | 'int32' | 'uint32' | 'float32' | 'double' | 'float64'} PLYDataType
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} PLYProperty
8
+ * @property {PLYDataType | 'list'} type
9
+ * @property {string} name
10
+ * @property {PLYDataType} [countType]
11
+ * @property {PLYDataType} [itemType]
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} PLYElement
16
+ * @property {string} name
17
+ * @property {number} count
18
+ * @property {PLYProperty[]} properties
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} PLYInfo
23
+ * @property {boolean} binary
24
+ * @property {boolean} littleEndian
25
+ * @property {PLYElement[]} elements
26
+ * @property {number} headerSize
27
+ * @property {Float32Array | null} vertices
28
+ * @property {Uint32Array | null} faces
29
+ * @property {Float32Array | null} colors
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} PLYMesh
34
+ * @property {Float32Array | null} vertices
35
+ * @property {Uint32Array | null} faces
36
+ * @property {Float32Array | null} colors
37
+ */
38
+
39
+ /**
40
+ * @typedef {(str: string) => number} ParseFunction
41
+ */
42
+
43
+ class PLYReader {
44
+ /**
45
+ * @param {PLYDataType} type
46
+ * @returns {number}
47
+ */
48
+ static typeToOctet(type) {
49
+ switch (type) {
50
+ case 'char':
51
+ case 'uchar':
52
+ case 'int8':
53
+ case 'uint8':
54
+ return 1;
55
+ case 'short':
56
+ case 'ushort':
57
+ case 'int16':
58
+ case 'uint16':
59
+ return 2;
60
+ case 'int':
61
+ case 'uint':
62
+ case 'float':
63
+ case 'int32':
64
+ case 'uint32':
65
+ case 'float32':
66
+ return 4;
67
+ case 'double':
68
+ case 'float64':
69
+ return 8;
70
+ default:
71
+ return 0;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @param {PLYDataType} type
77
+ * @param {boolean} isFloat
78
+ * @returns {ParseFunction}
79
+ */
80
+ static getParseFunc(type, isFloat) {
81
+ if (isFloat) {
82
+ return type === 'double' || type === 'float64' ? parseFloat : parseFloat;
83
+ }
84
+
85
+ switch (type) {
86
+ case 'char':
87
+ case 'int8':
88
+ case 'short':
89
+ case 'int16':
90
+ case 'int':
91
+ case 'int32':
92
+ return parseInt;
93
+ case 'uchar':
94
+ case 'uint8':
95
+ case 'ushort':
96
+ case 'uint16':
97
+ case 'uint':
98
+ case 'uint32':
99
+ return (str) => parseInt(str) >>> 0;
100
+ default:
101
+ return parseFloat;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * @param {ArrayBuffer} buffer
107
+ * @returns {PLYInfo}
108
+ */
109
+ static readHeader(buffer) {
110
+ let lineIndex = 0;
111
+ const uint8Array = new Uint8Array(buffer);
112
+ const decoder = new TextDecoder();
113
+ const headerEndMarker = "end_header\n";
114
+ let headerEndIndex = 0;
115
+
116
+ for (let i = 0; i < uint8Array.length; i++) {
117
+ if (String.fromCharCode(...uint8Array.slice(i, i + headerEndMarker.length)) === headerEndMarker) {
118
+ headerEndIndex = i + headerEndMarker.length;
119
+ break;
120
+ }
121
+ }
122
+
123
+ const headerText = decoder.decode(uint8Array.slice(0, headerEndIndex));
124
+ const lines = headerText.split('\n');
125
+
126
+ if (lines[lineIndex++].trim() !== 'ply') {
127
+ throw new Error('Invalid PLY file: missing ply header');
128
+ }
129
+
130
+ const infos = {
131
+ binary: false,
132
+ littleEndian: true,
133
+ elements: [],
134
+ headerSize: 0,
135
+ vertices: null,
136
+ faces: null,
137
+ colors: null
138
+ };
139
+
140
+ while (lineIndex < lines.length) {
141
+ const line = lines[lineIndex++].trim();
142
+
143
+ if (line === 'end_header') {
144
+ infos.headerSize = lines.slice(0, lineIndex).join('\n').length + 1;
145
+ break;
146
+ }
147
+
148
+ const parts = line.split(/\s+/);
149
+
150
+ if (parts[0] === 'format') {
151
+ infos.binary = parts[1] === 'binary_little_endian' || parts[1] === 'binary_big_endian';
152
+ infos.littleEndian = parts[1] !== 'binary_big_endian';
153
+ } else if (parts[0] === 'element') {
154
+ const element = {
155
+ name: parts[1],
156
+ count: parseInt(parts[2]),
157
+ properties: []
158
+ };
159
+ infos.elements.push(element);
160
+ } else if (parts[0] === 'property') {
161
+ const element = infos.elements[infos.elements.length - 1];
162
+ if (parts[1] === 'list') {
163
+ element.properties.push({
164
+ type: 'list',
165
+ countType: parts[2],
166
+ itemType: parts[3],
167
+ name: parts[4]
168
+ });
169
+ } else {
170
+ element.properties.push({
171
+ type: parts[1],
172
+ name: parts[2]
173
+ });
174
+ }
175
+ }
176
+ }
177
+
178
+ return infos;
179
+ }
180
+
181
+ /**
182
+ * @param {PLYElement} element
183
+ * @param {PLYInfo} infos
184
+ * @param {Uint8Array} data
185
+ * @param {number} offset
186
+ * @returns {number}
187
+ */
188
+ static readElementVertex(element, infos, data, offset) {
189
+ const vertices = [];
190
+ const colors = [];
191
+ let currentOffset = offset;
192
+
193
+ for (let i = 0; i < element.count; i++) {
194
+ let x, y, z;
195
+ let r = 1, g = 1, b = 1;
196
+
197
+ for (const prop of element.properties) {
198
+ if (infos.binary) {
199
+ const view = new DataView(data.buffer, currentOffset);
200
+ let value;
201
+
202
+ switch (prop.type) {
203
+ case 'float':
204
+ case 'float32':
205
+ value = view.getFloat32(0, infos.littleEndian);
206
+ currentOffset += 4;
207
+ break;
208
+ case 'double':
209
+ case 'float64':
210
+ value = view.getFloat64(0, infos.littleEndian);
211
+ currentOffset += 8;
212
+ break;
213
+ case 'uchar':
214
+ case 'uint8':
215
+ value = view.getUint8(0);
216
+ currentOffset += 1;
217
+ break;
218
+ default:
219
+ value = view.getInt32(0, infos.littleEndian);
220
+ currentOffset += 4;
221
+ }
222
+
223
+ switch (prop.name) {
224
+ case 'x': x = value; break;
225
+ case 'y': y = value; break;
226
+ case 'z': z = value; break;
227
+ case 'red': r = value / 255; break;
228
+ case 'green': g = value / 255; break;
229
+ case 'blue': b = value / 255; break;
230
+ }
231
+ }
232
+ }
233
+
234
+ if (x !== undefined && y !== undefined && z !== undefined) {
235
+ vertices.push(x, y, z);
236
+ colors.push(r, g, b);
237
+ }
238
+ }
239
+
240
+ infos.vertices = new Float32Array(vertices);
241
+ infos.colors = new Float32Array(colors);
242
+
243
+ return currentOffset;
244
+ }
245
+
246
+ /**
247
+ * @param {PLYElement} element
248
+ * @param {PLYInfo} infos
249
+ * @param {Uint8Array} data
250
+ * @param {number} offset
251
+ * @returns {number}
252
+ */
253
+ static readElementIndex(element, infos, data, offset) {
254
+ const faces = [];
255
+ let currentOffset = offset;
256
+
257
+ const listProperty = element.properties.find(p => p.type === 'list');
258
+ if (!listProperty || !listProperty.countType || !listProperty.itemType) {
259
+ console.error('无法找到有效的面片list属性');
260
+ return currentOffset;
261
+ }
262
+
263
+ const countTypeSize = this.typeToOctet(listProperty.countType);
264
+ const itemTypeSize = this.typeToOctet(listProperty.itemType);
265
+
266
+ for (let i = 0; i < element.count; i++) {
267
+ if (infos.binary) {
268
+ const countView = new DataView(data.buffer, currentOffset);
269
+
270
+ let count;
271
+ switch (listProperty.countType) {
272
+ case 'uchar':
273
+ case 'uint8':
274
+ count = countView.getUint8(0);
275
+ break;
276
+ case 'ushort':
277
+ case 'uint16':
278
+ count = countView.getUint16(0, infos.littleEndian);
279
+ break;
280
+ default:
281
+ count = countView.getUint8(0);
282
+ }
283
+ currentOffset += countTypeSize;
284
+
285
+ const indexView = new DataView(data.buffer, currentOffset);
286
+ const indices = [];
287
+
288
+ for (let j = 0; j < count; j++) {
289
+ let index;
290
+ const indexOffset = j * itemTypeSize;
291
+
292
+ switch (listProperty.itemType) {
293
+ case 'uchar':
294
+ case 'uint8':
295
+ index = indexView.getUint8(indexOffset);
296
+ break;
297
+ case 'ushort':
298
+ case 'uint16':
299
+ index = indexView.getUint16(indexOffset, infos.littleEndian);
300
+ break;
301
+ case 'uint':
302
+ case 'uint32':
303
+ index = indexView.getUint32(indexOffset, infos.littleEndian);
304
+ break;
305
+ default:
306
+ index = indexView.getUint32(indexOffset, infos.littleEndian);
307
+ }
308
+
309
+ if (infos.vertices && index >= 0 && index < infos.vertices.length / 3) {
310
+ indices.push(index);
311
+ } else {
312
+ console.warn(`面片索引超出范围: ${index}, 顶点数量: ${infos.vertices ? infos.vertices.length / 3 : 0}`);
313
+ indices.push(0);
314
+ }
315
+ }
316
+ currentOffset += count * itemTypeSize;
317
+
318
+ if (count === 3 && indices.length === 3) {
319
+ faces.push(3, indices[0], indices[1], indices[2]);
320
+ } else if (count > 3 && indices.length >= 3) {
321
+ for (let j = 1; j < count - 1 && j + 1 < indices.length; j++) {
322
+ faces.push(3, indices[0], indices[j], indices[j + 1]);
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ infos.faces = new Uint32Array(faces);
329
+ return currentOffset;
330
+ }
331
+
332
+ /**
333
+ * @param {ArrayBuffer} buffer
334
+ * @returns {PLYMesh}
335
+ */
336
+ static importPLY(buffer) {
337
+ const infos = this.readHeader(buffer);
338
+ const data = new Uint8Array(buffer);
339
+ let offset = infos.headerSize;
340
+
341
+ if (!infos.binary) {
342
+ const text = new TextDecoder().decode(buffer.slice(infos.headerSize));
343
+ const lines = text.split('\n').filter(line => line.trim());
344
+ let lineIndex = 0;
345
+
346
+ for (const element of infos.elements) {
347
+ if (element.name === 'vertex') {
348
+ const vertices = [];
349
+ const colors = [];
350
+
351
+ for (let i = 0; i < element.count; i++) {
352
+ if (lineIndex >= lines.length) {
353
+ console.error('读取顶点数据时超出文件范围');
354
+ break;
355
+ }
356
+
357
+ const values = lines[lineIndex++].trim().split(/\s+/).map(parseFloat);
358
+ if (values.length >= 3) {
359
+ vertices.push(values[0], values[1], values[2]);
360
+
361
+ if (values.length >= 6) {
362
+ colors.push(values[3] / 255, values[4] / 255, values[5] / 255);
363
+ } else {
364
+ colors.push(1, 1, 1);
365
+ }
366
+ }
367
+ }
368
+
369
+ infos.vertices = new Float32Array(vertices);
370
+ infos.colors = new Float32Array(colors);
371
+ } else if (element.name === 'face') {
372
+ const faces = [];
373
+
374
+ for (let i = 0; i < element.count; i++) {
375
+ if (lineIndex >= lines.length) {
376
+ console.error('读取面片数据时超出文件范围');
377
+ break;
378
+ }
379
+
380
+ const values = lines[lineIndex++].trim().split(/\s+/).map(v => parseInt(v));
381
+ if (values.length < 1) continue;
382
+
383
+ const count = values[0];
384
+ if (count < 3) continue;
385
+
386
+ if (count === 3) {
387
+ if (values.length >= 4) {
388
+ faces.push(3, values[1], values[2], values[3]);
389
+ }
390
+ } else {
391
+ for (let j = 1; j < count - 1; j++) {
392
+ if (values.length >= j + 3) {
393
+ faces.push(3, values[1], values[j + 1], values[j + 2]);
394
+ }
395
+ }
396
+ }
397
+ }
398
+
399
+ infos.faces = new Uint32Array(faces);
400
+ }
401
+ }
402
+ } else {
403
+ for (const element of infos.elements) {
404
+ if (element.name === 'vertex') {
405
+ offset = this.readElementVertex(element, infos, data, offset);
406
+ } else if (element.name === 'face') {
407
+ offset = this.readElementIndex(element, infos, data, offset);
408
+ }
409
+ }
410
+ }
411
+
412
+ return {
413
+ vertices: infos.vertices,
414
+ faces: infos.faces,
415
+ colors: infos.colors
416
+ };
417
+ }
418
+
419
+ /**
420
+ * @param {ArrayBuffer} buffer
421
+ * @returns {PLYMesh}
422
+ */
423
+ static readArrayBuffer(buffer) {
424
+ return this.importPLY(buffer);
425
+ }
426
+ }
427
+
428
+ export default PLYReader;
@@ -0,0 +1,249 @@
1
+ // 定义核心数据结构接口
2
+
3
+ /**
4
+ * @typedef {Object} STLMesh
5
+ * @property {Float32Array} vertices
6
+ * @property {Int32Array} faces
7
+ * @property {Float32Array | null} [colors]
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object.<string, number>} VertexMap
12
+ */
13
+
14
+ class STLReader {
15
+ /**
16
+ * @param {VertexMap} mapVertices
17
+ * @param {Float32Array} vb
18
+ * @param {number} start
19
+ * @param {number[]} nbVertices
20
+ * @returns {number}
21
+ */
22
+ static detectNewVertex(mapVertices, vb, start, nbVertices) {
23
+ const x = vb[start];
24
+ const y = vb[start + 1];
25
+ const z = vb[start + 2];
26
+ const key = x + ',' + y + ',' + z;
27
+
28
+ let idVertex = mapVertices[key];
29
+ if (idVertex === undefined) {
30
+ idVertex = nbVertices[0];
31
+ mapVertices[key] = idVertex;
32
+ nbVertices[0]++;
33
+ }
34
+ return idVertex;
35
+ }
36
+
37
+ /**
38
+ * @param {string} data
39
+ * @returns {STLMesh}
40
+ */
41
+ static importAsciiSTL(data) {
42
+ const lines = data.split('\n');
43
+ const vertices = [];
44
+ const faces = [];
45
+ const mapVertices = {};
46
+ const nbVertices = [0];
47
+
48
+ for (let i = 0; i < lines.length; i++) {
49
+ const line = lines[i].trim();
50
+ if (line.startsWith('facet')) {
51
+ // 读取三个顶点
52
+ const vertex1Line = lines[i + 2] ? lines[i + 2].trim() : '';
53
+ const vertex2Line = lines[i + 3] ? lines[i + 3].trim() : '';
54
+ const vertex3Line = lines[i + 4] ? lines[i + 4].trim() : '';
55
+
56
+ if (vertex1Line.startsWith('vertex') &&
57
+ vertex2Line.startsWith('vertex') &&
58
+ vertex3Line.startsWith('vertex')) {
59
+
60
+ // 解析顶点坐标
61
+ const v1 = vertex1Line.split(/\s+/);
62
+ const v2 = vertex2Line.split(/\s+/);
63
+ const v3 = vertex3Line.split(/\s+/);
64
+
65
+ if (v1.length >= 4 && v2.length >= 4 && v3.length >= 4) {
66
+ const coords1 = [parseFloat(v1[1]), parseFloat(v1[2]), parseFloat(v1[3])];
67
+ const coords2 = [parseFloat(v2[1]), parseFloat(v2[2]), parseFloat(v2[3])];
68
+ const coords3 = [parseFloat(v3[1]), parseFloat(v3[2]), parseFloat(v3[3])];
69
+
70
+ // 检测或添加唯一顶点
71
+ const tempVb1 = new Float32Array(coords1);
72
+ const tempVb2 = new Float32Array(coords2);
73
+ const tempVb3 = new Float32Array(coords3);
74
+
75
+ const idx1 = this.detectNewVertex(mapVertices, tempVb1, 0, nbVertices);
76
+ const idx2 = this.detectNewVertex(mapVertices, tempVb2, 0, nbVertices);
77
+ const idx3 = this.detectNewVertex(mapVertices, tempVb3, 0, nbVertices);
78
+
79
+ // 添加顶点到数组(如果是新顶点)
80
+ while (vertices.length / 3 <= idx1) {
81
+ vertices.push(...coords1);
82
+ }
83
+ while (vertices.length / 3 <= idx2) {
84
+ vertices.push(...coords2);
85
+ }
86
+ while (vertices.length / 3 <= idx3) {
87
+ vertices.push(...coords3);
88
+ }
89
+
90
+ // 添加面片 - 修改为指定格式:[3, 顶点1索引, 顶点2索引, 顶点3索引]
91
+ faces.push(3, idx1, idx2, idx3);
92
+ }
93
+ }
94
+
95
+ // 跳过这个facet的其他行
96
+ i += 6; // facet + normal + 3*vertex + endfacet + endloop
97
+ }
98
+ }
99
+
100
+ return {
101
+ vertices: new Float32Array(vertices),
102
+ faces: new Int32Array(faces)
103
+ };
104
+ }
105
+
106
+ /**
107
+ * @param {ArrayBuffer} buffer
108
+ * @param {number} nbTriangles
109
+ * @returns {STLMesh}
110
+ */
111
+ static importBinarySTL(buffer, nbTriangles) {
112
+ const data = new Uint8Array(buffer);
113
+ const dataHeader = data.subarray(0, 80);
114
+ const colorMagic = String.fromCharCode.apply(null, Array.from(dataHeader)).indexOf('COLOR=') !== -1;
115
+
116
+ const vertices = [];
117
+ const faces = [];
118
+ const colors = [];
119
+ const mapVertices = {};
120
+ const nbVertices = [0];
121
+
122
+ let offset = 84; // 跳过头部(80字节) + 三角形数量(4字节)
123
+
124
+ for (let i = 0; i < nbTriangles; i++) {
125
+ // 跳过法向量 (12字节)
126
+ offset += 12;
127
+
128
+ // 读取三个顶点坐标
129
+ const view = new DataView(buffer, offset);
130
+
131
+ const v1 = [
132
+ view.getFloat32(0, true), // x1
133
+ view.getFloat32(4, true), // y1
134
+ view.getFloat32(8, true) // z1
135
+ ];
136
+ const v2 = [
137
+ view.getFloat32(12, true), // x2
138
+ view.getFloat32(16, true), // y2
139
+ view.getFloat32(20, true) // z2
140
+ ];
141
+ const v3 = [
142
+ view.getFloat32(24, true), // x3
143
+ view.getFloat32(28, true), // y3
144
+ view.getFloat32(32, true) // z3
145
+ ];
146
+
147
+ offset += 36; // 3个顶点 * 12字节
148
+
149
+ // 读取属性字节(颜色信息)
150
+ const attributeBytes = view.getUint16(36, true);
151
+ offset += 2;
152
+
153
+ // 检测或添加唯一顶点
154
+ const tempVb1 = new Float32Array(v1);
155
+ const tempVb2 = new Float32Array(v2);
156
+ const tempVb3 = new Float32Array(v3);
157
+
158
+ const idx1 = this.detectNewVertex(mapVertices, tempVb1, 0, nbVertices);
159
+ const idx2 = this.detectNewVertex(mapVertices, tempVb2, 0, nbVertices);
160
+ const idx3 = this.detectNewVertex(mapVertices, tempVb3, 0, nbVertices);
161
+
162
+ // 添加顶点到数组(如果是新顶点)
163
+ while (vertices.length / 3 <= idx1) {
164
+ vertices.push(...v1);
165
+ }
166
+ while (vertices.length / 3 <= idx2) {
167
+ vertices.push(...v2);
168
+ }
169
+ while (vertices.length / 3 <= idx3) {
170
+ vertices.push(...v3);
171
+ }
172
+
173
+ // 添加面片 - 修改为指定格式:[3, 顶点1索引, 顶点2索引, 顶点3索引]
174
+ faces.push(3, idx1, idx2, idx3);
175
+
176
+ // 处理颜色信息
177
+ let r = 1.0, g = 1.0, b = 1.0;
178
+ const bit15 = attributeBytes & 32768;
179
+
180
+ if (colorMagic) {
181
+ if (!bit15) {
182
+ r = ((attributeBytes & 31) & 31) / 31.0;
183
+ g = ((attributeBytes >> 5) & 31) / 31.0;
184
+ b = ((attributeBytes >> 10) & 31) / 31.0;
185
+ }
186
+ } else if (bit15) {
187
+ r = ((attributeBytes >> 10) & 31) / 31.0;
188
+ g = ((attributeBytes >> 5) & 31) / 31.0;
189
+ b = ((attributeBytes & 31) & 31) / 31.0;
190
+ }
191
+
192
+ // 为每个顶点添加颜色(如果还没有)
193
+ while (colors.length / 3 <= idx1) {
194
+ colors.push(r, g, b);
195
+ }
196
+ while (colors.length / 3 <= idx2) {
197
+ colors.push(r, g, b);
198
+ }
199
+ while (colors.length / 3 <= idx3) {
200
+ colors.push(r, g, b);
201
+ }
202
+ }
203
+
204
+ return {
205
+ vertices: new Float32Array(vertices),
206
+ faces: new Int32Array(faces),
207
+ colors: colors.length > 0 ? new Float32Array(colors) : null
208
+ };
209
+ }
210
+
211
+ /**
212
+ * @param {ArrayBuffer} buffer
213
+ * @returns {STLMesh}
214
+ */
215
+ static importSTL(buffer) {
216
+ const data = new Uint8Array(buffer);
217
+
218
+ // 检查是否为二进制STL
219
+ if (data.length < 84) {
220
+ throw new Error('Invalid STL file: too small');
221
+ }
222
+
223
+ // 读取三角形数量(二进制格式)
224
+ const nbTriangles = new Uint32Array(buffer.slice(80, 84))[0];
225
+ const expectedSize = 84 + nbTriangles * 50;
226
+
227
+ // 检查文件大小是否符合二进制STL格式
228
+ if (Math.abs(data.length - expectedSize) <= 2) { // 允许小的误差
229
+ // 二进制STL
230
+ // console.log(`检测到二进制STL文件,包含 ${nbTriangles} 个三角形`);
231
+ return this.importBinarySTL(buffer, nbTriangles);
232
+ } else {
233
+ // ASCII STL
234
+ // console.log('检测到ASCII STL文件');
235
+ const text = new TextDecoder('utf-8').decode(data);
236
+ return this.importAsciiSTL(text);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * @param {ArrayBuffer} buffer
242
+ * @returns {STLMesh}
243
+ */
244
+ static readArrayBuffer(buffer) {
245
+ return this.importSTL(buffer);
246
+ }
247
+ }
248
+
249
+ export default STLReader;
@@ -69,12 +69,12 @@ class FontManager {
69
69
  generateShapes(text, fontName = 'defaultFont', scale = [0.018, 0.018, 0.018]) {
70
70
  const scaleM4 = mat4.scale(mat4.create(), mat4.create(), scale);
71
71
 
72
- if (this.cacheFontMap[text]) {
73
- const newVertices = verticesApplyMat4(scaleM4, this.cacheFontMap[text].vertices);
72
+ if (this.cacheFontMap[fontName] && this.cacheFontMap[fontName][text]) {
73
+ const newVertices = verticesApplyMat4(scaleM4, this.cacheFontMap[fontName][text].vertices);
74
74
  return {
75
75
  vertices: newVertices,
76
- faces: this.cacheFontMap[text].faces,
77
- indexData: this.cacheFontPointMap[text]
76
+ faces: this.cacheFontMap[fontName][text].faces,
77
+ indexData: this.cacheFontPointMap[fontName][text]
78
78
  };
79
79
  }
80
80
 
@@ -85,8 +85,8 @@ class FontManager {
85
85
  const indexData = this.getPointsMapData(vfData.vertices);
86
86
  // console.log(indexData);
87
87
  const newVertices = verticesApplyMat4(scaleM4, vfData.vertices);
88
- this.cacheFontMap[text] = vfData;
89
- this.cacheFontPointMap[text] = indexData;
88
+ this.cacheFontMap[fontName][text] = vfData;
89
+ this.cacheFontPointMap[fontName][text] = indexData;
90
90
 
91
91
  return {
92
92
  vertices: newVertices,
package/src/vtkUtils.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import { vec3, mat4, mat3, quat } from 'gl-matrix';
2
- import { exportBinarySTL } from './exportSTL.js';
3
- import { writePLY } from "./exportPLY.js";
2
+ import { exportBinarySTL } from './model/wirter/exportSTL.js';
3
+ import { writePLY } from "./model/wirter/exportPLY.js";
4
4
  import { enCodeMesh } from './drcUtils.js';
5
- import { int8ArrayToBase64 } from "./utils.js";
5
+ import { int8ArrayToBase64, getFileExtension } from "./utils.js";
6
+ import PLYReader from './model/reader/PLYReader.js';
7
+ import STLReader from './model/reader/STLReader';
8
+ import OBJReader from './model/reader/OBJReader';
9
+ import DRCReader from './model/reader/DRCReader';
6
10
  /**
7
11
  * 计算屏幕坐标到三维坐标
8
12
  * @param {vtkRenderer} renderer vtkRenderer
@@ -1003,4 +1007,95 @@ export function getPolydataCenterOffset(vertices) {
1003
1007
  y: -(_boundBox.max[1] - _height / 2),
1004
1008
  z: -(_boundBox.max[2] - _surface / 2),
1005
1009
  };
1010
+ }
1011
+
1012
+ /**
1013
+ * 加载3D模型文件
1014
+ * @param {string | ArrayBuffer} data - 文件路径或文件数据
1015
+ * @param {string} name - 文件名(用于确定文件类型)
1016
+ * @param {string} [typeOverride] - 可选,手动指定的文件类型,将覆盖根据文件名推断的类型
1017
+ * @returns {Promise<{ vertices: MeshVF, faces: MeshVF }>}
1018
+ */
1019
+ export async function loadFile(data, name = "", typeOverride = "") {
1020
+ // 优先使用手动指定的类型,否则根据文件名推断
1021
+ const extension = typeOverride || getFileExtension(name);
1022
+
1023
+ if (typeof data === "string") {
1024
+ // 如果是字符串,作为URL获取文件
1025
+ const response = await fetch(data);
1026
+
1027
+ if (extension === 'obj') {
1028
+ // OBJ文件需要文本格式
1029
+ const text = await response.text();
1030
+ return loadData(text, extension);
1031
+ } else {
1032
+ // 其他格式获取二进制数据
1033
+ const buffer = await response.arrayBuffer();
1034
+ return loadData(buffer, extension);
1035
+ }
1036
+ } else {
1037
+ // 已经是二进制数据
1038
+ return loadData(data, extension);
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * 根据文件类型加载3D模型数据
1044
+ * @param {ArrayBuffer | string} file - 文件数据
1045
+ * @param {string} type - 文件类型: 'stl', 'obj', 'ply', 'drc'
1046
+ * @returns {{ vertices: MeshVF, faces: MeshVF }}
1047
+ */
1048
+ export function loadData(file, type = "ply") {
1049
+ let reader;
1050
+ // 根据文件类型选择对应的Reader
1051
+ switch(type) {
1052
+ case 'stl':
1053
+ reader = STLReader;
1054
+ break;
1055
+ case 'obj':
1056
+ reader = OBJReader;
1057
+ break;
1058
+ case 'ply':
1059
+ reader = PLYReader;
1060
+ break;
1061
+ case 'drc':
1062
+ reader = DRCReader;
1063
+ break;
1064
+ default:
1065
+ throw new Error(`Unsupported file type: ${type}`);
1066
+ }
1067
+
1068
+ // 根据reader类型调用不同的方法
1069
+ let result;
1070
+ if (type === 'obj') {
1071
+ // OBJReader需要字符串数据
1072
+ if (typeof file === 'string') {
1073
+ result = reader.readFile(file);
1074
+ } else {
1075
+ // 如果是ArrayBuffer,转换为字符串
1076
+ const decoder = new TextDecoder('utf-8');
1077
+ const text = decoder.decode(file);
1078
+ result = reader.readFile(text);
1079
+ }
1080
+
1081
+ // OBJReader可能返回单个mesh或mesh数组,这里取第一个
1082
+ let mesh;
1083
+ if (Array.isArray(result)) {
1084
+ mesh = result[0];
1085
+ } else {
1086
+ mesh = result;
1087
+ }
1088
+ return mesh
1089
+ } else {
1090
+ // 其他Reader使用ArrayBuffer数据
1091
+ if (typeof file === 'string') {
1092
+ throw new Error(`File type ${type} requires ArrayBuffer, but got string`);
1093
+ }
1094
+ result = reader.readArrayBuffer(file);
1095
+ // 确保所有reader返回的格式一致
1096
+ return {
1097
+ vertices: result.vertices,
1098
+ faces: result.faces
1099
+ };
1100
+ }
1006
1101
  }
package/tsconfig.json CHANGED
@@ -5,5 +5,5 @@
5
5
  "emitDeclarationOnly": true,
6
6
  "outDir": "types" // 可选,定义.d.ts文件输出目录
7
7
  },
8
- "include": ["src/*.js", "src/vtkUtils.js", "src/threeFont/font.js",] // 指定包含的 JavaScript 文件路径
8
+ "include": ["src/*.js", "src/vtkUtils.js", "src/threeFont/font.js", "src/model/wirter/exportPLY.js", "src/model/wirter/exportSTL.js",] // 指定包含的 JavaScript 文件路径
9
9
  }
@@ -1,3 +1,7 @@
1
+
2
+ import { vec3 } from 'gl-matrix';
3
+ import { mat4 } from 'gl-matrix';
4
+ import { mat3 } from 'gl-matrix';
1
5
  /**
2
6
  * 计算 w2l(word 2 local, word to local) 的变换矩阵
3
7
  * @param {vec3} center 中心点
@@ -276,6 +280,18 @@ export function getmatw2l(center: vec3, xaxis: vec3, yaxis: vec3, zaxis: vec3):
276
280
  * @returns { mat4 } 变换矩阵
277
281
  */
278
282
  export function getmatofl2w(center: vec3, xaxis: vec3, yaxis: vec3, zaxis: vec3): mat4;
283
+ /**
284
+ * 加载3D模型文件
285
+ * @param data - 文件路径(字符串)或文件数据(ArrayBuffer)
286
+ * @param name - 文件名,用于推断文件类型(可选)
287
+ * @param typeOverride - 手动指定的文件类型,将覆盖文件名推断的类型(可选)
288
+ * @returns Promise<{ vertices: Float32Array, faces: Uint32Array }> - 包含顶点和面片数据的网格对象
289
+ */
290
+ export declare function loadFile(
291
+ data: string | ArrayBuffer,
292
+ name?: string,
293
+ typeOverride?: string
294
+ ): Promise<{ vertices: Float32Array; faces: Uint32Array }>;
279
295
  export type Mesh = {
280
296
  /**
281
297
  * 点信息
@@ -327,6 +343,3 @@ export type Plane = {
327
343
  */
328
344
  normal: vec3;
329
345
  };
330
- import { vec3 } from 'gl-matrix';
331
- import { mat4 } from 'gl-matrix';
332
- import { mat3 } from 'gl-matrix';
File without changes
File without changes