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

visgl / loaders.gl / 25285056678

03 May 2026 04:52PM UTC coverage: 60.005% (+0.3%) from 59.717%
25285056678

push

github

web-flow
feat: Add COPC and potree sources (#3413)

12967 of 23938 branches covered (54.17%)

Branch coverage included in aggregate %.

875 of 1287 new or added lines in 33 files covered. (67.99%)

5 existing lines in 5 files now uncovered.

26843 of 42406 relevant lines covered (63.3%)

14662.33 hits per line

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

91.57
/modules/obj/src/obj-loader-with-parser.ts
1
// loaders.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {
6
  concatenateArrayBuffersAsync,
7
  makeLineIterator,
8
  makeTextDecoderIterator,
9
  type Loader,
10
  type LoaderOptions,
11
  type LoaderWithParser
12
} from '@loaders.gl/loader-utils';
13
import type {ArrowTableBatch, Mesh, MeshArrowTable} from '@loaders.gl/schema';
14
import {convertMeshToTable, convertTableToMesh, getMeshBoundingBox} from '@loaders.gl/schema-utils';
15
import {getOBJSchema} from './lib/get-obj-schema';
16
import {parseOBJ} from './lib/parse-obj';
17
import {OBJWorkerLoader as OBJWorkerLoaderMetadata} from './obj-loader';
18
import {OBJLoader as OBJLoaderMetadata} from './obj-loader';
19

20
const {preload: _OBJWorkerLoaderPreload, ...OBJWorkerLoaderMetadataWithoutPreload} =
21
  OBJWorkerLoaderMetadata;
5✔
22
const {preload: _OBJLoaderPreload, ...OBJLoaderMetadataWithoutPreload} = OBJLoaderMetadata;
5✔
23

24
export type OBJLoaderOptions = LoaderOptions & {
25
  obj?: {
26
    /** Output shape. Defaults to a legacy Mesh object. */
27
    shape?: 'mesh' | 'arrow-table';
28
    /** Treat OBJ vertex records as a point cloud and stream `v` rows in batches. */
29
    pointCloud?: boolean;
30
    /** Override the URL to the worker bundle (by default loads from unpkg.com) */
31
    workerUrl?: string;
32
  };
33
};
34

35
type OBJMeshBatch = {
36
  /** Batch shape for legacy Mesh output. */
37
  shape: 'mesh';
38
  /** Indicates a parsed data batch. */
39
  batchType: 'data';
40
  /** Parsed OBJ mesh. */
41
  data: Mesh;
42
  /** Number of vertices in the batch. */
43
  length: number;
44
};
45

46
type OBJParsedBatch = OBJMeshBatch | ArrowTableBatch;
47

48
function convertOBJMesh(mesh: Mesh, options?: OBJLoaderOptions): Mesh | MeshArrowTable {
49
  const table = convertMeshToTable(mesh, 'arrow-table');
10✔
50
  return options?.obj?.shape === 'arrow-table' ? table : convertTableToMesh(table);
10✔
51
}
52

53
/**
54
 * Worker loader for the OBJ geometry format
55
 */
56
export const OBJWorkerLoaderWithParser = {
5✔
57
  ...OBJWorkerLoaderMetadataWithoutPreload
58
} as const satisfies Loader<Mesh | MeshArrowTable, never, OBJLoaderOptions>;
59

60
// OBJLoaderWithParser
61

62
/**
63
 * Loader for the OBJ geometry format
64
 */
65
export const OBJLoaderWithParser = {
5✔
66
  ...OBJLoaderMetadataWithoutPreload,
67
  parse: async (arrayBuffer: ArrayBuffer, options?: OBJLoaderOptions) =>
68
    convertOBJMesh(parseOBJ(new TextDecoder().decode(arrayBuffer), options), options),
×
69
  parseTextSync: (text: string, options?: OBJLoaderOptions) =>
70
    convertOBJMesh(parseOBJ(text, options), options),
10✔
71
  parseInBatches: async function* (
72
    arrayBuffer:
73
      | AsyncIterable<ArrayBufferLike | ArrayBufferView>
74
      | Iterable<ArrayBufferLike | ArrayBufferView>,
75
    options
76
  ): AsyncIterable<OBJParsedBatch> {
77
    const batchSize = getNumericBatchSize(options);
8✔
78
    if (options?.obj?.pointCloud) {
8✔
79
      const textIterator = makeTextDecoderIterator(arrayBuffer as AsyncIterable<ArrayBuffer>);
2✔
80
      const lineIterator = makeLineIterator(textIterator);
2✔
81
      yield* parseOBJPointCloudLinesInBatches(lineIterator, options, batchSize);
2✔
82
      return;
2✔
83
    }
84

85
    const data = await concatenateArrayBuffersAsync(arrayBuffer);
6✔
86
    const text = new TextDecoder().decode(data);
6✔
87
    if (hasOBJGeometryRecords(text)) {
6✔
88
      yield makeOBJBatch(parseOBJ(text, options), options);
4✔
89
      return;
4✔
90
    }
91

92
    yield* parseOBJPointCloudInBatches(text, options, batchSize);
2✔
93
  }
94
} as const satisfies LoaderWithParser<Mesh | MeshArrowTable, OBJParsedBatch, OBJLoaderOptions>;
95

