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

View File

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

View File

@ -30,22 +30,20 @@
*/
package org.poly2tri
import collection.jcl.ArrayList
import scala.collection.mutable.ArrayBuffer
// Directed Acyclic graph (DAG)
// See "Computational Geometry", 3rd edition, by Mark de Berg et al, Chapter 6.2
class QueryGraph(var head: Node) {
def locate(s: Segment): Trapezoid = {
val sink = head.locate(s)
return sink.trapezoid
}
def locate(s: Segment) = head.locate(s).trapezoid
def followSegment(s: Segment) = {
val trapezoids = new ArrayList[Trapezoid]
val trapezoids = new ArrayBuffer[Trapezoid]
trapezoids += locate(s)
var j = 0
try {
while(s.q.x > trapezoids(j).rightPoint.x) {
if(s > trapezoids(j).rightPoint) {
trapezoids += trapezoids(j).upperRight
@ -54,6 +52,9 @@ class QueryGraph(var head: Node) {
}
j += 1
}
} catch {
case e => println("# of Trapezoids = " + j)
}
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 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)
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 pNode = new XNode(new Point(s.p.x, s.p.y, s), Sink.init(tList(0)), yNode)
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)))
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)))
if(s.left != null) {
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
import collection.jcl.ArrayList
import scala.collection.mutable.{Map, HashSet}
import scala.collection.mutable.{Map, HashSet, ArrayBuffer}
// 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)
val trapezoids = new ArrayList[Trapezoid]
val trapezoids = new ArrayBuffer[Trapezoid]
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, s, t.bottom)
@ -92,7 +91,7 @@ class TrapezoidalMap {
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(s.p, rp, t.top, s)
trapezoids += new Trapezoid(s.p, rp, s, t.bottom)
@ -121,7 +120,7 @@ class TrapezoidalMap {
val topCross = (tCross == t.top)
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(bottomCross) t.lowerLeft else new Trapezoid(lp, rp, s, t.bottom)}
@ -159,7 +158,7 @@ class TrapezoidalMap {
val topCross = (tCross == t.top)
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(bottomCross) t.lowerLeft else new Trapezoid(lp, s.q, s, t.bottom)}
trapezoids += new Trapezoid(s.q, t.rightPoint, t.top, t.bottom)
@ -189,7 +188,7 @@ class TrapezoidalMap {
}
// Create an AABB around segments
def boundingBox(segments: ArrayList[Segment]): Trapezoid = {
def boundingBox(segments: ArrayBuffer[Segment]): Trapezoid = {
var max = segments(0).p + margin
var min = segments(0).q + margin

View File

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

View File

@ -1,7 +1,7 @@
package org.poly2tri
import collection.jcl.ArrayList
import scala.collection.mutable.ArrayBuffer
object Util {
@ -30,7 +30,7 @@ object Random extends scala.util.Random {
* @param seq the sequence to shuffle
* @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
// again been defeated by the lack of higher-kinded type inference. I can
// only make it work that way if it's called like