• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / loaders.gl / 20352515932

18 Dec 2025 09:56PM UTC coverage: 35.115% (-28.4%) from 63.485%
20352515932

push

github

web-flow
feat(loader-utils): Export is-type helpers (#3258)

1188 of 1998 branches covered (59.46%)

Branch coverage included in aggregate %.

147 of 211 new or added lines in 13 files covered. (69.67%)

30011 existing lines in 424 files now uncovered.

37457 of 108056 relevant lines covered (34.66%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

18.55
/modules/pcd/src/lib/parse-pcd.ts
1
// loaders.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
// PCD Loader, adapted from THREE.js (MIT license)
1✔
6
// Description: A loader for PCD ascii and binary files.
1✔
7
// Limitations: Compressed binary files are not supported.
1✔
8
//
1✔
9
// Attributions per original THREE.js source file:
1✔
10
// @author Filipe Caixeta / http://filipecaixeta.com.br
1✔
11
// @author Mugen87 / https://github.com/Mugen87
1✔
12

1✔
13
import {MeshAttribute, MeshAttributes} from '@loaders.gl/schema';
1✔
14
import {getMeshBoundingBox} from '@loaders.gl/schema-utils';
1✔
15
import {decompressLZF} from './decompress-lzf';
1✔
16
import {getPCDSchema} from './get-pcd-schema';
1✔
17
import type {PCDHeader, PCDMesh} from './pcd-types';
1✔
18

1✔
19
type MeshHeader = {
1✔
20
  vertexCount: number;
1✔
21
  boundingBox: [[number, number, number], [number, number, number]];
1✔
22
};
1✔
23

1✔
24
type NormalizedAttributes = {
1✔
25
  POSITION: {
1✔
26
    value: Float32Array;
1✔
27
    size: number;
1✔
28
  };
1✔
29
  NORMAL?: {
1✔
30
    value: Float32Array;
1✔
31
    size: number;
1✔
32
  };
1✔
33
  COLOR_0?: {
1✔
34
    value: Uint8Array;
1✔
35
    size: number;
1✔
36
  };
1✔
37
};
1✔
38

1✔
39
type HeaderAttributes = {
1✔
40
  [attributeName: string]: number[];
1✔
41
};
1✔
42

1✔
43
const LITTLE_ENDIAN: boolean = true;
1✔
44

1✔
45
/**
1✔
46
 *
1✔
47
 * @param data
1✔
48
 * @returns
1✔
49
 */
1✔
50
export function parsePCD(data: ArrayBufferLike): PCDMesh {
1✔
UNCOV
51
  // parse header (always ascii format)
×
UNCOV
52
  const textData = new TextDecoder().decode(data);
×
UNCOV
53
  const pcdHeader = parsePCDHeader(textData);
×
UNCOV
54

×
UNCOV
55
  let attributes: any = {};
×
UNCOV
56

×
UNCOV
57
  // parse data
×
UNCOV
58
  switch (pcdHeader.data) {
×
UNCOV
59
    case 'ascii':
×
UNCOV
60
      attributes = parsePCDASCII(pcdHeader, textData);
×
UNCOV
61
      break;
×
UNCOV
62

×
UNCOV
63
    case 'binary':
×
UNCOV
64
      attributes = parsePCDBinary(pcdHeader, data);
×
UNCOV
65
      break;
×
UNCOV
66

×
UNCOV
67
    case 'binary_compressed':
×
68
      attributes = parsePCDBinaryCompressed(pcdHeader, data);
×
69
      break;
×
UNCOV
70

×
UNCOV
71
    default:
×
UNCOV
72
      throw new Error(`PCD: ${pcdHeader.data} files are not supported`);
×
UNCOV
73
  }
×
UNCOV
74

×
UNCOV
75
  attributes = getMeshAttributes(attributes);
×
UNCOV
76

×
UNCOV
77
  const header = getMeshHeader(pcdHeader, attributes);
×
UNCOV
78

×
UNCOV
79
  const schemaMetadata = Object.fromEntries([
×
UNCOV
80
    ['topology', 'point-list'],
×
UNCOV
81
    ['mode', '0'],
×
UNCOV
82
    ['boundingBox', JSON.stringify(header.boundingBox)]
×
UNCOV
83
  ]);
×
UNCOV
84

×
UNCOV
85
  const schema = getPCDSchema(pcdHeader, schemaMetadata);
×
UNCOV
86

×
UNCOV
87
  return {
×
UNCOV
88
    loader: 'pcd',
×
UNCOV
89
    loaderData: pcdHeader,
×
UNCOV
90
    header,
×
UNCOV
91
    schema,
×
UNCOV
92
    topology: 'point-list',
×
UNCOV
93
    mode: 0, // POINTS (deprecated)
×
UNCOV
94
    attributes
×
UNCOV
95
  };
×
UNCOV
96
}
×
97

1✔
98
// Create a header that contains common data for PointCloud category loaders
1✔
UNCOV
99
function getMeshHeader(pcdHeader: PCDHeader, attributes: NormalizedAttributes): MeshHeader {
×
UNCOV
100
  if (typeof pcdHeader.width === 'number' && typeof pcdHeader.height === 'number') {
×
UNCOV
101
    const pointCount = pcdHeader.width * pcdHeader.height; // Supports "organized" point sets
×
UNCOV
102
    return {
×
UNCOV
103
      vertexCount: pointCount,
×
UNCOV
104
      boundingBox: getMeshBoundingBox(attributes)
×
UNCOV
105
    };
×
UNCOV
106
  }
×
107
  return {
×
108
    vertexCount: pcdHeader.vertexCount,
×
109
    boundingBox: pcdHeader.boundingBox
×
110
  };
×
111
}
×
112

1✔
113
/**
1✔
114
 * @param attributes
1✔
115
 * @returns Normalized attributes
1✔
116
 */
1✔
UNCOV
117
function getMeshAttributes(attributes: HeaderAttributes): {[attributeName: string]: MeshAttribute} {
×
UNCOV
118
  const normalizedAttributes: MeshAttributes = {
×
UNCOV
119
    POSITION: {
×
UNCOV
120
      // Binary PCD is only 32 bit
×
UNCOV
121
      value: new Float32Array(attributes.position),
×
UNCOV
122
      size: 3
×
UNCOV
123
    }
×
UNCOV
124
  };
×
UNCOV
125

×
UNCOV
126
  if (attributes.normal && attributes.normal.length > 0) {
×
127
    normalizedAttributes.NORMAL = {
×
128
      value: new Float32Array(attributes.normal),
×
129
      size: 3
×
130
    };
×
131
  }
×
UNCOV
132

×
UNCOV
133
  if (attributes.color && attributes.color.length > 0) {
×
UNCOV
134
    // TODO - RGBA
×
UNCOV
135
    normalizedAttributes.COLOR_0 = {
×
UNCOV
136
      value: new Uint8Array(attributes.color),
×
UNCOV
137
      size: 3
×
UNCOV
138
    };
×
UNCOV
139
  }
×
UNCOV
140

×
UNCOV
141
  if (attributes.intensity && attributes.intensity.length > 0) {
×
142
    // TODO - RGBA
×
143
    normalizedAttributes.COLOR_0 = {
×
144
      value: new Uint8Array(attributes.color),
×
145
      size: 3
×
146
    };
×
147
  }
×
UNCOV
148

×
UNCOV
149
  if (attributes.label && attributes.label.length > 0) {
×
150
    // TODO - RGBA
×
151
    normalizedAttributes.COLOR_0 = {
×
152
      value: new Uint8Array(attributes.label),
×
153
      size: 3
×
154
    };
×
155
  }
×
UNCOV
156

×
UNCOV
157
  return normalizedAttributes;
×
UNCOV
158
}
×
159

1✔
160
/**
1✔
161
 * Incoming data parsing
1✔
162
 * @param data
1✔
163
 * @returns Header
1✔
164
 */
1✔
165
/* eslint-disable complexity, max-statements */
1✔
UNCOV
166
function parsePCDHeader(data: string): PCDHeader {
×
UNCOV
167
  const result1 = data.search(/[\r\n]DATA\s(\S*)\s/i);
×
UNCOV
168
  const result2 = /[\r\n]DATA\s(\S*)\s/i.exec(data.substr(result1 - 1));
×
UNCOV
169

×
UNCOV
170
  const pcdHeader: any = {};
×
UNCOV
171
  pcdHeader.data = result2 && result2[1];
×
UNCOV
172
  if (result2 !== null) {
×
UNCOV
173
    pcdHeader.headerLen = (result2 && result2[0].length) + result1;
×
UNCOV
174
  }
×
UNCOV
175
  pcdHeader.str = data.substr(0, pcdHeader.headerLen);
×
UNCOV
176

×
UNCOV
177
  // remove comments
×
UNCOV
178

×
UNCOV
179
  pcdHeader.str = pcdHeader.str.replace(/\#.*/gi, '');
×
UNCOV
180

×
UNCOV
181
  // parse
×
UNCOV
182

×
UNCOV
183
  pcdHeader.version = /VERSION (.*)/i.exec(pcdHeader.str);
×
UNCOV
184
  pcdHeader.fields = /FIELDS (.*)/i.exec(pcdHeader.str);
×
UNCOV
185
  pcdHeader.size = /SIZE (.*)/i.exec(pcdHeader.str);
×
UNCOV
186
  pcdHeader.type = /TYPE (.*)/i.exec(pcdHeader.str);
×
UNCOV
187
  pcdHeader.count = /COUNT (.*)/i.exec(pcdHeader.str);
×
UNCOV
188
  pcdHeader.width = /WIDTH (.*)/i.exec(pcdHeader.str);
×
UNCOV
189
  pcdHeader.height = /HEIGHT (.*)/i.exec(pcdHeader.str);
×
UNCOV
190
  pcdHeader.viewpoint = /VIEWPOINT (.*)/i.exec(pcdHeader.str);
×
UNCOV
191
  pcdHeader.points = /POINTS (.*)/i.exec(pcdHeader.str);
×
UNCOV
192

×
UNCOV
193
  // evaluate
×
UNCOV
194

×
UNCOV
195
  if (pcdHeader.version !== null) {
×
UNCOV
196
    pcdHeader.version = parseFloat(pcdHeader.version[1]);
×
UNCOV
197
  }
×
UNCOV
198

×
UNCOV
199
  if (pcdHeader.fields !== null) {
×
UNCOV
200
    pcdHeader.fields = pcdHeader.fields[1].split(' ');
×
UNCOV
201
  }
×
UNCOV
202

×
UNCOV
203
  if (pcdHeader.type !== null) {
×
UNCOV
204
    pcdHeader.type = pcdHeader.type[1].split(' ');
×
UNCOV
205
  }
×
UNCOV
206

×
UNCOV
207
  if (pcdHeader.width !== null) {
×
UNCOV
208
    pcdHeader.width = parseInt(pcdHeader.width[1], 10);
×
UNCOV
209
  }
×
UNCOV
210

×
UNCOV
211
  if (pcdHeader.height !== null) {
×
UNCOV
212
    pcdHeader.height = parseInt(pcdHeader.height[1], 10);
×
UNCOV
213
  }
×
UNCOV
214

×
UNCOV
215
  if (pcdHeader.viewpoint !== null) {
×
UNCOV
216
    pcdHeader.viewpoint = pcdHeader.viewpoint[1];
×
UNCOV
217
  }
×
UNCOV
218

×
UNCOV
219
  if (pcdHeader.points !== null) {
×
UNCOV
220
    pcdHeader.points = parseInt(pcdHeader.points[1], 10);
×
UNCOV
221
  }
×
UNCOV
222

×
UNCOV
223
  if (
×
UNCOV
224
    pcdHeader.points === null &&
×
UNCOV
225
    typeof pcdHeader.width === 'number' &&
×
226
    typeof pcdHeader.height === 'number'
×
UNCOV
227
  ) {
×
228
    pcdHeader.points = pcdHeader.width * pcdHeader.height;
×
229
  }
×
UNCOV
230

×
UNCOV
231
  if (pcdHeader.size !== null) {
×
UNCOV
232
    pcdHeader.size = pcdHeader.size[1].split(' ').map((x) => parseInt(x, 10));
×
UNCOV
233
  }
×
UNCOV
234

×
UNCOV
235
  if (pcdHeader.count !== null) {
×
UNCOV
236
    pcdHeader.count = pcdHeader.count[1].split(' ').map((x) => parseInt(x, 10));
×
UNCOV
237
  } else {
×
UNCOV
238
    pcdHeader.count = [];
×
UNCOV
239
    if (pcdHeader.fields !== null) {
×
240
      for (let i = 0; i < pcdHeader.fields.length; i++) {
×
241
        pcdHeader.count.push(1);
×
242
      }
×
243
    }
×
UNCOV
244
  }
×
UNCOV
245

×
UNCOV
246
  pcdHeader.offset = {};
×
UNCOV
247

×
UNCOV
248
  let sizeSum = 0;
×
UNCOV
249
  if (pcdHeader.fields !== null && pcdHeader.size !== null) {
×
UNCOV
250
    for (let i = 0; i < pcdHeader.fields.length; i++) {
×
UNCOV
251
      if (pcdHeader.data === 'ascii') {
×
UNCOV
252
        pcdHeader.offset[pcdHeader.fields[i]] = i;
×
UNCOV
253
      } else {
×
UNCOV
254
        pcdHeader.offset[pcdHeader.fields[i]] = sizeSum;
×
UNCOV
255
        sizeSum += pcdHeader.size[i];
×
UNCOV
256
      }
×
UNCOV
257
    }
×
UNCOV
258
  }
×
UNCOV
259

×
UNCOV
260
  // for binary only
×
UNCOV
261
  pcdHeader.rowSize = sizeSum;
×
UNCOV
262

×
UNCOV
263
  return pcdHeader;
×
UNCOV
264
}
×
265

1✔
266
/**
1✔
267
 * @param pcdHeader
1✔
268
 * @param textData
1✔
269
 * @returns [attributes]
1✔
270
 */
1✔
271
// eslint-enable-next-line complexity, max-statements
1✔
UNCOV
272
function parsePCDASCII(pcdHeader: PCDHeader, textData: string): HeaderAttributes {
×
UNCOV
273
  const position: number[] = [];
×
UNCOV
274
  const normal: number[] = [];
×
UNCOV
275
  const color: number[] = [];
×
UNCOV
276
  const intensity: number[] = [];
×
UNCOV
277
  const label: number[] = [];
×
UNCOV
278

×
UNCOV
279
  const offset = pcdHeader.offset;
×
UNCOV
280
  const pcdData = textData.substr(pcdHeader.headerLen);
×
UNCOV
281
  const lines = pcdData.split('\n');
×
UNCOV
282

×
UNCOV
283
  for (let i = 0; i < lines.length; i++) {
×
UNCOV
284
    if (lines[i] !== '') {
×
UNCOV
285
      const line = lines[i].split(' ');
×
UNCOV
286

×
UNCOV
287
      if (offset.x !== undefined) {
×
UNCOV
288
        position.push(parseFloat(line[offset.x]));
×
UNCOV
289
        position.push(parseFloat(line[offset.y]));
×
UNCOV
290
        position.push(parseFloat(line[offset.z]));
×
UNCOV
291
      }
×
UNCOV
292

×
UNCOV
293
      if (offset.rgb !== undefined) {
×
UNCOV
294
        const floatValue = parseFloat(line[offset.rgb]);
×
UNCOV
295
        const binaryColor = new Float32Array([floatValue]);
×
UNCOV
296
        const dataview = new DataView(binaryColor.buffer, 0);
×
UNCOV
297
        color.push(dataview.getUint8(0));
×
UNCOV
298
        color.push(dataview.getUint8(1));
×
UNCOV
299
        color.push(dataview.getUint8(2));
×
UNCOV
300
        // TODO - handle alpha channel / RGBA?
×
UNCOV
301
      }
×
UNCOV
302

×
UNCOV
303
      if (offset.normal_x !== undefined) {
×
304
        normal.push(parseFloat(line[offset.normal_x]));
×
305
        normal.push(parseFloat(line[offset.normal_y]));
×
306
        normal.push(parseFloat(line[offset.normal_z]));
×
307
      }
×
UNCOV
308

×
UNCOV
309
      if (offset.intensity !== undefined) {
×
310
        intensity.push(parseFloat(line[offset.intensity]));
×
311
      }
×
UNCOV
312

×
UNCOV
313
      if (offset.label !== undefined) {
×
314
        label.push(parseInt(line[offset.label]));
×
315
      }
×
UNCOV
316
    }
×
UNCOV
317
  }
×
UNCOV
318

×
UNCOV
319
  return {position, normal, color};
×
UNCOV
320
}
×
321

1✔
322
/**
1✔
323
 * @param pcdHeader
1✔
324
 * @param data
1✔
325
 * @returns [attributes]
1✔
326
 */
1✔
UNCOV
327
function parsePCDBinary(pcdHeader: PCDHeader, data: ArrayBufferLike): HeaderAttributes {
×
UNCOV
328
  const position: number[] = [];
×
UNCOV
329
  const normal: number[] = [];
×
UNCOV
330
  const color: number[] = [];
×
UNCOV
331
  const intensity: number[] = [];
×
UNCOV
332
  const label: number[] = [];
×
UNCOV
333

×
UNCOV
334
  const dataview = new DataView(data, pcdHeader.headerLen);
×
UNCOV
335
  const offset = pcdHeader.offset;
×
UNCOV
336

×
UNCOV
337
  for (let i = 0, row = 0; i < pcdHeader.points; i++, row += pcdHeader.rowSize) {
×
UNCOV
338
    if (offset.x !== undefined) {
×
UNCOV
339
      position.push(dataview.getFloat32(row + offset.x, LITTLE_ENDIAN));
×
UNCOV
340
      position.push(dataview.getFloat32(row + offset.y, LITTLE_ENDIAN));
×
UNCOV
341
      position.push(dataview.getFloat32(row + offset.z, LITTLE_ENDIAN));
×
UNCOV
342
    }
×
UNCOV
343

×
UNCOV
344
    if (offset.rgb !== undefined) {
×
345
      color.push(dataview.getUint8(row + offset.rgb + 0));
×
346
      color.push(dataview.getUint8(row + offset.rgb + 1));
×
347
      color.push(dataview.getUint8(row + offset.rgb + 2));
×
348
    }
×
UNCOV
349

×
UNCOV
350
    if (offset.normal_x !== undefined) {
×
351
      normal.push(dataview.getFloat32(row + offset.normal_x, LITTLE_ENDIAN));
×
352
      normal.push(dataview.getFloat32(row + offset.normal_y, LITTLE_ENDIAN));
×
353
      normal.push(dataview.getFloat32(row + offset.normal_z, LITTLE_ENDIAN));
×
354
    }
×
UNCOV
355

×
UNCOV
356
    if (offset.intensity !== undefined) {
×
357
      intensity.push(dataview.getFloat32(row + offset.intensity, LITTLE_ENDIAN));
×
358
    }
×
UNCOV
359

×
UNCOV
360
    if (offset.label !== undefined) {
×
361
      label.push(dataview.getInt32(row + offset.label, LITTLE_ENDIAN));
×
362
    }
×
UNCOV
363
  }
×
UNCOV
364

×
UNCOV
365
  return {position, normal, color, intensity, label};
×
UNCOV
366
}
×
367

1✔
368
/** Parse compressed PCD data in in binary_compressed form ( https://pointclouds.org/documentation/tutorials/pcd_file_format.html)
1✔
369
 * from https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/PCDLoader.js
1✔
370
 * @license MIT (http://opensource.org/licenses/MIT)
1✔
371
 * @param pcdHeader
1✔
372
 * @param data
1✔
373
 * @returns [attributes]
1✔
374
 */
1✔
375
// eslint-enable-next-line complexity, max-statements
1✔
376
function parsePCDBinaryCompressed(pcdHeader: PCDHeader, data: ArrayBufferLike): HeaderAttributes {
×
377
  const position: number[] = [];
×
378
  const normal: number[] = [];
×
379
  const color: number[] = [];
×
380
  const intensity: number[] = [];
×
381
  const label: number[] = [];
×
382

×
383
  const sizes = new Uint32Array(data.slice(pcdHeader.headerLen, pcdHeader.headerLen + 8));
×
384
  const compressedSize = sizes[0];
×
385
  const decompressedSize = sizes[1];
×
386
  const decompressed = decompressLZF(
×
387
    new Uint8Array(data, pcdHeader.headerLen + 8, compressedSize),
×
388
    decompressedSize
×
389
  );
×
390
  const dataview = new DataView(decompressed.buffer);
×
391

×
392
  const offset = pcdHeader.offset;
×
393

×
394
  for (let i = 0; i < pcdHeader.points; i++) {
×
395
    if (offset.x !== undefined) {
×
396
      position.push(
×
397
        dataview.getFloat32(pcdHeader.points * offset.x + pcdHeader.size[0] * i, LITTLE_ENDIAN)
×
398
      );
×
399
      position.push(
×
400
        dataview.getFloat32(pcdHeader.points * offset.y + pcdHeader.size[1] * i, LITTLE_ENDIAN)
×
401
      );
×
402
      position.push(
×
403
        dataview.getFloat32(pcdHeader.points * offset.z + pcdHeader.size[2] * i, LITTLE_ENDIAN)
×
404
      );
×
405
    }
×
406

×
407
    if (offset.rgb !== undefined) {
×
408
      color.push(
×
409
        dataview.getUint8(pcdHeader.points * offset.rgb + pcdHeader.size[3] * i + 0) / 255.0
×
410
      );
×
411
      color.push(
×
412
        dataview.getUint8(pcdHeader.points * offset.rgb + pcdHeader.size[3] * i + 1) / 255.0
×
413
      );
×
414
      color.push(
×
415
        dataview.getUint8(pcdHeader.points * offset.rgb + pcdHeader.size[3] * i + 2) / 255.0
×
416
      );
×
417
    }
×
418

×
419
    if (offset.normal_x !== undefined) {
×
420
      normal.push(
×
421
        dataview.getFloat32(
×
422
          pcdHeader.points * offset.normal_x + pcdHeader.size[4] * i,
×
423
          LITTLE_ENDIAN
×
424
        )
×
425
      );
×
426
      normal.push(
×
427
        dataview.getFloat32(
×
428
          pcdHeader.points * offset.normal_y + pcdHeader.size[5] * i,
×
429
          LITTLE_ENDIAN
×
430
        )
×
431
      );
×
432
      normal.push(
×
433
        dataview.getFloat32(
×
434
          pcdHeader.points * offset.normal_z + pcdHeader.size[6] * i,
×
435
          LITTLE_ENDIAN
×
436
        )
×
437
      );
×
438
    }
×
439

×
440
    if (offset.intensity !== undefined) {
×
441
      const intensityIndex = pcdHeader.fields.indexOf('intensity');
×
442
      intensity.push(
×
443
        dataview.getFloat32(
×
444
          pcdHeader.points * offset.intensity + pcdHeader.size[intensityIndex] * i,
×
445
          LITTLE_ENDIAN
×
446
        )
×
447
      );
×
448
    }
×
449

×
450
    if (offset.label !== undefined) {
×
451
      const labelIndex = pcdHeader.fields.indexOf('label');
×
452
      label.push(
×
453
        dataview.getInt32(
×
454
          pcdHeader.points * offset.label + pcdHeader.size[labelIndex] * i,
×
455
          LITTLE_ENDIAN
×
456
        )
×
457
      );
×
458
    }
×
459
  }
×
460

×
461
  return {
×
462
    position,
×
463
    normal,
×
464
    color,
×
465
    intensity,
×
466
    label
×
467
  };
×
468
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc