mirror of
https://github.com/jhasse/poly2tri.git
synced 2024-11-30 01:03:30 +01:00
updated demo framework
This commit is contained in:
parent
1f418be366
commit
6054e161da
@ -46,7 +46,7 @@ import cdt.CDT
|
|||||||
object Poly2Tri {
|
object Poly2Tri {
|
||||||
|
|
||||||
def main(args: Array[String]) {
|
def main(args: Array[String]) {
|
||||||
val container = new AppGameContainer(new Poly2TriDemo())
|
val container = new AppGameContainer(new Poly2TriDemo())
|
||||||
container.setDisplayMode(800,600,false)
|
container.setDisplayMode(800,600,false)
|
||||||
container.start()
|
container.start()
|
||||||
}
|
}
|
||||||
@ -55,6 +55,12 @@ object Poly2Tri {
|
|||||||
|
|
||||||
class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
||||||
|
|
||||||
|
object Algo extends Enumeration {
|
||||||
|
type Algo = Value
|
||||||
|
val CDT, Seidel, EarClip = Value
|
||||||
|
}
|
||||||
|
import Algo._
|
||||||
|
|
||||||
// Sedidel Triangulator
|
// Sedidel Triangulator
|
||||||
var seidel: Triangulator = null
|
var seidel: Triangulator = null
|
||||||
var segments: ArrayBuffer[Segment] = null
|
var segments: ArrayBuffer[Segment] = null
|
||||||
@ -68,17 +74,10 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
// Sweep Line Constraied Delauney Triangulator (CDT)
|
// Sweep Line Constraied Delauney Triangulator (CDT)
|
||||||
var slCDT: CDT = null
|
var slCDT: CDT = null
|
||||||
|
|
||||||
var polyX: ArrayBuffer[Float] = null
|
|
||||||
var polyY: ArrayBuffer[Float] = null
|
|
||||||
|
|
||||||
var quit = false
|
var quit = false
|
||||||
var debug = false
|
|
||||||
var drawMap = false
|
var drawMap = false
|
||||||
var drawSegs = true
|
var drawSegs = true
|
||||||
var hiLighter = 0
|
var drawCDTMesh = false
|
||||||
var drawEarClip = false
|
|
||||||
var drawCDT = true
|
|
||||||
var drawcdtMesh = false
|
|
||||||
|
|
||||||
val nazcaMonkey = "data/nazca_monkey.dat"
|
val nazcaMonkey = "data/nazca_monkey.dat"
|
||||||
val nazcaHeron = "data/nazca_heron_old.dat"
|
val nazcaHeron = "data/nazca_heron_old.dat"
|
||||||
@ -92,6 +91,8 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
|
|
||||||
var currentModel = dude
|
var currentModel = dude
|
||||||
var doCDT = true
|
var doCDT = true
|
||||||
|
// The current algorithm
|
||||||
|
var algo = CDT
|
||||||
|
|
||||||
var mouseButton = 0
|
var mouseButton = 0
|
||||||
var mousePressed = false
|
var mousePressed = false
|
||||||
@ -115,129 +116,107 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
|
|
||||||
def render(container: GameContainer, g: Graphics) {
|
def render(container: GameContainer, g: Graphics) {
|
||||||
|
|
||||||
g.drawString("'1-9' to cycle models, mouse to pan & zoom", 10, 520)
|
g.drawString("'1-9' to cycle models, mouse to pan & zoom", 10, 522)
|
||||||
g.drawString("'SPACE' to show Seidel debug info", 10, 532)
|
g.drawString("'c,s,e' to switch CDT / Seidel / EarClip algos", 10, 537)
|
||||||
g.drawString("'m' to show trapezoidal map (Seidel debug mode)", 10, 544)
|
g.drawString("'t' to show trapezoidal map (Seidel)", 10, 552)
|
||||||
g.drawString("'e' to switch Seidel / EarClip", 10, 556)
|
g.drawString("'m' to show triangle mesh (CDT)", 10, 567)
|
||||||
g.drawString("'d' to switch CDT / Seidel", 10, 568)
|
g.drawString("'d' to draw edges", 10, 582)
|
||||||
g.drawString("'c' to show CDT mesh, 's' to draw edges", 10, 580)
|
|
||||||
|
|
||||||
g.scale(scaleFactor, scaleFactor)
|
g.scale(scaleFactor, scaleFactor)
|
||||||
g.translate(deltaX, deltaY)
|
g.translate(deltaX, deltaY)
|
||||||
|
|
||||||
val red = new Color(1f, 0f,0.0f)
|
val red = new Color(1f, 0f,0.0f)
|
||||||
val blue = new Color(0f, 0f, 1f)
|
val blue = new Color(0f, 0f, 1f)
|
||||||
val green = new Color(0f, 1f, 0f)
|
val green = new Color(0f, 1f, 0f)
|
||||||
val yellow = new Color(1f, 1f, 0f)
|
val yellow = new Color(1f, 1f, 0f)
|
||||||
|
|
||||||
if(debug) {
|
algo match {
|
||||||
val draw = if(drawMap) seidel.trapezoidMap else seidel.trapezoids
|
|
||||||
for(t <- draw) {
|
|
||||||
val polygon = new Polygon()
|
|
||||||
for(v <- t.vertices) {
|
|
||||||
polygon.addPoint(v.x, v.y)
|
|
||||||
}
|
|
||||||
if(!drawMap) {
|
|
||||||
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+5, t.rightPoint.y, 4)
|
|
||||||
g.setColor(yellow); g.draw(rCirc); g.fill(rCirc)
|
|
||||||
}
|
|
||||||
g.setColor(red)
|
|
||||||
g.draw(polygon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!debug && !drawEarClip && !drawCDT) {
|
case Algo.Seidel => {
|
||||||
var i = 0
|
for(t <- seidel.polygons) {
|
||||||
for(t <- seidel.polygons) {
|
val poly = new Polygon
|
||||||
val poly = new Polygon
|
t.foreach(p => poly.addPoint(p.x, p.y))
|
||||||
t.foreach(p => poly.addPoint(p.x, p.y))
|
g.setColor(red)
|
||||||
g.setColor(red)
|
g.draw(poly)
|
||||||
g.draw(poly)
|
}
|
||||||
|
if (drawMap) {
|
||||||
|
for(t <- seidel.trapezoidMap) {
|
||||||
|
val polygon = new Polygon()
|
||||||
|
for(v <- t.vertices) {
|
||||||
|
polygon.addPoint(v.x, v.y)
|
||||||
|
}
|
||||||
|
g.setColor(red)
|
||||||
|
g.draw(polygon)
|
||||||
|
}/*
|
||||||
|
for(mp <- seidel.monoPolies) {
|
||||||
|
val poly = new Polygon
|
||||||
|
mp.foreach(p => poly.addPoint(p.x, p.y))
|
||||||
|
g.setColor(yellow)
|
||||||
|
g.draw(poly)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (debug && drawMap && !drawEarClip){
|
|
||||||
for(mp <- seidel.monoPolies) {
|
case Algo.EarClip => {
|
||||||
val poly = new Polygon
|
earClipResults.foreach(t => {
|
||||||
mp.foreach(p => poly.addPoint(p.x, p.y))
|
val triangle = new Polygon
|
||||||
g.setColor(yellow)
|
triangle.addPoint(t.x(0), t.y(0))
|
||||||
g.draw(poly)
|
triangle.addPoint(t.x(1), t.y(1))
|
||||||
|
triangle.addPoint(t.x(2), t.y(2))
|
||||||
|
g.setColor(red)
|
||||||
|
g.draw(triangle)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case Algo.CDT => {
|
||||||
|
val draw = if(drawCDTMesh) slCDT.triangleMesh else slCDT.triangles
|
||||||
|
draw.foreach( t => {
|
||||||
|
val triangle = new Polygon
|
||||||
|
triangle.addPoint(t.points(0).x, t.points(0).y)
|
||||||
|
triangle.addPoint(t.points(1).x, t.points(1).y)
|
||||||
|
triangle.addPoint(t.points(2).x, t.points(2).y)
|
||||||
|
g.setColor(red)
|
||||||
|
g.draw(triangle)
|
||||||
|
})
|
||||||
|
|
||||||
|
slCDT.debugTriangles.foreach( t => {
|
||||||
|
val triangle = new Polygon
|
||||||
|
triangle.addPoint(t.points(0).x, t.points(0).y)
|
||||||
|
triangle.addPoint(t.points(1).x, t.points(1).y)
|
||||||
|
triangle.addPoint(t.points(2).x, t.points(2).y)
|
||||||
|
g.setColor(blue)
|
||||||
|
g.draw(triangle)
|
||||||
|
})
|
||||||
|
|
||||||
|
for(i <- 0 until slCDT.cList.size) {
|
||||||
|
val circ = new Circle(slCDT.cList(i).x, slCDT.cList(i).y, 0.5f)
|
||||||
|
g.setColor(blue); g.draw(circ); g.fill(circ)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
|
||||||
|
if(drawSegs) {
|
||||||
|
g.setColor(green)
|
||||||
|
for(i <- 0 until segments.size) {
|
||||||
|
val s = segments(i)
|
||||||
|
g.drawLine(s.p.x,s.p.y,s.q.x,s.q.y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(drawEarClip)
|
if(currentModel == "data/dude.dat" && drawSegs) {
|
||||||
earClipResults.foreach(t => {
|
g.setColor(green)
|
||||||
val triangle = new Polygon
|
for(i <- 0 until chestSegs.size) {
|
||||||
triangle.addPoint(t.x(0), t.y(0))
|
val s = chestSegs(i)
|
||||||
triangle.addPoint(t.x(1), t.y(1))
|
g.drawLine(s.p.x,s.p.y,s.q.x,s.q.y)
|
||||||
triangle.addPoint(t.x(2), t.y(2))
|
}
|
||||||
g.setColor(red)
|
for(i <- 0 until headSegs.size) {
|
||||||
g.draw(triangle)
|
val s = headSegs(i)
|
||||||
})
|
g.drawLine(s.p.x,s.p.y,s.q.x,s.q.y)
|
||||||
|
}
|
||||||
if(drawCDT) {
|
}
|
||||||
|
|
||||||
val draw = if(drawcdtMesh) slCDT.triangleMesh else slCDT.triangles
|
|
||||||
|
|
||||||
draw.foreach( t => {
|
|
||||||
if(false) {
|
|
||||||
for(i <- 0 to 2) {
|
|
||||||
val s = t.points(i)
|
|
||||||
val e = if(i == 2) t.points(0) else t.points(i + 1)
|
|
||||||
val j = if(i == 0) 2 else if(i == 1) 0 else 1
|
|
||||||
if(t.edges(j))
|
|
||||||
g.setColor(yellow)
|
|
||||||
else
|
|
||||||
g.setColor(red)
|
|
||||||
g.drawLine(s.x,s.y,e.x,e.y)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val triangle = new Polygon
|
|
||||||
triangle.addPoint(t.points(0).x, t.points(0).y)
|
|
||||||
triangle.addPoint(t.points(1).x, t.points(1).y)
|
|
||||||
triangle.addPoint(t.points(2).x, t.points(2).y)
|
|
||||||
g.setColor(red)
|
|
||||||
g.draw(triangle)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
slCDT.debugTriangles.foreach( t => {
|
|
||||||
val triangle = new Polygon
|
|
||||||
triangle.addPoint(t.points(0).x, t.points(0).y)
|
|
||||||
triangle.addPoint(t.points(1).x, t.points(1).y)
|
|
||||||
triangle.addPoint(t.points(2).x, t.points(2).y)
|
|
||||||
g.setColor(blue)
|
|
||||||
g.draw(triangle)
|
|
||||||
})
|
|
||||||
|
|
||||||
//slCDT.cList.foreach(c => {
|
|
||||||
for(i <- 0 until slCDT.cList.size) {
|
|
||||||
val circ = new Circle(slCDT.cList(i).x, slCDT.cList(i).y, 0.5f)
|
|
||||||
g.setColor(blue); g.draw(circ); g.fill(circ)
|
|
||||||
}
|
|
||||||
//})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(drawSegs) {
|
|
||||||
g.setColor(green)
|
|
||||||
for(i <- 0 until segments.size) {
|
|
||||||
val s = segments(i)
|
|
||||||
g.drawLine(s.p.x,s.p.y,s.q.x,s.q.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(currentModel == "data/dude.dat" && drawSegs) {
|
|
||||||
g.setColor(green)
|
|
||||||
for(i <- 0 until chestSegs.size) {
|
|
||||||
val s = chestSegs(i)
|
|
||||||
g.drawLine(s.p.x,s.p.y,s.q.x,s.q.y)
|
|
||||||
}
|
|
||||||
for(i <- 0 until headSegs.size) {
|
|
||||||
val s = headSegs(i)
|
|
||||||
g.drawLine(s.p.x,s.p.y,s.q.x,s.q.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,6 +225,7 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
* @param p The screen location that the mouse is down at.
|
* @param p The screen location that the mouse is down at.
|
||||||
*/
|
*/
|
||||||
override def mousePressed(b: Int, x: Int, y: Int) {
|
override def mousePressed(b: Int, x: Int, y: Int) {
|
||||||
|
|
||||||
mouseButton = b
|
mouseButton = b
|
||||||
mousePressed = true
|
mousePressed = true
|
||||||
mousePosOld = mousePos
|
mousePosOld = mousePos
|
||||||
@ -278,7 +258,7 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
mousePosOld = mousePos
|
mousePosOld = mousePos
|
||||||
mousePos = Point(x,y)
|
mousePos = Point(x,y)
|
||||||
if(mousePressed) {
|
if(mousePressed) {
|
||||||
deltaX += mousePos.x - mousePosOld.x
|
deltaX += mousePos.x - mousePosOld.x
|
||||||
deltaY += mousePos.y - mousePosOld.y
|
deltaY += mousePos.y - mousePosOld.y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -296,23 +276,7 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
|
|
||||||
// ESC
|
// ESC
|
||||||
if(key == 1) quit = true
|
if(key == 1) quit = true
|
||||||
// SPACE
|
|
||||||
if(key == 57) debug = !debug
|
|
||||||
// UP
|
|
||||||
if(key == 200) {
|
|
||||||
hiLighter += 1
|
|
||||||
if (hiLighter == seidel.polygons.size)
|
|
||||||
hiLighter = 0
|
|
||||||
}
|
|
||||||
// DOWN
|
|
||||||
if(key == 208) {
|
|
||||||
hiLighter -= 1
|
|
||||||
if (hiLighter == -1)
|
|
||||||
hiLighter = seidel.polygons.size-1
|
|
||||||
}
|
|
||||||
|
|
||||||
if(c == 'm') drawMap = !drawMap
|
|
||||||
if(c == 'd') drawCDT = !drawCDT
|
|
||||||
if(c == '1') selectModel(nazcaMonkey)
|
if(c == '1') selectModel(nazcaMonkey)
|
||||||
if(c == '2') selectModel(bird)
|
if(c == '2') selectModel(bird)
|
||||||
if(c == '3') selectModel(strange)
|
if(c == '3') selectModel(strange)
|
||||||
@ -322,9 +286,16 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
if(c == '7') selectModel(nazcaHeron)
|
if(c == '7') selectModel(nazcaHeron)
|
||||||
if(c == '8') selectModel(tank)
|
if(c == '8') selectModel(tank)
|
||||||
if(c == '9') selectModel(dude)
|
if(c == '9') selectModel(dude)
|
||||||
if(c == 's') drawSegs = !drawSegs
|
|
||||||
if(c == 'c') drawcdtMesh = !drawcdtMesh
|
if(c == 'd') drawSegs = !drawSegs
|
||||||
if(c == 'e') {drawEarClip = !drawEarClip; drawCDT = false; selectModel(currentModel)}
|
if(c == 'm') drawCDTMesh = !drawCDTMesh
|
||||||
|
if(c == 't') drawMap = !drawMap
|
||||||
|
|
||||||
|
if(c == 's') {algo = Seidel; selectModel(currentModel) }
|
||||||
|
if(c == 'c') {algo = CDT; selectModel(currentModel) }
|
||||||
|
if(c == 'e') {algo = EarClip; selectModel(currentModel) }
|
||||||
|
|
||||||
|
// Experimental...
|
||||||
if(c == 'r') slCDT.refine
|
if(c == 'r') slCDT.refine
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -359,8 +330,6 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
val clearPoint = Point(365, 427)
|
val clearPoint = Point(365, 427)
|
||||||
loadModel(dude, -1f, Point(100f, -200f), 10, clearPoint)
|
loadModel(dude, -1f, Point(100f, -200f), 10, clearPoint)
|
||||||
case _ =>
|
case _ =>
|
||||||
assert(false)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
currentModel = model
|
currentModel = model
|
||||||
}
|
}
|
||||||
@ -370,8 +339,8 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
println
|
println
|
||||||
println("************** " + model + " **************")
|
println("************** " + model + " **************")
|
||||||
|
|
||||||
polyX = new ArrayBuffer[Float]
|
val polyX = new ArrayBuffer[Float]
|
||||||
polyY = new ArrayBuffer[Float]
|
val polyY = new ArrayBuffer[Float]
|
||||||
var points = new ArrayBuffer[Point]
|
var points = new ArrayBuffer[Point]
|
||||||
|
|
||||||
val angle = Math.Pi
|
val angle = Math.Pi
|
||||||
@ -379,12 +348,12 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
val s = line.replaceAll("\n", "")
|
val s = line.replaceAll("\n", "")
|
||||||
val tokens = s.split("[ ]+")
|
val tokens = s.split("[ ]+")
|
||||||
if(tokens.size == 2) {
|
if(tokens.size == 2) {
|
||||||
var x = tokens(0).toFloat
|
var x = tokens(0).toFloat
|
||||||
var y = tokens(1).toFloat
|
var y = tokens(1).toFloat
|
||||||
// Transform the shape
|
// Transform the shape
|
||||||
polyX += (Math.cos(angle)*x - Math.sin(angle)*y).toFloat * scale + center.x
|
polyX += (Math.cos(angle)*x - Math.sin(angle)*y).toFloat * scale + center.x
|
||||||
polyY += (Math.sin(angle)*x + Math.cos(angle)*y).toFloat * scale + center.y
|
polyY += (Math.sin(angle)*x + Math.cos(angle)*y).toFloat * scale + center.y
|
||||||
points += new Point(polyX.last, polyY.last)
|
points += new Point(polyX.last, polyY.last)
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Bad input file")
|
throw new Exception("Bad input file")
|
||||||
}
|
}
|
||||||
@ -392,18 +361,20 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
|
|
||||||
segments = new ArrayBuffer[Segment]
|
segments = new ArrayBuffer[Segment]
|
||||||
for(i <- 0 until points.size-1)
|
for(i <- 0 until points.size-1)
|
||||||
segments += new Segment(points(i), points(i+1))
|
segments += new Segment(points(i), points(i+1))
|
||||||
segments += new Segment(points.first, points.last)
|
segments += new Segment(points.first, points.last)
|
||||||
|
|
||||||
println("Number of points = " + polyX.size)
|
println("Number of points = " + polyX.size)
|
||||||
println
|
println
|
||||||
|
|
||||||
if(doCDT) {
|
algo match {
|
||||||
|
|
||||||
|
case Algo.CDT => {
|
||||||
|
|
||||||
val pts = points.toArray
|
val pts = points.toArray
|
||||||
|
|
||||||
val t1 = System.nanoTime
|
val t1 = System.nanoTime
|
||||||
slCDT = new CDT(pts, clearPoint)
|
slCDT = new CDT(pts, clearPoint)
|
||||||
|
|
||||||
// Add some holes....
|
// Add some holes....
|
||||||
if(model == "data/dude.dat") {
|
if(model == "data/dude.dat") {
|
||||||
@ -418,22 +389,22 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
val hx = -headHole(i).x*scale + center.x
|
val hx = -headHole(i).x*scale + center.x
|
||||||
val hy = -headHole(i).y*scale + center.y
|
val hy = -headHole(i).y*scale + center.y
|
||||||
headHole(i) = Point(hx, hy)
|
headHole(i) = Point(hx, hy)
|
||||||
}
|
}
|
||||||
for(i <- 0 until chestHole.size) {
|
for(i <- 0 until chestHole.size) {
|
||||||
val cx = -chestHole(i).x*scale + center.x
|
val cx = -chestHole(i).x*scale + center.x
|
||||||
val cy = -chestHole(i).y*scale + center.y
|
val cy = -chestHole(i).y*scale + center.y
|
||||||
chestHole(i) = Point(cx, cy)
|
chestHole(i) = Point(cx, cy)
|
||||||
}
|
}
|
||||||
|
|
||||||
chestSegs = new ArrayBuffer[Segment]
|
chestSegs = new ArrayBuffer[Segment]
|
||||||
for(i <- 0 until chestHole.size-1)
|
for(i <- 0 until chestHole.size-1)
|
||||||
chestSegs += new Segment(chestHole(i), chestHole(i+1))
|
chestSegs += new Segment(chestHole(i), chestHole(i+1))
|
||||||
chestSegs += new Segment(chestHole.first, chestHole.last)
|
chestSegs += new Segment(chestHole.first, chestHole.last)
|
||||||
|
|
||||||
headSegs = new ArrayBuffer[Segment]
|
headSegs = new ArrayBuffer[Segment]
|
||||||
for(i <- 0 until headHole.size-1)
|
for(i <- 0 until headHole.size-1)
|
||||||
headSegs += new Segment(headHole(i), headHole(i+1))
|
headSegs += new Segment(headHole(i), headHole(i+1))
|
||||||
headSegs += new Segment(headHole.first, headHole.last)
|
headSegs += new Segment(headHole.first, headHole.last)
|
||||||
|
|
||||||
// Add the holes
|
// Add the holes
|
||||||
slCDT.addHole(headHole)
|
slCDT.addHole(headHole)
|
||||||
@ -441,42 +412,49 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slCDT triangulate
|
slCDT triangulate
|
||||||
val runTime = System.nanoTime - t1
|
|
||||||
|
|
||||||
println("CDT average (ms) = " + runTime*1e-6)
|
|
||||||
println("Number of triangles = " + slCDT.triangles.size)
|
|
||||||
println
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!drawEarClip) {
|
|
||||||
|
|
||||||
// Sediel triangulation
|
|
||||||
val t1 = System.nanoTime
|
|
||||||
seidel = new Triangulator(points)
|
|
||||||
val runTime = System.nanoTime - t1
|
val runTime = System.nanoTime - t1
|
||||||
|
|
||||||
println("Poly2Tri average (ms) = " + runTime*1e-6)
|
println("CDT average (ms) = " + runTime*1e-6)
|
||||||
println("Number of triangles = " + seidel.polygons.size)
|
println("Number of triangles = " + slCDT.triangles.size)
|
||||||
|
println
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
case Algo.Seidel => {
|
||||||
|
|
||||||
// Earclip
|
// Sediel triangulation
|
||||||
|
val t1 = System.nanoTime
|
||||||
|
seidel = new Triangulator(points)
|
||||||
|
val runTime = System.nanoTime - t1
|
||||||
|
|
||||||
earClipResults = new Array[poly2tri.earClip.Triangle](maxTriangles)
|
println("Seidel average (ms) = " + runTime*1e-6)
|
||||||
|
println("Number of triangles = " + seidel.polygons.size)
|
||||||
|
|
||||||
for(i <- 0 until earClipResults.size) earClipResults(i) = new poly2tri.earClip.Triangle
|
}
|
||||||
|
|
||||||
var xVerts = polyX.toArray
|
case Algo.EarClip => {
|
||||||
var yVerts = polyY.toArray
|
|
||||||
|
|
||||||
val t1 = System.nanoTime
|
// Earclip
|
||||||
earClip.triangulatePolygon(xVerts, yVerts, xVerts.size, earClipResults)
|
|
||||||
val runTime = System.nanoTime - t1
|
|
||||||
|
|
||||||
println
|
earClipResults = new Array[poly2tri.earClip.Triangle](maxTriangles)
|
||||||
println("Earclip average (ms) = " + runTime*1e-6)
|
|
||||||
println("Number of triangles = " + earClip.numTriangles)
|
for(i <- 0 until earClipResults.size) earClipResults(i) = new poly2tri.earClip.Triangle
|
||||||
}
|
|
||||||
|
var xVerts = polyX.toArray
|
||||||
|
var yVerts = polyY.toArray
|
||||||
|
|
||||||
|
val xv = if(currentModel != "data/strange.dat") xVerts.reverse.toArray else xVerts
|
||||||
|
val yv = if(currentModel != "data/strange.dat") yVerts.reverse.toArray else yVerts
|
||||||
|
|
||||||
|
val t1 = System.nanoTime
|
||||||
|
earClip.triangulatePolygon(xv, yv, xVerts.size, earClipResults)
|
||||||
|
val runTime = System.nanoTime - t1
|
||||||
|
|
||||||
|
println
|
||||||
|
println("Earclip average (ms) = " + runTime*1e-6)
|
||||||
|
println("Number of triangles = " + earClip.numTriangles)
|
||||||
|
}
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -109,76 +109,65 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
|
|
||||||
// Create edges and connect end points; update edge event pointer
|
// Create edges and connect end points; update edge event pointer
|
||||||
private def initEdges(pts: Array[Point]) {
|
private def initEdges(pts: Array[Point]) {
|
||||||
|
|
||||||
// Connect pts
|
// Connect pts
|
||||||
for(i <- 0 until pts.size-1) {
|
for(i <- 0 until pts.size-1) {
|
||||||
val endPoints = validatePoints(pts(i), pts(i+1))
|
val endPoints = validatePoints(pts(i), pts(i+1))
|
||||||
val edge = new Segment(endPoints(0), endPoints(1))
|
val edge = new Segment(endPoints(0), endPoints(1))
|
||||||
endPoints(1).edges += edge
|
endPoints(1).edges += edge
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect endpoints
|
// Connect endpoints
|
||||||
val endPoints = validatePoints(pts.first, pts.last)
|
val endPoints = validatePoints(pts.first, pts.last)
|
||||||
val edge = new Segment(endPoints(0), endPoints(1))
|
val edge = new Segment(endPoints(0), endPoints(1))
|
||||||
endPoints(1).edges += edge
|
endPoints(1).edges += edge
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def validatePoints(p1: Point, p2: Point): List[Point] = {
|
private def validatePoints(p1: Point, p2: Point): List[Point] = {
|
||||||
|
|
||||||
if(p1.y > p2.y) {
|
if(p1.y > p2.y) {
|
||||||
// For CDT we want q to be the point with > y
|
// For CDT we want q to be the point with > y
|
||||||
return List(p2, p1)
|
return List(p2, p1)
|
||||||
} else if(p1.y == p2.y) {
|
} else if(p1.y == p2.y) {
|
||||||
// If y values are equal, make sure point with smaller x value
|
// If y values are equal, make sure point with smaller x value
|
||||||
// is to the left
|
// is to the left
|
||||||
if(p1.x > p2.x) {
|
if(p1.x > p2.x) {
|
||||||
return List(p2, p1)
|
return List(p2, p1)
|
||||||
} else if(p1.x == p2.x) {
|
} else if(p1.x == p2.x) {
|
||||||
throw new Exception("Duplicate point")
|
throw new Exception("Duplicate point")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List(p1, p2)
|
List(p1, p2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge sort: O(n log n)
|
// Merge sort: O(n log n)
|
||||||
private def pointSort: List[Point] =
|
private def pointSort: List[Point] =
|
||||||
Util.msort((p1: Point, p2: Point) => p1 > p2)(points)
|
Util.msort((p1: Point, p2: Point) => p1 > p2)(points)
|
||||||
|
|
||||||
// Implement sweep-line
|
// Implement sweep-line
|
||||||
private def sweep {
|
private def sweep {
|
||||||
|
|
||||||
val size = if(refined) 1 else points.size
|
val size = if(refined) 1 else points.size
|
||||||
|
|
||||||
for(i <- 1 until points.size) {
|
for(i <- 1 until points.size) {
|
||||||
|
|
||||||
val point = points(i)
|
val point = points(i)
|
||||||
// Process Point event
|
// Process Point event
|
||||||
val node = pointEvent(point)
|
val node = pointEvent(point)
|
||||||
// Process edge events
|
// Process edge events
|
||||||
point.edges.foreach(e => edgeEvent(e, node))
|
point.edges.foreach(e => edgeEvent(e, node))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final step in the sweep-line CDT
|
// Final step in the sweep-line CDT
|
||||||
// Clean exterior triangles
|
// Clean exterior triangles
|
||||||
private def finalization {
|
private def finalization {
|
||||||
|
|
||||||
var found = false
|
var found = false
|
||||||
mesh.map.foreach(m => {
|
mesh.map.foreach(m => {
|
||||||
if(!found)
|
if(!found)
|
||||||
// Mark the originating clean triangle
|
// Mark the originating clean triangle
|
||||||
if(m.pointIn(clearPoint)) {
|
if(m.pointIn(clearPoint)) {
|
||||||
found = true
|
found = true
|
||||||
cleanTri = m
|
cleanTri = m
|
||||||
}
|
}
|
||||||
m.markNeighborEdges
|
m.markNeighborEdges
|
||||||
})
|
})
|
||||||
// Collect interior triangles constrained by edges
|
// Collect interior triangles constrained by edges
|
||||||
mesh clean cleanTri
|
mesh clean cleanTri
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delauney Refinement: Refine triangules using Steiner points
|
// Delauney Refinement: Refine triangules using Steiner points
|
||||||
@ -191,13 +180,13 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
mesh.triangles.foreach(t => {
|
mesh.triangles.foreach(t => {
|
||||||
if(t.thin) {
|
if(t.thin) {
|
||||||
val center = Util.circumcenter(t.points(0), t.points(1), t.points(2))
|
val center = Util.circumcenter(t.points(0), t.points(1), t.points(2))
|
||||||
cList += center
|
cList += center
|
||||||
addPoint(center)
|
addPoint(center)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Retriangulate
|
// Retriangulate
|
||||||
if(cList.size > 0)
|
if(cList.size > 0)
|
||||||
triangulate
|
triangulate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Point event
|
// Point event
|
||||||
@ -224,63 +213,63 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
|
|
||||||
// Locate the first intersected triangle
|
// Locate the first intersected triangle
|
||||||
val firstTriangle = if(!node.triangle.contains(edge.q))
|
val firstTriangle = if(!node.triangle.contains(edge.q))
|
||||||
node.triangle
|
node.triangle
|
||||||
else
|
else
|
||||||
node.triangle.locateFirst(edge)
|
node.triangle.locateFirst(edge)
|
||||||
|
|
||||||
if(firstTriangle != null && !firstTriangle.contains(edge)) {
|
if(firstTriangle != null && !firstTriangle.contains(edge)) {
|
||||||
|
|
||||||
// Interior mesh traversal - edge is "burried" in the mesh
|
// Interior mesh traversal - edge is "burried" in the mesh
|
||||||
// Constrained edge lies below the advancing front. Traverse through intersected triangles,
|
// Constrained edge lies below the advancing front. Traverse through intersected triangles,
|
||||||
// form empty pseudo-polygons, and re-triangulate
|
// form empty pseudo-polygons, and re-triangulate
|
||||||
|
|
||||||
// Collect intersected triangles
|
// Collect intersected triangles
|
||||||
val tList = new ArrayBuffer[Triangle]
|
val tList = new ArrayBuffer[Triangle]
|
||||||
tList += firstTriangle
|
tList += firstTriangle
|
||||||
|
|
||||||
while(tList.last != null && !tList.last.contains(edge.p))
|
while(tList.last != null && !tList.last.contains(edge.p))
|
||||||
tList += tList.last.findNeighbor(edge.p)
|
tList += tList.last.findNeighbor(edge.p)
|
||||||
|
|
||||||
// TODO: Finish implementing edge insertion which combines advancing front (AF)
|
// TODO: Finish implementing edge insertion which combines advancing front (AF)
|
||||||
// and triangle traversal respectively. See figure 14(a) from Domiter et al.
|
// and triangle traversal respectively. See figure 14(a) from Domiter et al.
|
||||||
// Should only occur with complex patterns of interior points
|
// Should only occur with complex patterns of interior points
|
||||||
// Already added provision for transitioning from AFront traversal to
|
// Already added provision for transitioning from AFront traversal to
|
||||||
// interior mesh traversal - may need to add the opposite case
|
// interior mesh traversal - may need to add the opposite case
|
||||||
if(tList.last == null)
|
if(tList.last == null)
|
||||||
throw new Exception("Not implemented yet - interior points too complex")
|
throw new Exception("Not implemented yet - interior points too complex")
|
||||||
|
|
||||||
// Neighbor triangles
|
// Neighbor triangles
|
||||||
// HashMap or set may improve performance
|
// HashMap or set may improve performance
|
||||||
val nTriangles = new ArrayBuffer[Triangle]
|
val nTriangles = new ArrayBuffer[Triangle]
|
||||||
|
|
||||||
// Remove old triangles; collect neighbor triangles
|
// Remove old triangles; collect neighbor triangles
|
||||||
// Keep duplicates out
|
// Keep duplicates out
|
||||||
tList.foreach(t => {
|
tList.foreach(t => {
|
||||||
t.neighbors.foreach(n => if(n != null && !tList.contains(n)) nTriangles += n)
|
t.neighbors.foreach(n => if(n != null && !tList.contains(n)) nTriangles += n)
|
||||||
mesh.map -= t
|
mesh.map -= t
|
||||||
})
|
})
|
||||||
|
|
||||||
// Using a hashMap or set may improve performance
|
// Using a hashMap or set may improve performance
|
||||||
val lPoints = new ArrayBuffer[Point]
|
val lPoints = new ArrayBuffer[Point]
|
||||||
val rPoints = new ArrayBuffer[Point]
|
val rPoints = new ArrayBuffer[Point]
|
||||||
|
|
||||||
// Collect points left and right of edge
|
// Collect points left and right of edge
|
||||||
tList.foreach(t => {
|
tList.foreach(t => {
|
||||||
t.points.foreach(p => {
|
t.points.foreach(p => {
|
||||||
if(p != edge.q && p != edge.p) {
|
if(p != edge.q && p != edge.p) {
|
||||||
if(Util.orient2d(edge.q, edge.p, p) > 0 ) {
|
if(Util.orient2d(edge.q, edge.p, p) > 0 ) {
|
||||||
// Keep duplicate points out
|
// Keep duplicate points out
|
||||||
if(!lPoints.contains(p)) {
|
if(!lPoints.contains(p)) {
|
||||||
lPoints += p
|
lPoints += p
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Keep duplicate points out
|
|
||||||
if(!rPoints.contains(p))
|
|
||||||
rPoints += p
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
})
|
// Keep duplicate points out
|
||||||
})
|
if(!rPoints.contains(p))
|
||||||
|
rPoints += p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Triangulate empty areas.
|
// Triangulate empty areas.
|
||||||
val T1 = new ArrayBuffer[Triangle]
|
val T1 = new ArrayBuffer[Triangle]
|
||||||
@ -296,8 +285,8 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
// Update advancing front
|
// Update advancing front
|
||||||
|
|
||||||
val ahead = (edge.p.x > edge.q.x)
|
val ahead = (edge.p.x > edge.q.x)
|
||||||
val point1 = if(ahead) edge.q else edge.p
|
val point1 = if(ahead) edge.q else edge.p
|
||||||
val point2 = if(ahead) edge.p else edge.q
|
val point2 = if(ahead) edge.p else edge.q
|
||||||
|
|
||||||
val sNode = if(ahead) node else aFront.locate(point1)
|
val sNode = if(ahead) node else aFront.locate(point1)
|
||||||
val eNode = aFront.locate(point2)
|
val eNode = aFront.locate(point2)
|
||||||
@ -333,23 +322,23 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
if(ahead) {
|
if(ahead) {
|
||||||
// Scan right
|
// Scan right
|
||||||
pNode = pNode.next
|
pNode = pNode.next
|
||||||
while(pNode.point != edge.p && !aboveEdge) {
|
while(pNode.point != edge.p && !aboveEdge) {
|
||||||
points += pNode.point
|
points += pNode.point
|
||||||
nTriangles += pNode.triangle
|
nTriangles += pNode.triangle
|
||||||
pNode = pNode.next
|
pNode = pNode.next
|
||||||
aboveEdge = edge < pNode.point
|
aboveEdge = edge < pNode.point
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Scan left
|
// Scan left
|
||||||
pNode = pNode.prev
|
pNode = pNode.prev
|
||||||
while(pNode.point != edge.p && !aboveEdge) {
|
while(pNode.point != edge.p && !aboveEdge) {
|
||||||
points += pNode.point
|
points += pNode.point
|
||||||
nTriangles += pNode.triangle
|
nTriangles += pNode.triangle
|
||||||
pNode = pNode.prev
|
pNode = pNode.prev
|
||||||
aboveEdge = edge < pNode.point
|
aboveEdge = edge < pNode.point
|
||||||
}
|
}
|
||||||
nTriangles += pNode.triangle
|
nTriangles += pNode.triangle
|
||||||
}
|
}
|
||||||
|
|
||||||
val point2 = if(aboveEdge) {
|
val point2 = if(aboveEdge) {
|
||||||
val p1 = pNode.point
|
val p1 = pNode.point
|
||||||
@ -455,7 +444,7 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
if(node2.prev != null) {
|
if(node2.prev != null) {
|
||||||
var angle = 0.0
|
var angle = 0.0
|
||||||
do {
|
do {
|
||||||
angle = fill(node2)
|
angle = fill(node2)
|
||||||
node2 = node2.prev
|
node2 = node2.prev
|
||||||
} while(angle <= PI_2 && angle >= -PI_2 && node2.prev != null)
|
} while(angle <= PI_2 && angle >= -PI_2 && node2.prev != null)
|
||||||
}
|
}
|
||||||
@ -468,31 +457,28 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
val a = (node.prev.point - node.point)
|
val a = (node.prev.point - node.point)
|
||||||
val b = (node.next.point - node.point)
|
val b = (node.next.point - node.point)
|
||||||
val angle = Math.atan2(a cross b, a dot b)
|
val angle = Math.atan2(a cross b, a dot b)
|
||||||
// Is the angle acute?
|
// Is the angle acute?
|
||||||
if(angle <= PI_2 && angle >= -PI_2) {
|
if(angle <= PI_2 && angle >= -PI_2) {
|
||||||
val points = Array(node.prev.point, node.point, node.next.point)
|
val points = Array(node.prev.point, node.point, node.next.point)
|
||||||
val triangle = new Triangle(points)
|
val triangle = new Triangle(points)
|
||||||
// Update neighbor pointers
|
// Update neighbor pointers
|
||||||
node.prev.triangle.markNeighbor(triangle)
|
node.prev.triangle.markNeighbor(triangle)
|
||||||
node.triangle.markNeighbor(triangle)
|
node.triangle.markNeighbor(triangle)
|
||||||
mesh.map += triangle
|
mesh.map += triangle
|
||||||
aFront -= (node.prev, node, triangle)
|
aFront -= (node.prev, node, triangle)
|
||||||
}
|
}
|
||||||
angle
|
angle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Circumcircle test.
|
// Circumcircle test.
|
||||||
// Determines if point d lies inside triangle abc's circumcircle
|
// Determines if point d lies inside triangle abc's circumcircle
|
||||||
private def illegal(a: Point, b: Point, c: Point, d: Point): Boolean = {
|
private def illegal(a: Point, b: Point, c: Point, d: Point): Boolean = {
|
||||||
|
|
||||||
val ccw = Util.orient2d(a, b, c) > 0
|
val ccw = Util.orient2d(a, b, c) > 0
|
||||||
|
|
||||||
// Make sure abc is oriented counter-clockwise
|
// Make sure abc is oriented counter-clockwise
|
||||||
if(ccw)
|
if(ccw)
|
||||||
Util.incircle(a, b, c, d)
|
Util.incircle(a, b, c, d)
|
||||||
else
|
else
|
||||||
Util.incircle(a, c, b, d)
|
Util.incircle(a, c, b, d)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure adjacent triangles are legal
|
// Ensure adjacent triangles are legal
|
||||||
@ -509,33 +495,33 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
|
|
||||||
if(illegal && !t2.finalized) {
|
if(illegal && !t2.finalized) {
|
||||||
|
|
||||||
// Flip edge and rotate everything clockwise
|
// Flip edge and rotate everything clockwise
|
||||||
|
|
||||||
// Legalize points
|
// Legalize points
|
||||||
t1.legalize(oPoint)
|
t1.legalize(oPoint)
|
||||||
t2.legalize(oPoint, point)
|
t2.legalize(oPoint, point)
|
||||||
|
|
||||||
// Update neighbors
|
// Update neighbors
|
||||||
|
|
||||||
// Copy old neighbors
|
// Copy old neighbors
|
||||||
val neighbors = List(t2.neighbors(0), t2.neighbors(1), t2.neighbors(2))
|
val neighbors = List(t2.neighbors(0), t2.neighbors(1), t2.neighbors(2))
|
||||||
// Clear old neighbors
|
// Clear old neighbors
|
||||||
t2.clearNeighbors
|
t2.clearNeighbors
|
||||||
// Update new neighbors
|
// Update new neighbors
|
||||||
for(n <- neighbors) {
|
for(n <- neighbors) {
|
||||||
if(n != null) {
|
if(n != null) {
|
||||||
t1.markNeighbor(n)
|
t1.markNeighbor(n)
|
||||||
t2.markNeighbor(n)
|
t2.markNeighbor(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t2.markNeighbor(t1)
|
t2.markNeighbor(t1)
|
||||||
|
|
||||||
// Don't legalize these triangles again
|
// Don't legalize these triangles again
|
||||||
t2.finalized = true
|
t2.finalized = true
|
||||||
t1.finalized = true
|
t1.finalized = true
|
||||||
|
|
||||||
// Update advancing front
|
// Update advancing front
|
||||||
aFront.insertLegalized(t1.points(1), t1, node)
|
aFront.insertLegalized(t1.points(1), t1, node)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@ -545,7 +531,6 @@ class CDT(polyLine: Array[Point], clearPoint: Point) {
|
|||||||
aFront.insert(point, t1, node)
|
aFront.insert(point, t1, node)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The triangle mesh
|
// The triangle mesh
|
||||||
|
@ -38,21 +38,10 @@ class EarClip {
|
|||||||
|
|
||||||
var numTriangles = 0
|
var numTriangles = 0
|
||||||
|
|
||||||
def triangulatePolygon(x: Array[Float], y: Array[Float], vn: Int, results: Array[Triangle]): Int = {
|
def triangulatePolygon(xv: Array[Float], yv: Array[Float], vn: Int, results: Array[Triangle]): Int = {
|
||||||
|
|
||||||
/*
|
if (vn < 3) return 0
|
||||||
val p1 = Point(x(0), y(0))
|
var vNum = vn
|
||||||
val p2 = Point(x(1), y(1))
|
|
||||||
val p3 = Point(x(2), y(2))
|
|
||||||
|
|
||||||
val ccw = Util.orient2d(p1, p2, p3) > 0
|
|
||||||
*/
|
|
||||||
|
|
||||||
val xv = x.reverse.toArray
|
|
||||||
val yv = y.reverse.toArray
|
|
||||||
|
|
||||||
if (vn < 3) return 0
|
|
||||||
var vNum = vn
|
|
||||||
|
|
||||||
//Recurse and split on pinch points
|
//Recurse and split on pinch points
|
||||||
val pA = new Poly
|
val pA = new Poly
|
||||||
@ -133,15 +122,18 @@ class EarClip {
|
|||||||
|
|
||||||
System.out.println("Couldn't find an ear, dumping remaining poly:\n");
|
System.out.println("Couldn't find an ear, dumping remaining poly:\n");
|
||||||
System.out.println("Please submit this dump to ewjordan at Box2d forums\n");
|
System.out.println("Please submit this dump to ewjordan at Box2d forums\n");
|
||||||
|
|
||||||
|
assert(false)
|
||||||
|
|
||||||
for (i <- 0 until bufferSize) {
|
for (i <- 0 until bufferSize) {
|
||||||
results(i).set(buffer(i));
|
results(i).set(buffer(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferSize > 0) return bufferSize;
|
if (bufferSize > 0) return bufferSize;
|
||||||
else {
|
else {
|
||||||
numTriangles = -1
|
numTriangles = -1
|
||||||
return numTriangles
|
return numTriangles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clip off the ear:
|
// Clip off the ear:
|
||||||
|
Loading…
Reference in New Issue
Block a user