UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

446 lines (364 loc) 12.2 kB
/** * @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 { Subspan } from "./Subspan.js"; /** * Small number * @type {number} */ const Epsilon = 1.0E-14; /** * Limit to how many iterations 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. * * @copyright Company Named Limited (c) 2025 */ export class Miniball { /** * 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 {number} */ this.iteration = 0; /** * * @type {number} */ this.distToAff = 0; /** * * @type {number} */ this.distToAffSquare = 0; /** * * The squared radius of the miniball. * * This is equivalent to `radius() * radius()`. * * Precondition: `!isEmpty()` * * @type {number} * @private */ this.__squaredRadius = 0; /** * * @type {number} * @private */ this.__radius = 0; /** * * @type {number} */ this.stopper = 0; /** * @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 size = this.__size; for (j = 0; j < size; ++j) { if (support.isMember(j)) { continue; } for (i = 0; i < dim; ++i) { centerToPoint[i] = pointSet.coord(j, i) - center[i]; } let dirPointProd = 0; for (i = 0; i < dim; ++i) { dirPointProd += centerToAff[i] * centerToPoint[i]; } if ((this.distToAffSquare - dirPointProd) < (Epsilon * this.__radius * this.distToAff)) { continue; } let bound = 0; for (i = 0; i < dim; ++i) { bound += centerToPoint[i] * centerToPoint[i]; } bound = (this.__squaredRadius - bound) / 2 / (this.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; } }