@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
328 lines (265 loc) • 7.39 kB
JavaScript
import { assert } from "../../../../core/assert.js";
import { array_shrink_to_size } from "../../../../core/collection/array/array_shrink_to_size.js";
import { binarySearchHighIndex } from "../../../../core/collection/array/binarySearchHighIndex.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { IllegalStateException } from "../../../../core/fsm/exceptions/IllegalStateException.js";
/**
* @readonly
* @enum {number}
*/
const IncrementalDeltaSetState = {
Clear: 0,
Building: 1,
Ready: 2
};
/**
* accelerated Set data structure optimized for incremental re-building
* Always sorted
* @template V
*/
export class IncrementalDeltaSet {
/**
*
* @param {function(V,V):number} compare stable comparator
*/
constructor(compare) {
assert.isFunction(compare, 'compare');
/**
*
* @type {V[]}
* @private
*/
this.__elements = [];
/**
*
* @type {number}
* @private
*/
this.__elements_cursor = 0;
/**
*
* @type {V[]}
* @private
*/
this.__swap_elements = [];
/**
*
* @type {number}
* @private
*/
this.__swap_elements_count = 0;
/**
*
* @type {function(V, V): number}
* @private
*/
this.__compare = compare;
/**
* Current version of the state, each time the set is updated - version changes
* @type {number}
*/
this.version = 0;
/**
* Number of currently held elements
* @type {number}
*/
this.size = 0;
/**
*
* @type {Signal}
*/
this.onUpdateFinished = new Signal();
/**
*
* @type {Signal}
*/
this.onAdded = new Signal();
/**
*
* @type {Signal}
*/
this.onRemoved = new Signal();
/**
*
* @type {IncrementalDeltaSetState|number}
*/
this.state = IncrementalDeltaSetState.Clear;
}
/**
*
* @return {V[]}
*/
get elements() {
return this.__elements;
}
/**
*
* @param {(V)=>any} visitor
* @param {*} [visitorContext]
*/
forEach(visitor, visitorContext) {
const n = this.size;
const elements = this.__elements;
for (let i = 0; i < n; i++) {
const el = elements[i];
visitor.call(visitorContext, el);
}
}
/**
*
* @param {V} element
* @returns {boolean}
*/
contains(element) {
return this.__indexOf(element) !== -1;
}
/**
*
* @param {V} element
* @return {number}
* @private
*/
__indexOf(element) {
const i = this.__indexExpected(element);
if (this.__elements[i] === element) {
return i;
} else {
return -1;
}
}
/**
*
* @param {V} element
* @return {number}
* @private
*/
__indexExpected(element) {
return binarySearchHighIndex(this.__elements, element, this.__compare, 0, this.size - 1);
}
/**
*
* @param {V} element
* @return {boolean}
*/
forceAdd(element) {
const i = this.__indexExpected(element);
if (this.elements[i] === element) {
return false;
}
this.__elements.splice(i, 0, element);
this.size++;
this.version++;
return true;
}
/**
*
* @param {V} element
* @returns {boolean}
*/
forceRemove(element) {
if (this.state === IncrementalDeltaSetState.Building) {
throw new IllegalStateException("Cannot remove during build");
}
const i = this.__indexOf(element);
if (i !== -1) {
this.__elements.splice(i, 1);
this.size--;
this.version++;
return true;
} else {
return false;
}
}
clear() {
if (this.size === 0) {
// already empty
return;
}
this.size = 0;
this.state = IncrementalDeltaSetState.Clear;
this.version++;
}
includeAll() {
throw new Error('deprecated #includeAll');
}
initializeUpdate() {
// perform swap
const swp_els = this.__elements;
this.__elements = this.__swap_elements
this.__swap_elements = swp_els;
const swp_count = this.size;
this.size = this.__swap_elements_count;
this.__swap_elements_count = swp_count;
this.__elements_cursor = 0;
this.state = IncrementalDeltaSetState.Building;
this.version++;
}
finalizeUpdate() {
assert.equal(this.state, IncrementalDeltaSetState.Building, `Expected BUILDING state, instead got '${this.state}'`);
const array_main_count = this.__elements_cursor;
const array_previous_count = this.__swap_elements_count;
this.size = array_main_count;
// sort newly populated elements array
const array_main = this.__elements;
const array_previous = this.__swap_elements;
const compare = this.__compare;
const onAdded = this.onAdded;
const onRemoved = this.onRemoved;
array_shrink_to_size(array_main, array_main_count);
array_main.sort(compare);
let i0 = 0, i1 = 0;
while (i0 < array_main_count && i1 < array_previous_count) {
const el_0 = array_main[i0];
const el_1 = array_previous[i1];
const diff = compare(el_0, el_1);
if (diff === 0) {
i0++;
i1++;
// found match
} else if (diff < 0) {
i0++;
// addition
onAdded.send1(el_0);
} else {
i1++;
// removal
onRemoved.send1(el_1);
}
}
// process remainders
for (; i0 < array_main_count; i0++) {
onAdded.send1(array_main[i0]);
}
for (; i1 < array_previous_count; i1++) {
onRemoved.send1(array_previous[i1]);
}
this.state = IncrementalDeltaSetState.Ready;
//dispatch notification
this.onUpdateFinished.send0();
}
/**
*
* @param {V} element
*/
push(element) {
const elements = this.__elements;
elements[this.__elements_cursor++] = element;
}
/**
*
* @param {IncrementalDeltaSet<V>} other
*/
pushOther(other) {
const elements = other.__elements;
const n = other.size;
for (let i = 0; i < n; i++) {
const el = elements[i];
this.push(el);
}
}
}
/**
* @readonly
* @type {boolean}
*/
IncrementalDeltaSet.prototype.isIncrementalDeltaSet = true;