Repository: playcanvas/engine
Pull request #7312 (2025-01-27 14:55:42)

VecN/Color array conversion support
### Motivation
When using Vectors/Colors it is very common to want to serialize and deserialize data to either be used for UI (in conjunction with observers) or to synchronise over a network

### Overview
- Adds `fromArray(offset: number = 0)` methods on VecN and Color classes to allow setting from arrays
- Adds `toArray(out: any[], offset: number = 0)` methods to VecN and Color classes to for explicit conversion to array (avoids potential class fields being added from using `Object.values`

### Testing
- [x] Add unit tests

Maksims: I wander if it should be on `set` method or `copy` method. As copy - feels a bit more appropriate, as it copies from another thing. While set using array - feels weird, as Color or Vec - is not an array-like structure.
kpal81xd: > I wander if it should be on `set` method or `copy` method. As copy - feels a bit more appropriate, as it copies from another thing. While set using array - feels weird, as Color or Vec - is not an array-like structure.

Copy implies that the target you are copying from is of the same type.
kungfooman: I understand the wish for `#toArray`, but not the updat efor `#set`, since that's just `.set(...arr)`
kpal81xd: > I understand the wish for `#toArray`, but not the update for `#set`, since that's just `.set(...arr)`
> 
> Edit: same goes for `#fromArray`, we have always argued against bloating the API when it is not necessary

It was mainly from typechecking standpoint as we can convert from type `number[]` whereas `set(...arr)` requires arr to be a tuple `[number, number, ...]`. Yes you can cast but the majority of the time you will be dealing with `number[]` over tuples. This can also get super annoying converting from an array multiple times and usually ends up just being added anyway as a helper. 
kungfooman: So `#fromArray` accepts `number[]` as an argument, which implies that even `vec3.set([1])` is "valid" now (so we are ending up with `{x: 1, y: undefined, z: undefined}`). You just watered down type-checking.
kpal81xd: > So `#fromArray` accepts `number[]` as an argument, which implies that even `vec3.set([1])` is "valid" now (so we are ending up with `{x: 1, y: undefined, z: undefined}`). You just watered down type-checking.

That can easily be fixed by just added undefined defaults
LeXXik: Can we add an optional index to the to/from methods? I have similar methods, but I use it with buffers and need to point the values location.
kpal81xd: > Can we add an optional index to the to/from methods? I have similar methods, but I use it with large arrays and need to point to the values location.

I can do it for `fromArray` but `toArray` just generates a new array

Patch:
diff --git a/src/core/math/color.js b/src/core/math/color.js
index 3f62f4338..08e001730 100644
--- a/src/core/math/color.js
+++ b/src/core/math/color.js
@@ -220,6 +220,27 @@ class Color {
         return this;
     }
 
+    /**
+     * Set the values of the vector from an array.
+     *
+     * @param {number[]} arr - The array to set the vector values from.
+     * @param {number} [offset] - The zero-based index at which to start copying elements from the
+     * array. Default is 0.
+     * @returns {Color} Self for chaining.
+     * @example
+     * const c = new pc.Color();
+     * c.fromArray([1, 0, 1, 1]);
+     * // c is set to [1, 0, 1, 1]
+     */
+    fromArray(arr, offset = 0) {
+        this.r = arr[offset] ?? this.r;
+        this.g = arr[offset + 1] ?? this.g;
+        this.b = arr[offset + 2] ?? this.b;
+        this.a = arr[offset + 3] ?? this.a;
+
+        return this;
+    }
+
     /**
      * Converts the color to string form. The format is '#RRGGBBAA', where RR, GG, BB, AA are the
      * red, green, blue and alpha values. When the alpha value is not included (the default), this
@@ -254,6 +275,30 @@ class Color {
         return s;
     }
 
+    /**
+     * Converts the color to an array of numbers.
+     *
+     * @param {number[]} [arr] - The array to populate with the color components. If not specified,
+     * a new array is created. Default is true.
+     * @param {number} [offset] - The zero-based index at which to start copying elements to the
+     * array. Default is 0.
+     * @param {boolean} [alpha] - If true, the output array will include the alpha value.
+     * @returns {number[]} The color as an array of numbers.
+     * @example
+     * const c = new pc.Color(1, 1, 1);
+     * // Outputs [1, 1, 1, 1]
+     * console.log(c.toArray());
+     */
+    toArray(arr = [], offset = 0, alpha = true) {
+        arr[offset] = this.r;
+        arr[offset + 1] = this.g;
+        arr[offset + 2] = this.b;
+        if (alpha) {
+            arr[offset + 3] = this.a;
+        }
+        return arr;
+    }
+
     /**
      * A constant color set to black [0, 0, 0, 1].
      *
diff --git a/src/core/math/vec2.js b/src/core/math/vec2.js
index 22b102128..d500a79c5 100644
--- a/src/core/math/vec2.js
+++ b/src/core/math/vec2.js
@@ -642,6 +642,25 @@ class Vec2 {
         return this;
     }
 
+    /**
+     * Set the values of the vector from an array.
+     *
+     * @param {number[]} arr - The array to set the vector values from.
+     * @param {number} [offset] - The zero-based index at which to start copying elements from the
+     * array. Default is 0.
+     * @returns {Vec2} Self for chaining.
+     * @example
+     * const v = new pc.Vec2();
+     * v.fromArray([20, 10]);
+     * // v is set to [20, 10]
+     */
+    fromArray(arr, offset = 0) {
+        this.x = arr[offset] ?? this.x;
+        this.y = arr[offset + 1] ?? this.y;
+
+        return this;
+    }
+
     /**
      * Converts the vector to string form.
      *
@@ -655,6 +674,26 @@ class Vec2 {
         return `[${this.x}, ${this.y}]`;
     }
 
+    /**
+     * Converts the vector to an array.
+     *
+     * @param {number[]} [arr] - The array to populate with the color components. If not specified,
+     * a new array is created.
+     * @param {number} [offset] - The zero-based index at which to start copying elements to the
+     * array. Default is 0.
+     * @returns {number[]} The vector as an array.
+     * @example
+     * const v = new pc.Vec3(20, 10);
+     * // Outputs [20, 10]
+     * console.log(v.toArray());
+     */
+    toArray(arr = [], offset = 0) {
+        arr[offset] = this.x;
+        arr[offset + 1] = this.y;
+
+        return arr;
+    }
+
     /**
      * Calculates the angle between two Vec2's in radians.
      *
diff --git a/src/core/math/vec3.js b/src/core/math/vec3.js
index 7ef39d2ae..08766c5b3 100644
--- a/src/core/math/vec3.js
+++ b/src/core/math/vec3.js
@@ -659,6 +659,26 @@ class Vec3 {
         return this;
     }
 
+    /**
+     * Set the values of the vector from an array.
+     *
+     * @param {number[]} arr - The array to set the vector values from.
+     * @param {number} [offset] - The zero-based index at which to start copying elements from the
+     * array. Default is 0.
+     * @returns {Vec3} Self for chaining.
+     * @example
+     * const v = new pc.Vec3();
+     * v.fromArray([20, 10, 5]);
+     * // v is set to [20, 10, 5]
+     */
+    fromArray(arr, offset = 0) {
+        this.x = arr[offset] ?? this.x;
+        this.y = arr[offset + 1] ?? this.y;
+        this.z = arr[offset + 2] ?? this.z;
+
+        return this;
+    }
+
     /**
      * Converts the vector to string form.
      *
@@ -672,6 +692,27 @@ class Vec3 {
         return `[${this.x}, ${this.y}, ${this.z}]`;
     }
 
+    /**
+     * Converts the vector to an array.
+     *
+     * @param {number[]} [arr] - The array to populate with the color components. If not specified,
+     * a new array is created.
+     * @param {number} [offset] - The zero-based index at which to start copying elements to the
+     * array. Default is 0.
+     * @returns {number[]} The vector as an array.
+     * @example
+     * const v = new pc.Vec3(20, 10, 5);
+     * // Outputs [20, 10, 5]
+     * console.log(v.toArray());
+     */
+    toArray(arr = [], offset = 0) {
+        arr[offset] = this.x;
+        arr[offset + 1] = this.y;
+        arr[offset + 2] = this.z;
+
+        return arr;
+    }
+
     /**
      * A constant vector set to [0, 0, 0].
      *
diff --git a/src/core/math/vec4.js b/src/core/math/vec4.js
index b333a8e28..86d59b0ae 100644
--- a/src/core/math/vec4.js
+++ b/src/core/math/vec4.js
@@ -622,6 +622,27 @@ class Vec4 {
         return this;
     }
 
+    /**
+     * Set the values of the vector from an array.
+     *
+     * @param {number[]} arr - The array to set the vector values from.
+     * @param {number} [offset] - The zero-based index at which to start copying elements from the
+     * array. Default is 0.
+     * @returns {Vec4} Self for chaining.
+     * @example
+     * const v = new pc.Vec4();
+     * v.fromArray([20, 10, 5, 0]);
+     * // v is set to [20, 10, 5, 0]
+     */
+    fromArray(arr, offset = 0) {
+        this.x = arr[offset] ?? this.x;
+        this.y = arr[offset + 1] ?? this.y;
+        this.z = arr[offset + 2] ?? this.z;
+        this.w = arr[offset + 3] ?? this.w;
+
+        return this;
+    }
+
     /**
      * Converts the vector to string form.
      *
@@ -635,6 +656,28 @@ class Vec4 {
         return `[${this.x}, ${this.y}, ${this.z}, ${this.w}]`;
     }
 
+    /**
+     * Converts the vector to an array.
+     *
+     * @param {number[]} [arr] - The array to populate with the color components. If not specified,
+     * a new array is created.
+     * @param {number} [offset] - The zero-based index at which to start copying elements to the
+     * array. Default is 0.
+     * @returns {number[]} The vector as an array.
+     * @example
+     * const v = new pc.Vec3(20, 10, 5, 1);
+     * // Outputs [20, 10, 5, 1]
+     * console.log(v.toArray());
+     */
+    toArray(arr = [], offset = 0) {
+        arr[offset] = this.x;
+        arr[offset + 1] = this.y;
+        arr[offset + 2] = this.z;
+        arr[offset + 3] = this.w;
+
+        return arr;
+    }
+
     /**
      * A constant vector set to [0, 0, 0, 0].
      *


Test patch:
diff --git a/test/core/math/color.test.mjs b/test/core/math/color.test.mjs
index b6377a9d5..1daa7f6db 100644
--- a/test/core/math/color.test.mjs
+++ b/test/core/math/color.test.mjs
@@ -173,6 +173,28 @@ describe('Color', function () {
 
     });
 
+    describe('#fromArray', function () {
+
+        it('sets a color from an array', function () {
+            const c = new Color();
+            c.fromArray([0.1, 0.2, 0.3, 0.4]);
+            expect(c.r).to.equal(0.1);
+            expect(c.g).to.equal(0.2);
+            expect(c.b).to.equal(0.3);
+            expect(c.a).to.equal(0.4);
+        });
+
+        it('sets a color from an array with an offset', function () {
+            const c = new Color();
+            c.fromArray([0, 0.1, 0.2, 0.3, 0.4, 0.5], 1);
+            expect(c.r).to.equal(0.1);
+            expect(c.g).to.equal(0.2);
+            expect(c.b).to.equal(0.3);
+            expect(c.a).to.equal(0.4);
+        });
+
+    });
+
     describe('#toString', function () {
 
         it('returns a string representation of black (no alpha)', function () {
@@ -199,4 +221,21 @@ describe('Color', function () {
         });
 
     });
+
+    describe('#toArray', function () {
+        it('returns an array of 4 values', function () {
+            expect(Color.RED.toArray()).to.eql([1, 0, 0, 1]);
+        });
+
+        it('returns an array of 3 values', function () {
+            expect(Color.RED.toArray(undefined, undefined, false)).to.eql([1, 0, 0]);
+        });
+
+        it('returns an array of 3 values with an offset and target array', function () {
+            const target = [0, 0, 0, 0, 0, 0];
+            Color.RED.toArray(target, 3, false);
+            expect(target).to.eql([0, 0, 0, 1, 0, 0]);
+        });
+
+    });
 });
diff --git a/test/core/math/vec2.test.mjs b/test/core/math/vec2.test.mjs
index e48f36328..2f808b011 100644
--- a/test/core/math/vec2.test.mjs
+++ b/test/core/math/vec2.test.mjs
@@ -533,6 +533,24 @@ describe('Vec2', function () {
 
     });
 
+    describe('#fromArray', function () {
+
+        it('sets a vector from an array of 2 values', function () {
+            const v = new Vec2();
+            v.fromArray([1, 2]);
+            expect(v.x).to.equal(1);
+            expect(v.y).to.equal(2);
+        });
+
+        it('sets a vector from an array with offset', function () {
+            const v = new Vec2();
+            v.fromArray([0, 1, 2, 3], 2);
+            expect(v.x).to.equal(2);
+            expect(v.y).to.equal(3);
+        });
+
+    });
+
     describe('#toString', function () {
 
         it('returns a string representation of a vector', function () {
@@ -542,4 +560,19 @@ describe('Vec2', function () {
 
     });
 
+    describe('#toArray', function () {
+
+        it('returns an array representation of a vector', function () {
+            const v = new Vec2(1, 2);
+            expect(v.toArray()).to.eql([1, 2]);
+        });
+
+        it('returns an array representation of a vector with an offset and target array', function () {
+            const v = new Vec2(1, 2);
+            const array = [0, 0, 0, 0];
+            v.toArray(array, 2);
+            expect(array).to.eql([0, 0, 1, 2]);
+        });
+
+    });
 });
diff --git a/test/core/math/vec3.test.mjs b/test/core/math/vec3.test.mjs
index 109424881..b7bedb908 100644
--- a/test/core/math/vec3.test.mjs
+++ b/test/core/math/vec3.test.mjs
@@ -609,6 +609,26 @@ describe('Vec3', function () {
 
     });
 
+    describe('#fromArray', function () {
+
+        it('sets a vector from an array', function () {
+            const v = new Vec3();
+            v.fromArray([1, 2, 3]);
+            expect(v.x).to.equal(1);
+            expect(v.y).to.equal(2);
+            expect(v.z).to.equal(3);
+        });
+
+        it('sets a vector from an array with an offset', function () {
+            const v = new Vec3();
+            v.fromArray([0, 0, 1, 2, 3], 2);
+            expect(v.x).to.equal(1);
+            expect(v.y).to.equal(2);
+            expect(v.z).to.equal(3);
+        });
+
+    });
+
     describe('#toString', function () {
 
         it('returns a string representation of a vector', function () {
@@ -618,4 +638,20 @@ describe('Vec3', function () {
 
     });
 
+    describe('#toArray', function () {
+
+        it('returns an array representation of a vector', function () {
+            const v = new Vec3(1, 2, 3);
+            expect(v.toArray()).to.eql([1, 2, 3]);
+        });
+
+        it('returns an array representation of a vector with an offset and target array', function () {
+            const v = new Vec3(1, 2, 3);
+            const array = [0, 0, 0, 0, 0, 0];
+            v.toArray(array, 2);
+            expect(array).to.eql([0, 0, 1, 2, 3, 0]);
+        });
+
+    });
+
 });
diff --git a/test/core/math/vec4.test.mjs b/test/core/math/vec4.test.mjs
index 6f5500d5e..e03444d61 100644
--- a/test/core/math/vec4.test.mjs
+++ b/test/core/math/vec4.test.mjs
@@ -590,6 +590,28 @@ describe('Vec4', function () {
 
     });
 
+    describe('#fromArray', function () {
+
+        it('sets a vector from an array', function () {
+            const v = new Vec4();
+            v.fromArray([1, 2, 3, 4]);
+            expect(v.x).to.equal(1);
+            expect(v.y).to.equal(2);
+            expect(v.z).to.equal(3);
+            expect(v.w).to.equal(4);
+        });
+
+        it('sets a vector from an array with an offset', function () {
+            const v = new Vec4();
+            v.fromArray([0, 0, 1, 2, 3, 4], 2);
+            expect(v.x).to.equal(1);
+            expect(v.y).to.equal(2);
+            expect(v.z).to.equal(3);
+            expect(v.w).to.equal(4);
+        });
+
+    });
+
     describe('#toString', function () {
 
         it('returns a string representation of a vector', function () {
@@ -599,4 +621,20 @@ describe('Vec4', function () {
 
     });
 
+    describe('#toArray', function () {
+
+        it('returns an array representation of a vector', function () {
+            const v = new Vec4(1, 2, 3, 4);
+            expect(v.toArray()).to.eql([1, 2, 3, 4]);
+        });
+
+        it('returns an array representation of a vector with an offset and target array', function () {
+            const v = new Vec4(1, 2, 3, 4);
+            const array = [0, 0, 0, 0, 0, 0, 0, 0];
+            v.toArray(array, 2);
+            expect(array).to.eql([0, 0, 1, 2, 3, 4, 0, 0]);
+        });
+
+    });
+
 });