Files
2025-01-04 00:34:03 +01:00

176 lines
7.8 KiB
JavaScript

/**
* Determine the center of the circle.
* Trivial, but used to match center method for other shapes.
* @type {PIXI.Point}
*/
Object.defineProperty(PIXI.Circle.prototype, "center", { get: function() {
return new PIXI.Point(this.x, this.y);
}});
/* -------------------------------------------- */
/**
* Determine if a point is on or nearly on this circle.
* @param {Point} point Point to test
* @param {number} epsilon Tolerated margin of error
* @returns {boolean} Is the point on the circle within the allowed tolerance?
*/
PIXI.Circle.prototype.pointIsOn = function(point, epsilon = 1e-08) {
const dist2 = Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2);
const r2 = Math.pow(this.radius, 2);
return dist2.almostEqual(r2, epsilon);
};
/* -------------------------------------------- */
/**
* Get all intersection points on this circle for a segment A|B
* Intersections are sorted from A to B.
* @param {Point} a The first endpoint on segment A|B
* @param {Point} b The second endpoint on segment A|B
* @returns {Point[]} Points where the segment A|B intersects the circle
*/
PIXI.Circle.prototype.segmentIntersections = function(a, b) {
const ixs = foundry.utils.lineCircleIntersection(a, b, this, this.radius);
return ixs.intersections;
};
/* -------------------------------------------- */
/**
* Calculate an x,y point on this circle's circumference given an angle
* 0: due east
* π / 2: due south
* π or -π: due west
* -π/2: due north
* @param {number} angle Angle of the point, in radians
* @returns {Point} The point on the circle at the given angle
*/
PIXI.Circle.prototype.pointAtAngle = function(angle) {
return {
x: this.x + (this.radius * Math.cos(angle)),
y: this.y + (this.radius * Math.sin(angle))
};
};
/* -------------------------------------------- */
/**
* Get all the points for a polygon approximation of this circle between two points.
* The two points can be anywhere in 2d space. The intersection of this circle with the line from this circle center
* to the point will be used as the start or end point, respectively.
* This is used to draw the portion of the circle (the arc) between two intersection points on this circle.
* @param {Point} a Point in 2d space representing the start point
* @param {Point} b Point in 2d space representing the end point
* @param {object} [options] Options passed on to the pointsForArc method
* @returns { Point[]} An array of points arranged clockwise from start to end
*/
PIXI.Circle.prototype.pointsBetween = function(a, b, options) {
const fromAngle = Math.atan2(a.y - this.y, a.x - this.x);
const toAngle = Math.atan2(b.y - this.y, b.x - this.x);
return this.pointsForArc(fromAngle, toAngle, { includeEndpoints: false, ...options });
};
/* -------------------------------------------- */
/**
* Get the points that would approximate a circular arc along this circle, given a starting and ending angle.
* Points returned are clockwise. If from and to are the same, a full circle will be returned.
* @param {number} fromAngle Starting angle, in radians. π is due north, π/2 is due east
* @param {number} toAngle Ending angle, in radians
* @param {object} [options] Options which affect how the circle is converted
* @param {number} [options.density] The number of points which defines the density of approximation
* @param {boolean} [options.includeEndpoints] Whether to include points at the circle where the arc starts and ends
* @returns {Point[]} An array of points along the requested arc
*/
PIXI.Circle.prototype.pointsForArc = function(fromAngle, toAngle, {density, includeEndpoints=true} = {}) {
const pi2 = 2 * Math.PI;
density ??= this.constructor.approximateVertexDensity(this.radius);
const points = [];
const delta = pi2 / density;
if ( includeEndpoints ) points.push(this.pointAtAngle(fromAngle));
// Determine number of points to add
let dAngle = toAngle - fromAngle;
while ( dAngle <= 0 ) dAngle += pi2; // Angles may not be normalized, so normalize total.
const nPoints = Math.round(dAngle / delta);
// Construct padding rays (clockwise)
for ( let i = 1; i < nPoints; i++ ) points.push(this.pointAtAngle(fromAngle + (i * delta)));
if ( includeEndpoints ) points.push(this.pointAtAngle(toAngle));
return points;
};
/* -------------------------------------------- */
/**
* Approximate this PIXI.Circle as a PIXI.Polygon
* @param {object} [options] Options forwarded on to the pointsForArc method
* @returns {PIXI.Polygon} The Circle expressed as a PIXI.Polygon
*/
PIXI.Circle.prototype.toPolygon = function(options) {
const points = this.pointsForArc(0, 0, options);
points.pop(); // Drop the repeated endpoint
return new PIXI.Polygon(points);
};
/* -------------------------------------------- */
/**
* The recommended vertex density for the regular polygon approximation of a circle of a given radius.
* Small radius circles have fewer vertices. The returned value will be rounded up to the nearest integer.
* See the formula described at:
* https://math.stackexchange.com/questions/4132060/compute-number-of-regular-polgy-sides-to-approximate-circle-to-defined-precision
* @param {number} radius Circle radius
* @param {number} [epsilon] The maximum tolerable distance between an approximated line segment and the true radius.
* A larger epsilon results in fewer points for a given radius.
* @returns {number} The number of points for the approximated polygon
*/
PIXI.Circle.approximateVertexDensity = function(radius, epsilon=1) {
return Math.ceil(Math.PI / Math.sqrt(2 * (epsilon / radius)));
};
/* -------------------------------------------- */
/**
* Intersect this PIXI.Circle with a PIXI.Polygon.
* @param {PIXI.Polygon} polygon A PIXI.Polygon
* @param {object} [options] Options which configure how the intersection is computed
* @param {number} [options.density] The number of points which defines the density of approximation
* @param {number} [options.clipType] The clipper clip type
* @param {string} [options.weilerAtherton=true] Use the Weiler-Atherton algorithm. Otherwise, use Clipper.
* @returns {PIXI.Polygon} The intersected polygon
*/
PIXI.Circle.prototype.intersectPolygon = function(polygon, {density, clipType, weilerAtherton=true, ...options}={}) {
if ( !this.radius ) return new PIXI.Polygon([]);
clipType ??= ClipperLib.ClipType.ctIntersection;
// Use Weiler-Atherton for efficient intersection or union
if ( weilerAtherton && polygon.isPositive ) {
const res = WeilerAthertonClipper.combine(polygon, this, {clipType, density, ...options});
if ( !res.length ) return new PIXI.Polygon([]);
return res[0];
}
// Otherwise, use Clipper polygon intersection
const approx = this.toPolygon({density});
return polygon.intersectPolygon(approx, options);
};
/* -------------------------------------------- */
/**
* Intersect this PIXI.Circle with an array of ClipperPoints.
* Convert the circle to a Polygon approximation and use intersectPolygon.
* In the future we may replace this with more specialized logic which uses the line-circle intersection formula.
* @param {ClipperPoint[]} clipperPoints Array of ClipperPoints generated by PIXI.Polygon.toClipperPoints()
* @param {object} [options] Options which configure how the intersection is computed
* @param {number} [options.density] The number of points which defines the density of approximation
* @returns {PIXI.Polygon} The intersected polygon
*/
PIXI.Circle.prototype.intersectClipper = function(clipperPoints, {density, ...options}={}) {
if ( !this.radius ) return [];
const approx = this.toPolygon({density});
return approx.intersectClipper(clipperPoints, options);
};