Files
Foundry-VTT-Docker/resources/app/client/pixi/extensions/polygon.js
2025-01-04 00:34:03 +01:00

237 lines
9.0 KiB
JavaScript

/**
* Test whether the polygon is has a positive signed area.
* Using a y-down axis orientation, this means that the polygon is "clockwise".
* @type {boolean}
*/
Object.defineProperties(PIXI.Polygon.prototype, {
isPositive: {
get: function() {
if ( this._isPositive !== undefined ) return this._isPositive;
if ( this.points.length < 6 ) return undefined;
return this._isPositive = this.signedArea() > 0;
}
},
_isPositive: {value: undefined, writable: true, enumerable: false}
});
/* -------------------------------------------- */
/**
* Clear the cached signed orientation.
*/
PIXI.Polygon.prototype.clearCache = function() {
this._isPositive = undefined;
};
/* -------------------------------------------- */
/**
* Compute the signed area of polygon using an approach similar to ClipperLib.Clipper.Area.
* The math behind this is based on the Shoelace formula. https://en.wikipedia.org/wiki/Shoelace_formula.
* The area is positive if the orientation of the polygon is positive.
* @returns {number} The signed area of the polygon
*/
PIXI.Polygon.prototype.signedArea = function() {
const points = this.points;
const ln = points.length;
if ( ln < 6 ) return 0;
// Compute area
let area = 0;
let x1 = points[ln - 2];
let y1 = points[ln - 1];
for ( let i = 0; i < ln; i += 2 ) {
const x2 = points[i];
const y2 = points[i + 1];
area += (x2 - x1) * (y2 + y1);
x1 = x2;
y1 = y2;
}
// Negate the area because in Foundry canvas, y-axis is reversed
// See https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperorientation
// The 1/2 comes from the Shoelace formula
return area * -0.5;
};
/* -------------------------------------------- */
/**
* Reverse the order of the polygon points in-place, replacing the points array into the polygon.
* Note: references to the old points array will not be affected.
* @returns {PIXI.Polygon} This polygon with its orientation reversed
*/
PIXI.Polygon.prototype.reverseOrientation = function() {
const reversed_pts = [];
const pts = this.points;
const ln = pts.length - 2;
for ( let i = ln; i >= 0; i -= 2 ) reversed_pts.push(pts[i], pts[i + 1]);
this.points = reversed_pts;
if ( this._isPositive !== undefined ) this._isPositive = !this._isPositive;
return this;
};
/* -------------------------------------------- */
/**
* Add a de-duplicated point to the Polygon.
* @param {Point} point The point to add to the Polygon
* @returns {PIXI.Polygon} A reference to the polygon for method chaining
*/
PIXI.Polygon.prototype.addPoint = function({x, y}={}) {
const l = this.points.length;
if ( (x === this.points[l-2]) && (y === this.points[l-1]) ) return this;
this.points.push(x, y);
this.clearCache();
return this;
};
/* -------------------------------------------- */
/**
* Return the bounding box for a PIXI.Polygon.
* The bounding rectangle is normalized such that the width and height are non-negative.
* @returns {PIXI.Rectangle} The bounding PIXI.Rectangle
*/
PIXI.Polygon.prototype.getBounds = function() {
if ( this.points.length < 2 ) return new PIXI.Rectangle(0, 0, 0, 0);
let maxX; let maxY;
let minX = maxX = this.points[0];
let minY = maxY = this.points[1];
for ( let i=3; i<this.points.length; i+=2 ) {
const x = this.points[i-1];
const y = this.points[i];
if ( x < minX ) minX = x;
else if ( x > maxX ) maxX = x;
if ( y < minY ) minY = y;
else if ( y > maxY ) maxY = y;
}
return new PIXI.Rectangle(minX, minY, maxX - minX, maxY - minY);
};
/* -------------------------------------------- */
/**
* @typedef {Object} ClipperPoint
* @property {number} X
* @property {number} Y
*/
/**
* Construct a PIXI.Polygon instance from an array of clipper points [{X,Y}, ...].
* @param {ClipperPoint[]} points An array of points returned by clipper
* @param {object} [options] Options which affect how canvas points are generated
* @param {number} [options.scalingFactor=1] A scaling factor used to preserve floating point precision
* @returns {PIXI.Polygon} The resulting PIXI.Polygon
*/
PIXI.Polygon.fromClipperPoints = function(points, {scalingFactor=1}={}) {
const polygonPoints = [];
for ( const point of points ) {
polygonPoints.push(point.X / scalingFactor, point.Y / scalingFactor);
}
return new PIXI.Polygon(polygonPoints);
};
/* -------------------------------------------- */
/**
* Convert a PIXI.Polygon into an array of clipper points [{X,Y}, ...].
* Note that clipper points must be rounded to integers.
* In order to preserve some amount of floating point precision, an optional scaling factor may be provided.
* @param {object} [options] Options which affect how clipper points are generated
* @param {number} [options.scalingFactor=1] A scaling factor used to preserve floating point precision
* @returns {ClipperPoint[]} An array of points to be used by clipper
*/
PIXI.Polygon.prototype.toClipperPoints = function({scalingFactor=1}={}) {
const points = [];
for ( let i = 1; i < this.points.length; i += 2 ) {
points.push({
X: Math.round(this.points[i-1] * scalingFactor),
Y: Math.round(this.points[i] * scalingFactor)
});
}
return points;
};
/* -------------------------------------------- */
/**
* Determine whether the PIXI.Polygon is closed, defined by having the same starting and ending point.
* @type {boolean}
*/
Object.defineProperty(PIXI.Polygon.prototype, "isClosed", {
get: function() {
const ln = this.points.length;
if ( ln < 4 ) return false;
return (this.points[0] === this.points[ln-2]) && (this.points[1] === this.points[ln-1]);
},
enumerable: false
});
/* -------------------------------------------- */
/* Intersection Methods */
/* -------------------------------------------- */
/**
* Intersect this PIXI.Polygon with another PIXI.Polygon using the clipper library.
* @param {PIXI.Polygon} other Another PIXI.Polygon
* @param {object} [options] Options which configure how the intersection is computed
* @param {number} [options.clipType] The clipper clip type
* @param {number} [options.scalingFactor] A scaling factor passed to Polygon#toClipperPoints to preserve precision
* @returns {PIXI.Polygon} The intersected polygon
*/
PIXI.Polygon.prototype.intersectPolygon = function(other, {clipType, scalingFactor}={}) {
const otherPts = other.toClipperPoints({scalingFactor});
const solution = this.intersectClipper(otherPts, {clipType, scalingFactor});
return PIXI.Polygon.fromClipperPoints(solution.length ? solution[0] : [], {scalingFactor});
};
/* -------------------------------------------- */
/**
* Intersect this PIXI.Polygon with an array of ClipperPoints.
* @param {ClipperPoint[]} clipperPoints Array of clipper points generated by PIXI.Polygon.toClipperPoints()
* @param {object} [options] Options which configure how the intersection is computed
* @param {number} [options.clipType] The clipper clip type
* @param {number} [options.scalingFactor] A scaling factor passed to Polygon#toClipperPoints to preserve precision
* @returns {ClipperPoint[]} The resulting ClipperPaths
*/
PIXI.Polygon.prototype.intersectClipper = function(clipperPoints, {clipType, scalingFactor} = {}) {
clipType ??= ClipperLib.ClipType.ctIntersection;
const c = new ClipperLib.Clipper();
c.AddPath(this.toClipperPoints({scalingFactor}), ClipperLib.PolyType.ptSubject, true);
c.AddPath(clipperPoints, ClipperLib.PolyType.ptClip, true);
const solution = new ClipperLib.Paths();
c.Execute(clipType, solution);
return solution;
};
/* -------------------------------------------- */
/**
* Intersect this PIXI.Polygon with a PIXI.Circle.
* For now, 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 {PIXI.Circle} circle A PIXI.Circle
* @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.Polygon.prototype.intersectCircle = function(circle, options) {
return circle.intersectPolygon(this, options);
};
/* -------------------------------------------- */
/**
* Intersect this PIXI.Polygon with a PIXI.Rectangle.
* For now, convert the rectangle to a Polygon and use intersectPolygon.
* In the future we may replace this with more specialized logic which uses the line-line intersection formula.
* @param {PIXI.Rectangle} rect A PIXI.Rectangle
* @param {object} [options] Options which configure how the intersection is computed
* @returns {PIXI.Polygon} The intersected polygon
*/
PIXI.Polygon.prototype.intersectRectangle = function(rect, options) {
return rect.intersectPolygon(this, options);
};