Standardization beyond colors and vectors (i.e. Matrix, Scalar, Quaternion)

TL;DR:

`Tensor` would be added, which provides a standardized API for interacting with vectors, colors, matrices, quaternions, etc.

Rational

After merging #13699, the various vectors implement `Vector`. `Vector`, along with the color classes implement `VectorLike`. `VectorLike` defines a standard signature for different methods (e.g. `copyFrom`, `clone`, `add`, `subtractInPlaceFromFloats`, etc.). `Vector` extends `VectorLike` and adds normalization methods and `length` / `lengthSquared`. This proposal’s goals are based on the ideas behind #13699, with the core idea being to create a standard interface from which various math constructs are defined.

Semantics

Note:

• `CurrentClass` is a placeholder for a class that implements `Tensor`. In fields, it is the current class.
• `Tensor` and its related types will be defined in `src/Maths/tensor.ts` unless otherwise noted

`MultidimensionalArray`

Defined in `src/types.ts`

``````type MultidimensionalArray<T, D extends number> = D extends 1
? T[]
: MultidimensionalArray<T, D - 1>[];
``````

Represents a multidimensional array of `T` with depth `D`.

TensorValue

``````type TensorValue = number | MultidimensionalArray<TensorValue, number>
``````

Tensor

``````declare class Tensor<V extends TensorValue>
``````

This proposal replaces `VectorLike` with a new type, `Tensor`, for tensor-like classes to implement. `Vector` would be changed to extend `Tensor`,

`Tensor` includes all of the methods in `VectorLike`. This includes

• Math operations (`add`, `subtract`, `multiply`, `divide`, `scale`, …)
• Array conversion (`fromArray`, `toArray`, `asArray`, …)
• Since the type parameter is no longer assignable to `number[]`, the return type for array-related methods will be changed to `number[]`
• See `Tensor.value`
• Transfering (`clone`, `copyFrom`, `copyFromFloats`, …)
• The above methods’ `InPlace`, `ToRef`, `FromFloats`, etc.

It adds or changes the following methods:

Tensor.From

``````public static From(source: Tensor, fillValue: number = 0): CurrentClass;
``````

`From` creates a new instance of `CurrentClass` from `source`.
`source`: The tensor to copy data from.
`fillValue`: The value to use for filling empty parts of the resulting `CurrentClass`.

Example:

``````const vec3 = new Vector3(1, 2, 3);
const vec4 = Vector4.From(vec3, 4); // { 1, 2, 3, 4 }
const matrix = Matrix.From(vec3, 0); // this is possible!
``````

Additionally, `From` could be changed to be `From(...args: [...Tensor[], number]): CurrentClass`. This is better understood by this invalid Typescript signature (since rest parameters must be last):

``````public static From(...source: Tensor[], fillValue: number = 0): CurrentClass;
``````

Tensor.as

``````public as<T extends typeof Tensor>(type: T, fillValue: number = 0): InstanceType<T>;
``````

`as` creates an instance of `type` from an instance of `CurrentClass`.
`type`: The class to create an instance of.
`fillValue`: The value to use for filling empty parts of the resulting instance.

Example:

``````const vec3 = new Vector3(1, 2, 3);
const vec4 = vec3.as(Vector4, 4); // { 1, 2, 3, 4 }
const matrix = vec3.as(Matrix, 0);
``````

Tensor.sum

``````public sum(): number;
``````

`sum` return the sum of the components of the `Tensor`.

Example:

``````const scalar = new Scalar(1),
vec3 = new Vector3(1, 2, 3),
vec4 = new Vector4(4, 5, 6, 7),
matrix = new Matrix();

scalar.sum() // 1
scalar.sum() // 6
scalar.sum() // 22
scalar.sum() // 0
``````

The return value of the `lengthSquared` or `length` method of vectors is not the same as the return value of `sum`.

Tensor.rank

``````public abstract rank: number;
``````

The `rank` of a tensor is the number of indices required to uniquely select each element of the tensor.

🛈 The `rank` of a `Tensor` is the same as the `length` of its `dimension`.

Example:

``````const scalar = new Scalar(),
vec3 = new Vector3(),
matrix = new Matrix();

scalar.rank // 0, since indicies are not needed
vec3.rank // 1
matrix.rank // 2
``````

Tensor.dimension

``````public abstract dimension: number[];
``````

`dimension` is the mathematical dimension [2] of the tensor. In dynamic tensor types, it can be defined as a getter.

Example:

``````const scalar = new Scalar(),
vec3 = new Vector3(),
vec4 = new Vector4(),
matrix = new Matrix();

scalar.dimension // []
vec3.dimension // [3]
vec4.dimension // [4]
matrix.dimension // [4, 4]
``````

Tensor.value

``````public get value(): V;
public set value(value: V): void;
``````

`value` is the values of the tensor in a multidimensional array with the type `V` (the type parameter of the class). Unlike `dimension`, this must be implemented as a getter and setter.

Example:

``````const vec3 = new Vector3(),
matrix = new Matrix();

vec3.value
// [0, 0, 0]

matrix.value
// [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ] ]

vec3.value = [ 1, 2, 3 ];
vec3.x // 1
``````

