@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
461 lines (365 loc) • 12.3 kB
JavaScript
/**
* @author Alex Goldring 14/05/2018 - Ported to JS using JSweet + manual editing
* @author Kaspar Fischer (hbf) 09/05/2013
* @source https://github.com/hbf/miniball
* @generated Generated from Java with JSweet 2.2.0-SNAPSHOT - http://www.jsweet.org
*/
import { assert } from "../../../assert.js";
import { sqr } from "../../../math/sqr.js";
import { vector_dot } from "../../vec/vector_dot.js";
import { Subspan } from "./Subspan.js";
/**
* Small number
* @type {number}
*/
const Epsilon = 1e-14;
/**
* Limit to how many iterations the algorithm is allowed to perform, this is to cover poorly converging cases
* @type {number}
*/
const MAX_ITERATIONS = 10000;
/**
* Computes the miniball of the given point set.
*
* Based on paper "Fast Smallest-Enclosing-Ball Computation in High Dimensions" by "Kaspar Fischer" et. al.
* @copyright Company Named Limited (c) 2025
*/
export class Miniball {
/**
*
* @type {number}
*/
iteration = 0;
/**
*
* @type {number}
*/
distToAff = 0;
/**
*
* @type {number}
*/
distToAffSquare = 0;
/**
*
* The squared radius of the miniball.
*
* This is equivalent to `radius() * radius()`.
*
* Precondition: `!isEmpty()`
*
* @type {number}
* @private
*/
__squaredRadius = 0;
/**
*
* @type {number}
* @private
*/
__radius = 0;
/**
*
* @type {number}
*/
stopper = 0;
/**
* Notice that the point set `points` is assumed to be immutable during the computation.
* That is, if you add, remove, or change points in the point set, you have to create a new instance of {@link Miniball}.
*
* @param {PointSet} points the point set
*/
constructor(points) {
assert.defined(points, 'points');
/**
* @type {PointSet}
*/
this.S = points;
/**
*
* @type {number}
* @private
*/
this.__size = this.S.size();
const dimension_count = this.S.dimension();
/**
* Number of dimensions (2 for 2d, 3 for 3d etc.)
* @type {number}
*/
this.dim = dimension_count;
// pre-allocated continuous chunk of memory to make execution faster by reducing cache miss rate
const buffer = new ArrayBuffer(8 * (dimension_count * 4 + 1));
/**
*
* @type {number[]|Float64Array}
* @private
*/
this.__center = new Float64Array(buffer, 0, dimension_count);
/**
*
* @type {number[]|Float64Array}
*/
this.centerToAff = new Float64Array(buffer, 8 * dimension_count, dimension_count);
/**
*
* @type {number[]|Float64Array}
*/
this.centerToPoint = new Float64Array(buffer, 8 * 2 * dimension_count, dimension_count);
/**
*
* @type {number[]|Float64Array}
*/
this.lambdas = new Float64Array(buffer, 8 * 3 * dimension_count, dimension_count + 1);
/**
*
* @type {Subspan}
* @private
*/
this.__support = this.initBall();
this.compute();
}
/**
* Whether the miniball is the empty set, equivalently, whether `points.size() == 0`
* was true when this miniball instance was constructed.
*
* Notice that the miniball of a point set <i>S</i> is empty if and only if <i>S={}</i>.
*
* @return {boolean} true iff
*/
isEmpty() {
return this.__size === 0;
}
/**
* The radius of the miniball.
*
* Precondition: `!isEmpty()`
*
* @return {number} the radius of the miniball, a number ≥ 0
*/
radius() {
return this.__radius;
}
/**
* The Euclidean coordinates of the center of the miniball.
*
* Precondition: `!isEmpty()`
*
* @return {number[]} an array holding the coordinates of the center of the miniball
*/
center() {
return this.__center;
}
/**
*
* @returns {Subspan}
*/
get support() {
return this.__support;
}
/**
* The number of input points.
*
* @return {number} the number of points in the original point set, i.e., `pts.size()` where
* `pts` was the {@link PointSet} instance passed to the constructor of this
* instance
*/
size() {
return this.__size;
}
/**
* Sets up the search ball with an arbitrary point of <i>S</i> as center and with exactly one of
* the points farthest from center in the support. So the current ball contains all points of
* <i>S</i> and has radius at most twice as large as the minball.
*
* Precondition: `size > 0`
* @return {Subspan}
* @private
*/
initBall() {
let i, j;
const dim = this.dim;
const center = this.__center;
const pointSet = this.S;
for (i = 0; i < dim; ++i) {
center[i] = pointSet.coord(0, i);
}
this.__squaredRadius = 0;
let farthest = 0;
const numPoints = pointSet.size();
for (j = 1; j < numPoints; ++j) {
let dist = 0;
for (i = 0; i < dim; ++i) {
dist += sqr(pointSet.coord(j, i) - center[i]);
}
if (dist >= this.__squaredRadius) {
this.__squaredRadius = dist;
farthest = j;
}
}
this.__radius = Math.sqrt(this.__squaredRadius);
return new Subspan(this.dim, pointSet, farthest);
}
/**
* @private
*/
computeDistToAff() {
this.distToAffSquare = this.__support.shortestVectorToSpan(this.__center, this.centerToAff);
this.distToAff = Math.sqrt(this.distToAffSquare);
}
/**
* @private
*/
updateRadius() {
const any = this.__support.anyMember();
this.__squaredRadius = 0;
const dim = this.dim;
const center = this.__center;
const points = this.S;
for (let i = 0; i < dim; ++i) {
this.__squaredRadius += sqr(points.coord(any, i) - center[i]);
}
this.__radius = Math.sqrt(this.__squaredRadius);
}
/**
* The main function containing the main loop.
*
* Iteratively, we compute the point in support that is closest to the current center and then
* walk towards this target as far as we can, i.e., we move until some new point touches the
* boundary of the ball and must thus be inserted into support.
*
* In each of these two alternating phases, we always have to check whether some point must be dropped from support,
* which is the case when the center lies in <i>aff(support)</i>.
* If such an attempt to drop fails, we are done; because then the center lies even <i>conv(support)</i>.
* @private
*/
compute() {
const center = this.__center;
const support = this.__support;
const dim = this.dim;
const centerToAff = this.centerToAff;
for (this.iteration = 0; this.iteration < MAX_ITERATIONS; this.iteration++) {
this.computeDistToAff();
while (
this.distToAff <= Epsilon * this.__radius
|| support.size() === dim + 1
) {
if (!this.successfulDrop()) {
// done
return;
}
this.computeDistToAff();
}
const scale = this.findStopFraction();
if (this.stopper >= 0) {
for (let i = 0; i < dim; ++i) {
center[i] += scale * centerToAff[i];
}
this.updateRadius();
support.add(this.stopper);
} else {
for (let i = 0; i < dim; ++i) {
center[i] += centerToAff[i];
}
this.updateRadius();
if (!this.successfulDrop()) {
return;
}
}
}
}
/**
* If center doesn't already lie in <i>conv(support)</i> and is thus not optimal yet,
* {@link successfulDrop()} elects a suitable point <i>k</i> to be removed from the support and
* returns true. If the center lies in the convex hull, however, false is returned (and the
* support remains unaltered).
*
* Precondition: center lies in <i>aff(support)</i>.
* @return {boolean}
*/
successfulDrop() {
const lambdas = this.lambdas;
const support = this.__support;
support.findAffineCoefficients(this.__center, lambdas);
let smallest = 0;
let minimum = 1;
const support_set_size = support.size();
for (let i = 0; i < support_set_size; ++i) {
const lambda = lambdas[i];
if (lambda < minimum) {
minimum = lambda;
smallest = i;
}
}
if (minimum <= 0) {
support.remove(smallest);
return true;
}
return false;
}
/**
* Given the center of the current enclosing ball and the walking direction `centerToAff`,
* determine how much we can walk into this direction without losing a point from <i>S</i>.
*
* The (positive) factor by which we can walk along `centerToAff` is returned.
* Further, `stopper` is set to the index of the most restricting point and to -1 if no such point was found.
* @return {number}
* @private
*/
findStopFraction() {
let scale = 1;
this.stopper = -1;
let i, j;
const dim = this.dim;
const center = this.__center;
const pointSet = this.S;
const support = this.__support;
const centerToPoint = this.centerToPoint;
const centerToAff = this.centerToAff;
const distToAffSquare = this.distToAffSquare;
const squaredRadius = this.__squaredRadius;
const size = this.__size;
const distance_error = Epsilon * this.__radius * this.distToAff;
for (j = 0; j < size; ++j) {
if (support.isMember(j)) {
continue;
}
for (i = 0; i < dim; ++i) {
// read out a single point, offset by center
centerToPoint[i] = pointSet.coord(j, i) - center[i];
}
const dirPointProd = vector_dot(centerToAff, centerToPoint, dim);
if ((distToAffSquare - dirPointProd) < distance_error) {
continue;
}
const bound_sq = vector_dot(centerToPoint, centerToPoint, dim);
const bound = (squaredRadius - bound_sq) / 2 / (distToAffSquare - dirPointProd);
if (bound > 0 && bound < scale) {
//if (com.dreizak.miniball.highdim.Logging.log)
// com.dreizak.miniball.highdim.Logging.debug("found stopper " + j + " bound=" + bound + " scale=" + scale);
scale = bound;
this.stopper = j;
}
}
return scale;
}
/**
* Outputs information about the Miniball
* @return {string}
*/
toString() {
let s = "Miniball [";
if (this.isEmpty()) {
s += "isEmpty=true";
} else {
s += "center=(";
for (let i = 0; i < this.dim; ++i) {
s += (this.__center[i]);
if (i < this.dim - 1)
s += (", ");
}
s += `), radius=${this.__radius}, squaredRadius=${this.__squaredRadius}`;
}
s += "]";
return s;
}
}