@woosh/meep-engine 2.43.3 → 2.43.4

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 (48) hide show
  1. package/core/binary/BinaryBuffer.js +13 -1
  2. package/core/binary/BitSet.js +2 -2
  3. package/core/collection/array/array_range_equal_strict.js +22 -0
  4. package/core/color/sRGB_to_linear.js +9 -4
  5. package/core/geom/3d/plane/orient3d_fast.js +3 -0
  6. package/core/geom/3d/plane/orient3d_robust.js +41 -0
  7. package/core/geom/3d/sphere/harmonics/README.md +15 -0
  8. package/core/geom/3d/sphere/harmonics/sh3_add.js +21 -0
  9. package/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.js +618 -0
  10. package/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.js +49 -0
  11. package/core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.js +53 -0
  12. package/core/geom/3d/tetrahedra/TetrahedralMesh.js +251 -68
  13. package/core/geom/3d/tetrahedra/TetrahedralMesh.spec.js +80 -3
  14. package/core/geom/3d/tetrahedra/build_tetrahedral_mesh_buffer_geometry.js +75 -0
  15. package/core/geom/3d/tetrahedra/delaunay/Cavity.js +5 -1
  16. package/core/geom/3d/tetrahedra/delaunay/compute_delaunay_tetrahedral_mesh.js +30 -31
  17. package/core/geom/3d/tetrahedra/delaunay/fill_in_a_cavity.js +54 -18
  18. package/core/geom/3d/tetrahedra/delaunay/push_boundary_with_validation.js +27 -0
  19. package/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_compute_cavity.js +89 -0
  20. package/core/geom/3d/tetrahedra/delaunay/{tetrahedral_mesh_walk_toward_cavity.js → tetrahedral_mesh_walk_towards_containing_tetrahedron.js} +15 -12
  21. package/core/geom/3d/tetrahedra/delaunay/validate_cavity_boundary.js +60 -0
  22. package/core/geom/3d/tetrahedra/{point_in_tetrahedron_circumsphere.js → in_sphere_fast.js} +2 -4
  23. package/core/geom/3d/tetrahedra/in_sphere_robust.js +53 -0
  24. package/core/geom/3d/tetrahedra/prototypeTetrahedraBuilder.js +44 -35
  25. package/core/geom/3d/tetrahedra/validate_tetrahedral_mesh.js +85 -38
  26. package/core/geom/3d/util/make_justified_point_grid.js +31 -0
  27. package/core/process/delay.js +5 -0
  28. package/editor/Editor.js +3 -0
  29. package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.js +195 -11
  30. package/editor/ecs/component/editors/ecs/ParameterTrackSetEditor.js +16 -0
  31. package/editor/ecs/component/editors/ecs/ParticleEmitterLayerEditor.js +4 -0
  32. package/engine/EngineHarness.js +11 -5
  33. package/engine/ecs/terrain/ecs/TerrainSystem.js +7 -1
  34. package/engine/ecs/transform/copy_three_transform.js +15 -0
  35. package/engine/graphics/ecs/light/Light.js +6 -1
  36. package/engine/graphics/ecs/light/LightSystem.d.ts +1 -1
  37. package/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.js +2 -17
  38. package/engine/graphics/geometry/instancing/InstancedMeshGroup.js +2 -2
  39. package/engine/graphics/sh3/LightProbeVolume.js +595 -0
  40. package/engine/graphics/sh3/SH3VisualisationMaterial.js +79 -0
  41. package/engine/graphics/sh3/prototypeSH3Probe.js +427 -0
  42. package/engine/graphics/sh3/visualise_probe.js +40 -0
  43. package/engine/graphics/texture/atlas/TextureAtlas.js +15 -3
  44. package/engine/intelligence/blackboard/AbstractBlackboard.d.ts +1 -1
  45. package/package.json +2 -1
  46. package/samples/terrain/from_image_2.js +127 -82
  47. package/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_compute_cavity2.js +0 -224
  48. package/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_insert_point.js +0 -98
