@woosh/meep-engine 2.34.19 → 2.34.24

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 (26) hide show
  1. package/core/binary/BitSet.js +20 -0
  2. package/core/bvh2/aabb3/aabb3_nearest_point_on_surface.js +65 -0
  3. package/core/collection/list/List.js +30 -0
  4. package/core/geom/3d/shape/UnitCubeShape3D.js +7 -37
  5. package/core/geom/3d/{sphereIntersectsPoint.js → sphere/sphereIntersectsPoint.js} +0 -0
  6. package/core/geom/3d/sphere/sphere_array_intersects_ray.js +25 -0
  7. package/core/geom/3d/sphere/sphere_intersects_ray.js +51 -0
  8. package/core/model/object/invokeObjectClone.js +2 -0
  9. package/core/model/object/invokeObjectCompare.js +9 -0
  10. package/engine/graphics/micron/build/hierarchy/merge_patches.js +7 -1
  11. package/engine/graphics/micron/format/MicronGeometry.js +24 -6
  12. package/engine/graphics/micron/format/serialization/deserialize_patch.js +40 -11
  13. package/engine/graphics/render/forward_plus/LightManager.js +23 -6
  14. package/engine/graphics/render/forward_plus/PointLightData.js +9 -0
  15. package/engine/graphics/render/forward_plus/model/AbstractLight.js +16 -0
  16. package/engine/graphics/render/layers/RenderLayer.js +3 -2
  17. package/engine/graphics/render/view/CameraView.js +2 -1
  18. package/engine/graphics/render/visibility/IncrementalDeltaSet.d.ts +2 -2
  19. package/engine/graphics/render/visibility/IncrementalDeltaSet.js +95 -20
  20. package/engine/graphics/render/visibility/IncrementalDeltaSet.spec.js +46 -0
  21. package/engine/graphics/three/compare_three_objects.js +8 -0
  22. package/engine/sound/ecs/emitter/SoundEmitter.js +59 -0
  23. package/engine/sound/ecs/emitter/SoundEmitterComponentContext.js +8 -0
  24. package/engine/sound/ecs/emitter/SoundEmitterSystem.js +2 -1
  25. package/engine/sound/ecs/emitter/SoundTrack.js +41 -0
  26. package/package.json +1 -1
@@ -509,6 +509,26 @@ BitSet.prototype.getAndClear = function (index) {
509
509
  return v;
510
510
  };
511
511
 
512
+ /**
513
+ *
514
+ * @param {number} distance
515
+ * @param {number} start_index
516
+ * @param {number} end_index
517
+ */
518
+ BitSet.prototype.shift = function (distance, start_index, end_index) {
519
+ if (distance > 0) {
520
+ for (let i = end_index; i >= start_index; i--) {
521
+ const v = this.get(i);
522
+ this.set(i + distance, v);
523
+ }
524
+ } else {
525
+ for (let i = start_index; i <= end_index; i++) {
526
+ const v = this.get(i);
527
+ this.set(i + distance, v);
528
+ }
529
+ }
530
+ };
531
+
512
532
  /**
513
533
  * Sets all of the bits in this BitSet to false.
514
534
  */
@@ -0,0 +1,65 @@
1
+ import { clamp } from "../../math/clamp.js";
2
+ import { min2 } from "../../math/min2.js";
3
+
4
+ //
5
+
6
+ /**
7
+ *
8
+ * @param {number[]|ArrayLike<number>} result
9
+ * @param {number} x0
10
+ * @param {number} y0
11
+ * @param {number} z0
12
+ * @param {number} x1
13
+ * @param {number} y1
14
+ * @param {number} z1
15
+ * @param {number} r_x
16
+ * @param {number} r_y
17
+ * @param {number} r_z
18
+ */
19
+ export function aabb3_nearest_point_on_surface(result, x0, y0, z0, x1, y1, z1, r_x, r_y, r_z) {
20
+ const ar_x0 = Math.abs(r_x - x0);
21
+ const ar_x1 = Math.abs(r_x - x1);
22
+ const ar_x_min = min2(ar_x0, ar_x1);
23
+
24
+ const ar_y0 = Math.abs(r_y - y0);
25
+ const ar_y1 = Math.abs(r_y - y1);
26
+ const ar_y_min = min2(ar_y0, ar_y1);
27
+
28
+ const ar_z0 = Math.abs(r_z - z0);
29
+ const ar_z1 = Math.abs(r_z - z1);
30
+ const ar_z_min = min2(ar_z0, ar_z1);
31
+
32
+ // figure out closest face to the reference point
33
+ if (ar_x_min > ar_y_min && ar_x_min > ar_z_min) {
34
+ // X is smallest, project onto X axis
35
+ if (ar_x0 <= ar_x1) {
36
+ result[0] = x0;
37
+ } else {
38
+ result[0] = x1;
39
+ }
40
+
41
+ result[1] = clamp(r_y, y0, y1);
42
+ result[2] = clamp(r_z, z0, z1);
43
+ } else if (ar_y_min > ar_x_min && ar_y_min > ar_z_min) {
44
+ // Y is smallest, project onto X axis
45
+ if (ar_y0 <= ar_y1) {
46
+ result[1] = y0;
47
+ } else {
48
+ result[1] = y1;
49
+ }
50
+
51
+ result[0] = clamp(r_x, x0, x1);
52
+ result[2] = clamp(r_z, z0, z1);
53
+ } else {
54
+ // project onto Z axis
55
+
56
+ if (ar_z0 <= ar_z1) {
57
+ result[2] = z0;
58
+ } else {
59
+ result[2] = z1;
60
+ }
61
+
62
+ result[0] = clamp(r_x, x0, x1);
63
+ result[1] = clamp(r_y, y0, y1);
64
+ }
65
+ }
@@ -919,4 +919,34 @@ List.prototype.equals = function (other) {
919
919
  return true;
920
920
  };
