Переглянути джерело

changed Graphics triangulation from poly2tri to earcut (more reliable, faster)
keep a single point list for both fill and lines

Nicolas Cannasse 9 роки тому
батько
коміт
ba5738fae8
3 змінених файлів з 565 додано та 87 видалено
  1. 57 78
      h2d/Graphics.hx
  2. 494 0
      hxd/earcut/Earcut.hx
  3. 14 9
      samples/draw/Draw.hx

+ 57 - 78
h2d/Graphics.hx

@@ -3,7 +3,7 @@ import hxd.Math;
 
 
 private typedef GraphicsPoint = hxd.poly2tri.Point;
 private typedef GraphicsPoint = hxd.poly2tri.Point;
 
 
-private class LinePoint {
+private class GPoint {
 	public var x : Float;
 	public var x : Float;
 	public var y : Float;
 	public var y : Float;
 	public var r : Float;
 	public var r : Float;
@@ -98,10 +98,8 @@ private class GraphicsContent extends h3d.prim.Primitive {
 class Graphics extends Drawable {
 class Graphics extends Drawable {
 
 
 	var content : GraphicsContent;
 	var content : GraphicsContent;
-	var pts : Array<GraphicsPoint>;
-	var linePts : Array<LinePoint>;
+	var tmpPoints : Array<GPoint>;
 	var pindex : Int;
 	var pindex : Int;
-	var prev : Array<Array<GraphicsPoint>>;
 	var curR : Float;
 	var curR : Float;
 	var curG : Float;
 	var curG : Float;
 	var curB : Float;
 	var curB : Float;
@@ -141,9 +139,7 @@ class Graphics extends Drawable {
 
 
 	public function clear() {
 	public function clear() {
 		content.clear();
 		content.clear();
-		pts = [];
-		prev = [];
-		linePts = [];
+		tmpPoints = [];
 		pindex = 0;
 		pindex = 0;
 		lineSize = 0;
 		lineSize = 0;
 		xMin = Math.POSITIVE_INFINITY;
 		xMin = Math.POSITIVE_INFINITY;
@@ -157,7 +153,7 @@ class Graphics extends Drawable {
 		if( tile != null ) addBounds(relativeTo, out, xMin, yMin, xMax - xMin, yMax - yMin);
 		if( tile != null ) addBounds(relativeTo, out, xMin, yMin, xMax - xMin, yMax - yMin);
 	}
 	}
 
 
-	function isConvex( points : Array<GraphicsPoint> ) {
+	function isConvex( points : Array<GPoint> ) {
 		var first = true, sign = false;
 		var first = true, sign = false;
 		for( i in 0...points.length ) {
 		for( i in 0...points.length ) {
 			var p1 = points[i];
 			var p1 = points[i];
@@ -173,33 +169,29 @@ class Graphics extends Drawable {
 		return true;
 		return true;
 	}
 	}
 
 
-	function flushLine() {
-		if( linePts.length == 0 )
-			return;
-
-		var last = linePts.length - 1;
-		var prev = linePts[last];
-		var p = linePts[0];
+	function flushLine( start ) {
+		var pts = tmpPoints;
+		var last = pts.length - 1;
+		var prev = pts[last];
+		var p = pts[0];
 
 
 		var closed = p.x == prev.x && p.y == prev.y;
 		var closed = p.x == prev.x && p.y == prev.y;
-		var count = linePts.length;
+		var count = pts.length;
 		if( !closed ) {
 		if( !closed ) {
-			var prevLast = linePts[last - 1];
+			var prevLast = pts[last - 1];
 			if( prevLast == null ) prevLast = p;
 			if( prevLast == null ) prevLast = p;
-			linePts.push(new LinePoint(prev.x * 2 - prevLast.x, prev.y * 2 - prevLast.y, 0, 0, 0, 0));
-			var pNext = linePts[1];
+			pts.push(new GPoint(prev.x * 2 - prevLast.x, prev.y * 2 - prevLast.y, 0, 0, 0, 0));
+			var pNext = pts[1];
 			if( pNext == null ) pNext = p;
 			if( pNext == null ) pNext = p;
-			prev = new LinePoint(p.x * 2 - pNext.x, p.y * 2 - pNext.y, 0, 0, 0, 0);
+			prev = new GPoint(p.x * 2 - pNext.x, p.y * 2 - pNext.y, 0, 0, 0, 0);
 		} else if( p != prev ) {
 		} else if( p != prev ) {
 			count--;
 			count--;
 			last--;
 			last--;
-			prev = linePts[last];
+			prev = pts[last];
 		}
 		}
 
 
-		var start = pindex;
-
 		for( i in 0...count ) {
 		for( i in 0...count ) {
-			var next = linePts[(i + 1) % linePts.length];
+			var next = pts[(i + 1) % pts.length];
 
 
 			var nx1 = prev.y - p.y;
 			var nx1 = prev.y - p.y;
 			var ny1 = p.x - prev.x;
 			var ny1 = p.x - prev.x;
@@ -242,45 +234,56 @@ class Graphics extends Drawable {
 			prev = p;
 			prev = p;
 			p = next;
 			p = next;
 		}
 		}
-		linePts = [];
-		if( content.next() )
-			pindex = 0;
 	}
 	}
 
 
-	function flushFill() {
-		if( pts.length > 0 )
-			closeShape();
-		if( prev.length == 0 )
+	static var EARCUT = null;
+
+	function flushFill( i0 ) {
+
+		if( tmpPoints.length < 3 )
 			return;
 			return;
 
 
-		if( prev.length == 1 && isConvex(prev[0]) ) {
-			var p0 = prev[0][0].id;
-			for( i in 1...prev[0].length - 1 ) {
-				content.addIndex(p0);
-				content.addIndex(p0 + i);
-				content.addIndex(p0 + i + 1);
+		var pts = tmpPoints;
+		var p0 = pts[0];
+		var p1 = pts[pts.length - 1];
+		var last = null;
+		// closed poly
+		if( hxd.Math.abs(p0.x - p1.x) < 1e-9 && hxd.Math.abs(p0.y - p1.y) < 1e-9 )
+			last = pts.pop();
+
+		if( isConvex(pts) ) {
+			for( i in 1...pts.length - 1 ) {
+				content.addIndex(i0);
+				content.addIndex(i0 + i);
+				content.addIndex(i0 + i + 1);
 			}
 			}
 		} else {
 		} else {
-			var ctx = new hxd.poly2tri.SweepContext();
-			for( p in prev )
-				ctx.addPolyline(p);
-
-			var p = new hxd.poly2tri.Sweep(ctx);
-			p.triangulate();
-
-			for( t in ctx.triangles )
-				for( p in t.points )
-					content.addIndex(p.id);
+			var ear = EARCUT;
+			if( ear == null )
+				EARCUT = ear = new hxd.earcut.Earcut();
+			for( i in ear.triangulate(pts) )
+				content.addIndex(i + i0);
 		}
 		}
 
 
-		prev = [];
-		if( content.next() )
-			pindex = 0;
+		if( last != null )
+			pts.push(last);
 	}
 	}
 
 
 	public function flush() {
 	public function flush() {
-		flushFill();
-		flushLine();
+		if( tmpPoints.length == 0 )
+			return;
+		if( doFill ) {
+			flushFill(pindex);
+			pindex += tmpPoints.length;
+			if( content.next() )
+				pindex = 0;
+		}
+		if( lineSize > 0 ) {
+			flushLine(pindex);
+			if( content.next() )
+				pindex = 0;
+		}
+		tmpPoints = [];
 	}
 	}
 
 
 	public function beginFill( color : Int = 0, alpha = 1.  ) {
 	public function beginFill( color : Int = 0, alpha = 1.  ) {
@@ -385,25 +388,6 @@ class Graphics extends Drawable {
 		flush();
 		flush();
 	}
 	}
 
 
-	function closeShape() {
-		if( pts.length < 3 ) {
-			pts = [];
-			return;
-		}
-		var p0 = pts[0];
-		var p1 = pts[pts.length - 1];
-		if( hxd.Math.abs(p0.x - p1.x) < 1e-9 && hxd.Math.abs(p0.y - p1.y) < 1e-9 )
-			pts.pop();
-		prev.push(pts);
-		pts = [];
-	}
-
-	public function addHole() {
-		if( pts.length > 0 )
-			closeShape();
-		flushLine();
-	}
-
 	public inline function addPoint( x : Float, y : Float ) {
 	public inline function addPoint( x : Float, y : Float ) {
 		addPointFull(x, y, curR, curG, curB, curA, x * ma + y * mc + mx, x * mb + y * md + my);
 		addPointFull(x, y, curR, curG, curB, curA, x * ma + y * mc + mx, x * mb + y * md + my);
 	}
 	}
@@ -413,14 +397,9 @@ class Graphics extends Drawable {
 		if( y < yMin ) yMin = y;
 		if( y < yMin ) yMin = y;
 		if( x > xMax ) xMax = x;
 		if( x > xMax ) xMax = x;
 		if( y > yMax ) yMax = y;
 		if( y > yMax ) yMax = y;
-		if( doFill ) {
-			var p = new GraphicsPoint(x, y);
-			p.id = pindex++;
-			pts.push(p);
+		if( doFill )
 			content.add(x, y, u, v, r, g, b, a);
 			content.add(x, y, u, v, r, g, b, a);
-		}
-		if( lineSize > 0 )
-			linePts.push(new LinePoint(x, y, lineR, lineG, lineB, lineA));
+		tmpPoints.push(new GPoint(x, y, lineR, lineG, lineB, lineA));
 	}
 	}
 
 
 	override function draw(ctx:RenderContext) {
 	override function draw(ctx:RenderContext) {

+ 494 - 0
hxd/earcut/Earcut.hx

@@ -0,0 +1,494 @@
+package hxd.earcut;
+
+class EarNode {
+	public var next : EarNode;
+	public var prev : EarNode;
+	public var nextZ : EarNode;
+	public var prevZ : EarNode;
+	public var allocNext : EarNode;
+	public var x : Float;
+	public var y : Float;
+	public var i : Int;
+	public var z : Int;
+	public var steiner : Bool;
+	public function new() {
+	}
+}
+
+/**
+	Ported from https://github.com/mapbox/earcut by @ncannasse
+**/
+class Earcut {
+
+	var triangles : Array<Int>;
+	var cache : EarNode;
+	var allocated : EarNode;
+	var minX : Float;
+	var minY : Float;
+	var size : Float;
+	var hasSize : Bool;
+
+	public function new() {
+	}
+
+	@:generic public function triangulate<T:{x:Float,y:Float}>( points : Array<T> ) : Array<Int> {
+		var outerLen = points.length;
+
+		if( outerLen < 3 )
+			return [];
+
+		var root : EarNode = allocNode( -1, 0, 0, null);
+		var first = root;
+
+		// check polygon winding
+		var sum = 0.;
+		var p = points[points.length - 1];
+		for( p2 in points ) {
+			sum += (p.x - p2.x) * (p.y + p2.y);
+			p = p2;
+		}
+		if( sum > 0 ) {
+			// clockwise
+			for( i in 0...points.length ) {
+				var p = points[i];
+				root = allocNode(i, p.x, p.y, root);
+			}
+ 		} else {
+			var last = points.length - 1;
+			for( i in 0...points.length ) {
+				var p = points[last - i];
+				root = allocNode(last - i, p.x, p.y, root);
+			}
+		}
+
+		// close loop
+		root.next = first.next;
+		root.next.prev = root;
+		return triangulateNode(root, points.length > 80);
+	}
+
+	public function triangulateNode( root : EarNode, useZOrder ) {
+		triangles = [];
+		root = filterPoints(root);
+		if( useZOrder && root != null ) {
+			var maxX, maxY;
+			minX = maxX = root.x;
+			minY = maxY = root.y;
+			var p = root.next;
+			while( p != root ) {
+				var x = p.x;
+				var y = p.y;
+				if (x < minX) minX = x;
+				if (y < minY) minY = y;
+				if (x > maxX) maxX = x;
+				if (y > maxY) maxY = y;
+				p = p.next;
+			}
+			// minX, minY and size are later used to transform coords into integers for z-order calculation
+			size = Math.max(maxX - minX, maxY - minY);
+			hasSize = true;
+		} else
+			hasSize = false;
+		earcutLinked(root);
+		var result = triangles;
+		triangles = null;
+
+		// recycle allocated into cache
+		var n = allocated;
+		if( cache != null ) {
+			while( n != cache )
+				n = n.allocNext;
+			n = n.allocNext;
+		}
+		while( n != null ) {
+			n.next = cache;
+			cache = n;
+			n = n.allocNext;
+		}
+
+		return result;
+	}
+
+	inline function equals(p1:EarNode, p2:EarNode) {
+		return p1.x == p2.x && p1.y == p2.y;
+	}
+
+	inline function area(p:EarNode, q:EarNode, r:EarNode) {
+		return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
+	}
+
+	inline function intersects(p1, q1, p2, q2) {
+		return (area(p1, q1, p2) > 0) != (area(p1, q1, q2) > 0) && (area(p2, q2, p1) > 0) != (area(p2, q2, q1) > 0);
+	}
+
+	// check if a polygon diagonal is locally inside the polygon
+	inline function locallyInside(a:EarNode, b:EarNode) {
+		return area(a.prev, a, a.next) < 0 ?
+			area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
+			area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
+	}
+
+	function filterPoints(start:EarNode, end:EarNode=null) {
+		if( start == null ) return start;
+		if( end == null ) end = start;
+		var p = start, again;
+		do {
+			again = false;
+			if( !p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) == 0) ) {
+				removeNode(p);
+				p = end = p.prev;
+				if (p == p.next) return null;
+				again = true;
+			} else {
+				p = p.next;
+			}
+		} while (again || p != end);
+		return end;
+	}
+
+	inline function removeNode(p:EarNode) {
+		p.next.prev = p.prev;
+		p.prev.next = p.next;
+		if (p.prevZ != null) p.prevZ.nextZ = p.nextZ;
+		if (p.nextZ != null) p.nextZ.prevZ = p.prevZ;
+	}
+
+	inline function allocNode(i, x, y, last=null) {
+		var n = cache;
+		if( n == null ) {
+			n = new EarNode();
+			n.allocNext = allocated;
+			allocated = n;
+		} else
+			cache = n.next;
+		n.i = i;
+		n.z = -1;
+		n.x = x;
+		n.y = y;
+		n.next = null;
+		n.prev = last;
+		n.steiner = false;
+		n.prevZ = null;
+		n.nextZ = null;
+		if( last != null )
+			last.next = n;
+		return n;
+	}
+
+	function earcutLinked(ear:EarNode, pass = 0) {
+		if ( ear == null ) return;
+
+		// interlink polygon nodes in z-order
+		if( pass == 0 && hasSize ) indexCurve(ear);
+
+		var stop = ear,
+			prev, next;
+
+		// iterate through ears, slicing them one by one
+		while (ear.prev != ear.next) {
+			prev = ear.prev;
+			next = ear.next;
+
+			if ( hasSize ? isEarHashed(ear) : isEar(ear)) {
+				// cut off the triangle
+				triangles.push(prev.i);
+				triangles.push(ear.i);
+				triangles.push(next.i);
+
+				removeNode(ear);
+
+				// skipping the next vertice leads to less sliver triangles
+				ear = next.next;
+				stop = next.next;
+
+				continue;
+			}
+
+			ear = next;
+
+			// if we looped through the whole remaining polygon and can't find any more ears
+			if (ear == stop) {
+				// try filtering points and slicing again
+				switch( pass ) {
+				case 0:
+					earcutLinked(filterPoints(ear), 1);
+				// if this didn't work, try curing all small self-intersections locally
+				case 1:
+					ear = cureLocalIntersections(ear);
+					earcutLinked(ear, 2);
+				// as a last resort, try splitting the remaining polygon into two
+				case 2:
+					splitEarcut(ear);
+				}
+				break;
+			}
+		}
+	}
+
+	// check whether a polygon node forms a valid ear with adjacent nodes
+	function isEar(ear:EarNode) {
+		var a = ear.prev,
+			b = ear,
+			c = ear.next;
+
+		if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
+
+		// now make sure we don't have other points inside the potential ear
+		var p = ear.next.next;
+
+		while (p != ear.prev) {
+			if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
+				area(p.prev, p, p.next) >= 0) return false;
+			p = p.next;
+		}
+
+		return true;
+	}
+
+	function isEarHashed(ear:EarNode) {
+		var a = ear.prev,
+			b = ear,
+			c = ear.next;
+
+		if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
+
+		// triangle bbox; min & max are calculated like this for speed
+		var minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x),
+			minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y),
+			maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x),
+			maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y);
+
+		// z-order range for the current triangle bbox;
+		var minZ = zOrder(minTX, minTY),
+			maxZ = zOrder(maxTX, maxTY);
+
+		// first look for points inside the triangle in increasing z-order
+		var p = ear.nextZ;
+
+		while (p != null && p.z <= maxZ) {
+			if (p != ear.prev && p != ear.next &&
+				pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
+				area(p.prev, p, p.next) >= 0) return false;
+			p = p.nextZ;
+		}
+
+		// then look for points in decreasing z-order
+		p = ear.prevZ;
+
+		while (p != null && p.z >= minZ) {
+			if (p != ear.prev && p != ear.next &&
+				pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
+				area(p.prev, p, p.next) >= 0) return false;
+			p = p.prevZ;
+		}
+
+		return true;
+	}
+
+	// go through all polygon nodes and cure small local self-intersections
+	function cureLocalIntersections(start:EarNode) {
+		var p = start;
+		do {
+			var a = p.prev,
+				b = p.next.next;
+
+			// a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2])
+			if (intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
+
+				triangles.push(a.i);
+				triangles.push(p.i);
+				triangles.push(b.i);
+
+				// remove two nodes involved
+				removeNode(p);
+				removeNode(p.next);
+
+				p = start = b;
+			}
+			p = p.next;
+		} while (p != start);
+
+		return p;
+	}
+
+	// try splitting polygon into two and triangulate them independently
+	function splitEarcut(start:EarNode) {
+		// look for a valid diagonal that divides the polygon into two
+		var a = start;
+		do {
+			var b = a.next.next;
+			while (b != a.prev) {
+				if (a.i != b.i && isValidDiagonal(a, b)) {
+					// split the polygon in two by the diagonal
+					var c = splitPolygon(a, b);
+
+					// filter colinear points around the cuts
+					a = filterPoints(a, a.next);
+					c = filterPoints(c, c.next);
+
+					// run earcut on each half
+					earcutLinked(a);
+					earcutLinked(c);
+					return;
+				}
+				b = b.next;
+			}
+			a = a.next;
+		} while (a != start);
+	}
+
+	// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
+	// if one belongs to the outer ring and another to a hole, it merges it into a single ring
+	function splitPolygon(a : EarNode, b : EarNode) {
+		var a2 = allocNode(a.i, a.x, a.y),
+			b2 = allocNode(b.i, b.x, b.y),
+			an = a.next,
+			bp = b.prev;
+
+		a.next = b;
+		b.prev = a;
+
+		a2.next = an;
+		an.prev = a2;
+
+		b2.next = a2;
+		a2.prev = b2;
+
+		bp.next = b2;
+		b2.prev = bp;
+
+		return b2;
+	}
+
+	inline function pointInTriangle(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, px:Float, py:Float) {
+		return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
+			   (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
+			   (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
+	}
+
+	// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
+	function isValidDiagonal(a:EarNode, b:EarNode) {
+		return equals(a, b) || a.next.i != b.i && a.prev.i != b.i && !intersectsPolygon(a, b) &&
+			   locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b);
+	}
+
+	// check if the middle point of a polygon diagonal is inside the polygon
+	function middleInside(a:EarNode, b:EarNode) {
+		var p = a,
+			inside = false,
+			px = (a.x + b.x) / 2,
+			py = (a.y + b.y) / 2;
+		do {
+			if (((p.y > py) != (p.next.y > py)) && (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
+				inside = !inside;
+			p = p.next;
+		} while (p != a);
+
+		return inside;
+	}
+
+	// check if a polygon diagonal intersects any polygon segments
+	function intersectsPolygon(a:EarNode, b:EarNode) {
+		var p = a;
+		do {
+			if (p.i != a.i && p.next.i != a.i && p.i != b.i && p.next.i != b.i &&
+					intersects(p, p.next, a, b)) return true;
+			p = p.next;
+		} while (p != a);
+		return false;
+	}
+
+	inline function zOrder(px:Float, py:Float) {
+		// coords are transformed into non-negative 15-bit integer range
+		var x = Std.int(32767 * (px - minX) / size);
+		var y = Std.int(32767 * (py - minY) / size);
+
+		x = (x | (x << 8)) & 0x00FF00FF;
+		x = (x | (x << 4)) & 0x0F0F0F0F;
+		x = (x | (x << 2)) & 0x33333333;
+		x = (x | (x << 1)) & 0x55555555;
+
+		y = (y | (y << 8)) & 0x00FF00FF;
+		y = (y | (y << 4)) & 0x0F0F0F0F;
+		y = (y | (y << 2)) & 0x33333333;
+		y = (y | (y << 1)) & 0x55555555;
+
+		return x | (y << 1);
+	}
+
+	function indexCurve( start : EarNode ) {
+		var p = start;
+		do {
+			if( p.z < 0 ) p.z = zOrder(p.x, p.y);
+			p.prevZ = p.prev;
+			p.nextZ = p.next;
+			p = p.next;
+		} while (p != start);
+
+		p.prevZ.nextZ = null;
+		p.prevZ = null;
+
+		sortLinked(p);
+	}
+
+	function sortLinked(list:EarNode) {
+		var p : EarNode, q : EarNode, e : EarNode, tail : EarNode, numMerges, pSize, qSize,
+			inSize = 1;
+
+		do {
+			p = list;
+			list = null;
+			tail = null;
+			numMerges = 0;
+
+			while (p != null) {
+				numMerges++;
+				q = p;
+				pSize = 0;
+				for( i in 0...inSize ) {
+					pSize++;
+					q = q.nextZ;
+					if (q == null) break;
+				}
+
+				qSize = inSize;
+
+				while (pSize > 0 || (qSize > 0 && q != null)) {
+
+					if (pSize == 0) {
+						e = q;
+						q = q.nextZ;
+						qSize--;
+					} else if (qSize == 0 || q == null) {
+						e = p;
+						p = p.nextZ;
+						pSize--;
+					} else if (p.z <= q.z) {
+						e = p;
+						p = p.nextZ;
+						pSize--;
+					} else {
+						e = q;
+						q = q.nextZ;
+						qSize--;
+					}
+
+					if (tail != null) tail.nextZ = e;
+					else list = e;
+
+					e.prevZ = tail;
+					tail = e;
+				}
+
+				p = q;
+			}
+
+			tail.nextZ = null;
+			inSize *= 2;
+
+		} while (numMerges > 1);
+
+		return list;
+	}
+
+}

+ 14 - 9
samples/draw/Draw.hx

@@ -1,13 +1,12 @@
 class Draw extends hxd.App {
 class Draw extends hxd.App {
 
 
 	var bclone : h2d.Bitmap;
 	var bclone : h2d.Bitmap;
+	var texture : h3d.mat.Texture;
 
 
 	override function init() {
 	override function init() {
 		var g = new h2d.Graphics(s2d);
 		var g = new h2d.Graphics(s2d);
 		g.beginFill(0xFF0000);
 		g.beginFill(0xFF0000);
 		g.drawRect(10, 10, 100, 100);
 		g.drawRect(10, 10, 100, 100);
-		g.addHole();
-		g.drawRect(20, 20, 80, 80);
 		g.beginFill(0x00FF00, 0.5);
 		g.beginFill(0x00FF00, 0.5);
 		g.lineStyle(1, 0xFF00FF);
 		g.lineStyle(1, 0xFF00FF);
 		g.drawCircle(100, 100, 30);
 		g.drawCircle(100, 100, 30);
@@ -25,7 +24,7 @@ class Draw extends hxd.App {
 		g.lineStyle();
 		g.lineStyle();
 
 
 		g.beginTileFill(-32,-32,tile);
 		g.beginTileFill(-32,-32,tile);
-		//g.drawPie(0, 0, 32, Math.PI / 3, Math.PI);
+		g.drawPie(0, 0, 32, Math.PI / 3, Math.PI * 5 / 4);
 		g.endFill();
 		g.endFill();
 
 
 		g.beginTileFill(100, -64, 2, 2, tile);
 		g.beginTileFill(100, -64, 2, 2, tile);
@@ -60,11 +59,9 @@ class Draw extends hxd.App {
 
 
 		// check drawTo texture
 		// check drawTo texture
 
 
-		var t = new h3d.mat.Texture(256, 256,[Target]);
-		var b = new h2d.Bitmap(h2d.Tile.fromTexture(t), s2d);
+		texture = new h3d.mat.Texture(256, 256,[Target]);
+		var b = new h2d.Bitmap(h2d.Tile.fromTexture(texture), s2d);
 		b.blendMode = None; // prevent residual alpha bugs
 		b.blendMode = None; // prevent residual alpha bugs
-		var timer = new haxe.Timer(100);
-		timer.run = function() redraw(t);
 		b.y = 256;
 		b.y = 256;
 
 
 		// test capture bitmap
 		// test capture bitmap
@@ -79,8 +76,12 @@ class Draw extends hxd.App {
 	function redraw(t:h3d.mat.Texture) {
 	function redraw(t:h3d.mat.Texture) {
 		var g = new h2d.Graphics();
 		var g = new h2d.Graphics();
 		g.beginFill(0xFF8040, 0.5);
 		g.beginFill(0xFF8040, 0.5);
-		for( i in 0...10 )
-			g.drawCircle(Math.random() * 256, Math.random() * 256, (0.1 + Math.random()) * 10);
+		for( i in 0...100 ) {
+			var r = (0.1 + Math.random()) * 10;
+			var s = Math.random() * Math.PI * 2;
+			var a = Math.random() * Math.PI * 2;
+			g.drawPie(Math.random() * 256, Math.random() * 256, r, s, a);
+		}
 		g.filters = [new h2d.filter.Blur(2,2,10)];
 		g.filters = [new h2d.filter.Blur(2,2,10)];
 		g.drawTo(t);
 		g.drawTo(t);
 
 
@@ -89,6 +90,10 @@ class Draw extends hxd.App {
 		pix.dispose();
 		pix.dispose();
 	}
 	}
 
 
+	override function update(dt:Float) {
+		redraw(texture);
+	}
+
 	static function main() {
 	static function main() {
 		new Draw();
 		new Draw();
 	}
 	}