fixed nemerical robustness issue in triangulation

This commit is contained in:
zzzrrr 2009-07-15 12:35:14 -04:00
parent 2752b9b021
commit e484852d39
6 changed files with 89 additions and 70 deletions

View File

@ -39,8 +39,13 @@ class MonotoneMountain {
var size = 0 var size = 0
val convexPoints = new Queue[Point] val convexPoints = new Queue[Point]
// Monotone mountain points
val monoPoly = new ArrayBuffer[Point]
// Triangles that constitute the mountain
val triangles = new ArrayBuffer[Array[Point]] val triangles = new ArrayBuffer[Array[Point]]
var angle = 0f
// Append a point to the list // Append a point to the list
def +=(point: Point) { def +=(point: Point) {
size match { size match {
@ -71,6 +76,9 @@ class MonotoneMountain {
// See "Computational Geometry in C", 2nd edition, by Joseph O'Rourke, page 52 // See "Computational Geometry in C", 2nd edition, by Joseph O'Rourke, page 52
def triangulate { def triangulate {
// create monotone polygon - for dubug purposes
genMonoPoly
if(size == 3) { if(size == 3) {
lastTriangle lastTriangle
} else { } else {
@ -97,28 +105,29 @@ class MonotoneMountain {
if(c.prev != null && convex(c)) convexPoints.enqueue(c) if(c.prev != null && convex(c)) convexPoints.enqueue(c)
} }
assert(size <= 3, "Triangulation bug")
if(size == 3)lastTriangle if(size == 3)lastTriangle
} }
} }
// Return the monotone polygon // Return the monotone polygon
def monoPoly: Array[Point] = { private def genMonoPoly {
val poly = new Array[Point](size)
var i = 0
var p = head var p = head
while(p != null) { while(p != null) {
poly(i) = p monoPoly += p
p = p.next p = p.next
i += 1
} }
poly
} }
// Determines if the inslide angle between edge v2-v3 and edge v2-v1 is convex // Determines if the inslide angle between edge v2-v3 and edge v2-v1 is convex
private def convex(p: Point) = { private def convex(p: Point) = {
val a = (p.next - p) val a = (p.next - p)
val b = (p.prev - p) val b = (p.prev - p)
var angle = Math.atan2(b.y,b.x).toFloat - Math.atan2(a.y,a.x).toFloat angle = Math.atan2(b.y,b.x).toFloat - Math.atan2(a.y,a.x).toFloat
if(angle < 0) while(angle < -Math.Pi) angle += Math.Pi.toFloat
if(angle > 0) while(angle > Math.Pi) angle -= Math.Pi.toFloat
// For numerical robustness....
angle = 0.01f * Math.round( angle * 10.0f)
if(p.y >= head.y) { if(p.y >= head.y) {
(angle < 0) (angle < 0)
} else { } else {

View File

@ -38,7 +38,7 @@ package org.poly2tri
import org.newdawn.slick.{BasicGame, GameContainer, Graphics, Color, AppGameContainer} import org.newdawn.slick.{BasicGame, GameContainer, Graphics, Color, AppGameContainer}
import org.newdawn.slick.geom.{Polygon, Circle} import org.newdawn.slick.geom.{Polygon, Circle}
import collection.jcl.ArrayList import scala.collection.mutable.ArrayBuffer
// TODO: Lots of documentation! // TODO: Lots of documentation!
@ -61,9 +61,6 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
var drawMap = false var drawMap = false
def init(container: GameContainer) { def init(container: GameContainer) {
// TODO: Add text file point loader
//poly
//poly2
snake snake
} }
@ -79,7 +76,7 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
val yellow = new Color(1f, 1f, 0f) val yellow = new Color(1f, 1f, 0f)
if(debug) { if(debug) {
val draw = if(drawMap) tesselator.allTrapezoids else tesselator.trapezoids val draw = if(drawMap) tesselator.trapezoidMap else tesselator.trapezoids
for(t <- draw) { for(t <- draw) {
val polygon = new Polygon() val polygon = new Polygon()
for(v <- t.vertices) { for(v <- t.vertices) {
@ -88,32 +85,37 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
val lCirc = new Circle(t.leftPoint.x, t.leftPoint.y, 4) val lCirc = new Circle(t.leftPoint.x, t.leftPoint.y, 4)
g.setColor(blue); g.draw(lCirc); g.fill(lCirc) g.setColor(blue); g.draw(lCirc); g.fill(lCirc)
val rCirc = new Circle(t.rightPoint.x, t.rightPoint.y, 6) val rCirc = new Circle(t.rightPoint.x, t.rightPoint.y, 6)
g.setColor(yellow); g.draw(rCirc); g.fill(rCirc) //g.setColor(yellow); g.draw(rCirc); g.fill(rCirc)
g.setColor(red) g.setColor(red)
g.draw(polygon) g.draw(polygon)
} }
} }
var i = 0 if(!debug) {
for(x <- tesselator.xMonoPoly) { for(t <- tesselator.triangles) {
var t = x.triangles val triangle = new Polygon
var j = 0
for(t <- x.triangles) {
val triangle = new Polygon()
t.foreach(p => triangle.addPoint(p.x, p.y)) t.foreach(p => triangle.addPoint(p.x, p.y))
val color = if(i == 0 && j == 3) blue else green g.setColor(red)
g.setColor(color)
g.draw(triangle) g.draw(triangle)
j += 1
} }
i += 1 } else {
for(mp <- tesselator.monoPolies) {
val poly = new Polygon
mp.foreach(p => poly.addPoint(p.x, p.y))
g.setColor(yellow)
g.draw(poly)
}
} }
} }
override def keyPressed(key:Int, c:Char) { override def keyPressed(key:Int, c:Char) {
if(key == 1) quit = true if(key == 1) quit = true
if(key == 57) debug = !debug if(key == 57) debug = !debug
if(c == 'm') drawMap = !drawMap if(c == 'm') drawMap = !drawMap
if(c == '1') poly
if(c == '2') snake
if(c == '3') star
} }
// Test #1 // Test #1
@ -127,7 +129,7 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
val p5 = new Point(400,300)*scale val p5 = new Point(400,300)*scale
val p6 = new Point(650,250)*scale val p6 = new Point(650,250)*scale
val segments = new ArrayList[Segment] val segments = new ArrayBuffer[Segment]
segments += new Segment(p1, p2) segments += new Segment(p1, p2)
segments += new Segment(p3, p4) segments += new Segment(p3, p4)
segments += new Segment(p1, p3) segments += new Segment(p1, p3)
@ -139,7 +141,7 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
tesselator.process tesselator.process
} }
def poly2 { def star {
val scale = 1.0f val scale = 1.0f
val displace = 0f val displace = 0f
@ -154,7 +156,7 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
val p9 = new Point(231,161)*scale+displace val p9 = new Point(231,161)*scale+displace
val p10 = new Point(321,161)*scale+displace val p10 = new Point(321,161)*scale+displace
val segments = new ArrayList[Segment] val segments = new ArrayBuffer[Segment]
segments += new Segment(p1, p2) segments += new Segment(p1, p2)
segments += new Segment(p2, p3) segments += new Segment(p2, p3)
segments += new Segment(p3, p4) segments += new Segment(p3, p4)
@ -187,7 +189,7 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
val p11 = new Point(1,20)*scale+displace val p11 = new Point(1,20)*scale+displace
val p12 = new Point(1,10)*scale+displace val p12 = new Point(1,10)*scale+displace
val segments = new ArrayList[Segment] val segments = new ArrayBuffer[Segment]
segments += new Segment(p1, p2) segments += new Segment(p1, p2)
segments += new Segment(p2, p3) segments += new Segment(p2, p3)
segments += new Segment(p3, p4) segments += new Segment(p3, p4)

View File

@ -30,29 +30,30 @@
*/ */
package org.poly2tri package org.poly2tri
import collection.jcl.ArrayList import scala.collection.mutable.ArrayBuffer
// Directed Acyclic graph (DAG) // Directed Acyclic graph (DAG)
// See "Computational Geometry", 3rd edition, by Mark de Berg et al, Chapter 6.2 // See "Computational Geometry", 3rd edition, by Mark de Berg et al, Chapter 6.2
class QueryGraph(var head: Node) { class QueryGraph(var head: Node) {
def locate(s: Segment): Trapezoid = { def locate(s: Segment) = head.locate(s).trapezoid
val sink = head.locate(s)
return sink.trapezoid
}
def followSegment(s: Segment) = { def followSegment(s: Segment) = {
val trapezoids = new ArrayList[Trapezoid] val trapezoids = new ArrayBuffer[Trapezoid]
trapezoids += locate(s) trapezoids += locate(s)
var j = 0 var j = 0
while(s.q.x > trapezoids(j).rightPoint.x) { try {
if(s > trapezoids(j).rightPoint) { while(s.q.x > trapezoids(j).rightPoint.x) {
trapezoids += trapezoids(j).upperRight if(s > trapezoids(j).rightPoint) {
} else { trapezoids += trapezoids(j).upperRight
trapezoids += trapezoids(j).lowerRight } else {
} trapezoids += trapezoids(j).lowerRight
j += 1 }
j += 1
}
} catch {
case e => println("# of Trapezoids = " + j)
} }
trapezoids trapezoids
} }
@ -65,25 +66,25 @@ class QueryGraph(var head: Node) {
} }
} }
def case1(sink: Sink, s: Segment, tList: ArrayList[Trapezoid]) { def case1(sink: Sink, s: Segment, tList: ArrayBuffer[Trapezoid]) {
val yNode = new YNode(s, Sink.init(tList(1)), Sink.init(tList(2))) val yNode = new YNode(s, Sink.init(tList(1)), Sink.init(tList(2)))
val qNode = new XNode(new Point(s.q.x, s.q.y, s), yNode, Sink.init(tList(3))) val qNode = new XNode(new Point(s.q.x, s.q.y, s), yNode, Sink.init(tList(3)))
val pNode = new XNode(new Point(s.p.x, s.p.y, s), Sink.init(tList(0)), qNode) val pNode = new XNode(new Point(s.p.x, s.p.y, s), Sink.init(tList(0)), qNode)
replace(sink, pNode) replace(sink, pNode)
} }
def case2(sink: Sink, s: Segment, tList: ArrayList[Trapezoid]) { def case2(sink: Sink, s: Segment, tList: ArrayBuffer[Trapezoid]) {
val yNode = new YNode(s, Sink.init(tList(1)), Sink.init(tList(2))) val yNode = new YNode(s, Sink.init(tList(1)), Sink.init(tList(2)))
val pNode = new XNode(new Point(s.p.x, s.p.y, s), Sink.init(tList(0)), yNode) val pNode = new XNode(new Point(s.p.x, s.p.y, s), Sink.init(tList(0)), yNode)
replace(sink, pNode) replace(sink, pNode)
} }
def case3(sink: Sink, s: Segment, tList: ArrayList[Trapezoid]) { def case3(sink: Sink, s: Segment, tList: ArrayBuffer[Trapezoid]) {
val yNode = new YNode(s, Sink.init(tList(0)), Sink.init(tList(1))) val yNode = new YNode(s, Sink.init(tList(0)), Sink.init(tList(1)))
replace(sink, yNode) replace(sink, yNode)
} }
def case4(sink: Sink, s: Segment, tList: ArrayList[Trapezoid]) { def case4(sink: Sink, s: Segment, tList: ArrayBuffer[Trapezoid]) {
val yNode = new YNode(s, Sink.init(tList(0)), Sink.init(tList(1))) val yNode = new YNode(s, Sink.init(tList(0)), Sink.init(tList(1)))
if(s.left != null) { if(s.left != null) {
val pNode = new XNode(new Point(s.p.x, s.p.y, s), Sink.init(s.left), yNode) val pNode = new XNode(new Point(s.p.x, s.p.y, s), Sink.init(s.left), yNode)

View File

@ -30,8 +30,7 @@
*/ */
package org.poly2tri package org.poly2tri
import collection.jcl.ArrayList import scala.collection.mutable.{Map, HashSet, ArrayBuffer}
import scala.collection.mutable.{Map, HashSet}
// See "Computational Geometry", 3rd edition, by Mark de Berg et al, Chapter 6.2 // See "Computational Geometry", 3rd edition, by Mark de Berg et al, Chapter 6.2
@ -68,7 +67,7 @@ class TrapezoidalMap {
assert(s.p.x != s.q.x) assert(s.p.x != s.q.x)
val trapezoids = new ArrayList[Trapezoid] val trapezoids = new ArrayBuffer[Trapezoid]
trapezoids += new Trapezoid(t.leftPoint, s.p, t.top, t.bottom) trapezoids += new Trapezoid(t.leftPoint, s.p, t.top, t.bottom)
trapezoids += new Trapezoid(s.p, s.q, t.top, s) trapezoids += new Trapezoid(s.p, s.q, t.top, s)
trapezoids += new Trapezoid(s.p, s.q, s, t.bottom) trapezoids += new Trapezoid(s.p, s.q, s, t.bottom)
@ -92,7 +91,7 @@ class TrapezoidalMap {
val rp = if(s.q.x == t.rightPoint.x) s.q else t.rightPoint val rp = if(s.q.x == t.rightPoint.x) s.q else t.rightPoint
val trapezoids = new ArrayList[Trapezoid] val trapezoids = new ArrayBuffer[Trapezoid]
trapezoids += new Trapezoid(t.leftPoint, s.p, t.top, t.bottom) trapezoids += new Trapezoid(t.leftPoint, s.p, t.top, t.bottom)
trapezoids += new Trapezoid(s.p, rp, t.top, s) trapezoids += new Trapezoid(s.p, rp, t.top, s)
trapezoids += new Trapezoid(s.p, rp, s, t.bottom) trapezoids += new Trapezoid(s.p, rp, s, t.bottom)
@ -121,7 +120,7 @@ class TrapezoidalMap {
val topCross = (tCross == t.top) val topCross = (tCross == t.top)
val bottomCross = (bCross == t.bottom) val bottomCross = (bCross == t.bottom)
val trapezoids = new ArrayList[Trapezoid] val trapezoids = new ArrayBuffer[Trapezoid]
trapezoids += {if(topCross) t.upperLeft else new Trapezoid(lp, rp, t.top, s)} trapezoids += {if(topCross) t.upperLeft else new Trapezoid(lp, rp, t.top, s)}
trapezoids += {if(bottomCross) t.lowerLeft else new Trapezoid(lp, rp, s, t.bottom)} trapezoids += {if(bottomCross) t.lowerLeft else new Trapezoid(lp, rp, s, t.bottom)}
@ -159,7 +158,7 @@ class TrapezoidalMap {
val topCross = (tCross == t.top) val topCross = (tCross == t.top)
val bottomCross = (bCross == t.bottom) val bottomCross = (bCross == t.bottom)
val trapezoids = new ArrayList[Trapezoid] val trapezoids = new ArrayBuffer[Trapezoid]
trapezoids += {if(topCross) t.upperLeft else new Trapezoid(lp, s.q, t.top, s)} trapezoids += {if(topCross) t.upperLeft else new Trapezoid(lp, s.q, t.top, s)}
trapezoids += {if(bottomCross) t.lowerLeft else new Trapezoid(lp, s.q, s, t.bottom)} trapezoids += {if(bottomCross) t.lowerLeft else new Trapezoid(lp, s.q, s, t.bottom)}
trapezoids += new Trapezoid(s.q, t.rightPoint, t.top, t.bottom) trapezoids += new Trapezoid(s.q, t.rightPoint, t.top, t.bottom)
@ -189,7 +188,7 @@ class TrapezoidalMap {
} }
// Create an AABB around segments // Create an AABB around segments
def boundingBox(segments: ArrayList[Segment]): Trapezoid = { def boundingBox(segments: ArrayBuffer[Segment]): Trapezoid = {
var max = segments(0).p + margin var max = segments(0).p + margin
var min = segments(0).q + margin var min = segments(0).q + margin

View File

@ -30,17 +30,16 @@
*/ */
package org.poly2tri package org.poly2tri
import collection.jcl.ArrayList import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.{HashSet, Map, Stack, ListBuffer}
// Based on Raimund Seidel's paper "A simple and fast incremental randomized // Based on Raimund Seidel's paper "A simple and fast incremental randomized
// algorithm for computing trapezoidal decompositions and for triangulating polygons" // algorithm for computing trapezoidal decompositions and for triangulating polygons"
class Triangulator(var segments: ArrayList[Segment]) { class Triangulator(var segments: ArrayBuffer[Segment]) {
// Trapezoid decomposition list // Trapezoid decomposition list
var trapezoids : ArrayList[Trapezoid] = null var trapezoids : ArrayBuffer[Trapezoid] = null
// Triangle decomposition list // Triangle decomposition list
// var triangles: ArrayList[Triangle] = null var triangles = new ArrayBuffer[Array[Point]]
// Build the trapezoidal map and query graph // Build the trapezoidal map and query graph
def process { def process {
@ -50,7 +49,7 @@ class Triangulator(var segments: ArrayList[Segment]) {
// Remove trapezoids from trapezoidal Map // Remove trapezoids from trapezoidal Map
traps.foreach(trapezoidalMap.remove) traps.foreach(trapezoidalMap.remove)
for(t <- traps) { for(t <- traps) {
var tList: ArrayList[Trapezoid] = null var tList: ArrayBuffer[Trapezoid] = null
val containsP = t.contains(s.p) val containsP = t.contains(s.p)
val containsQ = t.contains(s.q) val containsQ = t.contains(s.q)
if(containsP && containsQ) { if(containsP && containsQ) {
@ -77,39 +76,48 @@ class Triangulator(var segments: ArrayList[Segment]) {
} }
trapezoids = trim trapezoids = trim
createMountains createMountains
// Extract all the triangles into a single list
for(i <- 0 until xMonoPoly.size)
for(t <- xMonoPoly(i).triangles)
triangles += t
} }
def allTrapezoids = trapezoidalMap.map // For debugging
def trapezoidMap = trapezoidalMap.map
// Monotone polygons - these are monotone mountains
def monoPolies: ArrayBuffer[ArrayBuffer[Point]] = {
val polies = new ArrayBuffer[ArrayBuffer[Point]]
for(i <- 0 until xMonoPoly.size)
polies += xMonoPoly(i).monoPoly
return polies
}
// Initialize trapezoidal map and query structure // Initialize trapezoidal map and query structure
private val trapezoidalMap = new TrapezoidalMap private val trapezoidalMap = new TrapezoidalMap
private val boundingBox = trapezoidalMap.boundingBox(segments) private val boundingBox = trapezoidalMap.boundingBox(segments)
private val queryGraph = new QueryGraph(new Sink(boundingBox)) private val queryGraph = new QueryGraph(new Sink(boundingBox))
private val xMonoPoly = new ArrayBuffer[MonotoneMountain]
val xMonoPoly = new ArrayList[MonotoneMountain]
segments = orderSegments segments = orderSegments
// Build a list of x-monotone mountains // Build a list of x-monotone mountains
private def createMountains { private def createMountains {
for(s <- segments) { for(s <- segments) {
if(s.mPoints.size > 0) {
val mountain = new MonotoneMountain val mountain = new MonotoneMountain
val k = Util.msort((x: Point, y: Point) => x < y)(s.mPoints.toList) val k = Util.msort((x: Point, y: Point) => x < y)(s.mPoints.toList)
val points = s.p :: k ::: List(s.q) val points = s.p :: k ::: List(s.q)
points.foreach(p => mountain += p.clone) points.foreach(p => mountain += p.clone)
if(mountain.size > 2) { if(mountain.size > 2) {
mountain.triangulate mountain.triangulate
//mountain.monoPoly
xMonoPoly += mountain xMonoPoly += mountain
} }
}
} }
} }
// Trim off the extraneous trapezoids surrounding the polygon // Trim off the extraneous trapezoids surrounding the polygon
private def trim = { private def trim = {
val traps = new ArrayList[Trapezoid] val traps = new ArrayBuffer[Trapezoid]
// Mark outside trapezoids // Mark outside trapezoids
for(t <- trapezoidalMap.map) { for(t <- trapezoidalMap.map) {
if(t.top == boundingBox.top || t.bottom == boundingBox.bottom) { if(t.top == boundingBox.top || t.bottom == boundingBox.bottom) {
@ -127,7 +135,7 @@ class Triangulator(var segments: ArrayList[Segment]) {
private def orderSegments = { private def orderSegments = {
// Ignore vertical segments! // Ignore vertical segments!
val segs = new ArrayList[Segment] val segs = new ArrayBuffer[Segment]
for(s <- segments) { for(s <- segments) {
// Point p must be to the left of point q // Point p must be to the left of point q
if(s.p.x > s.q.x) { if(s.p.x > s.q.x) {

View File

@ -1,7 +1,7 @@
package org.poly2tri package org.poly2tri
import collection.jcl.ArrayList import scala.collection.mutable.ArrayBuffer
object Util { object Util {
@ -30,7 +30,7 @@ object Random extends scala.util.Random {
* @param seq the sequence to shuffle * @param seq the sequence to shuffle
* @return the shuffled sequence * @return the shuffled sequence
*/ */
def shuffle[T](buf: ArrayList[T]): ArrayList[T] = { def shuffle[T](buf: ArrayBuffer[T]): ArrayBuffer[T] = {
// It would be better if this preserved the shape of its container, but I have // It would be better if this preserved the shape of its container, but I have
// again been defeated by the lack of higher-kinded type inference. I can // again been defeated by the lack of higher-kinded type inference. I can
// only make it work that way if it's called like // only make it work that way if it's called like