@@ -0,0 +1,53 @@
1
+ /**
2
+ *
3
+ *
4
+ * c1 = 0.42904276540489171563379376569857; // 4 * Â2.Y22 = 1/4 * sqrt(15.PI)
5
+ * c2 = 0.51166335397324424423977581244463; // 0.5 * Â1.Y10 = 1/2 * sqrt(PI/3)
6
+ * c3 = 0.24770795610037568833406429782001; // Â2.Y20 = 1/16 * sqrt(5.PI)
7
+ * c4 = 0.88622692545275801364908374167057; // Â0.Y00 = 1/2 * sqrt(PI)
8
+ *
9
+ * @see https://github.com/mrdoob/three.js/blob/d081c5a3501d272d19375fab1b01fedf9df29b22/src/math/SphericalHarmonics3.js#L85
10
+ * @see https://graphics.stanford.edu/papers/envmap/envmap.pdf
11
+ * @see https://www.ppsloan.org/publications/StupidSH36.pdf
12
+ *
13
+ * @param {number[]} result Result will be written here
14
+ * @param {number} result_offset
15
+ * @param {number[]} harmonics coefficients are read from here
16
+ * @param {number} harmonics_offset offset into coefficients array where to start reading the data
17
+ * @param {number} dimension_count number of encoded dimensions, this is essentially a shortcut for multiple harmonics being read at the same time, such as with RGB values
18
+ * @param {number[]} direction 3d vector read from here, we will read 3 values from here to sample spherical harmonic, direction is assumed to be normalized
19
+ * @param {number} direction_offset offset into direction array
20
+ */
21
+ export function sh3_sample_irradiance_by_direction(
22
+ result, result_offset,
23
+ harmonics, harmonics_offset,
24
+ dimension_count,
25
+ direction, direction_offset
26
+ ) {
27
+ const x = direction[direction_offset];
28
+ const y = direction[direction_offset + 1];
29
+ const z = direction[direction_offset + 2];
30
+
31
+ let channel_value;
32
+
33
+ for (let i = 0; i < dimension_count; i++) {
34
+
35
+ // band 0
36
+ channel_value = harmonics[harmonics_offset + i] * 0.88622692545275801364908374167057;
37
+
38
+ // band 1
39
+ channel_value += harmonics[harmonics_offset + dimension_count + i] * 2.0 * 0.51166335397324424423977581244463 * y;
40
+ channel_value += harmonics[harmonics_offset + dimension_count * 2 + i] * 2.0 * 0.51166335397324424423977581244463 * z;
41
+ channel_value += harmonics[harmonics_offset + dimension_count * 3 + i] * 2.0 * 0.51166335397324424423977581244463 * x;
42
+
43
+ // band 2
44
+ channel_value += harmonics[harmonics_offset + dimension_count * 4 + i] * 2.0 * 0.42904276540489171563379376569857 * x * y;
45
+ channel_value += harmonics[harmonics_offset + dimension_count * 5 + i] * 2.0 * 0.42904276540489171563379376569857 * y * z;
46
+ channel_value += harmonics[harmonics_offset + dimension_count * 6 + i] * (3 * z * z - 1) * 0.24770795610037568833406429782001;
47
+ channel_value += harmonics[harmonics_offset + dimension_count * 7 + i] * 2.0 * 0.42904276540489171563379376569857 * x * z;
48
+ channel_value += harmonics[harmonics_offset + dimension_count * 8 + i] * 0.42904276540489171563379376569857 * (x * x - y * y);
49
+
50
+ // write out
51
+ result[result_offset + i] = channel_value;
52
+ }
53
+ }
@@ -1,15 +1,25 @@
1
1
  import { tetrahedron_contains_point } from "./tetrahedron_contains_point.js";
2
- import { BitSet } from "../../../binary/BitSet.js";
3
2
  import { typed_array_copy } from "../../../collection/array/typed/typed_array_copy.js";
4
3
  import { max3 } from "../../../math/max3.js";
5
4
  import { assert } from "../../../assert.js";
5
+ import { array_copy } from "../../../collection/array/copyArray.js";
6
+ import { array_quick_sort_by_comparator } from "../../../collection/array/arrayQuickSort.js";
7
+ import { compareNumbersDescending } from "../../../function/Functions.js";
8
+ import { BinaryBuffer } from "../../../binary/BinaryBuffer.js";
9
+ import { Base64 } from "../../../binary/Base64.js";
10
+
11
+ /**
12
+ * @readonly
13
+ * @type {number}
14
+ */
15
+ const LAYOUT_TETRA_WORD_COUNT = 8;
6
16
 
7
17
  /**
8
18
  * Size in bytes of a single tetrahedron record
9
19
  * @readonly
10
20
  * @type {number}
11
21
  */