921
921
 
922
+ /**
923
+ *
924
+ * @param {List<T>} other
925
+ * @returns {number}
926
+ */
927
+ List.prototype.compare = function (other) {
928
+ const l = this.length;
929
+
930
+ const other_length = other.length;
931
+ if (l !== other_length) {
932
+ return l - other_length;
933
+ }
934
+
935
+ for (let i = 0; i < l; i++) {
936
+ const a = this.get(i);
937
+
938
+ const b = other.get(i);
939
+
940
+ const delta = a.compare(b);
941
+
942
+ if (delta !== 0) {
943
+ return delta;
944
+ }
945
+
946
+ }
947
+
948
+ // same
949
+ return 0;
950
+ };
951
+
922
952
  export default List;
@@ -4,8 +4,8 @@ import { v3_length } from "../../v3_length.js";
4
4
  import { max2 } from "../../../math/max2.js";
5
5
  import { max3 } from "../../../math/max3.js";
6
6
  import { min2 } from "../../../math/min2.js";
7
- import { clamp } from "../../../math/clamp.js";
8
7
  import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";
8
+ import { aabb3_nearest_point_on_surface } from "../../../bvh2/aabb3/aabb3_nearest_point_on_surface.js";
9
9
 
10
10
  export class UnitCubeShape3D extends AbstractShape3D {
11
11
 
@@ -18,43 +18,13 @@ export class UnitCubeShape3D extends AbstractShape3D {
18
18
  const r_y = reference[1];
19
19
  const r_z = reference[2];
20
20
 
21
- // figure out closest face to the reference point
22
- const ar_x = Math.abs(r_x);
23
- const ar_y = Math.abs(r_y);
24
- const ar_z = Math.abs(r_z);
21
+ aabb3_nearest_point_on_surface(
22
+ result,
23
+ -0.5, -0.5, -0.5,
24
+ 0.5, 0.5, 0.5,
25
+ r_x, r_y, r_z
26
+ );
25
27
 
26
- if (ar_x > ar_y && ar_x > ar_z) {
27
- // X is smallest, project onto X axis
28
- if (r_x <= 0) {
29
- result[0] = -0.5;
30
- } else {
31
- result[0] = 0.5;
32
- }
33
-
34
- result[1] = clamp(r_y, -0.5, 0.5);
35
- result[2] = clamp(r_z, -0.5, 0.5);
36
- } else if (ar_y > ar_x && ar_y > ar_z) {
37
- // Y is smallest, project onto X axis
38
- if (r_y <= 0) {
39
- result[1] = -0.5;
40
- } else {
41
- result[1] = 0.5;
42
- }
43
-
44
- result[0] = clamp(r_x, -0.5, 0.5);
45
- result[2] = clamp(r_z, -0.5, 0.5);
46
- } else {
47
- // project onto Z axis
48
-
49
- if (r_z <= 0) {
50
- result[2] = -0.5;
51
- } else {
52
- result[2] = 0.5;
53
- }
54
-
55
- result[0] = clamp(r_x, -0.5, 0.5);
56
- result[1] = clamp(r_y, -0.5, 0.5);
57
- }
58
28
  }
59
29
 
60
30
  signed_distance_gradient_at_point(result, point) {
@@ -0,0 +1,25 @@
1
+ //
2
+
3
+ import { sphere_intersects_ray } from "./sphere_intersects_ray.js";
4
+
5
+ /**
6
+ *
7
+ * @param {number[]|ArrayLike<number>} sphere
8
+ * @param {number} origin_x
9
+ * @param {number} origin_y
10
+ * @param {number} origin_z
11
+ * @param {number} direction_x
12
+ * @param {number} direction_y
13
+ * @param {number} direction_z
14
+ * @returns {boolean}
15
+ */
16
+ export function sphere_array_intersects_ray(
17
+ sphere,
18
+ origin_x, origin_y, origin_z,
19
+ direction_x, direction_y, direction_z
20
+ ) {
21
+ return sphere_intersects_ray(sphere[0], sphere[1], sphere[2], sphere[3],
22
+ origin_x, origin_y, origin_z,
23
+ direction_x, direction_y, direction_z
24
+ );
25
+ }
@@ -0,0 +1,51 @@
1
+ //
2
+
3
+ import { v3_dot } from "../../v3_dot.js";
4
+
5
+ /**
6
+ *
7
+ * @param {number} center_x
8
+ * @param {number} center_y
9
+ * @param {number} center_z
10
+ * @param {number} radius
11
+ * @param {number} origin_x
12
+ * @param {number} origin_y
13
+ * @param {number} origin_z
14
+ * @param {number} direction_x
15
+ * @param {number} direction_y
16
+ * @param {number} direction_z
17
+ * @returns {boolean}
18
+ */
19
+ export function sphere_intersects_ray(
20
+ center_x, center_y, center_z, radius,
21
+ origin_x, origin_y, origin_z,
22
+ direction_x, direction_y, direction_z
23
+ ) {
24
+ const delta_x = center_x - origin_x;
25
+ const delta_y = center_y - origin_y;
26
+ const delta_z = center_z - origin_z;
27
+
28
+ const tca = v3_dot(delta_x, delta_y, delta_z, direction_x, direction_y, direction_z);
29
+ const d2 = v3_dot(delta_x, delta_y, delta_z, delta_x, delta_y, delta_z) - tca * tca;
30
+
31
+ const radius2 = radius * radius;
32
+
33
+ if (d2 > radius2) {
34
+ return false;
35
+ }
36
+
37
+ const thc = Math.sqrt(radius2 - d2);
38
+
39
+ // t0 = first intersect point - entrance on front of sphere
40
+ const t0 = tca - thc;
41
+
42
+ // t1 = second intersect point - exit point on back of sphere
43
+ const t1 = tca + thc;
44
+
45
+ // test to see if both t0 and t1 are behind the ray - if so, no intersection
46
+ if (t0 < 0 && t1 < 0) {
47
+ return false;
48
+ }
49
+
50
+ return true;
51
+ }
@@ -6,3 +6,5 @@
6
6
  export function invokeObjectClone(object) {
7
7
  return object.clone();
8
8
  }
9
+
10
+
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @template {{compare:function(T):number}} T
3
+ * @param {T} a
4
+ * @param {T} b
5
+ * @return {number}
6
+ */
7
+ export function invokeObjectCompare(a, b) {
8
+ return a.compare(b);
9
+ }
@@ -513,7 +513,13 @@ export async function merge_patches(
513
513
  return 0;
514
514
  } else if (node_count === 1) {
515
515
  // only 1 patch, no merge opportunity, forward it to output directly
516
- output[output_offset.value++] = nodes[0];
516
+ const node = nodes[0];
517
+
518
+ //set group bounds to produce a valid root
519
+ node.group_aabb.copy(node.bounding_box);
520
+ node.group_bounding_sphere.set(node.bounding_sphere);
521
+
522
+ output[output_offset.value++] = node;
517
523
  return 1;
518
524
  } else {
519
525
 
@@ -3,6 +3,7 @@ import { assert } from "../../../../core/assert.js";
3
3
  import { computeTriangleRayIntersection } from "../../../../core/geom/3d/triangle/computeTriangleRayIntersection.js";
4
4
  import { SurfacePoint3 } from "../../../../core/geom/3d/SurfacePoint3.js";
5
5
  import { aabb3_array_intersects_ray } from "../../../../core/bvh2/aabb3/aabb3_array_intersects_ray.js";
6
+ import { sphere_array_intersects_ray } from "../../../../core/geom/3d/sphere/sphere_array_intersects_ray.js";
6
7
 
7
8
  let id_counter = 0;
8
9
 
@@ -104,16 +105,23 @@ export class MicronGeometry {
104
105
 
105
106
  const patch = stack[stack_cursor];
106
107
 
107
- // using group bounding box because it's conservative, as we want to be able to hit LOD_0
108
- const bb = patch.group_bounding_box;
108
+ // using group bounds because it's conservative, as we want to be able to hit LOD_0
109
109
 
110
- const bb_intersection_exists = aabb3_array_intersects_ray(
111
- bb,
110
+ // sphere test
111
+ if (!sphere_array_intersects_ray(patch.group_bounding_sphere,
112
112
  origin_x, origin_y, origin_z,
113
113
  direction_x, direction_y, direction_z
114
- );
114
+ )) {
115
+ continue;
116
+ }
117
+
115
118
 
116
- if (!bb_intersection_exists) {
119
+ // box test (more expensive, different volume)
120
+ if (!aabb3_array_intersects_ray(
121
+ patch.group_bounding_box,
122
+ origin_x, origin_y, origin_z,
123
+ direction_x, direction_y, direction_z
124
+ )) {
117
125
  // no intersection
118
126
  continue;
119
127
  }
@@ -122,6 +130,16 @@ export class MicronGeometry {
122
130
  // LOD 0 - source geometry
123
131
 
124
132
  // cast against bounds
133
+
134
+ // sphere test
135
+ if (!sphere_array_intersects_ray(patch.bounding_sphere,
136
+ origin_x, origin_y, origin_z,
137
+ direction_x, direction_y, direction_z
138
+ )) {
139
+ continue;
140
+ }
141
+
142
+ // box test
125
143
  if (!aabb3_array_intersects_ray(
126
144
  patch.bounding_box,
127
145
  origin_x, origin_y, origin_z,
@@ -1,6 +1,7 @@
1
1
  import { aabb3_build_corners } from "../../../../../core/geom/3d/aabb/aabb3_build_corners.js";
2
2
  import { DataTypeByteSizes } from "../../../../../core/collection/table/DataTypeByteSizes.js";
3
3
  import { DataType2TypedArrayConstructorMapping } from "../../../../../core/collection/table/DataType2TypedArrayConstructorMapping.js";
4
+ import { v3_distance } from "../../../../../core/geom/v3_distance.js";
4
5
 
5
6
  /**
6
7
  *
@@ -18,17 +19,6 @@ export function deserialize_patch(buffer, patch, vertex_spec, patch_array) {
18
19
  buffer.readFloat32Array(gbb, 0, 6);
19
20
  buffer.readFloat32Array(patch.group_bounding_sphere, 0, 4);
20
21
 
21
- // reconstruct corners
22
- aabb3_build_corners(
23
- patch.group_bounding_box_corners,
24
- 0,
25
- gbb[0],
26
- gbb[1],
27
- gbb[2],
28
- gbb[3],
29
- gbb[4],
30
- gbb[5]
31
- );
32
22
 
33
23
  patch.group_error = buffer.readFloat32();
34
24
  patch.lod = buffer.readUintVar();
@@ -101,4 +91,43 @@ export function deserialize_patch(buffer, patch, vertex_spec, patch_array) {
101
91
 
102
92
  patch.vertex_attribute_arrays[i] = array;
103
93
  }
94
+
95
+
96
+ // validate and fix group bounds as necessary
97
+ if (
98
+ patch.group_bounding_box[0] > patch.bounding_box[0] // x0
99
+ || patch.group_bounding_box[1] > patch.bounding_box[1] // y0
100
+ || patch.group_bounding_box[2] > patch.bounding_box[2] // x0
101
+ || patch.group_bounding_box[3] < patch.bounding_box[3] // x1
102
+ || patch.group_bounding_box[4] < patch.bounding_box[4] // y1
103
+ || patch.group_bounding_box[5] < patch.bounding_box[5] // z1
104
+ ) {
105
+ // bounds are violated, fix
106
+ patch.group_bounding_box.set(patch.bounding_box);
107
+ }
108
+
109
+ if (v3_distance(
110
+ patch.group_bounding_sphere[0],
111
+ patch.group_bounding_sphere[1],
112
+ patch.group_bounding_sphere[2],
113
+ patch.bounding_sphere[0],
114
+ patch.bounding_sphere[1],
115
+ patch.bounding_sphere[2]
116
+ ) + patch.bounding_sphere[3] > patch.group_bounding_sphere[3]) {
117
+ // group's sphere bounds are violated, fix
118
+ patch.group_bounding_sphere.set(patch.bounding_sphere);
119
+ }
120
+
121
+
122
+ // reconstruct corners
123
+ aabb3_build_corners(
124
+ patch.group_bounding_box_corners,
125
+ 0,
126
+ gbb[0],
127
+ gbb[1],
128
+ gbb[2],
129
+ gbb[3],
130
+ gbb[4],
131
+ gbb[5]
132
+ );
104
133
  }
@@ -42,6 +42,9 @@ import { Decal } from "./model/Decal.js";
42
42
  import { ReferencedTextureAtlas } from "../../texture/atlas/ReferencedTextureAtlas.js";
43
43
  import { v3_morton_encode_transformed } from "../../../../core/geom/3d/morton/v3_morton_encode_transformed.js";
44
44
  import { v3_distance } from "../../../../core/geom/v3_distance.js";
45
+ import { array_copy } from "../../../../core/collection/array/copyArray.js";
46
+ import { arrayQuickSort } from "../../../../core/collection/array/arrayQuickSort.js";
47
+ import { invokeObjectCompare } from "../../../../core/model/object/invokeObjectCompare.js";
45
48
 
46
49
  const LOOKUP_CACHE_SIZE = 1024; // must be power of two
47
50
 
@@ -227,7 +230,13 @@ export class LightManager {
227
230
  * @readonly
228
231
  * @private
229
232
  */
230
- this.__visible_lights = new IncrementalDeltaSet();
233
+ this.__visible_lights = new IncrementalDeltaSet(invokeObjectCompare);
234
+ /**
235
+ *
236
+ * @type {PointLightData[]}
237
+ * @private
238
+ */
239
+ this.__sorted_visible_lights = [];
231
240
 
232
241
  /**
233
242
  *
@@ -520,10 +529,18 @@ export class LightManager {
520
529
  * @private
521
530
  */
522
531
  __sort_visible_light() {
523
- this.__visible_lights.sort(
524
- this.__sort_visible_light_score,
525
- this
526
- );
532
+ const sorted_lights = this.__sorted_visible_lights;
533
+ const visible_light_set = this.__visible_lights;
534
+ const visible_light_count = visible_light_set.size;
535
+
536
+ if (sorted_lights.length > visible_light_count) {
537
+ // crop array if it's too big
538
+ sorted_lights.splice(visible_light_count, sorted_lights.length - visible_light_count)
539
+ }
540
+
541
+ array_copy(visible_light_set.elements, 0, sorted_lights, 0, visible_light_count);
542
+
543
+ arrayQuickSort(sorted_lights, this.__sort_visible_light_score, this, 0, visible_light_count - 1);
527
544
  }
528
545
 
529
546
  __update_visible_bvh() {
@@ -615,7 +632,7 @@ export class LightManager {
615
632
  *
616
633
  * @type {PointLightData[]}
617
634
  */
618
- const visible_lights = this.__visible_lights.elements;
635
+ const visible_lights = this.__sorted_visible_lights;
619
636
 
620
637
  // write light data into texture
621
638
  const visible_light_count = visible_lights.length;
@@ -21,6 +21,15 @@ export class PointLightData {
21
21
  this.bvh_leaf_main = new LeafNode(this, 0, 0, 0, 0, 0, 0);
22
22
  }
23
23
 
24
+ /**
25
+ *
26
+ * @param {PointLightData} other
27
+ * @returns {number}
28
+ */
29
+ compare(other) {
30
+ return this.light.compare(other.light);
31
+ }
32
+
24
33
  update() {
25
34
 
26
35
  const leaf = this.bvh_leaf_main;
@@ -1,4 +1,9 @@
1
+ let id_counter = 0;
2
+
1
3
  export class AbstractLight {
4
+ constructor() {
5
+ this.id = id_counter++;
6
+ }
2
7
 
3
8
  /**
4
9
  *
@@ -47,4 +52,15 @@ export class AbstractLight {
47
52
  toArray(destination, address) {
48
53
  throw new Error('Not Implemented');
49
54
  }
55
+
56
+ /**
57
+ *
58
+ * @param {AbstractLight} other
59
+ * @returns {number}
60
+ */
61
+ compare(other) {
62
+ return this.id - other.id;
63
+ }
50
64
  }
65
+
66
+ AbstractLight.prototype.type = 'Abstract';
@@ -12,6 +12,7 @@ import { ThreeFrustumsIntersectionBVHVisitor } from "../../../../core/bvh2/trave
12
12
  import { traverseBinaryNodeUsingVisitor } from "../../../../core/bvh2/traversal/traverseBinaryNodeUsingVisitor.js";
13
13
  import { ThreeClippingPlaneComputingBVHVisitor } from "../../../../core/bvh2/traversal/ThreeClippingPlaneComputingBVHVisitor.js";
14
14
  import { RenderLayerState } from "./RenderLayerState.js";
15
+ import { compare_three_objects } from "../../three/compare_three_objects.js";
15
16
 
16
17
 
17
18
  class VisibleLeafCollector extends BVHVisitor {
@@ -109,9 +110,9 @@ export class RenderLayer {
109
110
 
110
111
  /**
111
112
  * Contains visible elements across all views, if element is in at least one view - it will be here
112
- * @type {IncrementalDeltaSet<Object3D>}
113
+ * @type {IncrementalDeltaSet<THREE.Object3D>}
113
114
  */
114
- this.visibleSet = new IncrementalDeltaSet();
115
+ this.visibleSet = new IncrementalDeltaSet(compare_three_objects);
115
116
 
116
117
  /**
117
118
  *
@@ -5,6 +5,7 @@ import { frustumFromCamera } from "../../ecs/camera/CameraSystem.js";
5
5
  import { Frustum } from "three";
6
6
  import { read_frustum_planes_to_array } from "../forward_plus/cluster/read_frustum_planes_to_array.js";
7
7
  import Signal from "../../../../core/events/signal/Signal.js";
8
+ import { compare_three_objects } from "../../three/compare_three_objects.js";
8
9
 
9
10
  const DEFAULT_FLAGS = CameraViewFlags.Active
10
11
  | CameraViewFlags.AdaptivePlaneNear
@@ -52,7 +53,7 @@ export class CameraView {
52
53
  *
53
54
  * @type {IncrementalDeltaSet<THREE.Object3D>}
54
55
  */
55
- this.visible_objects = new IncrementalDeltaSet();
56
+ this.visible_objects = new IncrementalDeltaSet(compare_three_objects);
56
57
 
57
58
  /**
58
59
  *
@@ -3,6 +3,8 @@ export class IncrementalDeltaSet<T> {
3
3
  version: number
4
4
  size: number
5
5
 
6
+ constructor(compare: (a: T, b: T) => number)
7
+
6
8
  contains(element: T): boolean
7
9
 
8
10
  push(element: T): void
@@ -14,6 +16,4 @@ export class IncrementalDeltaSet<T> {
14
16
  initializeUpdate(): void
15
17
 
16
18
  finalizeUpdate(): void
17
-
18
- sort(f: (T) => number, ctx?: any): void
19
19
  }
@@ -2,7 +2,7 @@ import { BitSet } from "../../../../core/binary/BitSet.js";
2
2
  import Signal from "../../../../core/events/signal/Signal.js";
3
3
  import { assert } from "../../../../core/assert.js";
4
4
  import { IllegalStateException } from "../../../../core/fsm/exceptions/IllegalStateException.js";
5
- import { arrayQuickSort } from "../../../../core/collection/array/arrayQuickSort.js";
5
+ import { binarySearchHighIndex } from "../../../../core/collection/array/binarySearchHighIndex.js";
6
6
 
7
7
  /**
8
8
  * @readonly
@@ -16,15 +16,30 @@ const IncrementalDeltaSetState = {
16
16
 
17
17
  /**
18
18
  * accelerated Set data structure optimized for incremental re-building
19
+ * Always sorted
19
20
  * @template V
20
21
  */
21
22
  export class IncrementalDeltaSet {
22
- constructor() {
23
+ /**
24
+ *
25
+ * @param {function(V,V):number} compare stable comparator
26
+ */
27
+ constructor(compare) {
28
+ assert.isFunction(compare, 'compare');
29
+
23
30
  /**
24
31
  *
25
32
  * @type {V[]}
33
+ * @private
26
34
  */
27
- this.elements = [];
35
+ this.__elements = [];
36
+
37
+ /**
38
+ *
39
+ * @type {function(V, V): number}
40
+ * @private
41
+ */
42
+ this.__compare = compare;
28
43
 
29
44
  /**
30
45
  * Current version of the state, each time the set is updated - version changes
@@ -69,15 +84,39 @@ export class IncrementalDeltaSet {
69
84
  this.state = IncrementalDeltaSetState.Clear;
70
85
  }
71
86
 
87
+ /**
88
+ *
89
+ * @return {V[]}
90
+ */
91
+ get elements() {
92
+ return this.__elements;
93
+ }
72
94
 
73
95
  /**
74
96
  *
97
+ * @param {(V)=>any} visitor
98
+ * @param {*} [visitorContext]
99
+ */
100
+ forEach(visitor, visitorContext) {
101
+ const n = this.size;
102
+ const elements = this.__elements;
103
+
104
+ for (let i = 0; i < n; i++) {
105
+ const el = elements[i];
106
+
107
+ visitor.call(visitorContext, el);
108
+ }
109
+ }
110
+
111
+
112
+ /**
113
+ * @deprecated
75
114
  * @param {function(T):number} score_function
76
115
  * @param {*} [score_context]
77
116
  * @returns {void}
78
117
  */
79
118
  sort(score_function, score_context) {
80
- arrayQuickSort(this.elements, score_function, score_context, 0, this.size - 1);
119
+ throw new Error('Deprecated, the set is always sorted');
81
120
  }
82
121
 
83
122
  /**
@@ -86,7 +125,33 @@ export class IncrementalDeltaSet {
86
125
  * @returns {boolean}
87
126
  */
88
127
  contains(element) {
89
- return this.elements.indexOf(element) !== -1;
128
+ return this.__indexOf(element) !== -1;
129
+ }
130
+
131
+ /**
132
+ *
133
+ * @param {V} element
134
+ * @return {number}
135
+ * @private
136
+ */
137
+ __indexOf(element) {
138
+ const i = this.__indexExpected(element);
139
+
140
+ if (this.__elements[i] === element) {
141
+ return i;
142
+ } else {
143
+ return -1;
144
+ }
145
+ }
146
+
147
+ /**
148
+ *
149
+ * @param {V} element
150
+ * @return {number}
151
+ * @private
152
+ */
153
+ __indexExpected(element) {
154
+ return binarySearchHighIndex(this.__elements, element, this.__compare, 0, this.size - 1);
90
155
  }
91
156
 
92
157
  /**
@@ -99,10 +164,10 @@ export class IncrementalDeltaSet {
99
164
  throw new IllegalStateException("Cannot remove during build");
100
165
  }
101
166
 
102
- const i = this.elements.indexOf(element, 0);
167
+ const i = this.__indexOf(element);
103
168
 
104
169
  if (i !== -1) {
105
- this.elements.splice(i, 1);
170
+ this.__elements.splice(i, 1);
106
171
  this.size--;
107
172
 
108
173
  this.version++;
@@ -119,7 +184,7 @@ export class IncrementalDeltaSet {
119
184
  return;
120
185
  }
121
186
 
122
- this.elements.splice(0, this.size);
187
+ this.__elements.splice(0, this.size);
123
188
  this.size = 0;
124
189
  this.flags.reset();
125
190
 
@@ -153,14 +218,17 @@ export class IncrementalDeltaSet {
153
218
  let deletions = 0;
154
219
 
155
220
  //go through all dirty flags and remove corresponding elements
221
+ const flags = this.flags;
222
+ const elements = this.__elements;
223
+
156
224
  for (
157
- let i = this.flags.nextSetBit(0);
225
+ let i = flags.nextSetBit(0);
158
226
  i !== -1;
159
- i = this.flags.nextSetBit(i + 1)
227
+ i = flags.nextSetBit(i + 1)
160
228
  ) {
161
229
  const index = i - deletions;
162
230
 
163
- const removed = this.elements.splice(index, 1);
231
+ const removed = elements.splice(index, 1);
164
232
 
165
233
  this.onRemoved.send1(removed[0]);
166
234
 
@@ -170,7 +238,7 @@ export class IncrementalDeltaSet {
170
238
  //update size
171
239
  this.size -= deletions;
172
240
 
173
- this.flags.reset();
241
+ flags.reset();
174
242
 
175
243
  this.state = IncrementalDeltaSetState.Ready;
176
244
 
@@ -184,19 +252,26 @@ export class IncrementalDeltaSet {
184
252
  */
185
253
  push(element) {
186
254
  //check if element is already in the set
187
- const elements = this.elements;
188
- const i = elements.indexOf(element); //TODO this can probably be done faster. Maybe use a bloom filter? or some kind of a hash
255
+ const index = this.__indexExpected(element);
189
256
 
190
- if (i === -1) {
191
- elements.push(element);
257
+ const elements = this.__elements;
258
+
259
+ if (elements[index] !== element) {
260
+ // element is not in the set
261
+
262
+ // shift flags to make space for it
263
+ this.flags.shift(1, index, this.size);
264
+
265
+ // insert
266
+ elements.splice(index, 0, element);
192
267
  this.size++;
193
268
 
194
269
  this.onAdded.send1(element);
195
- } else {
196
- //Existing element. We're keeping it. clear dirty flag
197
- this.flags.clear(i);
198
270
  }
199
271
 
272
+ // clear flag for inserted element
273
+ this.flags.clear(index);
274
+
200
275
  }
201
276
 
202
277
  /**
@@ -204,7 +279,7 @@ export class IncrementalDeltaSet {
204
279
  * @param {IncrementalDeltaSet<V>} other
205
280
  */
206
281
  pushOther(other) {
207
- const elements = other.elements;
282
+ const elements = other.__elements;
208
283
  const n = other.size;
209
284
  for (let i = 0; i < n; i++) {
210
285
  const el = elements[i];
@@ -0,0 +1,46 @@
1
+ import { IncrementalDeltaSet } from "./IncrementalDeltaSet.js";
2
+ import { compareNumbersAscending } from "../../../../core/function/Functions.js";
3
+
4
+ test('set is sorted after insert, push 1,2', () => {
5
+ const set = new IncrementalDeltaSet(compareNumbersAscending);
6
+
7
+ set.push(1);
8
+ set.push(2);
9
+
10
+ expect(set.elements[0]).toBe(1);
11
+ expect(set.elements[1]).toBe(2);
12
+ });
13
+
14
+ test('set is sorted after insert, push 2,1', () => {
15
+ const set = new IncrementalDeltaSet(compareNumbersAscending);
16
+
17
+ set.push(2);
18
+ set.push(1);
19
+
20
+ expect(set.elements[0]).toBe(1);
21
+ expect(set.elements[1]).toBe(2);
22
+ });
23
+
24
+ test('set is sorted after insert, push 1,2,3', () => {
25
+ const set = new IncrementalDeltaSet(compareNumbersAscending);
26
+
27
+ set.push(1);
28
+ set.push(2);
29
+ set.push(3);
30
+
31
+ expect(set.elements[0]).toBe(1);
32
+ expect(set.elements[1]).toBe(2);
33
+ expect(set.elements[2]).toBe(3);
34
+ });
35
+
36
+ test('set is sorted after insert, push 3,2,1', () => {
37
+ const set = new IncrementalDeltaSet(compareNumbersAscending);
38
+
39
+ set.push(3);
40
+ set.push(2);
41
+ set.push(1);
42
+
43
+ expect(set.elements[0]).toBe(1);
44
+ expect(set.elements[1]).toBe(2);
45
+ expect(set.elements[2]).toBe(3);
46
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ *
3
+ * @param {THREE.Object3D} a
4
+ * @param {THREE.Object3D} b
5
+ */
6
+ export function compare_three_objects(a, b) {
7
+ return a.id - b.id;
8
+ }
@@ -16,6 +16,8 @@ import { objectKeyByValue } from "../../../../core/model/object/objectKeyByValue
16
16
  import { computeStringHash } from "../../../../core/primitives/strings/computeStringHash.js";
17
17
  import { computeHashIntegerArray } from "../../../../core/collection/array/computeHashIntegerArray.js";
18
18
  import { computeHashFloat } from "../../../../core/math/hash/computeHashFloat.js";
19
+ import { compareStrings } from "../../../../core/primitives/strings/compareStrings.js";
20
+ import { compareNumbers } from "../../../../core/primitives/numbers/compareNumbers.js";
19
21
 
20
22
  /**
21
23
  * Convert decibel to percentage volume
@@ -127,6 +129,63 @@ export class SoundEmitter {
127
129
 
128
130
  }
129
131
 
132
+ /**
133
+ *
134
+ * @param {SoundEmitter} other
135
+ * @returns number
136
+ */
137
+ compare(other) {
138
+ const tracks = this.tracks.compare(other.tracks);
139
+
140
+ if (tracks !== 0) {
141
+ return tracks;
142
+ }
143
+
144
+ const channel = compareStrings(this.channel, other.channel);
145
+
146
+ if (channel !== 0) {
147
+ return channel;
148
+ }
149
+
150
+ const distanceMin = compareNumbers(this.__distanceMin, other.__distanceMin);
151
+
152
+ if (distanceMin !== 0) {
153
+ return distanceMin;
154
+ }
155
+ const distanceMax = compareNumbers(this.__distanceMax, other.__distanceMax);
156
+
157
+ if (distanceMax !== 0) {
158
+ return distanceMax;
159
+ }
160
+
161
+ const panningModel = compareNumbers(this.panningModel, other.panningModel);
162
+
163
+ if (panningModel !== 0) {
164
+ return panningModel;
165
+ }
166
+
167
+ const attenuation = compareNumbers(this.attenuation, other.attenuation);
168
+
169
+ if (attenuation !== 0) {
170
+ return attenuation;
171
+ }
172
+
173
+ const flags = compareNumbers(this.flags, other.flags);
174
+
175
+ if (flags !== 0) {
176
+ return flags;
177
+ }
178
+
179
+ const volume = compareNumbers(this.volume.getValue(), other.volume.getValue());
180
+
181
+ if (volume !== 0) {
182
+ return volume;
183
+ }
184
+
185
+
186
+ return 0;
187
+ }
188
+
130
189
 
131
190
  /**
132
191
  *
@@ -45,6 +45,14 @@ export class SoundEmitterComponentContext {
45
45
  this.__isConnected = false;
46
46
  }
47
47
 
48
+ /**
49
+ *
50
+ * @param {SoundEmitterComponentContext} other
51
+ */
52
+ compare(other) {
53
+ return this.emitter.compare(other.emitter);
54
+ }
55
+
48
56
  update() {
49
57
 
50
58
  const position = this.transform.position;
@@ -18,6 +18,7 @@ import { tryRotateSingleNode } from "../../../../core/bvh2/transform/RotationOpt
18
18
  import { SoundEmitterComponentContext } from "./SoundEmitterComponentContext.js";
19
19
  import { SoundEmitterFlags } from "./SoundEmitterFlags.js";
20
20
  import { SoundEmitterChannel } from "./SoundEmitterChannel.js";
21
+ import { invokeObjectCompare } from "../../../../core/model/object/invokeObjectCompare.js";
21
22
 
22
23
  /**
23
24
  * @readonly
@@ -96,7 +97,7 @@ export class SoundEmitterSystem extends System {
96
97
  *
97
98
  * @type {IncrementalDeltaSet<SoundEmitterComponentContext>}
98
99
  */
99
- this.activeSet = new IncrementalDeltaSet();
100
+ this.activeSet = new IncrementalDeltaSet(invokeObjectCompare);
100
101
 
101
102
  /**
102
103
  * Spatial index
@@ -4,6 +4,8 @@ import { assert } from "../../../../core/assert.js";
4
4
  import { computeStringHash } from "../../../../core/primitives/strings/computeStringHash.js";
5
5
  import { computeHashIntegerArray } from "../../../../core/collection/array/computeHashIntegerArray.js";
6
6
  import { computeHashFloat } from "../../../../core/math/hash/computeHashFloat.js";
7
+ import { compareStrings } from "../../../../core/primitives/strings/compareStrings.js";
8
+ import { compareNumbers } from "../../../../core/primitives/numbers/compareNumbers.js";
7
9
 
8
10
  const DEFAULT_FLAGS = SoundTrackFlags.StartWhenReady;
9
11
 
@@ -55,6 +57,45 @@ export class SoundTrack {
55
57
  this.nodes = null;
56
58
  }
57
59
 
60
+ /**
61
+ *
62
+ * @param {SoundTrack} other
63
+ * @returns number
64
+ */
65
+ compare(other) {
66
+ const url = compareStrings(this.url, other.url);
67
+
68
+ if (url !== 0) {
69
+ return url;
70
+ }
71
+
72
+ const time = compareNumbers(this.time, other.time);
73
+
74
+ if (time !== 0) {
75
+ return time;
76
+ }
77
+
78
+ const channel = compareStrings(this.channel, other.channel);
79
+
80
+ if (channel !== 0) {
81
+ return channel;
82
+ }
83
+
84
+ const volume = compareNumbers(this.__volume, other.__volume);
85
+
86
+ if (volume !== 0) {
87
+ return volume;
88
+ }
89
+
90
+ const flags = compareNumbers(this.flags, other.flags);
91
+
92
+ if (flags !== 0) {
93
+ return flags;
94
+ }
95
+
96
+ return 0;
97
+ }
98
+
58
99
  /**
59
100
  *
60
101
  * @param {number|SoundEmitterFlags} flag
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "productName": "Meep",
6
6
  "description": "production-ready JavaScript game engine based on Entity Component System Architecture",
7
7
  "author": "Alexander Goldring",
8
- "version": "2.34.19",
8
+ "version": "2.34.24",
9
9
  "dependencies": {
10
10
  "gl-matrix": "3.4.3",
11
11
  "fast-levenshtein": "2.0.6",