96
/** Returns a numeric batch size when batching has an explicit row count. */
97
function getNumericBatchSize(options?: OBJLoaderOptions): number | undefined {
98
  const batchSize = options?.batchSize ?? options?.core?.batchSize;
8✔
99
  return typeof batchSize === 'number' ? batchSize : undefined;
8✔
100
}
101

102
function hasOBJGeometryRecords(text: string): boolean {
103
  return text.split(/\r?\n/).some(line => {
6✔
104
    const trimmedLine = line.trimStart();
5,032✔
105
    return trimmedLine.startsWith('f ') || trimmedLine.startsWith('l ');
5,032✔
106
  });
107
}
108

109
function* parseOBJPointCloudInBatches(
110
  text: string,
111
  options?: OBJLoaderOptions,
112
  batchSize?: number
113
): Iterable<OBJParsedBatch> {
114
  const vertexLines = text.split(/\r?\n/).filter(line => line.trimStart().startsWith('v '));
10✔
115
  yield* makeOBJPointCloudBatches(vertexLines, options, batchSize);
2✔
116
}
117

118
async function* parseOBJPointCloudLinesInBatches(
119
  lines: AsyncIterable<string>,
120
  options?: OBJLoaderOptions,
121
  batchSize?: number
122
): AsyncIterable<OBJParsedBatch> {
123
  const normalizedBatchSize = batchSize || 1000;
2!
124
  let vertexLines: string[] = [];
2✔
125

126
  for await (const line of lines) {
2✔
127
    if (!line.trimStart().startsWith('v ')) {
10✔
128
      continue;
2✔
129
    }
130
    vertexLines.push(line);
8✔
131
    if (vertexLines.length >= normalizedBatchSize) {
8✔
132
      yield makeOBJBatch(parseOBJPointCloudMesh(vertexLines), options);
4✔
133
      vertexLines = [];
4✔
134
    }
135
  }
136

137
  if (vertexLines.length > 0) {
2!
NEW
138
    yield makeOBJBatch(parseOBJPointCloudMesh(vertexLines), options);
×
139
  }
140
}
141

142
function* makeOBJPointCloudBatches(
143
  vertexLines: string[],
144
  options?: OBJLoaderOptions,
145
  batchSize?: number
146
): Iterable<OBJParsedBatch> {
147
  const normalizedBatchSize = batchSize || vertexLines.length || 1;
2!
148

149
  for (let rowIndex = 0; rowIndex < vertexLines.length; rowIndex += normalizedBatchSize) {
2✔
150
    const mesh = parseOBJPointCloudMesh(
4✔
151
      vertexLines.slice(rowIndex, rowIndex + normalizedBatchSize)
152
    );
153
    yield makeOBJBatch(mesh, options);
4✔
154
  }
155
}
156

157
/** Builds a point-list mesh directly from OBJ `v` records. */
158
function parseOBJPointCloudMesh(vertexLines: string[]): Mesh {
159
  const positions = new Float32Array(vertexLines.length * 3);
8✔
160

161
  for (let vertexIndex = 0; vertexIndex < vertexLines.length; vertexIndex++) {
8✔
162
    const values = vertexLines[vertexIndex].trim().split(/\s+/);
16✔
163
    positions[vertexIndex * 3 + 0] = Number(values[1]);
16✔
164
    positions[vertexIndex * 3 + 1] = Number(values[2]);
16✔
165
    positions[vertexIndex * 3 + 2] = Number(values[3]);
16✔
166
  }
167

168
  const attributes = {
8✔
169
    POSITION: {
170
      value: positions,
171
      size: 3
172
    }
173
  };
174
  const boundingBox = getMeshBoundingBox(attributes);
8✔
175

176
  return {
8✔
177
    loaderData: {
178
      header: {}
179
    },
180
    schema: getOBJSchema(attributes, {
181
      mode: 0,
182
      boundingBox
183
    }),
184
    header: {
185
      vertexCount: vertexLines.length,
186
      boundingBox
187
    },
188
    mode: 0,
189
    topology: 'point-list',
190
    attributes
191
  };
192
}
193

194
function makeOBJBatch(mesh: Mesh, options?: OBJLoaderOptions): OBJParsedBatch {
195
  const table = convertMeshToTable(mesh, 'arrow-table');
12✔
196
  if (options?.obj?.shape !== 'arrow-table') {
12✔
197
    const convertedMesh = convertTableToMesh(table);
2✔
198
    return {
2✔
199
      shape: 'mesh',
200
      batchType: 'data',
201
      data: convertedMesh,
202
      length: convertedMesh.header?.vertexCount || 0
2!
203
    };
204
  }
205

206
  return {
10✔
207
    shape: 'arrow-table',
208
    batchType: 'data',
209
    schema: table.schema,
210
    data: table.data,
211
    length: table.data.numRows
212
  };
213
}
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