isTensor

``````function isTensor(value: unknown): value is Tensor;
``````

Checks if a value is a `Tensor`.
Example:

``````const vec3 = new Vector3();
isTensor(vec3) // true
isTensor(vec3.value) // false
isTensor([1, 2, 3]) // false
isTensor(0) // false
isTensor(null) // false
``````

isTensorValue

``````function isTensorValue(value: unknown): value is TensorValue;
``````

Checks if a value is a `TensorValue`.
Example:

``````const vec3 = new Vector3();
isTensorValue(vec3) // false
isTensorValue(vec3.value) // true
isTensorValue([1, 2, 3]) // true
isTensorValue(0) // true
isTensorValue(null) // false
``````

getDimension

``````function getDimension(value: Tensor | TensorValue): number[];
``````

`getDimension` returns the mathimatical dimension [2] of `value`, similar to `Tensor.dimension`. If `value` is not a `Tensor` or `TensorValue`, it will throw a `TypeError`

🛈 `Tensor` based classes can use `getDimension(this.value)` for their `dimension` implementations.

Considerations

1. Static methods not pertaining to data transfer and formatting (e.g. `Add`, `Normalize`, `Lerp`) and non-static normalization methods are outside the scope of this proposal. While `Tensor` may include them in the future, this proposal does not
2. This proposal does not provide for a dimensionally dynamic `Tensor`.

Performance

Since `Tensor` is defined using the `declare class` and classes that follow `Tensor` do so using the `implements` keyword, there are no runtime changes to `Tensor`-based classes.

Bundle size

The JS bundle size will increase only by the size of the added functions (`isTensor`, `getDimension`, etc.). The TS declaration size will also increase due to `Tensor`.

Questions

1. Should `Scalar` be included in this proposal? As a rank 0 tensor, the benefits of standardizing `Scalar` may not be worth it, especially considering it is not currently meant to be instantiated.

2. How should incompatibility be managed? Should incompatible classes be dropped or should they be modified to follow `Tensor`?

3. What other classes (e.g. `Size`) would be included/standardized?

None yet

Revisions

If revisions to this proposal are made, edits will be made to the above proposal with revision numbers attached to revised semantics, questions, considerations, and FAQs. Revisions were made on the below dates:

#0: 23 August 2023

I am not opposed but really want to see a concrete use case and usage first to justify the extra maintenance and size ?

Take the current difference in behavior between vectors and colors:

``````const c1 = Color3.Random(),
c2 = Color3.Random(),
c3 = new Color3(),
v1 = Vector3.Random(),
v2 = Vector3.Random(),
v3 = new Vector3();

c1.addToRef(c2, c3) === c3 // false
v1.addToRef(v2, v3) === v3 // true
``````

And the lack of a method that should exist in Color3:

``````const v1 = Vector3.Random(),
v2 = Vector3.Random(),
c1 = Color3.Random(),
c2 = Color3.Random();

``````

`Tensor` provides a single interface for tensor-like objects which users can depend on. By having tensor-like classes implement `Tensor`, it ensures that all the methods exist and have the same and correct behavior. It is meant for organization and standardization. Since there is no implementation (just a class declaration), the extra maintenance is minimized to member signatures. The benefits of using `Tensor` far outweigh the potential for differentiating behavior and interfaces, to include the complete lack of methods that should exist.

I am not sure to understand the difference with your current PR aside of the naming in this case ?

The primary difference is with how dimensions are handled. With `Tensor`, it is generalized further. A mathematical vector can be represented as `number[]`. A mathematical tensor is represented as a `TensorValue` (from the proposal). I’ve created this table to make sense of it:

tensor rank TS representation BJS class (if exists)
0 `number` `Scalar`
1 `number[]` `Vector`, `DynamicVector`
1 `[number, number]` `Vector2`
1 `[number, number, number]` `Vector3`, `Color3`
1 `[number, number, number, number]` `Vector4`, `Color4`, `Quaternion`
2 `number[][]` `Matrix`
3 `number[][][]`
4 `number[][][][]`
N `TensorValue`

Also, @sebavan, what do you think about me closing #13699 and moving `Vector` into the new PR along with the `Tensor` types? I think it would be good to clear the 100+ comments in #13699 and start from a fresh PR. I would keep the commits I’ve already made.

Yup but in this case as you do it mostly for consistency in the API, why having both Vector and Tensor as declared types ?

`Vector` added normalization methods (i.e. `normalize`, `normalizeFromLength`, `normalizeToRef`, etc.), `length`, and `lengthSquared`. This change was made because you brought this up in the PR:

I do not know if color and vector needs to be that close. (they could but I am wondering if they need to, for instance, what does length represent for a color ?

Which makes sense. Furthering your point, what would `length` represent for a `Tensor`? There is simply no use or meaning for `length` when abstracted to that level.

Ok so basically vector and color would inherit/implement tensor instead of color implementing vectorLike ???

Yes, you could think of it like this

`Tensor``Scalar`
`Tensor``Vector``VectorN`
`Tensor``ColorN`
`Tensor``Quaternion`
`Tensor``Matrix`

When this plan will be realized, this design is useful

1 Like