Initial
This commit is contained in:
175
resources/app/client/pixi/extensions/circle.js
Normal file
175
resources/app/client/pixi/extensions/circle.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
150
resources/app/client/pixi/extensions/graphics.js
Normal file
150
resources/app/client/pixi/extensions/graphics.js
Normal file
@@ -0,0 +1,150 @@
|
||||
|
||||
/**
|
||||
* Draws a path.
|
||||
* @param {number[]|PIXI.IPointData[]|PIXI.Polygon|...number|...PIXI.IPointData} path The polygon or points.
|
||||
* @returns {this} This Graphics instance.
|
||||
*/
|
||||
PIXI.Graphics.prototype.drawPath = function(...path) {
|
||||
let closeStroke = false;
|
||||
let polygon = path[0];
|
||||
let points;
|
||||
if ( polygon.points ) {
|
||||
closeStroke = polygon.closeStroke;
|
||||
points = polygon.points;
|
||||
} else if ( Array.isArray(path[0]) ) {
|
||||
points = path[0];
|
||||
} else {
|
||||
points = path;
|
||||
}
|
||||
polygon = new PIXI.Polygon(points);
|
||||
polygon.closeStroke = closeStroke;
|
||||
return this.drawShape(polygon);
|
||||
};
|
||||
PIXI.LegacyGraphics.prototype.drawPath = PIXI.Graphics.prototype.drawPath;
|
||||
PIXI.smooth.SmoothGraphics.prototype.drawPath = PIXI.Graphics.prototype.drawPath;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draws a smoothed polygon.
|
||||
* @param {number[]|PIXI.IPointData[]|PIXI.Polygon|...number|...PIXI.IPointData} path The polygon or points.
|
||||
* @param {number} [smoothing=0] The smoothness in the range [0, 1]. 0: no smoothing; 1: maximum smoothing.
|
||||
* @returns {this} This Graphics instance.
|
||||
*/
|
||||
PIXI.Graphics.prototype.drawSmoothedPolygon = function(...path) {
|
||||
let closeStroke = true;
|
||||
let polygon = path[0];
|
||||
let points;
|
||||
let factor;
|
||||
if ( polygon.points ) {
|
||||
closeStroke = polygon.closeStroke;
|
||||
points = polygon.points;
|
||||
factor = path[1];
|
||||
} else if ( Array.isArray(path[0]) ) {
|
||||
points = path[0];
|
||||
factor = path[1];
|
||||
} else if ( typeof path[0] === "number" ) {
|
||||
points = path;
|
||||
factor = path.length % 2 ? path.at(-1) : 0;
|
||||
} else {
|
||||
const n = path.length - (typeof path.at(-1) !== "object" ? 1 : 0);
|
||||
points = [];
|
||||
for ( let i = 0; i < n; i++ ) points.push(path[i].x, path[i].y);
|
||||
factor = path.at(n);
|
||||
}
|
||||
factor ??= 0;
|
||||
if ( (points.length < 6) || (factor <= 0) ) {
|
||||
polygon = new PIXI.Polygon(points.slice(0, points.length - (points.length % 2)));
|
||||
polygon.closeStroke = closeStroke;
|
||||
return this.drawShape(polygon);
|
||||
}
|
||||
const dedupedPoints = [points[0], points[1]];
|
||||
for ( let i = 2; i < points.length - 1; i += 2 ) {
|
||||
const x = points[i];
|
||||
const y = points[i + 1];
|
||||
if ( (x === points[i - 2]) && (y === points[i - 1]) ) continue;
|
||||
dedupedPoints.push(x, y);
|
||||
}
|
||||
points = dedupedPoints;
|
||||
if ( closeStroke && (points[0] === points.at(-2)) && (points[1] === points.at(-1)) ) points.length -= 2;
|
||||
if ( points.length < 6 ) {
|
||||
polygon = new PIXI.Polygon(points);
|
||||
polygon.closeStroke = closeStroke;
|
||||
return this.drawShape(polygon);
|
||||
}
|
||||
const getBezierControlPoints = (fromX, fromY, toX, toY, nextX, nextY) => {
|
||||
const vectorX = nextX - fromX;
|
||||
const vectorY = nextY - fromY;
|
||||
const preDistance = Math.hypot(toX - fromX, toY - fromY);
|
||||
const postDistance = Math.hypot(nextX - toX, nextY - toY);
|
||||
const totalDistance = preDistance + postDistance;
|
||||
const cp0d = 0.5 * factor * (preDistance / totalDistance);
|
||||
const cp1d = 0.5 * factor * (postDistance / totalDistance);
|
||||
return [
|
||||
toX - (vectorX * cp0d),
|
||||
toY - (vectorY * cp0d),
|
||||
toX + (vectorX * cp1d),
|
||||
toY + (vectorY * cp1d)
|
||||
];
|
||||
};
|
||||
let [fromX, fromY, toX, toY] = points;
|
||||
let [cpX, cpY, cpXNext, cpYNext] = getBezierControlPoints(points.at(-2), points.at(-1), fromX, fromY, toX, toY);
|
||||
this.moveTo(fromX, fromY);
|
||||
for ( let i = 2, n = points.length + (closeStroke ? 2 : 0); i < n; i += 2 ) {
|
||||
const nextX = points[(i + 2) % points.length];
|
||||
const nextY = points[(i + 3) % points.length];
|
||||
cpX = cpXNext;
|
||||
cpY = cpYNext;
|
||||
let cpX2;
|
||||
let cpY2;
|
||||
[cpX2, cpY2, cpXNext, cpYNext] = getBezierControlPoints(fromX, fromY, toX, toY, nextX, nextY);
|
||||
if ( !closeStroke && (i === 2) ) this.quadraticCurveTo(cpX2, cpY2, toX, toY);
|
||||
else if ( !closeStroke && (i === points.length - 2) ) this.quadraticCurveTo(cpX, cpY, toX, toY);
|
||||
else this.bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY);
|
||||
fromX = toX;
|
||||
fromY = toY;
|
||||
toX = nextX;
|
||||
toY = nextY;
|
||||
}
|
||||
if ( closeStroke ) this.closePath();
|
||||
this.finishPoly();
|
||||
return this;
|
||||
};
|
||||
PIXI.LegacyGraphics.prototype.drawSmoothedPolygon = PIXI.Graphics.prototype.drawSmoothedPolygon;
|
||||
PIXI.smooth.SmoothGraphics.prototype.drawSmoothedPolygon = PIXI.Graphics.prototype.drawSmoothedPolygon;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draws a smoothed path.
|
||||
* @param {number[]|PIXI.IPointData[]|PIXI.Polygon|...number|...PIXI.IPointData} path The polygon or points.
|
||||
* @param {number} [smoothing=0] The smoothness in the range [0, 1]. 0: no smoothing; 1: maximum smoothing.
|
||||
* @returns {this} This Graphics instance.
|
||||
*/
|
||||
PIXI.Graphics.prototype.drawSmoothedPath = function(...path) {
|
||||
let closeStroke = false;
|
||||
let polygon = path[0];
|
||||
let points;
|
||||
let factor;
|
||||
if ( polygon.points ) {
|
||||
closeStroke = polygon.closeStroke;
|
||||
points = polygon.points;
|
||||
factor = path[1];
|
||||
} else if ( Array.isArray(path[0]) ) {
|
||||
points = path[0];
|
||||
factor = path[1];
|
||||
} else if ( typeof path[0] === "number" ) {
|
||||
points = path;
|
||||
factor = path.length % 2 ? path.at(-1) : 0;
|
||||
} else {
|
||||
const n = path.length - (typeof path.at(-1) !== "object" ? 1 : 0);
|
||||
points = [];
|
||||
for ( let i = 0; i < n; i++ ) points.push(path[i].x, path[i].y);
|
||||
factor = path.at(n);
|
||||
}
|
||||
polygon = new PIXI.Polygon(points);
|
||||
polygon.closeStroke = closeStroke;
|
||||
return this.drawSmoothedPolygon(polygon, factor);
|
||||
};
|
||||
PIXI.LegacyGraphics.prototype.drawSmoothedPath = PIXI.Graphics.prototype.drawSmoothedPath;
|
||||
PIXI.smooth.SmoothGraphics.prototype.drawSmoothedPath = PIXI.Graphics.prototype.drawSmoothedPath;
|
||||
50
resources/app/client/pixi/extensions/observable-transform.js
Normal file
50
resources/app/client/pixi/extensions/observable-transform.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* A custom Transform class allowing to observe changes with a callback.
|
||||
* @extends PIXI.Transform
|
||||
*
|
||||
* @param {Function} callback The callback called to observe changes.
|
||||
* @param {Object} scope The scope of the callback.
|
||||
*/
|
||||
class ObservableTransform extends PIXI.Transform {
|
||||
constructor(callback, scope) {
|
||||
super();
|
||||
if ( !(callback instanceof Function) ) {
|
||||
throw new Error("The callback bound to an ObservableTransform class must be a valid function.")
|
||||
}
|
||||
if ( !(scope instanceof Object) ) {
|
||||
throw new Error("The scope bound to an ObservableTransform class must be a valid object/class.")
|
||||
}
|
||||
this.scope = scope;
|
||||
this.cb = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback which is observing the changes.
|
||||
* @type {Function}
|
||||
*/
|
||||
cb;
|
||||
|
||||
/**
|
||||
* The scope of the callback.
|
||||
* @type {Object}
|
||||
*/
|
||||
scope;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
onChange() {
|
||||
super.onChange();
|
||||
this.cb.call(this.scope);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
updateSkew() {
|
||||
super.updateSkew();
|
||||
this.cb.call(this.scope);
|
||||
}
|
||||
}
|
||||
236
resources/app/client/pixi/extensions/polygon.js
Normal file
236
resources/app/client/pixi/extensions/polygon.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
520
resources/app/client/pixi/extensions/rectangle.js
Normal file
520
resources/app/client/pixi/extensions/rectangle.js
Normal file
@@ -0,0 +1,520 @@
|
||||
/**
|
||||
* Bit code labels splitting a rectangle into zones, based on the Cohen-Sutherland algorithm.
|
||||
* See https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
|
||||
* left central right
|
||||
* top 1001 1000 1010
|
||||
* central 0001 0000 0010
|
||||
* bottom 0101 0100 0110
|
||||
* @enum {number}
|
||||
*/
|
||||
PIXI.Rectangle.CS_ZONES = {
|
||||
INSIDE: 0x0000,
|
||||
LEFT: 0x0001,
|
||||
RIGHT: 0x0010,
|
||||
TOP: 0x1000,
|
||||
BOTTOM: 0x0100,
|
||||
TOPLEFT: 0x1001,
|
||||
TOPRIGHT: 0x1010,
|
||||
BOTTOMRIGHT: 0x0110,
|
||||
BOTTOMLEFT: 0x0101
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Calculate center of this rectangle.
|
||||
* @type {Point}
|
||||
*/
|
||||
Object.defineProperty(PIXI.Rectangle.prototype, "center", { get: function() {
|
||||
return { x: this.x + (this.width * 0.5), y: this.y + (this.height * 0.5) };
|
||||
}});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return the bounding box for a PIXI.Rectangle.
|
||||
* The bounding rectangle is normalized such that the width and height are non-negative.
|
||||
* @returns {PIXI.Rectangle}
|
||||
*/
|
||||
PIXI.Rectangle.prototype.getBounds = function() {
|
||||
let {x, y, width, height} = this;
|
||||
x = width > 0 ? x : x + width;
|
||||
y = height > 0 ? y : y + height;
|
||||
return new PIXI.Rectangle(x, y, Math.abs(width), Math.abs(height));
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine if a point is on or nearly on this rectangle.
|
||||
* @param {Point} p Point to test
|
||||
* @returns {boolean} Is the point on the rectangle boundary?
|
||||
*/
|
||||
PIXI.Rectangle.prototype.pointIsOn = function(p) {
|
||||
const CSZ = PIXI.Rectangle.CS_ZONES;
|
||||
return this._getZone(p) === CSZ.INSIDE && this._getEdgeZone(p) !== CSZ.INSIDE;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Calculate the rectangle Zone for a given point located around, on, or in the rectangle.
|
||||
* See https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
|
||||
* This differs from _getZone in how points on the edge are treated: they are not considered inside.
|
||||
* @param {Point} point A point to test for location relative to the rectangle
|
||||
* @returns {PIXI.Rectangle.CS_ZONES} Which edge zone does the point belong to?
|
||||
*/
|
||||
PIXI.Rectangle.prototype._getEdgeZone = function(point) {
|
||||
const CSZ = PIXI.Rectangle.CS_ZONES;
|
||||
let code = CSZ.INSIDE;
|
||||
if ( point.x < this.x || point.x.almostEqual(this.x) ) code |= CSZ.LEFT;
|
||||
else if ( point.x > this.right || point.x.almostEqual(this.right) ) code |= CSZ.RIGHT;
|
||||
if ( point.y < this.y || point.y.almostEqual(this.y) ) code |= CSZ.TOP;
|
||||
else if ( point.y > this.bottom || point.y.almostEqual(this.bottom) ) code |= CSZ.BOTTOM;
|
||||
return code;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get all the points (corners) for a polygon approximation of a rectangle between two points on the rectangle.
|
||||
* The two points can be anywhere in 2d space on or outside the rectangle.
|
||||
* The starting and ending side are based on the zone of the corresponding a and b points.
|
||||
* (See PIXI.Rectangle.CS_ZONES.)
|
||||
* This is the rectangular version of PIXI.Circle.prototype.pointsBetween, and is similarly used
|
||||
* to draw the portion of the shape between two intersection points on that shape.
|
||||
* @param { Point } a A point on or outside the rectangle, representing the starting position.
|
||||
* @param { Point } b A point on or outside the rectangle, representing the starting position.
|
||||
* @returns { Point[]} Points returned are clockwise from start to end.
|
||||
*/
|
||||
PIXI.Rectangle.prototype.pointsBetween = function(a, b) {
|
||||
const CSZ = PIXI.Rectangle.CS_ZONES;
|
||||
|
||||
// Assume the point could be outside the rectangle but not inside (which would be undefined).
|
||||
const zoneA = this._getEdgeZone(a);
|
||||
if ( !zoneA ) return [];
|
||||
const zoneB = this._getEdgeZone(b);
|
||||
if ( !zoneB ) return [];
|
||||
|
||||
// If on the same wall, return none if end is counterclockwise to start.
|
||||
if ( zoneA === zoneB && foundry.utils.orient2dFast(this.center, a, b) <= 0 ) return [];
|
||||
let z = zoneA;
|
||||
const pts = [];
|
||||
for ( let i = 0; i < 4; i += 1) {
|
||||
if ( (z & CSZ.LEFT) ) {
|
||||
if ( z !== CSZ.TOPLEFT ) pts.push({ x: this.left, y: this.top });
|
||||
z = CSZ.TOP;
|
||||
} else if ( (z & CSZ.TOP) ) {
|
||||
if ( z !== CSZ.TOPRIGHT ) pts.push({ x: this.right, y: this.top });
|
||||
z = CSZ.RIGHT;
|
||||
} else if ( (z & CSZ.RIGHT) ) {
|
||||
if ( z !== CSZ.BOTTOMRIGHT ) pts.push({ x: this.right, y: this.bottom });
|
||||
z = CSZ.BOTTOM;
|
||||
} else if ( (z & CSZ.BOTTOM) ) {
|
||||
if ( z !== CSZ.BOTTOMLEFT ) pts.push({ x: this.left, y: this.bottom });
|
||||
z = CSZ.LEFT;
|
||||
}
|
||||
if ( z & zoneB ) break;
|
||||
}
|
||||
return pts;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get all intersection points for a segment A|B
|
||||
* Intersections are sorted from A to B.
|
||||
* @param {Point} a Endpoint A of the segment
|
||||
* @param {Point} b Endpoint B of the segment
|
||||
* @returns {Point[]} Array of intersections or empty if no intersection.
|
||||
* If A|B is parallel to an edge of this rectangle, returns the two furthest points on
|
||||
* the segment A|B that are on the edge.
|
||||
* The return object's t0 property signifies the location of the intersection on segment A|B.
|
||||
* This will be NaN if the segment is a point.
|
||||
* The return object's t1 property signifies the location of the intersection on the rectangle edge.
|
||||
* The t1 value is measured relative to the intersecting edge of the rectangle.
|
||||
*/
|
||||
PIXI.Rectangle.prototype.segmentIntersections = function(a, b) {
|
||||
|
||||
// The segment is collinear with a vertical edge
|
||||
if ( a.x.almostEqual(b.x) && (a.x.almostEqual(this.left) || a.x.almostEqual(this.right)) ) {
|
||||
const minY1 = Math.min(a.y, b.y);
|
||||
const minY2 = Math.min(this.top, this.bottom);
|
||||
const maxY1 = Math.max(a.y, b.y);
|
||||
const maxY2 = Math.max(this.top, this.bottom);
|
||||
const minIxY = Math.max(minY1, minY2);
|
||||
const maxIxY = Math.min(maxY1, maxY2);
|
||||
|
||||
// Test whether the two segments intersect
|
||||
const pointIntersection = minIxY.almostEqual(maxIxY);
|
||||
if ( pointIntersection || (minIxY < maxIxY) ) {
|
||||
// Determine t-values of the a|b segment intersections (t0) and the rectangle edge (t1).
|
||||
const distAB = Math.abs(b.y - a.y);
|
||||
const distRect = this.height;
|
||||
const y = (b.y - a.y) > 0 ? a.y : b.y;
|
||||
const rectY = a.x.almostEqual(this.right) ? this.top : this.bottom;
|
||||
const minRes = {x: a.x, y: minIxY, t0: (minIxY - y) / distAB, t1: Math.abs((minIxY - rectY) / distRect)};
|
||||
|
||||
// If true, the a|b segment is nearly a point and t0 is likely NaN.
|
||||
if ( pointIntersection ) return [minRes];
|
||||
|
||||
// Return in order nearest a, nearest b
|
||||
const maxRes = {x: a.x, y: maxIxY, t0: (maxIxY - y) / distAB, t1: Math.abs((maxIxY - rectY) / distRect)};
|
||||
return Math.abs(minIxY - a.y) < Math.abs(maxIxY - a.y)
|
||||
? [minRes, maxRes]
|
||||
: [maxRes, minRes];
|
||||
}
|
||||
}
|
||||
|
||||
// The segment is collinear with a horizontal edge
|
||||
else if ( a.y.almostEqual(b.y) && (a.y.almostEqual(this.top) || a.y.almostEqual(this.bottom))) {
|
||||
const minX1 = Math.min(a.x, b.x);
|
||||
const minX2 = Math.min(this.right, this.left);
|
||||
const maxX1 = Math.max(a.x, b.x);
|
||||
const maxX2 = Math.max(this.right, this.left);
|
||||
const minIxX = Math.max(minX1, minX2);
|
||||
const maxIxX = Math.min(maxX1, maxX2);
|
||||
|
||||
// Test whether the two segments intersect
|
||||
const pointIntersection = minIxX.almostEqual(maxIxX);
|
||||
if ( pointIntersection || (minIxX < maxIxX) ) {
|
||||
// Determine t-values of the a|b segment intersections (t0) and the rectangle edge (t1).
|
||||
const distAB = Math.abs(b.x - a.x);
|
||||
const distRect = this.width;
|
||||
const x = (b.x - a.x) > 0 ? a.x : b.x;
|
||||
const rectX = a.y.almostEqual(this.top) ? this.left : this.right;
|
||||
const minRes = {x: minIxX, y: a.y, t0: (minIxX - x) / distAB, t1: Math.abs((minIxX - rectX) / distRect)};
|
||||
|
||||
// If true, the a|b segment is nearly a point and t0 is likely NaN.
|
||||
if ( pointIntersection ) return [minRes];
|
||||
|
||||
// Return in order nearest a, nearest b
|
||||
const maxRes = {x: maxIxX, y: a.y, t0: (maxIxX - x) / distAB, t1: Math.abs((maxIxX - rectX) / distRect)};
|
||||
return Math.abs(minIxX - a.x) < Math.abs(maxIxX - a.x) ? [minRes, maxRes] : [maxRes, minRes];
|
||||
}
|
||||
}
|
||||
|
||||
// Follows structure of lineSegmentIntersects
|
||||
const zoneA = this._getZone(a);
|
||||
const zoneB = this._getZone(b);
|
||||
if ( !(zoneA | zoneB) ) return []; // Bitwise OR is 0: both points inside rectangle.
|
||||
|
||||
// Regular AND: one point inside, one outside
|
||||
// Otherwise, both points outside
|
||||
const zones = !(zoneA && zoneB) ? [zoneA || zoneB] : [zoneA, zoneB];
|
||||
|
||||
// If 2 zones, line likely intersects two edges.
|
||||
// It is possible to have a line that starts, for example, at center left and moves to center top.
|
||||
// In this case it may not cross the rectangle.
|
||||
if ( zones.length === 2 && !this.lineSegmentIntersects(a, b) ) return [];
|
||||
const CSZ = PIXI.Rectangle.CS_ZONES;
|
||||
const lsi = foundry.utils.lineSegmentIntersects;
|
||||
const lli = foundry.utils.lineLineIntersection;
|
||||
const { leftEdge, rightEdge, bottomEdge, topEdge } = this;
|
||||
const ixs = [];
|
||||
for ( const z of zones ) {
|
||||
let ix;
|
||||
if ( (z & CSZ.LEFT)
|
||||
&& lsi(leftEdge.A, leftEdge.B, a, b)) ix = lli(a, b, leftEdge.A, leftEdge.B);
|
||||
if ( !ix && (z & CSZ.RIGHT)
|
||||
&& lsi(rightEdge.A, rightEdge.B, a, b)) ix = lli(a, b, rightEdge.A, rightEdge.B);
|
||||
if ( !ix && (z & CSZ.TOP)
|
||||
&& lsi(topEdge.A, topEdge.B, a, b)) ix = lli(a, b, topEdge.A, topEdge.B);
|
||||
if ( !ix && (z & CSZ.BOTTOM)
|
||||
&& lsi(bottomEdge.A, bottomEdge.B, a, b)) ix = lli(a, b, bottomEdge.A, bottomEdge.B);
|
||||
|
||||
// The ix should always be a point by now
|
||||
if ( !ix ) throw new Error("PIXI.Rectangle.prototype.segmentIntersections returned an unexpected null point.");
|
||||
ixs.push(ix);
|
||||
}
|
||||
return ixs;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Compute the intersection of this Rectangle with some other Rectangle.
|
||||
* @param {PIXI.Rectangle} other Some other rectangle which intersects this one
|
||||
* @returns {PIXI.Rectangle} The intersected rectangle
|
||||
*/
|
||||
PIXI.Rectangle.prototype.intersection = function(other) {
|
||||
const x0 = this.x < other.x ? other.x : this.x;
|
||||
const x1 = this.right > other.right ? other.right : this.right;
|
||||
const y0 = this.y < other.y ? other.y : this.y;
|
||||
const y1 = this.bottom > other.bottom ? other.bottom : this.bottom;
|
||||
return new PIXI.Rectangle(x0, y0, x1 - x0, y1 - y0);
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Convert this PIXI.Rectangle into a PIXI.Polygon
|
||||
* @returns {PIXI.Polygon} The Rectangle expressed as a PIXI.Polygon
|
||||
*/
|
||||
PIXI.Rectangle.prototype.toPolygon = function() {
|
||||
const points = [this.left, this.top, this.right, this.top, this.right, this.bottom, this.left, this.bottom];
|
||||
return new PIXI.Polygon(points);
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the left edge of this rectangle.
|
||||
* The returned edge endpoints are oriented clockwise around the rectangle.
|
||||
* @type {{A: Point, B: Point}}
|
||||
*/
|
||||
Object.defineProperty(PIXI.Rectangle.prototype, "leftEdge", { get: function() {
|
||||
return { A: { x: this.left, y: this.bottom }, B: { x: this.left, y: this.top }};
|
||||
}});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the right edge of this rectangle.
|
||||
* The returned edge endpoints are oriented clockwise around the rectangle.
|
||||
* @type {{A: Point, B: Point}}
|
||||
*/
|
||||
Object.defineProperty(PIXI.Rectangle.prototype, "rightEdge", { get: function() {
|
||||
return { A: { x: this.right, y: this.top }, B: { x: this.right, y: this.bottom }};
|
||||
}});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the top edge of this rectangle.
|
||||
* The returned edge endpoints are oriented clockwise around the rectangle.
|
||||
* @type {{A: Point, B: Point}}
|
||||
*/
|
||||
Object.defineProperty(PIXI.Rectangle.prototype, "topEdge", { get: function() {
|
||||
return { A: { x: this.left, y: this.top }, B: { x: this.right, y: this.top }};
|
||||
}});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the bottom edge of this rectangle.
|
||||
* The returned edge endpoints are oriented clockwise around the rectangle.
|
||||
* @type {{A: Point, B: Point}}
|
||||
*/
|
||||
Object.defineProperty(PIXI.Rectangle.prototype, "bottomEdge", { get: function() {
|
||||
return { A: { x: this.right, y: this.bottom }, B: { x: this.left, y: this.bottom }};
|
||||
}});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Calculate the rectangle Zone for a given point located around or in the rectangle.
|
||||
* https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
|
||||
*
|
||||
* @param {Point} p Point to test for location relative to the rectangle
|
||||
* @returns {PIXI.Rectangle.CS_ZONES}
|
||||
*/
|
||||
PIXI.Rectangle.prototype._getZone = function(p) {
|
||||
const CSZ = PIXI.Rectangle.CS_ZONES;
|
||||
let code = CSZ.INSIDE;
|
||||
|
||||
if ( p.x < this.x ) code |= CSZ.LEFT;
|
||||
else if ( p.x > this.right ) code |= CSZ.RIGHT;
|
||||
|
||||
if ( p.y < this.y ) code |= CSZ.TOP;
|
||||
else if ( p.y > this.bottom ) code |= CSZ.BOTTOM;
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test whether a line segment AB intersects this rectangle.
|
||||
* @param {Point} a The first endpoint of segment AB
|
||||
* @param {Point} b The second endpoint of segment AB
|
||||
* @param {object} [options] Options affecting the intersect test.
|
||||
* @param {boolean} [options.inside] If true, a line contained within the rectangle will
|
||||
* return true.
|
||||
* @returns {boolean} True if intersects.
|
||||
*/
|
||||
PIXI.Rectangle.prototype.lineSegmentIntersects = function(a, b, { inside = false } = {}) {
|
||||
const zoneA = this._getZone(a);
|
||||
const zoneB = this._getZone(b);
|
||||
|
||||
if ( !(zoneA | zoneB) ) return inside; // Bitwise OR is 0: both points inside rectangle.
|
||||
if ( zoneA & zoneB ) return false; // Bitwise AND is not 0: both points share outside zone
|
||||
if ( !(zoneA && zoneB) ) return true; // Regular AND: one point inside, one outside
|
||||
|
||||
// Line likely intersects, but some possibility that the line starts at, say, center left
|
||||
// and moves to center top which means it may or may not cross the rectangle
|
||||
const CSZ = PIXI.Rectangle.CS_ZONES;
|
||||
const lsi = foundry.utils.lineSegmentIntersects;
|
||||
|
||||
// If the zone is a corner, like top left, test one side and then if not true, test
|
||||
// the other. If the zone is on a side, like left, just test that side.
|
||||
const leftEdge = this.leftEdge;
|
||||
if ( (zoneA & CSZ.LEFT) && lsi(leftEdge.A, leftEdge.B, a, b) ) return true;
|
||||
|
||||
const rightEdge = this.rightEdge;
|
||||
if ( (zoneA & CSZ.RIGHT) && lsi(rightEdge.A, rightEdge.B, a, b) ) return true;
|
||||
|
||||
const topEdge = this.topEdge;
|
||||
if ( (zoneA & CSZ.TOP) && lsi(topEdge.A, topEdge.B, a, b) ) return true;
|
||||
|
||||
const bottomEdge = this.bottomEdge;
|
||||
if ( (zoneA & CSZ.BOTTOM ) && lsi(bottomEdge.A, bottomEdge.B, a, b) ) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Intersect this PIXI.Rectangle with a PIXI.Polygon.
|
||||
* Currently uses the clipper library.
|
||||
* In the future we may replace this with more specialized logic which uses the line-line intersection formula.
|
||||
* @param {PIXI.Polygon} polygon A 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 for precision
|
||||
* @param {string} [options.weilerAtherton=true] Use the Weiler-Atherton algorithm. Otherwise, use Clipper.
|
||||
* @param {boolean} [options.canMutate] If the WeilerAtherton constructor could mutate or not
|
||||
* @returns {PIXI.Polygon} The intersected polygon
|
||||
*/
|
||||
PIXI.Rectangle.prototype.intersectPolygon = function(polygon, {clipType, scalingFactor, canMutate, weilerAtherton=true}={}) {
|
||||
if ( !this.width || !this.height ) 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, canMutate, scalingFactor});
|
||||
if ( !res.length ) return new PIXI.Polygon([]);
|
||||
return res[0];
|
||||
}
|
||||
|
||||
// Use Clipper polygon intersection
|
||||
return polygon.intersectPolygon(this.toPolygon(), {clipType, canMutate, scalingFactor});
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Intersect this PIXI.Rectangle with an array of ClipperPoints. Currently, uses the clipper library.
|
||||
* In the future we may replace this with more specialized logic which uses the line-line intersection formula.
|
||||
* @param {ClipperPoint[]} clipperPoints An array of ClipperPoints 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 {PIXI.Polygon|null} The intersected polygon or null if no solution was present
|
||||
*/
|
||||
PIXI.Rectangle.prototype.intersectClipper = function(clipperPoints, {clipType, scalingFactor}={}) {
|
||||
if ( !this.width || !this.height ) return [];
|
||||
return this.toPolygon().intersectPolygon(clipperPoints, {clipType, scalingFactor});
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine whether some other Rectangle overlaps with this one.
|
||||
* This check differs from the parent class Rectangle#intersects test because it is true for adjacency (zero area).
|
||||
* @param {PIXI.Rectangle} other Some other rectangle against which to compare
|
||||
* @returns {boolean} Do the rectangles overlap?
|
||||
*/
|
||||
PIXI.Rectangle.prototype.overlaps = function(other) {
|
||||
return (other.right >= this.left)
|
||||
&& (other.left <= this.right)
|
||||
&& (other.bottom >= this.top)
|
||||
&& (other.top <= this.bottom);
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Normalize the width and height of the rectangle in-place, enforcing that those dimensions be positive.
|
||||
* @returns {PIXI.Rectangle}
|
||||
*/
|
||||
PIXI.Rectangle.prototype.normalize = function() {
|
||||
if ( this.width < 0 ) {
|
||||
this.x += this.width;
|
||||
this.width = Math.abs(this.width);
|
||||
}
|
||||
if ( this.height < 0 ) {
|
||||
this.y += this.height;
|
||||
this.height = Math.abs(this.height);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Fits this rectangle around this rectangle rotated around the given pivot counterclockwise by the given angle in radians.
|
||||
* @param {number} radians The angle of rotation.
|
||||
* @param {PIXI.Point} [pivot] An optional pivot point (normalized).
|
||||
* @returns {this} This rectangle.
|
||||
*/
|
||||
PIXI.Rectangle.prototype.rotate = function(radians, pivot) {
|
||||
if ( radians === 0 ) return this;
|
||||
return this.constructor.fromRotation(this.x, this.y, this.width, this.height, radians, pivot, this);
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create normalized rectangular bounds given a rectangle shape and an angle of central rotation.
|
||||
* @param {number} x The top-left x-coordinate of the un-rotated rectangle
|
||||
* @param {number} y The top-left y-coordinate of the un-rotated rectangle
|
||||
* @param {number} width The width of the un-rotated rectangle
|
||||
* @param {number} height The height of the un-rotated rectangle
|
||||
* @param {number} radians The angle of rotation about the center
|
||||
* @param {PIXI.Point} [pivot] An optional pivot point (if not provided, the pivot is the centroid)
|
||||
* @param {PIXI.Rectangle} [_outRect] (Internal)
|
||||
* @returns {PIXI.Rectangle} The constructed rotated rectangle bounds
|
||||
*/
|
||||
PIXI.Rectangle.fromRotation = function(x, y, width, height, radians, pivot, _outRect) {
|
||||
const cosAngle = Math.cos(radians);
|
||||
const sinAngle = Math.sin(radians);
|
||||
|
||||
// Create the output rect if necessary
|
||||
_outRect ??= new PIXI.Rectangle();
|
||||
|
||||
// Is it possible to do with the simple computation?
|
||||
if ( pivot === undefined || ((pivot.x === 0.5) && (pivot.y === 0.5)) ) {
|
||||
_outRect.height = (height * Math.abs(cosAngle)) + (width * Math.abs(sinAngle));
|
||||
_outRect.width = (height * Math.abs(sinAngle)) + (width * Math.abs(cosAngle));
|
||||
_outRect.x = x + ((width - _outRect.width) / 2);
|
||||
_outRect.y = y + ((height - _outRect.height) / 2);
|
||||
return _outRect;
|
||||
}
|
||||
|
||||
// Calculate the pivot point in absolute coordinates
|
||||
const pivotX = x + (width * pivot.x);
|
||||
const pivotY = y + (height * pivot.y);
|
||||
|
||||
// Calculate vectors from pivot to the rectangle's corners
|
||||
const tlX = x - pivotX;
|
||||
const tlY = y - pivotY;
|
||||
const trX = x + width - pivotX;
|
||||
const trY = y - pivotY;
|
||||
const blX = x - pivotX;
|
||||
const blY = y + height - pivotY;
|
||||
const brX = x + width - pivotX;
|
||||
const brY = y + height - pivotY;
|
||||
|
||||
// Apply rotation to the vectors
|
||||
const rTlX = (cosAngle * tlX) - (sinAngle * tlY);
|
||||
const rTlY = (sinAngle * tlX) + (cosAngle * tlY);
|
||||
const rTrX = (cosAngle * trX) - (sinAngle * trY);
|
||||
const rTrY = (sinAngle * trX) + (cosAngle * trY);
|
||||
const rBlX = (cosAngle * blX) - (sinAngle * blY);
|
||||
const rBlY = (sinAngle * blX) + (cosAngle * blY);
|
||||
const rBrX = (cosAngle * brX) - (sinAngle * brY);
|
||||
const rBrY = (sinAngle * brX) + (cosAngle * brY);
|
||||
|
||||
// Find the new corners of the bounding rectangle
|
||||
const minX = Math.min(rTlX, rTrX, rBlX, rBrX);
|
||||
const minY = Math.min(rTlY, rTrY, rBlY, rBrY);
|
||||
const maxX = Math.max(rTlX, rTrX, rBlX, rBrX);
|
||||
const maxY = Math.max(rTlY, rTrY, rBlY, rBrY);
|
||||
|
||||
// Assign the new computed bounding box
|
||||
_outRect.x = pivotX + minX;
|
||||
_outRect.y = pivotY + minY;
|
||||
_outRect.width = maxX - minX;
|
||||
_outRect.height = maxY - minY;
|
||||
return _outRect;
|
||||
};
|
||||
Reference in New Issue
Block a user