@woosh/meep-engine 2.84.6 → 2.84.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/build/meep.cjs +137 -107
  2. package/build/meep.min.js +1 -1
  3. package/build/meep.module.js +137 -107
  4. package/package.json +10 -5
  5. package/src/core/cache/LoadingCache.js +2 -1
  6. package/src/core/cache/LoadingCache.spec.js +13 -1
  7. package/src/core/collection/array/arrayIndexByEquality.js +1 -0
  8. package/src/core/collection/list/List.js +25 -17
  9. package/src/core/collection/list/List.spec.js +73 -0
  10. package/src/core/geom/3d/SurfacePoint3.js +3 -40
  11. package/src/core/geom/Vector3.js +25 -14
  12. package/src/core/model/stat/LinearModifier.spec.js +5 -6
  13. package/src/core/process/PromiseWatcher.spec.js +27 -23
  14. package/src/core/process/worker/WorkerBuilder.js +5 -1
  15. package/src/core/process/worker/WorkerProxy.js +3 -2
  16. package/src/engine/animation/behavior/animateProperty.js +4 -4
  17. package/src/engine/ecs/Entity.spec.js +33 -0
  18. package/src/engine/ecs/EntityComponentDataset.js +17 -11
  19. package/src/engine/ecs/EntityReference.js +12 -0
  20. package/src/engine/ecs/dynamic_actions/actions/definition/SpeakLineActionDescription.js +1 -1
  21. package/src/engine/ecs/transform/Transform.js +1 -1
  22. package/src/engine/ecs/transform/Transform.spec.js +44 -0
  23. package/src/engine/graphics/camera/CameraShake.js +1 -127
  24. package/src/engine/graphics/camera/CameraShakeBehavior.js +91 -0
  25. package/src/engine/graphics/camera/CameraShakeTraumaBehavior.js +38 -0
  26. package/src/engine/graphics/ecs/animation/animator/AnimationGraphSystem.js +9 -23
  27. package/src/engine/graphics/ecs/animation/animator/blending/BlendStateMatrix.js +11 -6
  28. package/src/engine/graphics/ecs/animation/animator/graph/AnimationGraph.js +20 -12
  29. package/src/engine/graphics/ecs/animation/animator/graph/AnimationState.js +3 -3
  30. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.js +1 -1
  31. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTable.js +6 -6
  32. package/src/engine/intelligence/behavior/ecs/SendEventBehavior.js +43 -0
  33. package/src/view/View.js +52 -30
  34. package/src/view/writeCssTransformMatrix.js +26 -0
