UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

301 lines (229 loc) • 7.75 kB
import { assert } from "../../../core/assert.js"; import { BinaryDataType } from "../../../core/binary/type/BinaryDataType.js"; import { RowFirstTable } from "../../../core/collection/table/RowFirstTable.js"; import { RowFirstTableSpec } from "../../../core/collection/table/RowFirstTableSpec.js"; import { inverseLerp } from "../../../core/math/inverseLerp.js"; import { lerp } from "../../../core/math/lerp.js"; import { max2 } from "../../../core/math/max2.js"; import { min2 } from "../../../core/math/min2.js"; import { validate_enum_schema } from "../../../core/model/validate_enum_schema.js"; import { table_find_min_index_in_ordered_column } from "./table_find_min_index_in_ordered_column.js"; /** * * @type {number[]} */ const scratch_row = []; /** * @template T */ export class TimeSeries { /** * @type {RowFirstTableSpec} */ spec /** * @type {RowFirstTable} */ table time_column_index = 0; /** * * @type {Object<BinaryDataType>} */ #schema = {} /** * * @type {Object<number>} */ #schema_column_names_to_indices = {}; /** * * @type {string[]} */ #schema_column_indices_to_names = []; /** * * @param {Object<BinaryDataType>} schema * @param {string} [time_column_name] */ constructor(schema, time_column_name = 'time') { assert.isString(time_column_name, 'time_column_name'); assert.notNull(schema, 'schema'); assert.isObject(schema, 'schema'); let issues = []; if (!validate_enum_schema(schema, BinaryDataType, (issue) => issues.push(issue))) { throw new TypeError(`Invalid schema. Problems:\n ${issues.join('\n')}`); } this.#schema = schema; const schema_keys = Object.keys(schema); const time_row_index = schema_keys.indexOf(time_column_name); if (time_row_index === -1) { throw new TypeError(`supplies schema does not include time column '${time_column_name}', exising columns: ${schema_keys.join(', ')}`); } this.time_column_index = time_row_index; this.#schema_column_names_to_indices = {}; for (let i = 0; i < schema_keys.length; i++) { const key = schema_keys[i]; this.#schema_column_names_to_indices[key] = i; this.#schema_column_indices_to_names[i] = key; } this.spec = RowFirstTableSpec.get(Object.values(schema)); this.table = new RowFirstTable(this.spec); } /** * * @param {number[]} row * @param {Object} object */ #object_to_row(row, object) { const columnCount = this.spec.getColumnCount(); for (let i = 0; i < columnCount; i++) { const key = this.#schema_column_indices_to_names[i]; const value = object[key]; const t = typeof value; if (t !== "number") { throw new TypeError(`Expected sample.${key} to be a number, instead was ${t} (=${value})`); } row[i] = value; } } /** * * @param {Object} object * @param {number[]} row */ #row_to_object(object, row) { const columnCount = this.spec.getColumnCount(); for (let i = 0; i < columnCount; i++) { const key = this.#schema_column_indices_to_names[i]; object[key] = row[i]; } } /** * * @param {number[]} row * @returns {Object} */ #row_to_object_allocating(row) { const result = {}; this.#row_to_object(result, row); return result; } /** * * @param {number} index * @returns {Object} */ getSampleObjectByIndex(index) { this.table.readRow(index, scratch_row); return this.#row_to_object_allocating(scratch_row); } /** * * @returns {number} */ get sample_count() { return this.table.length; } /** * * @returns {number} */ get last_timestamp() { const table = this.table; const record_count = table.length; if (record_count === 0) { // default return 0; } const time_column = this.time_column_index; return table.readCellValue(record_count - 1, time_column); } /** * * @param {number[]} data */ validateNextSample(data) { assert.isArrayLike(data, 'data'); const table = this.table; const record_count = table.length; if (record_count > 0) { const time_column = this.time_column_index; const last_time = table.readCellValue(record_count - 1, time_column); const record = data[time_column]; assert.isNumber(record, `data[${time_column}](time)`); assert.notNaN(record, `data[${time_column}](time)`); assert.isFiniteNumber(record, `data[${time_column}](time)`); if (record <= last_time) { throw new Error(`Sample.time[${time_column}] = ${record}, which is <= to previous time stamp(=${last_time}). Each sample's time stamp must be strictly greater than the previous one`); } } } /** * * @param {number[]} data */ addSample(data) { // validate this.validateNextSample(data); const table = this.table; table.addRow(data); } /** * * @param {Object} sample */ addObjectSample(sample) { this.#object_to_row(scratch_row, sample); this.addSample(scratch_row); } /** * * @param {number[]} result * @param {number} index */ getSampleByIndex(result, index) { this.table.readRow(index, result) } /** * * @param {number} time * @returns {number} */ findLowSampleIndexByTime(time) { const table = this.table; const time_column_index = this.time_column_index; return max2(0, table_find_min_index_in_ordered_column(table, time, time_column_index)) } /** * * @param {number[]} result * @param {number} result_offset * @param {number} time */ sampleLinear(result, result_offset, time) { const table = this.table; const time_column_index = this.time_column_index; // seek to the right sample const sample_index = this.findLowSampleIndexByTime(time); const next_sample_index = min2(table.length - 1, sample_index + 1); const prev = table.readCellValue(sample_index, time_column_index); const next = table.readCellValue(next_sample_index, time_column_index); const normalized_offset = inverseLerp(prev, next, time); const column_count = this.spec.getColumnCount(); for (let i = 0; i < column_count; i++) { const v0 = table.readCellValue(sample_index, i); const v1 = table.readCellValue(next_sample_index, i); result[result_offset + i] = lerp(v0, v1, normalized_offset) } } /** * Get linearly interpolated sample for given time in object form, following supplied schema * @param {number} time * @returns {Object} */ sampleObjectLinear(time) { this.sampleLinear(scratch_row, 0, time); return this.#row_to_object_allocating(scratch_row); } }