From 97e61af1f2469d610b650bf98b28f8977134e295 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 19 Feb 2024 08:38:52 -0800 Subject: [PATCH 01/16] Improve segment collision detection Streamline the functions and avoid returning two collisions where overlap due to collinearity. --- scripts/pathfinding/WallTracer.js | 78 ++++++++++++++++++------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 6a74e8b..e977c1e 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -291,12 +291,7 @@ export class WallTracerEdge extends GraphEdge { findEdgeCollisions(A, B) { const C = this.A.point; const D = this.B.point; - const collisions = endpointIntersection(A, B, C, D) - ?? segmentIntersection(A, B, C, D) - ?? segmentOverlap(A, B, C, D); - if ( !collisions ) return []; - if ( !(collisions instanceof Array) ) return [collisions]; - return collisions; + return segmentCollisions(A, B, C, D) ?? []; } /** @@ -786,38 +781,43 @@ wt.tokenEdges.forEach(s => s.forEach(e => e.draw({color: Draw.COLORS.orange}))) * @property {number} t1 Intersection location on the c --> d segment */ +/** + * Locate collisions between two segments. Uses almostEqual to get near collisions. + * 1. Shared endpoints. + * 2. Endpoint of one segment within the other segment. + * 3. Two segments intersect. + * 4. Collinear segments overlap: return start and end of the intersections. + * @param {PIXI.Point} a Endpoint on a|b segment + * @param {PIXI.Point} b Endpoint on a|b segment + * @param {PIXI.Point} c Endpoint on c|d segment + * @param {PIXI.Point} d Endpoint on c|d segment + * @returns {SegmentIntersection[]|null} + */ +function segmentCollisions(a, b, c, d) { + // Check for true overlap. If shared endpoints, will be handled later. + const overlaps = segmentOverlap(a, b, c, d); // Returns null or SegmentIntersection[2] + if ( overlaps && !overlaps[0].pt.almostEqual(overlaps[1].pt) ) return overlaps; + + // If endpoints are shared, return the shared point. + // Otherwise, return the segment intersection or empty array. + return endpointIntersection(a, b, c, d) ?? segmentIntersection(a, b, c, d) ?? []; +} + /** * Determine if two segments intersect at an endpoint and return t0, t1 based on that intersection. + * Does not consider segment collinearity, and only picks the first shared endpoint. + * (If segments are collinear, possible they are the same and share both endpoints.) * @param {PIXI.Point} a Endpoint on a|b segment * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection|null} + * @returns {SegmentIntersection[1]|null} */ function endpointIntersection(a, b, c, d) { - // Avoid overlaps - // Distinguish a---b|c---d from a---c---b|d. Latter is an overlap. - // Okay: - // a---b|c---d - // b---a|c---d - // b---a|d---c - // a---b|d---c - // Overlap: - // a---c---b|d - // a---d---b|c - // b---c---a|d - // b---d---a|c - const orient2d = foundry.utils.orient2dFast; - if ( orient2d(a, b, c).almostEqual(0) && orient2d(a, b, d).almostEqual(0) ) { - const dSquared = PIXI.Point.distanceSquaredBetween; - const dAB = dSquared(a, b); - if ( dAB > dSquared(a, c) || dAB > dSquared(a, d) ) return null; - } - - if ( a.key === c.key || c.almostEqual(a) ) return { t0: 0, t1: 0, pt: a }; - if ( a.key === d.key || d.almostEqual(a) ) return { t0: 0, t1: 1, pt: a }; - if ( b.key === c.key || c.almostEqual(b) ) return { t0: 1, t1: 0, pt: b }; - if ( b.key === d.key || d.almostEqual(b) ) return { t0: 1, t1: 1, pt: b }; + if ( a.key === c.key || c.almostEqual(a) ) return [{ t0: 0, t1: 0, pt: a }]; + if ( a.key === d.key || d.almostEqual(a) ) return [{ t0: 0, t1: 1, pt: a }]; + if ( b.key === c.key || c.almostEqual(b) ) return [{ t0: 1, t1: 0, pt: b }]; + if ( b.key === d.key || d.almostEqual(b) ) return [{ t0: 1, t1: 1, pt: b }]; return null; } @@ -830,18 +830,18 @@ function endpointIntersection(a, b, c, d) { * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection|null} + * @returns {SegmentIntersection[1]|null} */ function segmentIntersection(a, b, c, d) { if ( !foundry.utils.lineSegmentIntersects(a, b, c, d) ) return null; const ix = CONFIG.GeometryLib.utils.lineLineIntersection(a, b, c, d, { t1: true }); ix.pt = PIXI.Point.fromObject(ix); - return ix; + return [ix]; } /** * Determine if two segments overlap and return the two points at which the segments - * begin their overlap. + * begin their overlap. If overlap is an endpoint, may return that endpoint twice. * @param {PIXI.Point} a Endpoint on a|b segment * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment @@ -850,6 +850,18 @@ function segmentIntersection(a, b, c, d) { * The 2 intersections will be sorted so that [0] --> [1] is the overlap. */ function segmentOverlap(a, b, c, d) { + // Distinguish a---b|c---d from a---c---b|d. Latter is an overlap. + // Okay: + // a---b|c---d + // b---a|c---d + // b---a|d---c + // a---b|d---c + // Overlap: + // a---c---b|d + // a---d---b|c + // b---c---a|d + // b---d---a|c + // First, ensure the segments are overlapping. const orient2d = foundry.utils.orient2dFast; if ( !orient2d(a, b, c).almostEqual(0) || !orient2d(a, b, d).almostEqual(0) ) return null; From 6d367ad941653846703310585775b3f62cd8392a Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 19 Feb 2024 08:45:09 -0800 Subject: [PATCH 02/16] Add overlap property when testing for collisions --- scripts/pathfinding/WallTracer.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index e977c1e..85ca501 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -658,13 +658,6 @@ export class WallTracer extends Graph { const collisions = edge.findEdgeCollisions(edgeA, edgeB); if ( !collisions.length ) continue; collisions.forEach(c => c.edge = edge); - - // If two collisions, there is overlap. - // Identify the overlapping objects. - if ( collisions.length === 2 ) { - collisions[0].overlap = true; - collisions[1].overlap = true; - } edgeCollisions.push(...collisions); } return groupBy(edgeCollisions, this.constructor._keyGetter); @@ -887,17 +880,18 @@ function segmentOverlap(a, b, c, d) { // Overlap: c|d --- aIx|bIx --- aIx|bIx --- c|d + const overlap = true; if ( aIx && bIx ) return [ - { t0: 0, t1: aIx.t0, pt: PIXI.Point.fromObject(aIx) }, - { t0: 1, t1: bIx.t0, pt: PIXI.Point.fromObject(bIx) } + { t0: 0, t1: aIx.t0, pt: PIXI.Point.fromObject(aIx), overlap }, + { t0: 1, t1: bIx.t0, pt: PIXI.Point.fromObject(bIx), overlap } ]; // Overlap: a|b --- cIx|dIx --- cIx|dIx --- a|b const cIx = ix2.t0.between(0, 1) ? ix2 : null; const dIx = ix3.t0.between(0, 1) ? ix3 : null; if ( cIx && dIx ) return [ - { t0: cIx.t0, t1: 0, pt: PIXI.Point.fromObject(cIx) }, - { t0: dIx.t0, t1: 1, pt: PIXI.Point.fromObject(dIx) } + { t0: cIx.t0, t1: 0, pt: PIXI.Point.fromObject(cIx), overlap }, + { t0: dIx.t0, t1: 1, pt: PIXI.Point.fromObject(dIx), overlap } ]; // Overlap: a|b --- cIx|dIx --- aIx|bIx --- c|d @@ -905,8 +899,8 @@ function segmentOverlap(a, b, c, d) { const cdIx = cIx ?? dIx; if ( abIx && cdIx ) { return [ - { t0: cdIx.t0, t1: cIx ? 0 : 1, pt: PIXI.Point.fromObject(cdIx) }, - { t0: aIx ? 0 : 1, t1: abIx.t0, pt: PIXI.Point.fromObject(abIx) } + { t0: cdIx.t0, t1: cIx ? 0 : 1, pt: PIXI.Point.fromObject(cdIx), overlap }, + { t0: aIx ? 0 : 1, t1: abIx.t0, pt: PIXI.Point.fromObject(abIx), overlap } ]; } From b5b1294bb7ae6d35647548d59a1d286174cc70b5 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 19 Feb 2024 09:36:57 -0800 Subject: [PATCH 03/16] Strictly enforce no duplicate edge additions Avoid duplicating edges in the quadtree. --- scripts/pathfinding/WallTracer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 85ca501..5cc9971 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -454,6 +454,8 @@ export class WallTracer extends Graph { * @inherited */ addEdge(edge) { + if ( this.edges.has(edge.key) ) return this.edges.get(edge.key); + edge = super.addEdge(edge); this.edgesQuadtree.insert({ r: edge.bounds, t: edge }); @@ -593,8 +595,8 @@ export class WallTracer extends Graph { if ( this.edges.has(wallId) ) return; // Construct a new wall edge set. - this.wallIds.add(wallId); this.addObjectEdge(PIXI.Point.fromObject(wall.A), PIXI.Point.fromObject(wall.B), wall); + this.wallIds.add(wallId); } /** From 069399cd3b52b8b56a0e5b0d69a6d2237e7dae49 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 19 Feb 2024 12:33:53 -0800 Subject: [PATCH 04/16] Refactor addObject edge and fix incorrect overlap object Needed to sort the splits by t0 so that the object can be assigned to the correct split --- scripts/pathfinding/WallTracer.js | 72 +++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 5cc9971..44cb6c5 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -514,8 +514,19 @@ export class WallTracer extends Graph { if ( !collisions.size ) { const edge = WallTracerEdge.fromObjects(edgeA, edgeB, [object]); this.addEdge(edge); + return; } + this.#processCollisions(collisions, edgeA, edgeB, object); + } + /** + * Process collisions and split edges at collision points. + * @param {SegmentIntersection[]} collisions + * @param {PIXI.Point} edgeA First edge endpoint + * @param {PIXI.Point} edgeB Other edge endpoint + * @param {Wall|Token} object + */ + #processCollisions(collisions, edgeA, edgeB, object) { // Sort the keys so we can progress from A --> B along the edge. const tArr = [...collisions.keys()]; tArr.sort((a, b) => a - b); @@ -525,7 +536,7 @@ export class WallTracer extends Graph { // - update the collision links for the colliding edge and this new edge if ( !collisions.has(1) ) tArr.push(1); let priorT = 0; - const overlaps = new Set(); + const overlapFn = this.#processOverlapCollisionFn(object); for ( const t of tArr ) { // Check each collision point. // For endpoint collisions, nothing will be added. @@ -538,21 +549,10 @@ export class WallTracer extends Graph { for ( const cObj of cObjs ) { const splitEdges = cObj.edge.splitAtT(cObj.t1); // If the split is at the endpoint, will be null. if ( cObj.overlap ) { - if ( overlaps.has(cObj.edge) ) { // Ending an overlap. - overlaps.delete(cObj.edge); - if ( splitEdges ) splitEdges[0].objects.add(object); // Share the edge with this object. - else { - cObj.edge.objects.add(object); - - // Make sure the object's edges include this cObj.edge. - this._addEdgeToObjectSet(object.id, cObj.edge); - } - addEdge = false; // Only want one edge here: the existing. - } else { // Starting a new overlap. - overlaps.add(cObj.edge); - if ( splitEdges ) splitEdges[1].objects.add(object); // Share the edge with this object. - } + const overlapRes = overlapFn(cObj.edge, splitEdges); + addEdge &&= overlapRes; } + if ( splitEdges ) { // Remove the existing edge and add the new edges. // With overlaps, it is possible the edge was already removed. @@ -572,6 +572,40 @@ export class WallTracer extends Graph { } } + /** + * Process a collision that has an overlapping edge. + * @param {Wall|Token} object Object that the edge represents + * @returns {function} + * - @param {WallTracerEdge} overlappingEdge The overlapping edge to process + * - @param {WallTracerEdge[2]|null} splitEdges Two edges split at the current t position + * - @returns {boolean} True if an edge should be added. + */ + #processOverlapCollisionFn(object) { + const currOverlappingEdges = new Set(); + return (overlappingEdge, splitEdges) => { + // Order the splits along t0 to get the correct overlap. + if ( splitEdges && splitEdges[0].t0 > splitEdges[1].t0 ) splitEdges.reverse(); + + // Overlap is ending. + if ( currOverlappingEdges.has(overlappingEdge) ) { + currOverlappingEdges.delete(overlappingEdge); + if ( splitEdges ) splitEdges[0].objects.add(object); + else { + overlappingEdge.objects.add(object); + + // Make sure the object's edges include this cObj.edge. + this._addEdgeToObjectSet(object.id, overlappingEdge); + } + return false; // Only want one edge here: the existing. + } + + // Start a new overlap. + currOverlappingEdges.add(overlappingEdge); + if ( splitEdges ) splitEdges[1].objects.add(object); // Share the edge with this object. + return true; + }; + } + /** * Split the token edges by edges already in this graph. * @param {Token} token Token to convert to edge(s) @@ -779,9 +813,10 @@ wt.tokenEdges.forEach(s => s.forEach(e => e.draw({color: Draw.COLORS.orange}))) /** * Locate collisions between two segments. Uses almostEqual to get near collisions. * 1. Shared endpoints. - * 2. Endpoint of one segment within the other segment. + * 2. Endpoint of one segment within the other segment. Ignored. * 3. Two segments intersect. * 4. Collinear segments overlap: return start and end of the intersections. + * To add back in endpoint intersection, if `segmentCollisions` is [], then call `endpointIntersection`. * @param {PIXI.Point} a Endpoint on a|b segment * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment @@ -793,9 +828,10 @@ function segmentCollisions(a, b, c, d) { const overlaps = segmentOverlap(a, b, c, d); // Returns null or SegmentIntersection[2] if ( overlaps && !overlaps[0].pt.almostEqual(overlaps[1].pt) ) return overlaps; - // If endpoints are shared, return the shared point. + // If endpoints are shared, return empty array. // Otherwise, return the segment intersection or empty array. - return endpointIntersection(a, b, c, d) ?? segmentIntersection(a, b, c, d) ?? []; + if ( endpointIntersection(a, b, c, d) ) return []; + return segmentIntersection(a, b, c, d) ?? []; } /** From 5e157cb44be9b5be46c00a218ca40ea40deb5a13 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 19 Feb 2024 13:59:18 -0800 Subject: [PATCH 05/16] Fix for matching object to split walls Order splits based on the t0 value of the other edge --- scripts/pathfinding/WallTracer.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 44cb6c5..9988b3f 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -549,6 +549,7 @@ export class WallTracer extends Graph { for ( const cObj of cObjs ) { const splitEdges = cObj.edge.splitAtT(cObj.t1); // If the split is at the endpoint, will be null. if ( cObj.overlap ) { + if ( splitEdges && cObj.t0 ) splitEdges.reverse(); const overlapRes = overlapFn(cObj.edge, splitEdges); addEdge &&= overlapRes; } @@ -583,13 +584,13 @@ export class WallTracer extends Graph { #processOverlapCollisionFn(object) { const currOverlappingEdges = new Set(); return (overlappingEdge, splitEdges) => { - // Order the splits along t0 to get the correct overlap. - if ( splitEdges && splitEdges[0].t0 > splitEdges[1].t0 ) splitEdges.reverse(); - // Overlap is ending. if ( currOverlappingEdges.has(overlappingEdge) ) { currOverlappingEdges.delete(overlappingEdge); - if ( splitEdges ) splitEdges[0].objects.add(object); + if ( splitEdges ) { + splitEdges[0].objects.add(object); + splitEdges[1].objects.delete(object); + } else { overlappingEdge.objects.add(object); From 0e57f2cb5a03ae30f4425a76af92a8139ddf3341 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 19 Feb 2024 14:56:02 -0800 Subject: [PATCH 06/16] Add debug test for whether edgeSet is always defined --- scripts/pathfinding/WallTracer.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 9988b3f..bc9d8b0 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -494,7 +494,12 @@ export class WallTracer extends Graph { */ _removeEdgeFromObjectSet(id, edge) { const edgeSet = this.objectEdges.get(id); - if ( edgeSet ) edgeSet.delete(edge); + //if ( edgeSet ) edgeSet.delete(edge); + if ( !edgeSet ) { + console.debug("_removeEdgeFromObjectSet|edgeSet undefined"); + return; + } + edgeSet.delete(edge); } /** @@ -658,6 +663,13 @@ export class WallTracer extends Graph { if ( !edge.objects.size && this.edges.has(edge.key) ) this.deleteEdge(edge); } this.objectEdges.delete(id); + + // For each remaining object in the object set, remove it temporarily and re-add it. + // This will remove unnecessary vertices and recombine edges. + const remainingObjects = edgesArr.reduce((acc, curr) => acc = acc.union(curr.objects), new Set()); + if ( !remainingObjects.size ) return; + remainingObjects.forEach(obj => obj instanceof Wall ? this.removeWall(obj) : this.removeToken(obj)); + remainingObjects.forEach(obj => obj instanceof Wall ? this.addWall(obj) : this.addToken(obj)); } /** From 46bbdd6053a9372f52010347b935721d5dbfbfbb Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Tue, 20 Feb 2024 13:27:39 -0800 Subject: [PATCH 07/16] WIP to fix overlapping detection --- scripts/pathfinding/WallTracer.js | 258 ++++++++++++++++++------------ 1 file changed, 158 insertions(+), 100 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index bc9d8b0..e4c435d 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -283,15 +283,15 @@ export class WallTracerEdge extends GraphEdge { * Find the collision, if any, between this edge and another object's edge. * @param {PIXI.Point} A First edge endpoint for the object * @param {PIXI.Point} B Second edge endpoint for the object - * @returns {SegmentIntersection[]} + * @returns {SegmentIntersection|null} * Also rounds the t0 and t1 collision percentages to WallTracerEdge.PLACES. * t0 is the collision point for the A, B object edge. * t1 is the collision point for this edge. */ - findEdgeCollisions(A, B) { + findEdgeCollision(A, B) { const C = this.A.point; const D = this.B.point; - return segmentCollisions(A, B, C, D) ?? []; + return segmentCollision(A, B, C, D); } /** @@ -494,9 +494,9 @@ export class WallTracer extends Graph { */ _removeEdgeFromObjectSet(id, edge) { const edgeSet = this.objectEdges.get(id); - //if ( edgeSet ) edgeSet.delete(edge); + // Debug: if ( edgeSet ) edgeSet.delete(edge); if ( !edgeSet ) { - console.debug("_removeEdgeFromObjectSet|edgeSet undefined"); + console.warn("_removeEdgeFromObjectSet|edgeSet undefined"); return; } edgeSet.delete(edge); @@ -538,37 +538,79 @@ export class WallTracer extends Graph { // For each collision, ordered along the wall from A --> B // - construct a new edge for this wall portion + // - split the colliding edge if not at that edge's endpoint // - update the collision links for the colliding edge and this new edge + + // Overlapping edges: + // If overlap found, can ignore other collisions in-between. + // Split this edge at the start/end of the overlap. + // Split the overlapping edge at the start and end of the overlap. + // Add this object to the overlapping edge's objects. + // By definition, should only be a single overlap at a time. + // Possible for there to be collisions in between, b/c collisions checked by edgeA --> edgeB. Ignore. if ( !collisions.has(1) ) tArr.push(1); let priorT = 0; - const overlapFn = this.#processOverlapCollisionFn(object); - for ( const t of tArr ) { - // Check each collision point. - // For endpoint collisions, nothing will be added. - // For normal intersections, split the other edge. - // If overlapping, split the other edge if not at endpoint. - // One or more edges may be split at this collision point. - // Track when we start or end overlapping on an edge. + const OVERLAP = IX_TYPES.OVERLAP; + + const numT = tArr.length; + for ( let i = 0; i < numT; i += 1 ) { + // Note: it is possible for more than one collision to occur at a given t location. + // (multiple T-endpoint collisions) + const t = tArr[i]; const cObjs = collisions.get(t) ?? []; - let addEdge = Boolean(t); // Don't add an edge for 0 --> 0. - for ( const cObj of cObjs ) { - const splitEdges = cObj.edge.splitAtT(cObj.t1); // If the split is at the endpoint, will be null. - if ( cObj.overlap ) { - if ( splitEdges && cObj.t0 ) splitEdges.reverse(); - const overlapRes = overlapFn(cObj.edge, splitEdges); - addEdge &&= overlapRes; + let addEdge = true; + + // Prioritize overlaps. + // Only one overlap should start at a given t. + const overlapC = cObjs.findSplice(obj => obj.type === OVERLAP); + if ( overlapC ) { + // Beginning overlap. + let overlappingEdge = overlapC.edge; + let splitEdges = overlappingEdge.splitAtT(overlapC.t1); + if ( splitEdges ) { + this.deleteEdge(overlappingEdge); + const overlapIdx = overlapC.t1 > overlapC.endT1 ? 0 : 1; + overlappingEdge = splitEdges[overlapIdx]; + splitEdges.forEach(e => this.addEdge(e)); } + // Add this object to the overlapping portion. + overlappingEdge.objects.add(object); + this._addEdgeToObjectSet(object.id, overlappingEdge); + + // Ending overlap. + const splitT = prorateTSplit(overlapC.t1, overlapC.endT1); + splitEdges = overlappingEdge.splitAtT(splitT); + if ( splitEdges ) { + this.deleteEdge(overlappingEdge); + const overlapIdx = overlapC.t1 > overlapC.endT1 ? 0 : 1; + splitEdges[overlapIdx].objects.delete(object); // Remove object from non-overlapping portion. + splitEdges.forEach(e => this.addEdge(e)); + } + + // Jump to new t position in the array. + i = tArr.findIndex(t => t >= overlapC.endT0); + priorT = t; + continue; + } + + // For normal intersections, split the other edge. If the other edge forms a T-intersection, + // it will not get split (splits at t1 = 0 or t1 = 1). + for ( const cObj of cObjs ) { + const splitEdges = cObj.edge.splitAtT(cObj.t1); // If the split is at the endpoint, will be null. if ( splitEdges ) { // Remove the existing edge and add the new edges. // With overlaps, it is possible the edge was already removed. - if ( this.edges.has(cObj.edge.key) ) this.deleteEdge(cObj.edge); + // if ( this.edges.has(cObj.edge.key) ) this.deleteEdge(cObj.edge); + this.deleteEdge(cObj.edge); splitEdges.forEach(e => this.addEdge(e)); } } + // At each t value after 0, we must construct a new edge. + // Exception: If this portion overlaps another edge, use that edge instead. // Build edge for portion of wall between priorT and t, skipping when t === 0 - if ( addEdge ) { + if ( addEdge && t ) { const edge = WallTracerEdge.fromObjects(edgeA, edgeB, [object], priorT, t); this.addEdge(edge); } @@ -578,40 +620,6 @@ export class WallTracer extends Graph { } } - /** - * Process a collision that has an overlapping edge. - * @param {Wall|Token} object Object that the edge represents - * @returns {function} - * - @param {WallTracerEdge} overlappingEdge The overlapping edge to process - * - @param {WallTracerEdge[2]|null} splitEdges Two edges split at the current t position - * - @returns {boolean} True if an edge should be added. - */ - #processOverlapCollisionFn(object) { - const currOverlappingEdges = new Set(); - return (overlappingEdge, splitEdges) => { - // Overlap is ending. - if ( currOverlappingEdges.has(overlappingEdge) ) { - currOverlappingEdges.delete(overlappingEdge); - if ( splitEdges ) { - splitEdges[0].objects.add(object); - splitEdges[1].objects.delete(object); - } - else { - overlappingEdge.objects.add(object); - - // Make sure the object's edges include this cObj.edge. - this._addEdgeToObjectSet(object.id, overlappingEdge); - } - return false; // Only want one edge here: the existing. - } - - // Start a new overlap. - currOverlappingEdges.add(overlappingEdge); - if ( splitEdges ) splitEdges[1].objects.add(object); // Share the edge with this object. - return true; - }; - } - /** * Split the token edges by edges already in this graph. * @param {Token} token Token to convert to edge(s) @@ -694,20 +702,22 @@ export class WallTracer extends Graph { /** * Locate collision points for any edges that collide with this edge. + * Skips edges that simply share a single endpoint. * @param {PIXI.Point} edgeA Edge endpoint * @param {PIXI.Point} edgeB Other edge endpoint - * @returns {Map} Map of locations of the collisions + * @returns {Map} Map of locations of the collisions along A|B */ findEdgeCollisions(edgeA, edgeB) { const edgeCollisions = []; const bounds = segmentBounds(edgeA, edgeB); const collisionTest = (o, _rect) => segmentsOverlap(edgeA, edgeB, o.t.A, o.t.B); const collidingEdges = this.edgesQuadtree.getObjects(bounds, { collisionTest }); + const ENDPOINT = IX_TYPES.ENDPOINT; for ( const edge of collidingEdges ) { - const collisions = edge.findEdgeCollisions(edgeA, edgeB); - if ( !collisions.length ) continue; - collisions.forEach(c => c.edge = edge); - edgeCollisions.push(...collisions); + const collision = edge.findEdgeCollision(edgeA, edgeB); + if ( !collision || collision.type === ENDPOINT ) continue; + collision.edge = edge; + edgeCollisions.push(collision); } return groupBy(edgeCollisions, this.constructor._keyGetter); } @@ -815,36 +825,46 @@ wt.tokenEdges.forEach(s => s.forEach(e => e.draw({color: Draw.COLORS.orange}))) // NOTE: Helper functions +const IX_TYPES = { + NONE: 0, + NORMAL: 1, + ENDPOINT: 2, + OVERLAP: 3 +}; + /** * @typedef {object} SegmentIntersection * Represents intersection between two segments, a|b and c|d - * @property {PIXI.Point} pt Point of intersection - * @property {number} t0 Intersection location on the a --> b segment - * @property {number} t1 Intersection location on the c --> d segment + * @property {PIXI.Point} pt Point of intersection + * @property {number} t0 Intersection location on the a|b segment + * @property {number} t1 Intersection location on the c|d segment + * @property {IX_TYPES} ixType Type of intersection + * @property {number} [endT0] If overlap, this is the end intersection on a|b + * @property {number} [endT1] If overlap, this is the end intersection on c|d + * @property {PIXI.Point} [endPoint] If overlap, the ending intersection */ /** * Locate collisions between two segments. Uses almostEqual to get near collisions. * 1. Shared endpoints. - * 2. Endpoint of one segment within the other segment. Ignored. + * 2. Endpoint of one segment within the other segment. * 3. Two segments intersect. * 4. Collinear segments overlap: return start and end of the intersections. - * To add back in endpoint intersection, if `segmentCollisions` is [], then call `endpointIntersection`. * @param {PIXI.Point} a Endpoint on a|b segment * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection[]|null} + * @returns {SegmentIntersection|null} */ -function segmentCollisions(a, b, c, d) { - // Check for true overlap. If shared endpoints, will be handled later. - const overlaps = segmentOverlap(a, b, c, d); // Returns null or SegmentIntersection[2] - if ( overlaps && !overlaps[0].pt.almostEqual(overlaps[1].pt) ) return overlaps; - - // If endpoints are shared, return empty array. - // Otherwise, return the segment intersection or empty array. - if ( endpointIntersection(a, b, c, d) ) return []; - return segmentIntersection(a, b, c, d) ?? []; +function segmentCollision(a, b, c, d) { + // Endpoint intersections can occur as part of a segment overlap. So test overlap first. + // Overlap will be fast if the segments are not collinear. + const overlap = segmentOverlap(a, b, c, d); + + // If the overlap is a single point, it is likely an endpoint intersection. + if ( overlap && !overlap.pt.almostEqual(overlap.endPt) ) return overlap; + return endpointIntersection(a, b, c, d) + ?? segmentIntersection(a, b, c, d); } /** @@ -855,13 +875,14 @@ function segmentCollisions(a, b, c, d) { * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection[1]|null} + * @returns {SegmentIntersection|null} */ function endpointIntersection(a, b, c, d) { - if ( a.key === c.key || c.almostEqual(a) ) return [{ t0: 0, t1: 0, pt: a }]; - if ( a.key === d.key || d.almostEqual(a) ) return [{ t0: 0, t1: 1, pt: a }]; - if ( b.key === c.key || c.almostEqual(b) ) return [{ t0: 1, t1: 0, pt: b }]; - if ( b.key === d.key || d.almostEqual(b) ) return [{ t0: 1, t1: 1, pt: b }]; + const type = IX_TYPES.ENDPOINT; + if ( a.key === c.key || c.almostEqual(a) ) return { t0: 0, t1: 0, pt: a, type }; + if ( a.key === d.key || d.almostEqual(a) ) return { t0: 0, t1: 1, pt: a, type }; + if ( b.key === c.key || c.almostEqual(b) ) return { t0: 1, t1: 0, pt: b, type }; + if ( b.key === d.key || d.almostEqual(b) ) return { t0: 1, t1: 1, pt: b, type }; return null; } @@ -874,23 +895,24 @@ function endpointIntersection(a, b, c, d) { * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection[1]|null} + * @returns {SegmentIntersection|null} */ function segmentIntersection(a, b, c, d) { if ( !foundry.utils.lineSegmentIntersects(a, b, c, d) ) return null; const ix = CONFIG.GeometryLib.utils.lineLineIntersection(a, b, c, d, { t1: true }); ix.pt = PIXI.Point.fromObject(ix); - return [ix]; + ix.type = IX_TYPES.NORMAL; + return ix; } /** * Determine if two segments overlap and return the two points at which the segments - * begin their overlap. If overlap is an endpoint, may return that endpoint twice. + * begin their overlap. * @param {PIXI.Point} a Endpoint on a|b segment * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection[2]|null} + * @returns {SegmentIntersection|null} * The 2 intersections will be sorted so that [0] --> [1] is the overlap. */ function segmentOverlap(a, b, c, d) { @@ -931,30 +953,66 @@ function segmentOverlap(a, b, c, d) { // Overlap: c|d --- aIx|bIx --- aIx|bIx --- c|d - const overlap = true; - if ( aIx && bIx ) return [ - { t0: 0, t1: aIx.t0, pt: PIXI.Point.fromObject(aIx), overlap }, - { t0: 1, t1: bIx.t0, pt: PIXI.Point.fromObject(bIx), overlap } - ]; + const type = IX_TYPES.OVERLAP; + if ( aIx && bIx ) return { + t0: 0, t1: aIx.t0, pt: PIXI.Point.fromObject(aIx), type, + endT0: 1, endT1: bIx.t0, endPt: PIXI.Point.fromObject(bIx) }; // Overlap: a|b --- cIx|dIx --- cIx|dIx --- a|b const cIx = ix2.t0.between(0, 1) ? ix2 : null; const dIx = ix3.t0.between(0, 1) ? ix3 : null; - if ( cIx && dIx ) return [ - { t0: cIx.t0, t1: 0, pt: PIXI.Point.fromObject(cIx), overlap }, - { t0: dIx.t0, t1: 1, pt: PIXI.Point.fromObject(dIx), overlap } - ]; + if ( cIx && dIx ) return { + t0: cIx.t0, t1: 0, pt: PIXI.Point.fromObject(cIx), type, + endT0: dIx.t0, endT1: 1, endPt: PIXI.Point.fromObject(dIx) }; // Overlap: a|b --- cIx|dIx --- aIx|bIx --- c|d const abIx = aIx ?? bIx; const cdIx = cIx ?? dIx; - if ( abIx && cdIx ) { - return [ - { t0: cdIx.t0, t1: cIx ? 0 : 1, pt: PIXI.Point.fromObject(cdIx), overlap }, - { t0: aIx ? 0 : 1, t1: abIx.t0, pt: PIXI.Point.fromObject(abIx), overlap } - ]; - } + if ( abIx && cdIx ) return { + t0: cdIx.t0, t1: cIx ? 0 : 1, pt: PIXI.Point.fromObject(cdIx), type, + endT0: aIx ? 0 : 1, endT1: abIx.t0, endPt: PIXI.Point.fromObject(abIx) }; // No overlap. return null; } + + +/** + * Find all array objects that match a condition, remove them from the array, and return them. + * Like Array.findSplice, but handles multiples. + * Modifies the array in place + * @param {array} arr Array to search + * @param {function} filterFn Function used for the filter test + * @returns {array} + */ +function filterSplice(arr, filterFn) { + const indices = []; + const filteredElems = arr.filter((elem, idx, arr) => { + if ( !filterFn(elem, idx, arr) ) return false; + indices.push(idx); + return true; + }); + indices.sort((a, b) => b - a); // So we can splice without changing other indices. + indices.forEach(idx => arr.splice(idx, 1)); + return filteredElems; +} + +/** + * Prorate a t value based on some preexisting split. + * Example: Split a segment length 10 at .2 and .8. + * - Split at .2: Segments length 2 and length 8. + * - Split second segment: (.8 - .2) / .8 = .75. Split length 8 segment at .7 to get length 6. + * - Segments 2, 6, 2 + * If the first split is higher, must reverse the numbers. + */ +function prorateTSplit(firstT, secondT) { + if ( secondT.almostEqual(0) ) return 0; + if ( firstT.almostEqual(0) ) return secondT; + if ( firstT.almostEqual(1) ) return secondT; + if ( secondT.almostEqual(1) ) return 1; + + const flip = secondT < firstT; + if ( flip ) [firstT, secondT] = [secondT, firstT]; + const newT = (secondT - firstT) / secondT; + return flip ? 1 - newT : newT; +} From 4f760fc7fa0de49b4925172d58f5ac1803fb3dfc Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Tue, 20 Feb 2024 15:11:54 -0800 Subject: [PATCH 08/16] Apparently working overlap detection! --- scripts/pathfinding/WallTracer.js | 71 +++++++++++-------------------- scripts/util.js | 20 +++++++++ 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index e4c435d..a867b0f 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -553,12 +553,20 @@ export class WallTracer extends Graph { const OVERLAP = IX_TYPES.OVERLAP; const numT = tArr.length; + let addEdge = true; for ( let i = 0; i < numT; i += 1 ) { // Note: it is possible for more than one collision to occur at a given t location. // (multiple T-endpoint collisions) const t = tArr[i]; const cObjs = collisions.get(t) ?? []; - let addEdge = true; + + // Build edge for portion of wall between priorT and t, skipping when t === 0. + // Exception: If this portion overlaps another edge, use that edge instead. + if ( t && addEdge ) { + const edge = WallTracerEdge.fromObjects(edgeA, edgeB, [object], priorT, t); + this.addEdge(edge); + } + addEdge = true; // Prioritize overlaps. // Only one overlap should start at a given t. @@ -589,32 +597,24 @@ export class WallTracer extends Graph { } // Jump to new t position in the array. - i = tArr.findIndex(t => t >= overlapC.endT0); - priorT = t; - continue; - } - - // For normal intersections, split the other edge. If the other edge forms a T-intersection, - // it will not get split (splits at t1 = 0 or t1 = 1). - for ( const cObj of cObjs ) { - const splitEdges = cObj.edge.splitAtT(cObj.t1); // If the split is at the endpoint, will be null. - if ( splitEdges ) { - // Remove the existing edge and add the new edges. - // With overlaps, it is possible the edge was already removed. - // if ( this.edges.has(cObj.edge.key) ) this.deleteEdge(cObj.edge); - this.deleteEdge(cObj.edge); - splitEdges.forEach(e => this.addEdge(e)); + const idx = tArr.findIndex(t => t >= overlapC.endT0); + if ( ~idx ) i = idx - 1; // Will be increased by the for loop. Avoid getting into infinite loop. + addEdge = false; // Use the overlap instead. + + } else { + // For normal intersections, split the other edge. If the other edge forms a T-intersection, + // it will not get split (splits at t1 = 0 or t1 = 1). + for ( const cObj of cObjs ) { + const splitEdges = cObj.edge.splitAtT(cObj.t1); // If the split is at the endpoint, will be null. + if ( splitEdges ) { + // Remove the existing edge and add the new edges. + // With overlaps, it is possible the edge was already removed. + // if ( this.edges.has(cObj.edge.key) ) this.deleteEdge(cObj.edge); + this.deleteEdge(cObj.edge); + splitEdges.forEach(e => this.addEdge(e)); + } } } - - // At each t value after 0, we must construct a new edge. - // Exception: If this portion overlaps another edge, use that edge instead. - // Build edge for portion of wall between priorT and t, skipping when t === 0 - if ( addEdge && t ) { - const edge = WallTracerEdge.fromObjects(edgeA, edgeB, [object], priorT, t); - this.addEdge(edge); - } - // Cycle to next. priorT = t; } @@ -976,27 +976,6 @@ function segmentOverlap(a, b, c, d) { return null; } - -/** - * Find all array objects that match a condition, remove them from the array, and return them. - * Like Array.findSplice, but handles multiples. - * Modifies the array in place - * @param {array} arr Array to search - * @param {function} filterFn Function used for the filter test - * @returns {array} - */ -function filterSplice(arr, filterFn) { - const indices = []; - const filteredElems = arr.filter((elem, idx, arr) => { - if ( !filterFn(elem, idx, arr) ) return false; - indices.push(idx); - return true; - }); - indices.sort((a, b) => b - a); // So we can splice without changing other indices. - indices.forEach(idx => arr.splice(idx, 1)); - return filteredElems; -} - /** * Prorate a t value based on some preexisting split. * Example: Split a segment length 10 at .2 and .8. diff --git a/scripts/util.js b/scripts/util.js index 3872ec9..b4567f5 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -223,4 +223,24 @@ export async function injectConfiguration(app, html, data, template, findString) const form = html.find(findString); form.append(myHTML); app.setPosition(app.position); +} + +/** + * Find all array objects that match a condition, remove them from the array, and return them. + * Like Array.findSplice, but handles multiples. + * Modifies the array in place + * @param {array} arr Array to search + * @param {function} filterFn Function used for the filter test + * @returns {array} + */ +export function filterSplice(arr, filterFn) { + const indices = []; + const filteredElems = arr.filter((elem, idx, arr) => { + if ( !filterFn(elem, idx, arr) ) return false; + indices.push(idx); + return true; + }); + indices.sort((a, b) => b - a); // So we can splice without changing other indices. + indices.forEach(idx => arr.splice(idx, 1)); + return filteredElems; } \ No newline at end of file From 0596c9cd2b5303a4237996fe83b7d8f48596a5e9 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Tue, 20 Feb 2024 15:48:09 -0800 Subject: [PATCH 09/16] Use simpler overlappingPoints function Uses minMax x/y to find the overlap instead of intersecting a bunch of lines. --- scripts/pathfinding/WallTracer.js | 111 ++++++++++++------------------ 1 file changed, 44 insertions(+), 67 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index a867b0f..907cdc0 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -13,7 +13,7 @@ Wall // WallTracer3 -import { groupBy, segmentBounds, perpendicularPoints } from "../util.js"; +import { groupBy, segmentBounds } from "../util.js"; import { Draw } from "../geometry/Draw.js"; import { Graph, GraphVertex, GraphEdge } from "../geometry/Graph.js"; import { Settings } from "../settings.js"; @@ -859,11 +859,8 @@ const IX_TYPES = { function segmentCollision(a, b, c, d) { // Endpoint intersections can occur as part of a segment overlap. So test overlap first. // Overlap will be fast if the segments are not collinear. - const overlap = segmentOverlap(a, b, c, d); - - // If the overlap is a single point, it is likely an endpoint intersection. - if ( overlap && !overlap.pt.almostEqual(overlap.endPt) ) return overlap; - return endpointIntersection(a, b, c, d) + return segmentOverlap(a, b, c, d) + ?? endpointIntersection(a, b, c, d) ?? segmentIntersection(a, b, c, d); } @@ -906,74 +903,54 @@ function segmentIntersection(a, b, c, d) { } /** - * Determine if two segments overlap and return the two points at which the segments - * begin their overlap. + * Determine if two collinear segments overlap and return the two points at which the segments + * begin/end their overlap. If you just need the points, use findOverlappingPoints. * @param {PIXI.Point} a Endpoint on a|b segment * @param {PIXI.Point} b Endpoint on a|b segment * @param {PIXI.Point} c Endpoint on c|d segment * @param {PIXI.Point} d Endpoint on c|d segment * @returns {SegmentIntersection|null} - * The 2 intersections will be sorted so that [0] --> [1] is the overlap. + * Either an ENDPOINT or an OVERLAP intersection. */ function segmentOverlap(a, b, c, d) { - // Distinguish a---b|c---d from a---c---b|d. Latter is an overlap. - // Okay: - // a---b|c---d - // b---a|c---d - // b---a|d---c - // a---b|d---c - // Overlap: - // a---c---b|d - // a---d---b|c - // b---c---a|d - // b---d---a|c - - // First, ensure the segments are overlapping. - const orient2d = foundry.utils.orient2dFast; - if ( !orient2d(a, b, c).almostEqual(0) || !orient2d(a, b, d).almostEqual(0) ) return null; - - // To detect overlap, construct small perpendicular lines to the endpoints. - const aP = perpendicularPoints(a, b); // Line perpendicular to a|b that intersects a - const bP = perpendicularPoints(b, a); - const cP = perpendicularPoints(c, d); - const dP = perpendicularPoints(d, c); - - // Intersect each segment with the perpendicular lines. - const lli = CONFIG.GeometryLib.utils.lineLineIntersection; - const ix0 = lli(c, d, aP[0], aP[1]); - const ix1 = lli(c, d, bP[0], bP[1]); - const ix2 = lli(a, b, cP[0], cP[1]); - const ix3 = lli(a, b, dP[0], dP[1]); - - // Shouldn't happen unless a,b,c, or d are not distinct points. - if ( !(ix0 && ix1 && ix2 && ix3) ) return null; - - const aIx = ix0.t0.between(0, 1) ? ix0 : null; - const bIx = ix1.t0.between(0, 1) ? ix1 : null; - - - // Overlap: c|d --- aIx|bIx --- aIx|bIx --- c|d - const type = IX_TYPES.OVERLAP; - if ( aIx && bIx ) return { - t0: 0, t1: aIx.t0, pt: PIXI.Point.fromObject(aIx), type, - endT0: 1, endT1: bIx.t0, endPt: PIXI.Point.fromObject(bIx) }; - - // Overlap: a|b --- cIx|dIx --- cIx|dIx --- a|b - const cIx = ix2.t0.between(0, 1) ? ix2 : null; - const dIx = ix3.t0.between(0, 1) ? ix3 : null; - if ( cIx && dIx ) return { - t0: cIx.t0, t1: 0, pt: PIXI.Point.fromObject(cIx), type, - endT0: dIx.t0, endT1: 1, endPt: PIXI.Point.fromObject(dIx) }; - - // Overlap: a|b --- cIx|dIx --- aIx|bIx --- c|d - const abIx = aIx ?? bIx; - const cdIx = cIx ?? dIx; - if ( abIx && cdIx ) return { - t0: cdIx.t0, t1: cIx ? 0 : 1, pt: PIXI.Point.fromObject(cdIx), type, - endT0: aIx ? 0 : 1, endT1: abIx.t0, endPt: PIXI.Point.fromObject(abIx) }; - - // No overlap. - return null; + const pts = findOverlappingPoints(a, b, c, d); + if ( !pts.length ) return null; + + // Calculate t value for a single point, which must be an endpoint. + if ( pts.length === 1 ) { + const pt = pts[0]; + const res = { pt, type: IX_TYPES.ENDPOINT }; + res.t0 = pt.almostEqual(a) ? 0 : 1; + res.t1 = pt.almostEqual(c) ? 0 : 1; + return res; + } + + // Calculate t value for overlapping points. + const res = { type: IX_TYPES.OVERLAP }; + const distAB = PIXI.Point.distanceBetween(a, b); + const distCD = PIXI.Point.distanceBetween(c, d); + const tA0 = PIXI.Point.distanceBetween(a, pts[0]) / distAB; + const tA1 = PIXI.Point.distanceBetween(a, pts[1]) / distAB; + const tC0 = PIXI.Point.distanceBetween(c, pts[0]) / distCD; + const tC1 = PIXI.Point.distanceBetween(c, pts[1]) / distCD; + + if ( tA0 <= tA1 ) { + res.t0 = tA0; + res.endT0 = tA1; + res.t1 = tC0; + res.endT1 = tC1; + res.pt = pts[0]; + res.endPt = pts[1]; + } else { + res.t0 = tA1; + res.endT0 = tA0; + res.t1 = tC1; + res.endT1 = tC0; + res.pt = pts[1]; + res.endPt = pts[0]; + } + + return res; } /** From d7d687e6b8b0563c314433758d81188ef61536c6 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Tue, 20 Feb 2024 15:49:18 -0800 Subject: [PATCH 10/16] Change name to doSegmentsOverlap for clarity --- scripts/pathfinding/WallTracer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 907cdc0..97a1196 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -710,7 +710,7 @@ export class WallTracer extends Graph { findEdgeCollisions(edgeA, edgeB) { const edgeCollisions = []; const bounds = segmentBounds(edgeA, edgeB); - const collisionTest = (o, _rect) => segmentsOverlap(edgeA, edgeB, o.t.A, o.t.B); + const collisionTest = (o, _rect) => doSegmentsOverlap(edgeA, edgeB, o.t.A, o.t.B); const collidingEdges = this.edgesQuadtree.getObjects(bounds, { collisionTest }); const ENDPOINT = IX_TYPES.ENDPOINT; for ( const edge of collidingEdges ) { @@ -746,7 +746,7 @@ export class WallTracer extends Graph { * @param {PIXI.Point} d Endpoint of segment C|D * @returns {boolean} */ -function segmentsOverlap(a, b, c, d) { +function doSegmentsOverlap(a, b, c, d) { if ( foundry.utils.lineSegmentIntersects(a, b, c, d) ) return true; // If collinear, B is within A|B or D is within A|B From e1188993008fed8a6dc6970d5bbb268376d08a4a Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Tue, 20 Feb 2024 15:58:15 -0800 Subject: [PATCH 11/16] Switch to using lib geometry for segment ix/overlap methods --- scripts/geometry | 2 +- scripts/pathfinding/WallTracer.js | 187 +----------------------------- 2 files changed, 2 insertions(+), 187 deletions(-) diff --git a/scripts/geometry b/scripts/geometry index 9d91e12..ea93e25 160000 --- a/scripts/geometry +++ b/scripts/geometry @@ -1 +1 @@ -Subproject commit 9d91e12958d858573d5a4a5689fc857789700b70 +Subproject commit ea93e256589fb336194ca48238128894d50ded60 diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 97a1196..1b745f0 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -1,8 +1,6 @@ /* globals CanvasQuadtree, -CONFIG, CONST, -foundry, PIXI, Token, Wall @@ -17,6 +15,7 @@ import { groupBy, segmentBounds } from "../util.js"; import { Draw } from "../geometry/Draw.js"; import { Graph, GraphVertex, GraphEdge } from "../geometry/Graph.js"; import { Settings } from "../settings.js"; +import { doSegmentsOverlap, IX_TYPES, segmentCollision } from "../geometry/util.js"; /* WallTracerVertex @@ -737,62 +736,6 @@ export class WallTracer extends Graph { } } -/** - * Do two segments overlap? - * Overlap means they intersect or they are collinear and overlap - * @param {PIXI.Point} a Endpoint of segment A|B - * @param {PIXI.Point} b Endpoint of segment A|B - * @param {PIXI.Point} c Endpoint of segment C|D - * @param {PIXI.Point} d Endpoint of segment C|D - * @returns {boolean} - */ -function doSegmentsOverlap(a, b, c, d) { - if ( foundry.utils.lineSegmentIntersects(a, b, c, d) ) return true; - - // If collinear, B is within A|B or D is within A|B - const pts = findOverlappingPoints(a, b, c, d); - return pts.length; -} - -/** - * Find the points of overlap between two segments A|B and C|D. - * @param {PIXI.Point} a Endpoint of segment A|B - * @param {PIXI.Point} b Endpoint of segment A|B - * @param {PIXI.Point} c Endpoint of segment C|D - * @param {PIXI.Point} d Endpoint of segment C|D - * @returns {PIXI.Point[]} Array with 0, 1, or 2 points. - * The points returned will be a, b, c, and/or d, whichever are contained by the others. - * No points are returned if A|B and C|D are not collinear, or if they do not overlap. - * A single point is returned if a single endpoint is shared. - */ -function findOverlappingPoints(a, b, c, d) { - if ( !foundry.utils.orient2dFast(a, b, c).almostEqual(0) - || !foundry.utils.orient2dFast(a, b, d).almostEqual(0) ) return []; - - // B is within A|B or D is within A|B - const abx = Math.minMax(a.x, b.x); - const aby = Math.minMax(a.y, b.y); - const cdx = Math.minMax(c.x, d.x); - const cdy = Math.minMax(c.y, d.y); - - const p0 = new PIXI.Point( - Math.max(abx.min, cdx.min), - Math.max(aby.min, cdy.min) - ); - - const p1 = new PIXI.Point( - Math.min(abx.max, cdx.max), - Math.min(aby.max, cdy.max) - ); - - const xEqual = p0.x.almostEqual(p1.x); - const yEqual = p1.y.almostEqual(p1.y); - if ( xEqual && yEqual ) return [p0]; - if ( xEqual ^ yEqual - || (p0.x < p1.x && p0.y < p1.y)) return [p0, p1]; - - return []; -} // Must declare this variable after defining WallTracer. export const SCENE_GRAPH = new WallTracer(); @@ -825,134 +768,6 @@ wt.tokenEdges.forEach(s => s.forEach(e => e.draw({color: Draw.COLORS.orange}))) // NOTE: Helper functions -const IX_TYPES = { - NONE: 0, - NORMAL: 1, - ENDPOINT: 2, - OVERLAP: 3 -}; - -/** - * @typedef {object} SegmentIntersection - * Represents intersection between two segments, a|b and c|d - * @property {PIXI.Point} pt Point of intersection - * @property {number} t0 Intersection location on the a|b segment - * @property {number} t1 Intersection location on the c|d segment - * @property {IX_TYPES} ixType Type of intersection - * @property {number} [endT0] If overlap, this is the end intersection on a|b - * @property {number} [endT1] If overlap, this is the end intersection on c|d - * @property {PIXI.Point} [endPoint] If overlap, the ending intersection - */ - -/** - * Locate collisions between two segments. Uses almostEqual to get near collisions. - * 1. Shared endpoints. - * 2. Endpoint of one segment within the other segment. - * 3. Two segments intersect. - * 4. Collinear segments overlap: return start and end of the intersections. - * @param {PIXI.Point} a Endpoint on a|b segment - * @param {PIXI.Point} b Endpoint on a|b segment - * @param {PIXI.Point} c Endpoint on c|d segment - * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection|null} - */ -function segmentCollision(a, b, c, d) { - // Endpoint intersections can occur as part of a segment overlap. So test overlap first. - // Overlap will be fast if the segments are not collinear. - return segmentOverlap(a, b, c, d) - ?? endpointIntersection(a, b, c, d) - ?? segmentIntersection(a, b, c, d); -} - -/** - * Determine if two segments intersect at an endpoint and return t0, t1 based on that intersection. - * Does not consider segment collinearity, and only picks the first shared endpoint. - * (If segments are collinear, possible they are the same and share both endpoints.) - * @param {PIXI.Point} a Endpoint on a|b segment - * @param {PIXI.Point} b Endpoint on a|b segment - * @param {PIXI.Point} c Endpoint on c|d segment - * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection|null} - */ -function endpointIntersection(a, b, c, d) { - const type = IX_TYPES.ENDPOINT; - if ( a.key === c.key || c.almostEqual(a) ) return { t0: 0, t1: 0, pt: a, type }; - if ( a.key === d.key || d.almostEqual(a) ) return { t0: 0, t1: 1, pt: a, type }; - if ( b.key === c.key || c.almostEqual(b) ) return { t0: 1, t1: 0, pt: b, type }; - if ( b.key === d.key || d.almostEqual(b) ) return { t0: 1, t1: 1, pt: b, type }; - return null; -} - -/** - * Determine if two segments intersect and return t0, t1 based on that intersection. - * Generally will detect endpoint intersections but no special handling. - * To ensure near-endpoint-intersections are captured, use endpointIntersection. - * Will not detect overlap. See segmentOverlap - * @param {PIXI.Point} a Endpoint on a|b segment - * @param {PIXI.Point} b Endpoint on a|b segment - * @param {PIXI.Point} c Endpoint on c|d segment - * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection|null} - */ -function segmentIntersection(a, b, c, d) { - if ( !foundry.utils.lineSegmentIntersects(a, b, c, d) ) return null; - const ix = CONFIG.GeometryLib.utils.lineLineIntersection(a, b, c, d, { t1: true }); - ix.pt = PIXI.Point.fromObject(ix); - ix.type = IX_TYPES.NORMAL; - return ix; -} - -/** - * Determine if two collinear segments overlap and return the two points at which the segments - * begin/end their overlap. If you just need the points, use findOverlappingPoints. - * @param {PIXI.Point} a Endpoint on a|b segment - * @param {PIXI.Point} b Endpoint on a|b segment - * @param {PIXI.Point} c Endpoint on c|d segment - * @param {PIXI.Point} d Endpoint on c|d segment - * @returns {SegmentIntersection|null} - * Either an ENDPOINT or an OVERLAP intersection. - */ -function segmentOverlap(a, b, c, d) { - const pts = findOverlappingPoints(a, b, c, d); - if ( !pts.length ) return null; - - // Calculate t value for a single point, which must be an endpoint. - if ( pts.length === 1 ) { - const pt = pts[0]; - const res = { pt, type: IX_TYPES.ENDPOINT }; - res.t0 = pt.almostEqual(a) ? 0 : 1; - res.t1 = pt.almostEqual(c) ? 0 : 1; - return res; - } - - // Calculate t value for overlapping points. - const res = { type: IX_TYPES.OVERLAP }; - const distAB = PIXI.Point.distanceBetween(a, b); - const distCD = PIXI.Point.distanceBetween(c, d); - const tA0 = PIXI.Point.distanceBetween(a, pts[0]) / distAB; - const tA1 = PIXI.Point.distanceBetween(a, pts[1]) / distAB; - const tC0 = PIXI.Point.distanceBetween(c, pts[0]) / distCD; - const tC1 = PIXI.Point.distanceBetween(c, pts[1]) / distCD; - - if ( tA0 <= tA1 ) { - res.t0 = tA0; - res.endT0 = tA1; - res.t1 = tC0; - res.endT1 = tC1; - res.pt = pts[0]; - res.endPt = pts[1]; - } else { - res.t0 = tA1; - res.endT0 = tA0; - res.t1 = tC1; - res.endT1 = tC0; - res.pt = pts[1]; - res.endPt = pts[0]; - } - - return res; -} - /** * Prorate a t value based on some preexisting split. * Example: Split a segment length 10 at .2 and .8. From ed71a81aa156acdd78e6ff197144fae6b2bf228f Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Wed, 21 Feb 2024 06:22:10 -0800 Subject: [PATCH 12/16] Fix for wall not getting added again correctly 2 walls with overlap added. First wall removed then re-added. The edge was getting skipped from being added at the end of the for loop. Basically, if there is any distance between the overlap end and t === 1, an edge will need to be added. --- scripts/pathfinding/WallTracer.js | 33 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 1b745f0..99ccd27 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -552,7 +552,6 @@ export class WallTracer extends Graph { const OVERLAP = IX_TYPES.OVERLAP; const numT = tArr.length; - let addEdge = true; for ( let i = 0; i < numT; i += 1 ) { // Note: it is possible for more than one collision to occur at a given t location. // (multiple T-endpoint collisions) @@ -561,11 +560,10 @@ export class WallTracer extends Graph { // Build edge for portion of wall between priorT and t, skipping when t === 0. // Exception: If this portion overlaps another edge, use that edge instead. - if ( t && addEdge ) { + if ( t > priorT ) { const edge = WallTracerEdge.fromObjects(edgeA, edgeB, [object], priorT, t); this.addEdge(edge); } - addEdge = true; // Prioritize overlaps. // Only one overlap should start at a given t. @@ -598,22 +596,23 @@ export class WallTracer extends Graph { // Jump to new t position in the array. const idx = tArr.findIndex(t => t >= overlapC.endT0); if ( ~idx ) i = idx - 1; // Will be increased by the for loop. Avoid getting into infinite loop. - addEdge = false; // Use the overlap instead. - - } else { - // For normal intersections, split the other edge. If the other edge forms a T-intersection, - // it will not get split (splits at t1 = 0 or t1 = 1). - for ( const cObj of cObjs ) { - const splitEdges = cObj.edge.splitAtT(cObj.t1); // If the split is at the endpoint, will be null. - if ( splitEdges ) { - // Remove the existing edge and add the new edges. - // With overlaps, it is possible the edge was already removed. - // if ( this.edges.has(cObj.edge.key) ) this.deleteEdge(cObj.edge); - this.deleteEdge(cObj.edge); - splitEdges.forEach(e => this.addEdge(e)); - } + priorT = overlapC.endT0; + continue; + + } + // For normal intersections, split the other edge. If the other edge forms a T-intersection, + // it will not get split (splits at t1 = 0 or t1 = 1). + for ( const cObj of cObjs ) { + const splitEdges = cObj.edge.splitAtT(cObj.t1); // If the split is at the endpoint, will be null. + if ( splitEdges ) { + // Remove the existing edge and add the new edges. + // With overlaps, it is possible the edge was already removed. + // if ( this.edges.has(cObj.edge.key) ) this.deleteEdge(cObj.edge); + this.deleteEdge(cObj.edge); + splitEdges.forEach(e => this.addEdge(e)); } } + // Cycle to next. priorT = t; } From 023a8e328b66375519a63cba3836fe9f840d7ba8 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Wed, 21 Feb 2024 06:23:23 -0800 Subject: [PATCH 13/16] Avoid double-recusion when removing object When removing objects, we want to clean up the remaining edges by removing unneeded splits. But this need only happen 1 cycle, not for all related objects. --- scripts/pathfinding/WallTracer.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 99ccd27..8d6c118 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -650,7 +650,7 @@ export class WallTracer extends Graph { * @param {string} id Id of the edge object to remove * @param {Map>} Map of edges to remove from */ - removeObject(id) { + removeObject(id, _recurse = true) { const edges = this.objectEdges.get(id); if ( !edges || !edges.size ) return; @@ -672,30 +672,32 @@ export class WallTracer extends Graph { // For each remaining object in the object set, remove it temporarily and re-add it. // This will remove unnecessary vertices and recombine edges. - const remainingObjects = edgesArr.reduce((acc, curr) => acc = acc.union(curr.objects), new Set()); - if ( !remainingObjects.size ) return; - remainingObjects.forEach(obj => obj instanceof Wall ? this.removeWall(obj) : this.removeToken(obj)); - remainingObjects.forEach(obj => obj instanceof Wall ? this.addWall(obj) : this.addToken(obj)); + if ( _recurse ) { + const remainingObjects = edgesArr.reduce((acc, curr) => acc = acc.union(curr.objects), new Set()); + if ( !remainingObjects.size ) return; + remainingObjects.forEach(obj => obj instanceof Wall ? this.removeWall(obj.id, false) : this.removeToken(obj.id, false)); + remainingObjects.forEach(obj => obj instanceof Wall ? this.addWall(obj) : this.addToken(obj)); + } } /** * Remove all associated edges with this wall. * @param {string|Wall} wallId Id of the wall to remove, or the wall itself. */ - removeWall(wallId) { + removeWall(wallId, _recurse = true) { if ( wallId instanceof Wall ) wallId = wallId.id; this.wallIds.delete(wallId); - return this.removeObject(wallId); + return this.removeObject(wallId, _recurse); } /** * Remove all associated edges with this token. * @param {string|Token} tokenId Id of the token to remove, or the token itself. */ - removeToken(tokenId) { + removeToken(tokenId, _recurse = true) { if ( tokenId instanceof Token ) tokenId = tokenId.id; this.tokenIds.delete(tokenId); - return this.removeObject(tokenId); + return this.removeObject(tokenId, _recurse); } /** From 5c3c3127a518c9f87fd715d5a6abdda40ec3e00b Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Wed, 21 Feb 2024 06:53:29 -0800 Subject: [PATCH 14/16] Fix incorrect prorateT values --- scripts/pathfinding/WallTracer.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 8d6c118..237ecbd 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -775,16 +775,11 @@ wt.tokenEdges.forEach(s => s.forEach(e => e.draw({color: Draw.COLORS.orange}))) * - Split at .2: Segments length 2 and length 8. * - Split second segment: (.8 - .2) / .8 = .75. Split length 8 segment at .7 to get length 6. * - Segments 2, 6, 2 - * If the first split is higher, must reverse the numbers. + * Handles when the segment is split moving from 1 --> 0, indicated by secondT < firstT. */ function prorateTSplit(firstT, secondT) { - if ( secondT.almostEqual(0) ) return 0; - if ( firstT.almostEqual(0) ) return secondT; - if ( firstT.almostEqual(1) ) return secondT; - if ( secondT.almostEqual(1) ) return 1; - - const flip = secondT < firstT; - if ( flip ) [firstT, secondT] = [secondT, firstT]; - const newT = (secondT - firstT) / secondT; - return flip ? 1 - newT : newT; + if ( secondT.almostEqual(0) ) return 1; + if ( firstT.almostEqual(secondT) ) return 0; + if ( secondT < firstT ) return secondT / firstT; + return (secondT - firstT) / (1 - firstT); } From 9fa400e70476ad559471c62e9c72fa712daebc8b Mon Sep 17 00:00:00 2001 From: caewok Date: Wed, 21 Feb 2024 07:00:04 -0800 Subject: [PATCH 15/16] Update lib geometry to v0.2.18 --- scripts/geometry | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/geometry b/scripts/geometry index ea93e25..45ee911 160000 --- a/scripts/geometry +++ b/scripts/geometry @@ -1 +1 @@ -Subproject commit ea93e256589fb336194ca48238128894d50ded60 +Subproject commit 45ee911ebf2ea09bf7a9b13914c2eeab188b61c1 From f90a61b25d1dacff07dc8cd5cf22fd6c1ebe5691 Mon Sep 17 00:00:00 2001 From: caewok Date: Wed, 21 Feb 2024 07:06:25 -0800 Subject: [PATCH 16/16] Update changelog --- Changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Changelog.md b/Changelog.md index 3022c96..de12995 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,8 @@ +# 0.8.8 +Improvements to updating the scene graph. Avoid leaving unneeded vertices and split edges when a token or wall is removed. Fixes to handling overlapping edges to correctly reflect what objects make up the edge. + +Update lib geometry to v0.2.18. + # 0.8.7 ## New Features