diff --git a/README.md b/README.md index d785dbf..3dcee77 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,30 @@ The library works with Haxe 2 and 3, and has been tested on OpenFl's flash, html haxe -cp ../src -main me.nerik.poly2trihx.NekoDemo -neko build/nekoDemo.n neko build/nekoDemo.n +**How to use it :** + +```haxe +//Imports +import org.poly2tri.VisiblePolygon; +import org.poly2tri.Point; +import org.poly2tri.utils.TriangleCell; +import org.poly2tri.utils.AStar; +import org.poly2tri.utils.Funnel; + +//... +//Triangulation +var vp:VisiblePolygon = new VisiblePolygon(); +vp.addPolyline( poly ); //poly and hole = Array +vp.addPolyline( hole ); + +vp.performTriangulationOnce(); +var graph:Array = vp.getGraph(); + +//... +//Pathfinding and string pulling +var channel:Array = AStar.find(startTriangle,endTriangle,graph); //startTriangle and endTriangle = org.poly2tri.TriangleCell +var path:Array = Funnel.stringPull(startPoint,endPoint, channel); //startPoint and endPoint = org.poly2tri.Point +``` ## License diff --git a/src/org/poly2tri/Edge.hx b/src/org/poly2tri/Edge.hx index ca499b3..0685b0a 100644 --- a/src/org/poly2tri/Edge.hx +++ b/src/org/poly2tri/Edge.hx @@ -37,6 +37,10 @@ class Edge q.edge_list.push( this ); } + public function hasPoint(point:Point):Bool { + return p.equals(point) || q.equals(point); + } + public function toString() diff --git a/src/org/poly2tri/Triangle.hx b/src/org/poly2tri/Triangle.hx index f92bcd7..b1f249c 100644 --- a/src/org/poly2tri/Triangle.hx +++ b/src/org/poly2tri/Triangle.hx @@ -110,6 +110,35 @@ class Triangle } } + static public function getNotCommonVertex(t1:Triangle, t2:Triangle):Point { + return t1.points[Triangle.getNotCommonVertexIndex(t1, t2)]; + } + + + static public function getNotCommonVertexIndex(t1:Triangle, t2:Triangle):Int { + var sum:Int = 0, index:Int = -1; + if (!t2.containsPoint(t1.points[0])) { index = 0; sum++; } + if (!t2.containsPoint(t1.points[1])) { index = 1; sum++; } + if (!t2.containsPoint(t1.points[2])) { index = 2; sum++; } + if (sum != 1) { + throw "Triangle::Triangles not adjacent"; + } + return index; + } + + static public function getCommonEdge(t1:Triangle, t2:Triangle):Edge { + var commonIndexes:Array = new Array(); + for(point in t1.points) { + if (t2.containsPoint(point)) { + commonIndexes.push(point); + } + } + if (commonIndexes.length != 2) { + throw "Triangle::Triangles not adjacent"; + }; + return new Edge(commonIndexes[0], commonIndexes[1]); + } + // Optimized? @@ -125,12 +154,6 @@ class Triangle } throw "Triangle::Point not in triangle"; - // for (var n:uint = 0; n < 3; n++, no++) { - // while (no < 0) no += 3; - // while (no > 2) no -= 3; - // if (p.equals(this.points[n])) return no; - // } - // throw(new Error("Point not in triangle")); } diff --git a/src/org/poly2tri/VisiblePolygon.hx b/src/org/poly2tri/VisiblePolygon.hx index 0a0adde..3cc23c4 100644 --- a/src/org/poly2tri/VisiblePolygon.hx +++ b/src/org/poly2tri/VisiblePolygon.hx @@ -1,5 +1,7 @@ package org.poly2tri; +import org.poly2tri.Point; +import org.poly2tri.utils.TriangleCell; class VisiblePolygon { @@ -44,6 +46,17 @@ class VisiblePolygon sweep.triangulate(); } + //get graph of triangles with neighbours and all + public function getGraph():Array { + var graph:Array = new Array(); + for (t in sweepContext.triangles){ + var triangle:TriangleCell = new TriangleCell(t); + triangle.setNeighbours(graph); + graph.push(triangle); + } + return graph; + } + //returns vertices in a 3D engine-friendly, XYZ format public function getVerticesAndTriangles() { diff --git a/src/org/poly2tri/utils/AStar.hx b/src/org/poly2tri/utils/AStar.hx new file mode 100644 index 0000000..ec617d2 --- /dev/null +++ b/src/org/poly2tri/utils/AStar.hx @@ -0,0 +1,94 @@ +package org.poly2tri.utils; + +import org.poly2tri.utils.TriangleCell; + + +/** + * ... + * @author LunaFromTheMoon + */ + + +class AStar +{ + + private function new() + { + + } + + private static function getPath(last:TriangleCell):Array { + //trace the path by going backwards from goal node to start node + var path:Array = [last]; + var node:TriangleCell = last.prevCell; + while (node != null) + { + path.unshift(node); + node = node.prevCell; + } + return path; + } + + private static function cleanGraph(map:Array) { + for (node in map) { + node.prevCell = null; + node.marked = false; + node.distance = 0; + } + } + + private static function sortByHeuristic(nodes:Array, last:TriangleCell):Array { + nodes.sort( function(a:TriangleCell, b:TriangleCell):Int { + var aF:Float = a.distance + a.heuristicTo(last); + var bF:Float = b.distance + b.heuristicTo(last); + if (aF < bF) return -1; + if (aF > bF) return 1; + return 0; + } ); + return nodes; + } + + /** + * This function does very little. + * @param map The graph + * @param startNode The first node in the path + * @param goalNode The last node in the path + * @return The path if found, else null + **/ + public static function find(startNode:TriangleCell, goalNode:TriangleCell,map:Array):Array { + cleanGraph(map); + var queue:Array = new Array(); + queue.push(startNode); + while (!(queue.length == 0)) { + //obtain the first node (with smallest aproximate distance to goal) + var currentNode:TriangleCell = queue[0]; + queue.splice(0, 1); + if (!currentNode.marked) { + currentNode.marked = true; + //check if we're in goal + if (currentNode == goalNode) { + return getPath(goalNode); + } + //try to get there through the neighbours + for (next in currentNode.neighbours) { + //process only new nodes + if (!next.marked) { + var distance = currentNode.distance + (currentNode.distanceTo(next) * currentNode.weightTo(next)); + if (next.prevCell == null || distance < next.distance) { + //node not in queue, or in queue but with longer distance + next.prevCell = currentNode; + next.distance = distance; + } + //if not in queue, add + if (queue.indexOf(next) == -1) { + queue.push(next); + } + } + } + //sort queue by aproximate shortest distance to goal node + queue = sortByHeuristic(queue, goalNode); + } + } + return null; + } +} \ No newline at end of file diff --git a/src/org/poly2tri/utils/Funnel.hx b/src/org/poly2tri/utils/Funnel.hx new file mode 100644 index 0000000..cd5a81f --- /dev/null +++ b/src/org/poly2tri/utils/Funnel.hx @@ -0,0 +1,146 @@ +package org.poly2tri.utils; + +import org.poly2tri.Point; +import org.poly2tri.utils.PointUtils; +import org.poly2tri.utils.TriangleCell; + +/** + * ... + * @author LunaFromTheMoon + */ +class Funnel +{ + + private function new() + { + + } + + public static function channelToPortals(startPoint:Point, endPoint:Point, channel:Array):Array { + var portals:Array = new Array(); + portals.push(new Portal(startPoint, startPoint)); + if (channel.length >= 2) { + var firstTriangle:Triangle = channel[0].triangle; + var secondTriangle:Triangle = channel[1].triangle; + var lastTriangle:Triangle = channel[channel.length - 1].triangle; + var startVertex:Point = Triangle.getNotCommonVertex(firstTriangle, secondTriangle); + var vertexCW0:Point = startVertex; + var vertexCCW0:Point = startVertex; + for (n in 0...(channel.length-1)) { + var triangleCurrent:Triangle = channel[n + 0].triangle; + var triangleNext:Triangle = channel[n + 1].triangle; + var commonEdge:Edge = Triangle.getCommonEdge(triangleCurrent, triangleNext); + var vertexCW1:Point = triangleCurrent.pointCW (vertexCW0 ); + var vertexCCW1:Point = triangleCurrent.pointCCW(vertexCCW0); + if (!commonEdge.hasPoint(vertexCW0)) { + vertexCW0 = vertexCW1; + } + if (!commonEdge.hasPoint(vertexCCW0)) { + vertexCCW0 = vertexCCW1; + } + portals.push(new Portal(vertexCW0, vertexCCW0)); + } + } + portals.push(new Portal(endPoint, endPoint)); + return portals; + } + + public static function stringPull(startPoint:Point, endPoint:Point, channel:Array):Array { + var pts:Array = new Array(); + // Init scan state + var portals:Array = channelToPortals(startPoint, endPoint, channel); + var portalApex:Point = portals[0].left; + var portalLeft:Point = portals[0].left; + var portalRight:Point = portals[0].right; + var apexIndex:Int = 0, leftIndex:Int = 0, rightIndex:Int = 0; + + // Add start point. + pts.push(portalApex); + var i:Int = 1; + while (i < portals.length) { + var left:Point = portals[i].left; + var right:Point = portals[i].right; + // Update right vertex. + if (PointUtils.triarea2(portalApex, portalRight, right) <= 0.0) { + if (PointUtils.vequal(portalApex, portalRight) || PointUtils.triarea2(portalApex, portalLeft, right) > 0.0) { + // Tighten the funnel. + portalRight = right; + rightIndex = i; + } else { + // Right over left, insert left to path and restart scan from portal left point. + pts.push(portalLeft); + // Make current left the new apex. + portalApex = portalLeft; + apexIndex = getNextPortalIndex(portals,leftIndex,portalApex); + // Reset portal + portalLeft = portalApex; + portalRight = portalApex; + leftIndex = apexIndex; + rightIndex = apexIndex; + // Restart scan + i = apexIndex; + continue; + } + } + + // Update left vertex. + if (PointUtils.triarea2(portalApex, portalLeft, left) >= 0.0) { + if (PointUtils.vequal(portalApex, portalLeft) || PointUtils.triarea2(portalApex, portalRight, left) < 0.0) { + // Tighten the funnel. + portalLeft = left; + leftIndex = i; + } else { + // Left over right, insert right to path and restart scan from portal right point. + pts.push(portalRight); + // Make current right the new apex. + portalApex = portalRight; + apexIndex = getNextPortalIndex(portals,rightIndex,portalApex); + // Reset portal + portalLeft = portalApex; + portalRight = portalApex; + leftIndex = apexIndex; + rightIndex = apexIndex; + // Restart scan + i = apexIndex; + continue; + } + } + i++; + } + if ((pts.length == 0) || (!PointUtils.vequal(pts[pts.length - 1], portals[portals.length - 1].left))) { + // Append last point to path. + pts.push(portals[portals.length - 1].left); + } + return pts; + } + + //search next portal that doesn't contain the apex + private static function getNextPortalIndex(portals:Array, apexIndex:Int, portalApex:Point):Int { + var i:Int = apexIndex; + while (i < portals.length && portals[i].contains(portalApex)) { + i++; + } + return i; + } +} + + + +private class Portal { + public var left:Point; + public var right:Point; + + public function new(left:Point, right:Point) + { + this.left = left; + this.right = right; + } + + public function toString() { + return this.left.toString() + ".." + this.right.toString(); + } + + public function contains(point:Point) { + return this.left.equals(point) || this.right.equals(point); + } +} \ No newline at end of file diff --git a/src/org/poly2tri/utils/PointUtils.hx b/src/org/poly2tri/utils/PointUtils.hx new file mode 100644 index 0000000..f62e998 --- /dev/null +++ b/src/org/poly2tri/utils/PointUtils.hx @@ -0,0 +1,35 @@ +package org.poly2tri.utils; + +import org.poly2tri.Point; + +/** + * ... + * @author LunaFromTheMoon + */ +class PointUtils +{ + + private function new() + { + + } + + static public function vdistsqr(a:Point, b:Point):Float { + var x:Float = b.x - a.x; + var y:Float = b.y - a.y; + return Math.sqrt(x * x + y * y); + } + + static public function vequal(a:Point, b:Point):Bool { + return vdistsqr(a, b) < (0.001 * 0.001); + } + + static public function triarea2(a:Point, b:Point, c:Point):Float { + var ax:Float = b.x - a.x; + var ay:Float = b.y - a.y; + var bx:Float = c.x - a.x; + var by:Float = c.y - a.y; + return bx * ay - ax * by; + } + +} \ No newline at end of file diff --git a/src/org/poly2tri/utils/TriangleCell.hx b/src/org/poly2tri/utils/TriangleCell.hx new file mode 100644 index 0000000..5215a3f --- /dev/null +++ b/src/org/poly2tri/utils/TriangleCell.hx @@ -0,0 +1,102 @@ +package org.poly2tri.utils; + +import org.poly2tri.Point; +import org.poly2tri.Triangle; +/** + * ... + * @author LunaFromTheMoon + */ +class TriangleCell +{ + + public var triangle:Triangle; + public var neighbours:Array; + + //for A-star + public var distance:Float; + public var marked:Bool; + public var prevCell:TriangleCell; + + public function new(triangle:Triangle) + { + this.triangle = triangle; + neighbours = new Array(); + } + + public function getCenter():Point { + var points:Array = triangle.points; + var x = (points[0].x + points[1].x + points[2].x) / 3; + var y = (points[0].y + points[1].y + points[2].y) / 3; + return new Point(x, y); + } + + public function getArea() { + var points:Array = triangle.points; + var ax:Float = points[1].x - points[0].x; + var ay:Float = points[1].y - points[0].y; + var bx:Float = points[2].x - points[0].x; + var by:Float = points[2].y - points[0].y; + return bx * ay - ax * by; + } + + public function inTriangle(point:Point):Bool { + // Compute vectors + //C(2)-A(0) + var v0 = [triangle.points[2].x - triangle.points[0].x, triangle.points[2].y - triangle.points[0].y]; + //B(1)-A(0) + var v1 = [triangle.points[1].x - triangle.points[0].x, triangle.points[1].y - triangle.points[0].y]; + //the point - A(0) + var v2 = [point.x - triangle.points[0].x, point.y - triangle.points[0].y]; + + // Compute dot products = a1 * b1 + a2 * b2 + var dot00 = v0[0] * v0[0] + v0[1] * v0[1]; + var dot01 = v0[0] * v1[0] + v0[1] * v1[1]; + var dot02 = v0[0] * v2[0] + v0[1] * v2[1]; + var dot11 = v1[0] * v1[0] + v1[1] * v1[1]; + var dot12 = v1[0] * v2[0] + v1[1] * v2[1]; + + // Compute barycentric coordinates + var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); + var u = (dot11 * dot02 - dot01 * dot12) * invDenom; + var v = (dot00 * dot12 - dot01 * dot02) * invDenom; + + // Check if point is in triangle + return (u >= 0) && (v >= 0) && (u + v < 1) ; + } + + //for A-star + public function distanceTo(otherTriangle:TriangleCell):Float { + // distance from center to center + return PointUtils.vdistsqr(this.getCenter(), otherTriangle.getCenter()); + } + + public function heuristicTo(otherTriangle:TriangleCell):Float { + return distanceTo(otherTriangle); + } + + public function weightTo(otherCell:TriangleCell):Float { + return 1; + } + + //seeks neighbours in a triangle map + public function setNeighbours(trianglesGraph:Array) { + var points:Array = triangle.points; + for (tri in trianglesGraph) { + var commonVertices:Int = 0; + var triPoints:Array = tri.triangle.points; + for (i in 0...3) { + for (j in 0...3) { + if (points[i].x == triPoints[j].x && points[i].y == triPoints[j].y) { + commonVertices++; + break; + } + } + } + if (commonVertices == 2) { + //neighbours! + neighbours.push(tri); + tri.neighbours.push(this); + } + } + } +} \ No newline at end of file