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

visgl / loaders.gl / 24247634071

10 Apr 2026 02:21PM UTC coverage: 55.037% (+0.08%) from 54.956%
24247634071

push

github

web-flow
feat: Add mesh writers, update home page (#3360)

8690 of 16989 branches covered (51.15%)

Branch coverage included in aggregate %.

255 of 412 new or added lines in 6 files covered. (61.89%)

18209 of 31885 relevant lines covered (57.11%)

5230.93 hits per line

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

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

5
import type {WriterOptions, WriterWithEncoder} from '@loaders.gl/loader-utils';
6
import type {Mesh, MeshArrowTable, MeshAttribute} from '@loaders.gl/schema';
7
import {convertMeshToTable, convertTableToMesh} from '@loaders.gl/schema-utils';
8
import {OBJFormat} from './obj-format';
9

10
// __VERSION__ is injected by babel-plugin-version-inline
11
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
12
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
6!
13

14
/** Options for `OBJWriter`. */
15
export type OBJWriterOptions = WriterOptions & {
16
  /** Reserved for future OBJ writer options. */
17
  obj?: Record<string, never>;
18
};
19

20
/**
21
 * Writer for the OBJ geometry format.
22
 */
23
export const OBJWriter = {
6✔
24
  ...OBJFormat,
25
  dataType: null as unknown as Mesh | MeshArrowTable,
26
  batchType: null as never,
27
  version: VERSION,
28
  options: {
29
    obj: {}
30
  },
31
  text: true,
32
  encode: async (data, options) => encodeOBJSync(data, options),
2✔
33
  encodeSync: encodeOBJSync,
34
  encodeTextSync: encodeOBJ
35
} as const satisfies WriterWithEncoder<Mesh | MeshArrowTable, never, OBJWriterOptions>;
36

37
/** Encode mesh category data as OBJ bytes. */
38
function encodeOBJSync(data: Mesh | MeshArrowTable, options?: OBJWriterOptions): ArrayBuffer {
39
  const text = encodeOBJ(data, options);
2✔
40
  return new TextEncoder().encode(text).buffer;
2✔
41
}
42

43
/** Encode mesh category data as OBJ text. */
44
function encodeOBJ(data: Mesh | MeshArrowTable, options?: OBJWriterOptions): string {
45
  const mesh = convertTableToMesh(normalizeMeshArrowTable(data));
2✔
46
  const positionAttribute = getRequiredAttribute(mesh, 'POSITION');
2✔
47
  const normalAttribute = mesh.attributes.NORMAL;
2✔
48
  const textureCoordinateAttribute = mesh.attributes.TEXCOORD_0;
2✔
49
  const colorAttribute = mesh.attributes.COLOR_0;
2✔
50
  const vertexCount = positionAttribute.value.length / positionAttribute.size;
2✔
51
  const lines: string[] = ['# loaders.gl OBJ'];
2✔
52

53
  for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
2✔
54
    const vertex = [
6✔
55
      getComponent(positionAttribute, vertexIndex, 0),
56
      getComponent(positionAttribute, vertexIndex, 1),
57
      getComponent(positionAttribute, vertexIndex, 2)
58
    ];
59

60
    if (colorAttribute) {
6!
NEW
61
      vertex.push(
×
62
        getColorComponent(colorAttribute, vertexIndex, 0),
63
        getColorComponent(colorAttribute, vertexIndex, 1),
64
        getColorComponent(colorAttribute, vertexIndex, 2)
65
      );
66
    }
67

68
    lines.push(`v ${vertex.join(' ')}`);
6✔
69
  }
70

71
  if (textureCoordinateAttribute) {
2!
72
    for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
2✔
73
      lines.push(
6✔
74
        `vt ${getComponent(textureCoordinateAttribute, vertexIndex, 0)} ${getComponent(
75
          textureCoordinateAttribute,
76
          vertexIndex,
77
          1
78
        )}`
79
      );
80
    }
81
  }
82

83
  if (normalAttribute) {
2!
84
    for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
2✔
85
      lines.push(
6✔
86
        `vn ${getComponent(normalAttribute, vertexIndex, 0)} ${getComponent(
87
          normalAttribute,
88
          vertexIndex,
89
          1
90
        )} ${getComponent(normalAttribute, vertexIndex, 2)}`
91
      );
92
    }
