Initial
This commit is contained in:
4
resources/app/client-esm/canvas/regions/_module.mjs
Normal file
4
resources/app/client-esm/canvas/regions/_module.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
export {default as RegionGeometry} from "./geometry.mjs";
|
||||
export {default as RegionMesh} from "./mesh.mjs";
|
||||
export {default as RegionPolygonTree} from "./polygon-tree.mjs";
|
||||
export {default as RegionShape} from "./shape.mjs";
|
||||
65
resources/app/client-esm/canvas/regions/geometry.mjs
Normal file
65
resources/app/client-esm/canvas/regions/geometry.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* The geometry of a {@link Region}.
|
||||
* - Vertex Attribute: `aVertexPosition` (`vec2`)
|
||||
* - Draw Mode: `PIXI.DRAW_MODES.TRIANGLES`
|
||||
*/
|
||||
export default class RegionGeometry extends PIXI.Geometry {
|
||||
|
||||
/**
|
||||
* Create a RegionGeometry.
|
||||
* @param {Region} region The Region to create the RegionGeometry from.
|
||||
* @internal
|
||||
*/
|
||||
constructor(region) {
|
||||
super();
|
||||
this.#region = region;
|
||||
this.addAttribute("aVertexPosition", new PIXI.Buffer(new Float32Array(), true, false), 2);
|
||||
this.addIndex(new PIXI.Buffer(new Uint16Array(), true, true));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The Region this geometry belongs to.
|
||||
* @type {Region}
|
||||
*/
|
||||
get region() {
|
||||
return this.#region;
|
||||
}
|
||||
|
||||
#region;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Do the buffers need to be updated?
|
||||
* @type {boolean}
|
||||
*/
|
||||
#invalidBuffers = true;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the buffers.
|
||||
* @internal
|
||||
*/
|
||||
_clearBuffers() {
|
||||
this.buffers[0].update(new Float32Array());
|
||||
this.indexBuffer.update(new Uint16Array());
|
||||
this.#invalidBuffers = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the buffers.
|
||||
* @internal
|
||||
*/
|
||||
_updateBuffers() {
|
||||
if ( !this.#invalidBuffers ) return;
|
||||
const triangulation = this.region.triangulation;
|
||||
this.buffers[0].update(triangulation.vertices);
|
||||
this.indexBuffer.update(triangulation.indices);
|
||||
this.#invalidBuffers = false;
|
||||
}
|
||||
}
|
||||
213
resources/app/client-esm/canvas/regions/mesh.mjs
Normal file
213
resources/app/client-esm/canvas/regions/mesh.mjs
Normal file
@@ -0,0 +1,213 @@
|
||||
|
||||
/**
|
||||
* A mesh of a {@link Region}.
|
||||
* @extends {PIXI.Container}
|
||||
*/
|
||||
export default class RegionMesh extends PIXI.Container {
|
||||
|
||||
/**
|
||||
* Create a RegionMesh.
|
||||
* @param {Region} region The Region to create the RegionMesh from.
|
||||
* @param {AbstractBaseShader} [shaderClass] The shader class to use.
|
||||
*/
|
||||
constructor(region, shaderClass=RegionShader) {
|
||||
super();
|
||||
this.#region = region;
|
||||
this.region.geometry.refCount++;
|
||||
if ( !AbstractBaseShader.isPrototypeOf(shaderClass) ) {
|
||||
throw new Error("RegionMesh shader class must inherit from AbstractBaseShader.");
|
||||
}
|
||||
this.#shader = shaderClass.create();
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Shared point instance.
|
||||
* @type {PIXI.Point}
|
||||
*/
|
||||
static #SHARED_POINT = new PIXI.Point();
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The Region of this RegionMesh.
|
||||
* @type {RegionMesh}
|
||||
*/
|
||||
get region() {
|
||||
return this.#region;
|
||||
}
|
||||
|
||||
#region;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The shader bound to this RegionMesh.
|
||||
* @type {AbstractBaseShader}
|
||||
*/
|
||||
get shader() {
|
||||
return this.#shader;
|
||||
}
|
||||
|
||||
#shader;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The blend mode assigned to this RegionMesh.
|
||||
* @type {PIXI.BLEND_MODES}
|
||||
*/
|
||||
get blendMode() {
|
||||
return this.#state.blendMode;
|
||||
}
|
||||
|
||||
set blendMode(value) {
|
||||
if ( this.#state.blendMode === value ) return;
|
||||
this.#state.blendMode = value;
|
||||
this._tintAlphaDirty = true;
|
||||
}
|
||||
|
||||
#state = PIXI.State.for2d();
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The tint applied to the mesh. This is a hex value.
|
||||
*
|
||||
* A value of 0xFFFFFF will remove any tint effect.
|
||||
* @type {number}
|
||||
* @defaultValue 0xFFFFFF
|
||||
*/
|
||||
get tint() {
|
||||
return this._tintColor.value;
|
||||
}
|
||||
|
||||
set tint(tint) {
|
||||
const currentTint = this._tintColor.value;
|
||||
this._tintColor.setValue(tint);
|
||||
if ( currentTint === this._tintColor.value ) return;
|
||||
this._tintAlphaDirty = true;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The tint applied to the mesh. This is a hex value. A value of 0xFFFFFF will remove any tint effect.
|
||||
* @type {PIXI.Color}
|
||||
* @protected
|
||||
*/
|
||||
_tintColor = new PIXI.Color(0xFFFFFF);
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Cached tint value for the shader uniforms.
|
||||
* @type {[red: number, green: number, blue: number, alpha: number]}
|
||||
* @protected
|
||||
* @internal
|
||||
*/
|
||||
_cachedTint = [1, 1, 1, 1];
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Used to track a tint or alpha change to execute a recomputation of _cachedTint.
|
||||
* @type {boolean}
|
||||
* @protected
|
||||
*/
|
||||
_tintAlphaDirty = true;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize shader based on the shader class type.
|
||||
* @param {type AbstractBaseShader} shaderClass The shader class, which must inherit from {@link AbstractBaseShader}.
|
||||
*/
|
||||
setShaderClass(shaderClass) {
|
||||
if ( !AbstractBaseShader.isPrototypeOf(shaderClass) ) {
|
||||
throw new Error("RegionMesh shader class must inherit from AbstractBaseShader.");
|
||||
}
|
||||
if ( this.#shader.constructor === shaderClass ) return;
|
||||
|
||||
// Create shader program
|
||||
this.#shader = shaderClass.create();
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
updateTransform() {
|
||||
super.updateTransform();
|
||||
|
||||
// We set tintAlphaDirty to true if the worldAlpha has changed
|
||||
// It is needed to recompute the _cachedTint vec4 which is a combination of tint and alpha
|
||||
if ( this.#worldAlpha !== this.worldAlpha ) {
|
||||
this.#worldAlpha = this.worldAlpha;
|
||||
this._tintAlphaDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
#worldAlpha;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_render(renderer) {
|
||||
if ( this._tintAlphaDirty ) {
|
||||
const premultiply = PIXI.utils.premultiplyBlendMode[1][this.blendMode] === this.blendMode;
|
||||
PIXI.Color.shared.setValue(this._tintColor)
|
||||
.premultiply(this.worldAlpha, premultiply)
|
||||
.toArray(this._cachedTint);
|
||||
this._tintAlphaDirty = false;
|
||||
}
|
||||
this.#shader._preRender(this, renderer);
|
||||
this.#shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true);
|
||||
|
||||
// Flush batch renderer
|
||||
renderer.batch.flush();
|
||||
|
||||
// Set state
|
||||
renderer.state.set(this.#state);
|
||||
|
||||
// Bind shader and geometry
|
||||
renderer.shader.bind(this.#shader);
|
||||
const geometry = this.region.geometry;
|
||||
geometry._updateBuffers();
|
||||
renderer.geometry.bind(geometry, this.#shader);
|
||||
|
||||
// Draw the geometry
|
||||
renderer.geometry.draw(PIXI.DRAW_MODES.TRIANGLES);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_calculateBounds() {
|
||||
const {left, top, right, bottom} = this.region.bounds;
|
||||
this._bounds.addFrame(this.transform, left, top, right, bottom);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Tests if a point is inside this RegionMesh.
|
||||
* @param {PIXI.IPointData} point
|
||||
* @returns {boolean}
|
||||
*/
|
||||
containsPoint(point) {
|
||||
return this.region.polygonTree.testPoint(this.worldTransform.applyInverse(point, RegionMesh.#SHARED_POINT));
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
destroy(options) {
|
||||
super.destroy(options);
|
||||
const geometry = this.region.geometry;
|
||||
geometry.refCount--;
|
||||
if ( geometry.refCount === 0 ) geometry.dispose();
|
||||
this.#shader = null;
|
||||
this.#state = null;
|
||||
}
|
||||
}
|
||||
298
resources/app/client-esm/canvas/regions/polygon-tree.mjs
Normal file
298
resources/app/client-esm/canvas/regions/polygon-tree.mjs
Normal file
@@ -0,0 +1,298 @@
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
371
resources/app/client-esm/canvas/regions/shape.mjs
Normal file
371
resources/app/client-esm/canvas/regions/shape.mjs
Normal file
@@ -0,0 +1,371 @@
|
||||
import {CircleShapeData, EllipseShapeData, PolygonShapeData, RectangleShapeData} from "../../data/_module.mjs";
|
||||
|
||||
/**
|
||||
* A shape of a {@link Region}.
|
||||
* @template {data.BaseShapeData} T
|
||||
* @abstract
|
||||
*/
|
||||
export default class RegionShape {
|
||||
|
||||
/**
|
||||
* Create a RegionShape.
|
||||
* @param {T} data The shape data.
|
||||
* @internal
|
||||
*/
|
||||
constructor(data) {
|
||||
this.#data = data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the RegionShape from the shape data.
|
||||
* @template {data.BaseShapeData} T
|
||||
* @param {T} data The shape data.
|
||||
* @returns {RegionShape<T>}
|
||||
*/
|
||||
static create(data) {
|
||||
switch ( data.type ) {
|
||||
case "circle": return new RegionCircle(data);
|
||||
case "ellipse": return new RegionEllipse(data);
|
||||
case "polygon": return new RegionPolygon(data);
|
||||
case "rectangle": return new RegionRectangle(data);
|
||||
default: throw new Error("Invalid shape type");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The data of this shape.
|
||||
* It is owned by the shape and must not be modified.
|
||||
* @type {T}
|
||||
*/
|
||||
get data() {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
#data;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is this a hole?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isHole() {
|
||||
return this.data.hole;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The Clipper paths of this shape.
|
||||
* The winding numbers are 1 or 0.
|
||||
* @type {ReadonlyArray<ReadonlyArray<ClipperLib.IntPoint>>}
|
||||
*/
|
||||
get clipperPaths() {
|
||||
return this.#clipperPaths ??= ClipperLib.Clipper.PolyTreeToPaths(this.clipperPolyTree);
|
||||
}
|
||||
|
||||
#clipperPaths;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The Clipper polygon tree of this shape.
|
||||
* @type {ClipperLib.PolyTree}
|
||||
*/
|
||||
get clipperPolyTree() {
|
||||
let clipperPolyTree = this.#clipperPolyTree;
|
||||
if ( !clipperPolyTree ) {
|
||||
clipperPolyTree = this._createClipperPolyTree();
|
||||
if ( Array.isArray(clipperPolyTree) ) {
|
||||
const clipperPolyNode = new ClipperLib.PolyNode();
|
||||
clipperPolyNode.m_polygon = clipperPolyTree;
|
||||
clipperPolyTree = new ClipperLib.PolyTree();
|
||||
clipperPolyTree.AddChild(clipperPolyNode);
|
||||
clipperPolyTree.m_AllPolys.push(clipperPolyNode);
|
||||
}
|
||||
this.#clipperPolyTree = clipperPolyTree;
|
||||
}
|
||||
return clipperPolyTree;
|
||||
}
|
||||
|
||||
#clipperPolyTree;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the Clipper polygon tree of this shape.
|
||||
* This function may return a single positively-orientated and non-selfintersecting Clipper path instead of a tree,
|
||||
* which is automatically converted to a Clipper polygon tree.
|
||||
* This function is called only once. It is not called if the shape is empty.
|
||||
* @returns {ClipperLib.PolyTree|ClipperLib.IntPoint[]}
|
||||
* @protected
|
||||
* @abstract
|
||||
*/
|
||||
_createClipperPolyTree() {
|
||||
throw new Error("A subclass of the RegionShape must implement the _createClipperPolyTree method.");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw shape into the graphics.
|
||||
* @param {PIXI.Graphics} graphics The graphics to draw the shape into.
|
||||
* @protected
|
||||
* @internal
|
||||
*/
|
||||
_drawShape(graphics) {
|
||||
throw new Error("A subclass of the RegionShape must implement the _drawShape method.");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A circle of a {@link Region}.
|
||||
* @extends {RegionShape<data.CircleShapeData>}
|
||||
*
|
||||
* @param {data.CircleShapeData} data The shape data.
|
||||
*/
|
||||
class RegionCircle extends RegionShape {
|
||||
constructor(data) {
|
||||
if ( !(data instanceof CircleShapeData) ) throw new Error("Invalid shape data");
|
||||
super(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The vertex density epsilon used to create a polygon approximation of the circle.
|
||||
* @type {number}
|
||||
*/
|
||||
static #VERTEX_DENSITY_EPSILON = 1;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_createClipperPolyTree() {
|
||||
const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
|
||||
const data = this.data;
|
||||
const x = data.x * scalingFactor;
|
||||
const y = data.y * scalingFactor;
|
||||
const radius = data.radius * scalingFactor;
|
||||
const epsilon = RegionCircle.#VERTEX_DENSITY_EPSILON * scalingFactor;
|
||||
const density = PIXI.Circle.approximateVertexDensity(radius, epsilon);
|
||||
const path = new Array(density);
|
||||
for ( let i = 0; i < density; i++ ) {
|
||||
const angle = 2 * Math.PI * (i / density);
|
||||
path[i] = new ClipperLib.IntPoint(
|
||||
Math.round(x + (Math.cos(angle) * radius)),
|
||||
Math.round(y + (Math.sin(angle) * radius))
|
||||
);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_drawShape(graphics) {
|
||||
const {x, y, radius} = this.data;
|
||||
graphics.drawCircle(x, y, radius);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An ellipse of a {@link Region}.
|
||||
* @extends {RegionShape<data.EllipseShapeData>}
|
||||
*
|
||||
* @param {data.EllipseShapeData} data The shape data.
|
||||
*/
|
||||
class RegionEllipse extends RegionShape {
|
||||
constructor(data) {
|
||||
if ( !(data instanceof EllipseShapeData) ) throw new Error("Invalid shape data");
|
||||
super(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The vertex density epsilon used to create a polygon approximation of the circle.
|
||||
* @type {number}
|
||||
*/
|
||||
static #VERTEX_DENSITY_EPSILON = 1;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_createClipperPolyTree() {
|
||||
const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
|
||||
const data = this.data;
|
||||
const x = data.x * scalingFactor;
|
||||
const y = data.y * scalingFactor;
|
||||
const radiusX = data.radiusX * scalingFactor;
|
||||
const radiusY = data.radiusY * scalingFactor;
|
||||
const epsilon = RegionEllipse.#VERTEX_DENSITY_EPSILON * scalingFactor;
|
||||
const density = PIXI.Circle.approximateVertexDensity((radiusX + radiusY) / 2, epsilon);
|
||||
const rotation = Math.toRadians(data.rotation);
|
||||
const cos = Math.cos(rotation);
|
||||
const sin = Math.sin(rotation);
|
||||
const path = new Array(density);
|
||||
for ( let i = 0; i < density; i++ ) {
|
||||
const angle = 2 * Math.PI * (i / density);
|
||||
const dx = Math.cos(angle) * radiusX;
|
||||
const dy = Math.sin(angle) * radiusY;
|
||||
path[i] = new ClipperLib.IntPoint(
|
||||
Math.round(x + ((cos * dx) - (sin * dy))),
|
||||
Math.round(y + ((sin * dx) + (cos * dy)))
|
||||
);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_drawShape(graphics) {
|
||||
const {x, y, radiusX, radiusY, rotation} = this.data;
|
||||
if ( rotation === 0 ) {
|
||||
graphics.drawEllipse(x, y, radiusX, radiusY);
|
||||
} else {
|
||||
graphics.setMatrix(new PIXI.Matrix()
|
||||
.translate(-x, -x)
|
||||
.rotate(Math.toRadians(rotation))
|
||||
.translate(x, y));
|
||||
graphics.drawEllipse(x, y, radiusX, radiusY);
|
||||
graphics.setMatrix(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A polygon of a {@link Region}.
|
||||
* @extends {RegionShape<data.PolygonShapeData>}
|
||||
*
|
||||
* @param {data.PolygonShapeData} data The shape data.
|
||||
*/
|
||||
class RegionPolygon extends RegionShape {
|
||||
constructor(data) {
|
||||
if ( !(data instanceof PolygonShapeData) ) throw new Error("Invalid shape data");
|
||||
super(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_createClipperPolyTree() {
|
||||
const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
|
||||
const points = this.data.points;
|
||||
const path = new Array(points.length / 2);
|
||||
for ( let i = 0, j = 0; i < path.length; i++ ) {
|
||||
path[i] = new ClipperLib.IntPoint(
|
||||
Math.round(points[j++] * scalingFactor),
|
||||
Math.round(points[j++] * scalingFactor)
|
||||
);
|
||||
}
|
||||
if ( !ClipperLib.Clipper.Orientation(path) ) path.reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_drawShape(graphics) {
|
||||
graphics.drawPolygon(this.data.points);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A rectangle of a {@link Region}.
|
||||
* @extends {RegionShape<data.RectangleShapeData>}
|
||||
*
|
||||
* @param {data.RectangleShapeData} data The shape data.
|
||||
*/
|
||||
class RegionRectangle extends RegionShape {
|
||||
constructor(data) {
|
||||
if ( !(data instanceof RectangleShapeData) ) throw new Error("Invalid shape data");
|
||||
super(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_createClipperPolyTree() {
|
||||
let p0;
|
||||
let p1;
|
||||
let p2;
|
||||
let p3;
|
||||
const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
|
||||
const {x, y, width, height, rotation} = this.data;
|
||||
let x0 = x * scalingFactor;
|
||||
let y0 = y * scalingFactor;
|
||||
let x1 = (x + width) * scalingFactor;
|
||||
let y1 = (y + height) * scalingFactor;
|
||||
|
||||
// The basic non-rotated case
|
||||
if ( rotation === 0 ) {
|
||||
x0 = Math.round(x0);
|
||||
y0 = Math.round(y0);
|
||||
x1 = Math.round(x1);
|
||||
y1 = Math.round(y1);
|
||||
p0 = new ClipperLib.IntPoint(x0, y0);
|
||||
p1 = new ClipperLib.IntPoint(x1, y0);
|
||||
p2 = new ClipperLib.IntPoint(x1, y1);
|
||||
p3 = new ClipperLib.IntPoint(x0, y1);
|
||||
}
|
||||
|
||||
// The more complex rotated case
|
||||
else {
|
||||
const tx = (x0 + x1) / 2;
|
||||
const ty = (y0 + y1) / 2;
|
||||
x0 -= tx;
|
||||
y0 -= ty;
|
||||
x1 -= tx;
|
||||
y1 -= ty;
|
||||
const angle = Math.toRadians(rotation);
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
const x00 = Math.round((cos * x0) - (sin * y0) + tx);
|
||||
const y00 = Math.round((sin * x0) + (cos * y0) + ty);
|
||||
const x10 = Math.round((cos * x1) - (sin * y0) + tx);
|
||||
const y10 = Math.round((sin * x1) + (cos * y0) + ty);
|
||||
const x11 = Math.round((cos * x1) - (sin * y1) + tx);
|
||||
const y11 = Math.round((sin * x1) + (cos * y1) + ty);
|
||||
const x01 = Math.round((cos * x0) - (sin * y1) + tx);
|
||||
const y01 = Math.round((sin * x0) + (cos * y1) + ty);
|
||||
p0 = new ClipperLib.IntPoint(x00, y00);
|
||||
p1 = new ClipperLib.IntPoint(x10, y10);
|
||||
p2 = new ClipperLib.IntPoint(x11, y11);
|
||||
p3 = new ClipperLib.IntPoint(x01, y01);
|
||||
}
|
||||
return [p0, p1, p2, p3];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_drawShape(graphics) {
|
||||
const {x, y, width, height, rotation} = this.data;
|
||||
if ( rotation === 0 ) {
|
||||
graphics.drawRect(x, y, width, height);
|
||||
} else {
|
||||
const centerX = x + (width / 2);
|
||||
const centerY = y + (height / 2);
|
||||
graphics.setMatrix(new PIXI.Matrix()
|
||||
.translate(-centerX, -centerY)
|
||||
.rotate(Math.toRadians(rotation))
|
||||
.translate(centerX, centerY));
|
||||
graphics.drawRect(x, y, width, height);
|
||||
graphics.setMatrix(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user