@@ -2638,35 +2638,32 @@ let Vector3$1 = class Vector3 {
2638
2638
 
2639
2639
  /**
2640
2640
  *
2641
- * @param {ArrayList<number>|number[]|Float32Array} m4
2641
+ * @param {ArrayLike<number>|number[]|Float32Array} m4
2642
2642
  */
2643
2643
  applyMatrix4(m4) {
2644
2644
  const x = this.x;
2645
2645
  const y = this.y;
2646
2646
  const z = this.z;
2647
2647
 
2648
- const _x = m4[0] * x + m4[4] * y + m4[8] * z + m4[12];
2649
- const _y = m4[1] * x + m4[5] * y + m4[9] * z + m4[13];
2650
- const _z = m4[2] * x + m4[6] * y + m4[10] * z + m4[14];
2648
+ const w = 1 / (m4[3] * x + m4[7] * y + m4[11] * z + m4[15]);
2649
+
2650
+ const _x = (m4[0] * x + m4[4] * y + m4[8] * z + m4[12])* w;
2651
+ const _y = (m4[1] * x + m4[5] * y + m4[9] * z + m4[13])* w;
2652
+ const _z = (m4[2] * x + m4[6] * y + m4[10] * z + m4[14])* w;
2651
2653
 
2652
2654
  this.set(_x, _y, _z);
2653
2655
  }
2654
2656
 
2655
2657
  /**
2656
2658
  * Assume current vector holds a direction, transform using a matrix to produce a new directional unit vector
2657
- * @param {THREE.Matrix4} m
2659
+ * @param {ArrayLike<number>|number[]|Float32Array} m4
2658
2660
  */
2659
- transformDirection_three(m) {
2660
-
2661
- // input: THREE.Matrix4 affine matrix
2662
- // vector interpreted as a direction
2663
-
2661
+ applyDirectionMatrix4(m4){
2664
2662
  const x = this.x, y = this.y, z = this.z;
2665
- const e = m.elements;
2666
2663
 
2667
- const _x = e[0] * x + e[4] * y + e[8] * z;
2668
- const _y = e[1] * x + e[5] * y + e[9] * z;
2669
- const _z = e[2] * x + e[6] * y + e[10] * z;
2664
+ const _x = m4[0] * x + m4[4] * y + m4[8] * z;
2665
+ const _y = m4[1] * x + m4[5] * y + m4[9] * z;
2666
+ const _z = m4[2] * x + m4[6] * y + m4[10] * z;
2670
2667
 
2671
2668
  // normalize the result
2672
2669
  const _l = 1 / v3_length(_x, _y, _z);
@@ -2678,6 +2675,20 @@ let Vector3$1 = class Vector3 {
2678
2675
  );
2679
2676
  }
2680
2677
 
2678
+ /**
2679
+ * @deprecated use non-three.js version instead
2680
+ * @param {THREE.Matrix4} m
2681
+ */
2682
+ transformDirection_three(m) {
2683
+
2684
+ // input: THREE.Matrix4 affine matrix
2685
+ // vector interpreted as a direction
2686
+
2687
+ const e = m.elements;
2688
+
2689
+ this.applyDirectionMatrix4(e);
2690
+ }
2691
+
2681
2692
  /**
2682
2693
  *
2683
2694
  * @param {THREE.Matrix3} m
@@ -4741,7 +4752,7 @@ class Transform {
4741
4752
  get forward() {
4742
4753
  const result = Vector3$1.forward.clone();
4743
4754
 
4744
- result.applyMatrix4(this.matrix);
4755
+ result.applyDirectionMatrix4(this.matrix);
4745
4756
 
4746
4757
  return result;
4747
4758
  }
@@ -47189,44 +47200,8 @@ class SurfacePoint3 {
47189
47200
  */
47190
47201
  applyMatrix4(m) {
47191
47202
 
47192
- // transform position
47193
- const p = this.position;
47194
-
47195
- const p_x = p.x;
47196
- const p_y = p.y;
47197
- const p_z = p.z;
47198
-
47199
- // compute perspective projection
47200
- const w = 1 / (m[3] * p_x + m[7] * p_y + m[11] * p_z + m[15]);
47201
-
47202
- const result_p_x = (m[0] * p_x + m[4] * p_y + m[8] * p_z + m[12]) * w;
47203
- const result_p_y = (m[1] * p_x + m[5] * p_y + m[9] * p_z + m[13]) * w;
47204
- const result_p_z = (m[2] * p_x + m[6] * p_y + m[10] * p_z + m[14]) * w;
47205
-
47206
- p.set(
47207
- result_p_x,
47208
- result_p_y,
47209
- result_p_z
47210
- );
47211
-
47212
- // transform normal
47213
- const n = this.normal;
47214
-
47215
- const n_x = n.x;
47216
- const n_y = n.y;
47217
- const n_z = n.z;
47218
-
47219
- const result_n_x = m[0] * n_x + m[4] * n_y + m[8] * n_z;
47220
- const result_n_y = m[1] * n_x + m[5] * n_y + m[9] * n_z;
47221
- const result_n_z = m[2] * n_x + m[6] * n_y + m[10] * n_z;
47222
-
47223
- const normal_multiplier = 1 / v3_length(result_n_x, result_n_y, result_n_z);
47224
-
47225
- n.set(
47226
- result_n_x * normal_multiplier,
47227
- result_n_y * normal_multiplier,
47228
- result_n_z * normal_multiplier,
47229
- );
47203
+ this.position.applyMatrix4(m);
47204
+ this.normal.applyDirectionMatrix4(m);
47230
47205
  }
47231
47206
 
47232
47207
  /**
@@ -61116,6 +61091,7 @@ function objectsEqual(a, b) {
61116
61091
  }
61117
61092
 
61118
61093
  /**
61094
+ * Works similarly to `Array.prototype.indexOf`, but instead of strict equality - uses provided equality method
61119
61095
  * @template T
61120
61096
  * @param {T[]} array
61121
61097
  * @param {T} element
@@ -61219,7 +61195,7 @@ class List {
61219
61195
  this.data = array !== undefined ? array.slice() : [];
61220
61196
 
61221
61197
  /**
61222
- *
61198
+ * Number of elements in the list
61223
61199
  * @type {number}
61224
61200
  */
61225
61201
  this.length = this.data.length;
@@ -61788,30 +61764,30 @@ class List {
61788
61764
  }
61789
61765
 
61790
61766
  /**
61791
- *
61767
+ * @deprecated use `#reset` directly in combination with `this.on.removed` signal
61792
61768
  * @param {function(element:T,index:number)} callback
61793
61769
  * @param {*} [thisArg]
61794
61770
  */
61795
61771
  resetViaCallback(callback, thisArg) {
61772
+
61796
61773
  const length = this.length;
61797
- if (length > 0) {
61798
61774
 
61799
- const removed = this.on.removed;
61775
+ const removed = this.on.removed;
61800
61776
 
61801
- const oldElements = this.data;
61777
+ const data = this.data;
61802
61778
 
61803
- //only signal if there are listeners attached
61804
- for (let i = length - 1; i >= 0; i--) {
61805
- const element = oldElements[i];
61806
- // decrement data length gradually to allow handlers access to the rest of the elements
61807
- this.data.length = i;
61808
- this.length = i;
61809
- removed.send2(element, i);
61779
+ for (let i = length - 1; i >= 0; i--) {
61780
+ const element = data[i];
61810
61781
 
61811
- callback.call(thisArg, element, i);
61812
- }
61782
+ // decrement data length gradually to allow handlers access to the rest of the elements
61783
+ data.length = i;
61784
+ this.length = i;
61813
61785
 
61786
+ removed.send2(element, i);
61787
+
61788
+ callback.call(thisArg, element, i);
61814
61789
  }
61790
+
61815
61791
  }
61816
61792
 
61817
61793
  reset() {
@@ -62031,11 +62007,19 @@ class List {
62031
62007
  }
62032
62008
 
62033
62009
  /**
62034
- *
62010
+ * First element in the list
62011
+ * @returns {T|undefined}
62012
+ */
62013
+ first() {
62014
+ return this.get(0);
62015
+ }
62016
+
62017
+ /**
62018
+ * Last element in the list
62035
62019
  * @return {T|undefined}
62036
62020
  */
62037
62021
  last() {
62038
- return this.data[this.length - 1];
62022
+ return this.get(this.length - 1);
62039
62023
  }
62040
62024
 
62041
62025
  /**
@@ -63119,10 +63103,11 @@ class WorkerProxy {
63119
63103
  }
63120
63104
 
63121
63105
  /**
63106
+ * Invoke a given method on the worker, as defined by the `WorkerBuilder`
63122
63107
  * @template T
63123
- * @param {number} name
63108
+ * @param {number} name Method's name
63124
63109
  * @param {Array} args
63125
- * @return {Promise<T>}
63110
+ * @return {Promise<T>} eventual result of the invoked method
63126
63111
  */
63127
63112
  $submitRequest(name, args) {
63128
63113
  const pending = this.__pending[name];
@@ -63345,6 +63330,10 @@ class WorkerBuilder {
63345
63330
  functions = [];
63346
63331
  preamble = new LineBuilder();
63347
63332
 
63333
+ /**
63334
+ *
63335
+ * @param {string} code
63336
+ */
63348
63337
  addCode(code) {
63349
63338
  this.preamble.add(code);
63350
63339
  }
@@ -79747,28 +79736,19 @@ function m3_cm_compose_transform(
79747
79736
  }
79748
79737
 
79749
79738
  /**
79750
- * @author Alex Goldring, 2018
79751
- * @copyright Alex Goldring 2018
79739
+ * Smallest safe increment for a Float32
79740
+ * @see https://www.cplusplus.com/reference/cfloat/
79741
+ * @see https://bitbashing.io/comparing-floats.html
79742
+ * @type {number}
79752
79743
  */
79753
-
79754
-
79755
-
79756
- const scratch_m3_0 = new Float32Array(9);
79757
-
79744
+ const FLT_EPSILON_32 = 1.192092896E-7;
79745
+
79758
79746
  /**
79759
- * @see https://dev.opera.com/articles/understanding-the-css-transforms-matrix/
79760
- * @param domElement
79761
- * @param {Vector2} position
79762
- * @param {Vector2} scale
79763
- * @param {number} rotation angle in radians
79747
+ *
79748
+ * @param {Float32Array} m3
79749
+ * @param {HTMLElement} domElement
79764
79750
  */
79765
- function setElementTransform(domElement, position, scale, rotation) {
79766
-
79767
- const m3 = scratch_m3_0;
79768
-
79769
- m3_cm_compose_transform(m3, position.x, position.y, scale.x, scale.y, 0, 0, rotation);
79770
-
79771
-
79751
+ function writeCssTransformMatrix(m3, domElement) {
79772
79752
  /*
79773
79753
  * CSS matrix is:
79774
79754
  * a c e
@@ -79788,7 +79768,13 @@ function setElementTransform(domElement, position, scale, rotation) {
79788
79768
  const style = domElement.style;
79789
79769
 
79790
79770
  style.transform = transform;
79791
- }
79771
+ }
79772
+
79773
+ /**
79774
+ * @author Alex Goldring, 2018
79775
+ * @copyright Alex Goldring 2018
79776
+ */
79777
+
79792
79778
 
79793
79779
  /**
79794
79780
  *
@@ -79827,6 +79813,9 @@ const INITIAL_FLAGS = ViewFlags.Visible;
79827
79813
  * @class
79828
79814
  */
79829
79815
  class View {
79816
+ #transform_written = new Float32Array(9);
79817
+ #transform_current = new Float32Array(9);
79818
+
79830
79819
  /**
79831
79820
  * @constructor
79832
79821
  */
@@ -79851,31 +79840,32 @@ class View {
79851
79840
  this.flags = INITIAL_FLAGS;
79852
79841
 
79853
79842
  /**
79854
- *
79843
+ * @readonly
79855
79844
  * @type {Vector2}
79856
79845
  */
79857
79846
  const position = this.position = new Vector2(0, 0);
79858
79847
 
79859
79848
  /**
79860
- *
79849
+ * @readonly
79861
79850
  * @type {Vector1}
79862
79851
  */
79863
79852
  const rotation = this.rotation = new Vector1(0);
79864
79853
 
79865
79854
  /**
79866
- *
79855
+ * @readonly
79867
79856
  * @type {Vector2}
79868
79857
  */
79869
79858
  const scale = this.scale = new Vector2(1, 1);
79870
79859
 
79871
79860
  /**
79872
- *
79861
+ * @readonly
79873
79862
  * @type {Vector2}
79874
79863
  */
79875
79864
  const size = this.size = new Vector2(0, 0);
79876
79865
 
79877
79866
  /**
79878
79867
  * Origin from which rotation and scaling is applied
79868
+ * @readonly
79879
79869
  * @type {Vector2}
79880
79870
  */
79881
79871
  this.transformOrigin = new Vector2(0.5, 0.5);
@@ -80013,7 +80003,41 @@ class View {
80013
80003
  * @private
80014
80004
  */
80015
80005
  __updateTransform() {
80016
- setElementTransform(this.el, this.position, this.scale, this.rotation.getValue());
80006
+ const position = this.position;
80007
+ const scale = this.scale;
80008
+ const rotation = this.rotation.getValue();
80009
+
80010
+ m3_cm_compose_transform(this.#transform_current, position.x, position.y, scale.x, scale.y, 0, 0, rotation);
80011
+
80012
+ this.#tryWriteTransform();
80013
+ }
80014
+
80015
+ #tryWriteTransform() {
80016
+
80017
+ const current = this.#transform_current;
80018
+ const written = this.#transform_written;
80019
+
80020
+ for (let i = 0; i < 9; i++) {
80021
+ const a = current[i];
80022
+ const b = written[i];
80023
+
80024
+ if (epsilonEquals(a, b, FLT_EPSILON_32)) {
80025
+ // common path
80026
+ continue;
80027
+ }
80028
+
80029
+ this.#writeTransform();
80030
+ return true;
80031
+
80032
+ }
80033
+
80034
+ return false;
80035
+ }
80036
+
80037
+ #writeTransform() {
80038
+ writeCssTransformMatrix(this.#transform_current, this.el);
80039
+
80040
+ this.#transform_written.set(this.#transform_current);
80017
80041
  }
80018
80042
 
80019
80043
  /**
@@ -93077,10 +93101,10 @@ class EntityComponentDataset {
93077
93101
 
93078
93102
  /**
93079
93103
  *
93080
- * @param {number} entityIndex
93104
+ * @param {number} entity_id
93081
93105
  */
93082
- removeEntity(entityIndex) {
93083
- if (!this.entityExists(entityIndex)) {
93106
+ removeEntity(entity_id) {
93107
+ if (!this.entityExists(entity_id)) {
93084
93108
  // entity doesn't exist
93085
93109
  return;
93086
93110
  }
@@ -93088,26 +93112,31 @@ class EntityComponentDataset {
93088
93112
  const componentOccupancy = this.componentOccupancy;
93089
93113
  const typeCount = this.componentTypeCount;
93090
93114
 
93091
- const occupancyStart = entityIndex * typeCount;
93115
+ const occupancyStart = entity_id * typeCount;
93092
93116
  const occupancyEnd = occupancyStart + typeCount;
93093
93117
 
93094
- for (let i = componentOccupancy.nextSetBit(occupancyStart); i < occupancyEnd && i !== -1; i = componentOccupancy.nextSetBit(i + 1)) {
93118
+ // remove all components from the entity
93119
+ for (
93120
+ let i = componentOccupancy.nextSetBit(occupancyStart);
93121
+ i < occupancyEnd && i !== -1;
93122
+ i = componentOccupancy.nextSetBit(i + 1)
93123
+ ) {
93095
93124
  const componentIndex = i % typeCount;
93096
- this.removeComponentFromEntityByIndex_Unchecked(entityIndex, componentIndex, i);
93125
+ this.removeComponentFromEntityByIndex_Unchecked(entity_id, componentIndex, i);
93097
93126
  }
93098
93127
 
93099
93128
  //dispatch event
93100
- this.sendEvent(entityIndex, EventType.EntityRemoved, entityIndex);
93129
+ this.sendEvent(entity_id, EventType.EntityRemoved, entity_id);
93101
93130
 
93102
93131
  //purge all event listeners
93103
- delete this.__entityEventListeners[entityIndex];
93104
- delete this.__entityAnyEventListeners[entityIndex];
93132
+ delete this.__entityEventListeners[entity_id];
93133
+ delete this.__entityAnyEventListeners[entity_id];
93105
93134
 
93106
- this.entityOccupancy.set(entityIndex, false);
93135
+ this.entityOccupancy.set(entity_id, false);
93107
93136
 
93108
93137
  this.entityCount--;
93109
93138
 
93110
- this.onEntityRemoved.send1(entityIndex);
93139
+ this.onEntityRemoved.send1(entity_id);
93111
93140
  }
93112
93141
 
93113
93142
  /**
@@ -93157,6 +93186,7 @@ class EntityComponentDataset {
93157
93186
  }
93158
93187
 
93159
93188
  /**
93189
+ * This method doesn't perform any checks, make sure you understand what you are doing when using it
93160
93190
  * @private
93161
93191
  * @param {number} entityIndex
93162
93192
  * @param {number} componentIndex
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "description": "Fully featured ECS game engine written in JavaScript",
6
6
  "type": "module",
7
7
  "author": "Alexander Goldring",
8
- "version": "2.84.6",
8
+ "version": "2.84.9",
9
9
  "main": "build/meep.module.js",
10
10
  "module": "build/meep.module.js",
11
11
  "exports": {
@@ -18,6 +18,7 @@
18
18
  "./*.md": "./*"
19
19
  },
20
20
  "scripts": {
21
+ "test:ci": "jest --config ./jest.conf.json --collectCoverage --coverageDirectory=\"./coverage\" --ci --reporters=default --reporters=jest-junit --watchAll=false",
21
22
  "build-module": "rollup -c rollup.config.js",
22
23
  "publish-npm": "npm publish"
23
24
  },
@@ -34,6 +35,9 @@
34
35
  "package.json",
35
36
  "README.md"
36
37
  ],
38
+ "optionalDependencies": {
39
+ "three": "0.136.0"
40
+ },
37
41
  "dependencies": {
38
42
  "fastest-levenshtein": "1.0.16",
39
43
  "gl-matrix": "3.4.3",
@@ -46,16 +50,17 @@
46
50
  "three": ">=0.135.0"
47
51
  },
48
52
  "devDependencies": {
49
- "@babel/core": "7.22.9",
50
- "@babel/preset-env": "7.22.9",
53
+ "@babel/core": "7.22.15",
54
+ "@babel/preset-env": "7.22.15",
51
55
  "@rollup/plugin-commonjs": "25.0.2",
52
56
  "@rollup/plugin-node-resolve": "15.1.0",
53
57
  "@rollup/plugin-terser": "0.4.3",
54
58
  "@rollup/plugin-strip": "3.0.2",
55
59
  "@types/three": "^0.135.0",
56
60
  "babel-jest": "29.5.0",
57
- "jest": "29.5.0",
58
- "jest-environment-jsdom": "29.5.0",
61
+ "jest": "29.6.4",
62
+ "jest-environment-jsdom": "29.6.4",
63
+ "jest-junit": "16.0.0",
59
64
  "rollup": "3.26.2"
60
65
  },
61
66
  "keywords": [
@@ -99,9 +99,10 @@ export class LoadingCache {
99
99
  /**
100
100
  *
101
101
  * @param {K} key
102
+ * @returns {boolean}
102
103
  */
103
104
  invalidate(key) {
104
- this.#internal.remove(key);
105
+ return this.#internal.remove(key);
105
106
  }
106
107
 
107
108
  /**
@@ -1,5 +1,5 @@
1
- import { LoadingCache } from "./LoadingCache.js";
2
1
  import { delay } from "../process/delay.js";
2
+ import { LoadingCache } from "./LoadingCache.js";
3
3
 
4
4
  test("successful load", async () => {
5
5
  const cache = new LoadingCache({
@@ -11,6 +11,18 @@ test("successful load", async () => {
11
11
  expect(await cache.get(1)).toEqual(17);
12
12
  });
13
13
 
14
+ test("when loader throws an exception, we should get a failed promise", async () => {
15
+ const cache = new LoadingCache({
16
+ load(key) {
17
+ throw 1;
18
+ }
19
+ });
20
+
21
+ const promise = cache.get("a");
22
+
23
+ await expect(promise).rejects.toEqual(1);
24
+ });
25
+
14
26
  test("record reuse", async () => {
15
27
  const load = jest.fn(async () => 17);
16
28
 
@@ -1,4 +1,5 @@
1
1
  /**
2
+ * Works similarly to `Array.prototype.indexOf`, but instead of strict equality - uses provided equality method
2
3
  * @template T
3
4
  * @param {T[]} array
4
5
  * @param {T} element
@@ -43,7 +43,7 @@ class List {
43
43
  this.data = array !== undefined ? array.slice() : [];
44
44
 
45
45
  /**
46
- *
46
+ * Number of elements in the list
47
47
  * @type {number}
48
48
  */
49
49
  this.length = this.data.length;
@@ -628,30 +628,30 @@ class List {
628
628
  }
629
629
 
630
630
  /**
631
- *
631
+ * @deprecated use `#reset` directly in combination with `this.on.removed` signal
632
632
  * @param {function(element:T,index:number)} callback
633
633
  * @param {*} [thisArg]
634
634
  */
635
635
  resetViaCallback(callback, thisArg) {
636
+
636
637
  const length = this.length;
637
- if (length > 0) {
638
638
 
639
- const removed = this.on.removed;
639
+ const removed = this.on.removed;
640
640
 
641
- const oldElements = this.data;
641
+ const data = this.data;
642
642
 
643
- //only signal if there are listeners attached
644
- for (let i = length - 1; i >= 0; i--) {
645
- const element = oldElements[i];
646
- // decrement data length gradually to allow handlers access to the rest of the elements
647
- this.data.length = i;
648
- this.length = i;
649
- removed.send2(element, i);
650
-
651
- callback.call(thisArg, element, i);
652
- }
643
+ for (let i = length - 1; i >= 0; i--) {
644
+ const element = data[i];
645
+
646
+ // decrement data length gradually to allow handlers access to the rest of the elements
647
+ data.length = i;
648
+ this.length = i;
653
649
 
650
+ removed.send2(element, i);
651
+
652
+ callback.call(thisArg, element, i);
654
653
  }
654
+
655
655
  }
656
656
 
657
657
  reset() {
@@ -875,11 +875,19 @@ class List {
875
875
  }
876
876
 
877
877
  /**
878
- *
878
+ * First element in the list
879
+ * @returns {T|undefined}
880
+ */
881
+ first() {
882
+ return this.get(0);
883
+ }
884
+
885
+ /**
886
+ * Last element in the list
879
887
  * @return {T|undefined}
880
888
  */
881
889
  last() {
882
- return this.data[this.length - 1];
890
+ return this.get(this.length - 1);
883
891
  }
884
892
 
885
893
  /**
@@ -9,6 +9,18 @@ class DummyNumber {
9
9
  equals(o) {
10
10
  return o.v === this.v;
11
11
  }
12
+
13
+ copy(other) {
14
+ this.v = other.v;
15
+ }
16
+
17
+ clone() {
18
+ const r = new DummyNumber();
19
+
20
+ r.copy(this);
21
+
22
+ return r;
23
+ }
12
24
  }
13
25
 
14
26
  test("constructor doesn't throw", () => {
@@ -254,4 +266,65 @@ test("patch", () => {
254
266
 
255
267
  expect(list.asArray()).toEqual([3, 1, 2]);
256
268
  expect(list.length).toBe(3);
269
+ });
270
+
271
+ test("equals", () => {
272
+
273
+ expect(
274
+ new List().equals(new List())
275
+ ).toBe(true);
276
+
277
+ expect(
278
+ new List(["a"]).equals(new List(["a"]))
279
+ ).toBe(true);
280
+
281
+ expect(
282
+ new List(["a"]).equals(new List(["b"]))
283
+ ).toBe(false);
284
+
285
+ expect(
286
+ new List(["a", "b"]).equals(new List(["a"]))
287
+ ).toBe(false);
288
+
289
+ expect(
290
+ new List(["a", "b"]).equals(new List(["b"]))
291
+ ).toBe(false);
292
+
293
+ expect(
294
+ new List(["a", "b"]).equals(new List(["a", "b"]))
295
+ ).toBe(true);
296
+
297
+ });
298
+
299
+ test("fist", () => {
300
+
301
+ expect(new List([1]).first()).toBe(1);
302
+ expect(new List([1, 2]).first()).toBe(1);
303
+
304
+ });
305
+ test("last", () => {
306
+
307
+ expect(new List([1]).last()).toBe(1);
308
+ expect(new List([1, 2]).last()).toBe(2);
309
+
310
+ });
311
+
312
+ test("deepCopy", () => {
313
+
314
+ const a = new List();
315
+ const b = new List();
316
+
317
+ b.deepCopy(a);
318
+
319
+ expect(b.isEmpty()).toBe(true);
320
+
321
+ const v = new DummyNumber(7);
322
+ b.addAll([v]);
323
+
324
+ a.deepCopy(b);
325
+
326
+ expect(a.length).toBe(1);
327
+ expect(a.first()).toBeDefined();
328
+ expect(a.first().v).toBe(7);
329
+ expect(a.first()).not.toBe(v);
257
330
  });