93
  }
94

95
  const triangleIndices = getTriangleIndices(mesh, vertexCount);
2✔
96
  for (let triangleIndex = 0; triangleIndex < triangleIndices.length; triangleIndex += 3) {
2✔
97
    const face = [
2✔
98
      getFaceVertex(
99
        triangleIndices[triangleIndex],
100
        Boolean(textureCoordinateAttribute),
101
        Boolean(normalAttribute)
102
      ),
103
      getFaceVertex(
104
        triangleIndices[triangleIndex + 1],
105
        Boolean(textureCoordinateAttribute),
106
        Boolean(normalAttribute)
107
      ),
108
      getFaceVertex(
109
        triangleIndices[triangleIndex + 2],
110
        Boolean(textureCoordinateAttribute),
111
        Boolean(normalAttribute)
112
      )
113
    ];
114
    lines.push(`f ${face.join(' ')}`);
2✔
115
  }
116

117
  return `${lines.join('\n')}\n`;
2✔
118
}
119

120
/** Return mesh data as a MeshArrowTable, converting plain Mesh data first. */
121
function normalizeMeshArrowTable(data: Mesh | MeshArrowTable): MeshArrowTable {
122
  if ('shape' in data && data.shape === 'arrow-table') {
2✔
123
    return data;
1✔
124
  }
125
  return convertMeshToTable(data as Mesh, 'arrow-table');
1✔
126
}
127

128
/** Return a required mesh attribute or throw a format-specific error. */
129
function getRequiredAttribute(mesh: Mesh, attributeName: string): MeshAttribute {
130
  const attribute = mesh.attributes[attributeName];
2✔
131
  if (!attribute) {
2!
NEW
132
    throw new Error(`OBJWriter: ${attributeName} attribute is required`);
×
133
  }
134
  return attribute;
2✔
135
}
136

137
/** Return a single attribute component with 0 as the missing component fallback. */
138
function getComponent(
139
  attribute: MeshAttribute,
140
  vertexIndex: number,
141
  componentIndex: number
142
): number {
143
  return attribute.value[vertexIndex * attribute.size + componentIndex] || 0;
48✔
144
}
145

146
/** Return a color component normalized for OBJ vertex colors. */
147
function getColorComponent(
148
  attribute: MeshAttribute,
149
  vertexIndex: number,
150
  componentIndex: number
151
): number {
NEW
152
  const value = getComponent(attribute, vertexIndex, componentIndex);
×
NEW
153
  return attribute.normalized || value > 1 ? value / 255 : value;
×
154
}
155

156
/** Return triangle indices for indexed or sequential triangle-list meshes. */
157
function getTriangleIndices(mesh: Mesh, vertexCount: number): number[] {
158
  if (mesh.indices?.value?.length) {
2!
159
    return Array.from(mesh.indices.value);
2✔
160
  }
161

NEW
162
  if (mesh.mode !== 4 && mesh.topology !== 'triangle-list') {
×
NEW
163
    return [];
×
164
  }
165

NEW
166
  const triangleIndices: number[] = [];
×
NEW
167
  for (let vertexIndex = 0; vertexIndex + 2 < vertexCount; vertexIndex += 3) {
×
NEW
168
    triangleIndices.push(vertexIndex, vertexIndex + 1, vertexIndex + 2);
×
169
  }
NEW
170
  return triangleIndices;
×
171
}
172

173
/** Return an OBJ face vertex reference. */
174
function getFaceVertex(
175
  vertexIndex: number,
176
  hasTextureCoordinates: boolean,
177
  hasNormals: boolean
178
): string {
179
  const faceIndex = vertexIndex + 1;
6✔
180
  if (hasTextureCoordinates && hasNormals) {
6!
181
    return `${faceIndex}/${faceIndex}/${faceIndex}`;
6✔
182
  }
NEW
183
  if (hasTextureCoordinates) {
×
NEW
184
    return `${faceIndex}/${faceIndex}`;
×
185
  }
NEW
186
  if (hasNormals) {
×
NEW
187
    return `${faceIndex}//${faceIndex}`;
×
188
  }
NEW
189
  return String(faceIndex);
×
190
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc