Repository: playcanvas/engine
Pull request #5072 (2023-02-11 15:25:22)

Use nullish coalescing where appropriate
Minor refactor to leverage [nullish coalescing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) across the codebase where it makes sense. It's more succinct and easier to read.

Also leverages default parameters in some places.

I confirm I have read the [contributing guidelines](https://github.com/playcanvas/engine/blob/master/.github/CONTRIBUTING.md) and signed the [Contributor License Agreement](https://docs.google.com/a/playcanvas.com/forms/d/1Ih69zQfJG-QDLIEpHr6CsaAs6fPORNOVnMv5nuo0cjk/viewform).



Patch:
diff --git a/src/core/math/bit-packing.js b/src/core/math/bit-packing.js
new file mode 100644
index 000000000..11d768ec5
--- /dev/null
+++ b/src/core/math/bit-packing.js
@@ -0,0 +1,64 @@
+/**
+ * BitPacking API - functionality for operating on values stored as bits in a number.
+ *
+ * @namespace
+ * @ignore
+ */
+const BitPacking = {
+
+    /**
+     * Sets a value to specified bits of a number.
+     *
+     * @param {number} storage - Number to store the bits into.
+     * @param {number} value - Value to store.
+     * @param {number} shift - Number of bits to shift the value.
+     * @param {number} [mask] - Mask for the value to limit the number of storage bits. Defaults to 1.
+     * @returns {number} Returns the storage updated with the value.
+     */
+    set: function (storage, value, shift, mask = 1) {
+        // clear the space
+        const data = storage & ~(mask << shift);
+
+        // set the bits
+        return data | (value << shift);
+    },
+
+    /**
+     * Gets the value of specified bits from a number.
+     *
+     * @param {number} storage - Number to extract the bits from.
+     * @param {number} shift - Number of bits to shift the mask.
+     * @param {number} [mask] - Mask for the value to limit the number of storage bits. Defaults to 1.
+     * @returns {number} Returns the extracted value.
+     */
+    get: function (storage, shift, mask = 1) {
+        return (storage >> shift) & mask;
+    },
+
+    /**
+     * Tests if all specified bits are set.
+     *
+     * @param {number} storage - Number to test.
+     * @param {number} shift - Number of bits to shift the mask.
+     * @param {number} [mask] - Mask to limit the number of storage bits. Defaults to 1.
+     * @returns {boolean} Returns true if all bits in the mask are set in the storage.
+     */
+    all: function (storage, shift, mask = 1) {
+        const shifted = mask << shift;
+        return (storage & shifted) === shifted;
+    },
+
+    /**
+     * Tests if any specified bits are set.
+     *
+     * @param {number} storage - Number to test.
+     * @param {number} shift - Number of bits to shift the mask.
+     * @param {number} [mask] - Mask to limit the number of storage bits. Defaults to 1.
+     * @returns {boolean} Returns true if any bits in the mask are set in the storage.
+     */
+    any: function (storage, shift, mask = 1) {
+        return (storage & (mask << shift)) !== 0;
+    }
+};
+
+export { BitPacking };
diff --git a/src/framework/anim/controller/anim-state.js b/src/framework/anim/controller/anim-state.js
index a4fae11e7..c5a994f30 100644
--- a/src/framework/anim/controller/anim-state.js
+++ b/src/framework/anim/controller/anim-state.js
@@ -18,25 +18,29 @@ import {
  * @ignore
  */
 class AnimState {
+    /** @private */
+    _animations = {};
+
+    /** @private */
+    _animationList = [];
+
     /**
      * Create a new AnimState instance.
      *
      * @param {AnimController} controller - The controller this AnimState is associated with.
      * @param {string} name - The name of the state. Used to find this state when the controller
      * transitions between states and links animations.
-     * @param {number} speed - The speed animations in the state should play at. Individual
+     * @param {number} [speed] - The speed animations in the state should play at. Individual
      * {@link AnimNodes} can override this value.
-     * @param {boolean} loop - Determines whether animations in this state should loop.
-     * @param {object|null} blendTree - If supplied, the AnimState will recursively build a
+     * @param {boolean} [loop] - Determines whether animations in this state should loop.
+     * @param {object|null} [blendTree] - If supplied, the AnimState will recursively build a
      * {@link AnimBlendTree} hierarchy, used to store, blend and play multiple animations.
      */
-    constructor(controller, name, speed, loop, blendTree) {
+    constructor(controller, name, speed = 1, loop = true, blendTree) {
         this._controller = controller;
         this._name = name;
-        this._animations = {};
-        this._animationList = [];
-        this._speed = speed || 1.0;
-        this._loop = loop === undefined ? true : loop;
+        this._speed = speed;
+        this._loop = loop;
         const findParameter = this._controller.findParameter.bind(this._controller);
         if (blendTree) {
             this._blendTree = this._createTree(
diff --git a/src/framework/asset/asset-file.js b/src/framework/asset/asset-file.js
index 33c4b3ba9..7282336b2 100644
--- a/src/framework/asset/asset-file.js
+++ b/src/framework/asset/asset-file.js
@@ -4,13 +4,13 @@
  * @ignore
  */
 class AssetFile {
-    constructor(url, filename, hash, size, opt, contents) {
-        this.url = url || '';
-        this.filename = filename || '';
-        this.hash = (hash === undefined) ? null : hash;
-        this.size = (size === undefined) ? null : size;
-        this.opt = (opt === undefined) ? null : opt;
-        this.contents = contents || null;
+    constructor(url = '', filename = '', hash = null, size = null, opt = null, contents = null) {
+        this.url = url;
+        this.filename = filename;
+        this.hash = hash;
+        this.size = size;
+        this.opt = opt;
+        this.contents = contents;
     }
 
     // Compare this AssetFile with another. Returns true if they have the same data
diff --git a/src/platform/audio/channel.js b/src/platform/audio/channel.js
index 13a5f2ec8..f8a96b78b 100644
--- a/src/platform/audio/channel.js
+++ b/src/platform/audio/channel.js
@@ -22,9 +22,9 @@ class Channel {
      * end or not.
      */
     constructor(manager, sound, options = {}) {
-        this.volume = (options.volume === undefined) ? 1 : options.volume;
-        this.loop = (options.loop === undefined) ? false : options.loop;
-        this.pitch = (options.pitch === undefined ? 1 : options.pitch);
+        this.volume = options.volume ?? 1;
+        this.loop = options.loop ?? false;
+        this.pitch = options.pitch ?? 1;
 
         this.sound = sound;
 
diff --git a/src/platform/graphics/vertex-format.js b/src/platform/graphics/vertex-format.js
index bdfd71ac3..170c5cc3f 100644
--- a/src/platform/graphics/vertex-format.js
+++ b/src/platform/graphics/vertex-format.js
@@ -159,7 +159,7 @@ class VertexFormat {
                 stride: (vertexCount ? elementSize : (elementDesc.hasOwnProperty('stride') ? elementDesc.stride : this.size)),
                 dataType: elementDesc.type,
                 numComponents: elementDesc.components,
-                normalize: (elementDesc.normalize === undefined) ? false : elementDesc.normalize,
+                normalize: elementDesc.normalize ?? false,
                 size: elementSize
             };
             this._elements.push(element);
diff --git a/src/platform/graphics/webgl/webgl-graphics-device.js b/src/platform/graphics/webgl/webgl-graphics-device.js
index 12b72d348..c2ca41a8b 100644
--- a/src/platform/graphics/webgl/webgl-graphics-device.js
+++ b/src/platform/graphics/webgl/webgl-graphics-device.js
@@ -2062,27 +2062,27 @@ class WebglGraphicsDevice extends GraphicsDevice {
         const defaultOptions = this.defaultClearOptions;
         options = options || defaultOptions;
 
-        const flags = (options.flags === undefined) ? defaultOptions.flags : options.flags;
+        const flags = options.flags ?? defaultOptions.flags;
         if (flags !== 0) {
             const gl = this.gl;
 
             // Set the clear color
             if (flags & CLEARFLAG_COLOR) {
-                const color = (options.color === undefined) ? defaultOptions.color : options.color;
+                const color = options.color ?? defaultOptions.color;
                 this.setClearColor(color[0], color[1], color[2], color[3]);
                 this.setColorWrite(true, true, true, true);
             }
 
             if (flags & CLEARFLAG_DEPTH) {
                 // Set the clear depth
-                const depth = (options.depth === undefined) ? defaultOptions.depth : options.depth;
+                const depth = options.depth ?? defaultOptions.depth;
                 this.setClearDepth(depth);
                 this.setDepthWrite(true);
             }
 
             if (flags & CLEARFLAG_STENCIL) {
                 // Set the clear stencil
-                const stencil = (options.stencil === undefined) ? defaultOptions.stencil : options.stencil;
+                const stencil = options.stencil ?? defaultOptions.stencil;
                 this.setClearStencil(stencil);
             }
 
diff --git a/src/scene/layer.js b/src/scene/layer.js
index 5c345d7bf..40e2b62c9 100644
--- a/src/scene/layer.js
+++ b/src/scene/layer.js
@@ -130,7 +130,7 @@ class Layer {
          * @type {boolean}
          * @private
          */
-        this._enabled = options.enabled === undefined ? true : options.enabled;
+        this._enabled = options.enabled ?? true;
         /**
          * @type {number}
          * @private
@@ -151,7 +151,7 @@ class Layer {
          *
          * @type {number}
          */
-        this.opaqueSortMode = options.opaqueSortMode === undefined ? SORTMODE_MATERIALMESH : options.opaqueSortMode;
+        this.opaqueSortMode = options.opaqueSortMode ?? SORTMODE_MATERIALMESH;
 
         /**
          * Defines the method used for sorting semi-transparent mesh instances before rendering. Can be:
@@ -166,7 +166,7 @@ class Layer {
          *
          * @type {number}
          */
-        this.transparentSortMode = options.transparentSortMode === undefined ? SORTMODE_BACK2FRONT : options.transparentSortMode;
+        this.transparentSortMode = options.transparentSortMode ?? SORTMODE_BACK2FRONT;
 
         if (options.renderTarget) {
             this.renderTarget = options.renderTarget;
@@ -185,7 +185,7 @@ class Layer {
          *
          * @type {number}
          */
-        this.shaderPass = options.shaderPass === undefined ? SHADER_FORWARD : options.shaderPass;
+        this.shaderPass = options.shaderPass ?? SHADER_FORWARD;
 
         /**
          * Tells that this layer is simple and needs to just render a bunch of mesh instances
@@ -194,7 +194,7 @@ class Layer {
          *
          * @type {boolean}
          */
-        this.passThrough = options.passThrough === undefined ? false : options.passThrough;
+        this.passThrough = options.passThrough ?? false;
 
         // clear flags
         /**
diff --git a/src/scene/scene.js b/src/scene/scene.js
index 8729c82b0..5f9ae8e05 100644
--- a/src/scene/scene.js
+++ b/src/scene/scene.js
@@ -710,9 +710,9 @@ class Scene extends EventHandler {
         this.lightmapMaxResolution = render.lightmapMaxResolution;
         this.lightmapMode = render.lightmapMode;
         this.exposure = render.exposure;
-        this._skyboxIntensity = render.skyboxIntensity === undefined ? 1 : render.skyboxIntensity;
-        this._skyboxLuminance = render.skyboxLuminance === undefined ? 20000 : render.skyboxLuminance;
-        this._skyboxMip = render.skyboxMip === undefined ? 0 : render.skyboxMip;
+        this._skyboxIntensity = render.skyboxIntensity ?? 1;
+        this._skyboxLuminance = render.skyboxLuminance ?? 20000;
+        this._skyboxMip = render.skyboxMip ?? 0;
 
         if (render.skyboxRotation) {
             this.skyboxRotation = (new Quat()).setFromEulerAngles(render.skyboxRotation[0], render.skyboxRotation[1], render.skyboxRotation[2]);


Test patch:
diff --git a/test/core/math/bit-packing.test.mjs b/test/core/math/bit-packing.test.mjs
new file mode 100644
index 000000000..9a690d80f
--- /dev/null
+++ b/test/core/math/bit-packing.test.mjs
@@ -0,0 +1,51 @@
+import { BitPacking } from '../../../src/core/math/bit-packing.js';
+
+import { expect } from 'chai';
+
+describe('BitPacking', function () {
+
+    describe('#set', function () {
+
+        it('sets bits', function () {
+
+            let d = 0;
+            d = BitPacking.set(d, 0b11, 1, 0b11);
+            expect(d).to.equal(0b110);
+            d = BitPacking.set(d, 0, 1, 0b11);
+            expect(d).to.equal(0);
+            d = BitPacking.set(d, 1, 3);
+            expect(d).to.equal(0b1000);
+        });
+    });
+
+    describe('#get', function () {
+        it('gets bits', function () {
+
+            const d = 0b110011;
+            expect(BitPacking.get(d, 0, 0b111111)).to.equal(d);
+            expect(BitPacking.get(d, 4, 0b11)).to.equal(0b11);
+            expect(BitPacking.get(d, 3)).to.equal(0);
+            expect(BitPacking.get(d, 5)).to.equal(1);
+        });
+    });
+
+    describe('#any', function () {
+        it('any', function () {
+
+            const d = 0b110011;
+            expect(BitPacking.any(d, 0, 0b111111)).to.equal(true);
+            expect(BitPacking.any(d, 2, 0b11)).to.equal(false);
+            expect(BitPacking.any(d, 2, 0b111)).to.equal(true);
+        });
+    });
+
+    describe('#all', function () {
+        it('all', function () {
+
+            const d = 0b110011;
+            expect(BitPacking.all(d, 0, 0b111111)).to.equal(false);
+            expect(BitPacking.all(d, 2, 0b11)).to.equal(false);
+            expect(BitPacking.all(d, 4, 0b11)).to.equal(true);
+        });
+    });
+});