UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

602 lines (474 loc) • 14.4 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 { line3_computeSegmentPointDistance_sqr } from "../../../../core/geom/3d/line/line3_computeSegmentPointDistance_sqr.js"; import { v3_distance } from "../../../../core/geom/vec3/v3_distance.js"; import { clamp } from "../../../../core/math/clamp.js"; import { lerp } from "../../../../core/math/lerp.js"; import { min2 } from "../../../../core/math/min2.js"; import { computeNonuniformCatmullRomSplineSample } from "../../../../core/math/spline/computeNonuniformCatmullRomSplineSample.js"; import { AttributeGroupSpec } from "../../../graphics/geometry/AttributeGroupSpec.js"; import { AttributeSpec } from "../../../graphics/geometry/AttributeSpec.js"; import { InterpolationType } from "./InterpolationType.js"; const scratch_v3_a = new Float32Array(3); const scratch_v3_b = new Float32Array(3); /** * * @type {number[]} */ const scratch_array_0 = []; /** * * @type {number[]} */ const scratch_array_1 = []; /** * * @type {number[]} */ const scratch_array_2 = []; /** * * @type {number[]} */ const scratch_array_3 = []; /** * * @type {number[]} */ const scratch_array_4 = []; /** * * @type {RowFirstTableSpec} */ const DEFAULT_SPEC = Object.freeze(new RowFirstTableSpec([ BinaryDataType.Float32, // position X BinaryDataType.Float32, // position Y BinaryDataType.Float32 // position Z ])); class Path { constructor() { /** * * @type {AttributeGroupSpec} * @private */ this.__spec = AttributeGroupSpec.from( AttributeSpec.fromJSON({ name: 'position', type: BinaryDataType.Float32, itemSize: 3, normalized: false }) ); /** * @readonly * @type {RowFirstTable} * @private */ this.__data = new RowFirstTable(DEFAULT_SPEC); /** * Now to get in-between values * @type {InterpolationType|number} */ this.interpolation = InterpolationType.Linear; /** * Absolute length of the path, measured as straight-line distance sum of point pairs * @type {number} * @private */ this.__length = -1; } /** * * @return {boolean} */ isEmpty() { return this.__data.length === 0; } clear() { this.__data.clear(); } /** * * @return {number} */ getPointCount() { return this.__data.length; } /** * * @returns {number} */ get count() { return this.getPointCount(); } /** * * @param {number} v */ setPointCount(v) { assert.isNonNegativeInteger(v, "v"); const old_length = this.__data.length; if (old_length > v) { this.__data.removeRows(v, old_length - v); } else { this.__data.resize(v); } // force row count this.__data.length = v; // reset computed length this.__length = -1; } /** * * @param {number} index * @returns {boolean} */ removePoint(index) { assert.isNonNegativeInteger(index, "index"); if (index >= this.getPointCount()) { return false; } else { this.__data.removeRows(index, 1); // reset computed length this.__length = -1; return true; } } /** * Insert an point * @param {number} index * @param {number} x * @param {number} y * @param {number} z */ insertPoint(index, x, y, z) { assert.isNonNegativeInteger(index, "index"); const data = this.__data; data.insertRows(index, 1); this.setPosition(index, x, y, z); } /** * * @param {number} index * @param {number} x * @param {number} y * @param {number} z */ setPosition(index, x, y, z) { assert.lessThanOrEqual(index, this.count, "overflow"); assert.isNumber(x, "x"); assert.isNumber(y, "y"); assert.isNumber(z, "z"); const data = this.__data; data.writeCellValue(index, 0, x); data.writeCellValue(index, 1, y); data.writeCellValue(index, 2, z); // reset computed length this.__length = -1; } /** * * @param {number} index * @param {Vector3} result */ getPosition(index, result) { const data = this.__data; const x = data.readCellValue(index, 0); const y = data.readCellValue(index, 1); const z = data.readCellValue(index, 2); result.set(x, y, z); } /** * * @param {number} index * @param {ArrayLike<number>|number[]|Float32Array|Float64Array} result * @param {number} result_offset */ readPositionToArray(index, result, result_offset) { const data = this.__data; const x = data.readCellValue(index, 0); const y = data.readCellValue(index, 1); const z = data.readCellValue(index, 2); result[result_offset] = x; result[result_offset + 1] = y; result[result_offset + 2] = z; } /** * * @param {number} index * @param {Vector3} v */ setPositionFromVector(index, v) { this.setPosition(index, v.x, v.y, v.z); } /** * * @param {Vector3[]} array */ setPositionsFromVectorArray(array) { const l = array.length; this.setPointCount(l); for (let i = 0; i < l; i++) { const v3 = array[i]; this.setPositionFromVector(i, v3); } } /** * Reverse order of points */ reverse() { this.__data.reverse_rows(); } /** * * @param {Path} other */ copy(other) { this.__data.copy(other.__data); this.interpolation = other.interpolation; this.__length = other.__length; } /** * * @return {Path} */ clone() { const r = new Path(); r.copy(this); return r; } /** * Get low index of a point that makes up a line segment closest to the reference point * @param {Vector3} reference * @returns {number} */ getLowIndexOfNearestPoint(reference) { const pointCount = this.getPointCount(); if (pointCount === 0) { return -1; } else if (pointCount === 1) { return 0; } let p0 = scratch_array_0; let p1 = scratch_array_1; // read first point this.readPositionToArray(0, p0, 0); let closest_distance = Number.POSITIVE_INFINITY; let closest_index = 0; for (let i = 1; i < pointCount; i++) { this.readPositionToArray(i, p1, 0); // const distance_sqr = line3_computeSegmentPointDistance_sqr( p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], reference.x, reference.y, reference.z ); if (distance_sqr < closest_distance) { closest_distance = distance_sqr; closest_index = i - 1; } // swap points const t = p1; p1 = p0; p0 = t; } return closest_index; } /** * Read position along the path at a given 1D offset * @param {Vector3} result * @param {number} offset * @returns {boolean} */ sample_linear(result, offset) { const found = this.find_index_and_normalized_distance(scratch_array_0, offset); if (!found) { return false; } const index = scratch_array_0[0]; const t = scratch_array_0[1]; const max_index = this.getPointCount() - 1; const index_next = min2(index + 1, max_index); this.readPositionToArray(index, scratch_v3_a, 0); this.readPositionToArray(index_next, scratch_v3_b, 0); result.set( lerp(scratch_v3_a[0], scratch_v3_b[0], t), lerp(scratch_v3_a[1], scratch_v3_b[1], t), lerp(scratch_v3_a[2], scratch_v3_b[2], t), ) return (t < 0 && index_next !== max_index); } /** * * @param {number[]} result * @param {number} absolute_distance absolute distance along the path * @returns {boolean} */ find_index_and_normalized_distance(result, absolute_distance) { const pointCount = this.getPointCount(); if (pointCount === 0) { return false; } let cursor = 0; let current = scratch_v3_a; let previous = scratch_v3_b; let distance = 0; this.readPositionToArray(0, previous, 0); for (let i = 1; i < pointCount; i++) { this.readPositionToArray(i, current, 0); distance = v3_distance( previous[0], previous[1], previous[2], current[0], current[1], current[2] ); if (cursor + distance > absolute_distance) { // we found v1 const local_offset = absolute_distance - cursor; const local_normalized_offset = local_offset / distance; result[0] = i - 1; result[1] = local_normalized_offset; return true; } // swap variables const t = previous; previous = current; current = t; cursor += distance } // we didn't find what we're looking for, return last point in the path result[0] = pointCount - 1; result[1] = 1; return true; } /** * * @param {Vector3} result * @param {number} offset * @returns {boolean} */ sample_catmull_rom(result, offset) { const found = this.find_index_and_normalized_distance(scratch_array_0, offset); if (!found) { return false; } /** * * @type {number} */ const i1 = scratch_array_0[0]; /** * * @type {number} */ const t = scratch_array_0[1]; const input_length = this.getPointCount(); const max_index = input_length - 1; const i0 = clamp(i1 - 1, 0, max_index); const i2 = clamp(i1 + 1, 0, max_index); const i3 = clamp(i1 + 2, 0, max_index); this.readPositionToArray(i0, scratch_array_0, 0); this.readPositionToArray(i1, scratch_array_1, 0); this.readPositionToArray(i2, scratch_array_2, 0); this.readPositionToArray(i3, scratch_array_3, 0); computeNonuniformCatmullRomSplineSample(scratch_array_4, scratch_array_0, scratch_array_1, scratch_array_2, scratch_array_3, 3, t, 0.5); result.fromArray(scratch_array_4, 0); return (t < 0 && i1 !== max_index); } /** * * @param {Vector3} result * @param {number} offset absolute distance offset along the path * @param {InterpolationType} [interpolation=this.interpolation] * @returns {boolean} */ sample(result, offset, interpolation = this.interpolation) { switch (interpolation) { case InterpolationType.Linear: return this.sample_linear(result, offset); case InterpolationType.CatmullRom: return this.sample_catmull_rom(result, offset); default: throw new Error(`Unsupported sampling type ${interpolation}`); } } toString() { return JSON.stringify(this.toJSON()); } toJSON() { const positions = []; const n = this.getPointCount(); for (let i = 0; i < n; i++) { this.readPositionToArray(i, positions, i * 3); } return { points: positions, rowSize: 3 }; } static fromJSON(j) { const r = new Path(); r.fromJSON(j); return r; } fromJSON({ points = [], rowSize = 3 }) { const n = points.length / rowSize; this.setPointCount(n); for (let i = 0; i < n; i++) { const i3 = i * 3; const x = points[i3]; const y = points[i3 + 1]; const z = points[i3 + 2]; this.setPosition(i, x, y, z); } } /** * * @returns {number} */ get length() { const length = this.__length; if (length !== -1) { return length; } const new_length = this.computeLength(); this.__length = new_length; return new_length; } /** * * @returns {number} */ computeLength() { const n = this.getPointCount(); if (n === 0) { // empty path return 0; } let r = 0; let p0 = scratch_v3_a; let p1 = scratch_v3_b; let distance = 0; this.readPositionToArray(0, p1, 0); for (let i = 1; i < n; i++) { this.readPositionToArray(i, p0, 0); distance = v3_distance( p1[0], p1[1], p1[2], p0[0], p0[1], p0[2] ); r += distance; // perform variable swap const swap_t = p0; p0 = p1; p1 = swap_t; } return r; } } Path.typeName = "Path"; export default Path;