12
- export const LAYOUT_TETRA_BYTE_SIZE = 8 * 4;
22
+ export const LAYOUT_TETRA_BYTE_SIZE = LAYOUT_TETRA_WORD_COUNT * 4;
13
23
 
14
24
  /**
15
25
  * @readonly
@@ -80,6 +90,13 @@ export class TetrahedralMesh {
80
90
  */
81
91
  this.__buffer = new ArrayBuffer(initial_size * LAYOUT_TETRA_BYTE_SIZE);
82
92
 
93
+ /**
94
+ *
95
+ * @type {Uint32Array}
96
+ * @private
97
+ */
98
+ this.__data_uint32 = new Uint32Array(this.__buffer);
99
+
83
100
  /**
84
101
  *
85
102
  * @type {DataView}
@@ -99,15 +116,21 @@ export class TetrahedralMesh {
99
116
  * @type {number}
100
117
  * @private
101
118
  */
102
- this.__length = 0;
119
+ this.__used_end = 0;
103
120
 
121
+ /**
122
+ * Unused slots
123
+ * @type {number[]}
124
+ * @private
125
+ */
126
+ this.__free = [];
104
127
 
105
128
  /**
106
- * Marks which indices are in use
107
- * @type {BitSet}
129
+ *
130
+ * @type {number}
108
131
  * @private
109
132
  */
110
- this.__occupancy = new BitSet();
133
+ this.__free_pointer = 0;
111
134
  }
112
135
 
113
136
  /**
@@ -116,8 +139,11 @@ export class TetrahedralMesh {
116
139
  * @param {*} [thisArg]
117
140
  */
