@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
497 lines (423 loc) • 14.7 kB
text/typescript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import type { TypedArray } from 'three';
import { MathUtils, Vector2, Vector3, Vector4 } from 'three';
import { nonNull } from '../utils/tsutils';
export type Dimension = 2 | 3 | 4;
const X = 0;
const Y = 1;
const Z = 2;
const W = 3;
export type Vector = Vector2 | Vector3 | Vector4;
/**
* A typed array of three.js {@link Vector}s.
*
* @param V - The underlying {@link Vector} type.
* @param Buffer - The underlying {@link TypedArray} type.
*/
export abstract class VectorArray<
V extends Vector = Vector,
Buffer extends TypedArray = TypedArray,
> {
private readonly _dimension: Dimension;
private _capacity: number;
protected _array: Buffer;
private _length: number;
private _lastExpansion = 0;
/**
* The length in bytes of the array.
*/
public get byteLength(): number {
return this.array.byteLength;
}
public get capacity(): number {
return this.array.length / this._dimension;
}
/**
* Gets the underlying {@link Buffer}.
*/
public get array(): Buffer {
return this._array;
}
/**
* Returns the {@link Float32Array} equivalent of this vector array.
* Note: if the underlying array is already a Float32Array, this array is returned.
* Otherwise, a new array is constructed.
*/
public toFloat32Array(): Float32Array {
if (this._array instanceof Float32Array) {
if (this._array.length === this._length * this._dimension) {
// Return the same array as it is already in the correct format and size
return this._array;
} else {
// Return a slice of the same array as it is already in the correct format
return this._array.slice(0, this._length * this._dimension);
}
}
// Create an intermediate array in the original format
// TODO might be more efficient to allocate the array directly with the correct size,
// then copy elements individually ?
return new Float32Array(this._array.slice(0, this._length * this._dimension));
}
protected constructor(buffer: Buffer, dimension: Dimension) {
this._dimension = dimension;
if (buffer.length % this._dimension !== 0) {
throw new Error(
`invalid size, expected a multiple of ${this._dimension}, got ${buffer.length}`,
);
}
this._array = buffer;
this._capacity = this._array.length / this._dimension;
this._length = this._capacity;
}
/**
* Returns the number of vectors in this array.
*/
public get length(): number {
return this._length;
}
public set length(v: number) {
this._length = v;
}
/**
* Gets the vector at the specified index.
*/
public abstract get(index: number, target?: V): V;
public setX(index: number, value: number): void {
const idx = index * this._dimension;
this._array[idx + X] = value;
}
/**
* Gets the first component of the vector at the specified index.
*/
public getX(index: number): number {
const idx = index * this._dimension;
return this._array[idx + X];
}
public setY(index: number, value: number): void {
const idx = index * this._dimension;
this._array[idx + Y] = value;
}
/**
* Gets the second component of the vector at the specified index.
*/
public getY(index: number): number {
const idx = index * this._dimension;
return this._array[idx + Y];
}
public setZ(index: number, value: number): void {
if (this._dimension >= 3) {
const idx = index * this._dimension;
this._array[idx + Z] = value;
}
}
/**
* Gets the third component of the vector at the specified index, or `null` if the dimension
* of this array is less than 3.
*/
public getZ(index: number): number | null {
if (this._dimension >= 3) {
const idx = index * this._dimension;
return this._array[idx + Z];
}
return null;
}
public setW(index: number, value: number): void {
if (this._dimension >= 4) {
const idx = index * this._dimension;
this._array[idx + W] = value;
}
}
/**
* Gets the fourth component of the vector at the specified index, or `null` if the dimension
* of this array is less than 4.
*/
public getW(index: number): number | null {
if (this._dimension >= 4) {
const idx = index * this._dimension;
return this._array[idx + W];
}
return null;
}
/**
* Sets the vector at the specified index.
*/
public setVector(index: number, v: V): void {
const idx = index * this._dimension;
this._array[idx + X] = v.getComponent(X);
this._array[idx + Y] = v.getComponent(Y);
if (this._dimension >= 3) {
this._array[idx + Z] = v.getComponent(Z);
}
if (this._dimension >= 4) {
this._array[idx + W] = v.getComponent(W);
}
}
/**
* Sets the component of the array at the specified index.
*/
public set(index: number, x: number, y: number, z?: number, w?: number): void {
const idx = index * this._dimension;
if (idx >= this._array.length) {
throw new Error('index out of bounds');
}
this._array[idx + X] = x;
this._array[idx + Y] = y;
if (this._dimension >= Z && z != null) {
this._array[idx + Z] = z;
}
if (this._dimension >= W && w != null) {
this._array[idx + W] = w;
}
}
/**
* Copies an element from one location to another in the array.
*/
public copyItem(from: number, to: number): void {
const dim = this._dimension;
const toIdx = to * dim;
const fromIdx = from * dim;
this._array[toIdx + X] = this._array[fromIdx + X];
this._array[toIdx + Y] = this._array[fromIdx + Y];
if (dim >= 3) {
this._array[toIdx + Z] = this._array[fromIdx + Z];
}
if (dim >= 4) {
this._array[toIdx + W] = this._array[fromIdx + W];
}
}
private computeExpansionSize(): number {
if (this._lastExpansion === 0) {
this._lastExpansion = 32;
} else {
this._lastExpansion *= 2;
this._lastExpansion = MathUtils.clamp(this._lastExpansion, 32, 65536);
}
return this._lastExpansion;
}
/**
* Removes unused capacity.
*/
public trim(): void {
if (this._capacity > this._length) {
this._capacity = this._length;
this._array = this._array.slice(0, this._length * this._dimension) as Buffer;
}
}
/**
* Adds capacity at the end of the array using a growing expansion size (i.e the first time
* this method is called, a small amount is added, and the amount grows every time this method
* is called up to a certain point).
* Contrary to {@link expand}, the length is left untouched (the expanded area is not considered
* used.)
*/
private allocateIfFull(): void {
if (this._capacity === this._length) {
const currentLength = this._length;
this.expand(this._length + this.computeExpansionSize());
this._capacity = this._array.length / this._dimension;
this._length = currentLength;
}
}
/**
* Pushes a vector at the end of the array, allocating memory if necessary.
*/
public push(x: number, y: number, z?: number, w?: number): void {
if (this._capacity === this._length) {
this.allocateIfFull();
}
this.setX(this._length, x);
this.setY(this._length, y);
if (z != null) {
this.setZ(this._length, z);
}
if (w != null) {
this.setW(this._length, w);
}
this._length++;
}
/**
* Pushes a vector at the end of the array, allocating memory if necessary.
*/
public pushVector(v: V): void {
if (this._capacity === this._length) {
this.allocateIfFull();
}
this.assignVector(this._length * this._dimension, v);
this._length++;
}
/**
* Allocates a new underlying array to match the new size, then copy the content
* of the previous array at the beginning of the new array.
* @param newSize - The new size, in number of vectors.
*/
public expand(newSize: number): this {
// @ts-expect-error "this expression is not constructable"
const newArray = new this.array.constructor(newSize * this._dimension);
newArray.set(this._array);
this._array = newArray;
this._capacity = this._array.length / this._dimension;
this._length = this._capacity;
return this;
}
/** @internal */
protected abstract getTempVector(): V;
/** @internal */
protected abstract readVector(rawIndex: number, tempVector: V): void;
/** @internal */
protected abstract assignVector(rawIndex: number, v: Readonly<V>): void;
/**
* Performs the specified action for each element in an array.
*
* Note that mutating the callback value will **not** mutate the underlying array. To mutate the
* underlying array, use the index provided as second argument, then mutate the array with a
* mutating method, such as {@link setVector}:
* ```ts
* const array = new Vector3Array(...);
*
* // Add one to each Y value of the array
* array.forEach((v, index) => {
* // This has no effect on the Vector3Array:
* v.setY(v.y + 1);
*
* // Use this pattern instead:
* array.setVector(index, new Vector3(v.x, v.y + 1, v.z));
*
* // Or this one
* array.setY(index, v.y + 1);
* })
* ```
* @param callbackfn - A function that accepts up to three arguments. forEach calls the
* callbackfn function one time for each element in the array.
*/
public forEach(callbackfn: (value: Readonly<V>, index: number, array: this) => void): void {
const value = this.getTempVector();
const stride = this._dimension;
// Raw index is the index to the first component of each vector, not the vector itself
for (let rawIndex = 0; rawIndex < this._array.length; rawIndex += stride) {
this.readVector(rawIndex, value);
const vectorIndex = rawIndex / stride;
callbackfn(value, vectorIndex, this);
}
}
/**
* Clones this array.
*/
public abstract clone(): ThisType<this>;
}
/**
* A typed array of three.js {@link Vector2}s.
*
* @param Buffer - The underlying {@link TypedArray} type.
*/
export class Vector2Array<Buffer extends TypedArray = TypedArray> extends VectorArray<
Vector2,
Buffer
> {
public readonly dimension = 2 as const;
public constructor(buffer: Buffer) {
super(buffer, 2);
}
public override get(index: number, target?: Vector2): Vector2 {
target = target ?? new Vector2();
return target.set(this.getX(index), this.getY(index));
}
public clone(): Vector2Array {
return new Vector2Array(this._array.slice(0));
}
protected getTempVector(): Vector2 {
return new Vector2();
}
protected readVector(rawIndex: number, tempVector: Vector2): void {
const arr = this._array;
tempVector.set(arr[rawIndex + X], arr[rawIndex + Y]);
}
protected assignVector(rawIndex: number, v: Readonly<Vector2>): void {
this._array[rawIndex + X] = v.x;
this._array[rawIndex + Y] = v.y;
}
}
/**
* A typed array of three.js {@link Vector3}s.
*
* @param Buffer - The underlying {@link TypedArray} type.
*/
export class Vector3Array<Buffer extends TypedArray = TypedArray> extends VectorArray<
Vector3,
Buffer
> {
public readonly dimension = 3 as const;
public constructor(buffer: Buffer) {
super(buffer, 3);
}
public override get(index: number, target?: Vector3): Vector3 {
target = target ?? new Vector3();
return target.set(this.getX(index), this.getY(index), nonNull(this.getZ(index)));
}
public clone(): Vector3Array {
return new Vector3Array(this._array.slice(0));
}
public override getZ(index: number): number {
return super.getZ(index) as number;
}
protected getTempVector(): Vector3 {
return new Vector3();
}
protected readVector(rawIndex: number, tempVector: Vector3): void {
const arr = this._array;
tempVector.set(arr[rawIndex + X], arr[rawIndex + Y], arr[rawIndex + Z]);
}
protected assignVector(rawIndex: number, v: Readonly<Vector3>): void {
this._array[rawIndex + X] = v.x;
this._array[rawIndex + Y] = v.y;
this._array[rawIndex + Z] = v.z;
}
}
/**
* A typed array of three.js {@link Vector4}s.
*
* @param Buffer - The underlying {@link TypedArray} type.
*/
export class Vector4Array<Buffer extends TypedArray = TypedArray> extends VectorArray<
Vector4,
Buffer
> {
public readonly dimension = 4 as const;
public constructor(buffer: Buffer) {
super(buffer, 4);
}
public override get(index: number, target?: Vector4): Vector4 {
target = target ?? new Vector4();
return target.set(
this.getX(index),
this.getY(index),
nonNull(this.getZ(index)),
nonNull(this.getW(index)),
);
}
public override getZ(index: number): number {
return super.getZ(index) as number;
}
public override getW(index: number): number {
return super.getW(index) as number;
}
public clone(): Vector4Array {
return new Vector4Array(this._array.slice(0));
}
protected getTempVector(): Vector4 {
return new Vector4();
}
protected readVector(rawIndex: number, tempVector: Vector4): void {
const arr = this._array;
tempVector.set(arr[rawIndex + X], arr[rawIndex + Y], arr[rawIndex + Z], arr[rawIndex + W]);
}
protected assignVector(rawIndex: number, v: Readonly<Vector4>): void {
this._array[rawIndex + X] = v.x;
this._array[rawIndex + Y] = v.y;
this._array[rawIndex + Z] = v.z;
this._array[rawIndex + W] = v.w;
}
}