722 lines
25 KiB
JavaScript
722 lines
25 KiB
JavaScript
|
|
/**
|
||
|
|
* @typedef {Map<number,PolygonVertex>} VertexMap
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {Set<Edge>} EdgeSet
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {Ray} PolygonRay
|
||
|
|
* @property {CollisionResult} result
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A PointSourcePolygon implementation that uses CCW (counter-clockwise) geometry orientation.
|
||
|
|
* Sweep around the origin, accumulating collision points based on the set of active walls.
|
||
|
|
* This algorithm was created with valuable contributions from https://github.com/caewok
|
||
|
|
*
|
||
|
|
* @extends PointSourcePolygon
|
||
|
|
*/
|
||
|
|
class ClockwiseSweepPolygon extends PointSourcePolygon {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A mapping of vertices which define potential collision points
|
||
|
|
* @type {VertexMap}
|
||
|
|
*/
|
||
|
|
vertices = new Map();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The set of edges which define potential boundaries of the polygon
|
||
|
|
* @type {EdgeSet}
|
||
|
|
*/
|
||
|
|
edges = new Set();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A collection of rays which are fired at vertices
|
||
|
|
* @type {PolygonRay[]}
|
||
|
|
*/
|
||
|
|
rays = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The squared maximum distance of a ray that is needed for this Scene.
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
#rayDistance2;
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Initialization */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritDoc */
|
||
|
|
initialize(origin, config) {
|
||
|
|
super.initialize(origin, config);
|
||
|
|
this.#rayDistance2 = Math.pow(canvas.dimensions.maxR, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritDoc */
|
||
|
|
clone() {
|
||
|
|
const poly = super.clone();
|
||
|
|
for ( const attr of ["vertices", "edges", "rays", "#rayDistance2"] ) { // Shallow clone only
|
||
|
|
poly[attr] = this[attr];
|
||
|
|
}
|
||
|
|
return poly;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Computation */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
_compute() {
|
||
|
|
|
||
|
|
// Clear prior data
|
||
|
|
this.points = [];
|
||
|
|
this.rays = [];
|
||
|
|
this.vertices.clear();
|
||
|
|
this.edges.clear();
|
||
|
|
|
||
|
|
// Step 1 - Identify candidate edges
|
||
|
|
this._identifyEdges();
|
||
|
|
|
||
|
|
// Step 2 - Construct vertex mapping
|
||
|
|
this._identifyVertices();
|
||
|
|
|
||
|
|
// Step 3 - Radial sweep over endpoints
|
||
|
|
this._executeSweep();
|
||
|
|
|
||
|
|
// Step 4 - Constrain with boundary shapes
|
||
|
|
this._constrainBoundaryShapes();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Edge Configuration */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the super-set of walls which could potentially apply to this polygon.
|
||
|
|
* Define a custom collision test used by the Quadtree to obtain candidate Walls.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_identifyEdges() {
|
||
|
|
const bounds = this.config.boundingBox = this._defineBoundingBox();
|
||
|
|
const edgeTypes = this._determineEdgeTypes();
|
||
|
|
for ( const edge of canvas.edges.values() ) {
|
||
|
|
if ( this._testEdgeInclusion(edge, edgeTypes, bounds) ) {
|
||
|
|
this.edges.add(edge.clone());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine the edge types and their manner of inclusion for this polygon instance.
|
||
|
|
* @returns {Record<EdgeTypes, 0|1|2>}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_determineEdgeTypes() {
|
||
|
|
const {type, useInnerBounds, includeDarkness} = this.config;
|
||
|
|
const edgeTypes = {};
|
||
|
|
if ( type !== "universal" ) edgeTypes.wall = 1;
|
||
|
|
if ( includeDarkness ) edgeTypes.darkness = 1;
|
||
|
|
if ( useInnerBounds && canvas.scene.padding ) edgeTypes.innerBounds = 2;
|
||
|
|
else edgeTypes.outerBounds = 2;
|
||
|
|
return edgeTypes;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test whether a wall should be included in the computed polygon for a given origin and type
|
||
|
|
* @param {Edge} edge The Edge being considered
|
||
|
|
* @param {Record<EdgeTypes, 0|1|2>} edgeTypes Which types of edges are being used? 0=no, 1=maybe, 2=always
|
||
|
|
* @param {PIXI.Rectangle} bounds The overall bounding box
|
||
|
|
* @returns {boolean} Should the edge be included?
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_testEdgeInclusion(edge, edgeTypes, bounds) {
|
||
|
|
const { type, boundaryShapes, useThreshold, wallDirectionMode, externalRadius } = this.config;
|
||
|
|
|
||
|
|
// Only include edges of the appropriate type
|
||
|
|
const m = edgeTypes[edge.type];
|
||
|
|
if ( !m ) return false;
|
||
|
|
if ( m === 2 ) return true;
|
||
|
|
|
||
|
|
// Test for inclusion in the overall bounding box
|
||
|
|
if ( !bounds.lineSegmentIntersects(edge.a, edge.b, { inside: true }) ) return false;
|
||
|
|
|
||
|
|
// Specific boundary shapes may impose additional requirements
|
||
|
|
for ( const shape of boundaryShapes ) {
|
||
|
|
if ( shape._includeEdge && !shape._includeEdge(edge.a, edge.b) ) return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ignore edges which do not block this polygon type
|
||
|
|
if ( edge[type] === CONST.WALL_SENSE_TYPES.NONE ) return false;
|
||
|
|
|
||
|
|
// Ignore edges which are collinear with the origin
|
||
|
|
const side = edge.orientPoint(this.origin);
|
||
|
|
if ( !side ) return false;
|
||
|
|
|
||
|
|
// Ignore one-directional walls which are facing away from the origin
|
||
|
|
const wdm = PointSourcePolygon.WALL_DIRECTION_MODES;
|
||
|
|
if ( edge.direction && (wallDirectionMode !== wdm.BOTH) ) {
|
||
|
|
if ( (wallDirectionMode === wdm.NORMAL) === (side === edge.direction) ) return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ignore threshold walls which do not satisfy their required proximity
|
||
|
|
if ( useThreshold ) return !edge.applyThreshold(type, this.origin, externalRadius);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Compute the aggregate bounding box which is the intersection of all boundary shapes.
|
||
|
|
* Round and pad the resulting rectangle by 1 pixel to ensure it always contains the origin.
|
||
|
|
* @returns {PIXI.Rectangle}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_defineBoundingBox() {
|
||
|
|
let b = this.config.useInnerBounds ? canvas.dimensions.sceneRect : canvas.dimensions.rect;
|
||
|
|
for ( const shape of this.config.boundaryShapes ) {
|
||
|
|
b = b.intersection(shape.getBounds());
|
||
|
|
}
|
||
|
|
return new PIXI.Rectangle(b.x, b.y, b.width, b.height).normalize().ceil().pad(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Vertex Identification */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Consolidate all vertices from identified edges and register them as part of the vertex mapping.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_identifyVertices() {
|
||
|
|
const edgeMap = new Map();
|
||
|
|
for ( let edge of this.edges ) {
|
||
|
|
edgeMap.set(edge.id, edge);
|
||
|
|
|
||
|
|
// Create or reference vertex A
|
||
|
|
const ak = foundry.canvas.edges.PolygonVertex.getKey(edge.a.x, edge.a.y);
|
||
|
|
if ( this.vertices.has(ak) ) edge.vertexA = this.vertices.get(ak);
|
||
|
|
else {
|
||
|
|
edge.vertexA = new foundry.canvas.edges.PolygonVertex(edge.a.x, edge.a.y);
|
||
|
|
this.vertices.set(ak, edge.vertexA);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create or reference vertex B
|
||
|
|
const bk = foundry.canvas.edges.PolygonVertex.getKey(edge.b.x, edge.b.y);
|
||
|
|
if ( this.vertices.has(bk) ) edge.vertexB = this.vertices.get(bk);
|
||
|
|
else {
|
||
|
|
edge.vertexB = new foundry.canvas.edges.PolygonVertex(edge.b.x, edge.b.y);
|
||
|
|
this.vertices.set(bk, edge.vertexB);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Learn edge orientation with respect to the origin and ensure B is clockwise of A
|
||
|
|
const o = foundry.utils.orient2dFast(this.origin, edge.vertexA, edge.vertexB);
|
||
|
|
if ( o > 0 ) Object.assign(edge, {vertexA: edge.vertexB, vertexB: edge.vertexA}); // Reverse vertices
|
||
|
|
if ( o !== 0 ) { // Attach non-collinear edges
|
||
|
|
edge.vertexA.attachEdge(edge, -1, this.config.type);
|
||
|
|
edge.vertexB.attachEdge(edge, 1, this.config.type);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add edge intersections
|
||
|
|
this._identifyIntersections(edgeMap);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add additional vertices for intersections between edges.
|
||
|
|
* @param {Map<string, Edge>} edgeMap
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_identifyIntersections(edgeMap) {
|
||
|
|
const processed = new Set();
|
||
|
|
for ( let edge of this.edges ) {
|
||
|
|
for ( const x of edge.intersections ) {
|
||
|
|
|
||
|
|
// Is the intersected edge also included in the polygon?
|
||
|
|
const other = edgeMap.get(x.edge.id);
|
||
|
|
if ( !other || processed.has(other) ) continue;
|
||
|
|
const i = x.intersection;
|
||
|
|
|
||
|
|
// Register the intersection point as a vertex
|
||
|
|
const vk = foundry.canvas.edges.PolygonVertex.getKey(Math.round(i.x), Math.round(i.y));
|
||
|
|
let v = this.vertices.get(vk);
|
||
|
|
if ( !v ) {
|
||
|
|
v = new foundry.canvas.edges.PolygonVertex(i.x, i.y);
|
||
|
|
v._intersectionCoordinates = i;
|
||
|
|
this.vertices.set(vk, v);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Attach edges to the intersection vertex
|
||
|
|
// Due to rounding, it is possible for an edge to be completely cw or ccw or only one of the two
|
||
|
|
// We know from _identifyVertices that vertex B is clockwise of vertex A for every edge.
|
||
|
|
// It is important that we use the true intersection coordinates (i) for this orientation test.
|
||
|
|
if ( !v.edges.has(edge) ) {
|
||
|
|
const dir = foundry.utils.orient2dFast(this.origin, edge.vertexB, i) < 0 ? 1 // Edge is fully CCW of v
|
||
|
|
: (foundry.utils.orient2dFast(this.origin, edge.vertexA, i) > 0 ? -1 : 0); // Edge is fully CW of v
|
||
|
|
v.attachEdge(edge, dir, this.config.type);
|
||
|
|
}
|
||
|
|
if ( !v.edges.has(other) ) {
|
||
|
|
const dir = foundry.utils.orient2dFast(this.origin, other.vertexB, i) < 0 ? 1 // Other is fully CCW of v
|
||
|
|
: (foundry.utils.orient2dFast(this.origin, other.vertexA, i) > 0 ? -1 : 0); // Other is fully CW of v
|
||
|
|
v.attachEdge(other, dir, this.config.type);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
processed.add(edge);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Radial Sweep */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Execute the sweep over wall vertices
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_executeSweep() {
|
||
|
|
|
||
|
|
// Initialize the set of active walls
|
||
|
|
const activeEdges = this._initializeActiveEdges();
|
||
|
|
|
||
|
|
// Sort vertices from clockwise to counter-clockwise and begin the sweep
|
||
|
|
const vertices = this._sortVertices();
|
||
|
|
|
||
|
|
// Iterate through the vertices, adding polygon points
|
||
|
|
let i = 1;
|
||
|
|
for ( const vertex of vertices ) {
|
||
|
|
if ( vertex._visited ) continue;
|
||
|
|
vertex._index = i++;
|
||
|
|
this.#updateActiveEdges(vertex, activeEdges);
|
||
|
|
|
||
|
|
// Include collinear vertices in this iteration of the sweep, treating their edges as active also
|
||
|
|
const hasCollinear = vertex.collinearVertices.size > 0;
|
||
|
|
if ( hasCollinear ) {
|
||
|
|
this.#includeCollinearVertices(vertex, vertex.collinearVertices);
|
||
|
|
for ( const cv of vertex.collinearVertices ) {
|
||
|
|
cv._index = i++;
|
||
|
|
this.#updateActiveEdges(cv, activeEdges);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine the result of the sweep for the given vertex
|
||
|
|
this._determineSweepResult(vertex, activeEdges, hasCollinear);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Include collinear vertices until they have all been added.
|
||
|
|
* Do not include the original vertex in the set.
|
||
|
|
* @param {PolygonVertex} vertex The current vertex
|
||
|
|
* @param {PolygonVertexSet} collinearVertices
|
||
|
|
*/
|
||
|
|
#includeCollinearVertices(vertex, collinearVertices) {
|
||
|
|
for ( const cv of collinearVertices) {
|
||
|
|
for ( const ccv of cv.collinearVertices ) {
|
||
|
|
collinearVertices.add(ccv);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
collinearVertices.delete(vertex);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update active edges at a given vertex
|
||
|
|
* Remove counter-clockwise edges which have now concluded.
|
||
|
|
* Add clockwise edges which are ongoing or beginning.
|
||
|
|
* @param {PolygonVertex} vertex The current vertex
|
||
|
|
* @param {EdgeSet} activeEdges A set of currently active edges
|
||
|
|
*/
|
||
|
|
#updateActiveEdges(vertex, activeEdges) {
|
||
|
|
for ( const ccw of vertex.ccwEdges ) {
|
||
|
|
if ( !vertex.cwEdges.has(ccw) ) activeEdges.delete(ccw);
|
||
|
|
}
|
||
|
|
for ( const cw of vertex.cwEdges ) {
|
||
|
|
if ( cw.vertexA._visited && cw.vertexB._visited ) continue; // Safeguard in case we have already visited the edge
|
||
|
|
activeEdges.add(cw);
|
||
|
|
}
|
||
|
|
vertex._visited = true; // Record that we have already visited this vertex
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine the initial set of active edges as those which intersect with the initial ray
|
||
|
|
* @returns {EdgeSet} A set of initially active edges
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_initializeActiveEdges() {
|
||
|
|
const initial = {x: Math.round(this.origin.x - this.#rayDistance2), y: this.origin.y};
|
||
|
|
const edges = new Set();
|
||
|
|
for ( let edge of this.edges ) {
|
||
|
|
const x = foundry.utils.lineSegmentIntersects(this.origin, initial, edge.vertexA, edge.vertexB);
|
||
|
|
if ( x ) edges.add(edge);
|
||
|
|
}
|
||
|
|
return edges;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sort vertices clockwise from the initial ray (due west).
|
||
|
|
* @returns {PolygonVertex[]} The array of sorted vertices
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_sortVertices() {
|
||
|
|
if ( !this.vertices.size ) return [];
|
||
|
|
let vertices = Array.from(this.vertices.values());
|
||
|
|
const o = this.origin;
|
||
|
|
|
||
|
|
// Sort vertices
|
||
|
|
vertices.sort((a, b) => {
|
||
|
|
|
||
|
|
// Use true intersection coordinates if they are defined
|
||
|
|
let pA = a._intersectionCoordinates || a;
|
||
|
|
let pB = b._intersectionCoordinates || b;
|
||
|
|
|
||
|
|
// Sort by hemisphere
|
||
|
|
const ya = pA.y > o.y ? 1 : -1;
|
||
|
|
const yb = pB.y > o.y ? 1 : -1;
|
||
|
|
if ( ya !== yb ) return ya; // Sort N, S
|
||
|
|
|
||
|
|
// Sort by quadrant
|
||
|
|
const qa = pA.x < o.x ? -1 : 1;
|
||
|
|
const qb = pB.x < o.x ? -1 : 1;
|
||
|
|
if ( qa !== qb ) { // Sort NW, NE, SE, SW
|
||
|
|
if ( ya === -1 ) return qa;
|
||
|
|
else return -qa;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sort clockwise within quadrant
|
||
|
|
const orientation = foundry.utils.orient2dFast(o, pA, pB);
|
||
|
|
if ( orientation !== 0 ) return orientation;
|
||
|
|
|
||
|
|
// At this point, we know points are collinear; track for later processing.
|
||
|
|
a.collinearVertices.add(b);
|
||
|
|
b.collinearVertices.add(a);
|
||
|
|
|
||
|
|
// Otherwise, sort closer points first
|
||
|
|
a._d2 ||= Math.pow(pA.x - o.x, 2) + Math.pow(pA.y - o.y, 2);
|
||
|
|
b._d2 ||= Math.pow(pB.x - o.x, 2) + Math.pow(pB.y - o.y, 2);
|
||
|
|
return a._d2 - b._d2;
|
||
|
|
});
|
||
|
|
return vertices;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test whether a target vertex is behind some closer active edge.
|
||
|
|
* If the vertex is to the left of the edge, is must be behind the edge relative to origin.
|
||
|
|
* If the vertex is collinear with the edge, it should be considered "behind" and ignored.
|
||
|
|
* We know edge.vertexA is ccw to edge.vertexB because of the logic in _identifyVertices.
|
||
|
|
* @param {PolygonVertex} vertex The target vertex
|
||
|
|
* @param {EdgeSet} activeEdges The set of active edges
|
||
|
|
* @returns {{isBehind: boolean, wasLimited: boolean}} Is the target vertex behind some closer edge?
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_isVertexBehindActiveEdges(vertex, activeEdges) {
|
||
|
|
let wasLimited = false;
|
||
|
|
for ( let edge of activeEdges ) {
|
||
|
|
if ( vertex.edges.has(edge) ) continue;
|
||
|
|
if ( foundry.utils.orient2dFast(edge.vertexA, edge.vertexB, vertex) > 0 ) {
|
||
|
|
if ( ( edge.isLimited(this.config.type) ) && !wasLimited ) wasLimited = true;
|
||
|
|
else return {isBehind: true, wasLimited};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return {isBehind: false, wasLimited};
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine the result for the sweep at a given vertex
|
||
|
|
* @param {PolygonVertex} vertex The target vertex
|
||
|
|
* @param {EdgeSet} activeEdges The set of active edges
|
||
|
|
* @param {boolean} hasCollinear Are there collinear vertices behind the target vertex?
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_determineSweepResult(vertex, activeEdges, hasCollinear=false) {
|
||
|
|
|
||
|
|
// Determine whether the target vertex is behind some other active edge
|
||
|
|
const {isBehind, wasLimited} = this._isVertexBehindActiveEdges(vertex, activeEdges);
|
||
|
|
|
||
|
|
// Case 1 - Some vertices can be ignored because they are behind other active edges
|
||
|
|
if ( isBehind ) return;
|
||
|
|
|
||
|
|
// Construct the CollisionResult object
|
||
|
|
const result = new foundry.canvas.edges.CollisionResult({
|
||
|
|
target: vertex,
|
||
|
|
cwEdges: vertex.cwEdges,
|
||
|
|
ccwEdges: vertex.ccwEdges,
|
||
|
|
isLimited: vertex.isLimited,
|
||
|
|
isBehind,
|
||
|
|
wasLimited
|
||
|
|
});
|
||
|
|
|
||
|
|
// Case 2 - No counter-clockwise edge, so begin a new edge
|
||
|
|
// Note: activeEdges always contain the vertex edge, so never empty
|
||
|
|
const nccw = vertex.ccwEdges.size;
|
||
|
|
if ( !nccw ) {
|
||
|
|
this._switchEdge(result, activeEdges);
|
||
|
|
result.collisions.forEach(pt => this.addPoint(pt));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Case 3 - Limited edges in both directions
|
||
|
|
// We can only guarantee this case if we don't have collinear endpoints
|
||
|
|
const ccwLimited = !result.wasLimited && vertex.isLimitingCCW;
|
||
|
|
const cwLimited = !result.wasLimited && vertex.isLimitingCW;
|
||
|
|
if ( !hasCollinear && cwLimited && ccwLimited ) return;
|
||
|
|
|
||
|
|
// Case 4 - Non-limited edges in both directions
|
||
|
|
if ( !ccwLimited && !cwLimited && nccw && vertex.cwEdges.size ) {
|
||
|
|
result.collisions.push(result.target);
|
||
|
|
this.addPoint(result.target);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Case 5 - Otherwise switching edges or edge types
|
||
|
|
this._switchEdge(result, activeEdges);
|
||
|
|
result.collisions.forEach(pt => this.addPoint(pt));
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Switch to a new active edge.
|
||
|
|
* Moving from the origin, a collision that first blocks a side must be stored as a polygon point.
|
||
|
|
* Subsequent collisions blocking that side are ignored. Once both sides are blocked, we are done.
|
||
|
|
*
|
||
|
|
* Collisions that limit a side will block if that side was previously limited.
|
||
|
|
*
|
||
|
|
* If neither side is blocked and the ray internally collides with a non-limited edge, n skip without adding polygon
|
||
|
|
* endpoints. Sight is unaffected before this edge, and the internal collision can be ignored.
|
||
|
|
* @private
|
||
|
|
*
|
||
|
|
* @param {CollisionResult} result The pending collision result
|
||
|
|
* @param {EdgeSet} activeEdges The set of currently active edges
|
||
|
|
*/
|
||
|
|
_switchEdge(result, activeEdges) {
|
||
|
|
const origin = this.origin;
|
||
|
|
|
||
|
|
// Construct the ray from the origin
|
||
|
|
const ray = Ray.towardsPointSquared(origin, result.target, this.#rayDistance2);
|
||
|
|
ray.result = result;
|
||
|
|
this.rays.push(ray); // For visualization and debugging
|
||
|
|
|
||
|
|
// Create a sorted array of collisions containing the target vertex, other collinear vertices, and collision points
|
||
|
|
const vertices = [result.target, ...result.target.collinearVertices];
|
||
|
|
const keys = new Set();
|
||
|
|
for ( const v of vertices ) {
|
||
|
|
keys.add(v.key);
|
||
|
|
v._d2 ??= Math.pow(v.x - origin.x, 2) + Math.pow(v.y - origin.y, 2);
|
||
|
|
}
|
||
|
|
this.#addInternalEdgeCollisions(vertices, keys, ray, activeEdges);
|
||
|
|
vertices.sort((a, b) => a._d2 - b._d2);
|
||
|
|
|
||
|
|
// As we iterate over intersection points we will define the insertion method
|
||
|
|
let insert = undefined;
|
||
|
|
const c = result.collisions;
|
||
|
|
for ( const x of vertices ) {
|
||
|
|
|
||
|
|
if ( x.isInternal ) { // Handle internal collisions
|
||
|
|
// If neither side yet blocked and this is a non-limited edge, return
|
||
|
|
if ( !result.blockedCW && !result.blockedCCW && !x.isLimited ) return;
|
||
|
|
|
||
|
|
// Assume any edge is either limited or normal, so if not limited, must block. If already limited, must block
|
||
|
|
result.blockedCW ||= !x.isLimited || result.limitedCW;
|
||
|
|
result.blockedCCW ||= !x.isLimited || result.limitedCCW;
|
||
|
|
result.limitedCW = true;
|
||
|
|
result.limitedCCW = true;
|
||
|
|
|
||
|
|
} else { // Handle true endpoints
|
||
|
|
result.blockedCW ||= (result.limitedCW && x.isLimitingCW) || x.isBlockingCW;
|
||
|
|
result.blockedCCW ||= (result.limitedCCW && x.isLimitingCCW) || x.isBlockingCCW;
|
||
|
|
result.limitedCW ||= x.isLimitingCW;
|
||
|
|
result.limitedCCW ||= x.isLimitingCCW;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Define the insertion method and record a collision point
|
||
|
|
if ( result.blockedCW ) {
|
||
|
|
insert ||= c.unshift;
|
||
|
|
if ( !result.blockedCWPrev ) insert.call(c, x);
|
||
|
|
}
|
||
|
|
if ( result.blockedCCW ) {
|
||
|
|
insert ||= c.push;
|
||
|
|
if ( !result.blockedCCWPrev ) insert.call(c, x);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update blocking flags
|
||
|
|
if ( result.blockedCW && result.blockedCCW ) return;
|
||
|
|
result.blockedCWPrev ||= result.blockedCW;
|
||
|
|
result.blockedCCWPrev ||= result.blockedCCW;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Identify the collision points between an emitted Ray and a set of active edges.
|
||
|
|
* @param {PolygonVertex[]} vertices Active vertices
|
||
|
|
* @param {Set<number>} keys Active vertex keys
|
||
|
|
* @param {PolygonRay} ray The candidate ray to test
|
||
|
|
* @param {EdgeSet} activeEdges The set of edges to check for collisions against the ray
|
||
|
|
*/
|
||
|
|
#addInternalEdgeCollisions(vertices, keys, ray, activeEdges) {
|
||
|
|
for ( const edge of activeEdges ) {
|
||
|
|
if ( keys.has(edge.vertexA.key) || keys.has(edge.vertexB.key) ) continue;
|
||
|
|
const x = foundry.utils.lineLineIntersection(ray.A, ray.B, edge.vertexA, edge.vertexB);
|
||
|
|
if ( !x ) continue;
|
||
|
|
const c = foundry.canvas.edges.PolygonVertex.fromPoint(x);
|
||
|
|
c.attachEdge(edge, 0, this.config.type);
|
||
|
|
c.isInternal = true;
|
||
|
|
c._d2 = Math.pow(x.x - ray.A.x, 2) + Math.pow(x.y - ray.A.y, 2);
|
||
|
|
vertices.push(c);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Collision Testing */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_testCollision(ray, mode) {
|
||
|
|
const {debug, type} = this.config;
|
||
|
|
|
||
|
|
// Identify candidate edges
|
||
|
|
this._identifyEdges();
|
||
|
|
|
||
|
|
// Identify collision points
|
||
|
|
let collisions = new Map();
|
||
|
|
for ( const edge of this.edges ) {
|
||
|
|
const x = foundry.utils.lineSegmentIntersection(this.origin, ray.B, edge.a, edge.b);
|
||
|
|
if ( !x || (x.t0 <= 0) ) continue;
|
||
|
|
if ( (mode === "any") && (!edge.isLimited(type) || collisions.size) ) return true;
|
||
|
|
let c = foundry.canvas.edges.PolygonVertex.fromPoint(x, {distance: x.t0});
|
||
|
|
if ( collisions.has(c.key) ) c = collisions.get(c.key);
|
||
|
|
else collisions.set(c.key, c);
|
||
|
|
c.attachEdge(edge, 0, type);
|
||
|
|
}
|
||
|
|
if ( mode === "any" ) return false;
|
||
|
|
|
||
|
|
// Sort collisions
|
||
|
|
collisions = Array.from(collisions.values()).sort((a, b) => a._distance - b._distance);
|
||
|
|
if ( collisions[0]?.isLimited ) collisions.shift();
|
||
|
|
|
||
|
|
// Visualize result
|
||
|
|
if ( debug ) this._visualizeCollision(ray, collisions);
|
||
|
|
|
||
|
|
// Return collision result
|
||
|
|
if ( mode === "all" ) return collisions;
|
||
|
|
else return collisions[0] || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Visualization */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
visualize() {
|
||
|
|
let dg = canvas.controls.debug;
|
||
|
|
dg.clear();
|
||
|
|
|
||
|
|
// Text debugging
|
||
|
|
if ( !canvas.controls.debug.debugText ) {
|
||
|
|
canvas.controls.debug.debugText = canvas.controls.addChild(new PIXI.Container());
|
||
|
|
}
|
||
|
|
const text = canvas.controls.debug.debugText;
|
||
|
|
text.removeChildren().forEach(c => c.destroy({children: true}));
|
||
|
|
|
||
|
|
// Define limitation colors
|
||
|
|
const limitColors = {
|
||
|
|
[CONST.WALL_SENSE_TYPES.NONE]: 0x77E7E8,
|
||
|
|
[CONST.WALL_SENSE_TYPES.NORMAL]: 0xFFFFBB,
|
||
|
|
[CONST.WALL_SENSE_TYPES.LIMITED]: 0x81B90C,
|
||
|
|
[CONST.WALL_SENSE_TYPES.PROXIMITY]: 0xFFFFBB,
|
||
|
|
[CONST.WALL_SENSE_TYPES.DISTANCE]: 0xFFFFBB
|
||
|
|
};
|
||
|
|
|
||
|
|
// Draw boundary shapes
|
||
|
|
for ( const constraint of this.config.boundaryShapes ) {
|
||
|
|
dg.lineStyle(2, 0xFF4444, 1.0).beginFill(0xFF4444, 0.10).drawShape(constraint).endFill();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Draw the final polygon shape
|
||
|
|
dg.beginFill(0x00AAFF, 0.25).drawShape(this).endFill();
|
||
|
|
|
||
|
|
// Draw candidate edges
|
||
|
|
for ( let edge of this.edges ) {
|
||
|
|
const c = limitColors[edge[this.config.type]];
|
||
|
|
dg.lineStyle(4, c).moveTo(edge.a.x, edge.a.y).lineTo(edge.b.x, edge.b.y);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Draw vertices
|
||
|
|
for ( let vertex of this.vertices.values() ) {
|
||
|
|
const r = vertex.restriction;
|
||
|
|
if ( r ) dg.lineStyle(1, 0x000000).beginFill(limitColors[r]).drawCircle(vertex.x, vertex.y, 8).endFill();
|
||
|
|
if ( vertex._index ) {
|
||
|
|
let t = text.addChild(new PIXI.Text(String(vertex._index), CONFIG.canvasTextStyle));
|
||
|
|
t.position.set(vertex.x, vertex.y);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Draw emitted rays
|
||
|
|
for ( let ray of this.rays ) {
|
||
|
|
const r = ray.result;
|
||
|
|
if ( r ) {
|
||
|
|
dg.lineStyle(2, 0x00FF00, r.collisions.length ? 1.0 : 0.33).moveTo(ray.A.x, ray.A.y).lineTo(ray.B.x, ray.B.y);
|
||
|
|
for ( let c of r.collisions ) {
|
||
|
|
dg.lineStyle(1, 0x000000).beginFill(0xFF0000).drawCircle(c.x, c.y, 6).endFill();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return dg;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Visualize the polygon, displaying its computed area, rays, and collision points
|
||
|
|
* @param {Ray} ray
|
||
|
|
* @param {PolygonVertex[]} collisions
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_visualizeCollision(ray, collisions) {
|
||
|
|
let dg = canvas.controls.debug;
|
||
|
|
dg.clear();
|
||
|
|
const limitColors = {
|
||
|
|
[CONST.WALL_SENSE_TYPES.NONE]: 0x77E7E8,
|
||
|
|
[CONST.WALL_SENSE_TYPES.NORMAL]: 0xFFFFBB,
|
||
|
|
[CONST.WALL_SENSE_TYPES.LIMITED]: 0x81B90C,
|
||
|
|
[CONST.WALL_SENSE_TYPES.PROXIMITY]: 0xFFFFBB,
|
||
|
|
[CONST.WALL_SENSE_TYPES.DISTANCE]: 0xFFFFBB
|
||
|
|
};
|
||
|
|
|
||
|
|
// Draw edges
|
||
|
|
for ( let edge of this.edges.values() ) {
|
||
|
|
const c = limitColors[edge[this.config.type]];
|
||
|
|
dg.lineStyle(4, c).moveTo(edge.a.x, edge.b.y).lineTo(edge.b.x, edge.b.y);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Draw the attempted ray
|
||
|
|
dg.lineStyle(4, 0x0066CC).moveTo(ray.A.x, ray.A.y).lineTo(ray.B.x, ray.B.y);
|
||
|
|
|
||
|
|
// Draw collision points
|
||
|
|
for ( let x of collisions ) {
|
||
|
|
dg.lineStyle(1, 0x000000).beginFill(0xFF0000).drawCircle(x.x, x.y, 6).endFill();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|