Files
Foundry-VTT-Docker/resources/app/client-esm/canvas/regions/polygon-tree.mjs
2025-01-04 00:34:03 +01:00

299 lines
8.4 KiB
JavaScript

/**
* The node of a {@link RegionPolygonTree}.
*/
class RegionPolygonTreeNode {
/**
* Create a RegionPolygonTreeNode.
* @param {RegionPolygonTreeNode|null} parent The parent node.
* @internal
*/
constructor(parent) {
this.#parent = parent;
this.#children = [];
this.#depth = parent ? parent.depth + 1 : 0;
this.#isHole = this.#depth % 2 === 0;
if ( parent ) parent.#children.push(this);
else {
this.#polygon = null;
this.#clipperPath = null;
}
}
/* -------------------------------------------- */
/**
* Create a node from the Clipper path and add it to the children of the parent.
* @param {ClipperLib.IntPoint[]} clipperPath The clipper path of this node.
* @param {RegionPolygonTreeNode|null} parent The parent node or `null` if root.
* @internal
*/
static _fromClipperPath(clipperPath, parent) {
const node = new RegionPolygonTreeNode(parent);
if ( parent ) node.#clipperPath = clipperPath;
return node;
}
/* -------------------------------------------- */
/**
* The parent of this node or `null` if this is the root node.
* @type {RegionPolygonTreeNode|null}
*/
get parent() {
return this.#parent;
}
#parent;
/* -------------------------------------------- */
/**
* The children of this node.
* @type {ReadonlyArray<RegionPolygonTreeNode>}
*/
get children() {
return this.#children;
}
#children;
/* -------------------------------------------- */
/**
* The depth of this node.
* The depth of the root node is 0.
* @type {number}
*/
get depth() {
return this.#depth;
}
#depth;
/* -------------------------------------------- */
/**
* Is this a hole?
* The root node is a hole.
* @type {boolean}
*/
get isHole() {
return this.#isHole;
}
#isHole;
/* -------------------------------------------- */
/**
* The Clipper path of this node.
* It is empty in case of the root node.
* @type {ReadonlyArray<ClipperLib.IntPoint>|null}
*/
get clipperPath() {
return this.#clipperPath;
}
#clipperPath;
/* -------------------------------------------- */
/**
* The polygon of this node.
* It is `null` in case of the root node.
* @type {PIXI.Polygon|null}
*/
get polygon() {
let polygon = this.#polygon;
if ( polygon === undefined ) polygon = this.#polygon = this.#createPolygon();
return polygon;
}
#polygon;
/* -------------------------------------------- */
/**
* The points of the polygon ([x0, y0, x1, y1, ...]).
* They are `null` in case of the root node.
* @type {ReadonlyArray<number>|null}
*/
get points() {
const polygon = this.polygon;
if ( !polygon ) return null;
return polygon.points;
}
/* -------------------------------------------- */
/**
* The bounds of the polygon.
* They are `null` in case of the root node.
* @type {PIXI.Rectangle|null}
*/
get bounds() {
let bounds = this.#bounds;
if ( bounds === undefined ) bounds = this.#bounds = this.polygon?.getBounds() ?? null;
return bounds;
}
#bounds;
/* -------------------------------------------- */
/**
* Iterate over recursively over the children in depth-first order.
* @yields {RegionPolygonTreeNode}
*/
*[Symbol.iterator]() {
for ( const child of this.children ) {
yield child;
yield *child;
}
}
/* -------------------------------------------- */
/**
* Test whether given point is contained within this node.
* @param {Point} point The point.
* @returns {boolean}
*/
testPoint(point) {
return this.#testPoint(point) === 2;
}
/* -------------------------------------------- */
/**
* Test point containment.
* @param {Point} point The point.
* @returns {0|1|2} - 0: not contained within the polygon of this node.
* - 1: contained within the polygon of this node but also contained
* inside the polygon of a sub-node that is a hole.
* - 2: contained within the polygon of this node and not contained
* inside any polygon of a sub-node that is a hole.
*/
#testPoint(point) {
const {x, y} = point;
if ( this.parent ) {
if ( !this.bounds.contains(x, y) || !this.polygon.contains(x, y) ) return 0;
}
const children = this.children;
for ( let i = 0, n = children.length; i < n; i++ ) {
const result = children[i].#testPoint(point);
if ( result !== 0 ) return result;
}
return this.isHole ? 1 : 2;
}
/* -------------------------------------------- */
/**
* Test circle containment/intersection with this node.
* @param {Point} center The center point of the circle.
* @param {number} radius The radius of the circle.
* @returns {-1|0|1} - -1: the circle is in the exterior and does not intersect the boundary.
* - 0: the circle is intersects the boundary.
* - 1: the circle is in the interior and does not intersect the boundary.
*/
testCircle(center, radius) {
switch ( this.#testCircle(center, radius) ) {
case 2: return 1;
case 3: return 0;
default: return -1;
}
}
/* -------------------------------------------- */
/**
* Test circle containment/intersection with this node.
* @param {Point} center The center point of the circle.
* @param {number} radius The radius of the circle.
* @returns {0|1|2|3} - 0: does not intersect the boundary or interior of this node.
* - 1: contained within the polygon of this node but also contained
* inside the polygon of a sub-node that is a hole.
* - 2: contained within the polygon of this node and not contained
* inside any polygon of a sub-node that is a hole.
* - 3: intersects the boundary of this node or any sub-node.
*/
#testCircle(center, radius) {
if ( this.parent ) {
const {x, y} = center;
// Test whether the circle intersects the bounds of this node
const {left, right, top, bottom} = this.bounds;
if ( (x < left - radius) || (x > right + radius) || (y < top - radius) || (y > bottom + radius) ) return 0;
// Test whether the circle intersects any edge of the polygon of this node
const intersects = foundry.utils.pathCircleIntersects(this.points, true, center, radius);
if ( intersects ) return 3;
// Test whether the circle is completely outside of the polygon
const inside = this.polygon.contains(x, y);
if ( !inside ) return 0;
}
// Test the children of this node now that we know that the circle is
// completely inside of the polygon of this node
const children = this.children;
for ( let i = 0, n = children.length; i < n; i++ ) {
const result = children[i].#testCircle(center, radius);
if ( result !== 0 ) return result;
}
return this.isHole ? 1 : 2;
}
/* -------------------------------------------- */
/**
* Create the polygon of this node.
* @returns {PIXI.Polygon|null}
*/
#createPolygon() {
if ( !this.parent ) return null;
const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
const polygon = PIXI.Polygon.fromClipperPoints(this.clipperPath, {scalingFactor});
polygon._isPositive = !this.isHole;
return polygon;
}
}
/* -------------------------------------------- */
/**
* The polygon tree of a {@link Region}.
*/
export default class RegionPolygonTree extends RegionPolygonTreeNode {
/**
* Create a RegionPolygonTree.
* @internal
*/
constructor() {
super(null);
}
/* -------------------------------------------- */
/**
* Create the tree from a Clipper polygon tree.
* @param {ClipperLib.PolyTree} clipperPolyTree
* @internal
*/
static _fromClipperPolyTree(clipperPolyTree) {
const visit = (clipperPolyNode, parent) => {
const clipperPath = clipperPolyNode.Contour();
const node = RegionPolygonTreeNode._fromClipperPath(clipperPath, parent);
clipperPolyNode.Childs().forEach(child => visit(child, node));
return node;
};
const tree = new RegionPolygonTree();
clipperPolyTree.Childs().forEach(child => visit(child, tree));
return tree;
}
}