@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
602 lines (474 loc) • 14.4 kB
JavaScript
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;