118
141
  forEach(visitor, thisArg) {
119
- const occupancy = this.__occupancy;
120
- for (let i = occupancy.nextSetBit(0); i !== -1; i = occupancy.nextSetBit(i + 1)) {
142
+ for (let i = 0; i < this.__used_end; i++) {
143
+ if (!this.exists(i)) {
144
+ continue;
145
+ }
146
+
121
147
  visitor.call(thisArg, i, this);
122
148
  }
123
149
  }
@@ -134,7 +160,7 @@ export class TetrahedralMesh {
134
160
  return;
135
161
  }
136
162
 
137
- if (capacity < this.__capacity && capacity < this.__occupancy.size()) {
163
+ if (capacity < this.__capacity && capacity < this.__used_end) {
138
164
  throw new Error('Reducing capacity would result in dropping information. This is an illegal operation. If you need to reduce capacity - either drop data or compact the layout first.');
139
165
  }
140
166
 
@@ -150,6 +176,7 @@ export class TetrahedralMesh {
150
176
  // set new buffer
151
177
  this.__buffer = new_buffer;
152
178
  this.__view = new DataView(new_buffer);
179
+ this.__data_uint32 = new Uint32Array(new_buffer);
153
180
 
154
181
  // write new capacity
155
182
  this.__capacity = capacity;
@@ -168,20 +195,20 @@ export class TetrahedralMesh {
168
195
  * @return {number}
169
196
  */
170
197
  size() {
171
- return this.__length;
198
+ return this.__used_end;
172
199
  }
173
200
 
174
201
  /**
175
- *
202
+ * Grow capacity to at least the specified size
176
203
  * @param {number} capacity
177
204
  */
178
- ensureCapacity(capacity) {
205
+ growCapacity(capacity) {
179
206
 
180
207
  const existing_capacity = this.__capacity;
181
208
 
182
209
  const new_capacity = max3(
183
210
  capacity,
184
- existing_capacity * CAPACITY_GROW_MULTIPLIER,
211
+ Math.ceil(existing_capacity * CAPACITY_GROW_MULTIPLIER),
185
212
  existing_capacity + CAPACITY_GROW_MIN_STEP
186
213
  );
187
214
 
@@ -189,23 +216,36 @@ export class TetrahedralMesh {
189
216
  }
190
217
 
191
218
  /**
192
- *
219
+ * NOTE: this method can be quite slow in cases of sparse allocation, please prefer not to use it
193
220
  * @param {number} tet
194
221
  * @return {boolean}
195
222
  */
196
223
  exists(tet) {
197
- return this.__occupancy.get(tet);
224
+
225
+ if (tet < 0 || tet >= this.__used_end) {
226
+ return false;
227
+ }
228
+
229
+ for (let i = 0; i < this.__free_pointer; i++) {
230
+ const free = this.__free[i];
231
+
232
+ if (tet === free) {
233
+ return false;
234
+ }
235
+ }
236
+
237
+ return true;
198
238
  }
199
239
 
200
240
  /**
201
241
  * NOTE: the neighbour value must be encoded, see format specification for details
202
242
  * @param {number} tetra_index
203
243
  * @param {number} neighbour_index
204
- * @returns {number}
244
+ * @returns {number} index of the neighbour encoded with the opposite corner
205
245
  */
206
246
  getNeighbour(tetra_index, neighbour_index) {
207
247
  assert.isNonNegativeInteger(tetra_index, 'tetra_index');
208
- assert.ok(this.__occupancy.get(tetra_index), 'tetrahedron does not exist');
248
+ assert.ok(this.exists(tetra_index), 'tetrahedron does not exist');
209
249
 
210
250
  assert.isNonNegativeInteger(neighbour_index, 'neighbour_index');
211
251
 
@@ -219,12 +259,12 @@ export class TetrahedralMesh {
219
259
  * NOTE: the neighbour value must be encoded, see format specification for details
220
260
  * @param {number} tetra_index
221
261
  * @param {number} neighbour_index which neighbour to set (0..11)
222
- * @param {number} neighbour index of the neighbour
262
+ * @param {number} neighbour index of the neighbour encoded with the opposite corner
223
263
  */
224
264
  setNeighbour(tetra_index, neighbour_index, neighbour) {
225
265
 
226
266
  assert.isNonNegativeInteger(tetra_index, 'tetra_index');
227
- assert.ok(this.__occupancy.get(tetra_index), 'tetrahedron does not exist');
267
+ assert.ok(this.exists(tetra_index), 'tetrahedron does not exist');
228
268
 
229
269
  assert.isNonNegativeInteger(neighbour_index, 'neighbour_index');
230
270
  assert.isNonNegativeInteger(neighbour, 'neighbour');
@@ -245,7 +285,7 @@ export class TetrahedralMesh {
245
285
  getVertexIndex(tet_index, point_index) {
246
286
  assert.isNonNegativeInteger(tet_index, 'tet_index');
247
287
  assert.lessThanOrEqual(tet_index, MAX_TET_INDEX, 'max index exceeded');
248
- assert.ok(this.__occupancy.get(tet_index), 'tetrahedron does not exist');
288
+ //assert.ok(this.exists(tet_index), 'tetrahedron does not exist');
249
289
 
250
290
  assert.isNonNegativeInteger(point_index, 'point_index');
251
291
  assert.lessThan(point_index, 4, 'point_index must be less than 4');
@@ -263,7 +303,7 @@ export class TetrahedralMesh {
263
303
  setVertexIndex(tet_index, point_index, vertex) {
264
304
  assert.isNonNegativeInteger(tet_index, 'tet_index');
265
305
  assert.lessThanOrEqual(tet_index, MAX_TET_INDEX, 'max index exceeded');
266
- assert.ok(this.__occupancy.get(tet_index), 'tetrahedron does not exist');
306
+ //assert.ok(this.exists(tet_index), 'tetrahedron does not exist');
267
307
 
268
308
  assert.isNonNegativeInteger(point_index, 'point_index');
269
309
  assert.lessThan(point_index, 4, 'point_index must be less than 4');
@@ -293,24 +333,6 @@ export class TetrahedralMesh {
293
333
  return false;
294
334
  }
295
335
 
296
- validateLength() {
297
- return this.countOccupancy() === this.__length;
298
- }
299
-
300
- /**
301
- * DEBUG method
302
- * Counts number of occupied(used) elements in the pool
303
- * Should be the same as __length
304
- * @return {number}
305
- */
306
- countOccupancy() {
307
- let r = 0;
308
- for (let i = this.__occupancy.nextSetBit(0); i !== -1; i = this.__occupancy.nextSetBit(i + 1)) {
309
- r++;
310
- }
311
-
312
- return r;
313
- }
314
336
 
315
337
  /**
316
338
  * Allocate empty tet
@@ -318,17 +340,20 @@ export class TetrahedralMesh {
318
340
  * @return {number} index of allocated tetrahedron
319
341
  */
320
342
  allocate() {
321
- const tetra_index = this.__occupancy.nextClearBit(0);
343
+ if (this.__free_pointer > 0) {
344
+ this.__free_pointer--;
345
+ return this.__free[this.__free_pointer];
346
+ }
347
+
348
+ const tetra_index = this.__used_end;
322
349
 
323
- const required_capacity = tetra_index + 1;
350
+ this.__used_end++;
324
351
 
325
- if (required_capacity > this.__capacity) {
352
+ if (tetra_index >= this.__capacity) {
326
353
  // needs to be increased in size
327
- this.ensureCapacity(required_capacity);
354
+ this.growCapacity(tetra_index);
328
355
  }
329
356
 
330
- this.__occupancy.set(tetra_index, true);
331
-
332
357
  // initialize neighbours
333
358
  for (let i = 0; i < 4; i++) {
334
359
  this.setNeighbour(tetra_index, i, INVALID_NEIGHBOUR);
@@ -336,7 +361,6 @@ export class TetrahedralMesh {
336
361
 
337
362
  //assert(this.validateLength());
338
363
 
339
- this.__length++;
340
364
 
341
365
  // assert(this.validateLength());
342
366
 
@@ -374,13 +398,11 @@ export class TetrahedralMesh {
374
398
  }
375
399
 
376
400
  /**
377
- * Remove tetrahedron
401
+ * Sets back-links on neighbours to this tet to INVALID_NEIGHBOUR basically making them into mesh surface
402
+ * This is a useful method for when you want to completely remove a given tet from the mesh to make sure that no dangling references will remain
378
403
  * @param {number} tetra_index
379
404
  */
380
- remove(tetra_index) {
381
- assert.isNonNegativeInteger(tetra_index, 'tera_index');
382
- assert.equal(this.__occupancy.get(tetra_index), true, 'tetrahedron does not exist');
383
-
405
+ disconnect(tetra_index) {
384
406
  // find neighbours and remove reference to self
385
407
  for (let i = 0; i < 4; i++) {
386
408
  const neighbour_encoded = this.getNeighbour(tetra_index, i);
@@ -397,43 +419,60 @@ export class TetrahedralMesh {
397
419
  // clear reference to self
398
420
  this.setNeighbour(neighbour_index, neighbour_point, INVALID_NEIGHBOUR);
399
421
  }
422
+ }
400
423
 
401
- //assert(this.validateLength());
424
+ /**
425
+ * Remove tetrahedron
426
+ * Please note that if there are any dangling references in the mesh neighbourhood - you will need to take care of that separately
427
+ * @param {number} tetra_index
428
+ */
429
+ delete(tetra_index) {
430
+ assert.isNonNegativeInteger(tetra_index, 'tera_index');
431
+ assert.lessThan(tetra_index, this.__used_end, 'attempting to remove tet outside of valid region');
432
+ // assert.equal(this.__occupancy.get(tetra_index), true, 'tetrahedron does not exist');
433
+
434
+
435
+ if (tetra_index === this.__used_end - 1) {
436
+ // tet was at the end of the allocated space
437
+ this.__used_end--;
438
+ } else {
439
+ // mark as dead
440
+ this.__free[this.__free_pointer++] = tetra_index;
441
+ }
402
442
 
403
- // mark as dead
404
- this.__occupancy.clear(tetra_index);
405
- this.__length--;
443
+ //assert(this.validateLength());
406
444
 
407
445
  //assert(this.validateLength());
408
446
  }
409
447
 
410
448
  /**
411
- * Remove tetrahedrons who's points touch the "super-tetrahedron's" points that was inserted originally
449
+ * Used mainly to remove tetrahedrons whos points touch the "super-tetrahedron's" points that was inserted originally
412
450
  * These points are identified by an offset + count parameters
413
- * @param {number[]|Float32Array} points
414
451
  * @param {number} range_start
415
452
  * @param {number} range_end
416
453
  */
417
- removeTetrasConnectedToPoints(points, range_start, range_end) {
418
- const data_view = this.__view;
454
+ removeTetrasConnectedToPoints(range_start, range_end) {
419
455
 
420
- let j = 0;
456
+ for (let i = this.__used_end - 1; i >= 0; i--) {
421
457
 
422
- const occupancy = this.__occupancy;
423
- for (let i = occupancy.nextSetBit(0); i !== -1; i = occupancy.nextSetBit(i + 1)) {
458
+ for (let j = 0; j < 4; j++) {
459
+ const point_index = this.getVertexIndex(i, j);
424
460
 
425
- const tet_address = i * LAYOUT_TETRA_BYTE_SIZE;
461
+ if (point_index >= range_start && point_index <= range_end) {
426
462
 
427
- for (j = 0; j < 4; j++) {
428
- const point_index = data_view.getUint32(tet_address + 4 * j);
463
+ if (!this.exists(i)) {
464
+ // tet doesn't actually exist (deallocated)
465
+ break;
466
+ }
429
467
 
430
- if (point_index >= range_start && point_index <= range_end) {
431
468
  // point index is in range, tetra should be removed
432
- this.remove(i);
469
+ this.disconnect(i);
470
+ this.delete(i);
433
471
  break;
434
472
  }
435
473
  }
436
474
  }
475
+
437
476
  }
438
477
 
439
478
  /**
@@ -446,7 +485,7 @@ export class TetrahedralMesh {
446
485
  const data_view = this.__view;
447
486
 
448
487
  // TODO improve this
449
- const n = this.__length;
488
+ const n = this.__used_end;
450
489
  for (let i = 0; i < n; i++) {
451
490
 
452
491
  const tet_address = i * LAYOUT_TETRA_BYTE_SIZE;
@@ -464,4 +503,148 @@ export class TetrahedralMesh {
464
503
  return -1;
465
504
  }
466
505
 
506
+
507
+ /**
508
+ * Relocate tetrahedron in memory, patches neighbourhood links as well
509
+ * NOTE: The destination slot will be overwritten. This is a dangerous method that can break the topology, make sure you fully understand what you are doing when using it
510
+ * @param {number} source_index index of source tetrahedron
511
+ * @param {number} destination_index new index, where the source tetrahedron is to be moved
512
+ */
513
+ relocate(source_index, destination_index) {
514
+ // validate_tetrahedron_neighbourhood(this, source_index, console.error);
515
+
516
+ // patch neighbours
517
+ for (let i = 0; i < 4; i++) {
518
+ const encoded_neighbour = this.getNeighbour(source_index, i);
519
+
520
+ if (encoded_neighbour === INVALID_NEIGHBOUR) {
521
+ // no neighbour
522
+ continue;
523
+ }
524
+
525
+ const neighbour_index = encoded_neighbour >> 2;
526
+ const neighbour_vertex = encoded_neighbour & 3;
527
+
528
+ const encoded_tet = (destination_index << 2) | (i & 3);
529
+
530
+ assert.equal(this.getNeighbour(neighbour_index, neighbour_vertex), (source_index << 2) | (i & 3), 'invalid source state');
531
+
532
+ this.setNeighbour(neighbour_index, neighbour_vertex, encoded_tet);
533
+ }
534
+
535
+
536
+ const layout_word_size = LAYOUT_TETRA_BYTE_SIZE >> 2;
537
+ array_copy(this.__data_uint32, source_index * layout_word_size, this.__data_uint32, destination_index * layout_word_size, layout_word_size);
538
+
539
+ // validate_tetrahedron_neighbourhood(this, destination_index, console.error);
540
+ }
541
+
542
+ /**
543
+ * Perform compaction, removing unused memory slots
544
+ * NOTE: existing tetrahedron indices can become invalidated as tets are moved into free slots
545
+ * @returns {number} number of relocated elements
546
+ */
547
+ compact() {
548
+ // sort free
549
+ array_quick_sort_by_comparator(this.__free, compareNumbersDescending, null, 0, this.__free_pointer - 1);
550
+
551
+ let relocation_count = 0;
552
+ let free_head_pointer = 0;
553
+
554
+ while (this.__free_pointer > free_head_pointer) {
555
+ const last_used = this.__used_end - 1;
556
+
557
+ if (this.__free[free_head_pointer] >= last_used) {
558
+ /*
559
+ The slot is actually free.
560
+ As we have sorted the free slots, it's expected to be at the start of the list
561
+ Adjust pointers and continue onto the next slot
562
+ */
563
+ free_head_pointer++
564
+ this.__used_end = last_used;
565
+ continue;
566
+ }
567
+
568
+ const free_slot = this.__free[this.__free_pointer - 1];
569
+
570
+ this.__free_pointer--;
571
+
572
+ if (last_used <= free_slot) {
573
+ continue;
574
+ }
575
+
576
+
577
+ this.relocate(last_used, free_slot);
578
+ relocation_count++;
579
+
580
+ this.__used_end--;
581
+
582
+ }
583
+
584
+ this.__free.splice(0, this.__free.length); // release memory
585
+ this.__free_pointer = 0;
586
+
587
+ return relocation_count;
588
+ }
589
+
590
+ /**
591
+ *
592
+ * @param {BinaryBuffer} buffer
593
+ */
594
+ serialize(buffer) {
595
+ buffer.writeUint32(1); // format version
596
+ buffer.writeUintVar(this.__used_end);
597
+ buffer.writeUintVar(this.__free_pointer);
598
+
599
+ // record main buffer
600
+ buffer.writeUint32Array(this.__data_uint32, 0, this.__used_end * LAYOUT_TETRA_WORD_COUNT);
601
+ buffer.writeUint32Array(this.__free, 0, this.__free_pointer);
602
+ }
603
+
604
+ /**
605
+ *
606
+ * @param {BinaryBuffer} buffer
607
+ */
608
+ deserialize(buffer) {
609
+ const version_number = buffer.readUint32();
610
+
611
+ if (version_number !== 1) {
612
+ throw new Error(`Unsupported version number, expected ${1}, instead got ${version_number}`);
613
+ }
614
+
615
+ this.__used_end = buffer.readUintVar();
616
+ this.__free_pointer = buffer.readUintVar();
617
+
618
+ this.growCapacity(this.__used_end);
619
+
620
+ buffer.readUint32Array(this.__data_uint32, 0, this.__used_end);
621
+
622
+ buffer.readUint32Array(this.__free, 0, this.__free_pointer);
623
+ }
624
+
625
+ /**
626
+ *
627
+ * @return {string}
628
+ */
629
+ serialize_base64() {
630
+ const buffer = new BinaryBuffer();
631
+
632
+ this.serialize(buffer);
633
+
634
+ buffer.trim();
635
+
636
+ return Base64.encode(buffer.data);
637
+ }
638
+
639
+ /**
640
+ *
641
+ * @param {string} str
642
+ */
643
+ deserialize_base64(str) {
644
+ const arrayBuffer = Base64.decode(str);
645
+
646
+ const binaryBuffer = BinaryBuffer.fromArrayBuffer(arrayBuffer);
647
+
648
+ this.deserialize(binaryBuffer);
649
+ }
467
650
  }
@@ -120,7 +120,7 @@ test("remove method reduces size", () => {
120
120
 
121
121
  expect(mesh.size()).toBe(1);
122
122
 
123
- mesh.remove(tet);
123
+ mesh.delete(tet);
124
124
 
125
125
  expect(mesh.size()).toBe(0);
126
126
  });
@@ -141,7 +141,7 @@ test("forEach visits all allocated tets", () => {
141
141
  expect(mock).toHaveBeenCalledWith(t1, mesh);
142
142
  });
143
143
 
144
- test("exists method", () => {
144
+ test("'exists' method", () => {
145
145
  const mesh = new TetrahedralMesh();
146
146
 
147
147
  expect(mesh.exists(0)).toBe(false);
@@ -149,8 +149,85 @@ test("exists method", () => {
149
149
  const tet = mesh.allocate();
150
150
 
151
151
  expect(mesh.exists(tet)).toBe(true);
152
+ expect(mesh.exists(tet + 1)).toBe(false);
152
153
 
153
- mesh.remove(tet);
154
+ mesh.delete(tet);
154
155
 
155
156
  expect(mesh.exists(tet)).toBe(false);
157
+ expect(mesh.exists(tet + 1)).toBe(false);
158
+ });
159
+
160
+
161
+ test("after calling 'removeTetrasConnectedToPoints' matching points are no logner indexed by live tetrahedrons", () => {
162
+
163
+
164
+ const tetrahedra = new TetrahedralMesh();
165
+
166
+ /*
167
+ * This "magic" mesh is a result of tetrahedral mesh generator with following input: 0, 0, 0, 10, 0, 0, 10, 0, 10, 0, 0, 10, 0, 10, 0,
168
+ * The mesh was generated on 27/10/2022
169
+ * It's a very small example with 21 total tets that was failing this test
170
+ *
171
+ */
172
+ tetrahedra.deserialize_base64('AAAAARUAAQAAAAAAAAAFAAAABgAAAAsAAAAUAAAAHwAAADQAAAADAAAABwAAAAUAAAAIAAAA/////yoAAABAAAAALgAAAAAAAAAFAAAABgAAAAgAAAD/////HAAAACgAAAAAAAAAAgAAAAUAAAAHAAAABgAAAP////8TAAAAFQAAACwAAAACAAAABgAAAAcAAAAIAAAA/////zwAAAAwAAAADQAAAAIAAAABAAAABQAAAAYAAAABAAAADgAAACAAAAA4AAAABAAAAAEAAAAGAAAACAAAAB0AAAAyAAAASQAAACEAAAABAAAAAAAAAAYAAAAIAAAACQAAABgAAABIAAAAAgAAAAQAAAACAAAABgAAAAEAAAAWAAAAGwAAACUAAAAzAAAABAAAAAMAAAACAAAAAQAAADsAAAAiAAAAUwAAAEYAAAADAAAABQAAAAAAAAAIAAAACgAAAEwAAAAFAAAANQAAAAMAAAAHAAAAAgAAAAUAAAAPAAAAOgAAAAcAAABEAAAABAAAAAYAAAACAAAACAAAABIAAAA+AAAAGQAAACMAAAADAAAAAQAAAAAAAAAFAAAAAwAAACsAAAA5AAAAUAAAAAMAAAACAAAAAQAAAAUAAAAXAAAANgAAAC0AAAAkAAAABAAAAAIAAAAHAAAACAAAABEAAABCAAAAMQAAAEUAAAAEAAAABwAAAAMAAAAIAAAABgAAAE4AAAA9AAAARwAAAAQAAAADAAAABwAAAAIAAAAvAAAAPwAAACcAAABDAAAABAAAAAAAAAABAAAACAAAAB4AAAAaAAAATQAAAFEAAAAEAAAAAwAAAAAAAAAIAAAAKQAAAEoAAABBAAAAUgAAAAQAAAADAAAAAQAAAAAAAAA3AAAASwAAAE8AAAAmAAAA');
173
+
174
+ tetrahedra.removeTetrasConnectedToPoints(5, 8);
175
+
176
+ //The mesh should contain only 5 vertices
177
+
178
+ tetrahedra.forEach((tet, mesh) => {
179
+
180
+ for (let i = 0; i < 4; i++) {
181
+ expect(mesh.getVertexIndex(tet, i)).toBeLessThan(5);
182
+ }
183
+
184
+ });
185
+ });
186
+
187
+ test('relocate operation copies tet data and updates back-links correctly', () => {
188
+ const mesh = new TetrahedralMesh();
189
+
190
+ const a = mesh.allocate();
191
+ const b = mesh.allocate();
192
+ const dest = mesh.allocate();
193
+
194
+ mesh.setVertexIndex(a, 0, 3);
195
+ mesh.setVertexIndex(a, 1, 7);
196
+ mesh.setVertexIndex(a, 2, 11);
197
+ mesh.setVertexIndex(a, 3, 13);
198
+
199
+ mesh.setNeighbour(a, 0, b << 2);
200
+ mesh.setNeighbour(b, 0, a << 2);
201
+
202
+ mesh.relocate(a, dest);
203
+
204
+ expect(mesh.getVertexIndex(dest, 0)).toBe(3);
205
+ expect(mesh.getVertexIndex(dest, 1)).toBe(7);
206
+ expect(mesh.getVertexIndex(dest, 2)).toBe(11);
207
+ expect(mesh.getVertexIndex(dest, 3)).toBe(13);
208
+
209
+ expect(mesh.getNeighbour(dest, 0)).toBe(b << 2);
210
+
211
+ // check back-link
212
+ expect(mesh.getNeighbour(b, 0)).toBe(dest << 2);
213
+ });
214
+
215
+ test('Compaction is non-destructive when moving resident data from the very end of the allocated region', () => {
216
+ const mesh = new TetrahedralMesh();
217
+
218
+ const a = mesh.allocate();
219
+ const b = mesh.allocate();
220
+
221
+ // force move "a" to free set
222
+ mesh.__free[mesh.__free_pointer++] = a;
223
+
224
+ expect(mesh.exists(b)).toBe(true);
225
+ expect(mesh.exists(a)).toBe(false);
226
+
227
+ mesh.compact();
228
+
229
+ expect(mesh.size()).toBe(1);
230
+
231
+ expect(mesh.exists(0)).toBe(true);
232
+ expect(mesh.exists(1)).toBe(false);
156
233
  });