/
PointsManager.ts
260 lines (237 loc) · 7.3 KB
/
PointsManager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import type { Point2, Point3, PointsXYZ } from '../types';
export type PolyDataPointConfiguration = {
/** The dimensionality of the points */
dimensions?: number;
/** The initial size of the backing array, not containing any data initially */
initialSize?: number;
/** The incremental size to grow by when required */
growSize?: number;
};
/**
* PointsManager handles Point type data contained in a TypedArray representation
* where all the point data is consecutive from start to end. That is, the
* organization is `x0,y0,z0,x1,y1,z1,...,xn,yn,zn`. This optimizes the storage
* costs for large arrays of data, while still providing access to the point
* data as though it were a simple array of objects.
*
* This representation is efficient for storing large numbers of points and for
* transferring them amongst systems and is planned to have more methods added
* for generic manipulation of data.
*/
export default class PointsManager<T> {
/**
* Allow storage for an index value to indicate where this array is
* contained in terms of the index location.
*/
public kIndex: number;
/**
* Sources data for this array. Just used for external access, not updated
* here.
*/
public sources: PointsManager<T>[];
data: Float32Array;
_dimensions = 3;
_length = 0;
_byteSize = 4;
growSize = 128;
array: ArrayBuffer;
constructor(configuration: PolyDataPointConfiguration = {}) {
const {
initialSize = 1024,
dimensions = 3,
growSize = 128,
} = configuration;
const itemLength = initialSize * dimensions;
this.growSize = growSize;
// TODO - use resizeable arrays when they become available in all browsers
this.array = new ArrayBuffer(itemLength * this._byteSize);
this.data = new Float32Array(this.array);
this._dimensions = dimensions;
}
public forEach(func: (value: T, index: number) => void) {
for (let i = 0; i < this._length; i++) {
func(this.getPoint(i), i);
}
}
public get length() {
return this._length;
}
public get dimensions() {
return this._dimensions;
}
public get dimensionLength() {
return this._length * this._dimensions;
}
/**
* Returns a Float32Array view of the given point.
* Changes to the data in this point will affect the underlying data.
*
* @param index - positive index from start, or negative from end
* @returns Float32Array view onto the point at the given index
*/
public getPoint(index: number): T {
if (index < 0) {
index += this._length;
}
if (index < 0 || index >= this._length) {
return;
}
const offset = this._dimensions * index;
return this.data.subarray(
offset,
offset + this._dimensions
) as unknown as T;
}
/**
* Returns a `number[]` version of the given point.
* Changes to the array will NOT affect the underlying data.
*
* @param index - positive index from start, or negative from end
* @returns A new number[] instance of the given point.
*/
public getPointArray(index: number): T {
const array = [];
if (index < 0) {
index += this._length;
}
if (index < 0 || index >= this._length) {
return;
}
const offset = this._dimensions * index;
for (let i = 0; i < this._dimensions; i++) {
array.push(this.data[i + offset]);
}
return array as unknown as T;
}
/**
* Updates the array size as needed to allow for at least the given
* additional number of elements.
*/
protected grow(additionalSize = 1, growSize = this.growSize) {
if (
this.dimensionLength + additionalSize * this._dimensions <=
this.data.length
) {
return;
}
const newSize = this.data.length + growSize;
const newArray = new ArrayBuffer(
newSize * this._dimensions * this._byteSize
);
const newData = new Float32Array(newArray);
newData.set(this.data);
this.data = newData;
this.array = newArray;
}
/**
* Reverse the points in place.
*/
public reverse() {
const midLength = Math.floor(this._length / 2);
for (let i = 0; i < midLength; i++) {
const indexStart = i * this._dimensions;
const indexEnd = (this._length - 1 - i) * this._dimensions;
for (let dimension = 0; dimension < this._dimensions; dimension++) {
const valueStart = this.data[indexStart + dimension];
this.data[indexStart + dimension] = this.data[indexEnd + dimension];
this.data[indexEnd + dimension] = valueStart;
}
}
}
/**
* Push a new point onto this arrays object
*/
public push(point: T) {
this.grow(1);
const offset = this.length * this._dimensions;
for (let i = 0; i < this._dimensions; i++) {
this.data[i + offset] = point[i];
}
this._length++;
}
/**
* Maps the array onto another type.
*/
public map<R>(f: (value, index: number) => R): R[] {
const mapData = [];
for (let i = 0; i < this._length; i++) {
mapData.push(f(this.getPoint(i), i));
}
return mapData;
}
/**
* A points object containing Float32Array instances referring to the underlying
* data, contained in a FloatArray32[] instance.
* Note - changes to the data store will directly affect the points value
* returned here, even if stored separately.
*/
public get points(): T[] {
return this.map((p) => p);
}
/**
* The XYZ representation of a points array is an object with three separate
* arrays, one for each of x,y and z, containing the point data, eg
* `x: {x0, x1, x2, ...., xn }`
* Will create just x,y for Point2 arrays.
*
* @returns An XYZ array
*/
public toXYZ(): PointsXYZ {
const xyz = { x: [], y: [] } as PointsXYZ;
if (this._dimensions >= 3) {
xyz.z = [];
}
const { x, y, z } = xyz;
this.forEach((p) => {
x.push(p[0]);
y.push(p[1]);
if (z) {
z.push(p[2]);
}
});
return xyz;
}
/**
* Create an PointsArray3 from the x,y,z individual arrays (see toXYZ)
* Will create a Point3 array even if z is missing, with 0 as the value.
*/
public static fromXYZ({ x, y, z }: PointsXYZ): PointsManager<Point3> {
const array = PointsManager.create3(x.length);
let offset = 0;
for (let i = 0; i < x.length; i++) {
array.data[offset++] = x[i];
array.data[offset++] = y[i];
array.data[offset++] = z ? z[i] : 0;
}
array._length = x.length;
return array;
}
/**
* Select the given number of points from the array, evenly spaced at the
* given offset (which must be between `(-count,count)`)
*/
public subselect(count = 10, offset = 0): PointsManager<T> {
const selected = new PointsManager<T>({
initialSize: count,
dimensions: this._dimensions,
});
for (let i = 0; i < count; i++) {
const index =
(offset + Math.floor((this.length * i) / count)) % this.length;
selected.push(this.getPoint(index));
}
return selected;
}
/**
* Create a PointsManager<Point3> instance with available capacity of initialSize
*/
public static create3(initialSize = 128) {
return new PointsManager<Point3>({ initialSize, dimensions: 3 });
}
/**
* Create a PointsManager<Point2> instance with available capacity of initialSize
*/
public static create2(initialSize = 128) {
return new PointsManager<Point2>({ initialSize, dimensions: 2 });
}
}