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

visgl / luma.gl / 25681495384

11 May 2026 03:59PM UTC coverage: 74.425% (+0.01%) from 74.412%
25681495384

push

github

web-flow
feat(engine); Build interleaved geometries (#2604)

Co-authored-by: Ib Green <ib.green.home@gmail.com>

5442 of 8256 branches covered (65.92%)

Branch coverage included in aggregate %.

83 of 99 new or added lines in 4 files covered. (83.84%)

2 existing lines in 2 files now uncovered.

12129 of 15353 relevant lines covered (79.0%)

764.13 hits per line

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

88.29
/modules/engine/src/geometry/geometry.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {TypedArray} from '@math.gl/core';
6
import type {BufferLayout, PrimitiveTopology} from '@luma.gl/core';
7
import {vertexFormatDecoder} from '@luma.gl/core';
8
import {uid} from '../utils/uid';
9

10
/** CPU-side attribute data accepted by {@link Geometry}. */
11
export type GeometryAttributeInput = GeometryAttribute | TypedArray;
12

13
/** Properties used to create a {@link Geometry}. */
14
export type GeometryProps = {
15
  /** Application-provided identifier. */
16
  id?: string;
17
  /** Determines how vertices are assembled into primitives. */
18
  topology: 'point-list' | 'line-list' | 'line-strip' | 'triangle-list' | 'triangle-strip';
19
  /** Draw vertex count. Auto-calculated from attributes or indices when omitted. */
20
  vertexCount?: number;
21
  /** CPU attributes, keyed by shader attribute name or supported glTF-style semantic name. */
22
  attributes: Record<string, GeometryAttributeInput>;
23
  /**
24
   * Maps geometry buffers to shader attributes.
25
   *
26
   * If omitted, the constructor creates one buffer layout entry for each normalized attribute.
27
   */
28
  bufferLayout?: BufferLayout[];
29
  /** Optional index data. Indices are always stored separately from vertex attributes. */
30
  indices?: GeometryAttribute | TypedArray;
31
};
32

33
/** Attributes returned by {@link Geometry.getAttributes}. */
34
export type GeometryAttributes = Record<string, GeometryAttribute | undefined> & {
35
  /** Optional index attribute, included when the geometry is indexed. */
36
  indices?: GeometryAttribute & {size: 1; value: Uint32Array | Uint16Array};
37
};
38

39
/** Typed-array backed CPU geometry attribute. */
40
export type GeometryAttribute = {
41
  /** Number of typed-array elements per vertex. */
42
  size?: number;
43
  /** Attribute data. */
44
  value: TypedArray;
45
  /** Additional attribute metadata consumed by geometry utilities. */
46
  [key: string]: any;
47
};
48

49
/**
50
 * CPU-side geometry container.
51
 *
52
 * `Geometry` stores typed-array vertex data, optional index data, and an always-populated
53
 * `bufferLayout` that describes how its attributes map to shader inputs. Attribute names are
54
 * normalized once in the constructor so glTF-style names such as `POSITION` and `TEXCOORD_0`
55
 * become shader names such as `positions` and `texCoords`.
56
 */
57
export class Geometry {
58
  /** Application-provided or generated identifier. */
59
  readonly id: string;
60

61
  /** Determines how vertices are assembled into primitives. */
62
  readonly topology?: PrimitiveTopology;
63

64
  /** Resolved draw vertex count. */
65
  readonly vertexCount: number;
66

67
  /** Optional index attribute. */
68
  readonly indices?: GeometryAttribute;
69

70
  /** CPU attributes, keyed by normalized buffer or shader attribute name. */
71
  readonly attributes: Record<string, GeometryAttribute | undefined>;
72

73
  /** Buffer layout matching the geometry attributes. Always populated. */
74
  readonly bufferLayout: BufferLayout[];
75

76
  /** Application-owned metadata. */
77
  userData: Record<string, unknown> = {};
370✔
78

79
  /** Creates a CPU geometry and normalizes attributes plus buffer layout metadata. */
80
  constructor(props: GeometryProps) {
81
    const {attributes = {}, indices = null, vertexCount = null} = props;
370✔
82

83
    this.id = props.id || uid('geometry');
370✔
84
    this.topology = props.topology;
370✔
85

86
    if (indices) {
370✔
87
      this.indices = ArrayBuffer.isView(indices) ? {value: indices, size: 1} : indices;
294✔
88
    }
89

90
    this.attributes = {};
370✔
91

92
    for (const [attributeName, attributeValue] of Object.entries(attributes)) {
370✔
93
      // Wrap "unwrapped" arrays and try to autodetect their type
94
      const attribute: GeometryAttribute = ArrayBuffer.isView(attributeValue)
963✔
95
        ? {value: attributeValue}
96
        : attributeValue;
97

98
      if (!ArrayBuffer.isView(attribute.value)) {
963✔
99
        throw new Error(
1✔
100
          `${this._print(attributeName)}: must be typed array or object with value as typed array`
101
        );
102
      }
103

104
      if ((attributeName === 'POSITION' || attributeName === 'positions') && !attribute.size) {
962✔
105
        attribute.size = 3;
2✔
106
      }
107

108
      // Move indices to separate field
109
      if (attributeName === 'indices') {
962✔
110
        if (this.indices) {
2✔
111
          throw new Error('Multiple indices detected');
1✔
112
        }
113
        this.indices = attribute;
1✔
114
      } else {
115
        const normalizedAttributeName = getGeometryShaderAttributeName(attributeName);
960✔
116
        this.attributes[normalizedAttributeName] = attribute;
960✔
117
      }
118
    }
119

120
    if (this.indices && this.indices['isIndexed'] !== undefined) {
368✔
121
      this.indices = Object.assign({}, this.indices);
1✔
122
      delete this.indices['isIndexed'];
1✔
123
    }
124

125
    this.vertexCount = vertexCount || this._calculateVertexCount(this.attributes, this.indices);
368✔
126
    this.bufferLayout = props.bufferLayout
370✔
127
      ? normalizeGeometryBufferLayout(props.bufferLayout)
128
      : getBufferLayoutFromGeometryAttributes(this.attributes);
129
  }
130

131
  /** Returns the resolved draw vertex count. */
132
  getVertexCount(): number {
133
    return this.vertexCount;
3✔
134
  }
135

136
  /** Returns all attributes, including `indices` when index data is present. */
137
  getAttributes(): GeometryAttributes {
NEW
138
    return (
×
139
      this.indices ? {indices: this.indices, ...this.attributes} : this.attributes
×
140
    ) as GeometryAttributes;
141
  }
142

143
  // PRIVATE
144

145
  _print(attributeName: string): string {
146
    return `Geometry ${this.id} attribute ${attributeName}`;
1✔
147
  }
148

149
  _setAttributes(attributes: Record<string, GeometryAttribute>, indices: any): this {
UNCOV
150
    return this;
×
151
  }
152

153
  _calculateVertexCount(
154
    attributes: Record<string, GeometryAttribute | undefined>,
155
    indices?: GeometryAttribute
156
  ): number {
157
    if (indices) {
274✔
158
      return indices.value.length;
266✔
159
    }
160
    let vertexCount = Infinity;
8✔
161
    for (const attribute of Object.values(attributes)) {
8✔
162
      if (!attribute) {
16!
NEW
163
        continue; // eslint-disable-line no-continue
×
164
      }
165
      const {value, size, constant} = attribute;
16✔
166
      if (!constant && value && size !== undefined && size >= 1) {
16!
167
        vertexCount = Math.min(vertexCount, value.length / size);
16✔
168
      }
169
    }
170

171
    // assert(Number.isFinite(vertexCount));
172
    return vertexCount;
8✔
173
  }
174
}
175

176
/**
177
 * Converts supported geometry semantic names to shader attribute names.
178
 *
179
 * Names that do not have a built-in mapping are returned unchanged.
180
 */
181
export function getGeometryShaderAttributeName(attributeName: string): string {
182
  switch (attributeName) {
1,309!
183
    case 'POSITION':
184
      return 'positions';
272✔
185
    case 'NORMAL':
186
      return 'normals';
267✔
187
    case 'TEXCOORD_0':
188
      return 'texCoords';
260✔
189
    case 'TEXCOORD_1':
NEW
190
      return 'texCoords1';
×
191
    case 'COLOR_0':
192
      return 'colors';
1✔
193
    default:
194
      return attributeName;
509✔
195
  }
196
}
197

198
function getBufferLayoutFromGeometryAttributes(
199
  attributes: Record<string, GeometryAttribute | undefined>
200
): BufferLayout[] {
201
  const bufferLayout: BufferLayout[] = [];
305✔
202
  for (const [name, attribute] of Object.entries(attributes)) {
305✔
203
    if (!attribute) {
897!
NEW
204
      continue; // eslint-disable-line no-continue
×
205
    }
206
    const {value, size, normalized} = attribute;
897✔
207
    if (size === undefined) {
897!
NEW
208
      throw new Error(`Attribute ${name} is missing a size`);
×
209
    }
210
    bufferLayout.push({
897✔
211
      name,
212
      format: vertexFormatDecoder.getVertexFormatFromAttribute(value, size, normalized)
213
    });
214
  }
215
  return bufferLayout;
305✔
216
}
217

218
function normalizeGeometryBufferLayout(bufferLayout: BufferLayout[]): BufferLayout[] {
219
  return bufferLayout.map(layout =>
63✔
220
    layout.attributes
63✔
221
      ? {
222
          ...layout,
223
          attributes: layout.attributes.map(attribute => ({
174✔
224
            ...attribute,
225
            attribute: getGeometryShaderAttributeName(attribute.attribute)
226
          }))
227
        }
228
      : {
229
          ...layout,
230
          name: getGeometryShaderAttributeName(layout.name)
231
        }
232
  );
233
}
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