瀏覽代碼

Merge branch 'master' of git://github.com/ncannasse/h3d

Nicolas Cannasse 11 年之前
父節點
當前提交
a2981d84c7
共有 100 個文件被更改,包括 17468 次插入7 次删除
  1. 5 5
      .gitignore
  2. 2 2
      README.md
  3. 34 0
      engine.hxml
  4. 53 0
      engine.hxproj
  5. 1 0
      extraParams.hxml
  6. 39 0
      h2d/Anim.hx
  7. 20 0
      h2d/Bitmap.hx
  8. 10 0
      h2d/BlendMode.hx
  9. 126 0
      h2d/CachedBitmap.hx
  10. 317 0
      h2d/Console.hx
  11. 465 0
      h2d/Drawable.hx
  12. 90 0
      h2d/Font.hx
  13. 302 0
      h2d/Graphics.hx
  14. 98 0
      h2d/HtmlText.hx
  15. 217 0
      h2d/Interactive.hx
  16. 88 0
      h2d/Layers.hx
  17. 27 0
      h2d/Mask.hx
  18. 3 0
      h2d/RenderContext.hx
  19. 106 0
      h2d/ScaleGrid.hx
  20. 442 0
      h2d/Scene.hx
  21. 16 0
      h2d/Scene3D.hx
  22. 339 0
      h2d/Sprite.hx
  23. 165 0
      h2d/SpriteBatch.hx
  24. 127 0
      h2d/Text.hx
  25. 270 0
      h2d/Tile.hx
  26. 195 0
      h2d/TileColorGroup.hx
  27. 110 0
      h2d/TileGroup.hx
  28. 74 0
      h2d/Tools.hx
  29. 131 0
      h2d/col/Bounds.hx
  30. 39 0
      h2d/col/Circle.hx
  31. 0 0
      h2d/col/Delaunay.hx
  32. 65 0
      h2d/col/Point.hx
  33. 77 0
      h2d/col/Poly.hx
  34. 146 0
      h2d/col/Polynomial.hx
  35. 62 0
      h2d/col/Seg.hx
  36. 47 0
      h2d/col/Triangle.hx
  37. 1502 0
      h2d/col/Voronoi.hx
  38. 216 0
      h2d/comp/Box.hx
  39. 36 0
      h2d/comp/Button.hx
  40. 37 0
      h2d/comp/Checkbox.hx
  41. 63 0
      h2d/comp/Color.hx
  42. 681 0
      h2d/comp/ColorPicker.hx
  43. 311 0
      h2d/comp/Component.hx
  44. 47 0
      h2d/comp/Context.hx
  45. 652 0
      h2d/comp/GradientEditor.hx
  46. 120 0
      h2d/comp/Input.hx
  47. 56 0
      h2d/comp/Interactive.hx
  48. 77 0
      h2d/comp/ItemList.hx
  49. 132 0
      h2d/comp/JQuery.hx
  50. 38 0
      h2d/comp/Label.hx
  51. 266 0
      h2d/comp/Parser.hx
  52. 138 0
      h2d/comp/Select.hx
  53. 77 0
      h2d/comp/Slider.hx
  54. 67 0
      h2d/comp/Value.hx
  55. 45 0
      h2d/css/Defs.hx
  56. 105 0
      h2d/css/Engine.hx
  57. 63 0
      h2d/css/Fill.hx
  58. 1016 0
      h2d/css/Parser.hx
  59. 122 0
      h2d/css/Style.hx
  60. 174 0
      h2d/css/default.css
  61. 214 0
      h3d/Camera.hx
  62. 24 0
      h3d/Drawable.hx
  63. 351 0
      h3d/Engine.hx
  64. 5 0
      h3d/IDrawable.hx
  65. 564 0
      h3d/Matrix.hx
  66. 257 0
      h3d/Quat.hx
  67. 163 0
      h3d/Vector.hx
  68. 225 0
      h3d/anim/Animation.hx
  69. 80 0
      h3d/anim/FrameAnimation.hx
  70. 188 0
      h3d/anim/LinearAnimation.hx
  71. 35 0
      h3d/anim/SimpleBlend.hx
  72. 201 0
      h3d/anim/Skin.hx
  73. 127 0
      h3d/anim/SmoothTransition.hx
  74. 60 0
      h3d/anim/Transition.hx
  75. 297 0
      h3d/col/Bounds.hx
  76. 92 0
      h3d/col/Plane.hx
  77. 115 0
      h3d/col/Point.hx
  78. 85 0
      h3d/col/Ray.hx
  79. 30 0
      h3d/col/Seg.hx
  80. 144 0
      h3d/fbx/Data.hx
  81. 64 0
      h3d/fbx/Filter.hx
  82. 101 0
      h3d/fbx/Geometry.hx
  83. 843 0
      h3d/fbx/Library.hx
  84. 273 0
      h3d/fbx/Parser.hx
  85. 104 0
      h3d/fbx/XBXReader.hx
  86. 76 0
      h3d/fbx/XBXWriter.hx
  87. 3 0
      h3d/impl/AllocPos.hx
  88. 91 0
      h3d/impl/Buffer.hx
  89. 68 0
      h3d/impl/DebugGL.hx
  90. 125 0
      h3d/impl/Driver.hx
  91. 546 0
      h3d/impl/GlDriver.hx
  92. 30 0
      h3d/impl/Indexes.hx
  93. 566 0
      h3d/impl/MemoryManager.hx
  94. 120 0
      h3d/impl/NullDriver.hx
  95. 121 0
      h3d/impl/Shader.hx
  96. 88 0
      h3d/impl/Shaders.hx
  97. 475 0
      h3d/impl/Stage3dDriver.hx
  98. 15 0
      h3d/mat/Bitmap.hx
  99. 60 0
      h3d/mat/Data.hx
  100. 93 0
      h3d/mat/Material.hx

+ 5 - 5
.gitignore

@@ -1,5 +1,5 @@
-/Render.hx
-/Shaders.hx
-/*.swf
-/hxsl.js
-/hxsl.js.map
+*.n
+*.swf
+*.js
+*.js.map
+/bin

+ 2 - 2
README.md

@@ -1,4 +1,4 @@
-hxsl3
+Heaps
 =====
 
-High Level Cross Platform Shader Language v3
+The GPU Game Framework

+ 34 - 0
engine.hxml

@@ -0,0 +1,34 @@
+-cp samples/basic
+-js engine.js
+-main Test
+--macro include('h3d')
+--macro include('h2d')
+--macro include('hxd',true,['hxd.res.FileTree'])
+-lib format
+-D resourcesPath=samples/res
+--next
+-swf engine.swf
+-swf-header 800:600:60:000000
+-swf-version 11.6
+-lib hxsl
+-D h3d
+-D resourcesPath=samples/res
+-cp samples/basic
+-main Test
+--macro include('h3d')
+--macro include('h2d')
+--macro include('hxd',true,['hxd.res.FileTree'])
+--next
+-cpp bin
+-lib format
+-lib openfl-native
+-debug
+--macro allowPackage('flash')
+-D h3d
+-D openfl
+-D resourcesPath=samples/res
+-cp samples/basic
+-main Test
+--macro include('h3d')
+--macro include('h2d')
+--macro include('hxd',true,['hxd.res.FileTree'])

+ 53 - 0
engine.hxproj

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<project version="2">
+  <!-- Output SWF options -->
+  <output>
+    <movie outputType="Application" />
+    <movie input="" />
+    <movie path="engine.js" />
+    <movie fps="60" />
+    <movie width="800" />
+    <movie height="600" />
+    <movie version="1" />
+    <movie minorVersion="0" />
+    <movie platform="JavaScript" />
+    <movie background="#FFFFFF" />
+  </output>
+  <!-- Other classes to be compiled into your SWF -->
+  <classpaths>
+    <class path="samples\basic" />
+  </classpaths>
+  <!-- Build options -->
+  <build>
+    <option directives="" />
+    <option flashStrict="True" />
+    <option mainClass="Test" />
+    <option enabledebug="False" />
+    <option additional="--macro include('h3d')&#xA;--macro include('h2d')&#xA;--macro include('hxd',true,['hxd.res.FileTree'])&#xA;-lib format&#xA;-D resourcesPath=samples/res&#xA;&#xA;--next&#xA;&#xA;-swf engine.swf&#xA;-swf-header 800:600:60:000000&#xA;-swf-version 11.6&#xA;-lib hxsl&#xA;-D h3d&#xA;-D resourcesPath=samples/res&#xA;-cp samples/basic&#xA;-main Test&#xA;--macro include('h3d')&#xA;--macro include('h2d')&#xA;--macro include('hxd',true,['hxd.res.FileTree'])&#xA;&#xA;--next&#xA;-cpp bin&#xA;-lib format&#xA;-lib openfl-native&#xA;-debug&#xA;--macro allowPackage('flash')&#xA;-D h3d&#xA;-D openfl&#xA;-D resourcesPath=samples/res&#xA;-cp samples/basic&#xA;-main Test&#xA;--macro include('h3d')&#xA;--macro include('h2d')&#xA;--macro include('hxd',true,['hxd.res.FileTree'])" />
+  </build>
+  <!-- haxelib libraries -->
+  <haxelib>
+    <!-- example: <library name="..." /> -->
+  </haxelib>
+  <!-- Class files to compile (other referenced classes will automatically be included) -->
+  <compileTargets>
+    <!-- example: <compile path="..." /> -->
+  </compileTargets>
+  <!-- Paths to exclude from the Project Explorer tree -->
+  <hiddenPaths>
+    <hidden path="engine.hxml" />
+    <hidden path="obj" />
+  </hiddenPaths>
+  <!-- Executed before build -->
+  <preBuildCommand />
+  <!-- Executed after build -->
+  <postBuildCommand alwaysRun="False" />
+  <!-- Other project options -->
+  <options>
+    <option showHiddenPaths="False" />
+    <option testMovie="OpenDocument" />
+    <option testMovieCommand="index.html" />
+  </options>
+  <!-- Plugin storage -->
+  <storage />
+</project>

+ 1 - 0
extraParams.hxml

@@ -0,0 +1 @@
+-swf-version 11.6

+ 39 - 0
h2d/Anim.hx

@@ -0,0 +1,39 @@
+package h2d;
+
+class Anim extends Drawable {
+
+	public var frames : Array<Tile>;
+	public var currentFrame : Float;
+	public var speed : Float;
+	public var loop : Bool = true;
+	
+	public function new( ?frames, ?speed, ?parent ) {
+		super(parent);
+		this.frames = frames == null ? [] : frames;
+		this.currentFrame = 0;
+		this.speed = speed == null ? 15 : speed;
+	}
+	
+	public function play( frames ) {
+		this.frames = frames;
+		this.currentFrame = 0;
+	}
+	
+	override function sync( ctx : RenderContext ) {
+		currentFrame += speed * ctx.elapsedTime;
+		if( loop )
+			currentFrame %= frames.length;
+		else if( currentFrame >= frames.length )
+			currentFrame = frames.length - 0.00001;
+	}
+	
+	public function getFrame() {
+		return frames[Std.int(currentFrame)];
+	}
+	
+	override function draw( ctx : RenderContext ) {
+		var t = getFrame();
+		if( t != null ) drawTile(ctx.engine,t);
+	}
+	
+}

+ 20 - 0
h2d/Bitmap.hx

@@ -0,0 +1,20 @@
+package h2d;
+
+class Bitmap extends Drawable {
+
+	public var tile : Tile;
+	
+	public function new( ?tile, ?parent ) {
+		super(parent);
+		this.tile = tile;
+	}
+	
+	override function draw( ctx : RenderContext ) {
+		drawTile(ctx.engine,tile);
+	}
+			
+	public static function create( bmp : hxd.BitmapData, ?allocPos : h3d.impl.AllocPos ) {
+		return new Bitmap(Tile.fromBitmap(bmp,allocPos));
+	}
+	
+}

+ 10 - 0
h2d/BlendMode.hx

@@ -0,0 +1,10 @@
+package h2d;
+
+enum BlendMode {
+	Normal;
+	None;
+	Add;
+	SoftAdd;
+	Multiply;
+	Erase;
+}

+ 126 - 0
h2d/CachedBitmap.hx

@@ -0,0 +1,126 @@
+package h2d;
+
+class CachedBitmap extends Drawable {
+
+	var tex : h3d.mat.Texture;
+	public var width(default, set) : Int;
+	public var height(default, set) : Int;
+	public var freezed : Bool;
+	
+	var renderDone : Bool;
+	var realWidth : Int;
+	var realHeight : Int;
+	var tile : Tile;
+	
+	public function new( ?parent, width = -1, height = -1 ) {
+		super(parent);
+		this.width = width;
+		this.height = height;
+	}
+
+	function clean() {
+		if( tex != null ) {
+			tex.dispose();
+			tex = null;
+		}
+		tile = null;
+	}
+
+	override function onDelete() {
+		clean();
+		super.onDelete();
+	}
+	
+	function set_width(w) {
+		clean();
+		width = w;
+		return w;
+	}
+
+	function set_height(h) {
+		if( tex != null ) {
+			tex.dispose();
+			tex = null;
+		}
+		height = h;
+		return h;
+	}
+	
+	public function getTile() {
+		if( tile == null ) {
+			var tw = 1, th = 1;
+			var engine = h3d.Engine.getCurrent();
+			realWidth = width < 0 ? engine.width : width;
+			realHeight = height < 0 ? engine.height : height;
+			while( tw < realWidth ) tw <<= 1;
+			while( th < realHeight ) th <<= 1;
+			tex = engine.mem.allocTargetTexture(tw, th);
+			renderDone = false;
+			tile = new Tile(tex,0, 0, realWidth, realHeight);
+		}
+		return tile;
+	}
+
+	override function drawRec( ctx : RenderContext ) {
+		drawTile(ctx.engine, tile);
+	}
+	
+	override function sync( ctx : RenderContext ) {
+		if( posChanged ) {
+			calcAbsPos();
+			for( c in childs )
+				c.posChanged = true;
+			posChanged = false;
+		}
+		if( tex != null && ((width < 0 && tex.width < ctx.engine.width) || (height < 0 && tex.height < ctx.engine.height)) )
+			clean();
+		var tile = getTile();
+		if( !freezed || !renderDone ) {
+			var oldA = matA, oldB = matB, oldC = matC, oldD = matD, oldX = absX, oldY = absY;
+			
+			// init matrix without rotation
+			matA = 1;
+			matB = 0;
+			matC = 0;
+			matD = 1;
+			absX = 0;
+			absY = 0;
+			
+			// adds a pixels-to-viewport transform
+			var w = 2 / tex.width;
+			var h = -2 / tex.height;
+			absX = absX * w - 1;
+			absY = absY * h + 1;
+			matA *= w;
+			matB *= h;
+			matC *= w;
+			matD *= h;
+
+			// force full resync
+			for( c in childs ) {
+				c.posChanged = true;
+				c.sync(ctx);
+			}
+
+			ctx.engine.setTarget(tex);
+			ctx.engine.setRenderZone(0, 0, realWidth, realHeight);
+			for( c in childs )
+				c.drawRec(ctx);
+			ctx.engine.setTarget(null);
+			ctx.engine.setRenderZone();
+			
+			// restore
+			matA = oldA;
+			matB = oldB;
+			matC = oldC;
+			matD = oldD;
+			absX = oldX;
+			absY = oldY;
+			
+			renderDone = true;
+		}
+
+		super.sync(ctx);
+	}
+	
+}

+ 317 - 0
h2d/Console.hx

@@ -0,0 +1,317 @@
+package h2d;
+
+import hxd.Key;
+
+enum ConsoleArg {
+	AInt;
+	AFloat;
+	AString;
+	ABool;
+	AEnum( values : Array<String> );
+}
+
+class Console extends h2d.Sprite {
+
+	public static var HIDE_LOG_TIMEOUT = 3.;
+	
+	var width : Int;
+	var height : Int;
+	var bg : h2d.Bitmap;
+	var tf : h2d.Text;
+	var logTxt : h2d.HtmlText;
+	var cursor : h2d.Bitmap;
+	var cursorPos(default, set) : Int;
+	var lastLogTime : Float;
+	var commands : Map < String, { help : String, args : Array<{ name : String, t : ConsoleArg, ?opt : Bool }>, callb : Dynamic } > ;
+	var aliases : Map<String,String>;
+	var logDY : Float = 0;
+	var logs : Array<String>;
+	var logIndex:Int;
+	var curCmd:String;
+	
+	public var shortKeyChar : Int = "/".code;
+	
+	public function new(font:h2d.Font,parent) {
+		super(parent);
+		height = font.lineHeight + 2;
+		logTxt = new h2d.HtmlText(font, this);
+		logTxt.x = 2;
+		logTxt.visible = false;
+		logs = [];
+		logIndex = -1;
+		bg = new h2d.Bitmap(h2d.Tile.fromColor(0x80000000), this);
+		bg.visible = false;
+		tf = new h2d.Text(font, bg);
+		tf.x = 2;
+		tf.y = 1;
+		tf.textColor = 0xFFFFFFFF;
+		cursor = new h2d.Bitmap(h2d.Tile.fromColor(tf.textColor | 0xFF000000, 1, font.lineHeight), tf);
+		commands = new Map();
+		aliases = new Map();
+		addCommand("help", "Show help", [ { name : "command", t : AString, opt : true } ], showHelp);
+		addAlias("?", "help");
+	}
+	
+	public function addCommand( name, help, args, callb : Dynamic ) {
+		commands.set(name, { help : help, args:args, callb:callb } );
+	}
+	
+	public function addAlias( name, command ) {
+		aliases.set(name, command);
+	}
+	
+	public function runCommand( commandLine : String ) {
+		handleCommand(commandLine);
+	}
+	
+	override function onAlloc() {
+		super.onAlloc();
+		getScene().addEventListener(onEvent);
+	}
+	
+	override function onDelete() {
+		getScene().removeEventListener(onEvent);
+		super.onDelete();
+	}
+	
+	function onEvent( e : hxd.Event ) {
+		switch( e.kind ) {
+		case EWheel:
+			if( logTxt.visible ) {
+				logDY -= tf.font.lineHeight * e.wheelDelta * 3;
+				if( logDY < 0 ) logDY = 0;
+				if( logDY > logTxt.textHeight ) logDY = logTxt.textHeight;
+				e.propagate = false;
+			}
+		case EKeyDown:
+			handleKey(e);
+			if( bg.visible ) e.propagate = false;
+		default:
+		}
+	}
+	
+	function showHelp( ?command : String ) {
+		var all;
+		if( command == null ) {
+			all = Lambda.array( { iterator : commands.keys } );
+			all.sort(Reflect.compare);
+			all.remove("help");
+			all.push("help");
+		} else {
+			if( aliases.exists(command) ) command = aliases.get(command);
+			if( !commands.exists(command) )
+				throw 'Command not found "$command"';
+			all = [command];
+		}
+		for( cmdName in all ) {
+			var c = commands.get(cmdName);
+			var str = "/" + cmdName;
+			for( a in aliases.keys() )
+				if( aliases.get(a) == cmdName )
+					str += "|" + a;
+			for( a in c.args ) {
+				var astr = a.name;
+				switch( a.t ) {
+				case AInt, AFloat:
+					astr += ":"+a.t.getName().substr(1);
+				case AString:
+					// nothing
+				case AEnum(values):
+					astr += "=" + values.join("|");
+				case ABool:
+					astr += "=0|1";
+				}
+				str += " " + (a.opt?"["+astr+"]":astr);
+			}
+			if( c.help != "" )
+				str += " : " + c.help;
+			log(str);
+		}
+	}
+	
+	public function isActive() {
+		return bg.visible;
+	}
+	
+	function set_cursorPos(v:Int) {
+		cursor.x = tf.calcTextWidth(tf.text.substr(0, v));
+		return cursorPos = v;
+	}
+	
+	function handleKey( e : hxd.Event ) {
+		if( e.charCode == shortKeyChar && !bg.visible ) {
+			bg.visible = true;
+			logIndex = -1;
+		}
+		if( !bg.visible )
+			return;
+		switch( e.keyCode ) {
+		case Key.LEFT:
+			if( cursorPos > 0 )
+				cursorPos--;
+		case Key.RIGHT:
+			if( cursorPos < tf.text.length )
+				cursorPos++;
+		case Key.HOME:
+			cursorPos = 0;
+		case Key.END:
+			cursorPos = tf.text.length;
+		case Key.DELETE:
+			tf.text = tf.text.substr(0, cursorPos) + tf.text.substr(cursorPos + 1);
+			return;
+		case Key.BACKSPACE:
+			if( cursorPos > 0 ) {
+				tf.text = tf.text.substr(0, cursorPos - 1) + tf.text.substr(cursorPos);
+				cursorPos--;
+			}
+			return;
+		case Key.ENTER:
+			var cmd = tf.text;
+			tf.text = "";
+			cursorPos = 0;
+			handleCommand(cmd);
+			if( !logTxt.visible ) bg.visible = false;
+			return;
+		case Key.ESCAPE:
+			hide();
+			return;
+		case Key.UP:
+			if(logs.length == 0 || logIndex == 0) return;
+			if(logIndex == -1) {
+				curCmd = tf.text;
+				logIndex = logs.length - 1;
+			}
+			else logIndex--;
+			tf.text = logs[logIndex];
+			cursorPos = tf.text.length;
+		case Key.DOWN:
+			if(tf.text == curCmd) return;
+			if(logIndex == logs.length - 1) {
+				tf.text = curCmd;
+				cursorPos = tf.text.length;
+				logIndex = -1;
+				return;
+			}
+			logIndex++;
+			tf.text = logs[logIndex];
+			cursorPos = tf.text.length;
+		}
+		if( e.charCode != 0 ) {
+			tf.text = curCmd = tf.text.substr(0, cursorPos) + String.fromCharCode(e.charCode) + tf.text.substr(cursorPos);
+			cursorPos++;
+		}
+	}
+	
+	function hide() {
+		bg.visible = false;
+		tf.text = "";
+	}
+	
+	function handleCommand( command : String ) {
+		command = StringTools.trim(command);
+		if( command.charCodeAt(0) == "/".code ) command = command.substr(1);
+		if( command == "" ) {
+			hide();
+			return;
+		}
+		logs.push(command);
+		logIndex = -1;
+		
+		var args = ~/[ \t]+/g.split(command);
+		var cmdName = args[0];
+		if( aliases.exists(cmdName) ) cmdName = aliases.get(cmdName);
+		var cmd = commands.get(cmdName);
+		var errorColor = 0xC00000;
+		if( cmd == null ) {
+			log('Unknown command "${cmdName}"',errorColor);
+			return;
+		}
+		var vargs = new Array<Dynamic>();
+		for( i in 0...cmd.args.length ) {
+			var a = cmd.args[i];
+			var v = args[i + 1];
+			if( v == null ) {
+				if( a.opt ) {
+					vargs.push(null);
+					continue;
+				}
+				log('Missing argument ${a.name}',errorColor);
+				return;
+			}
+			switch( a.t ) {
+			case AInt:
+				var i = Std.parseInt(v);
+				if( i == null ) {
+					log('$v should be Int for argument ${a.name}',errorColor);
+					return;
+				}
+				vargs.push(i);
+			case AFloat:
+				var f = Std.parseFloat(v);
+				if( Math.isNaN(f) ) {
+					log('$v should be Float for argument ${a.name}',errorColor);
+					return;
+				}
+				vargs.push(f);
+			case ABool:
+				switch( v ) {
+				case "true", "1": vargs.push(true);
+				case "false", "0": vargs.push(false);
+				default:
+					log('$v should be Bool for argument ${a.name}',errorColor);
+					return;
+				}
+			case AString:
+				vargs.push(v);
+			case AEnum(values):
+				var found = false;
+				for( v2 in values )
+					if( v == v2 ) {
+						found = true;
+						vargs.push(v2);
+					}
+				if( !found ) {
+					log('$v should be [${values.join("|")}] for argument ${a.name}', errorColor);
+					return;
+				}
+			}
+		}
+		try {
+			Reflect.callMethod(null, cmd.callb, vargs);
+		} catch( e : String ) {
+			log('ERROR $e', errorColor);
+		}
+	}
+	
+	public function log( text : String, ?color ) {
+		if( color == null ) color = tf.textColor;
+		var oldH = logTxt.textHeight;
+		logTxt.htmlText += '<font color="#${StringTools.hex(color&0xFFFFFF,6)}">${StringTools.htmlEscape(text)}</font><br/>';
+		if( logDY != 0 ) logDY += logTxt.textHeight - oldH;
+		logTxt.alpha = 1;
+		logTxt.visible = true;
+		lastLogTime = haxe.Timer.stamp();
+	}
+	
+	override function sync(ctx:h2d.RenderContext) {
+		var scene = getScene();
+		if( scene != null ) {
+			x = 0;
+			y = scene.height - height;
+			width = scene.width;
+			bg.tile.scaleToSize(width, height);
+		}
+		var log = logTxt;
+		if( log.visible ) {
+			log.y = bg.y - log.textHeight + logDY;
+			var dt = haxe.Timer.stamp() - lastLogTime;
+			if( dt > HIDE_LOG_TIMEOUT && !bg.visible ) {
+				log.alpha -= ctx.elapsedTime * 4;
+				if( log.alpha <= 0 )
+					log.visible = false;
+			}
+		}
+		super.sync(ctx);
+	}
+	
+}

+ 465 - 0
h2d/Drawable.hx

@@ -0,0 +1,465 @@
+package h2d;
+
+private class DrawableShader extends h3d.impl.Shader {
+	#if flash
+	static var SRC = {
+		var input : {
+			pos : Float2,
+			uv : Float2,
+			valpha : Float,
+			vcolor : Float4,
+		};
+		var tuv : Float2;
+		var tcolor : Float4;
+		var talpha : Float;
+
+		var hasVertexColor : Bool;
+		var hasVertexAlpha : Bool;
+		var uvScale : Float2;
+		var uvPos : Float2;
+		var skew : Float;
+		var zValue : Float;
+
+		function vertex( size : Float3, matA : Float3, matB : Float3 ) {
+			var tmp : Float4;
+			var spos = input.pos.xyw;
+			if( size != null ) spos *= size;
+			tmp.x = spos.dp3(matA);
+			tmp.y = spos.dp3(matB);
+			tmp.z = zValue;
+			tmp.w = skew != null ? 1 - skew * input.pos.y : 1;
+			out = tmp;
+			var t = input.uv;
+			if( uvScale != null ) t *= uvScale;
+			if( uvPos != null ) t += uvPos;
+			tuv = t;
+			if( hasVertexColor ) tcolor = input.vcolor;
+			if( hasVertexAlpha ) talpha = input.valpha;
+		}
+		
+		var hasAlpha : Bool;
+		var killAlpha : Bool;
+		
+		var alpha : Float;
+		var colorAdd : Float4;
+		var colorMul : Float4;
+		var colorMatrix : M44;
+
+		var hasAlphaMap : Bool;
+		var alphaMap : Texture;
+		var alphaUV : Float4;
+		var filter : Bool;
+		
+		var sinusDeform : Float3;
+		var tileWrap : Bool;
+
+		var hasMultMap : Bool;
+		var multMapFactor : Float;
+		var multMap : Texture;
+		var multUV : Float4;
+		var hasColorKey : Bool;
+		var colorKey : Int;
+
+		function fragment( tex : Texture ) {
+			var col = tex.get(sinusDeform != null ? [tuv.x + sin(tuv.y * sinusDeform.y + sinusDeform.x) * sinusDeform.z, tuv.y] : tuv, filter = ! !filter, wrap = tileWrap);
+			if( hasColorKey ) {
+				var cdiff = col.rgb - colorKey.rgb;
+				kill(cdiff.dot(cdiff) - 0.00001);
+			}
+			if( killAlpha ) kill(col.a - 0.001);
+			if( hasVertexAlpha ) col.a *= talpha;
+			if( hasVertexColor ) col *= tcolor;
+			if( hasAlphaMap ) col.a *= alphaMap.get(tuv * alphaUV.zw + alphaUV.xy).r;
+			if( hasMultMap ) col *= multMap.get(tuv * multUV.zw + multUV.xy) * multMapFactor;
+			if( hasAlpha ) col.a *= alpha;
+			if( colorMatrix != null ) col *= colorMatrix;
+			if( colorMul != null ) col *= colorMul;
+			if( colorAdd != null ) col += colorAdd;
+			out = col;
+		}
+
+
+	}
+	
+	#elseif (js || cpp)
+	
+	public var hasColorKey : Bool;
+	
+	// not supported
+	public var skew : Float;
+	public var sinusDeform : h3d.Vector;
+	public var hasAlphaMap : Bool;
+	public var hasMultMap : Bool;
+	public var multMap : h3d.mat.Texture;
+	public var multUV : h3d.Vector;
+	public var multMapFactor : Float;
+	public var alphaMap : h3d.mat.Texture;
+	public var alphaUV : h3d.Vector;
+	// --
+	
+	public var filter : Bool;
+	public var tileWrap : Bool;
+	public var killAlpha : Bool;
+	public var hasAlpha : Bool;
+	public var hasVertexAlpha : Bool;
+	public var hasVertexColor : Bool;
+	
+	override function customSetup(driver:h3d.impl.GlDriver) {
+		driver.setupTexture(tex, None, filter ? Linear : Nearest, tileWrap ? Repeat : Clamp);
+	}
+	
+	override function getConstants( vertex : Bool ) {
+		var cst = [];
+		if( vertex ) {
+			if( size != null ) cst.push("#define hasSize");
+			if( uvScale != null ) cst.push("#define hasUVScale");
+			if( uvPos != null ) cst.push("#define hasUVPos");
+		} else {
+			if( killAlpha ) cst.push("#define killAlpha");
+			if( hasColorKey ) cst.push("#define hasColorKey");
+			if( hasAlpha ) cst.push("#define hasAlpha");
+			if( colorMatrix != null ) cst.push("#define hasColorMatrix");
+			if( colorMul != null ) cst.push("#define hasColorMul");
+			if( colorAdd != null ) cst.push("#define hasColorAdd");
+		}
+		if( hasVertexAlpha ) cst.push("#define hasVertexAlpha");
+		if( hasVertexColor ) cst.push("#define hasVertexColor");
+		return cst.join("\n");
+	}
+	
+	static var VERTEX = "
+	
+		attribute vec2 pos;
+		attribute vec2 uv;
+		#if hasVertexAlpha
+		attribute float valpha;
+		varying lowp float talpha;
+		#end
+		#if hasVertexColor
+		attribute vec4 vcolor;
+		varying lowp vec4 tcolor;
+		#end
+
+        #if hasSize
+		uniform vec3 size;
+		#end
+		uniform vec3 matA;
+		uniform vec3 matB;
+		uniform lowp float zValue;
+		
+        #if hasUVPos
+		uniform vec2 uvPos;
+		#end
+        #if hasUVScale
+		uniform vec2 uvScale;
+		#end
+		
+		varying lowp vec2 tuv;
+
+		void main(void) {
+			vec3 spos = vec3(pos.xy, 1.0);
+			#if hasSize
+				spos = spos * size;
+			#end
+			vec4 tmp;
+			tmp.x = dot(spos,matA);
+			tmp.y = dot(spos,matB);
+			tmp.z = zValue;
+			tmp.w = 1.;
+			gl_Position = tmp;
+			vec2 t = uv;
+			#if hasUVScale
+				t *= uvScale;
+			#end
+			#if hasUVPos
+				t += uvPos;
+			#end
+			tuv = t;
+			#if hasVertexAlpha
+				talpha = valpha;
+			#end
+			#if hasVertexColor
+				tcolor = vcolor;
+			#end
+		}
+
+	";
+	
+	static var FRAGMENT = "
+	
+		varying lowp vec2 tuv;
+		uniform sampler2D tex;
+		
+		#if hasVertexAlpha
+		varying lowp float talpha;
+		#end
+		#if hasVertexColor
+		varying lowp vec4 tcolor;
+		#end
+		
+		uniform lowp float alpha;
+		uniform lowp vec3 colorKey/*byte4*/;
+	
+		uniform lowp vec4 colorAdd;
+		uniform lowp vec4 colorMul;
+		uniform mediump mat4 colorMatrix;
+
+		void main(void) {
+			lowp vec4 col = texture2D(tex, tuv);
+			#if killAlpha
+				if( c.a - 0.001 ) discard;
+			#end
+			#if hasColorKey
+				lowp vec3 dc = col.rgb - colorKey;
+				if( dot(dc,dc) < 0.001 ) discard;
+			#end
+			#if hasAlpha
+				col.w *= alpha;
+			#end
+			#if hasVertexAlpha
+				col.a *= talpha;
+			#end
+			#if hasVertexColor
+				col *= tcolor;
+			#end
+			#if hasColorMatrix
+				col = colorMatrix * col;
+			#end
+			#if hasColorMul
+				col *= colorMul;
+			#end
+			#if hasColorAdd
+				col += colorAdd;
+			#end
+			gl_FragColor = col;
+		}
+			
+	";
+	
+	#end
+}
+
+class Drawable extends Sprite {
+	
+	static inline var HAS_SIZE = 1;
+	static inline var HAS_UV_SCALE = 2;
+	static inline var HAS_UV_POS = 4;
+
+	var shader : DrawableShader;
+	
+	public var alpha(get, set) : Float;
+	public var skew(get, set) : Null<Float>;
+	
+	public var filter(get, set) : Bool;
+	public var color(get, set) : h3d.Vector;
+	public var colorAdd(get, set) : h3d.Vector;
+	public var colorMatrix(get, set) : h3d.Matrix;
+	
+	public var blendMode(default, set) : BlendMode;
+	
+	public var alphaMap(default, set) : h2d.Tile;
+
+	public var sinusDeform(get, set) : h3d.Vector;
+	public var tileWrap(get, set) : Bool;
+	public var killAlpha(get, set) : Bool;
+
+	public var multiplyMap(default, set) : h2d.Tile;
+	public var multiplyFactor(get, set) : Float;
+	
+	public var colorKey(get, set) : Int;
+	
+	public var writeAlpha : Bool;
+	
+	function new(parent) {
+		super(parent);
+		shader = new DrawableShader();
+		shader.alpha = 1;
+		shader.zValue = 0;
+		writeAlpha = true;
+		blendMode = Normal;
+	}
+	
+	inline function get_alpha() {
+		return shader.alpha;
+	}
+	
+	function set_alpha( v : Null<Float> ) {
+		shader.alpha = v;
+		shader.hasAlpha = v < 1;
+		return v;
+	}
+	
+	function set_blendMode(b) {
+		blendMode = b;
+		return b;
+	}
+	
+	inline function get_skew() : Null<Float> {
+		return shader.skew;
+	}
+	
+	inline function set_skew(v : Null<Float> ) {
+		return shader.skew = skew;
+	}
+
+	inline function get_multiplyFactor() {
+		return shader.multMapFactor;
+	}
+
+	inline function set_multiplyFactor(v) {
+		return shader.multMapFactor = v;
+	}
+	
+	function set_multiplyMap(t:h2d.Tile) {
+		multiplyMap = t;
+		shader.hasMultMap = t != null;
+		return t;
+	}
+	
+	function set_alphaMap(t:h2d.Tile) {
+		alphaMap = t;
+		shader.hasAlphaMap = t != null;
+		return t;
+	}
+	
+	inline function get_sinusDeform() {
+		return shader.sinusDeform;
+	}
+
+	inline function set_sinusDeform(v) {
+		return shader.sinusDeform = v;
+	}
+	
+	inline function get_colorMatrix() {
+		return shader.colorMatrix;
+	}
+	
+	inline function set_colorMatrix(m) {
+		return shader.colorMatrix = m;
+	}
+
+	inline function set_colorAdd(m) {
+		return shader.colorAdd = m;
+	}
+
+	inline function get_colorAdd() {
+		return shader.colorAdd;
+	}
+	
+	inline function get_color() {
+		return shader.colorMul;
+	}
+	
+	inline function set_color(m) {
+		return shader.colorMul = m;
+	}
+
+	inline function get_filter() {
+		return shader.filter;
+	}
+	
+	inline function set_filter(v) {
+		return shader.filter = v;
+	}
+
+	inline function get_tileWrap() {
+		return shader.tileWrap;
+	}
+	
+	inline function set_tileWrap(v) {
+		return shader.tileWrap = v;
+	}
+
+	inline function get_killAlpha() {
+		return shader.killAlpha;
+	}
+	
+	inline function set_killAlpha(v) {
+		return shader.killAlpha = v;
+	}
+
+	inline function get_colorKey() {
+		return shader.colorKey;
+	}
+	
+	inline function set_colorKey(v) {
+		shader.hasColorKey = true;
+		return shader.colorKey = v;
+	}
+	
+	function drawTile( engine, tile ) {
+		setupShader(engine, tile, HAS_SIZE | HAS_UV_POS | HAS_UV_SCALE);
+		engine.renderQuadBuffer(Tools.getCoreObjects().planBuffer);
+	}
+	
+	function setupShader( engine : h3d.Engine, tile : h2d.Tile, options : Int ) {
+		var core = Tools.getCoreObjects();
+		var shader = shader;
+		var mat = core.tmpMaterial;
+
+		if( tile == null )
+			tile = new Tile(core.getEmptyTexture(), 0, 0, 5, 5);
+
+		switch( blendMode ) {
+		case Normal:
+			mat.blend(SrcAlpha, OneMinusSrcAlpha);
+		case None:
+			mat.blend(One, Zero);
+		case Add:
+			mat.blend(SrcAlpha, One);
+		case SoftAdd:
+			mat.blend(OneMinusDstColor, One);
+		case Multiply:
+			mat.blend(DstColor, OneMinusSrcAlpha);
+		case Erase:
+			mat.blend(Zero, OneMinusSrcAlpha);
+		}
+
+		if( options & HAS_SIZE != 0 ) {
+			var tmp = core.tmpSize;
+			// adds 1/10 pixel size to prevent precision loss after scaling
+			tmp.x = tile.width + 0.1;
+			tmp.y = tile.height + 0.1;
+			tmp.z = 1;
+			shader.size = tmp;
+		}
+		if( options & HAS_UV_POS != 0 ) {
+			core.tmpUVPos.x = tile.u;
+			core.tmpUVPos.y = tile.v;
+			shader.uvPos = core.tmpUVPos;
+		}
+		if( options & HAS_UV_SCALE != 0 ) {
+			core.tmpUVScale.x = tile.u2 - tile.u;
+			core.tmpUVScale.y = tile.v2 - tile.v;
+			shader.uvScale = core.tmpUVScale;
+		}
+		
+		if( shader.hasAlphaMap ) {
+			shader.alphaMap = alphaMap.getTexture();
+			shader.alphaUV = new h3d.Vector(alphaMap.u, alphaMap.v, (alphaMap.u2 - alphaMap.u) / tile.u2, (alphaMap.v2 - alphaMap.v) / tile.v2);
+		}
+
+		if( shader.hasMultMap ) {
+			shader.multMap = multiplyMap.getTexture();
+			shader.multUV = new h3d.Vector(multiplyMap.u, multiplyMap.v, (multiplyMap.u2 - multiplyMap.u) / tile.u2, (multiplyMap.v2 - multiplyMap.v) / tile.v2);
+		}
+		
+		var cm = writeAlpha ? 15 : 7;
+		if( mat.colorMask != cm ) mat.colorMask = cm;
+		
+		var tmp = core.tmpMatA;
+		tmp.x = matA;
+		tmp.y = matC;
+		tmp.z = absX + tile.dx * matA + tile.dy * matC;
+		shader.matA = tmp;
+		var tmp = core.tmpMatB;
+		tmp.x = matB;
+		tmp.y = matD;
+		tmp.z = absY + tile.dx * matB + tile.dy * matD;
+		shader.matB = tmp;
+		shader.tex = tile.getTexture();
+		mat.shader = shader;
+		engine.selectMaterial(mat);
+	}
+	
+}

+ 90 - 0
h2d/Font.hx

@@ -0,0 +1,90 @@
+package h2d;
+
+class Kerning {
+	public var prevChar : Int;
+	public var offset : Int;
+	public var next : Null<Kerning>;
+	public function new(c, o) {
+		this.prevChar = c;
+		this.offset = o;
+	}
+}
+
+class FontChar {
+	
+	public var t : h2d.Tile;
+	public var width : Int;
+	var kerning : Null<Kerning>;
+	
+	public function new(t,w) {
+		this.t = t;
+		this.width = w;
+	}
+	
+	public function addKerning( prevChar : Int, offset : Int ) {
+		var k = new Kerning(prevChar, offset);
+		k.next = kerning;
+		kerning = k;
+	}
+	
+	public function getKerningOffset( prevChar : Int ) {
+		var k = kerning;
+		while( k != null ) {
+			if( k.prevChar == prevChar )
+				return k.offset;
+			k = k.next;
+		}
+		return 0;
+	}
+
+}
+
+class Font {
+	
+	public var name(default, null) : String;
+	public var size(default, null) : Int;
+	public var lineHeight(default, null) : Int;
+	public var tile(default,null) : h2d.Tile;
+	public var charset : hxd.Charset;
+	var glyphs : Map<Int,FontChar>;
+	var defaultChar : FontChar;
+	
+	function new(name,size) {
+		this.name = name;
+		this.size = size;
+		glyphs = new Map();
+		defaultChar = new FontChar(new Tile(null, 0, 0, 0, 0),0);
+		charset = hxd.Charset.getDefault();
+	}
+	
+	public inline function getChar( code : Int ) {
+		var c = glyphs.get(code);
+		if( c == null ) {
+			c = charset.resolveChar(code, glyphs);
+			if( c == null ) c = defaultChar;
+		}
+		return c;
+	}
+	
+	/**
+		This is meant to create smoother fonts by creating them with double size while still keeping the original glyph size.
+	**/
+	public function resizeTo( size : Int ) {
+		var ratio = size / this.size;
+		for( c in glyphs ) {
+			c.width = Std.int(c.width * ratio);
+			c.t.scaleToSize(Std.int(c.t.width * ratio), Std.int(c.t.height * ratio));
+		}
+		lineHeight = Std.int(lineHeight * ratio);
+		this.size = size;
+	}
+	
+	public function hasChar( code : Int ) {
+		return glyphs.get(code) != null;
+	}
+	
+	public function dispose() {
+		tile.dispose();
+	}
+	
+}

+ 302 - 0
h2d/Graphics.hx

@@ -0,0 +1,302 @@
+package h2d;
+import hxd.Math;
+
+private typedef GraphicsPoint = hxd.poly2tri.Point;
+
+private class LinePoint {
+	public var x : Float;
+	public var y : Float;
+	public var r : Float;
+	public var g : Float;
+	public var b : Float;
+	public var a : Float;
+	public function new(x, y, r, g, b, a) {
+		this.x = x;
+		this.y = y;
+		this.r = r;
+		this.g = g;
+		this.b = b;
+		this.a = a;
+	}
+}
+
+private class GraphicsContent extends h3d.prim.Primitive {
+
+	var tmp : hxd.FloatBuffer;
+	var index : hxd.IndexBuffer;
+	
+	var buffers : Array<{ buf : hxd.FloatBuffer, vbuf : h3d.impl.Buffer, idx : hxd.IndexBuffer, ibuf : h3d.impl.Indexes }>;
+	
+	public function new() {
+		buffers = [];
+	}
+
+	public inline function addIndex(i) {
+		index.push(i);
+	}
+
+	public inline function add( x : Float, y : Float, u : Float, v : Float, r : Float, g : Float, b : Float, a : Float ) {
+		tmp.push(x);
+		tmp.push(y);
+		tmp.push(u);
+		tmp.push(v);
+		tmp.push(r);
+		tmp.push(g);
+		tmp.push(b);
+		tmp.push(a);
+	}
+	
+	public function next() {
+		var nvect = tmp.length >> 3;
+		if( nvect < 1 << 15 )
+			return false;
+		buffers.push( { buf : tmp, idx : index, vbuf : null, ibuf : null } );
+		tmp = new hxd.FloatBuffer();
+		index = new hxd.IndexBuffer();
+		super.dispose();
+		return true;
+	}
+	
+	override function alloc( engine : h3d.Engine ) {
+		buffer = engine.mem.allocVector(tmp, 8, 0);
+		indexes = engine.mem.allocIndex(index);
+		for( b in buffers ) {
+			if( b.vbuf == null || b.vbuf.isDisposed() ) b.vbuf = engine.mem.allocVector(b.buf, 8, 0);
+			if( b.ibuf == null || b.ibuf.isDisposed() ) b.ibuf = engine.mem.allocIndex(b.idx);
+		}
+	}
+	
+	override function render( engine : h3d.Engine ) {
+		if( buffer == null || buffer.isDisposed() ) alloc(engine);
+		for( b in buffers )
+			engine.renderIndexed(b.vbuf, b.ibuf);
+		super.render(engine);
+	}
+	
+	override function dispose() {
+		for( b in buffers ) {
+			if( b.vbuf != null ) b.vbuf.dispose();
+			if( b.ibuf != null ) b.ibuf.dispose();
+			b.vbuf = null;
+			b.ibuf = null;
+		}
+		super.dispose();
+	}
+	
+	
+	public function reset() {
+		dispose();
+		tmp = new hxd.FloatBuffer();
+		index = new hxd.IndexBuffer();
+		buffers = [];
+	}
+	
+}
+
+class Graphics extends Drawable {
+
+	var content : GraphicsContent;
+	var pts : Array<GraphicsPoint>;
+	var linePts : Array<LinePoint>;
+	var pindex : Int;
+	var prev : Array<Array<GraphicsPoint>>;
+	var curR : Float;
+	var curG : Float;
+	var curB : Float;
+	var curA : Float;
+	var lineSize : Float;
+	var lineR : Float;
+	var lineG : Float;
+	var lineB : Float;
+	var lineA : Float;
+	var doFill : Bool;
+	
+	public var tile : h2d.Tile;
+	
+	public function new(?parent) {
+		super(parent);
+		content = new GraphicsContent();
+		shader.hasVertexColor = true;
+		tile = h2d.Tile.fromColor(0xFFFFFFFF);
+		clear();
+	}
+	
+	override function onDelete() {
+		super.onDelete();
+		clear();
+	}
+	
+	public function clear() {
+		content.reset();
+		pts = [];
+		prev = [];
+		linePts = [];
+		pindex = 0;
+		lineSize = 0;
+	}
+
+	function isConvex( points : Array<GraphicsPoint> ) {
+		for( i in 0...points.length ) {
+			var p1 = points[i];
+			var p2 = points[(i + 1) % points.length];
+			var p3 = points[(i + 2) % points.length];
+			if( (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) > 0 )
+				return false;
+		}
+		return true;
+	}
+	
+	function flushLine() {
+		if( linePts.length == 0 )
+			return;
+		var last = linePts.length - 1;
+		var prev = linePts[last];
+		var p = linePts[0];
+		var start = pindex;
+		for( i in 0...linePts.length ) {
+			var next = linePts[(i + 1) % linePts.length];
+			var nx1 = prev.y - p.y;
+			var ny1 = p.x - prev.x;
+			var ns1 = Math.invSqrt(nx1 * nx1 + ny1 * ny1);
+			var nx2 = p.y - next.y;
+			var ny2 = next.x - p.x;
+			var ns2 = Math.invSqrt(nx2 * nx2 + ny2 * ny2);
+			
+			var nx = (nx1 * ns1 + nx2 * ns2) * lineSize * 0.5;
+			var ny = (ny1 * ns1 + ny2 * ns2) * lineSize * 0.5;
+			
+			content.add(p.x + nx, p.y + ny, 0, 0, p.r, p.g, p.b, p.a);
+			content.add(p.x - nx, p.y - ny, 0, 0, p.r, p.g, p.b, p.a);
+			
+			var pnext = i == last ? start : pindex + 2;
+			
+			content.addIndex(pindex);
+			content.addIndex(pindex + 1);
+			content.addIndex(pnext);
+
+			content.addIndex(pindex + 1);
+			content.addIndex(pnext);
+			content.addIndex(pnext + 1);
+			
+			pindex += 2;
+			
+			prev = p;
+			p = next;
+		}
+		linePts = [];
+		if( content.next() )
+			pindex = 0;
+	}
+	
+	function flushFill() {
+		if( pts.length > 0 ) {
+			prev.push(pts);
+			pts = [];
+		}
+		if( prev.length == 0 )
+			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);
+			}
+		} 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);
+		}
+				
+		prev = [];
+		if( content.next() )
+			pindex = 0;
+	}
+	
+	function flush() {
+		flushFill();
+		flushLine();
+	}
+	
+	public function beginFill( color : Int = 0, alpha = 1.  ) {
+		flush();
+		setColor(color,alpha);
+		doFill = true;
+	}
+	
+	public function lineStyle( size : Float = 0, color = 0, alpha = 1. ) {
+		flush();
+		this.lineSize = size;
+		lineA = alpha;
+		lineR = ((color >> 16) & 0xFF) / 255.;
+		lineG = ((color >> 8) & 0xFF) / 255.;
+		lineB = (color & 0xFF) / 255.;
+	}
+	
+	public function endFill() {
+		flush();
+		doFill = false;
+	}
+	
+	public inline function setColor( color : Int, alpha : Float = 1. ) {
+		curA = alpha;
+		curR = ((color >> 16) & 0xFF) / 255.;
+		curG = ((color >> 8) & 0xFF) / 255.;
+		curB = (color & 0xFF) / 255.;
+	}
+	
+	public function drawRect( x : Float, y : Float, w : Float, h : Float ) {
+		addPoint(x, y);
+		addPoint(x + w, y);
+		addPoint(x + w, y + h);
+		addPoint(x, y + h);
+	}
+	
+	public function drawCircle( cx : Float, cy : Float, ray : Float, nsegments = 0 ) {
+		if( nsegments == 0 )
+			nsegments = Math.ceil(ray * 3.14 * 2 / 4);
+		if( nsegments < 3 ) nsegments = 3;
+		var angle = Math.PI * 2 / (nsegments + 1);
+		for( i in 0...nsegments ) {
+			var a = i * angle;
+			addPoint(cx + Math.cos(a) * ray, cy + Math.sin(a) * ray);
+		}
+	}
+	
+	public function addHole() {
+		if( pts.length > 0 ) {
+			prev.push(pts);
+			pts = [];
+		}
+	}
+	
+	public inline function addPoint( x : Float, y : Float ) {
+		addPointFull(x, y, curR, curG, curB, curA);
+	}
+
+	public function addPointFull( x : Float, y : Float, r : Float, g : Float, b : Float, a : Float, u : Float = 0., v : Float = 0. ) {
+		if( doFill ) {
+			var p = new GraphicsPoint(x, y);
+			p.id = pindex++;
+			pts.push(p);
+			content.add(x, y, u, v, r, g, b, a);
+		}
+		if( lineSize > 0 )
+			linePts.push(new LinePoint(x, y, lineR, lineG, lineB, lineA));
+	}
+	
+	override function draw(ctx:RenderContext) {
+		flush();
+		setupShader(ctx.engine, tile, 0);
+		content.render(ctx.engine);
+	}
+
+}

+ 98 - 0
h2d/HtmlText.hx

@@ -0,0 +1,98 @@
+package h2d;
+
+class HtmlText extends Drawable {
+
+	public var font(default, null) : Font;
+	public var htmlText(default, set) : String;
+	public var textColor(default, set) : Int;
+	
+	public var textWidth(get, null) : Int;
+	public var textHeight(get, null) : Int;
+	
+	public var letterSpacing : Int;
+	
+	var glyphs : TileColorGroup;
+	
+	public function new( font : Font, ?parent ) {
+		super(parent);
+		this.font = font;
+		glyphs = new TileColorGroup(font.tile, this);
+		htmlText = "";
+		shader = glyphs.shader;
+		textColor = 0xFFFFFF;
+	}
+	
+	override function onAlloc() {
+		super.onAlloc();
+		if( htmlText != null ) initGlyphs();
+	}
+	
+	function set_htmlText(t) {
+		this.htmlText = t == null ? "null" : t;
+		if( allocated ) initGlyphs();
+		return t;
+	}
+	
+	function initGlyphs( rebuild = true ) {
+		if( rebuild ) glyphs.reset();
+		glyphs.setDefaultColor(textColor);
+		var x = 0, y = 0, xMax = 0;
+		function loop( e : Xml ) {
+			if( e.nodeType == Xml.Element ) {
+				var colorChanged = false;
+				switch( e.nodeName.toLowerCase() ) {
+				case "font":
+					for( a in e.attributes() ) {
+						var v = e.get(a);
+						switch( a.toLowerCase() ) {
+						case "color":
+							colorChanged = true;
+							glyphs.setDefaultColor(Std.parseInt("0x" + v.substr(1)));
+						default:
+						}
+					}
+				case "br":
+					if( x > xMax ) xMax = x;
+					x = 0;
+					y += font.lineHeight;
+				default:
+				}
+				for( child in e )
+					loop(child);
+				if( colorChanged )
+					glyphs.setDefaultColor(textColor);
+			} else {
+				var t = e.nodeValue;
+				var prevChar = -1;
+				for( i in 0...t.length ) {
+					var cc = t.charCodeAt(i);
+					var e = font.getChar(cc);
+					x += e.getKerningOffset(prevChar);
+					if( rebuild ) glyphs.add(x, y, e.t);
+					x += e.width + letterSpacing;
+					prevChar = cc;
+				}
+			}
+		}
+		for( e in Xml.parse(htmlText) )
+			loop(e);
+		return { width : x > xMax ? x : xMax, height : x > 0 ? y + font.lineHeight : y };
+	}
+	
+	function get_textHeight() {
+		return initGlyphs(false).height;
+	}
+	
+	function get_textWidth() {
+		return initGlyphs(false).width;
+	}
+	
+	function set_textColor(c) {
+		if( textColor != c ) {
+			this.textColor = c;
+			if( allocated && htmlText != "" ) initGlyphs();
+		}
+		return c;
+	}
+
+}

+ 217 - 0
h2d/Interactive.hx

@@ -0,0 +1,217 @@
+package h2d;
+
+class Interactive extends Drawable {
+
+	public var width : Float;
+	public var height : Float;
+	public var cursor(default,set) : hxd.System.Cursor;
+	public var isEllipse : Bool;
+	public var blockEvents : Bool = true;
+	public var propagateEvents : Bool = false;
+	public var backgroundColor : Null<Int>;
+	public var enableRightButton : Bool;
+	var scene : Scene;
+	var isMouseDown : Int;
+	
+	public function new(width, height, ?parent) {
+		super(parent);
+		this.width = width;
+		this.height = height;
+		cursor = Button;
+	}
+
+	override function onAlloc() {
+		this.scene = getScene();
+		if( scene != null ) scene.addEventTarget(this);
+		super.onAlloc();
+	}
+	
+	override function draw( ctx : RenderContext ) {
+		if( backgroundColor != null ) drawTile(ctx.engine,h2d.Tile.fromColor(backgroundColor,Std.int(width),Std.int(height)));
+	}
+	
+	override function onParentChanged() {
+		if( scene != null ) {
+			scene.removeEventTarget(this);
+			scene.addEventTarget(this);
+		}
+	}
+	
+	override function onDelete() {
+		if( scene != null ) {
+			scene.removeEventTarget(this);
+			if( scene.currentOver == this ) {
+				scene.currentOver = null;
+				hxd.System.setCursor(Default);
+			}
+		}
+		super.onDelete();
+	}
+
+	function checkBounds( e : hxd.Event ) {
+		return switch( e.kind ) {
+		case EOut, ERelease, EFocus, EFocusLost: false;
+		default: true;
+		}
+	}
+	
+	@:allow(h2d.Scene)
+	function handleEvent( e : hxd.Event ) {
+		if( isEllipse && checkBounds(e) ) {
+			var cx = width * 0.5, cy = height * 0.5;
+			var dx = (e.relX - cx) / cx;
+			var dy = (e.relY - cy) / cy;
+			if( dx * dx + dy * dy > 1 ) {
+				e.cancel = true;
+				return;
+			}
+		}
+		if( propagateEvents ) e.propagate = true;
+		if( !blockEvents ) e.cancel = true;
+		switch( e.kind ) {
+		case EMove:
+			onMove(e);
+		case EPush:
+			if( enableRightButton || e.button == 0 ) {
+				isMouseDown = e.button;
+				onPush(e);
+			}
+		case ERelease:
+			if( enableRightButton || e.button == 0 ) {
+				onRelease(e);
+				if( isMouseDown == e.button )
+					onClick(e);
+			}
+			isMouseDown = -1;
+		case EOver:
+			hxd.System.setCursor(cursor);
+			onOver(e);
+		case EOut:
+			isMouseDown = -1;
+			hxd.System.setCursor(Default);
+			onOut(e);
+		case EWheel:
+			onWheel(e);
+		case EFocusLost:
+			onFocusLost(e);
+			if( !e.cancel && scene != null && scene.currentFocus == this ) scene.currentFocus = null;
+		case EFocus:
+			onFocus(e);
+			if( !e.cancel && scene != null ) scene.currentFocus = this;
+		case EKeyUp:
+			onKeyUp(e);
+		case EKeyDown:
+			onKeyDown(e);
+		}
+	}
+	
+	function set_cursor(c) {
+		this.cursor = c;
+		if( scene != null && scene.currentOver == this )
+			hxd.System.setCursor(cursor);
+		return c;
+	}
+	
+	function eventToLocal( e : hxd.Event ) {
+		// convert global event to our local space
+		var x = e.relX, y = e.relY;
+		var rx = x * scene.matA + y * scene.matB + scene.absX;
+		var ry = x * scene.matC + y * scene.matD + scene.absY;
+		var r = scene.height / scene.width;
+		
+		var i = this;
+		
+		var dx = rx - i.absX;
+		var dy = ry - i.absY;
+		
+		var w1 = i.width * i.matA * r;
+		var h1 = i.width * i.matC;
+		var ky = h1 * dx - w1 * dy;
+		
+		var w2 = i.height * i.matB * r;
+		var h2 = i.height * i.matD;
+		var kx = w2 * dy - h2 * dx;
+		
+		var max = h1 * w2 - w1 * h2;
+		
+		e.relX = (kx * r / max) * i.width;
+		e.relY = (ky / max) * i.height;
+	}
+	
+	public function startDrag(callb,?onCancel) {
+		scene.startDrag(function(event) {
+			var x = event.relX, y = event.relY;
+			eventToLocal(event);
+			callb(event);
+			event.relX = x;
+			event.relY = y;
+		},onCancel);
+	}
+	
+	public function stopDrag() {
+		scene.stopDrag();
+	}
+	
+	public function focus() {
+		if( scene == null )
+			return;
+		var ev = new hxd.Event(null);
+		if( scene.currentFocus != null ) {
+			if( scene.currentFocus == this )
+				return;
+			ev.kind = EFocusLost;
+			scene.currentFocus.handleEvent(ev);
+			if( ev.cancel ) return;
+		}
+		ev.kind = EFocus;
+		handleEvent(ev);
+	}
+	
+	public function blur() {
+		if( scene == null )
+			return;
+		if( scene.currentFocus == this ) {
+			var ev = new hxd.Event(null);
+			ev.kind = EFocusLost;
+			scene.currentFocus.handleEvent(ev);
+		}
+	}
+	
+	public function hasFocus() {
+		return scene != null && scene.currentFocus == this;
+	}
+	
+	public dynamic function onOver( e : hxd.Event ) {
+	}
+
+	public dynamic function onOut( e : hxd.Event ) {
+	}
+	
+	public dynamic function onPush( e : hxd.Event ) {
+	}
+
+	public dynamic function onRelease( e : hxd.Event ) {
+	}
+
+	public dynamic function onClick( e : hxd.Event ) {
+	}
+	
+	public dynamic function onMove( e : hxd.Event ) {
+	}
+
+	public dynamic function onWheel( e : hxd.Event ) {
+	}
+
+	public dynamic function onFocus( e : hxd.Event ) {
+	}
+	
+	public dynamic function onFocusLost( e : hxd.Event ) {
+	}
+
+	public dynamic function onKeyUp( e : hxd.Event ) {
+	}
+
+	public dynamic function onKeyDown( e : hxd.Event ) {
+	}
+	
+}

+ 88 - 0
h2d/Layers.hx

@@ -0,0 +1,88 @@
+package h2d;
+
+class Layers extends Sprite {
+	
+	// the per-layer insert position
+	var layers : Array<Int>;
+	var layerCount : Int;
+	
+	public function new(?parent) {
+		super(parent);
+		layers = [];
+		layerCount = 0;
+	}
+	
+	override function addChild(s) {
+		addChildAt(s, 0);
+	}
+	
+	public inline function add(s, layer) {
+		return addChildAt(s, layer);
+	}
+	
+	override function addChildAt( s : Sprite, layer : Int ) {
+		// new layer
+		while( layer >= layerCount )
+			layers[layerCount++] = childs.length;
+		super.addChildAt(s,layers[layer]);
+		for( i in layer...layerCount )
+			layers[i]++;
+	}
+	
+	override function removeChild( s : Sprite ) {
+		for( i in 0...childs.length ) {
+			if( childs[i] == s ) {
+				childs.splice(i, 1);
+				if( s.allocated ) s.onDelete();
+				s.parent = null;
+				var k = layerCount - 1;
+				while( k >= 0 && layers[k] > i ) {
+					layers[k]--;
+					k--;
+				}
+				break;
+			}
+		}
+	}
+	
+	public function over( s : Sprite ) {
+		for( i in 0...childs.length )
+			if( childs[i] == s ) {
+				for( l in layers )
+					if( l > i ) {
+						for( p in i...l-1 )
+							childs[p] = childs[p + 1];
+						childs[l - 1] = s;
+						break;
+					}
+				break;
+			}
+	}
+	
+	public function ysort( layer : Int ) {
+		if( layer >= layerCount ) return;
+		var start = layer == 0 ? 0 : layers[layer - 1];
+		var max = layers[layer];
+		if( start == max )
+			return;
+		var pos = start;
+		var ymax = childs[pos++].y;
+		while( pos < max ) {
+			var c = childs[pos];
+			if( c.y < ymax ) {
+				var p = pos - 1;
+				while( p >= start ) {
+					var c2 = childs[p];
+					if( c.y >= c2.y ) break;
+					childs[p + 1] = c2;
+					p--;
+				}
+				childs[p + 1] = c;
+			} else
+				ymax = c.y;
+			pos++;
+		}
+	}
+
+	
+}

+ 27 - 0
h2d/Mask.hx

@@ -0,0 +1,27 @@
+package h2d;
+
+class Mask extends Sprite {
+
+	public var width : Int;
+	public var height : Int;
+	
+	public function new(width, height, ?parent) {
+		super(parent);
+		this.width = width;
+		this.height = height;
+	}
+
+	override function drawRec( ctx : h2d.RenderContext ) {
+				
+		var x1 = (absX + 1) * 0.5 * ctx.engine.width;
+		var y1 = (1 - absY) * 0.5 * ctx.engine.height;
+		
+		var x2 = ((width * matA + height * matC + absX) + 1) * 0.5 * ctx.engine.width;
+		var y2 = (1 - (width * matB + height * matD + absY)) * 0.5 * ctx.engine.height;
+		
+		ctx.engine.setRenderZone(Std.int(x1+1e-10), Std.int(y1+1e-10), Std.int(x2-x1+1e-10), Std.int(y2-y1+1e-10));
+		super.drawRec(ctx);
+		ctx.engine.setRenderZone();
+	}
+	
+}

+ 3 - 0
h2d/RenderContext.hx

@@ -0,0 +1,3 @@
+package h2d;
+
+typedef RenderContext = h3d.scene.RenderContext;

+ 106 - 0
h2d/ScaleGrid.hx

@@ -0,0 +1,106 @@
+package h2d;
+
+class ScaleGrid extends h2d.TileGroup {
+	
+
+	public var borderWidth : Int;
+	public var borderHeight : Int;
+	
+	public var width(default,set) : Int;
+	public var height(default,set) : Int;
+	
+	public var tileBorders(default,set) : Bool;
+	
+	public function new( tile, borderW, borderH, ?parent ) {
+		super(tile,parent);
+		borderWidth = borderW;
+		borderHeight = borderH;
+		width = tile.width;
+		height = tile.height;
+	}
+
+	function set_tileBorders(b) {
+		this.tileBorders = b;
+		reset();
+		return b;
+	}
+	
+	function set_width(w) {
+		this.width = w;
+		reset();
+		return w;
+	}
+
+	function set_height(h) {
+		this.height = h;
+		reset();
+		return h;
+	}
+	
+	override function draw( ctx : RenderContext ) {
+		if( content.isEmpty() ) {
+			var bw = borderWidth, bh = borderHeight;
+			
+			// 4 corners
+			content.add(0, 0, tile.sub(0, 0, bw, bh));
+			content.add(width - bw, 0, tile.sub(tile.width - bw, 0, bw, bh));
+			content.add(0, height-bh, tile.sub(0, tile.height - bh, bw, bh));
+			content.add(width - bw, height - bh, tile.sub(tile.width - bw, tile.height - bh, bw, bh));
+
+			var sizeX = tile.width - bw * 2;
+			var sizeY = tile.height - bh * 2;
+			
+			if( !tileBorders ) {
+				
+				var w = width - bw * 2;
+				var h = height - bh * 2;
+				
+				var t = tile.sub(bw, 0, sizeX, bh);
+				t.scaleToSize(w, bh);
+				content.add(bw, 0, t);
+
+				var t = tile.sub(bw, tile.height - bh, sizeX, bh);
+				t.scaleToSize(w, bh);
+				content.add(bw, h + bh, t);
+
+				var t = tile.sub(0, bh, bw, sizeY);
+				t.scaleToSize(bw, h);
+				content.add(0, bh, t);
+
+				var t = tile.sub(tile.width - bw, bh, bw, sizeY);
+				t.scaleToSize(bw, h);
+				content.add(w + bw, bh, t);
+				
+			} else {
+				
+				var rw = Std.int((width - bw * 2) / sizeX);
+				for( x in 0...rw ) {
+					content.add(bw + x * sizeX, 0, tile.sub(bw, 0, sizeX, bh));
+					content.add(bw + x * sizeX, height - bh, tile.sub(bw, tile.height - bh, sizeX, bh));
+				}
+				var dx = width - bw * 2 - rw * sizeX;
+				if( dx > 0 ) {
+					content.add(bw + rw * sizeX, 0, tile.sub(bw, 0, dx, bh));
+					content.add(bw + rw * sizeX, height - bh, tile.sub(bw, tile.height - bh, dx, bh));
+				}
+
+				var rh = Std.int((height - bh * 2) / sizeY);
+				for( y in 0...rh ) {
+					content.add(0, bh + y * sizeY, tile.sub(0, bh, bw, sizeY));
+					content.add(width - bw, bh + y * sizeY, tile.sub(tile.width - bw, bh, bw, sizeY));
+				}
+				var dy = height - bh * 2 - rh * sizeY;
+				if( dy > 0 ) {
+					content.add(0, bh + rh * sizeY, tile.sub(0, bh, bw, dy));
+					content.add(width - bw, bh + rh * sizeY, tile.sub(tile.width - bw, bh, bw, dy));
+				}
+			}
+			
+			var t = tile.sub(bw, bh, sizeX, sizeY);
+			t.scaleToSize(width - bw * 2,height - bh * 2);
+			content.add(bw, bh, t);
+		}
+		super.draw(ctx);
+	}
+	
+}

+ 442 - 0
h2d/Scene.hx

@@ -0,0 +1,442 @@
+package h2d;
+import hxd.Math;
+
+class Scene extends Layers implements h3d.IDrawable {
+
+	public var width(default,null) : Int;
+	public var height(default, null) : Int;
+	
+	public var mouseX(get, null) : Float;
+	public var mouseY(get, null) : Float;
+	
+	var fixedSize : Bool;
+	var interactive : Array<Interactive>;
+	var pendingEvents : Array<hxd.Event>;
+	var ctx : RenderContext;
+	var stage : hxd.Stage;
+	
+	@:allow(h2d.Interactive)
+	var currentOver : Interactive;
+	@:allow(h2d.Interactive)
+	var currentFocus : Interactive;
+		
+	var pushList : Array<Interactive>;
+	var currentDrag : { f : hxd.Event -> Void, onCancel : Void -> Void, ref : Null<Int> };
+	var eventListeners : Array< hxd.Event -> Void >;
+	
+	public function new() {
+		super(null);
+		var e = h3d.Engine.getCurrent();
+		ctx = new RenderContext();
+		width = e.width;
+		height = e.height;
+		interactive = new Array();
+		pushList = new Array();
+		eventListeners = new Array();
+		stage = hxd.Stage.getInstance();
+		posChanged = true;
+	}
+	
+	public function setFixedSize( w, h ) {
+		width = w;
+		height = h;
+		fixedSize = true;
+		posChanged = true;
+	}
+
+	override function onAlloc() {
+		stage.addEventTarget(onEvent);
+		super.onAlloc();
+	}
+	
+	override function onDelete() {
+		stage.removeEventTarget(onEvent);
+		super.onDelete();
+	}
+	
+	function onEvent( e : hxd.Event ) {
+		if( pendingEvents != null ) {
+			e.relX = screenXToLocal(e.relX);
+			e.relY = screenYToLocal(e.relY);
+			pendingEvents.push(e);
+		}
+	}
+	
+	function screenXToLocal(mx:Float) {
+		return (mx - x) * width / (stage.width * scaleX);
+	}
+
+	function screenYToLocal(my:Float) {
+		return (my - y) * height / (stage.height * scaleY);
+	}
+	
+	function get_mouseX() {
+		return screenXToLocal(stage.mouseX);
+	}
+
+	function get_mouseY() {
+		return screenYToLocal(stage.mouseY);
+	}
+	
+	function dispatchListeners( event : hxd.Event ) {
+		event.propagate = true;
+		event.cancel = false;
+		for( l in eventListeners ) {
+			l(event);
+			if( !event.propagate ) break;
+		}
+	}
+
+	function emitEvent( event : hxd.Event ) {
+		var x = event.relX, y = event.relY;
+		var rx = x * matA + y * matB + absX;
+		var ry = x * matC + y * matD + absY;
+		var r = height / width;
+		var handled = false;
+		var checkOver = false, checkPush = false, cancelFocus = false;
+		switch( event.kind ) {
+		case EMove: checkOver = true;
+		case EPush: cancelFocus = true; checkPush = true;
+		case ERelease: checkPush = true;
+		case EKeyUp, EKeyDown, EWheel:
+			if( currentFocus != null )
+				currentFocus.handleEvent(event);
+			else {
+				if( currentOver != null ) {
+					event.propagate = true;
+					currentOver.handleEvent(event);
+					if( !event.propagate ) return;
+				}
+				dispatchListeners(event);
+			}
+			return;
+		default:
+		}
+		for( i in interactive ) {
+			
+
+			// TODO : we are not sure that the positions are correctly updated !
+			
+			// this is a bit tricky since we are not in the not-euclide viewport space
+			// (r = ratio correction)
+			var dx = rx - i.absX;
+			var dy = ry - i.absY;
+			
+			var w1 = i.width * i.matA * r;
+			var h1 = i.width * i.matC;
+			var ky = h1 * dx - w1 * dy;
+			// up line
+			if( ky < 0 )
+				continue;
+				
+			var w2 = i.height * i.matB * r;
+			var h2 = i.height * i.matD;
+			var kx = w2 * dy - h2 * dx;
+				
+			// left line
+			if( kx < 0 )
+				continue;
+			
+			var max = h1 * w2 - w1 * h2;
+			// bottom/right
+			if( ky >= max || kx * r >= max )
+				continue;
+
+			// check visibility
+			var visible = true;
+			var p : Sprite = i;
+			while( p != null ) {
+				if( !p.visible ) {
+					visible = false;
+					break;
+				}
+				p = p.parent;
+			}
+			if( !visible ) continue;
+			
+			event.relX = (kx * r / max) * i.width;
+			event.relY = (ky / max) * i.height;
+			
+			i.handleEvent(event);
+			
+			if( event.cancel )
+				event.cancel = false;
+			else if( checkOver ) {
+				if( currentOver != i ) {
+					var old = event.propagate;
+					if( currentOver != null ) {
+						event.kind = EOut;
+						// relX/relY is not correct here
+						currentOver.handleEvent(event);
+					}
+					event.kind = EOver;
+					event.cancel = false;
+					i.handleEvent(event);
+					if( event.cancel )
+						currentOver = null;
+					else {
+						currentOver = i;
+						checkOver = false;
+					}
+					event.kind = EMove;
+					event.cancel = false;
+					event.propagate = old;
+				} else
+					checkOver = false;
+			} else {
+				if( checkPush ) {
+					if( event.kind == EPush )
+						pushList.push(i);
+					else
+						pushList.remove(i);
+				}
+				if( cancelFocus && i == currentFocus )
+					cancelFocus = false;
+			}
+				
+			if( event.propagate ) {
+				event.propagate = false;
+				continue;
+			}
+			
+			handled = true;
+			break;
+		}
+		if( cancelFocus && currentFocus != null ) {
+			event.kind = EFocusLost;
+			currentFocus.handleEvent(event);
+			event.kind = EPush;
+		}
+		if( checkOver && currentOver != null ) {
+			event.kind = EOut;
+			currentOver.handleEvent(event);
+			event.kind = EMove;
+			currentOver = null;
+		}
+		if( !handled )
+			dispatchListeners(event);
+	}
+	
+	function hasEvents() {
+		return interactive.length > 0 || eventListeners.length > 0;
+	}
+	
+	public function checkEvents() {
+		if( pendingEvents == null ) {
+			if( !hasEvents() )
+				return;
+			pendingEvents = new Array();
+		}
+		var old = pendingEvents;
+		if( old.length == 0 )
+			return;
+		pendingEvents = null;
+		var ox = 0., oy = 0.;
+		for( e in old ) {
+			var hasPos = switch( e.kind ) {
+			case EKeyUp, EKeyDown: false;
+			default: true;
+			}
+			
+			if( hasPos ) {
+				ox = e.relX;
+				oy = e.relY;
+			}
+			
+			if( currentDrag != null && (currentDrag.ref == null || currentDrag.ref == e.touchId) ) {
+				currentDrag.f(e);
+				if( e.cancel )
+					continue;
+			}
+			emitEvent(e);
+			if( e.kind == ERelease && pushList.length > 0 ) {
+				for( i in pushList ) {
+					// relX/relY is not correct here
+					i.handleEvent(e);
+				}
+				pushList = new Array();
+			}
+		}
+		if( hasEvents() )
+			pendingEvents = new Array();
+	}
+	
+	public function addEventListener( f : hxd.Event -> Void ) {
+		eventListeners.push(f);
+	}
+
+	public function removeEventListener( f : hxd.Event -> Void ) {
+		return eventListeners.remove(f);
+	}
+	
+	public function startDrag( f : hxd.Event -> Void, ?onCancel : Void -> Void, ?refEvent : hxd.Event ) {
+		if( currentDrag != null && currentDrag.onCancel != null )
+			currentDrag.onCancel();
+		currentDrag = { f : f, ref : refEvent == null ? null : refEvent.touchId, onCancel : onCancel };
+	}
+	
+	public function stopDrag() {
+		currentDrag = null;
+	}
+	
+	public function getFocus() {
+		return currentFocus;
+	}
+	
+	@:allow(h2d)
+	function addEventTarget(i:Interactive) {
+		// sort by which is over the other in the scene hierarchy
+		inline function getLevel(i:Sprite) {
+			var lv = 0;
+			while( i != null ) {
+				i = i.parent;
+				lv++;
+			}
+			return lv;
+		}
+		inline function indexOf(p:Sprite, i:Sprite) {
+			var id = -1;
+			for( k in 0...p.childs.length )
+				if( p.childs[k] == i ) {
+					id = k;
+					break;
+				}
+			return id;
+		}
+		var level = getLevel(i);
+		for( index in 0...interactive.length ) {
+			var i1 : Sprite = i;
+			var i2 : Sprite = interactive[index];
+			var lv1 = level;
+			var lv2 = getLevel(i2);
+			var p1 : Sprite = i1;
+			var p2 : Sprite = i2;
+			while( lv1 > lv2 ) {
+				i1 = p1;
+				p1 = p1.parent;
+				lv1--;
+			}
+			while( lv2 > lv1 ) {
+				i2 = p2;
+				p2 = p2.parent;
+				lv2--;
+			}
+			while( p1 != p2 ) {
+				i1 = p1;
+				p1 = p1.parent;
+				i2 = p2;
+				p2 = p2.parent;
+			}
+			if( indexOf(p1,i1) > indexOf(p2,i2) ) {
+				interactive.insert(index, i);
+				return;
+			}
+		}
+		interactive.push(i);
+	}
+	
+	@:allow(h2d)
+	function removeEventTarget(i) {
+		for( k in 0...interactive.length )
+			if( interactive[k] == i ) {
+				interactive.splice(k, 1);
+				break;
+			}
+	}
+
+	override function calcAbsPos() {
+		// init matrix without rotation
+		matA = scaleX;
+		matB = 0;
+		matC = 0;
+		matD = scaleY;
+		absX = x;
+		absY = y;
+		
+		// adds a pixels-to-viewport transform
+		var w = 2 / width;
+		var h = -2 / height;
+		absX = absX * w - 1;
+		absY = absY * h + 1;
+		matA *= w;
+		matB *= h;
+		matC *= w;
+		matD *= h;
+		
+		// perform final rotation around center
+		if( rotation != 0 ) {
+			var cr = Math.cos(rotation);
+			var sr = Math.sin(rotation);
+			var tmpA = matA * cr + matB * sr;
+			var tmpB = matA * -sr + matB * cr;
+			var tmpC = matC * cr + matD * sr;
+			var tmpD = matC * -sr + matD * cr;
+			var tmpX = absX * cr + absY * sr;
+			var tmpY = absX * -sr + absY * cr;
+			matA = tmpA;
+			matB = tmpB;
+			matC = tmpC;
+			matD = tmpD;
+			absX = tmpX;
+			absY = tmpY;
+		}
+	}
+	
+	public function dispose() {
+		if( allocated )
+			onDelete();
+	}
+	
+	public function setElapsedTime( v : Float ) {
+		ctx.elapsedTime = v;
+	}
+	
+	public function render( engine : h3d.Engine ) {
+		ctx.engine = engine;
+		ctx.frame++;
+		ctx.time += ctx.elapsedTime;
+		ctx.currentPass = 0;
+		sync(ctx);
+		drawRec(ctx);
+	}
+	
+	override function sync( ctx : RenderContext ) {
+		if( !allocated )
+			onAlloc();
+		if( !fixedSize && (width != ctx.engine.width || height != ctx.engine.height) ) {
+			width = ctx.engine.width;
+			height = ctx.engine.height;
+			posChanged = true;
+		}
+		Tools.checkCoreObjects();
+		super.sync(ctx);
+	}
+	
+	public function captureBitmap( ?target : Tile ) {
+		var engine = h3d.Engine.getCurrent();
+		if( target == null ) {
+			var tw = 1, th = 1;
+			while( tw < width ) tw <<= 1;
+			while( th < height ) th <<= 1;
+			var tex = engine.mem.allocTargetTexture(tw, th);
+			target = new Tile(tex,0, 0, width, height);
+		}
+		engine.begin();
+		engine.setRenderZone(target.x, target.y, target.width, target.height);
+		var tex = target.getTexture();
+		engine.setTarget(tex);
+		var ow = width, oh = height, of = fixedSize;
+		setFixedSize(tex.width, tex.height);
+		render(engine);
+		width = ow;
+		height = oh;
+		fixedSize = of;
+		posChanged = true;
+		engine.setTarget(null);
+		engine.setRenderZone();
+		engine.end();
+		return new Bitmap(target);
+	}
+	
+	
+}

+ 16 - 0
h2d/Scene3D.hx

@@ -0,0 +1,16 @@
+package h2d;
+
+class Scene3D extends Sprite {
+	
+	public var scene : h3d.scene.Scene;
+	
+	public function new( scene, ?parent ) {
+		super(parent);
+		this.scene = scene;
+	}
+	
+	override function draw( ctx : RenderContext ) {
+		scene.render(ctx.engine);
+	}
+	
+}

+ 339 - 0
h2d/Sprite.hx

@@ -0,0 +1,339 @@
+package h2d;
+import hxd.Math;
+
+@:allow(h2d.Tools)
+class Sprite {
+
+	var childs : Array<Sprite>;
+	public var parent(default, null) : Sprite;
+	public var numChildren(get, never) : Int;
+	
+	public var x(default,set) : Float;
+	public var y(default, set) : Float;
+	public var scaleX(default,set) : Float;
+	public var scaleY(default,set) : Float;
+	public var rotation(default, set) : Float;
+	public var visible : Bool;
+
+	var matA : Float;
+	var matB : Float;
+	var matC : Float;
+	var matD : Float;
+	var absX : Float;
+	var absY : Float;
+	
+	var posChanged : Bool;
+	var allocated : Bool;
+	var lastFrame : Int;
+	
+	public function new( ?parent : Sprite ) {
+		matA = 1; matB = 0; matC = 0; matD = 1; absX = 0; absY = 0;
+		x = 0; y = 0; scaleX = 1; scaleY = 1; rotation = 0;
+		posChanged = false;
+		visible = true;
+		childs = [];
+		if( parent != null )
+			parent.addChild(this);
+	}
+	
+	public function getSpritesCount() {
+		var k = 0;
+		for( c in childs )
+			k += c.getSpritesCount() + 1;
+		return k;
+	}
+	
+	public function localToGlobal( ?pt : h2d.col.Point ) {
+		syncPos();
+		if( pt == null ) pt = new h2d.col.Point();
+		var px = pt.x * matA + pt.y * matC + absX;
+		var py = pt.x * matB + pt.y * matD + absY;
+		pt.x = (px + 1) * 0.5;
+		pt.y = (1 - py) * 0.5;
+		var scene = getScene();
+		if( scene != null ) {
+			pt.x *= scene.width;
+			pt.y *= scene.height;
+		} else {
+			pt.x *= hxd.System.width;
+			pt.y *= hxd.System.height;
+		}
+		return pt;
+	}
+
+	public function globalToLocal( pt : h2d.col.Point ) {
+		syncPos();
+		var scene = getScene();
+		if( scene != null ) {
+			pt.x /= scene.width;
+			pt.y /= scene.height;
+		} else {
+			pt.x /= hxd.System.width;
+			pt.y /= hxd.System.height;
+		}
+		pt.x = pt.x * 2 - 1;
+		pt.y = 1 - pt.y * 2;
+		pt.x -= absX;
+		pt.y -= absY;
+		var invDet = 1 / (matA * matD - matB * matC);
+		var px = (pt.x * matD - pt.y * matC) * invDet;
+		var py = (-pt.x * matB + pt.y * matA) * invDet;
+		pt.x = px;
+		pt.y = py;
+		return pt;
+	}
+	
+	function getScene() {
+		var p = this;
+		while( p.parent != null ) p = p.parent;
+		return Std.instance(p, Scene);
+	}
+	
+	public function addChild( s : Sprite ) {
+		addChildAt(s, childs.length);
+	}
+	
+	public function addChildAt( s : Sprite, pos : Int ) {
+		if( pos < 0 ) pos = 0;
+		if( pos > childs.length ) pos = childs.length;
+		var p = this;
+		while( p != null ) {
+			if( p == s ) throw "Recursive addChild";
+			p = p.parent;
+		}
+		if( s.parent != null ) {
+			// prevent calling onDelete
+			var old = s.allocated;
+			s.allocated = false;
+			s.parent.removeChild(s);
+			s.allocated = old;
+		}
+		childs.insert(pos, s);
+		if( !allocated && s.allocated )
+			s.onDelete();
+		s.parent = this;
+		s.posChanged = true;
+		// ensure that proper alloc/delete is done if we change parent
+		if( allocated ) {
+			if( !s.allocated )
+				s.onAlloc();
+			else
+				s.onParentChanged();
+		}
+	}
+	
+	// called when we're allocated already but moved in hierarchy
+	function onParentChanged() {
+	}
+	
+	// kept for internal init
+	function onAlloc() {
+		allocated = true;
+		for( c in childs )
+			c.onAlloc();
+	}
+		
+	// kept for internal cleanup
+	function onDelete() {
+		allocated = false;
+		for( c in childs )
+			c.onDelete();
+	}
+	
+	public function removeChild( s : Sprite ) {
+		if( childs.remove(s) ) {
+			if( s.allocated ) s.onDelete();
+			s.parent = null;
+		}
+	}
+	
+	// shortcut for parent.removeChild
+	public inline function remove() {
+		if( this != null && parent != null ) parent.removeChild(this);
+	}
+	
+	function draw( ctx : RenderContext ) {
+	}
+	
+	function sync( ctx : RenderContext ) {
+		/*
+		if( currentAnimation != null ) {
+			var old = parent;
+			var dt = ctx.elapsedTime;
+			while( dt > 0 && currentAnimation != null )
+				dt = currentAnimation.update(dt);
+			if( currentAnimation != null )
+				currentAnimation.sync();
+			if( parent == null && old != null ) return; // if we were removed by an animation event
+		}
+		*/
+		var changed = posChanged;
+		if( changed ) {
+			calcAbsPos();
+			posChanged = false;
+		}
+		
+		lastFrame = ctx.frame;
+		var p = 0, len = childs.length;
+		while( p < len ) {
+			var c = childs[p];
+			if( c == null )
+				break;
+			if( c.lastFrame != ctx.frame ) {
+				if( changed ) c.posChanged = true;
+				c.sync(ctx);
+			}
+			// if the object was removed, let's restart again.
+			// our lastFrame ensure that no object will get synched twice
+			if( childs[p] != c ) {
+				p = 0;
+				len = childs.length;
+			} else
+				p++;
+		}
+	}
+	
+	function syncPos() {
+		if( parent != null ) parent.syncPos();
+		if( posChanged ) {
+			calcAbsPos();
+			for( c in childs )
+				c.posChanged = true;
+			posChanged = false;
+		}
+	}
+	
+	function calcAbsPos() {
+		if( parent == null ) {
+			var cr, sr;
+			if( rotation == 0 ) {
+				cr = 1.; sr = 0.;
+				matA = scaleX;
+				matB = 0;
+				matC = 0;
+				matD = scaleY;
+			} else {
+				cr = Math.cos(rotation);
+				sr = Math.sin(rotation);
+				matA = scaleX * cr;
+				matB = scaleX * -sr;
+				matC = scaleY * sr;
+				matD = scaleY * cr;
+			}
+			absX = x;
+			absY = y;
+		} else {
+			// M(rel) = S . R . T
+			// M(abs) = M(rel) . P(abs)
+			if( rotation == 0 ) {
+				matA = scaleX * parent.matA;
+				matB = scaleX * parent.matB;
+				matC = scaleY * parent.matC;
+				matD = scaleY * parent.matD;
+			} else {
+				var cr = Math.cos(rotation);
+				var sr = Math.sin(rotation);
+				var tmpA = scaleX * cr;
+				var tmpB = scaleX * -sr;
+				var tmpC = scaleY * sr;
+				var tmpD = scaleY * cr;
+				matA = tmpA * parent.matA + tmpB * parent.matC;
+				matB = tmpA * parent.matB + tmpB * parent.matD;
+				matC = tmpC * parent.matA + tmpD * parent.matC;
+				matD = tmpC * parent.matB + tmpD * parent.matD;
+			}
+			absX = x * parent.matA + y * parent.matC + parent.absX;
+			absY = x * parent.matB + y * parent.matD + parent.absY;
+		}
+	}
+
+	function drawRec( ctx : RenderContext ) {
+		if( !visible ) return;
+		// fallback in case the object was added during a sync() event and we somehow didn't update it
+		if( posChanged ) {
+			// only sync anim, don't update() (prevent any event from occuring during draw())
+			// if( currentAnimation != null ) currentAnimation.sync();
+			calcAbsPos();
+			for( c in childs )
+				c.posChanged = true;
+			posChanged = false;
+		}
+		draw(ctx);
+		for( c in childs )
+			c.drawRec(ctx);
+	}
+
+	inline function set_x(v) {
+		x = v;
+		posChanged = true;
+		return v;
+	}
+
+	inline function set_y(v) {
+		y = v;
+		posChanged = true;
+		return v;
+	}
+	
+	inline function set_scaleX(v) {
+		scaleX = v;
+		posChanged = true;
+		return v;
+	}
+	
+	inline function set_scaleY(v) {
+		scaleY = v;
+		posChanged = true;
+		return v;
+	}
+	
+	inline function set_rotation(v) {
+		rotation = v;
+		posChanged = true;
+		return v;
+	}
+	
+	public function move( dx : Float, dy : Float ) {
+		x += dx * Math.cos(rotation);
+		y += dy * Math.sin(rotation);
+	}
+
+	public inline function setPos( x : Float, y : Float ) {
+		this.x = x;
+		this.y = y;
+	}
+	
+	public inline function rotate( v : Float ) {
+		rotation += v;
+	}
+	
+	public inline function scale( v : Float ) {
+		scaleX *= v;
+		scaleY *= v;
+	}
+	
+	public inline function setScale( v : Float ) {
+		scaleX = v;
+		scaleY = v;
+	}
+
+	public inline function getChildAt( n ) {
+		return childs[n];
+	}
+
+	public function getChildIndex( s ) {
+		for( i in 0...childs.length )
+			if( childs[i] == s )
+				return i;
+		return -1;
+	}
+	
+	inline function get_numChildren() {
+		return childs.length;
+	}
+
+	public inline function iterator() {
+		return new hxd.impl.ArrayIterator(childs);
+	}
+
+}

+ 165 - 0
h2d/SpriteBatch.hx

@@ -0,0 +1,165 @@
+package h2d;
+
+@:allow(h2d.SpriteBatch)
+class BatchElement {
+	public var x : Float;
+	public var y : Float;
+	public var scale : Float;
+	public var rotation : Float;
+	public var alpha : Float;
+	public var t : Tile;
+	public var batch(default, null) : SpriteBatch;
+	
+	var prev : BatchElement;
+	var next : BatchElement;
+	
+	function new(t) {
+		x = 0; y = 0; alpha = 1;
+		rotation = 0; scale = 1;
+		this.t = t;
+	}
+	
+	function update(et:Float) {
+		return true;
+	}
+	
+	public inline function remove() {
+		batch.delete(this);
+	}
+	
+}
+
+class SpriteBatch extends Drawable {
+
+	public var tile : Tile;
+	public var hasRotationScale : Bool;
+	public var hasUpdate : Bool;
+	var first : BatchElement;
+	var last : BatchElement;
+	var tmpBuf : hxd.FloatBuffer;
+		
+	public function new(t,?parent) {
+		super(parent);
+		tile = t;
+		shader.hasVertexAlpha = true;
+	}
+	
+	public function add(e:BatchElement) {
+		e.batch = this;
+		if( first == null )
+			first = last = e;
+		else {
+			last.next = e;
+			e.prev = last;
+			last = e;
+		}
+		return e;
+	}
+	
+	public function alloc(t) {
+		return add(new BatchElement(t));
+	}
+	
+	@:allow(h2d.BatchElement)
+	function delete(e : BatchElement) {
+		if( e.prev == null ) {
+			if( first == e )
+				first = e.next;
+		} else
+			e.prev.next = e.next;
+		if( e.next == null ) {
+			if( last == e )
+				last = e.prev;
+		} else
+			e.next.prev = e.prev;
+	}
+	
+	override function sync(ctx) {
+		super.sync(ctx);
+		if( hasUpdate ) {
+			var e = first;
+			while( e != null ) {
+				if( !e.update(ctx.elapsedTime) )
+					e.remove();
+				e = e.next;
+			}
+		}
+	}
+	
+	
+	override function draw( ctx : RenderContext ) {
+		if( first == null )
+			return;
+		if( tmpBuf == null ) tmpBuf = new hxd.FloatBuffer();
+		var pos = 0;
+		var e = first;
+		var tmp = tmpBuf;
+		while( e != null ) {
+			var t = e.t;
+			if( hasRotationScale ) {
+				var ca = Math.cos(e.rotation), sa = Math.sin(e.rotation);
+				var hx = t.width, hy = t.height;
+				var px = t.dx, py = t.dy;
+				tmp[pos++] = (px * ca + py * sa) * e.scale + e.x;
+				tmp[pos++] = (py * ca - px * sa) * e.scale + e.y;
+				tmp[pos++] = t.u;
+				tmp[pos++] = t.v;
+				tmp[pos++] = e.alpha;
+				var px = t.dx + hx, py = t.dy;
+				tmp[pos++] = (px * ca + py * sa) * e.scale + e.x;
+				tmp[pos++] = (py * ca - px * sa) * e.scale + e.y;
+				tmp[pos++] = t.u2;
+				tmp[pos++] = t.v;
+				tmp[pos++] = e.alpha;
+				var px = t.dx, py = t.dy + hy;
+				tmp[pos++] = (px * ca + py * sa) * e.scale + e.x;
+				tmp[pos++] = (py * ca - px * sa) * e.scale + e.y;
+				tmp[pos++] = t.u;
+				tmp[pos++] = t.v2;
+				tmp[pos++] = e.alpha;
+				var px = t.dx + hx, py = t.dy + hy;
+				tmp[pos++] = (px * ca + py * sa) * e.scale + e.x;
+				tmp[pos++] = (py * ca - px * sa) * e.scale + e.y;
+				tmp[pos++] = t.u2;
+				tmp[pos++] = t.v2;
+				tmp[pos++] = e.alpha;
+			} else {
+				var sx = e.x + t.dx;
+				var sy = e.y + t.dy;
+				tmp[pos++] = sx;
+				tmp[pos++] = sy;
+				tmp[pos++] = t.u;
+				tmp[pos++] = t.v;
+				tmp[pos++] = e.alpha;
+				tmp[pos++] = sx + t.width + 0.1;
+				tmp[pos++] = sy;
+				tmp[pos++] = t.u2;
+				tmp[pos++] = t.v;
+				tmp[pos++] = e.alpha;
+				tmp[pos++] = sx;
+				tmp[pos++] = sy + t.height + 0.1;
+				tmp[pos++] = t.u;
+				tmp[pos++] = t.v2;
+				tmp[pos++] = e.alpha;
+				tmp[pos++] = sx + t.width + 0.1;
+				tmp[pos++] = sy + t.height + 0.1;
+				tmp[pos++] = t.u2;
+				tmp[pos++] = t.v2;
+				tmp[pos++] = e.alpha;
+			}
+			e = e.next;
+		}
+		var stride = 5;
+		var nverts = Std.int(pos / stride);
+		var buffer = ctx.engine.mem.alloc(nverts, stride, 4);
+		buffer.uploadVector(tmpBuf, 0, nverts);
+		setupShader(ctx.engine, tile, 0);
+		ctx.engine.renderQuadBuffer(buffer);
+		buffer.dispose();
+	}
+	
+	public inline function isEmpty() {
+		return first == null;
+	}
+	
+}

+ 127 - 0
h2d/Text.hx

@@ -0,0 +1,127 @@
+package h2d;
+
+class Text extends Drawable {
+
+	public var font(default, set) : Font;
+	public var text(default, set) : String;
+	public var textColor(default, set) : Int;
+	public var maxWidth(default, set) : Null<Float>;
+	public var dropShadow : { dx : Float, dy : Float, color : Int, alpha : Float };
+	
+	public var textWidth(get, null) : Int;
+	public var textHeight(get, null) : Int;
+	public var letterSpacing : Int;
+	
+	var glyphs : TileGroup;
+	
+	public function new( font : Font, ?parent ) {
+		super(parent);
+		this.font = font;
+		text = "";
+		textColor = 0xFFFFFF;
+		letterSpacing = 1;
+	}
+	
+	function set_font(font) {
+		this.font = font;
+		if( glyphs != null ) glyphs.remove();
+		glyphs = new TileGroup(font == null ? null : font.tile, this);
+		shader = glyphs.shader;
+		this.text = text;
+		return font;
+	}
+	
+	override function onAlloc() {
+		super.onAlloc();
+		if( text != null && font != null ) initGlyphs(text);
+	}
+	
+	override function draw(ctx:RenderContext) {
+		glyphs.blendMode = blendMode;
+		if( dropShadow != null ) {
+			glyphs.x += dropShadow.dx;
+			glyphs.y += dropShadow.dy;
+			glyphs.calcAbsPos();
+			var old = glyphs.color;
+			glyphs.color = h3d.Vector.fromColor(dropShadow.color);
+			glyphs.color.w = dropShadow.alpha;
+			glyphs.draw(ctx);
+			glyphs.x -= dropShadow.dx;
+			glyphs.y -= dropShadow.dy;
+			glyphs.color = old;
+		}
+		super.draw(ctx);
+	}
+	
+	function set_text(t) {
+		this.text = t == null ? "null" : t;
+		if( allocated && font != null ) initGlyphs(text);
+		return t;
+	}
+	
+	public function calcTextWidth( text : String ) {
+		return initGlyphs(text,false).width;
+	}
+
+	function initGlyphs( text : String, rebuild = true ) {
+		if( rebuild ) glyphs.reset();
+		var x = 0, y = 0, xMax = 0, prevChar = -1;
+		for( i in 0...text.length ) {
+			var cc = text.charCodeAt(i);
+			var e = font.getChar(cc);
+			var newline = cc == '\n'.code;
+			var esize = e.width + e.getKerningOffset(prevChar);
+			// if the next word goes past the max width, change it into a newline
+			if( font.charset.isBreakChar(cc) && maxWidth != null ) {
+				var size = x + esize + letterSpacing;
+				var k = i + 1, max = text.length;
+				var prevChar = prevChar;
+				while( size <= maxWidth && k < text.length ) {
+					var cc = text.charCodeAt(k++);
+					if( font.charset.isSpace(cc) || cc == '\n'.code ) break;
+					var e = font.getChar(cc);
+					size += e.width + letterSpacing + e.getKerningOffset(prevChar);
+					prevChar = cc;
+				}
+				if( size > maxWidth ) {
+					newline = true;
+					if( font.charset.isSpace(cc) ) e = null;
+				}
+			}
+			if( e != null ) {
+				if( rebuild ) glyphs.add(x, y, e.t);
+				x += esize + letterSpacing;
+			}
+			if( newline ) {
+				if( x > xMax ) xMax = x;
+				x = 0;
+				y += font.lineHeight;
+				prevChar = -1;
+			} else
+				prevChar = cc;
+		}
+		return { width : x > xMax ? x : xMax, height : x > 0 ? y + font.lineHeight : y };
+	}
+	
+	function get_textHeight() {
+		return initGlyphs(text,false).height;
+	}
+	
+	function get_textWidth() {
+		return initGlyphs(text,false).width;
+	}
+	
+	function set_maxWidth(w) {
+		maxWidth = w;
+		this.text = text;
+		return w;
+	}
+	
+	function set_textColor(c) {
+		this.textColor = c;
+		glyphs.color = h3d.Vector.fromColor(c);
+		glyphs.color.w = alpha;
+		return c;
+	}
+
+}

+ 270 - 0
h2d/Tile.hx

@@ -0,0 +1,270 @@
+package h2d;
+
+@:allow(h2d)
+class Tile {
+	
+	static inline var EPSILON_PIXEL = 0.001;
+	
+	var innerTex : h3d.mat.Texture;
+	
+	var u : Float;
+	var v : Float;
+	var u2 : Float;
+	var v2 : Float;
+	
+	public var dx : Int;
+	public var dy : Int;
+	public var x(default,null) : Int;
+	public var y(default,null) : Int;
+	public var width(default,null) : Int;
+	public var height(default,null) : Int;
+	
+	function new(tex, x, y, w, h, dx=0, dy=0) {
+		this.innerTex = tex;
+		this.x = x;
+		this.y = y;
+		this.width = w;
+		this.height = h;
+		this.dx = dx;
+		this.dy = dy;
+		if( tex != null ) setTexture(tex);
+	}
+	
+	public function getTexture() {
+		if( innerTex == null || innerTex.isDisposed() )
+			return Tools.getCoreObjects().getEmptyTexture();
+		return innerTex;
+	}
+	
+	public function isDisposed() {
+		return innerTex == null || innerTex.isDisposed();
+	}
+		
+	function setTexture(tex) {
+		this.innerTex = tex;
+		if( tex != null ) {
+			this.u = (x + EPSILON_PIXEL) / tex.width;
+			this.v = (y + EPSILON_PIXEL) / tex.height;
+			this.u2 = (x + width - EPSILON_PIXEL) / tex.width;
+			this.v2 = (y + height - EPSILON_PIXEL) / tex.height;
+		}
+	}
+	
+	public inline function switchTexture( t : Tile ) {
+		setTexture(t.innerTex);
+	}
+	
+	public function sub( x, y, w, h, dx = 0, dy = 0 ) {
+		return new Tile(innerTex, this.x + x, this.y + y, w, h, dx, dy);
+	}
+	
+	public function center(dx, dy) {
+		return sub(0, 0, width, height, -dx, -dy);
+	}
+	
+	public function setPos(x, y) {
+		this.x = x;
+		this.y = y;
+		var tex = innerTex;
+		if( tex != null ) {
+			u = (x + EPSILON_PIXEL) / tex.width;
+			v = (y + EPSILON_PIXEL) / tex.height;
+			u2 = (width + x - EPSILON_PIXEL) / tex.width;
+			v2 = (height + y - EPSILON_PIXEL) / tex.height;
+		}
+	}
+	
+	public function setSize(w, h) {
+		this.width = w;
+		this.height = h;
+		var tex = innerTex;
+		if( tex != null ) {
+			u2 = (w + x - EPSILON_PIXEL) / tex.width;
+			v2 = (h + y - EPSILON_PIXEL) / tex.height;
+		}
+	}
+	
+	public function scaleToSize( w, h ) {
+		this.width = w;
+		this.height = h;
+	}
+	
+	public function scrollDiscrete( dx : Float, dy : Float ) {
+		var tex = innerTex;
+		u += dx / tex.width;
+		v -= dy / tex.height;
+		u2 += dx / tex.width;
+		v2 -= dy / tex.height;
+		x = Std.int(u * tex.width);
+		y = Std.int(v * tex.height);
+	}
+	
+	public function dispose() {
+		if( innerTex != null ) innerTex.dispose();
+		innerTex = null;
+	}
+	
+	public function clone() {
+		var t = new Tile(null, x, y, width, height, dx, dy);
+		t.innerTex = innerTex;
+		t.u = u;
+		t.u2 = u2;
+		t.v = v;
+		t.v2 = v2;
+		return t;
+	}
+	
+	
+	public function split( frames : Int, vertical = false ) {
+		var tl = [];
+		if( vertical ) {
+			var stride = Std.int(height / frames);
+			for( i in 0...frames )
+				tl.push(sub(0, i * stride, width, stride));
+		} else {
+			var stride = Std.int(width / frames);
+			for( i in 0...frames )
+				tl.push(sub(i * stride, 0, stride, height));
+		}
+		return tl;
+	}
+	
+	public function toString() {
+		return "Tile(" + x + "," + y + "," + width + "x" + height + (dx != 0 || dy != 0 ? "," + dx + ":" + dy:"") + ")";
+	}
+
+	function upload( bmp:hxd.BitmapData ) {
+		var w = innerTex.width;
+		var h = innerTex.height;
+		#if flash
+		if( w != bmp.width || h != bmp.height ) {
+			var bmp2 = new flash.display.BitmapData(w, h, true, 0);
+			var p0 = new flash.geom.Point(0, 0);
+			var bmp = bmp.toNative();
+			bmp2.copyPixels(bmp, bmp.rect, p0, bmp, p0, true);
+			innerTex.uploadBitmap(hxd.BitmapData.fromNative(bmp2));
+			bmp2.dispose();
+		} else
+		#end
+			innerTex.uploadBitmap(bmp);
+	}
+	
+
+	static var COLOR_CACHE = new Map<Int,h3d.mat.Texture>();
+	public static function fromColor( color : Int, ?width = 1, ?height = 1, ?allocPos : h3d.impl.AllocPos ) {
+		var t = COLOR_CACHE.get(color);
+		if( t == null || t.isDisposed() ) {
+			t = h3d.mat.Texture.fromColor(color, allocPos);
+			COLOR_CACHE.set(color, t);
+		}
+		var t = new Tile(t, 0, 0, 1, 1);
+		// scale to size
+		t.width = width;
+		t.height = height;
+		return t;
+	}
+	
+	public static function fromBitmap( bmp : hxd.BitmapData, ?allocPos : h3d.impl.AllocPos ) {
+		var w = 1, h = 1;
+		while( w < bmp.width )
+			w <<= 1;
+		while( h < bmp.height )
+			h <<= 1;
+		var tex = h3d.Engine.getCurrent().mem.allocTexture(w, h, false, allocPos);
+		var t = new Tile(tex, 0, 0, bmp.width, bmp.height);
+		t.upload(bmp);
+		return t;
+	}
+
+	public static function autoCut( bmp : hxd.BitmapData, width : Int, ?height : Int, ?allocPos : h3d.impl.AllocPos ) {
+		if( height == null ) height = width;
+		var colorBG = bmp.getPixel(bmp.width - 1, bmp.height - 1);
+		var tl = new Array();
+		var w = 1, h = 1;
+		while( w < bmp.width )
+			w <<= 1;
+		while( h < bmp.height )
+			h <<= 1;
+		var tex = h3d.Engine.getCurrent().mem.allocTexture(w, h, false, allocPos);
+		for( y in 0...Std.int(bmp.height / height) ) {
+			var a = [];
+			tl[y] = a;
+			for( x in 0...Std.int(bmp.width / width) ) {
+				var sz = isEmpty(bmp, x * width, y * height, width, height, colorBG);
+				if( sz == null )
+					break;
+				a.push(new Tile(tex,x*width+sz.dx, y*height+sz.dy, sz.w, sz.h, sz.dx, sz.dy));
+			}
+		}
+		var main = new Tile(tex, 0, 0, bmp.width, bmp.height);
+		main.upload(bmp);
+		return { main : main, tiles : tl };
+	}
+	
+	public static function fromTexture( t : h3d.mat.Texture ) {
+		return new Tile(t, 0, 0, t.width, t.height);
+	}
+	
+	public static function fromPixels( pixels : hxd.Pixels, ?allocPos : h3d.impl.AllocPos ) {
+		var pix2 = pixels.makeSquare(true);
+		var t = h3d.mat.Texture.fromPixels(pix2);
+		if( pix2 != pixels ) pix2.dispose();
+		return new Tile(t, 0, 0, pixels.width, pixels.height);
+	}
+	
+	#if flash
+	public static function fromSprites( sprites : Array<flash.display.Sprite>, ?allocPos : h3d.impl.AllocPos ) {
+		var tmp = [];
+		var width = 0;
+		var height = 0;
+		for( s in sprites ) {
+			var g = s.getBounds(s);
+			var dx = Math.floor(g.left);
+			var dy = Math.floor(g.top);
+			var w = Math.ceil(g.right) - dx;
+			var h = Math.ceil(g.bottom) - dy;
+			tmp.push( { s : s, x : width, dx : dx, dy : dy, w : w, h : h } );
+			width += w;
+			if( height < h ) height = h;
+		}
+		var rw = 1, rh = 1;
+		while( rw < width )
+			rw <<= 1;
+		while( rh < height )
+			rh <<= 1;
+		var bmp = new flash.display.BitmapData(rw, rh, true, 0);
+		var m = new flash.geom.Matrix();
+		for( t in tmp ) {
+			m.tx = t.x-t.dx;
+			m.ty = -t.dy;
+			bmp.draw(t.s, m);
+		}
+		var main = fromBitmap(hxd.BitmapData.fromNative(bmp), allocPos);
+		bmp.dispose();
+		var tiles = [];
+		for( t in tmp )
+			tiles.push(main.sub(t.x, 0, t.w, t.h, t.dx, t.dy));
+		return tiles;
+	}
+	#end
+	
+	static function isEmpty( b : hxd.BitmapData, px, py, width, height, bg : Int ) {
+		var empty = true;
+		var xmin = width, ymin = height, xmax = 0, ymax = 0;
+		for( x in 0...width )
+			for( y in 0...height ) {
+				var color : Int = b.getPixel(x+px, y+py);
+				if( color != bg ) {
+					empty = false;
+					if( x < xmin ) xmin = x;
+					if( y < ymin ) ymin = y;
+					if( x > xmax ) xmax = x;
+					if( y > ymax ) ymax = y;
+				}
+				if( color == bg )
+					b.setPixel(x+px, y+py, 0);
+			}
+		return empty ? null : { dx : xmin, dy : ymin, w : xmax - xmin + 1, h : ymax - ymin + 1 };
+	}
+	
+}

+ 195 - 0
h2d/TileColorGroup.hx

@@ -0,0 +1,195 @@
+package h2d;
+
+private class TileLayerContent extends h3d.prim.Primitive {
+
+	var tmp : hxd.FloatBuffer;
+
+	public function new() {
+		reset();
+	}
+
+	public function reset() {
+		tmp = new hxd.FloatBuffer();
+		if( buffer != null ) buffer.dispose();
+		buffer = null;
+	}
+
+	override public function triCount() {
+		if( buffer == null )
+			return tmp.length >> 4;
+		var v = 0;
+		var b = buffer;
+		while( b != null ) {
+			v += b.nvert;
+			b = b.next;
+		}
+		return v >> 1;
+	}
+
+	public function add( x : Int, y : Int, r : Float, g : Float, b : Float, a : Float, t : Tile ) {
+		var sx = x + t.dx;
+		var sy = y + t.dy;
+		tmp.push(sx);
+		tmp.push(sy);
+		tmp.push(t.u);
+		tmp.push(t.v);
+		tmp.push(r);
+		tmp.push(g);
+		tmp.push(b);
+		tmp.push(a);
+		tmp.push(sx + t.width);
+		tmp.push(sy);
+		tmp.push(t.u2);
+		tmp.push(t.v);
+		tmp.push(r);
+		tmp.push(g);
+		tmp.push(b);
+		tmp.push(a);
+		tmp.push(sx);
+		tmp.push(sy + t.height);
+		tmp.push(t.u);
+		tmp.push(t.v2);
+		tmp.push(r);
+		tmp.push(g);
+		tmp.push(b);
+		tmp.push(a);
+		tmp.push(sx + t.width);
+		tmp.push(sy + t.height);
+		tmp.push(t.u2);
+		tmp.push(t.v2);
+		tmp.push(r);
+		tmp.push(g);
+		tmp.push(b);
+		tmp.push(a);
+	}
+	
+	public function addPoint( x : Float, y : Float, color : Int ) {
+		tmp.push(x);
+		tmp.push(y);
+		tmp.push(0);
+		tmp.push(0);
+		insertColor(color);
+	}
+
+	inline function insertColor( c : Int ) {
+		tmp.push(((c >> 16) & 0xFF) / 255.);
+		tmp.push(((c >> 8) & 0xFF) / 255.);
+		tmp.push((c & 0xFF) / 255.);
+		tmp.push((c >>> 24) / 255.);
+	}
+
+	public inline function rectColor( x : Float, y : Float, w : Float, h : Float, color : Int ) {
+		tmp.push(x);
+		tmp.push(y);
+		tmp.push(0);
+		tmp.push(0);
+		insertColor(color);
+		tmp.push(x + w);
+		tmp.push(y);
+		tmp.push(1);
+		tmp.push(0);
+		insertColor(color);
+		tmp.push(x);
+		tmp.push(y + h);
+		tmp.push(0);
+		tmp.push(1);
+		insertColor(color);
+		tmp.push(x + w);
+		tmp.push(y + h);
+		tmp.push(1);
+		tmp.push(1);
+		insertColor(color);
+	}
+
+	public inline function rectGradient( x : Float, y : Float, w : Float, h : Float, ctl : Int, ctr : Int, cbl : Int, cbr : Int ) {
+		tmp.push(x);
+		tmp.push(y);
+		tmp.push(0);
+		tmp.push(0);
+		insertColor(ctl);
+		tmp.push(x + w);
+		tmp.push(y);
+		tmp.push(1);
+		tmp.push(0);
+		insertColor(ctr);
+		tmp.push(x);
+		tmp.push(y + h);
+		tmp.push(0);
+		tmp.push(1);
+		insertColor(cbl);
+		tmp.push(x + w);
+		tmp.push(y + h);
+		tmp.push(1);
+		tmp.push(0);
+		insertColor(cbr);
+	}
+
+	override public function alloc(engine:h3d.Engine) {
+		if( tmp == null ) reset();
+		buffer = engine.mem.allocVector(tmp, 8, 4);
+	}
+
+	public function doRender(engine, min, len) {
+		if( buffer == null || buffer.isDisposed() ) alloc(engine);
+		engine.renderQuadBuffer(buffer, min, len);
+	}
+
+}
+
+class TileColorGroup extends Drawable {
+
+	var content : TileLayerContent;
+	var curColor : h3d.Vector;
+
+	public var tile : Tile;
+	public var rangeMin : Int;
+	public var rangeMax : Int;
+
+	public function new(t,?parent) {
+		super(parent);
+		tile = t;
+		rangeMin = rangeMax = -1;
+		shader.hasVertexColor = true;
+		curColor = new h3d.Vector(1, 1, 1, 1);
+		content = new TileLayerContent();
+	}
+
+	public function reset() {
+		content.reset();
+	}
+
+	/**
+		Returns the number of tiles added to the group
+	**/
+	public function count() {
+		return content.triCount() >> 1;
+	}
+
+	override function onDelete() {
+		content.dispose();
+		super.onDelete();
+	}
+
+	public function setDefaultColor( rgb : Int, alpha = 1.0 ) {
+		curColor.x = ((rgb >> 16) & 0xFF) / 255;
+		curColor.y = ((rgb >> 8) & 0xFF) / 255;
+		curColor.z = (rgb & 0xFF) / 255;
+		curColor.w = alpha;
+	}
+
+	public inline function add(x, y, t) {
+		content.add(x, y, curColor.x, curColor.y, curColor.z, curColor.w, t);
+	}
+
+	public inline function addColor(x, y, r, g, b, a, t) {
+		content.add(x, y, r, g, b, a, t);
+	}
+
+	override function draw(ctx:RenderContext) {
+		setupShader(ctx.engine, tile, 0);
+		var min = rangeMin < 0 ? 0 : rangeMin * 2;
+		var max = content.triCount();
+		if( rangeMax > 0 && rangeMax < max * 2 ) max = rangeMax * 2;
+		content.doRender(ctx.engine, min, max - min);
+	}
+}

+ 110 - 0
h2d/TileGroup.hx

@@ -0,0 +1,110 @@
+package h2d;
+
+private class TileLayerContent extends h3d.prim.Primitive {
+
+	var tmp : hxd.FloatBuffer;
+	
+	public function new() {
+		reset();
+	}
+	
+	public function isEmpty() {
+		return buffer == null;
+	}
+	
+	public function reset() {
+		tmp = new hxd.FloatBuffer();
+		if( buffer != null ) buffer.dispose();
+		buffer = null;
+	}
+	
+	public function add( x : Int, y : Int, t : Tile ) {
+		var sx = x + t.dx;
+		var sy = y + t.dy;
+		var sx2 = sx + t.width;
+		var sy2 = sy + t.height;
+		tmp.push(sx);
+		tmp.push(sy);
+		tmp.push(t.u);
+		tmp.push(t.v);
+		tmp.push(sx2);
+		tmp.push(sy);
+		tmp.push(t.u2);
+		tmp.push(t.v);
+		tmp.push(sx);
+		tmp.push(sy2);
+		tmp.push(t.u);
+		tmp.push(t.v2);
+		tmp.push(sx2);
+		tmp.push(sy2);
+		tmp.push(t.u2);
+		tmp.push(t.v2);
+	}
+	
+	override public function triCount() {
+		if( buffer == null )
+			return tmp.length >> 3;
+		var v = 0;
+		var b = buffer;
+		while( b != null ) {
+			v += b.nvert;
+			b = b.next;
+		}
+		return v >> 1;
+	}
+	
+	override public function alloc(engine:h3d.Engine) {
+		if( tmp == null ) reset();
+		buffer = engine.mem.allocVector(tmp, 4, 4);
+	}
+
+	public function doRender(engine, min, len) {
+		if( buffer == null || buffer.isDisposed() ) alloc(engine);
+		engine.renderQuadBuffer(buffer, min, len);
+	}
+	
+}
+
+class TileGroup extends Drawable {
+	
+	var content : TileLayerContent;
+	
+	public var tile : Tile;
+	public var rangeMin : Int;
+	public var rangeMax : Int;
+	
+	public function new(t,?parent) {
+		tile = t;
+		rangeMin = rangeMax = -1;
+		content = new TileLayerContent();
+		super(parent);
+	}
+	
+	public function reset() {
+		content.reset();
+	}
+	
+	override function onDelete() {
+		content.dispose();
+		super.onDelete();
+	}
+	
+	public inline function add(x, y, t) {
+		content.add(x, y, t);
+	}
+	
+	/**
+		Returns the number of tiles added to the group
+	**/
+	public function count() {
+		return content.triCount() >> 1;
+	}
+		
+	override function draw(ctx:RenderContext) {
+		setupShader(ctx.engine, tile, 0);
+		var min = rangeMin < 0 ? 0 : rangeMin * 2;
+		var max = content.triCount();
+		if( rangeMax > 0 && rangeMax < max * 2 ) max = rangeMax * 2;
+		content.doRender(ctx.engine, min, max - min);
+	}
+}

+ 74 - 0
h2d/Tools.hx

@@ -0,0 +1,74 @@
+package h2d;
+
+private class CoreObjects  {
+	
+	public var tmpMatA : h3d.Vector;
+	public var tmpMatB : h3d.Vector;
+	public var tmpSize : h3d.Vector;
+	public var tmpUVPos : h3d.Vector;
+	public var tmpUVScale : h3d.Vector;
+	public var tmpColor : h3d.Vector;
+	public var tmpMatrix : h3d.Matrix;
+	public var tmpMaterial : h3d.mat.Material;
+	public var planBuffer : h3d.impl.Buffer;
+	
+	var emptyTexture : h3d.mat.Texture;
+	
+	public function new() {
+		tmpMatA = new h3d.Vector();
+		tmpMatB = new h3d.Vector();
+		tmpColor = new h3d.Vector();
+		tmpSize = new h3d.Vector();
+		tmpUVPos = new h3d.Vector();
+		tmpUVScale = new h3d.Vector();
+		tmpMatrix = new h3d.Matrix();
+		tmpMaterial = new h3d.mat.Material(null);
+		tmpMaterial.culling = None;
+		tmpMaterial.depth(false, Always);
+		
+		var vector = new hxd.FloatBuffer();
+		for( pt in [[0, 0], [1, 0], [0, 1], [1, 1]] ) {
+			vector.push(pt[0]);
+			vector.push(pt[1]);
+			vector.push(pt[0]);
+			vector.push(pt[1]);
+		}
+		
+		planBuffer = h3d.Engine.getCurrent().mem.allocVector(vector, 4, 4);
+	}
+	
+	public function getEmptyTexture() {
+		if( emptyTexture == null || emptyTexture.isDisposed() ) {
+			if( emptyTexture != null ) emptyTexture.dispose();
+			emptyTexture = h3d.mat.Texture.fromColor(0xFFFF00FF);
+		}
+		return emptyTexture;
+	}
+	
+}
+
+class Tools {
+	
+	static var CORE : CoreObjects = null;
+	
+	@:allow(h2d)
+	static function getCoreObjects() {
+		var c = CORE;
+		if( c == null ) {
+			c = new CoreObjects();
+			CORE = c;
+		}
+		return c;
+	}
+	
+	@:allow(h2d)
+	@:access(h3d.impl.BigBuffer)
+	static function checkCoreObjects() {
+		var c = CORE;
+		if( c == null ) return;
+		// if we have lost our context
+		if( c.planBuffer.b.isDisposed() )
+			CORE = null;
+	}
+	
+}

+ 131 - 0
h2d/col/Bounds.hx

@@ -0,0 +1,131 @@
+package h2d.col;
+
+class Bounds {
+	
+	public var xMin : Float;
+	public var yMin : Float;
+
+	public var xMax : Float;
+	public var yMax : Float;
+	
+	public inline function new() {
+		empty();
+	}
+	
+	public inline function collide( b : Bounds ) {
+		return !(xMin > b.xMax || yMin > b.yMax || xMax < b.xMin || yMax < b.yMin);
+	}
+	
+	public inline function include( p : Point ) {
+		return p.x >= xMin && p.x < xMax && p.y >= yMin && p.y < yMax;
+	}
+	
+	public inline function add( b : Bounds ) {
+		if( b.xMin < xMin ) xMin = b.xMin;
+		if( b.xMax > xMax ) xMax = b.xMax;
+		if( b.yMin < yMin ) yMin = b.yMin;
+		if( b.yMax > yMax ) yMax = b.yMax;
+	}
+
+	public inline function addPoint( p : Point ) {
+		if( p.x < xMin ) xMin = p.x;
+		if( p.x > xMax ) xMax = p.x;
+		if( p.y < yMin ) yMin = p.y;
+		if( p.y > yMax ) yMax = p.y;
+	}
+	
+	public inline function setMin( p : Point ) {
+		xMin = p.x;
+		yMin = p.y;
+	}
+
+	public inline function setMax( p : Point ) {
+		xMax = p.x;
+		yMax = p.y;
+	}
+	
+	public function load( b : Bounds ) {
+		xMin = b.xMin;
+		yMin = b.yMin;
+		xMax = b.xMax;
+		yMax = b.yMax;
+	}
+	
+	public function scaleCenter( v : Float ) {
+		var dx = (xMax - xMin) * 0.5 * v;
+		var dy = (yMax - yMin) * 0.5 * v;
+		var mx = (xMax + xMin) * 0.5;
+		var my = (yMax + yMin) * 0.5;
+		xMin = mx - dx * v;
+		yMin = my - dy * v;
+		xMax = mx + dx * v;
+		yMax = my + dy * v;
+	}
+	
+	public inline function offset( dx : Float, dy : Float ) {
+		xMin += dx;
+		xMax += dx;
+		yMin += dy;
+		yMax += dy;
+	}
+	
+	public inline function getMin() {
+		return new Point(xMin, yMin);
+	}
+	
+	public inline function getCenter() {
+		return new Point((xMin + xMax) * 0.5, (yMin + yMax) * 0.5);
+	}
+
+	public inline function getSize() {
+		return new Point(xMax - xMin, yMax - yMin);
+	}
+	
+	public inline function getMax() {
+		return new Point(xMax, yMax);
+	}
+	
+	public inline function empty() {
+		xMin = 1e20;
+		yMin = 1e20;
+		xMax = -1e20;
+		yMax = -1e20;
+	}
+
+	public inline function all() {
+		xMin = -1e20;
+		yMin = -1e20;
+		xMax = 1e20;
+		yMax = 1e20;
+	}
+	
+	public inline function clone() {
+		var b = new Bounds();
+		b.xMin = xMin;
+		b.yMin = yMin;
+		b.xMax = xMax;
+		b.yMax = yMax;
+		return b;
+	}
+	
+	public function toString() {
+		return "{" + getMin() + "," + getMax() + "}";
+	}
+
+	public static inline function fromValues( x0 : Float, y0 : Float, width : Float, height : Float ) {
+		var b = new Bounds();
+		b.xMin = x0;
+		b.yMin = y0;
+		b.xMax = x0 + width;
+		b.yMax = y0 + height;
+		return b;
+	}
+	
+	public static inline function fromPoints( min : Point, max : Point ) {
+		var b = new Bounds();
+		b.setMin(min);
+		b.setMax(max);
+		return b;
+	}
+	
+}

+ 39 - 0
h2d/col/Circle.hx

@@ -0,0 +1,39 @@
+package h2d.col;
+import hxd.Math;
+
+class Circle {
+
+	public var x : Float;
+	public var y : Float;
+	public var ray : Float;
+	
+	public inline function new( x : Float, y : Float, ray : Float ) {
+		this.x = x;
+		this.y = y;
+		this.ray = ray;
+	}
+	
+	public inline function distanceSq( p : Point ) {
+		var dx = p.x - x;
+		var dy = p.y - y;
+		var d = dx * dx + dy * dy - ray * ray;
+		return d < 0 ? 0 : d;
+	}
+
+	public inline function side( p : Point ) {
+		var dx = p.x - x;
+		var dy = p.y - y;
+		return ray * ray - (dx * dx + dy * dy);
+	}
+	
+	public inline function collideCircle( c : Circle ) {
+		var dx = x - c.x;
+		var dy = y - c.y;
+		return dx * dx + dy * dy < (ray + c.ray) * (ray + c.ray);
+	}
+
+	public function toString() {
+		return '{${Math.fmt(x)},${Math.fmt(y)},${Math.fmt(ray)}}';
+	}
+	
+}

文件差異過大導致無法顯示
+ 0 - 0
h2d/col/Delaunay.hx


+ 65 - 0
h2d/col/Point.hx

@@ -0,0 +1,65 @@
+package h2d.col;
+import hxd.Math;
+
+class Point {
+	
+	public var x : Float;
+	public var y : Float;
+	
+	public inline function new(x = 0., y = 0.) {
+		this.x = x;
+		this.y = y;
+	}
+	
+	public inline function distanceSq( p : Point ) {
+		var dx = x - p.x;
+		var dy = y - p.y;
+		return dx * dx + dy * dy;
+	}
+	
+	public inline function distance( p : Point ) {
+		return Math.sqrt(distanceSq(p));
+	}
+	
+	public function toString() {
+		return "{" + Math.fmt(x) + "," + Math.fmt(y) + "}";
+	}
+		
+	public inline function sub( p : Point ) {
+		return new Point(x - p.x, y - p.y);
+	}
+
+	public inline function add( p : Point ) {
+		return new Point(x + p.x, y + p.y);
+	}
+
+	public inline function dot( p : Point ) {
+		return x * p.x + y * p.y;
+	}
+
+	public inline function lengthSq() {
+		return x * x + y * y;
+	}
+
+	public inline function length() {
+		return Math.sqrt(lengthSq());
+	}
+
+	public function normalize() {
+		var k = lengthSq();
+		if( k < Math.EPSILON ) k = 0 else k = Math.invSqrt(k);
+		x *= k;
+		y *= k;
+	}
+
+	public inline function set(x,y) {
+		this.x = x;
+		this.y = y;
+	}
+
+	public inline function scale( f : Float ) {
+		x *= f;
+		y *= f;
+	}
+	
+}

+ 77 - 0
h2d/col/Poly.hx

@@ -0,0 +1,77 @@
+package h2d.col;
+import hxd.Math;
+
+class Poly {
+
+	public var points : Array<Point>;
+	var segments : Array<Seg>;
+	
+	public function new( points ) {
+		this.points = points;
+	}
+		
+	public function isConvex() {
+		for( i in 0...points.length ) {
+			var p1 = points[i];
+			var p2 = points[(i + 1) % points.length];
+			var p3 = points[(i + 2) % points.length];
+			if( (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) < 0 )
+				return false;
+		}
+		return true;
+	}
+	
+	public function calculateArea() {
+		var s = 0.;
+		for( i in 0...points.length ) {
+			var p = points[i];
+			var n = points[(i + 1) % points.length];
+			s += p.y * n.x - p.x * n.y;
+		}
+		return s * 0.5;
+	}
+
+	public function getSegments() {
+		if( segments != null )
+			return segments;
+		segments = [];
+		for( i in 0...points.length ) {
+			var s = new Seg(points[i], points[(i + 1) % points.length]);
+			segments.push(s);
+		}
+		return segments;
+	}
+	
+	public function hasPoint( p : Point ) {
+		for( s in getSegments() )
+			if( s.side(p) < 0 )
+				return false;
+		return true;
+	}
+	
+	public function project( p : Point ) : Point {
+		var dmin = 1e20, smin = null;
+		for( s in getSegments() ) {
+			var d = s.distanceSq(p);
+			if( d < dmin ) {
+				dmin = d;
+				smin = s;
+			}
+		}
+		return smin.project(p);
+	}
+	
+	public function distanceSq( p : Point ) {
+		var dmin = 1e20;
+		for( s in getSegments() ) {
+			var d = s.distanceSq(p);
+			if( d < dmin ) dmin = d;
+		}
+		return dmin;
+	}
+	
+	public inline function distance( p : Point ) {
+		return Math.sqrt(distanceSq(p));
+	}
+	
+}

+ 146 - 0
h2d/col/Polynomial.hx

@@ -0,0 +1,146 @@
+package h2d.col;
+
+private class Matrix {
+	public var data : haxe.ds.Vector<haxe.ds.Vector<Float>>;
+	public var m : Int;
+	public var n : Int;
+	
+	public function new(m, n) {
+		this.m = m;
+		this.n = n;
+		this.data = new haxe.ds.Vector(m);
+		for( i in 0...m )
+			data[i] = new haxe.ds.Vector(n);
+	}
+	
+	public function clone() {
+		var m2 = new Matrix(m, n);
+		for( i in 0...m )
+			for( j in 0...n )
+				m2.data[i][j] = data[i][j];
+		return m2;
+	}
+	
+	function toString() {
+		return "[" + [for( k in data ) "\n" + Std.string(k)].join("") + "\n]";
+	}
+}
+
+private class QR {
+	
+	var qr : haxe.ds.Vector<haxe.ds.Vector<Float>>;
+	var rDiag : haxe.ds.Vector<Float>;
+	var m : Int;
+	var n : Int;
+	
+	public function new( mat : Matrix ) {
+		this.m = mat.m;
+		this.n = mat.n;
+		qr = mat.clone().data;
+		rDiag = new haxe.ds.Vector(n);
+		
+		for( k in 0...n ) {
+			var nrm = 0.;
+			for( i in k...m )
+				nrm = hypot(nrm, qr[i][k]);
+			if( nrm != 0 ) {
+				if( qr[k][k] < 0 ) nrm = -nrm;
+				for( i in k...m )
+					qr[i][k] /= nrm;
+				qr[k][k] += 1.0;
+				for( j in k + 1...n ) {
+					var s = 0.;
+					for( i in k...m )
+						s += qr[i][k] * qr[i][j];
+					s = -s / qr[k][k];
+					for( i in k...m )
+						qr[i][j] += s * qr[i][k];
+				}
+			}
+			rDiag[k] = -nrm;
+		}
+	}
+	
+	function isFullRank() {
+		for( j in 0...n )
+			if( rDiag[j] == 0 )
+				return false;
+		return true;
+	}
+	
+	public function solve( b : Matrix ) {
+		if( b.m != m ) throw "Invalid matrix size";
+		if( !isFullRank() ) return null;
+		var nx = b.n;
+		var X = b.clone().data;
+		for( k in 0...n )
+			for( j in 0...nx ) {
+				var s = 0.;
+				for( i in k...m )
+					s += qr[i][k] * X[i][j];
+				s = -s / qr[k][k];
+				for( i in k...m )
+					X[i][j] += s * qr[i][k];
+			}
+		var k = n - 1;
+		while( k >= 0 ) {
+			for( j in 0...nx )
+				X[k][j] /= rDiag[k];
+			for( i in 0...k )
+				for( j in 0...nx )
+					X[i][j] -= X[k][j] * qr[i][k];
+			k--;
+		}
+		var beta = new Array();
+		for( i in 0...n )
+			beta.push(X[i][0]);
+		return beta;
+	}
+	
+	function hypot( x : Float, y : Float ) {
+		if( x < 0 ) x = -x;
+		if( y < 0 ) y = -y;
+		var t;
+		if( x < y ) {
+			t = x;
+			x = y;
+		} else
+			t = y;
+		t = t/x;
+		return x * Math.sqrt(1+t*t);
+	}
+	
+}
+
+class Polynomial {
+
+	/**
+		Calculate the best fit curve of given degree that match the input values. Returns the polynomial exponents. For instance [2,8,-5] will represent 2 + 8 x - 5 x^2
+	**/
+	public static function regress( xVals : Array<Float>, yVals : Array<Float>, degree : Int ) : Array<Float> {
+		var n = xVals.length;
+		// fix bug with 4 values
+		if( n == 4 ) {
+			xVals.unshift(xVals[0]);
+			yVals.unshift(yVals[0]);
+			n++;
+		}
+		var x = new Matrix(n, degree + 1);
+		for( i in 0...n )
+			for( j in 0...degree+1 )
+				x.data[i][j] = Math.pow(xVals[i], j);
+				
+		var y = new Matrix(yVals.length, n);
+		for( i in 0...yVals.length )
+			y.data[i][0] = yVals[i];
+		
+		var qr = new QR(x);
+		var beta = qr.solve(y);
+		if( beta == null ) {
+			beta = regress(xVals, yVals, degree-1);
+			beta.push(0);
+		}
+		return beta;
+	}
+	
+}

+ 62 - 0
h2d/col/Seg.hx

@@ -0,0 +1,62 @@
+package h2d.col;
+import hxd.Math;
+
+class Seg {
+
+	public var x : Float;
+	public var y : Float;
+	public var dx : Float;
+	public var dy : Float;
+	public var lenSq : Float;
+	public var invLenSq : Float;
+	
+	public inline function new( p1 : Point, p2 : Point ) {
+		x = p1.x;
+		y = p1.y;
+		dx = p2.x - x;
+		dy = p2.y - y;
+		lenSq = dx * dx + dy * dy;
+		invLenSq = 1 / lenSq;
+	}
+	
+	public inline function side( p : Point ) {
+		return dx * (p.y - y) - dy * (p.x - x);
+	}
+	
+	public inline function distanceSq( p : Point ) {
+		var px = p.x - x;
+		var py = p.y - y;
+		var t = px * dx + py * dy;
+		return if( t < 0 )
+			px * px + py * py
+		else if( t > lenSq ) {
+			var kx = p.x - (x + dx);
+			var ky = p.y - (y + dy);
+			kx * kx + ky * ky;
+		} else {
+			var tl2 = t * invLenSq;
+			var pdx = x + tl2 * dx - p.x;
+			var pdy = y + tl2 * dy - p.y;
+			pdx * pdx + pdy * pdy;
+		}
+	}
+
+	public inline function distance( p : Point ) {
+		return Math.sqrt(distanceSq(p));
+	}
+	
+	public inline function project( p : Point ) : Point {
+		var px = p.x - x;
+		var py = p.y - y;
+		var t = px * dx + py * dy;
+		return if( t < 0 )
+			new Point(x, y);
+		else if( t > lenSq )
+			new Point(x + dx, y + dy);
+		else {
+			var tl2 = t * invLenSq;
+			new Point(x + tl2 * dx, y + tl2 * dy);
+		}
+	}
+
+}

+ 47 - 0
h2d/col/Triangle.hx

@@ -0,0 +1,47 @@
+package h2d.col;
+
+class Triangle {
+	
+	static inline var UNDEF = 1.1315e-17;
+	
+	public var a : Point;
+	public var b : Point;
+	public var c : Point;
+	var area : Float;
+	var invArea : Float;
+	
+	public inline function new( a : Point, b : Point, c : Point ) {
+		this.a = a;
+		this.b = b;
+		this.c = c;
+		area = UNDEF;
+	}
+	
+	public inline function getCenter() {
+		return new Point((a.x + b.x + c.x) / 3, (a.y + b.y + c.y) / 3);
+	}
+	
+	public function getArea() {
+		if( area == UNDEF ) {
+			area = ((a.y * b.x - a.x * b.y) + (b.y * c.x - b.x * c.y) + (c.y * a.x - c.x * a.y)) * -0.5;
+			invArea = 1 / area;
+		}
+		return area;
+	}
+
+	public inline function getInvArea() {
+		getArea();
+		return invArea;
+	}
+	
+	/**
+		Calculate barycentric coordinates for the point p
+	**/
+	public inline function barycentric( p : Point ) {
+		var area = getInvArea() * 0.5;
+		var s = area * (a.y * c.x - a.x * c.y + (c.y - a.y) * p.x + (a.x - c.x) * p.y);
+		var t = area * (a.x * b.y - a.y * b.x + (a.y - b.y) * p.x + (b.x - a.x) * p.y);
+		return new h3d.col.Point(1 - s - t, s, t);
+	}
+	
+}

+ 1502 - 0
h2d/col/Voronoi.hx

@@ -0,0 +1,1502 @@
+/*
+	Port by Nicolas Cannasse
+	Copyright (C) 2010-2013 Raymond Hill
+	MIT License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md
+
+	Author: Raymond Hill ([email protected])
+	Contributor: Jesse Morgan ([email protected])
+	File: rhill-voronoi-core.js
+	Version: 0.98
+	Date: January 21, 2013
+	Description: This is my personal Javascript implementation of
+	Steven Fortune's algorithm to compute Voronoi diagrams.
+
+	License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md
+	Credits: See https://github.com/gorhill/Javascript-Voronoi/CREDITS.md
+	History: See https://github.com/gorhill/Javascript-Voronoi/CHANGELOG.md
+*/
+package h2d.col;
+import hxd.Math;
+
+// ---------------------------------------------------------------------------
+// Red-Black tree code (based on C version of "rbtree" by Franck Bui-Huu
+// https://github.com/fbuihuu/libtree/blob/master/rb.c
+
+private class RBNode<T:RBNode<T>> {
+	public var rbRed : Bool;
+	public var rbLeft : T;
+	public var rbRight : T;
+	public var rbParent : T;
+	public var rbNext : T;
+	public var rbPrevious : T;
+}
+
+@:generic private class RBTree<T:RBNode<T>> {
+	
+	public var root : T;
+	
+	public function new() {
+		this.root = null;
+    }
+
+	public function rbInsertSuccessor(node : T, successor : T) {
+		var parent;
+		if (node != null) {
+			// >>> rhill 2011-05-27: Performance: cache previous/next nodes
+			successor.rbPrevious = node;
+			successor.rbNext = node.rbNext;
+			if (node.rbNext != null ) {
+				node.rbNext.rbPrevious = successor;
+				}
+			node.rbNext = successor;
+			// <<<
+			if (node.rbRight != null) {
+				// in-place expansion of node.rbRight.getFirst();
+				node = node.rbRight;
+				while (node.rbLeft != null) {node = node.rbLeft;}
+				node.rbLeft = successor;
+				}
+			else {
+				node.rbRight = successor;
+				}
+			parent = node;
+			}
+		// rhill 2011-06-07: if node is null, successor must be inserted
+		// to the left-most part of the tree
+		else if (this.root != null) {
+			node = this.getFirst(this.root);
+			// >>> Performance: cache previous/next nodes
+			successor.rbPrevious = null;
+			successor.rbNext = node;
+			node.rbPrevious = successor;
+			// <<<
+			node.rbLeft = successor;
+			parent = node;
+			}
+		else {
+			// >>> Performance: cache previous/next nodes
+			successor.rbPrevious = successor.rbNext = null;
+			// <<<
+			this.root = successor;
+			parent = null;
+			}
+		successor.rbLeft = successor.rbRight = null;
+		successor.rbParent = parent;
+		successor.rbRed = true;
+		// Fixup the modified tree by recoloring nodes and performing
+		// rotations (2 at most) hence the red-black tree properties are
+		// preserved.
+		var grandpa, uncle;
+		node = successor;
+		while (parent != null && parent.rbRed) {
+			grandpa = parent.rbParent;
+			if (parent == grandpa.rbLeft) {
+				uncle = grandpa.rbRight;
+				if (uncle != null && uncle.rbRed) {
+					parent.rbRed = uncle.rbRed = false;
+					grandpa.rbRed = true;
+					node = grandpa;
+					}
+				else {
+					if (node == parent.rbRight) {
+						this.rbRotateLeft(parent);
+						node = parent;
+						parent = node.rbParent;
+						}
+					parent.rbRed = false;
+					grandpa.rbRed = true;
+					this.rbRotateRight(grandpa);
+					}
+				}
+			else {
+				uncle = grandpa.rbLeft;
+				if (uncle != null && uncle.rbRed) {
+					parent.rbRed = uncle.rbRed = false;
+					grandpa.rbRed = true;
+					node = grandpa;
+					}
+				else {
+					if (node == parent.rbLeft) {
+						this.rbRotateRight(parent);
+						node = parent;
+						parent = node.rbParent;
+						}
+					parent.rbRed = false;
+					grandpa.rbRed = true;
+					this.rbRotateLeft(grandpa);
+					}
+				}
+			parent = node.rbParent;
+			}
+		this.root.rbRed = false;
+    }
+
+	public function rbRemoveNode(node:T) {
+		// >>> rhill 2011-05-27: Performance: cache previous/next nodes
+		if (node.rbNext != null) {
+			node.rbNext.rbPrevious = node.rbPrevious;
+			}
+		if (node.rbPrevious != null) {
+			node.rbPrevious.rbNext = node.rbNext;
+			}
+		node.rbNext = node.rbPrevious = null;
+		// <<<
+		var parent = node.rbParent,
+			left = node.rbLeft,
+			right = node.rbRight,
+			next;
+		if (left == null) {
+			next = right;
+			}
+		else if (right == null) {
+			next = left;
+			}
+		else {
+			next = this.getFirst(right);
+			}
+		if (parent != null) {
+			if (parent.rbLeft == node) {
+				parent.rbLeft = next;
+				}
+			else {
+				parent.rbRight = next;
+				}
+			}
+		else {
+			this.root = next;
+			}
+		// enforce red-black rules
+		var isRed;
+		if (left != null && right != null) {
+			isRed = next.rbRed;
+			next.rbRed = node.rbRed;
+			next.rbLeft = left;
+			left.rbParent = next;
+			if (next != right) {
+				parent = next.rbParent;
+				next.rbParent = node.rbParent;
+				node = next.rbRight;
+				parent.rbLeft = node;
+				next.rbRight = right;
+				right.rbParent = next;
+				}
+			else {
+				next.rbParent = parent;
+				parent = next;
+				node = next.rbRight;
+				}
+			}
+		else {
+			isRed = node.rbRed;
+			node = next;
+			}
+		// 'node' is now the sole successor's child and 'parent' its
+		// new parent (since the successor can have been moved)
+		if (node != null) {
+			node.rbParent = parent;
+			}
+		// the 'easy' cases
+		if (isRed) {return;}
+		if (node != null && node.rbRed) {
+			node.rbRed = false;
+			return;
+			}
+		// the other cases
+		var sibling;
+		do {
+			if (node == this.root) {
+				break;
+				}
+			if (node == parent.rbLeft) {
+				sibling = parent.rbRight;
+				if (sibling.rbRed) {
+					sibling.rbRed = false;
+					parent.rbRed = true;
+					this.rbRotateLeft(parent);
+					sibling = parent.rbRight;
+					}
+				if ((sibling.rbLeft != null && sibling.rbLeft.rbRed) || (sibling.rbRight != null && sibling.rbRight.rbRed)) {
+					if (sibling.rbRight == null || !sibling.rbRight.rbRed) {
+						sibling.rbLeft.rbRed = false;
+						sibling.rbRed = true;
+						this.rbRotateRight(sibling);
+						sibling = parent.rbRight;
+						}
+					sibling.rbRed = parent.rbRed;
+					parent.rbRed = sibling.rbRight.rbRed = false;
+					this.rbRotateLeft(parent);
+					node = this.root;
+					break;
+					}
+				}
+			else {
+				sibling = parent.rbLeft;
+				if (sibling.rbRed) {
+					sibling.rbRed = false;
+					parent.rbRed = true;
+					this.rbRotateRight(parent);
+					sibling = parent.rbLeft;
+					}
+				if ((sibling.rbLeft != null && sibling.rbLeft.rbRed) || (sibling.rbRight != null && sibling.rbRight.rbRed)) {
+					if (sibling.rbLeft == null || !sibling.rbLeft.rbRed) {
+						sibling.rbRight.rbRed = false;
+						sibling.rbRed = true;
+						this.rbRotateLeft(sibling);
+						sibling = parent.rbLeft;
+						}
+					sibling.rbRed = parent.rbRed;
+					parent.rbRed = sibling.rbLeft.rbRed = false;
+					this.rbRotateRight(parent);
+					node = this.root;
+					break;
+					}
+				}
+			sibling.rbRed = true;
+			node = parent;
+			parent = parent.rbParent;
+		} while (!node.rbRed);
+		if (node != null) {node.rbRed = false;}
+    }
+
+	function rbRotateLeft(node:T) {
+		var p = node,
+			q = node.rbRight, // can't be null
+			parent = p.rbParent;
+		if (parent != null) {
+			if (parent.rbLeft == p) {
+				parent.rbLeft = q;
+				}
+			else {
+				parent.rbRight = q;
+				}
+			}
+		else {
+			this.root = q;
+			}
+		q.rbParent = parent;
+		p.rbParent = q;
+		p.rbRight = q.rbLeft;
+		if (p.rbRight != null) {
+			p.rbRight.rbParent = p;
+			}
+		q.rbLeft = p;
+    }
+
+	function rbRotateRight(node:T) {
+		var p = node,
+			q = node.rbLeft, // can't be null
+			parent = p.rbParent;
+		if (parent != null) {
+			if (parent.rbLeft == p) {
+				parent.rbLeft = q;
+				}
+			else {
+				parent.rbRight = q;
+				}
+			}
+		else {
+			this.root = q;
+			}
+		q.rbParent = parent;
+		p.rbParent = q;
+		p.rbLeft = q.rbRight;
+		if (p.rbLeft != null) {
+			p.rbLeft.rbParent = p;
+			}
+		q.rbRight = p;
+    }
+
+	public function getFirst(node:T) {
+		while(node.rbLeft != null)
+			node = node.rbLeft;
+		return node;
+    }
+
+	public function getLast(node:T) {
+		while( node.rbRight != null )
+			node = node.rbRight;
+		return node;
+    }
+}
+
+class Cell {
+	
+	public var id : Int;
+	public var point : Point;
+	public var halfedges : Array<Halfedge>;
+	
+	public function new(id, point) {
+		this.id = id;
+		this.point = point;
+		this.halfedges = [];
+    }
+	
+	public function getCircle() {
+		// still not the best enclosing circle
+		// would require implementing http://www.personal.kent.edu/~rmuhamma/Compgeometry/MyCG/CG-Applets/Center/centercli.htm for complete solution
+		var p = new Point(), ec = 0;
+		for( e in halfedges ) {
+			var ep = e.getStartpoint();
+			p.x += ep.x;
+			p.y += ep.y;
+			ec++;
+		}
+		p.x /= ec;
+		p.y /= ec;
+		var r = 0.;
+		for( e in halfedges ) {
+			var d = p.distanceSq(e.getStartpoint());
+			if( d > r ) r = d;
+		}
+		return new Circle(p.x, p.y, Math.sqrt(r));
+	}
+
+	public function prepare() {
+		var halfedges = this.halfedges, iHalfedge = halfedges.length, edge;
+		// get rid of unused halfedges
+		// rhill 2011-05-27: Keep it simple, no point here in trying
+		// to be fancy: dangling edges are a typically a minority.
+		while (iHalfedge-- != 0) {
+			edge = halfedges[iHalfedge].edge;
+			if (edge.vb == null || edge.va == null) {
+				halfedges.splice(iHalfedge,1);
+				}
+			}
+
+		// rhill 2011-05-26: I tried to use a binary search at insertion
+		// time to keep the array sorted on-the-fly (in Cell.addHalfedge()).
+		// There was no real benefits in doing so, performance on
+		// Firefox 3.6 was improved marginally, while performance on
+		// Opera 11 was penalized marginally.
+		halfedges.sort(sortByAngle);
+		return halfedges.length;
+	}
+	
+	static function sortByAngle(a:Halfedge, b:Halfedge) {
+		return b.angle > a.angle ? 1 : (b.angle < a.angle ? -1 : 0);
+	}
+
+	// Return a list of the neighbor Ids
+	public function getNeighborIds() {
+		var neighbors = [],
+			iHalfedge = this.halfedges.length,
+			edge;
+		while (iHalfedge-- != 0){
+			edge = this.halfedges[iHalfedge].edge;
+			// NC : changes pointId check to object == check
+			if (edge.lPoint != null && edge.lPoint != this.point) {
+				neighbors.push(edge.lPoint);
+				}
+			else if (edge.rPoint != null && edge.rPoint != this.point){
+				neighbors.push(edge.rPoint);
+				}
+			}
+		return neighbors;
+    }
+
+	public function getBbox() {
+		var halfedges = this.halfedges,
+			iHalfedge = halfedges.length,
+			xmin = Math.POSITIVE_INFINITY,
+			ymin = Math.POSITIVE_INFINITY,
+			xmax = Math.NEGATIVE_INFINITY,
+			ymax = Math.NEGATIVE_INFINITY;
+		while (iHalfedge-- != 0) {
+			var v = halfedges[iHalfedge].getStartpoint();
+			var vx = v.x;
+			var vy = v.y;
+			if (vx < xmin) {xmin = vx;}
+			if (vy < ymin) {ymin = vy;}
+			if (vx > xmax) {xmax = vx;}
+			if (vy > ymax) {ymax = vy;}
+			// we dont need to take into account end point,
+			// since each end point matches a start point
+			}
+		return {
+			x: xmin,
+			y: ymin,
+			width: xmax-xmin,
+			height: ymax-ymin
+		};
+    }
+
+	// Return whether a point is inside, on, or outside the cell:
+	//   -1: point is outside the perimeter of the cell
+	//    0: point is on the perimeter of the cell
+	//    1: point is inside the perimeter of the cell
+	//
+	public function pointIntersection(x:Float, y:Float) {
+		// Check if point in polygon. Since all polygons of a Voronoi
+		// diagram are convex, then:
+		// http://paulbourke.net/geometry/polygonmesh/
+		// Solution 3 (2D):
+		//   "If the polygon is convex then one can consider the polygon
+		//   "as a 'path' from the first vertex. A point is on the interior
+		//   "of this polygons if it is always on the same side of all the
+		//   "line segments making up the path. ...
+		//   "(y - y0) (x1 - x0) - (x - x0) (y1 - y0)
+		//   "if it is less than 0 then P is to the right of the line segment,
+		//   "if greater than 0 it is to the left, if equal to 0 then it lies
+		//   "on the line segment"
+		var halfedges = this.halfedges,
+			iHalfedge = halfedges.length,
+			p0, p1, r;
+		while (iHalfedge-- != 0) {
+			var halfedge = halfedges[iHalfedge];
+			p0 = halfedge.getStartpoint();
+			p1 = halfedge.getEndpoint();
+			r = (y-p0.y)*(p1.x-p0.x)-(x-p0.x)*(p1.y-p0.y);
+			if (r == 0) {
+				return 0;
+				}
+			if (r > 0) {
+				return -1;
+				}
+			}
+		return 1;
+	}
+	
+}
+
+class Edge {
+
+	public var id : Int;
+	public var lPoint : Point;
+	public var rPoint : Point;
+	public var lCell : Null<Cell>;
+	public var rCell : Null<Cell>;
+	public var va : Null<Point>;
+	public var vb : Null<Point>;
+	
+	public function new(lPoint, rPoint) {
+		this.lPoint = lPoint;
+		this.rPoint = rPoint;
+		this.va = this.vb = null;
+    }
+}
+
+
+class Halfedge {
+	
+	public var point : Point;
+	public var edge : Edge;
+	public var angle : Float;
+	
+	public function new(edge, lPoint:Point, rPoint:Point) {
+		this.point = lPoint;
+		this.edge = edge;
+		// 'angle' is a value to be used for properly sorting the
+		// halfsegments counterclockwise. By convention, we will
+		// use the angle of the line defined by the 'point to the left'
+		// to the 'point to the right'.
+		// However, border edges have no 'point to the right': thus we
+		// use the angle of line perpendicular to the halfsegment (the
+		// edge should have both end points defined in such case.)
+		if (rPoint != null) {
+			this.angle = Math.atan2(rPoint.y-lPoint.y, rPoint.x-lPoint.x);
+		} else {
+			var va = edge.va,
+				vb = edge.vb;
+			// rhill 2011-05-31: used to call getStartpoint()/getEndpoint(),
+			// but for performance purpose, these are expanded in place here.
+			this.angle = edge.lPoint == lPoint
+				? Math.atan2(vb.x-va.x, va.y-vb.y)
+				: Math.atan2(va.x-vb.x, vb.y-va.y);
+		}
+	}
+	
+	public inline function getStartpoint() {
+		return this.edge.lPoint == this.point ? this.edge.va : this.edge.vb;
+    }
+
+	public inline function getEndpoint() {
+		return this.edge.lPoint == this.point ? this.edge.vb : this.edge.va;
+    }
+	
+	public inline function getTarget() {
+		return this.edge.lCell != null && this.edge.lCell.point != point ? this.edge.lCell : this.edge.rCell;
+	}
+
+}
+
+class Diagram {
+	public var cells : Array<Cell>;
+	public var points : Array<Point>;
+	public var edges : Array<Edge>;
+	public var execTime : Float;
+	public function new() {
+	}
+}
+
+private class Beachsection extends RBNode<Beachsection> {
+	public var point : Point;
+	public var edge : Edge;
+	public var circleEvent : CircleEvent;
+	public function new() {
+	}
+}
+
+private class CircleEvent extends RBNode<CircleEvent> {
+	public var point : Point;
+	public var arc : Beachsection;
+	public var x : Float;
+	public var y : Float;
+	public var ycenter : Float;
+	public function new() {
+	}
+}
+
+class Voronoi {
+	
+	var epsilon : Float;
+	var beachline : RBTree<Beachsection>;
+	var vertices : Array<Point>;
+	var edges : Array<Edge>;
+	var cells : Array<Cell>;
+	var beachsectionJunkyard : Array<Beachsection>;
+	var circleEventJunkyard : Array<CircleEvent>;
+	var circleEvents : RBTree<CircleEvent>;
+	var firstCircleEvent : CircleEvent;
+	var pointCell : Map<Point,Cell>;
+
+	public function new( epsilon = 1e-9 ) {
+		this.epsilon = epsilon;
+		this.vertices = null;
+		this.edges = null;
+		this.cells = null;
+		this.beachsectionJunkyard = [];
+		this.circleEventJunkyard = [];
+    }
+
+	public function reset() {
+		if( this.beachline == null )
+			this.beachline = new RBTree<Beachsection>();
+		// Move leftover beachsections to the beachsection junkyard.
+		if (this.beachline.root != null) {
+			var beachsection = this.beachline.getFirst(this.beachline.root);
+			while (beachsection != null) {
+				this.beachsectionJunkyard.push(beachsection); // mark for reuse
+				beachsection = beachsection.rbNext;
+				}
+			}
+		this.beachline.root = null;
+		if (this.circleEvents == null) {
+			this.circleEvents = new RBTree<CircleEvent>();
+			}
+		pointCell = new Map();
+		this.circleEvents.root = this.firstCircleEvent = null;
+		this.vertices = [];
+		this.edges = [];
+		this.cells = [];
+	}
+
+	static inline function abs(x:Float) return x < 0 ? -x : x;
+	inline function equalWithepsilon(a:Float,b:Float) return abs(a-b)<epsilon;
+	inline function greaterThanWithepsilon(a:Float,b:Float) return a-b>epsilon;
+	inline function greaterThanOrEqualWithepsilon(a:Float,b:Float) return b-a<epsilon;
+	inline function lessThanWithepsilon(a:Float,b:Float) return b-a>epsilon;
+	inline function lessThanOrEqualWithepsilon(a:Float,b:Float) return a-b<epsilon;
+
+	function createVertex(x, y) {
+		var v = new Point(x, y);
+		this.vertices.push(v);
+		return v;
+    }
+
+	// this create and add an edge to internal collection, and also create
+	// two halfedges which are added to each point's counterclockwise array
+	// of halfedges.
+
+	function createEdge(lPoint, rPoint, va = null, vb = null) {
+		var edge = new Edge(lPoint, rPoint);
+		this.edges.push(edge);
+		if (va != null) {
+			this.setEdgeStartpoint(edge, lPoint, rPoint, va);
+			}
+		if (vb != null) {
+			this.setEdgeEndpoint(edge, lPoint, rPoint, vb);
+			}
+		pointCell.get(lPoint).halfedges.push(new Halfedge(edge, lPoint, rPoint));
+		pointCell.get(rPoint).halfedges.push(new Halfedge(edge, rPoint, lPoint));
+		return edge;
+	}
+
+	function createBorderEdge(lPoint, va, vb) {
+		var edge = new Edge(lPoint, null);
+		edge.va = va;
+		edge.vb = vb;
+		this.edges.push(edge);
+		return edge;
+    }
+
+	function setEdgeStartpoint(edge:Edge, lPoint, rPoint, vertex) {
+		if (edge.va == null && edge.vb == null) {
+			edge.va = vertex;
+			edge.lPoint = lPoint;
+			edge.rPoint = rPoint;
+			}
+		else if (edge.lPoint == rPoint) {
+			edge.vb = vertex;
+			}
+		else {
+			edge.va = vertex;
+			}
+    }
+
+	function setEdgeEndpoint(edge, lPoint, rPoint, vertex) {
+		this.setEdgeStartpoint(edge, rPoint, lPoint, vertex);
+    }
+	
+
+	// rhill 2011-06-02: A lot of Beachsection instanciations
+	// occur during the computation of the Voronoi diagram,
+	// somewhere between the number of points and twice the
+	// number of points, while the number of Beachsections on the
+	// beachline at any given time is comparatively low. For this
+	// reason, we reuse already created Beachsections, in order
+	// to avoid new memory allocation. This resulted in a measurable
+	// performance gain.
+
+	function createBeachsection(point:Point) {
+		var beachsection = this.beachsectionJunkyard.pop();
+		if ( beachsection == null )
+			beachsection = new Beachsection();
+		beachsection.point = point;
+		return beachsection;
+	}
+
+	// calculate the left break point of a particular beach section,
+	// given a particular sweep line
+	function leftBreakPoint(arc:Beachsection, directrix:Float) {
+		// http://en.wikipedia.org/wiki/Parabola
+		// http://en.wikipedia.org/wiki/Quadratic_equation
+		// h1 = x1,
+		// k1 = (y1+directrix)/2,
+		// h2 = x2,
+		// k2 = (y2+directrix)/2,
+		// p1 = k1-directrix,
+		// a1 = 1/(4*p1),
+		// b1 = -h1/(2*p1),
+		// c1 = h1*h1/(4*p1)+k1,
+		// p2 = k2-directrix,
+		// a2 = 1/(4*p2),
+		// b2 = -h2/(2*p2),
+		// c2 = h2*h2/(4*p2)+k2,
+		// x = (-(b2-b1) + Math.sqrt((b2-b1)*(b2-b1) - 4*(a2-a1)*(c2-c1))) / (2*(a2-a1))
+		// When x1 become the x-origin:
+		// h1 = 0,
+		// k1 = (y1+directrix)/2,
+		// h2 = x2-x1,
+		// k2 = (y2+directrix)/2,
+		// p1 = k1-directrix,
+		// a1 = 1/(4*p1),
+		// b1 = 0,
+		// c1 = k1,
+		// p2 = k2-directrix,
+		// a2 = 1/(4*p2),
+		// b2 = -h2/(2*p2),
+		// c2 = h2*h2/(4*p2)+k2,
+		// x = (-b2 + Math.sqrt(b2*b2 - 4*(a2-a1)*(c2-k1))) / (2*(a2-a1)) + x1
+
+		// change code below at your own risk: care has been taken to
+		// reduce errors due to computers' finite arithmetic precision.
+		// Maybe can still be improved, will see if any more of this
+		// kind of errors pop up again.
+		var point = arc.point,
+			rfocx = point.x,
+			rfocy = point.y,
+			pby2 = rfocy-directrix;
+		// parabola in degenerate case where focus is on directrix
+		if (pby2 == 0) {
+			return rfocx;
+			}
+		var lArc = arc.rbPrevious;
+		if (lArc == null) {
+			return Math.NEGATIVE_INFINITY;
+			}
+		point = lArc.point;
+		var lfocx = point.x,
+			lfocy = point.y,
+			plby2 = lfocy-directrix;
+		// parabola in degenerate case where focus is on directrix
+		if (plby2 == 0) {
+			return lfocx;
+			}
+		var    hl = lfocx-rfocx,
+			aby2 = 1/pby2-1/plby2,
+			b = hl/plby2;
+		if (aby2 != 0) {
+			return (-b+Math.sqrt(b*b-2*aby2*(hl*hl/(-2*plby2)-lfocy+plby2/2+rfocy-pby2/2)))/aby2+rfocx;
+			}
+		// both parabolas have same distance to directrix, thus break point is midway
+		return (rfocx+lfocx)/2;
+    }
+
+	// calculate the right break point of a particular beach section,
+	// given a particular directrix
+	function rightBreakPoint(arc:Beachsection, directrix) {
+		var rArc = arc.rbNext;
+		if (rArc != null) {
+			return this.leftBreakPoint(rArc, directrix);
+			}
+		var point = arc.point;
+		return point.y == directrix ? point.x : Math.POSITIVE_INFINITY;
+    }
+
+	function detachBeachsection(beachsection) {
+		this.detachCircleEvent(beachsection); // detach potentially attached circle event
+		this.beachline.rbRemoveNode(beachsection); // remove from RB-tree
+		this.beachsectionJunkyard.push(beachsection); // mark for reuse
+    }
+
+	function removeBeachsection(beachsection:Beachsection) {
+		var circle = beachsection.circleEvent,
+			x = circle.x,
+			y = circle.ycenter,
+			vertex = this.createVertex(x, y),
+			previous = beachsection.rbPrevious,
+			next = beachsection.rbNext,
+			disappearingTransitions = [beachsection];
+
+		// remove collapsed beachsection from beachline
+		this.detachBeachsection(beachsection);
+
+		// there could be more than one empty arc at the deletion point, this
+		// happens when more than two edges are linked by the same vertex,
+		// so we will collect all those edges by looking up both sides of
+		// the deletion point.
+		// by the way, there is *always* a predecessor/successor to any collapsed
+		// beach section, it's just impossible to have a collapsing first/last
+		// beach sections on the beachline, since they obviously are unconstrained
+		// on their left/right side.
+
+		// look left
+		var lArc = previous;
+		while (lArc.circleEvent != null && abs(x-lArc.circleEvent.x)<epsilon && abs(y-lArc.circleEvent.ycenter)<epsilon) {
+			previous = lArc.rbPrevious;
+			disappearingTransitions.unshift(lArc);
+			this.detachBeachsection(lArc); // mark for reuse
+			lArc = previous;
+			}
+		// even though it is not disappearing, I will also add the beach section
+		// immediately to the left of the left-most collapsed beach section, for
+		// convenience, since we need to refer to it later as this beach section
+		// is the 'left' point of an edge for which a start point is set.
+		disappearingTransitions.unshift(lArc);
+		this.detachCircleEvent(lArc);
+
+		// look right
+		var rArc = next;
+		while (rArc.circleEvent != null && abs(x-rArc.circleEvent.x)<epsilon && abs(y-rArc.circleEvent.ycenter)<epsilon) {
+			next = rArc.rbNext;
+			disappearingTransitions.push(rArc);
+			this.detachBeachsection(rArc); // mark for reuse
+			rArc = next;
+			}
+		// we also have to add the beach section immediately to the right of the
+		// right-most collapsed beach section, since there is also a disappearing
+		// transition representing an edge's start point on its left.
+		disappearingTransitions.push(rArc);
+		this.detachCircleEvent(rArc);
+
+		// walk through all the disappearing transitions between beach sections and
+		// set the start point of their (implied) edge.
+		var nArcs = disappearingTransitions.length,
+			iArc;
+		for( iArc in 1...nArcs ) {
+			rArc = disappearingTransitions[iArc];
+			lArc = disappearingTransitions[iArc-1];
+			this.setEdgeStartpoint(rArc.edge, lArc.point, rArc.point, vertex);
+		}
+
+		// create a new edge as we have now a new transition between
+		// two beach sections which were previously not adjacent.
+		// since this edge appears as a new vertex is defined, the vertex
+		// actually define an end point of the edge (relative to the point
+		// on the left)
+		lArc = disappearingTransitions[0];
+		rArc = disappearingTransitions[nArcs-1];
+		rArc.edge = this.createEdge(lArc.point, rArc.point, null, vertex);
+
+		// create circle events if any for beach sections left in the beachline
+		// adjacent to collapsed sections
+		this.attachCircleEvent(lArc);
+		this.attachCircleEvent(rArc);
+    }
+
+	function addBeachsection(point:Point) {
+		var x = point.x,
+			directrix = point.y;
+
+		// find the left and right beach sections which will surround the newly
+		// created beach section.
+		// rhill 2011-06-01: This loop is one of the most often executed,
+		// hence we expand in-place the comparison-against-epsilon calls.
+		var lArc = null, rArc = null,
+			dxl, dxr,
+			node = this.beachline.root;
+
+		while (node != null) {
+			dxl = this.leftBreakPoint(node,directrix)-x;
+			// x lessThanWithepsilon xl => falls somewhere before the left edge of the beachsection
+			if (dxl > epsilon) {
+				// this case should never happen
+				// if (!node.rbLeft) {
+				//    rArc = node.rbLeft;
+				//    break;
+				//    }
+				node = node.rbLeft;
+				}
+			else {
+				dxr = x-this.rightBreakPoint(node,directrix);
+				// x greaterThanWithepsilon xr => falls somewhere after the right edge of the beachsection
+				if (dxr > epsilon) {
+					if (node.rbRight == null) {
+						lArc = node;
+						break;
+						}
+					node = node.rbRight;
+					}
+				else {
+					// x equalWithepsilon xl => falls exactly on the left edge of the beachsection
+					if (dxl > -epsilon) {
+						lArc = node.rbPrevious;
+						rArc = node;
+						}
+					// x equalWithepsilon xr => falls exactly on the right edge of the beachsection
+					else if (dxr > -epsilon) {
+						lArc = node;
+						rArc = node.rbNext;
+						}
+					// falls exactly somewhere in the middle of the beachsection
+					else {
+						lArc = rArc = node;
+						}
+					break;
+					}
+				}
+			}
+		// at this point, keep in mind that lArc and/or rArc could be
+		// undefined or null.
+
+		// create a new beach section object for the point and add it to RB-tree
+		var newArc = this.createBeachsection(point);
+		this.beachline.rbInsertSuccessor(lArc, newArc);
+
+		// cases:
+		//
+
+		// [null,null]
+		// least likely case: new beach section is the first beach section on the
+		// beachline.
+		// This case means:
+		//   no new transition appears
+		//   no collapsing beach section
+		//   new beachsection become root of the RB-tree
+		if (lArc == null && rArc == null) {
+			return;
+			}
+
+		// [lArc,rArc] where lArc == rArc
+		// most likely case: new beach section split an existing beach
+		// section.
+		// This case means:
+		//   one new transition appears
+		//   the left and right beach section might be collapsing as a result
+		//   two new nodes added to the RB-tree
+		if (lArc == rArc) {
+			// invalidate circle event of split beach section
+			this.detachCircleEvent(lArc);
+
+			// split the beach section into two separate beach sections
+			rArc = this.createBeachsection(lArc.point);
+			this.beachline.rbInsertSuccessor(newArc, rArc);
+
+			// since we have a new transition between two beach sections,
+			// a new edge is born
+			newArc.edge = rArc.edge = this.createEdge(lArc.point, newArc.point);
+
+			// check whether the left and right beach sections are collapsing
+			// and if so create circle events, to be notified when the point of
+			// collapse is reached.
+			this.attachCircleEvent(lArc);
+			this.attachCircleEvent(rArc);
+			return;
+			}
+
+		// [lArc,null]
+		// even less likely case: new beach section is the *last* beach section
+		// on the beachline -- this can happen *only* if *all* the previous beach
+		// sections currently on the beachline share the same y value as
+		// the new beach section.
+		// This case means:
+		//   one new transition appears
+		//   no collapsing beach section as a result
+		//   new beach section become right-most node of the RB-tree
+		if (lArc != null && rArc == null) {
+			newArc.edge = this.createEdge(lArc.point,newArc.point);
+			return;
+			}
+
+		// [null,rArc]
+		// impossible case: because points are strictly processed from top to bottom,
+		// and left to right, which guarantees that there will always be a beach section
+		// on the left -- except of course when there are no beach section at all on
+		// the beach line, which case was handled above.
+		// rhill 2011-06-02: No point testing in non-debug version
+		//if (!lArc && rArc) {
+		//    throw "Voronoi.addBeachsection(): What is this I don't even";
+		//    }
+
+		// [lArc,rArc] where lArc != rArc
+		// somewhat less likely case: new beach section falls *exactly* in between two
+		// existing beach sections
+		// This case means:
+		//   one transition disappears
+		//   two new transitions appear
+		//   the left and right beach section might be collapsing as a result
+		//   only one new node added to the RB-tree
+		if (lArc != rArc) {
+			// invalidate circle events of left and right points
+			this.detachCircleEvent(lArc);
+			this.detachCircleEvent(rArc);
+
+			// an existing transition disappears, meaning a vertex is defined at
+			// the disappearance point.
+			// since the disappearance is caused by the new beachsection, the
+			// vertex is at the center of the circumscribed circle of the left,
+			// new and right beachsections.
+			// http://mathforum.org/library/drmath/view/55002.html
+			// Except that I bring the origin at A to simplify
+			// calculation
+			var lPoint = lArc.point,
+				ax = lPoint.x,
+				ay = lPoint.y,
+				bx=point.x-ax,
+				by=point.y-ay,
+				rPoint = rArc.point,
+				cx=rPoint.x-ax,
+				cy=rPoint.y-ay,
+				d=2*(bx*cy-by*cx),
+				hb=bx*bx+by*by,
+				hc=cx*cx+cy*cy,
+				vertex = this.createVertex((cy*hb-by*hc)/d+ax, (bx*hc-cx*hb)/d+ay);
+
+			// one transition disappear
+			this.setEdgeStartpoint(rArc.edge, lPoint, rPoint, vertex);
+
+			// two new transitions appear at the new vertex location
+			newArc.edge = this.createEdge(lPoint, point, null, vertex);
+			rArc.edge = this.createEdge(point, rPoint, null, vertex);
+
+			// check whether the left and right beach sections are collapsing
+			// and if so create circle events, to handle the point of collapse.
+			this.attachCircleEvent(lArc);
+			this.attachCircleEvent(rArc);
+			return;
+		}
+    }
+
+	function attachCircleEvent(arc:Beachsection) {
+		var lArc = arc.rbPrevious,
+			rArc = arc.rbNext;
+		if (lArc == null || rArc == null) {return;} // does that ever happen?
+		var lPoint = lArc.point,
+			cPoint = arc.point,
+			rPoint = rArc.point;
+
+		// If point of left beachsection is same as point of
+		// right beachsection, there can't be convergence
+		if (lPoint==rPoint) {return;}
+
+		// Find the circumscribed circle for the three points associated
+		// with the beachsection triplet.
+		// rhill 2011-05-26: It is more efficient to calculate in-place
+		// rather than getting the resulting circumscribed circle from an
+		// object returned by calling Voronoi.circumcircle()
+		// http://mathforum.org/library/drmath/view/55002.html
+		// Except that I bring the origin at cPoint to simplify calculations.
+		// The bottom-most part of the circumcircle is our Fortune 'circle
+		// event', and its center is a vertex potentially part of the final
+		// Voronoi diagram.
+		var bx = cPoint.x,
+			by = cPoint.y,
+			ax = lPoint.x-bx,
+			ay = lPoint.y-by,
+			cx = rPoint.x-bx,
+			cy = rPoint.y-by;
+
+		// If points l->c->r are clockwise, then center beach section does not
+		// collapse, hence it can't end up as a vertex (we reuse 'd' here, which
+		// sign is reverse of the orientation, hence we reverse the test.
+		// http://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon
+		// rhill 2011-05-21: Nasty finite precision error which caused circumcircle() to
+		// return infinites: 1e-12 seems to fix the problem.
+		var d = 2*(ax*cy-ay*cx);
+		if (d >= -2e-12){return;}
+
+		var    ha = ax*ax+ay*ay,
+			hc = cx*cx+cy*cy,
+			x = (cy*ha-ay*hc)/d,
+			y = (ax*hc-cx*ha)/d,
+			ycenter = y+by;
+
+		// Important: ybottom should always be under or at sweep, so no need
+		// to waste CPU cycles by checking
+
+		// recycle circle event object if possible
+		var circleEvent = this.circleEventJunkyard.pop();
+		if (circleEvent == null) {
+			circleEvent = new CircleEvent();
+			}
+		circleEvent.arc = arc;
+		circleEvent.point = cPoint;
+		circleEvent.x = x+bx;
+		circleEvent.y = ycenter+Math.sqrt(x*x+y*y); // y bottom
+		circleEvent.ycenter = ycenter;
+		arc.circleEvent = circleEvent;
+
+		// find insertion point in RB-tree: circle events are ordered from
+		// smallest to largest
+		var predecessor = null,
+			node = this.circleEvents.root;
+		while (node != null) {
+			if (circleEvent.y < node.y || (circleEvent.y == node.y && circleEvent.x <= node.x)) {
+				if (node.rbLeft != null) {
+					node = node.rbLeft;
+					}
+				else {
+					predecessor = node.rbPrevious;
+					break;
+					}
+				}
+			else {
+				if (node.rbRight != null) {
+					node = node.rbRight;
+					}
+				else {
+					predecessor = node;
+					break;
+					}
+				}
+			}
+		this.circleEvents.rbInsertSuccessor(predecessor, circleEvent);
+		if (predecessor == null) {
+			this.firstCircleEvent = circleEvent;
+			}
+    }
+
+	function detachCircleEvent(arc:Beachsection) {
+		var circle = arc.circleEvent;
+		if (circle != null) {
+			if (circle.rbPrevious == null) {
+				this.firstCircleEvent = circle.rbNext;
+				}
+			this.circleEvents.rbRemoveNode(circle); // remove from RB-tree
+			this.circleEventJunkyard.push(circle);
+			arc.circleEvent = null;
+		}
+    }
+
+	// ---------------------------------------------------------------------------
+	// Diagram completion methods
+
+	// connect dangling edges (not if a cursory test tells us
+	// it is not going to be visible.
+	// return value:
+	//   false: the dangling endpoint couldn't be connected
+	//   true: the dangling endpoint could be connected
+	function connectEdge(edge:Edge, bbox:Bounds) {
+		// skip if end point already connected
+		var vb = edge.vb;
+		if (vb != null) {return true;}
+
+		// make local copy for performance purpose
+		var va = edge.va,
+			xl = bbox.xMin,
+			xr = bbox.xMax,
+			yt = bbox.yMin,
+			yb = bbox.yMax,
+			lPoint = edge.lPoint,
+			rPoint = edge.rPoint,
+			lx = lPoint.x,
+			ly = lPoint.y,
+			rx = rPoint.x,
+			ry = rPoint.y,
+			fx = (lx+rx)/2,
+			fy = (ly+ry)/2,
+			fm = 0., fb = 0.;
+
+		// get the line equation of the bisector if line is not vertical
+		if (ry != ly) {
+			fm = (lx-rx)/(ry-ly);
+			fb = fy-fm*fx;
+			}
+
+		// remember, direction of line (relative to left point):
+		// upward: left.x < right.x
+		// downward: left.x > right.x
+		// horizontal: left.x == right.x
+		// upward: left.x < right.x
+		// rightward: left.y < right.y
+		// leftward: left.y > right.y
+		// vertical: left.y == right.y
+
+		// depending on the direction, find the best side of the
+		// bounding box to use to determine a reasonable start point
+
+		// special case: vertical line
+		if (ry == ly) {
+			// doesn't intersect with viewport
+			if (fx < xl || fx >= xr) {return false;}
+			// downward
+			if (lx > rx) {
+				if (va == null) {
+					va = this.createVertex(fx, yt);
+					}
+				else if (va.y >= yb) {
+					return false;
+					}
+				vb = this.createVertex(fx, yb);
+				}
+			// upward
+			else {
+				if (va == null) {
+					va = this.createVertex(fx, yb);
+					}
+				else if (va.y < yt) {
+					return false;
+					}
+				vb = this.createVertex(fx, yt);
+				}
+			}
+		// closer to vertical than horizontal, connect start point to the
+		// top or bottom side of the bounding box
+		else if (fm < -1 || fm > 1) {
+			// downward
+			if (lx > rx) {
+				if (va == null) {
+					va = this.createVertex((yt-fb)/fm, yt);
+					}
+				else if (va.y >= yb) {
+					return false;
+					}
+				vb = this.createVertex((yb-fb)/fm, yb);
+				}
+			// upward
+			else {
+				if (va == null) {
+					va = this.createVertex((yb-fb)/fm, yb);
+					}
+				else if (va.y < yt) {
+					return false;
+					}
+				vb = this.createVertex((yt-fb)/fm, yt);
+				}
+			}
+		// closer to horizontal than vertical, connect start point to the
+		// left or right side of the bounding box
+		else {
+			// rightward
+			if (ly < ry) {
+				if (va == null) {
+					va = this.createVertex(xl, fm*xl+fb);
+					}
+				else if (va.x >= xr) {
+					return false;
+					}
+				vb = this.createVertex(xr, fm*xr+fb);
+				}
+			// leftward
+			else {
+				if (va == null) {
+					va = this.createVertex(xr, fm*xr+fb);
+					}
+				else if (va.x < xl) {
+					return false;
+					}
+				vb = this.createVertex(xl, fm*xl+fb);
+				}
+			}
+		edge.va = va;
+		edge.vb = vb;
+		return true;
+	}
+
+	// line-clipping code taken from:
+	//   Liang-Barsky function by Daniel White
+	//   http://www.skytopia.com/project/articles/compsci/clipping.html
+	// Thanks!
+	// A bit modified to minimize code paths
+	function clipEdge(edge:Edge, bbox:Bounds) {
+		var ax = edge.va.x,
+			ay = edge.va.y,
+			bx = edge.vb.x,
+			by = edge.vb.y,
+			t0 = 0.,
+			t1 = 1.,
+			dx = bx-ax,
+			dy = by-ay;
+		// left
+		var q = ax-bbox.xMin;
+		if (dx==0 && q<0) {return false;}
+		var r = -q/dx;
+		if (dx<0) {
+			if (r<t0) {return false;}
+			else if (r<t1) {t1=r;}
+			}
+		else if (dx>0) {
+			if (r>t1) {return false;}
+			else if (r>t0) {t0=r;}
+			}
+		// right
+		q = bbox.xMax-ax;
+		if (dx==0 && q<0) {return false;}
+		r = q/dx;
+		if (dx<0) {
+			if (r>t1) {return false;}
+			else if (r>t0) {t0=r;}
+			}
+		else if (dx>0) {
+			if (r<t0) {return false;}
+			else if (r<t1) {t1=r;}
+			}
+		// top
+		q = ay-bbox.yMin;
+		if (dy==0 && q<0) {return false;}
+		r = -q/dy;
+		if (dy<0) {
+			if (r<t0) {return false;}
+			else if (r<t1) {t1=r;}
+			}
+		else if (dy>0) {
+			if (r>t1) {return false;}
+			else if (r>t0) {t0=r;}
+			}
+		// bottom
+		q = bbox.yMax-ay;
+		if (dy==0 && q<0) {return false;}
+		r = q/dy;
+		if (dy<0) {
+			if (r>t1) {return false;}
+			else if (r>t0) {t0=r;}
+			}
+		else if (dy>0) {
+			if (r<t0) {return false;}
+			else if (r<t1) {t1=r;}
+			}
+
+		// if we reach this point, Voronoi edge is within bbox
+
+		// if t0 > 0, va needs to change
+		// rhill 2011-06-03: we need to create a new vertex rather
+		// than modifying the existing one, since the existing
+		// one is likely shared with at least another edge
+		if (t0 > 0) {
+			edge.va = this.createVertex(ax+t0*dx, ay+t0*dy);
+			}
+
+		// if t1 < 1, vb needs to change
+		// rhill 2011-06-03: we need to create a new vertex rather
+		// than modifying the existing one, since the existing
+		// one is likely shared with at least another edge
+		if (t1 < 1) {
+			edge.vb = this.createVertex(ax+t1*dx, ay+t1*dy);
+			}
+
+		return true;
+	}
+
+	// Connect/cut edges at bounding box
+	function clipEdges(bbox:Bounds) {
+		// connect all dangling edges to bounding box
+		// or get rid of them if it can't be done
+		var edges = this.edges,
+			iEdge = edges.length,
+			edge;
+
+		// iterate backward so we can splice safely
+		while (iEdge-- != 0) {
+			edge = edges[iEdge];
+			// edge is removed if:
+			//   it is wholly outside the bounding box
+			//   it is actually a point rather than a line
+			if (!this.connectEdge(edge, bbox) || !this.clipEdge(edge, bbox) || (abs(edge.va.x-edge.vb.x)<epsilon && abs(edge.va.y-edge.vb.y)<epsilon)) {
+				edge.va = edge.vb = null;
+				edges.splice(iEdge,1);
+			}
+		}
+	}
+
+	// Close the cells.
+	// The cells are bound by the supplied bounding box.
+	// Each cell refers to its associated point, and a list
+	// of halfedges ordered counterclockwise.
+	function closeCells(bbox:Bounds) {
+		// prune, order halfedges, then add missing ones
+		// required to close cells
+		var xl = bbox.xMin,
+			xr = bbox.xMax,
+			yt = bbox.yMin,
+			yb = bbox.yMax,
+			cells = this.cells,
+			iCell = cells.length,
+			cell,
+			iLeft, iRight,
+			halfedges, nHalfedges,
+			edge,
+			startpoint, endpoint,
+			va, vb = null;
+
+		while (iCell-- != 0) {
+			cell = cells[iCell];
+			// trim non fully-defined halfedges and sort them counterclockwise
+			if (cell.prepare() == 0)
+				continue;
+			// close open cells
+			// step 1: find first 'unclosed' point, if any.
+			// an 'unclosed' point will be the end point of a halfedge which
+			// does not match the start point of the following halfedge
+			halfedges = cell.halfedges;
+			nHalfedges = halfedges.length;
+			// special case: only one point, in which case, the viewport is the cell
+			// ...
+			// all other cases
+			iLeft = 0;
+			while (iLeft < nHalfedges) {
+				iRight = (iLeft+1) % nHalfedges;
+				endpoint = halfedges[iLeft].getEndpoint();
+				startpoint = halfedges[iRight].getStartpoint();
+				// if end point is not equal to start point, we need to add the missing
+				// halfedge(s) to close the cell
+				if (abs(endpoint.x-startpoint.x)>=epsilon || abs(endpoint.y-startpoint.y)>=epsilon) {
+					// if we reach this point, cell needs to be closed by walking
+					// counterclockwise along the bounding box until it connects
+					// to next halfedge in the list
+					va = endpoint;
+					// walk downward along left side
+					if (this.equalWithepsilon(endpoint.x,xl) && this.lessThanWithepsilon(endpoint.y,yb)) {
+						vb = this.createVertex(xl, this.equalWithepsilon(startpoint.x,xl) ? startpoint.y : yb);
+					}
+					// walk rightward along bottom side
+					else if (this.equalWithepsilon(endpoint.y,yb) && this.lessThanWithepsilon(endpoint.x,xr)) {
+						vb = this.createVertex(this.equalWithepsilon(startpoint.y,yb) ? startpoint.x : xr, yb);
+					}
+					// walk upward along right side
+					else if (this.equalWithepsilon(endpoint.x,xr) && this.greaterThanWithepsilon(endpoint.y,yt)) {
+						vb = this.createVertex(xr, this.equalWithepsilon(startpoint.x,xr) ? startpoint.y : yt);
+					}
+					// walk leftward along top side
+					else if (this.equalWithepsilon(endpoint.y,yt) && this.greaterThanWithepsilon(endpoint.x,xl)) {
+						vb = this.createVertex(this.equalWithepsilon(startpoint.y,yt) ? startpoint.x : xl, yt);
+					}
+					edge = this.createBorderEdge(cell.point, va, vb);
+					halfedges.insert(iLeft+1, new Halfedge(edge, cell.point, null));
+					nHalfedges = halfedges.length;
+				}
+				iLeft++;
+			}
+		}
+	}
+
+	// ---------------------------------------------------------------------------
+	// Top-level Fortune loop
+
+	static function sortByXY(a:Point, b:Point) {
+		var r = b.y - a.y;
+		return r < 0 ? -1 : (r > 0 ? 1 : (b.x > a.x ? 1 : b.x < a.x ? -1 : 0));
+	}
+	
+	// rhill 2011-05-19:
+	//   Voronoi points are kept client-side now, to allow
+	//   user to freely modify content. At compute time,
+	//   *references* to points are copied locally.
+	public function compute(points:Array<Point>, bbox:Bounds) {
+		// to measure execution time
+		var startTime = haxe.Timer.stamp();
+
+		// init internal state
+		this.reset();
+
+		// Initialize point event queue
+		var pointEvents = points.slice(0);
+		pointEvents.sort(sortByXY);
+
+		// process queue
+		var point = pointEvents.pop(),
+			pointid = 0,
+			xpointx = Math.NEGATIVE_INFINITY, // to avoid duplicate points
+			xpointy = Math.NEGATIVE_INFINITY,
+			cells = this.cells,
+			circle;
+
+		// main loop
+		while( true ) {
+			// we need to figure whether we handle a point or circle event
+			// for this we find out if there is a point event and it is
+			// 'earlier' than the circle event
+			circle = this.firstCircleEvent;
+
+			// add beach section
+			if (point != null && (circle == null || point.y < circle.y || (point.y == circle.y && point.x < circle.x))) {
+				// only if point is not a duplicate
+				if (point.x != xpointx || point.y != xpointy) {
+					// first create cell for new point
+					var c = new Cell(pointid, point);
+					cells[pointid] = c;
+					pointCell.set(point, c);
+					pointid++;
+					// then create a beachsection for that point
+					this.addBeachsection(point);
+					// remember last point coords to detect duplicate
+					xpointy = point.y;
+					xpointx = point.x;
+					}
+				point = pointEvents.pop();
+				}
+
+			// remove beach section
+			else if (circle != null) {
+				this.removeBeachsection(circle.arc);
+				}
+
+			// all done, quit
+			else
+				break;
+				
+		}
+
+		// wrapping-up:
+		//   connect dangling edges to bounding box
+		//   cut edges as per bounding box
+		//   discard edges completely outside bounding box
+		//   discard edges which are point-like
+		this.clipEdges(bbox);
+
+		//   add missing edges in order to close opened cells
+		this.closeCells(bbox);
+		
+		var eid = 0;
+		for( e in edges ) {
+			e.id = eid++;
+			e.lCell = pointCell.get(e.lPoint);
+			e.rCell = pointCell.get(e.rPoint);
+		}
+
+		// to measure execution time
+		var stopTime = haxe.Timer.stamp();
+
+		// prepare return values
+		var diagram = new Diagram();
+		diagram.cells = this.cells;
+		diagram.edges = this.edges;
+		diagram.points = this.vertices;
+		diagram.execTime = stopTime - startTime;
+
+		// clean up
+		this.reset();
+
+		return diagram;
+	}
+
+}

+ 216 - 0
h2d/comp/Box.hx

@@ -0,0 +1,216 @@
+package h2d.comp;
+
+class Box extends Component {
+	
+	var input : h2d.Interactive;
+	
+	public function new(?layout,?parent) {
+		super("box", parent);
+		if( layout == null ) layout = h2d.css.Defs.Layout.Inline;
+		addClass(":"+layout.getName().toLowerCase());
+	}
+	
+	override function resizeRec( ctx : Context ) {
+		var extX = extLeft();
+		var extY = extTop();
+		var ctx2 = new Context(0, 0);
+		ctx2.measure = ctx.measure;
+		if( ctx.measure ) {
+			width = ctx.maxWidth;
+			height = ctx.maxHeight;
+			contentWidth = width - (extX + extRight());
+			contentHeight = height - (extY + extBottom());
+			if( style.width != null ) contentWidth = style.width;
+			if( style.height != null ) contentHeight = style.height;
+		} else {
+			ctx2.xPos = ctx.xPos;
+			ctx2.yPos = ctx.yPos;
+			if( ctx2.xPos == null ) ctx2.xPos = 0;
+			if( ctx2.yPos == null ) ctx2.yPos = 0;
+			resize(ctx2);
+		}
+		switch( style.layout ) {
+		case Inline:
+			var lineHeight = 0.;
+			var xPos = 0., yPos = 0., maxPos = 0.;
+			var prev = null;
+			for( c in components ) {
+				if( !c.visible )
+					continue;
+				if( ctx.measure ) {
+					ctx2.maxWidth = contentWidth;
+					ctx2.maxHeight = contentHeight - (yPos + lineHeight + style.verticalSpacing);
+					c.resizeRec(ctx2);
+					var next = xPos + c.width;
+					if( prev != null ) next += style.horizontalSpacing;
+					if( xPos > 0 && next > contentWidth ) {
+						yPos += lineHeight + style.verticalSpacing;
+						xPos = c.width;
+						lineHeight = c.height;
+					} else {
+						xPos = next;
+						if( c.height > lineHeight ) lineHeight = c.height;
+					}
+					if( xPos > maxPos ) maxPos = xPos;
+				} else {
+					var next = xPos + c.width;
+					if( xPos > 0 && next > contentWidth ) {
+						yPos += lineHeight + style.verticalSpacing;
+						xPos = 0;
+						lineHeight = c.height;
+					} else {
+						if( c.height > lineHeight ) lineHeight = c.height;
+					}
+					ctx2.xPos = xPos;
+					ctx2.yPos = yPos;
+					c.resizeRec(ctx2);
+					xPos += c.width + style.horizontalSpacing;
+				}
+				prev = c;
+			}
+			if( ctx.measure && style.dock == null ) {
+				if( maxPos < contentWidth && style.width == null ) contentWidth = maxPos;
+				if( yPos + lineHeight < contentHeight && style.height == null ) contentHeight = yPos + lineHeight;
+			}
+		case Horizontal:
+			var lineHeight = 0.;
+			var xPos = 0.;
+			var prev = null;
+			for( c in components ) {
+				if( !c.visible )
+					continue;
+				if( ctx.measure ) {
+					if( prev != null ) xPos += style.horizontalSpacing;
+					ctx2.maxWidth = contentWidth - xPos;
+					if( ctx2.maxWidth < 0 ) ctx2.maxWidth = 0;
+					ctx2.maxHeight = contentHeight;
+					c.resizeRec(ctx2);
+					xPos += c.width;
+					if( c.height > lineHeight ) lineHeight = c.height;
+				} else {
+					ctx2.xPos = xPos;
+					ctx2.yPos = 0;
+					c.resizeRec(ctx2);
+					xPos += c.width + style.horizontalSpacing;
+				}
+				prev = c;
+			}
+			if( ctx.measure && style.dock == null ) {
+				if( xPos < contentWidth && style.width == null ) contentWidth = xPos;
+				if( lineHeight < contentHeight && style.height == null ) contentHeight = lineHeight;
+			}
+		case Vertical:
+			var colWidth = 0.;
+			var yPos = 0.;
+			var prev = null;
+			for( c in components ) {
+				if( !c.visible )
+					continue;
+				if( ctx.measure ) {
+					if( prev != null ) yPos += style.verticalSpacing;
+					ctx2.maxWidth = contentWidth;
+					ctx2.maxHeight = contentHeight - yPos;
+					if( ctx2.maxHeight < 0 ) ctx2.maxHeight = 0;
+					c.resizeRec(ctx2);
+					yPos += c.height;
+					if( c.width > colWidth ) colWidth = c.width;
+				} else {
+					ctx2.xPos = 0;
+					ctx2.yPos = yPos;
+					c.resizeRec(ctx2);
+					yPos += c.height + style.verticalSpacing;
+				}
+				prev = c;
+			}
+			if( ctx.measure && style.dock == null ) {
+				if( colWidth < contentWidth && style.width == null ) contentWidth = colWidth;
+				if( yPos < contentHeight && style.height == null ) contentHeight = yPos;
+			}
+		case Absolute:
+			ctx2.xPos = null;
+			ctx2.yPos = null;
+			if( ctx.measure ) {
+				ctx2.maxWidth = contentWidth;
+				ctx2.maxHeight = contentHeight;
+			}
+			for( c in components )
+				c.resizeRec(ctx2);
+		case Dock:
+			ctx2.xPos = 0;
+			ctx2.yPos = 0;
+			var xPos = 0., yPos = 0., w = contentWidth, h = contentHeight;
+			if( ctx.measure ) {
+				for( c in components ) {
+					ctx2.maxWidth = w;
+					ctx2.maxHeight = h;
+					c.resizeRec(ctx2);
+					var d = c.style.dock;
+					if( d == null ) d = Full;
+					switch( d ) {
+					case Left, Right:
+						w -= c.width;
+					case Top, Bottom:
+						h -= c.height;
+					case Full:
+					}
+					if( w < 0 ) w = 0;
+					if( h < 0 ) h = 0;
+				}
+			} else {
+				for( c in components ) {
+					ctx2.maxWidth = w;
+					ctx2.maxHeight = h;
+					var d = c.style.dock;
+					if( d == null ) d = Full;
+					ctx2.xPos = xPos;
+					ctx2.yPos = yPos;
+					switch( d ) {
+					case Left, Top:
+					case Right:
+						ctx2.xPos += w - c.width;
+					case Bottom:
+						ctx2.yPos += h - c.height;
+					case Full:
+						ctx2.xPos += Std.int((w - c.width) * 0.5);
+						ctx2.yPos += Std.int((h - c.height) * 0.5);
+					}
+					c.resizeRec(ctx2);
+					switch( d ) {
+					case Left:
+						w -= c.width;
+						xPos += c.width;
+					case Right:
+						w -= c.width;
+					case Top:
+						h -= c.height;
+						yPos += c.height;
+					case Bottom:
+						h -= c.height;
+					case Full:
+					}
+					if( w < 0 ) w = 0;
+					if( h < 0 ) h = 0;
+				}
+			}
+		}
+		if( ctx.measure ) {
+			width = contentWidth + extX + extRight();
+			height = contentHeight + extY + extBottom();
+		} else {
+			if( style.backgroundColor != Transparent ) {
+				if( input == null ) {
+					input = new h2d.Interactive(0, 0);
+					input.cursor = Default;
+					bg.addChildAt(input, 0);
+				}
+				input.width = width - (style.marginLeft + style.marginRight);
+				input.height = height - (style.marginTop + style.marginBottom);
+			} else if( input != null ) {
+				input.remove();
+				input = null;
+			}
+		}
+	}
+	
+	
+}

+ 36 - 0
h2d/comp/Button.hx

@@ -0,0 +1,36 @@
+package h2d.comp;
+
+class Button extends Interactive {
+	
+	var tf : h2d.Text;
+	
+	public var text(default, set) : String;
+	
+	public function new(text, ?parent) {
+		super("button",parent);
+		tf = new h2d.Text(null, this);
+		this.text = text;
+	}
+
+	function get_text() {
+		return tf.text;
+	}
+	
+	function set_text(t) {
+		needRebuild = true;
+		return text = t;
+	}
+	
+	override function resize( ctx : Context ) {
+		if( ctx.measure ) {
+			tf.font = getFont();
+			tf.textColor = style.color;
+			tf.text = text;
+			tf.filter = true;
+			contentWidth = tf.textWidth;
+			contentHeight = tf.textHeight;
+		}
+		super.resize(ctx);
+	}
+	
+}

+ 37 - 0
h2d/comp/Checkbox.hx

@@ -0,0 +1,37 @@
+package h2d.comp;
+
+class Checkbox extends Interactive {
+	
+	public var checked(default, set) : Bool;
+	
+	public function new(?parent) {
+		super("checkbox", parent);
+		checked = false;
+	}
+	
+	function set_checked(b) {
+		toggleClass(":checked", b);
+		return checked = b;
+	}
+	
+	override function resize( ctx : Context ) {
+		super.resize(ctx);
+		if( !ctx.measure ) {
+			input.width = width - (style.marginLeft + style.marginRight);
+			input.height = height - (style.marginTop + style.marginBottom);
+			if( checked ) {
+				var m = style.borderSize + style.tickSpacing;
+				bg.fillRect(style.tickColor, m, m, input.width - m * 2, input.height - m * 2);
+			}
+		}
+	}
+	
+	override function onClick() {
+		checked = !checked;
+		onChange(checked);
+	}
+	
+	public dynamic function onChange( checked : Bool ) {
+	}
+	
+}

+ 63 - 0
h2d/comp/Color.hx

@@ -0,0 +1,63 @@
+package h2d.comp;
+
+class Color extends Component {
+	
+	var input : h2d.Interactive;
+	public var value(default, set) : Int;
+	
+	public function new(?parent) {
+		super("color", parent);
+		input = new h2d.Interactive(0, 0, bg);
+		var active = false;
+		input.onPush = function(_) {
+			active = true;
+		};
+		input.onOver = function(_) {
+			addClass(":hover");
+		};
+		input.onOut = function(_) {
+			active = false;
+			removeClass(":hover");
+		};
+		input.onRelease = function(_) {
+			if( active ) selectColor();
+		};
+		value = 0;
+	}
+	
+	function set_value(v) {
+		needRebuild = true;
+		return value = v;
+	}
+	
+	override function resize( ctx : Context ) {
+		super.resize(ctx);
+		if( !ctx.measure ) {
+			input.width = width - (style.marginLeft + style.marginRight);
+			input.height = height - (style.marginTop + style.marginBottom);
+			bg.fillRectColor(extLeft() - style.marginLeft, extTop() - style.marginTop, contentWidth, contentHeight, 0xFF000000 | value);
+		}
+	}
+	
+	function selectColor() {
+		var p : Component = this;
+		while( p.parentComponent != null )
+			p = p.parentComponent;
+		var b = new Box(p);
+		b.toggleClass("modal", true);
+		var pick = new ColorPicker(b);
+		pick.color = value;
+		pick.addStyleString("dock:full");
+		pick.onChange = function(c) {
+			value = c;
+			onChange(c);
+		};
+		pick.onClose = function() {
+			b.remove();
+		};
+	}
+	
+	public dynamic function onChange( color : Int ) {
+	}
+	
+}

+ 681 - 0
h2d/comp/ColorPicker.hx

@@ -0,0 +1,681 @@
+package h2d.comp;
+import h2d.css.Defs;
+import h2d.css.Fill;
+
+private enum RGBA {
+	R;
+	G;
+	B;
+	A;
+}
+
+private enum ChangeState {
+	SNone;
+	SColor;
+	SPalette;
+	SChart;
+	SRed;
+	SGreen;
+	SBlue;
+	SAlpha;
+}
+
+private enum CompStyle {
+	GaugeLabel;
+	GaugeInput;
+	ColorLabel;
+	ColorInput;
+}
+
+private class Style {
+	public function new () {
+	}
+	
+	public static function get(kind:CompStyle) {
+		var style = new h2d.css.Style();
+		switch(kind) {
+			case GaugeLabel: 	style.fontSize = 11;
+			case GaugeInput: 	style.width = 24;
+								style.height = 10;
+								style.fontSize = 11;
+			case ColorLabel: 	style.fontSize = 14;
+			case ColorInput: 	style.width = 50;
+								style.height = 10;
+								style.fontSize = 11;
+		}
+		return style;
+	}
+}
+
+private class Arrow extends h2d.css.Fill {
+	public function new (parent, x:Float, y:Float, ang = 0., color = 0xff000000) {
+		super(parent);
+		addPoint(-5, -4, color);
+		addPoint(-5, 4, color);
+		addPoint(0, 0, color);
+		addPoint(0, 0, color);
+		rotation = ang;
+		this.x = x;
+		this.y = y;
+	}
+}
+
+private class Cross extends h2d.css.Fill {
+	var size:Float;
+	
+	public function new (parent, size:Float, color = 0xff000000) {
+		super(parent);
+		this.size = size;
+		lineRect(FillStyle.Color(color), 0, 0, size, size, 1);
+	}
+	
+	public function setColor(color:Int) {
+		reset();
+		lineRect(FillStyle.Color(color), 0, 0, size, size, 1);
+	}
+}
+
+private class Color extends h2d.Sprite {
+	var picker : ColorPicker;
+	public var width :Float;
+	public var height :Float;
+	public var color(default, set):Int = 0xFFFFFFFF;
+	public var preview(default, set):Int = 0xFFFFFFFF;
+	public var alpha(default, set):Float = 1.;
+	
+	var canvas:h2d.css.Fill;
+	var label : h2d.comp.Label;
+	var input : h2d.comp.Input;
+	
+	
+	public function new (picker,ix, iy, iw, ih, parent) {
+		super(parent);
+		this.picker = picker;
+		x = ix;
+		y = iy;
+		width = iw;
+		height = ih;
+		init();
+	}
+	
+	function set_color(v:Int) {
+		if(v != color) {
+			color = v;
+			drawAll();
+		}
+		return color;
+	}
+	
+	function set_preview(v:Int) {
+		if(v != preview) {
+			preview = v;
+			drawAll();
+		}
+		return color;
+	}
+	
+	function set_alpha(v:Float) {
+		alpha = v;
+		drawAll();
+		return color;
+	}
+	
+	public function updateColor(v:Int) {
+		color = v;
+		input.value = StringTools.hex(preview, 6).substr(2);
+	}
+	
+	function init() {
+		label = new h2d.comp.Label("#", this);
+		label.setStyle(Style.get(ColorLabel));
+		input = new h2d.comp.Input(this);
+		input.setStyle(Style.get(ColorInput));
+		input.value = "FFFFFF";
+		input.x = 15; input.y = 3;
+		input.onChange = function (e) {
+			input.value = input.value.toUpperCase();
+			if(input.value.length > 6) {
+				input.value = input.value.substr(0, 6);
+				return;
+			}
+			var v = Std.parseInt("0x" + input.value);
+			if (v != null) {
+				color = 255 << 24 | v;
+				picker.change = SColor;
+			}
+		};
+		
+		canvas = new h2d.css.Fill(this);
+		canvas.y = 2 + height * 0.5;
+		drawAll();
+	}
+	
+	public function drawAll() {
+		canvas.reset();
+		canvas.fillRectColor(0, 0, width, height * 0.5, preview);
+		canvas.fillRectColor(0, 0, width * 0.5, height * 0.5, color);
+		canvas.fillRectColor(0, height * 0.5 - 4, width, 4, 0xFF000000);
+		canvas.fillRectColor(0, height * 0.5 - 4, width * alpha, 4, 0xFFFFFFFF);
+		canvas.lineRect(FillStyle.Color(ColorPicker.borderColor), 0, 0, width, height * 0.5, 1);
+	}
+}
+
+private class Palette extends h2d.Sprite {
+	public var width :Float;
+	public var height :Float;
+	public var color(default, set):Int;
+	
+	var picker : ColorPicker;
+	var canvas:h2d.css.Fill;
+	var interact:h2d.Interactive;
+	var cursor:h2d.Sprite;
+	
+	public function new (picker, ix, iy, iw, ih, parent) {
+		super(parent);
+		this.picker = picker;
+		x = ix;
+		y = iy;
+		width = iw;
+		height = ih;
+		init();
+	}
+	
+	function init() {
+		canvas = new h2d.css.Fill(this);
+		cursor = new h2d.Sprite(this);
+		var larrow = new Arrow(cursor, 0, 0, 0, 0xffcccccc);
+		var rarrow = new Arrow(cursor, width, 0, Math.PI, 0xffcccccc);
+		interact = new h2d.Interactive(width + 16, height, canvas);
+		interact.x = -8;
+		interact.onPush = function(e) {
+			setCursor(e.relY);
+			interact.startDrag(function(e) {
+				if( e.kind == EMove )
+					setCursor(e.relY);
+			});
+			picker.change = SPalette;
+		}
+		interact.onRelease = function(e) {
+			interact.stopDrag();
+			picker.change = SNone;
+		}
+		color = getColor(0);
+		drawAll();
+	}
+	
+	function set_color(v:Int) {
+		color = v;
+		if(!picker.change.equals(SPalette))
+			updateCursor();
+		drawAll();
+		return color;
+	}
+	
+	function updateCursor() {
+		var hsl = ColorPicker.INTtoHSL(color);
+		cursor.y = Math.round(Math.max(0, Math.min(height, (1 - hsl[0]) * height)));
+	}
+	
+	public function drawAll() {
+		var s = 1;
+		var l = 0.5;
+		var seg = height / 6;
+		canvas.reset();
+		for (i in 0...6) {
+			var up = ColorPicker.HSLtoINT(1 - i / 6, s, l);
+			var down = ColorPicker.HSLtoINT(1 - (i + 1) / 6, s, l);
+			canvas.fillRectGradient(0, i * seg, width, seg, up, up, down, down);
+		}
+		canvas.lineRect(FillStyle.Color(ColorPicker.borderColor), 0, 0, width, height, 1);
+	}
+	
+	public function setCursor(dy:Float) {
+		cursor.y = Math.round(Math.max(0, Math.min(height, dy)));
+		color = getColor(cursor.y);
+	}
+	
+	public function getColor(py:Float) {
+		var h = 1 - (py / height);
+		var s = 1;
+		var l = 0.5;
+		return(ColorPicker.HSLtoINT(h, s, l));
+	}
+	
+	public function setColorFrom(newColor:Int) {
+		var rgb = ColorPicker.INTtoRGB(newColor);
+		var hsl = ColorPicker.RGBtoHLS(rgb[0], rgb[1], rgb[2]);
+		hsl[1] = 1; hsl[2] = 0.5;
+		rgb = ColorPicker.HSLtoRGB(hsl[0], hsl[1], hsl[2]);
+		color = ColorPicker.RGBtoINT(rgb[0], rgb[1], rgb[2]);
+	}
+}
+
+private class Chart extends h2d.Sprite{
+	public var width :Int;
+	public var height :Int;
+	public var refColor(default, set):Int = 0xffffffff;
+	public var color:Int = 0xffffffff;
+	
+	var picker : ColorPicker;
+	var ray :Float;
+	var canvas:h2d.css.Fill;
+	var interact:h2d.Interactive;
+	var cursor:h2d.Sprite;
+	var lastPos:h3d.Vector;
+	var cross:Cross ;
+	
+	public function new (picker,ix, iy, iw, ih, ray, parent) {
+		super(parent);
+		this.picker = picker;
+		x = ix;
+		y = iy;
+		width = iw;
+		height = ih;
+		this.ray = ray;
+		init();
+	}
+	
+	function init() {
+		canvas = new h2d.css.Fill(this);
+		cursor = new h2d.Sprite(this);
+		cross = new Cross(cursor, ray * 2);
+		cross.x = cross.y = -ray;
+		interact = new h2d.Interactive(width, height, canvas);
+		interact.onPush = function(e) {
+			setCursor(e.relX, e.relY);
+			interact.startDrag(function(e) {
+				if( e.kind == EMove )
+					setCursor(e.relX, e.relY);
+			});
+			picker.change = SChart;
+		}
+		interact.onRelease = function(e) {
+			interact.stopDrag();
+			picker.change = SNone;
+		}
+		drawAll();
+		setCursor(0, 0);
+	}
+	
+	public function setCursor(dx:Float, dy:Float) {
+		cursor.x = Math.max(ray + 1, Math.min(width - ray - 1, dx));
+		cursor.y = Math.max(ray + 1, Math.min(height - ray - 1, dy));
+		lastPos = normalizePos(dx, dy);
+		color = getColor(lastPos.x, lastPos.y);
+		cross.setColor(ColorPicker.complementaryColor(color));
+	}
+	
+	function set_refColor(v:Int) {
+		refColor = v;
+		color = getColor(lastPos.x, lastPos.y);
+		cross.setColor(ColorPicker.complementaryColor(color));
+		drawAll();
+		return refColor;
+	}
+	
+	function normalizePos(dx:Float, dy:Float) {
+		var px = 1 - Math.min(width, Math.max(0, dx)) / width;
+		var py = 1 - Math.min(height, Math.max(0, dy)) / height;
+		return new h3d.Vector(px, py);
+	}
+	
+	public function drawAll() {
+		canvas.reset();
+		var rgb = [(refColor >> 16) & 0xFF, (refColor >> 8) & 0xFF,  refColor & 0xFF];
+		for (i in 0...width>>1) {
+			for (j in 0...height>>1) {
+				var di = Math.max(0, Math.min(width, i * 2));
+				var dj = Math.max(0, Math.min(width, j * 2));
+				var dw = (1 - di / width);
+				var dh = (1 - dj / height);
+				var r = Math.round(rgb[0] * dh);
+				var g = Math.round(rgb[1] * dh);
+				var b = Math.round(rgb[2] * dh);
+				var max = Math.max(r, Math.max(g, b));
+				r = Math.round(r + (max - r) * dw);
+				g = Math.round(g + (max - g) * dw);
+				b = Math.round(b + (max - b) * dw);
+				var c = (255 << 24) | (r << 16) | (g << 8) | b;
+				canvas.fillRectColor(i * 2, j * 2, 2, 2, c);
+			}
+		}
+		canvas.lineRect(FillStyle.Color(ColorPicker.borderColor), 0, 0, width, height, 1);
+	}
+	
+	function getColor(dw:Float, dh:Float) {
+		var rgb = [(refColor >> 16) & 0xFF, (refColor >> 8) & 0xFF,  refColor & 0xFF];
+		var r = Math.round(rgb[0] * dh);
+		var g = Math.round(rgb[1] * dh);
+		var b = Math.round(rgb[2] * dh);
+		var max = Math.max(r, Math.max(g, b));
+		r = Math.round(r + (max - r) * dw);
+		g = Math.round(g + (max - g) * dw);
+		b = Math.round(b + (max - b) * dw);
+		return ColorPicker.RGBtoINT(r, g, b);
+	}
+	
+	public function setColorFrom(newColor:Int) {
+		var rgb = ColorPicker.INTtoRGB(newColor);
+		var hsl = ColorPicker.RGBtoHLS(rgb[0], rgb[1], rgb[2]);
+		hsl[1] = 1; hsl[2] = 0.5;
+		rgb = ColorPicker.HSLtoRGB(hsl[0], hsl[1], hsl[2]);
+		refColor = ColorPicker.RGBtoINT(rgb[0], rgb[1], rgb[2]);
+		
+		var rgb = ColorPicker.INTtoRGB(newColor);
+		var min = Math.min(rgb[0], Math.min(rgb[1], rgb[2]));
+		var max = Math.max(rgb[0], Math.max(rgb[1], rgb[2]));
+		var dx = 1 - min / max;
+		var dy = 1 - max / 255;
+		setCursor(dx * width, dy * height);
+	}
+}
+
+private class ColorGauge extends h2d.Sprite{
+	public var width :Int;
+	public var height :Int;
+	public var color(default, set):Int = 0xffffffff;
+	public var ratio(get, null):Float;
+	
+	var picker : ColorPicker;
+	var canvas:h2d.css.Fill;
+	var interact:h2d.Interactive;
+	var cursor:h2d.Sprite;
+	var bindTo:RGBA;
+	var label : h2d.comp.Label;
+	var input : h2d.comp.Input;
+	var isFinal:Bool;
+	public function new (picker,ix, iy, iw, ih, rgba, parent) {
+		super(parent);
+		this.picker = picker;
+		x = ix;
+		y = iy;
+		width = iw;
+		height = ih;
+		bindTo = rgba;
+		init();
+	}
+	
+	function init() {
+		label = new h2d.comp.Label(bindTo.getName(), this);
+		label.setStyle(Style.get(GaugeLabel));
+		label.x = -45;
+		label.y = 2;
+		input = new h2d.comp.Input(this);
+		input.setStyle(Style.get(GaugeInput));
+		input.value = "255";
+		input.x = -30; input.y = 3;
+		input.onChange = function(e) {
+			setCursor(cursor.x);
+		};
+		
+		canvas = new h2d.css.Fill(this);
+		cursor = new h2d.Sprite(this);
+		cursor.x = width;
+		var larrow = new Arrow(cursor, 0, 0, -Math.PI / 2, 0xffcccccc);
+		var rarrow = new Arrow(cursor, 0, height, Math.PI / 2, 0xffcccccc);
+		interact = new h2d.Interactive(width, height + 8, canvas);
+		interact.y = -4;
+		interact.onPush = function(e) {
+			setCursor(e.relX);
+			interact.startDrag(function(e) {
+				if( e.kind == EMove )
+					setCursor(e.relX);
+			});
+			setState();
+		}
+		interact.onRelease = function(e) {
+			interact.stopDrag();
+			picker.change = SNone;
+		}
+		drawAll();
+	}
+	
+	function set_color(v:Int) {
+		color = v;
+		if(!bindTo.equals(RGBA.A))
+			updateCursor();
+		drawAll();
+		return color;
+	}
+	
+	function setState() {
+		picker.change = switch(bindTo) {
+			case RGBA.R: SRed;
+			case RGBA.G: SGreen;
+			case RGBA.B: SBlue;
+			case RGBA.A: SAlpha;
+		}
+	}
+	
+	public function get_ratio() {
+		return cursor.x / width;
+	}
+	
+	public function updateCursor() {
+		var a = color >>> 24;
+		var r = (color >> 16) & 0xFF;
+		var g =	(color >> 8) & 0xFF;
+		var b = color & 0xFF;
+		cursor.x = Math.round(switch(bindTo) {
+			case RGBA.R: r * width / 255;
+			case RGBA.G: g * width / 255;
+			case RGBA.B: b * width / 255;
+			case RGBA.A: a * width / 255;
+		});
+		input.value = Std.string(Std.int(255 * ratio));
+	}
+	
+	public function setCursor(dx:Float) {
+		cursor.x = Math.round(Math.max(0, Math.min(width, dx)));
+		var r = (color >> 16) & 0xFF;
+		var g =	(color >> 8) & 0xFF;
+		var b = color & 0xFF;
+		color = switch(bindTo) {
+			case RGBA.R: ColorPicker.RGBtoINT(Math.round(255 * ratio), g, b);
+			case RGBA.G: ColorPicker.RGBtoINT(r, Math.round(255 * ratio), b);
+			case RGBA.B: ColorPicker.RGBtoINT(r, g, Math.round(255 * ratio));
+			case RGBA.A: color;
+		}
+		input.value = Std.string(Math.round(255 * ratio));
+	}
+	
+	public function drawAll() {
+		var r = (color >> 16) & 0xFF;
+		var g =	(color >> 8) & 0xFF;
+		var b = color & 0xFF;
+		var left:Int;
+		var right:Int;
+		switch(bindTo) {
+			case RGBA.R: left = ColorPicker.RGBtoINT(0, g, b);	right = ColorPicker.RGBtoINT(255, g, b);
+			case RGBA.G: left = ColorPicker.RGBtoINT(r, 0, b);	right = ColorPicker.RGBtoINT(r, 255, b);
+			case RGBA.B: left = ColorPicker.RGBtoINT(r, g, 0);	right = ColorPicker.RGBtoINT(r, g, 255);
+			case RGBA.A: left = 0xFF000000;						right = 0xFFFFFFFF;
+		}
+		canvas.reset();
+		canvas.fillRectGradient(0, 0, width, height, left, right, left, right);
+		canvas.lineRect(FillStyle.Color(ColorPicker.borderColor), 0, 0, width, height, 1);
+	}
+}
+
+
+
+/////////////////////////////////////////////////////////////////
+
+@:allow(h2d.comp._ColorPicker)
+class ColorPicker extends h2d.comp.Component {
+	
+	public static var borderColor = 0xFFaaaaaa;
+	
+	var finalColor : Color;
+	var palette : Palette;
+	var chart : Chart;
+	var gaugeRed : ColorGauge;
+	var gaugeGreen : ColorGauge;
+	var gaugeBlue : ColorGauge;
+	var gaugeAlpha : ColorGauge;
+	var timer : haxe.Timer;
+	var change : ChangeState;
+	
+	public var color(get, set) : Int;
+	
+	public function new(?parent) {
+		super("colorpicker", parent);
+		init();
+	}
+	
+	inline function get_color() {
+		return finalColor.color;
+	}
+	
+	function set_color(v) {
+		finalColor.color = v;
+		palette.setColorFrom(v);
+		chart.setColorFrom(v);
+		return v;
+	}
+	
+	override function onAlloc() {
+		super.onAlloc();
+		if( timer == null ) {
+			timer = new haxe.Timer(10);
+			timer.run = doUpdate;
+		}
+	}
+	
+	override function onDelete() {
+		super.onDelete();
+		if( timer != null ) {
+			timer.stop();
+			timer = null;
+		}
+	}
+	
+	function init() {
+		finalColor = new Color(this, 15, 8, 175, 45, this);
+		palette = new Palette(this, 16, 65, 20, 140, this);
+		chart = new Chart(this,50, 65, 140, 140, 3.5, this);
+		gaugeRed = new ColorGauge(this, 50, 220, 140, 15, RGBA.R, this);
+		gaugeGreen = new ColorGauge(this, 50, 245, 140, 15, RGBA.G, this);
+		gaugeBlue = new ColorGauge(this, 50, 270, 140, 15, RGBA.B, this);
+		gaugeAlpha = new ColorGauge(this, 50, 295, 140, 15, RGBA.A, this);
+		chart.refColor = palette.color;
+		change = SNone;
+		var close = new Button("", this);
+		close.addClass(":close");
+//		close.addStyleString("layout:absolute;font-size:12px;height:10px;width:10px;");
+		close.x = 175;
+		close.y = 10;
+		close.onClick = function() {
+			onClose();
+		};
+	}
+
+	function doUpdate() {
+		finalColor.preview = chart.color;
+		if(change.equals(SNone)) {
+			if(finalColor.color != chart.color) {
+				finalColor.updateColor(chart.color);
+				onChange(finalColor.color);
+			}
+			return;
+		}
+			
+		switch(change) {
+			case SColor:	palette.setColorFrom(finalColor.color);
+							chart.setColorFrom(finalColor.color);
+							// require another change event since we have finalColor == chartColor
+							onChange(finalColor.color);
+			case SPalette:	chart.refColor = palette.color;
+			case SRed:		chart.setColorFrom(gaugeRed.color);
+							palette.color = chart.refColor;
+			case SGreen:	chart.setColorFrom(gaugeGreen.color);
+							palette.color = chart.refColor;
+			case SBlue:		chart.setColorFrom(gaugeBlue.color);
+							palette.color = chart.refColor;
+			case SAlpha:	finalColor.alpha = gaugeAlpha.ratio;
+			default:
+		}
+		
+		gaugeRed.color = chart.color;
+		gaugeGreen.color = chart.color;
+		gaugeBlue.color = chart.color;
+	}
+	
+	public dynamic function onClose() {
+	}
+	
+	public dynamic function onChange( value : Int ) {
+	}
+	
+//////////////////
+	public static function INTtoRGB(color:Int) {
+		return [(color >> 16) & 0xFF, (color >> 8) & 0xFF,  color & 0xFF];
+	}
+	
+	public static function INTtoHSL(color:Int) {
+		var rgb = INTtoRGB(color);
+		return RGBtoHLS(rgb[0], rgb[1], rgb[2]);
+	}
+	
+	public static function RGBtoINT(r:Int, g:Int, b:Int, a:Int = 255) {
+		return (a << 24) | (r << 16) | (g << 8) | b;
+	}
+	
+	public static function RGBtoHLS(r:Float, g:Float, b:Float) {
+		r /= 255;
+		g /= 255;
+		b /= 255;
+		var max = Math.max(r, Math.max(g, b));
+		var min = Math.min(r, Math.min(g, b));
+		var med = (max + min) / 2;
+		var h = med;
+		var s = med;
+		var l = med;
+		if(max == min)
+			h = s = 0;
+		else {
+			var d = max - min;
+			s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+			if(max == r) 		h = (g - b) / d + (g < b ? 6 : 0);
+			else if(max == g) 	h = (b - r) / d + 2;
+			else if(max == b) 	h = (r - g) / d + 4;
+			h /= 6;
+		}
+		return [h, s, l];
+	}
+	
+	public static function HSLtoINT(h:Float, s:Float, l:Float) {
+		var rgb = HSLtoRGB(h, s, l);
+		return RGBtoINT(rgb[0], rgb[1], rgb[2]);
+	}
+	
+	public static function HSLtoRGB(h:Float, s:Float, l:Float) {
+		var r, g, b;
+		if(s == 0)
+			r = g = b = l;
+		else {
+			function hue2rgb(p:Float, q:Float, t:Float) {
+				if(t < 0) t += 1;
+				if(t > 1) t -= 1;
+				if(t < 1 / 6) return p + (q - p) * 6 * t;
+				if(t < 1 / 2) return q;
+				if(t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+				return p;
+			}
+			var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+			var p = 2 * l - q;
+			r = hue2rgb(p, q, h + 1 / 3);
+			g = hue2rgb(p, q, h);
+			b = hue2rgb(p, q, h - 1 / 3);
+		}
+		return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+	}
+	
+	public static function complementaryColor (color:Int) {
+		var rgb = INTtoRGB(color);
+		var r = rgb[0] ^ 0xFF;
+		var g = rgb[1] ^ 0xFF;
+		var b = rgb[2] ^ 0xFF;
+		return (255 << 24) | (r << 16) | (g << 8) | b;
+	}
+}

+ 311 - 0
h2d/comp/Component.hx

@@ -0,0 +1,311 @@
+package h2d.comp;
+import h2d.css.Defs;
+
+class Component extends Sprite {
+	
+	public var name(default, null) : String;
+	public var id(default, set) : String;
+	var parentComponent : Component;
+	var classes : Array<String>;
+	var components : Array<Component>;
+	var iconBmp : h2d.Bitmap;
+	var bg : h2d.css.Fill;
+	// the total width and height (includes margin,borders and padding)
+	var width : Float;
+	var height : Float;
+	var contentWidth : Float = 0.;
+	var contentHeight : Float = 0.;
+	var style : h2d.css.Style;
+	var customStyle : h2d.css.Style;
+	var styleSheet : h2d.css.Engine;
+	var needRebuild(default,set) : Bool;
+	
+	public function new(name,?parent) {
+		super(parent);
+		this.name = name;
+		classes = [];
+		components = [];
+		bg = new h2d.css.Fill(this);
+		needRebuild = true;
+	}
+	
+	public function getParent() {
+		if( allocated )
+			return parentComponent;
+		var c = parent;
+		while( c != null ) {
+			var cm = Std.instance(c, Component);
+			if( cm != null ) return cm;
+			c = c.parent;
+		}
+		return null;
+	}
+	
+	public function getElementById(id:String) {
+		if( this.id == id )
+			return this;
+		if( !allocated )
+			return getElementByIdRec(this, id);
+		for( c in components ) {
+			var c = c.getElementById(id);
+			if( c != null )
+				return c;
+		}
+		return null;
+	}
+	
+	function getElementByIdRec( s : h2d.Sprite, id : String ) : Component {
+		var c = Std.instance(s, Component);
+		if( c != null && c.id == id )
+			return c;
+		for( s in s.childs ) {
+			var c = getElementByIdRec(s, id);
+			if( c != null ) return c;
+		}
+		return null;
+	}
+	
+	function set_needRebuild(v) {
+		needRebuild = v;
+		if( v && parentComponent != null && !parentComponent.needRebuild )
+			parentComponent.needRebuild = true;
+		return v;
+	}
+		
+	override function onAlloc() {
+		// lookup our parent component
+		var old = parentComponent;
+		var p = parent;
+		while( p != null ) {
+			var c = Std.instance(p, Component);
+			if( c != null ) {
+				parentComponent = c;
+				if( old != c ) {
+					if( old != null ) old.components.remove(this);
+					c.components.push(this);
+				}
+				needRebuild = true;
+				super.onAlloc();
+				return;
+			}
+			p = p.parent;
+		}
+		if( old != null ) old.components.remove(this);
+		parentComponent = null;
+		super.onAlloc();
+	}
+	
+	public function addCss(cssString) {
+		if( styleSheet == null ) evalStyle();
+		styleSheet.addRules(cssString);
+		needRebuild = true;
+	}
+	
+	public function setStyle(?s) {
+		customStyle = s;
+		needRebuild = true;
+		return this;
+	}
+
+	public function addStyle(s) {
+		if( customStyle == null )
+			customStyle = new h2d.css.Style();
+		customStyle.apply(s);
+		needRebuild = true;
+		return this;
+	}
+
+	public function addStyleString(s) {
+		if( customStyle == null )
+			customStyle = new h2d.css.Style();
+		new h2d.css.Parser().parse(s, customStyle);
+		needRebuild = true;
+		return this;
+	}
+	
+	public function getClasses() : Iterable<String> {
+		return classes;
+	}
+	
+	public function hasClass( name : String ) {
+		return Lambda.has(classes, name);
+	}
+	
+	public function addClass( name : String ) {
+		if( !Lambda.has(classes, name) ) {
+			classes.push(name);
+			needRebuild = true;
+		}
+		return this;
+	}
+	
+	public function toggleClass( name : String, ?flag : Null<Bool> ) {
+		if( flag != null ) {
+			if( flag )
+				addClass(name)
+			else
+				removeClass(name);
+		} else {
+			if( !classes.remove(name) )
+				classes.push(name);
+			needRebuild = true;
+		}
+		return this;
+	}
+	
+	public function removeClass( name : String ) {
+		if( classes.remove(name) )
+			needRebuild = true;
+		return this;
+	}
+	
+	function set_id(id) {
+		this.id = id;
+		needRebuild = true;
+		return id;
+	}
+	
+	function getFont() {
+		return Context.getFont(style.fontName, Std.int(style.fontSize));
+	}
+	
+	function evalStyle() {
+		if( parentComponent == null ) {
+			if( styleSheet == null )
+				styleSheet = Context.getDefaultCss();
+		} else {
+			styleSheet = parentComponent.styleSheet;
+			if( styleSheet == null ) {
+				parentComponent.evalStyle();
+				styleSheet = parentComponent.styleSheet;
+			}
+		}
+		styleSheet.applyClasses(this);
+	}
+	
+	inline function extLeft() {
+		return style.paddingLeft + style.marginLeft + style.borderSize;
+	}
+
+	inline function extTop() {
+		return style.paddingTop + style.marginTop + style.borderSize;
+	}
+	
+	inline function extRight() {
+		return style.paddingRight + style.marginRight + style.borderSize;
+	}
+
+	inline function extBottom() {
+		return style.paddingBottom + style.marginBottom + style.borderSize;
+	}
+	
+	function resize( c : Context ) {
+		if( c.measure ) {
+			if( style.width != null ) contentWidth = style.width;
+			if( style.height != null ) contentHeight = style.height;
+			width = contentWidth + extLeft() + extRight();
+			height = contentHeight + extTop() + extBottom();
+		} else {
+			if( style.positionAbsolute ) {
+				var p = parent == null ? new h2d.col.Point() : parent.localToGlobal();
+				x = style.offsetX + extLeft() - p.x;
+				y = style.offsetY + extTop() - p.y;
+			} else {
+				if( c.xPos != null ) x = c.xPos + style.offsetX + extLeft();
+				if( c.yPos != null ) y = c.yPos + style.offsetY + extTop();
+			}
+			bg.reset();
+			bg.x = style.marginLeft - extLeft();
+			bg.y = style.marginTop - extTop();
+			bg.lineRect(style.borderColor, 0, 0, width - (style.marginLeft + style.marginRight), height - (style.marginTop + style.marginBottom), style.borderSize);
+			bg.fillRect(style.backgroundColor, style.borderSize, style.borderSize, contentWidth + style.paddingLeft + style.paddingRight, contentHeight + style.paddingTop + style.paddingBottom);
+			if( style.icon != null ) {
+				if( iconBmp == null )
+					iconBmp = new h2d.Bitmap(null);
+				bg.addChildAt(iconBmp, 0);
+				iconBmp.x = extLeft() - style.paddingLeft + style.iconLeft;
+				iconBmp.y = extTop() - style.paddingTop + style.iconTop;
+				iconBmp.tile = Context.makeTileIcon(style.icon);
+				iconBmp.colorKey = 0xFFFF00FF;
+				if( iconBmp.color == null ) iconBmp.color = new h3d.Vector(1, 1, 1, 1);
+				iconBmp.color.loadColor(style.iconColor != null ? style.iconColor : 0xFFFFFFFF);
+			} else if( iconBmp != null ) {
+				iconBmp.remove();
+				iconBmp = null;
+			}
+		}
+	}
+	
+	function resizeRec( ctx : Context ) {
+		resize(ctx);
+		if( ctx.measure ) {
+			for( c in components )
+				c.resizeRec(ctx);
+		} else {
+			var oldx = ctx.xPos;
+			var oldy = ctx.yPos;
+			if( style.layout == Absolute ) {
+				ctx.xPos = null;
+				ctx.yPos = null;
+			} else {
+				ctx.xPos = 0;
+				ctx.yPos = 0;
+			}
+			for( c in components )
+				c.resizeRec(ctx);
+			ctx.xPos = oldx;
+			ctx.yPos = oldy;
+		}
+	}
+	
+	override function drawRec( ctx : h2d.RenderContext ) {
+		if( style.overflowHidden ) {
+			var px = (absX + 1) / matA + 1e-10;
+			var py = (absY - 1) / matD + 1e-10;
+			ctx.engine.setRenderZone(Std.int(px + style.marginLeft - extLeft()), Std.int(py + style.marginTop - extTop()), Std.int(width - (style.marginLeft + style.marginRight)), Std.int(height - (style.marginTop + style.marginBottom)));
+		}
+		super.drawRec(ctx);
+		if( style.overflowHidden )
+			ctx.engine.setRenderZone();
+	}
+	
+	function evalStyleRec() {
+		needRebuild = false;
+		evalStyle();
+		if( style.display != null )
+			visible = style.display;
+		for( c in components )
+			c.evalStyleRec();
+	}
+	
+	function textAlign( tf : h2d.Text ) {
+		if( style.width == null ) {
+			tf.x = 0;
+			return;
+		}
+		switch( style.textAlign ) {
+		case Left:
+			tf.x = 0;
+		case Right:
+			tf.x = style.width - tf.textWidth;
+		case Center:
+			tf.x = Std.int((style.width - tf.textWidth) * 0.5);
+		}
+	}
+	
+	public function refresh() {
+		needRebuild = true;
+	}
+	
+	override function sync( ctx : RenderContext ) {
+		if( needRebuild ) {
+			evalStyleRec();
+			var ctx = new Context(ctx.engine.width, ctx.engine.height);
+			resizeRec(ctx);
+			ctx.measure = false;
+			resizeRec(ctx);
+		}
+		super.sync(ctx);
+	}
+	
+}

+ 47 - 0
h2d/comp/Context.hx

@@ -0,0 +1,47 @@
+package h2d.comp;
+
+class Context {
+	
+	// measure props
+	public var measure : Bool;
+	public var maxWidth : Float;
+	public var maxHeight : Float;
+	// arrange props
+	public var xPos : Null<Float>;
+	public var yPos : Null<Float>;
+	
+	public function new(w, h) {
+		this.maxWidth = w;
+		this.maxHeight = h;
+		measure = true;
+	}
+	
+	// ------------- STATIC API ---------------------------------------
+	
+	public static function getFont( name : String, size : Int ) {
+		return hxd.res.FontBuilder.getFont(name, size);
+	}
+	
+	public static function makeTileIcon( pixels : hxd.Pixels ) : h2d.Tile {
+		var t = cachedIcons.get(pixels);
+		if( t != null && !t.isDisposed() )
+			return t;
+		t = h2d.Tile.fromPixels(pixels);
+		cachedIcons.set(pixels, t);
+		return t;
+	}
+	
+	static var cachedIcons = new Map<hxd.Pixels,h2d.Tile>();
+		
+	public static var DEFAULT_CSS = hxd.res.Embed.getFileContent("h2d/css/default.css");
+	
+	static var DEF = null;
+	public static function getDefaultCss() {
+		if( DEF != null )
+			return DEF;
+		var e = new h2d.css.Engine();
+		e.addRules(DEFAULT_CSS);
+		return e;
+	}
+	
+}

+ 652 - 0
h2d/comp/GradientEditor.hx

@@ -0,0 +1,652 @@
+package h2d.comp;
+import h2d.css.Defs;
+import h2d.css.Fill;
+
+private typedef Key = { x:Float, value:Int };
+
+private enum KCursor {
+	KAlpha;
+	KColor;
+}
+
+private enum CompStyle {
+	CLabel;
+	CInput;
+	CInputSmall;
+}
+
+private class Style {
+	public function new () {
+	}
+	
+	public static function get(kind:CompStyle) {
+		var style = new h2d.css.Style();
+		switch(kind) {
+			case CLabel: 		style.fontSize = 12;
+			case CInputSmall: 	style.width = 24;
+								style.height = 10;
+								style.fontSize = 11;
+			case CInput: 		style.width = 50;
+								style.height = 10;
+								style.fontSize = 11;
+		}
+		return style;
+	}
+}
+
+private class CFlag extends h2d.css.Fill {
+	
+	public function new (parent, x:Float, y:Float, ang = 0., color = 0xff000000, border = 0xFFaaaaaa) {
+		super(parent);
+		var fill = this;
+		fill.rotation = ang;
+		fill.x = x;
+		fill.y = y;
+		//bg
+		
+		// bgarrow
+		fill.addPoint(-5, -4, border);
+		fill.addPoint(5, -4, border);
+		fill.addPoint(0, 0, border);
+		fill.addPoint(0, 0, border);
+		
+		// bgsquare
+		var dy = -5;
+		fill.addPoint(-5, -9 + dy, border);
+		fill.addPoint(5, -9 + dy, border);
+		fill.addPoint(-5, 1 + dy, border);
+		fill.addPoint(5, 1 + dy, border);
+		
+		// arrow
+		fill.addPoint(-4, -5, color);
+		fill.addPoint(4, -5, color);
+		fill.addPoint(0, 0, color);
+		fill.addPoint(0, 0, color);
+		
+		// square
+		fill.addPoint(-4, -8+dy, color);
+		fill.addPoint(4, -8+dy, color);
+		fill.addPoint(-4, 0+dy, color);
+		fill.addPoint(4, 0+dy, color);
+	}
+}
+
+private class Cursor extends h2d.Sprite {
+	var gradient : GradientEditor;
+	public var value(default, set):Int;
+	public var coeff(get, null):Float;
+	public var color:Int = 0xFFFFFFFF;
+	public var bgcolor:Int = 0xFFFF00FF;
+	public var cursor:h2d.Sprite;
+	public var kind:KCursor;
+	var ang:Float;
+	var interact:h2d.Interactive;
+	var flag:CFlag;
+	
+	public function new(gradient,ix, iy, kind, value, ang, parent) {
+		super(parent);
+		this.gradient = gradient;
+		x = ix;
+		y = iy;
+		this.value = value;
+		this.ang = ang;
+		this.kind = kind;
+		init();
+	}
+	
+	function set_value(v) {
+		value = v;
+		if(flag != null) {
+			switch(kind) {
+				case KColor: color = value;
+				case KAlpha: color = (255 << 24) | (value << 16) | (value << 8) | value;
+			}
+			flag.remove();
+			flag = new CFlag(cursor, 0, 0, ang, color, bgcolor);
+		}
+		return value;
+	}
+	
+	function get_coeff() {
+		return x / gradient.boxWidth;
+	}
+	
+	function init() {
+		cursor = new h2d.Sprite(this);
+		switch(kind) {
+			case KColor: color = value;
+			case KAlpha: color = (255 << 24) | (value << 16) | (value << 8) | value;
+		}
+		flag = new CFlag(cursor, 0, 0, ang, color, bgcolor);
+		interact = new h2d.Interactive(10, 14, this);
+		interact.x = -5;
+		if(kind.equals(KAlpha))
+			interact.y = -14;
+			
+		interact.onPush = function(e) drag();
+		interact.onRelease = function(e) stopDrag();
+	}
+	
+	public function drag() {
+		interact.startDrag(function(e) {
+			if( e.kind == EMove ){
+				setCursor(x + (e.relX-5));
+				if(e.relY < - 6 ||  e.relY > 16 + 6)
+					gradient.dragOut = true;
+				else gradient.dragOut = false;
+			}
+			gradient.dragTarget = this ;
+		});
+			
+		gradient.updateTarget = this;
+		this.parent.addChild(this);
+	}
+	
+	public function stopDrag() {
+		interact.stopDrag();
+		if( !visible ) remove();
+		gradient.dragTarget = null ;
+		gradient.dragOut = false;
+	}
+	
+	public function select() {
+		bgcolor = 0xFFFF00FF;
+		flag.remove();
+		flag = new CFlag(cursor, 0, 0, ang, color, bgcolor);
+		
+		if(gradient.colorPicker.visible)
+			gradient.colorPicker.color = color;
+	}
+	
+	public function unselect() {
+		bgcolor = 0xFFAAAAAA;
+		flag.remove();
+		flag = new CFlag(cursor, 0, 0, ang, color, bgcolor);
+	}
+	
+	public function setCursor(px:Float) {
+		x = Math.max(0, Math.min(gradient.boxWidth, px));
+	}
+}
+
+	
+private class AlphaSelector extends h2d.Sprite {
+	public var target(default, set):Cursor;
+	var gradient : GradientEditor;
+	var title:h2d.comp.Label;
+	var slider:h2d.comp.Slider;
+	var alphaInput:h2d.comp.Input;
+	var locLabel:h2d.comp.Label;
+	var locInput:h2d.comp.Input;
+	
+	public function new (gradient,ix, iy, parent) {
+		super(parent);
+		this.gradient = gradient;
+		x = ix;
+		y = iy;
+		init();
+	}
+	
+	function init() {
+		title = new h2d.comp.Label("Alpha", this);
+		title.addStyle(Style.get(CLabel));
+		
+		slider = new h2d.comp.Slider(this);
+		slider.x = 55; slider.y = 4;
+		slider.onChange = function(v) { updateSlider(v); };
+		slider.value = 1;
+		
+		alphaInput = new h2d.comp.Input(this);
+		alphaInput.addStyle(Style.get(CInputSmall));
+		alphaInput.x = 170;
+		alphaInput.value = "255";
+		alphaInput.onChange = function(e) {
+			var v = Std.parseFloat(alphaInput.value);
+			if (!Math.isNaN(v)) {
+				v = Math.min(255, v) / 255;
+				updateSlider(v);
+				slider.value = v;
+			}
+		};
+		
+		locLabel = new h2d.comp.Label("Location             %", this);
+		locLabel.setStyle(Style.get(CLabel));
+		locLabel.x = 255;
+		locInput = new h2d.comp.Input(this);
+		locInput.setStyle(Style.get(CInput));
+		locInput.x = 320;
+		locInput.value = "100.00";
+		locInput.onChange = function(e) {
+			var v = Std.parseFloat(locInput.value);
+			if (!Math.isNaN(v)) {
+				v = Math.min(100, v);
+				target.setCursor( v * gradient.boxWidth / 100 );
+			}
+		};
+	}
+	
+	public function update() {
+		if(target == null)
+			return;
+		locInput.value = Std.string(Math.floor(target.coeff * 100 * 100) / 100);
+	}
+	
+	function set_target(cursor:Cursor) {
+		target = cursor;
+		var v = target.value / 255;
+		slider.value = v;
+		locInput.value = Std.string(Math.floor(target.coeff * 100 * 100) / 100);
+		updateSlider(v);
+		return target;
+	}
+	
+	function updateSlider(v:Float) {
+		var alpha = Math.round(255 * v);
+		alphaInput.value = Std.string(alpha);
+		if(target == null)
+			return;
+		target.value = alpha;
+	}
+}
+
+private class ColorSelector extends h2d.Sprite {
+	public var target(default, set):Cursor;
+	var gradient : GradientEditor;
+	var title:h2d.comp.Label;
+	var locLabel:h2d.comp.Label;
+	var locInput:h2d.comp.Input;
+	var colorInput:h2d.comp.Input;
+	var canvas:h2d.css.Fill;
+	var color:Int = 0xFFFFFFFF;
+	var interact : h2d.Interactive;
+	
+	public function new(gradient,ix, iy, parent) {
+		super(parent);
+		this.gradient = gradient;
+		x = ix;
+		y = iy;
+		init();
+	}
+	
+	public function update() {
+		if(target == null)
+			return;
+		locInput.value = Std.string(Math.floor(target.coeff * 100 * 100) / 100);
+	}
+	
+	function set_target(cursor:Cursor) {
+		target = cursor;
+		locInput.value = Std.string(Math.floor(target.coeff * 100 * 100) / 100);
+		color = target.value;
+		colorInput.value = StringTools.hex(color, 8).substr(2);
+		redraw();
+		return target;
+	}
+	
+	function init() {
+		title = new h2d.comp.Label("Color                         #", this);
+		title.setStyle(Style.get(CLabel));
+		
+		canvas = new Fill(this);
+		canvas.x = 45;
+		canvas.y = -8;
+		interact = new h2d.Interactive(110, 25, this);
+		interact.x = 50;
+		interact.y = -8;
+		interact.onPush = function(e) {
+			if(target == null)
+				return;
+			
+			if(!gradient.colorPicker.visible) {
+				gradient.colorPicker.visible = true;
+				gradient.colorPicker.color = color;
+				gradient.colorPicker.onChange = function(v) {
+					color = target.value = v;
+					colorInput.value = StringTools.hex(v, 8).substr(2);
+					redraw();
+				}
+			}
+			else {
+				gradient.colorPicker.onChange = function(v) { };
+				gradient.colorPicker.visible = false;
+			}
+		};
+		
+		colorInput = new h2d.comp.Input(this);
+		colorInput.setStyle(Style.get(CInput));
+		colorInput.x = 175;
+		colorInput.value = "FFFFFF";
+		colorInput.onChange = function (e) {
+			colorInput.value = colorInput.value.toUpperCase();
+			if(colorInput.value.length > 6) {
+				colorInput.value = colorInput.value.substr(0, 6);
+				return;
+			}
+			var v = Std.parseInt("0x" + colorInput.value);
+			if (v != null) {
+				color = target.value = 255 << 24 | v;
+				redraw();
+			}
+		};
+		
+		locLabel = new h2d.comp.Label("Location             %", this);
+		locLabel.setStyle(Style.get(CLabel));
+		locLabel.x = 265;
+		
+		locInput = new h2d.comp.Input(this);
+		locInput.setStyle(Style.get(CInput));
+		locInput.x = 330;
+		locInput.value = "100.00";
+		locInput.onChange = function(e) {
+			var v = Std.parseFloat(locInput.value);
+			if (!Math.isNaN(v)) {
+				v = Math.min(100, v);
+				locInput.value = Std.string(Math.floor(v * 100) / 100);
+				target.setCursor( v * gradient.boxWidth / 100 );
+			}
+		};
+		
+		redraw();
+	}
+	
+	function redraw() {
+		canvas.reset();
+		canvas.fillRectColor(0, 0, 110, 25, color);
+		canvas.lineRect(FillStyle.Color(gradient.borderColor), 0, 0, 110, 25, 1);
+	}
+}
+		
+		
+//////////////////////////////////////////////////////////
+
+class GradientEditor extends h2d.comp.Component {
+	
+	public var borderColor = 0xFF888888;
+	public var dragTarget:Cursor;
+	public var dragOut:Bool;
+	public var updateTarget:Cursor;
+	public var boxWidth = 430;
+	var boxHeight = 100;
+	
+	var keys:Array<Key>;
+	var colorsKeys:Array<Cursor>;
+	var alphaKeys:Array<Cursor>;
+	
+	var box:h2d.Sprite;
+	var gradient:Fill;
+	var hudAlpha: AlphaSelector;
+	var hudColor: ColorSelector;
+	
+	public var colorPicker:ColorPicker;
+	
+	var interactUp:h2d.Interactive;
+	var interactDown:h2d.Interactive;
+		
+
+	var withAlpha : Bool;
+	var holdCursor:Cursor;
+	
+	public function new(?withAlpha = true, ?parent) {
+		super("gradienteditor", parent);
+		this.withAlpha = withAlpha;
+		init();
+	}
+	
+	function init() {
+		colorsKeys = [];
+		alphaKeys = [];
+		
+		interactUp = new h2d.Interactive(boxWidth, 16, this);
+		interactUp.x = 10;
+		interactUp.y = 30 - 16;
+		interactUp.onPush =  function(e) createAlphaKey(e.relX, 0);
+		interactDown = new h2d.Interactive(boxWidth, 16, this);
+		interactDown.x = 10;
+		interactDown.y = 30 + boxHeight;
+		interactDown.onPush =  function(e) createColorKey(e.relX, boxHeight);
+		
+		box = new h2d.Sprite(this);
+		box.x = 10;
+		box.y = 30;
+		drawChecker();
+		
+		hudColor = new ColorSelector(this, 20, box.y + boxHeight + 30, this);
+		hudAlpha = new AlphaSelector(this, 20, box.y + boxHeight + 30, this);
+		hudAlpha.visible = false;
+		hudAlpha.y = 0;
+		
+		colorPicker = new ColorPicker(this);
+		colorPicker.y = 220;
+		colorPicker.visible = false;
+		colorPicker.onClose = function() colorPicker.visible = false;
+		
+		setKeys([ { x : 0, value:0xFFFFFFFF }, { x: 1, value:0xFFFFFFFF } ],null);
+	}
+	
+	public dynamic function onChange( keys : Array<Key> ) {
+	}
+	
+	public function setKeys(keys:Array<Key>,?kalpha:Array<Key>) {
+		while( colorsKeys.length > 0 )
+			colorsKeys.shift().remove();
+		while( alphaKeys.length > 0 )
+			alphaKeys.shift().remove();
+		for( k in keys ) {
+			var c = new Cursor(this, k.x * boxWidth, boxHeight, KColor, k.value | 0xFF000000, Math.PI, box);
+			colorsKeys.push(c);
+		}
+		if( withAlpha ) {
+			for( a in (kalpha == null ? keys : kalpha) ) {
+				var c = new Cursor(this, a.x * boxWidth, 0, KAlpha, a.value >>> 24, 0, box);
+				alphaKeys.push(c);
+			}
+		}
+		updateKeys();
+		updateTarget = colorsKeys[0];
+	}
+	
+	override function sync(ctx) {
+		if(dragTarget != null) {
+			if(dragOut) {
+				switch(dragTarget.kind) {
+					case KColor: if(colorsKeys.length > 1) {
+						colorsKeys.remove(dragTarget);
+						holdCursor = dragTarget;
+						dragTarget.visible = false;
+					}
+					case KAlpha: if(alphaKeys.length > 1) {
+						alphaKeys.remove(dragTarget);
+						holdCursor = dragTarget;
+						dragTarget.visible = false;
+					}
+				}
+			}
+			else if(holdCursor == dragTarget) {
+				holdCursor = null;
+				dragTarget.visible = true;
+				switch(dragTarget.kind) {
+					case KColor: colorsKeys.push(dragTarget);
+					case KAlpha: alphaKeys.push(dragTarget);
+				}
+			}
+			
+			hudAlpha.update();
+			hudColor.update();
+		}
+		else holdCursor = null;
+		
+		if(updateTarget != null) {
+			changeHud(updateTarget);
+			updateFlags(updateTarget);
+			updateTarget = null;
+		}
+		
+		updateKeys();
+		
+		super.sync(ctx);
+	}
+	
+	function createAlphaKey(px:Float, py:Float) {
+		if( !withAlpha )
+			return;
+		var cursor = new Cursor(this, px, py, KAlpha, getAlphaAt(px / boxWidth), 0, box);
+		alphaKeys.push(cursor);
+		updateTarget = cursor;
+		cursor.drag();
+	}
+	
+	function createColorKey(px:Float, py:Float) {
+		var cursor = new Cursor(this, px, py, KColor, getColorAt(px / boxWidth), Math.PI, box);
+		colorsKeys.push(cursor);
+		updateTarget = cursor;
+		cursor.drag();
+	}
+	
+	function updateKeys() {
+		var keys = [];
+		for (i in 0...colorsKeys.length) {
+			var k = colorsKeys[i];
+			var alpha = getAlphaAt(k.coeff);
+			var rgb = INTtoRGB(k.value);
+			keys.push( { x:k.coeff, value:RGBtoINT(rgb[0], rgb[1], rgb[2], alpha) } );
+		}
+		
+		for (i in 0...alphaKeys.length) {
+			var k = alphaKeys[i];
+			var alpha = k.value;
+			var rgb = INTtoRGB(getColorAt(k.coeff));
+			keys.push( { x:k.coeff, value:RGBtoINT(rgb[0], rgb[1], rgb[2], alpha) } );
+		}
+		
+		keys.sort(function(a, b) return Reflect.compare(a.x, b.x) );
+		if(keys[0].x != 0)
+			keys.insert(0, { x:0, value:keys[0].value } );
+		if(keys[keys.length - 1].x != 1)
+			keys.push( { x:1, value:keys[keys.length - 1].value } );
+		if( Std.string(keys) != Std.string(this.keys) ) {
+			this.keys = keys;
+			drawGradient();
+			onChange(keys);
+		}
+	}
+	
+	function getARGBAt(x:Float) {
+		var alpha = getAlphaAt(x);
+		var rgb = INTtoRGB(getColorAt(x));
+		return RGBtoINT(rgb[0], rgb[1], rgb[2], alpha);
+	}
+	
+	function getAlphaAt(x:Float) {
+		if( !withAlpha )
+			return 255;
+		alphaKeys.sort(function(a, b) return Reflect.compare(a.coeff, b.coeff) );
+		var prev = null;
+		var next = null;
+		for (i in 0...alphaKeys.length) {
+			var k = alphaKeys[i];
+			if (k.coeff == x)
+				return k.value;
+			else if (k.coeff < x)
+				prev = k;
+			else if (k.coeff > x) {
+				if(prev == null)
+					return k.value;
+				else next = k;
+				break;
+			}
+		}
+		if(next == null)
+			return prev.value;
+		var d = (x - prev.coeff) / (next.coeff - prev.coeff);
+		return Math.round(prev.value + (next.value - prev.value) * d);
+	}
+	
+	function getColorAt(x:Float) {
+		colorsKeys.sort(function(a, b) return Reflect.compare(a.coeff, b.coeff) );
+		var prev = null;
+		var next = null;
+		for (i in 0...colorsKeys.length) {
+			var k = colorsKeys[i];
+			if (k.coeff == x)
+				return k.value;
+			else if (k.coeff < x)
+				prev = k;
+			else if (k.coeff > x) {
+				if(prev == null)
+					return k.value;
+				else next = k;
+				break;
+			}
+		}
+		if(next == null)
+			return prev.value;
+			
+		var d = (x - prev.coeff) / (next.coeff - prev.coeff);
+		var pRGB = INTtoRGB(prev.value);
+		var nRGB = INTtoRGB(next.value);
+		var rgb = [];
+		for (i in 0...3)
+			rgb.push(Math.round(pRGB[i] + (nRGB[i] - pRGB[i]) * d));
+		return RGBtoINT(rgb[0], rgb[1], rgb[2]);
+	}
+	
+	function drawGradient() {
+		box.removeChild(gradient);
+		gradient = new Fill(box);
+		for (i in 0...keys.length - 1) {
+			var c1 = keys[i];
+			var c2 = keys[i + 1];
+			gradient.fillRectGradient(boxWidth * c1.x, 0, boxWidth * (c2.x - c1.x), boxHeight, c1.value, c2.value, c1.value, c2.value);
+		}
+		gradient.lineRect(FillStyle.Color(borderColor), 0, 0, boxWidth, boxHeight, 2);
+	}
+	
+	function drawChecker() {
+		var checker = new Fill(box);
+		var nb = 90;
+		var size = Math.ceil(boxWidth / nb);
+		for (i in 0...nb) {
+			for (j in 0...nb) {
+				if(i * size >= boxWidth) break;
+				if(j * size >= boxHeight) break;
+				var color = ((i + j) % 2 == 0) ? 0xFFFFFFFF:0xFFAAAAAA;
+				checker.fillRect(FillStyle.Color(color), i * size, j * size, size, size);
+			}
+		}
+	}
+	
+	function changeHud(cursor:Cursor) {
+		switch(cursor.kind) {
+			case KAlpha: 	hudAlpha.target = cursor;
+							hudAlpha.visible = true;
+							hudAlpha.y = box.y + boxHeight + 30;
+							hudColor.visible = false;
+			case KColor:	hudColor.target = cursor;
+							hudColor.visible = true;
+							hudAlpha.visible = false;
+							hudAlpha.y =  0;
+		}
+	}
+	
+	function updateFlags(cursor:Cursor) {
+		for (c in alphaKeys) {
+			if (c == cursor)
+				c.select();
+			else c.unselect();
+		}
+		for (c in colorsKeys) {
+			if (c == cursor)
+				c.select();
+			else c.unselect();
+		}
+	}
+	
+	
+	inline public static function INTtoRGB(color:Int) {
+		return [(color >> 16) & 0xFF, (color >> 8) & 0xFF,  color & 0xFF, color >>> 24];
+	}
+	
+	inline public static function RGBtoINT(r:Int, g:Int, b:Int, a:Int = 255) {
+		return (a << 24) | (r << 16) | (g << 8) | b;
+	}
+}

+ 120 - 0
h2d/comp/Input.hx

@@ -0,0 +1,120 @@
+package h2d.comp;
+import hxd.Key;
+
+@:access(h2d.comp.Input.scene)
+class Input extends Interactive {
+	
+	var tf : h2d.Text;
+	var cursor : h2d.Bitmap;
+	var cursorPos(default,set) : Int;
+	
+	public var value(default, set) : String;
+	
+	public function new(?parent) {
+		super("input",parent);
+		tf = new h2d.Text(null, this);
+		input.cursor = TextInput;
+		cursor = new h2d.Bitmap(null, bg);
+		cursor.visible = false;
+		input.onFocus = function(_) {
+			addClass(":focus");
+			cursor.visible = true;
+			onFocus();
+		};
+		input.onFocusLost = function(_) {
+			removeClass(":focus");
+			cursor.visible = false;
+			onBlur();
+		};
+		input.onKeyDown = function(e:hxd.Event) {
+			if( input.hasFocus() ) {
+				// BACK
+				switch( e.keyCode ) {
+				case Key.LEFT:
+					if( cursorPos > 0 )
+						cursorPos--;
+				case Key.RIGHT:
+					if( cursorPos < value.length )
+						cursorPos++;
+				case Key.HOME:
+					cursorPos = 0;
+				case Key.END:
+					cursorPos = value.length;
+				case Key.DELETE:
+					value = value.substr(0, cursorPos) + value.substr(cursorPos + 1);
+					onChange(value);
+					return;
+				case Key.BACKSPACE:
+					if( cursorPos > 0 ) {
+						value = value.substr(0, cursorPos - 1) + value.substr(cursorPos);
+						cursorPos--;
+						onChange(value);
+					}
+					return;
+				case Key.ENTER:
+					input.blur();
+					return;
+				}
+				if( e.charCode != 0 ) {
+					value = value.substr(0, cursorPos) + String.fromCharCode(e.charCode) + value.substr(cursorPos);
+					cursorPos++;
+					onChange(value);
+				}
+			}
+		};
+		this.value = "";
+	}
+	
+	function set_cursorPos(v:Int) {
+		cursor.x = tf.x + tf.calcTextWidth(value.substr(0, v)) + extLeft();
+		return cursorPos = v;
+	}
+
+	public function focus() {
+		input.focus();
+		cursorPos = value.length;
+	}
+	
+	function get_value() {
+		return tf.text;
+	}
+	
+	function set_value(t) {
+		needRebuild = true;
+		return value = t;
+	}
+	
+	override function resize( ctx : Context ) {
+		if( ctx.measure ) {
+			tf.font = getFont();
+			tf.textColor = style.color;
+			tf.text = value;
+			tf.filter = true;
+			textAlign(tf);
+			contentWidth = tf.textWidth;
+			contentHeight = tf.textHeight;
+			if( cursorPos < 0 ) cursorPos = 0;
+			if( cursorPos > value.length ) cursorPos = value.length;
+			cursorPos = cursorPos;
+		}
+		super.resize(ctx);
+		if( !ctx.measure ) {
+			cursor.y = extTop() - 1;
+			cursor.tile = h2d.Tile.fromColor(style.cursorColor | 0xFF000000, 1, Std.int(height - extTop() - extBottom() + 2));
+		}
+	}
+
+	override function onClick() {
+		focus();
+	}
+
+	public dynamic function onChange( value : String ) {
+	}
+
+	public dynamic function onFocus() {
+	}
+	
+	public dynamic function onBlur() {
+	}
+	
+}

+ 56 - 0
h2d/comp/Interactive.hx

@@ -0,0 +1,56 @@
+package h2d.comp;
+
+class Interactive extends Component {
+	
+	var input : h2d.Interactive;
+	var active : Bool;
+	
+	function new(kind,?parent) {
+		super(kind,parent);
+		input = new h2d.Interactive(0, 0, bg);
+		active = false;
+		input.onPush = function(_) {
+			active = true;
+			onMouseDown();
+		};
+		input.onOver = function(_) {
+			addClass(":hover");
+			onMouseOver();
+		};
+		input.onOut = function(_) {
+			active = false;
+			removeClass(":hover");
+			onMouseOut();
+		};
+		input.onRelease = function(_) {
+			if( active ) onClick();
+			onMouseUp();
+		};
+	}
+	
+	override function resize( ctx : Context ) {
+		super.resize(ctx);
+		if( !ctx.measure ) {
+			input.width = width - (style.marginLeft + style.marginRight);
+			input.height = height - (style.marginTop + style.marginBottom);
+			input.visible = !hasClass(":disabled");
+		}
+	}
+	
+	public dynamic function onMouseOver() {
+	}
+
+	public dynamic function onMouseOut() {
+	}
+	
+	public dynamic function onMouseDown() {
+	}
+
+	public dynamic function onMouseUp() {
+	}
+	
+	public dynamic function onClick() {
+	}
+	
+	
+}

+ 77 - 0
h2d/comp/ItemList.hx

@@ -0,0 +1,77 @@
+package h2d.comp;
+
+class ItemList extends Box {
+	
+	public var selected(default,set) = -1;
+	var inputs : Array<h2d.Interactive>;
+	
+	public function new(?parent) {
+		super(Vertical, parent);
+		this.name = "itemlist";
+		inputs = [];
+	}
+	
+	function set_selected(v:Int) {
+		needRebuild = true;
+		return selected = v;
+	}
+	
+	override function resizeRec( ctx : Context ) {
+		super.resizeRec(ctx);
+		if( !ctx.measure ) {
+			while( inputs.length < components.length )
+				inputs.push(new h2d.Interactive(0, 0, this));
+			while( inputs.length > components.length )
+				inputs.pop().remove();
+			for( i in 0...components.length ) {
+				var c = components[i];
+				var int = inputs[i];
+				var selected = selected == i;
+				var cursor = null;
+				int.x = -style.paddingLeft;
+				int.y = c.y - style.verticalSpacing * 0.5;
+				int.width = contentWidth + style.paddingLeft + style.paddingRight;
+				int.height = c.height + style.verticalSpacing;
+				var oldCursor = int.getChildAt(0);
+				if( oldCursor != null ) {
+					cursor = Std.instance(oldCursor, h2d.Bitmap);
+					if( cursor == null ) oldCursor.remove();
+				}
+				if( selected ) {
+					if( cursor != null ) cursor.remove();
+					cursor = new h2d.Bitmap(h2d.Tile.fromColor(style.selectionColor, Std.int(int.width), Std.int(int.height)), int);
+					int.onOver = function(_) {
+					};
+					int.onOut = function(_) {
+					}
+					int.onPush = function(_) {
+					}
+				} else {
+					int.onOver = function(_) {
+						if( cursor != null ) cursor.remove();
+						cursor = new h2d.Bitmap(h2d.Tile.fromColor(style.cursorColor, Std.int(int.width), Std.int(int.height)), int);
+					};
+					int.onOut = function(_) {
+						if( cursor != null ) cursor.remove();
+						cursor = null;
+					}
+					int.onPush = function(_) {
+						if( this.selected != i ) {
+							this.selected = i;
+							onChange(i);
+						}
+					}
+				}
+				if( Lambda.indexOf(childs,int) != 1 + i ) {
+					childs.remove(int);
+					childs.insert(1 + i, int); // insert over bg
+					int.onParentChanged();
+				}
+			}
+		}
+	}
+	
+	public dynamic function onChange( selected : Int ) {
+	}
+	
+}

+ 132 - 0
h2d/comp/JQuery.hx

@@ -0,0 +1,132 @@
+package h2d.comp;
+import h2d.css.Defs;
+
+@:access(h2d.comp.Component)
+@:keep
+class JQuery {
+	
+	var root : Component;
+	var select : Array<Component>;
+	
+	public function new( root : Component, query : Dynamic ) {
+		while( root.parentComponent != null )
+			root = root.parentComponent;
+		this.root = root;
+		select = getSet(query);
+	}
+	
+	public function toggleClass( cl : String, ?flag : Bool ) {
+		for( s in select ) s.toggleClass(cl,flag);
+		return this;
+	}
+
+	function _get_val() : Dynamic {
+		var c = select[0];
+		if( c == null ) return null;
+		return switch( c.name ) {
+		case "slider":
+			cast(c, h2d.comp.Slider).value;
+		case "checkbox":
+			cast(c, h2d.comp.Checkbox).checked;
+		case "input":
+			cast(c, h2d.comp.Input).value;
+		case "color":
+			cast(c, h2d.comp.Color).value;
+		case "itemlist":
+			cast(c, h2d.comp.ItemList).selected;
+		case "select":
+			cast(c, h2d.comp.Select).value;
+		default:
+			null;
+		}
+	}
+
+	function _set_val( v : Dynamic ) {
+		for( c in select )
+			switch( c.name ) {
+			case "slider":
+				cast(c, h2d.comp.Slider).value = v;
+			case "checkbox":
+				cast(c, h2d.comp.Checkbox).checked = v != null && v != false;
+			case "input":
+				cast(c, h2d.comp.Input).value = Std.string(v);
+			case "color":
+				cast(c, h2d.comp.Color).value = v;
+			case "itemlist":
+				cast(c, h2d.comp.ItemList).selected = v;
+			case "select":
+				cast(c, h2d.comp.Select).value = v;
+			default:
+				null;
+			}
+		return this;
+	}
+
+	function _get_text() {
+		var c = select[0];
+		if( c == null ) return "";
+		return switch( c.name ) {
+		case "button":
+			cast(c, h2d.comp.Button).text;
+		case "label":
+			cast(c, h2d.comp.Label).text;
+		default:
+			"";
+		}
+	}
+	
+	function _set_text(v:String) {
+		for( c in select )
+			switch( c.name ) {
+			case "button":
+				cast(c, h2d.comp.Button).text = v;
+			case "label":
+				cast(c, h2d.comp.Label).text = v;
+			default:
+			}
+		return this;
+	}
+	
+	function _set_style(v:String) {
+		var s = new h2d.css.Style();
+		new h2d.css.Parser().parse(v, s);
+		for( c in select )
+			c.addStyle(s);
+		return this;
+	}
+	
+	function getSet( query : Dynamic ) {
+		var set;
+		if( query == null )
+			set = [];
+		else if( Std.is(query,Component) )
+			set = [query];
+		else if( Std.is(query, Array) ) {
+			var a : Array<Dynamic> = query;
+			for( v in a ) if( !Std.is(v, Component) ) throw "Invalid JQuery "+query;
+			set = a;
+		} else if( Std.is(query, String) ) {
+			set = lookupSet(query);
+		} else
+			throw "Invalid JQuery " + query;
+		return set;
+	}
+	
+	function lookupSet( query : String ) {
+		var classes = new h2d.css.Parser().parseClasses(query);
+		var set = [];
+		lookupRec(root, classes, set);
+		return set;
+	}
+	
+	function lookupRec(comp:Component, classes:Array<CssClass>, set : Array<Component> ) {
+		for( c in classes )
+			if( h2d.css.Engine.ruleMatch(c, comp) ) {
+				set.push(comp);
+				break;
+			}
+		for( s in comp.components )
+			lookupRec(s, classes, set);
+	}
+	
+}

+ 38 - 0
h2d/comp/Label.hx

@@ -0,0 +1,38 @@
+package h2d.comp;
+
+class Label extends Component {
+	
+	var tf : h2d.Text;
+	
+	public var text(default, set) : String;
+	
+	public function new(text, ?parent) {
+		super("label",parent);
+		tf = new h2d.Text(null, this);
+		this.text = text;
+	}
+
+	function get_text() {
+		return tf.text;
+	}
+	
+	function set_text(t) {
+		needRebuild = true;
+		return text = t;
+	}
+	
+	override function resize( ctx : Context ) {
+		if( ctx.measure ) {
+			tf.font = getFont();
+			tf.textColor = style.color;
+			tf.text = text;
+			tf.filter = true;
+			contentWidth = tf.textWidth;
+			contentHeight = tf.textHeight;
+		}
+		super.resize(ctx);
+		if( !ctx.measure )
+			textAlign(tf);
+	}
+	
+}

+ 266 - 0
h2d/comp/Parser.hx

@@ -0,0 +1,266 @@
+package h2d.comp;
+
+#if hscript
+private class CustomInterp extends hscript.Interp {
+	override function fcall(o:Dynamic, f:String, args:Array<Dynamic>):Dynamic {
+		if( Std.is(o, h2d.comp.JQuery) && Reflect.field(o,f) == null ) {
+			var rf = args.length == 0 ? "_get_" + f : "_set_" + f;
+			if( Reflect.field(o, rf) == null ) throw "JQuery don't have " + f + " implemented";
+			f = rf;
+		}
+		return super.fcall(o, f, args);
+	}
+}
+#end
+
+class Parser {
+	
+	var api : {};
+	var comps : Map<String, haxe.xml.Fast -> Component -> Component>;
+	
+	public function new(?api) {
+		this.api = api;
+		comps = new Map();
+	}
+	
+	public function build( x : haxe.xml.Fast, ?parent : Component ) {
+		var c : Component;
+		switch( x.name.toLowerCase() ) {
+		case "body":
+			c = new Box(Absolute, parent);
+		case "style":
+			parent.addCss(x.innerData);
+			return null;
+		case "div", "box":
+			c = new Box(parent);
+		case "button":
+			c = new Button(x.has.value ? x.att.value : "", parent);
+		case "slider":
+			c = new Slider(parent);
+		case "label", "span":
+			c = new Label(x.x.firstChild() == null ? "" : x.innerData, parent);
+		case "h1", "h2", "h3", "h4":
+			c = new Label(x.x.firstChild() == null ? "" : x.innerData, parent);
+			c.addClass(":" + x.name.toLowerCase());
+		case "checkbox":
+			c = new Checkbox(parent);
+		case "itemlist":
+			c = new ItemList(parent);
+		case "input":
+			c = new Input(parent);
+		case "color":
+			c = new Color(parent);
+		case "colorpicker":
+			c = new ColorPicker(parent);
+		case "gradienteditor":
+			c = new GradientEditor(parent);
+		case "select":
+			c = new Select(parent);
+		case "option":
+			if( parent == null || parent.name != "select" )
+				throw "<option/> needs 'select' parent";
+			var select = Std.instance(parent, Select);
+			var label = x.innerData;
+			var value = x.has.value ? x.att.value : null;
+			select.addOption(label, value);
+			if( x.has.selected && x.att.selected != "false" )
+				select.selectedIndex = select.getOptions().length - 1;
+			return null;
+		case "value":
+			c = new Value(parent);
+		case n:
+			var make = comps.get(n);
+			if( make != null )
+				c = make(x, parent);
+			else
+				throw "Unknown node " + n;
+		}
+		for( n in x.x.attributes() ) {
+			var v = x.x.get(n);
+			switch( n.toLowerCase() ) {
+			case "class":
+				for( cl in v.split(" ") ) {
+					var cl = StringTools.trim(cl);
+					if( cl.length > 0 ) c.addClass(cl);
+				}
+			case "id":
+				c.id = v;
+			case "value":
+				switch( c.name ) {
+				case "slider":
+					var c : Slider = cast c;
+					c.value = Std.parseFloat(v);
+				case "input":
+					var c : Input = cast c;
+					c.value = v;
+				case "color":
+					var c : Color = cast c;
+					c.value = Std.parseInt(v);
+				case "value":
+					var c : Value = cast c;
+					c.value = Std.parseFloat(v);
+				default:
+				}
+			case "onchange":
+				switch( c.name ) {
+				case "slider":
+					var c : Slider = cast c;
+					var s = makeScript(c,v);
+					c.onChange = function(_) s();
+				case "checkbox":
+					var c : Checkbox = cast c;
+					var s = makeScript(c,v);
+					c.onChange = function(_) s();
+				case "itemlist":
+					var c : ItemList = cast c;
+					var s = makeScript(c,v);
+					c.onChange = function(_) s();
+				case "input":
+					var c : Input = cast c;
+					var s = makeScript(c,v);
+					c.onChange = function(_) s();
+				case "color":
+					var c : Color = cast c;
+					var s = makeScript(c,v);
+					c.onChange = function(_) s();
+				case "select":
+					var c : Select = cast c;
+					var s = makeScript(c,v);
+					c.onChange = function(_) s();
+				case "value":
+					var c : Value = cast c;
+					var s = makeScript(c,v);
+					c.onChange = function(_) s();
+				default:
+				}
+			case "onblur":
+				switch( c.name ) {
+				case "input":
+					var c : Input = cast c;
+					c.onBlur = makeScript(c,v);
+				default:
+				}
+			case "onfocus":
+				switch( c.name ) {
+				case "input":
+					var c : Input = cast c;
+					c.onFocus = makeScript(c,v);
+				default:
+				}
+			case "style":
+				var s = new h2d.css.Style();
+				new h2d.css.Parser().parse(v, s);
+				c.setStyle(s);
+			case "selected":
+				switch( c.name ) {
+				case "itemlist":
+					var c : ItemList = cast c;
+					c.selected = Std.parseInt(v);
+				default:
+				}
+			case "checked":
+				switch( c.name ) {
+				case "checkbox":
+					var c : Checkbox = cast c;
+					c.checked = v != "false";
+				default:
+				}
+			case "x":
+				c.x = Std.parseFloat(v);
+			case "y":
+				c.y = Std.parseFloat(v);
+			case "min":
+				switch( c.name ) {
+				case "slider":
+					var c : Slider = cast c;
+					c.minValue = Std.parseFloat(v);
+				default:
+				}
+			case "max":
+				switch( c.name ) {
+				case "slider":
+					var c : Slider = cast c;
+					c.maxValue = Std.parseFloat(v);
+				default:
+				}
+			case "increment":
+				switch( c.name ) {
+				case "value":
+					Std.instance(c, Value).increment = Std.parseFloat(v);
+				default:
+				}
+			case "onmouseover":
+				var int = Std.instance(c, Interactive);
+				if( int != null )
+					int.onMouseOver = makeScript(c, v);
+			case "onmouseout":
+				var int = Std.instance(c, Interactive);
+				if( int != null )
+					int.onMouseOut = makeScript(c, v);
+			case "onmousedown":
+				var int = Std.instance(c, Interactive);
+				if( int != null )
+					int.onMouseDown = makeScript(c, v);
+			case "onmouseup":
+				var int = Std.instance(c, Interactive);
+				if( int != null )
+					int.onMouseUp = makeScript(c, v);
+			case "onclick":
+				var int = Std.instance(c, Interactive);
+				if( int != null )
+					int.onClick = makeScript(c, v);
+			case "disabled":
+				if( v != "false" )
+					c.addClass(":disabled");
+			case n:
+				throw "Unknown attrib " + n;
+			}
+		}
+		for( e in x.elements )
+			build(e, c);
+		return c;
+	}
+	
+	public function register(name, make) {
+		this.comps.set(name, make);
+	}
+	
+	function makeScript( c : Component, script : String ) {
+		#if hscript
+		var p = new hscript.Parser();
+		p.identChars += "$";
+		var e = null;
+		try {
+			e = p.parseString(script);
+		} catch( e : hscript.Expr.Error ) {
+			throw "Invalid Script line " + p.line + " (" + e+ ")";
+		}
+		var i = new CustomInterp();
+		i.variables.set("api", api);
+		i.variables.set("this", c);
+		i.variables.set("$", function(rq) return new h2d.comp.JQuery(c,rq));
+		return function() try i.execute(e) catch( e : String ) throw "Error while running script " + script + " (" + e + ")" catch( e : hscript.Expr.Error ) throw "Error while running script " + script + " (" + e + ")" ;
+		#else
+		return function() throw "Please compile with -lib hscript to get script access";
+		#end
+	}
+	
+	public static function fromHtml( html : String, ?api : {} ) : Component {
+		function lookupBody(x:Xml) {
+			if( x.nodeType == Xml.Element && x.nodeName.toLowerCase() == "body" )
+				return x;
+			if( x.nodeType == Xml.PCData )
+				return null;
+			for( e in x ) {
+				var v = lookupBody(e);
+				if( v != null ) return v;
+			}
+			return null;
+		}
+		var x = Xml.parse(html);
+		var body = lookupBody(x);
+		if( body == null ) body = x;
+		return new Parser(api).build(new haxe.xml.Fast(body),null);
+	}
+	
+}

+ 138 - 0
h2d/comp/Select.hx

@@ -0,0 +1,138 @@
+package h2d.comp;
+
+class Select extends Interactive {
+
+	var tf : h2d.Text;
+	var options : Array<{ label : String, value : Null<String> }>;
+	var list : ItemList;
+	public var value(default, null) : String;
+	public var selectedIndex(default,set) : Int;
+	
+	public function new(?parent) {
+		super("select", parent);
+		tf = new h2d.Text(null, this);
+		options = [];
+		selectedIndex = 0;
+	}
+
+	override function onClick() {
+		popup();
+	}
+	
+	public function getOptions() {
+		return options.copy();
+	}
+	
+	public function popup() {
+		if( list != null )
+			return;
+		var p : Component = this;
+		while( p.parentComponent != null )
+			p = p.parentComponent;
+		list = new ItemList();
+		p.addChild(list);
+		list.addClass("popup");
+		list.evalStyle();
+		for( o in options )
+			new Label(o.label, list);
+		updateListPos();
+		list.selected = this.selectedIndex;
+		list.onChange = function(i) {
+			this.selectedIndex = i;
+			needRebuild = true;
+			close();
+			this.onChange(value);
+		};
+		var scene = getScene();
+		scene.startDrag(function(e) {
+			if( e.kind == ERelease ) {
+				scene.stopDrag();
+				close();
+			}
+		},close);
+	}
+	
+	public function close() {
+		list.remove();
+		list = null;
+		getScene().stopDrag();
+	}
+	
+	public dynamic function onChange( value : String ) {
+	}
+
+	function set_selectedIndex(i) {
+		var o = options[i];
+		value = o == null ? "" : (o.value == null ? o.label : o.value);
+		return selectedIndex = i;
+	}
+	
+	public function setValue(v) {
+		selectedIndex = -1;
+		for( i in 0...options.length )
+			if( options[i].value == v ) {
+				selectedIndex = i;
+				break;
+			}
+		if( selectedIndex < 0 ) {
+			for( i in 0...options.length )
+				if( options[i].label == v ) {
+					selectedIndex = i;
+					break;
+				}
+		}
+		return value;
+	}
+	
+	
+	function updateListPos() {
+		var scene = getScene();
+		var s = new h2d.css.Style();
+		var pos = localToGlobal();
+		s.offsetX = pos.x - extLeft();
+		s.offsetY = pos.y - extTop();
+		s.width = contentWidth + style.paddingLeft + style.paddingRight - (list.style.paddingLeft + list.style.paddingRight);
+		var yMargin = (list.style.paddingBottom + list.style.paddingTop) * 0.5;
+		var xMargin = (list.style.paddingLeft + list.style.paddingRight) * 0.5;
+		var maxY = (scene != null ? scene.height : h3d.Engine.getCurrent().height) - (list.height + yMargin);
+		var maxX = (scene != null ? scene.width : h3d.Engine.getCurrent().width) - (list.width + xMargin);
+		if( s.offsetX > maxX )
+			s.offsetX = maxX;
+		if( s.offsetX < xMargin )
+			s.offsetX = xMargin;
+		if( s.offsetY > maxY )
+			s.offsetY = maxY;
+		if( s.offsetY < yMargin )
+			s.offsetY = yMargin;
+		if( list.customStyle == null || s.offsetX != list.customStyle.offsetX || s.offsetY != list.customStyle.offsetY || s.width != list.customStyle.width )
+			list.setStyle(s);
+	}
+	
+	public function clear() {
+		options = [];
+		needRebuild = true;
+		selectedIndex = 0;
+	}
+	
+	public function addOption(label, ?value) {
+		options.push( { label : label, value : value } );
+		needRebuild = true;
+		if( selectedIndex == options.length - 1 )
+			selectedIndex = selectedIndex; // update value
+	}
+
+	override function resize( ctx : Context ) {
+		if( ctx.measure ) {
+			tf.font = getFont();
+			tf.textColor = style.color;
+			tf.text = options[selectedIndex] == null ? "" : options[selectedIndex].label;
+			tf.filter = true;
+			contentWidth = tf.textWidth;
+			contentHeight = tf.textHeight;
+		}
+		super.resize(ctx);
+		if( !ctx.measure && list != null )
+			updateListPos();
+	}
+	
+}

+ 77 - 0
h2d/comp/Slider.hx

@@ -0,0 +1,77 @@
+package h2d.comp;
+
+class Slider extends Component {
+	
+	var cursor : Button;
+	var input : h2d.Interactive;
+	public var minValue : Float = 0.;
+	public var maxValue : Float = 1.;
+	public var value(default, set) : Float;
+	
+	@:access(h2d.comp.Button)
+	public function new(?parent) {
+		super("slider", parent);
+		cursor = new Button("", this);
+		cursor.input.blockEvents = false;
+		cursor.onMouseDown = function() {
+			
+		};
+		input = new h2d.Interactive(0, 0, this);
+		input.onPush = function(e) {
+			gotoValue(pixelToVal(e));
+			input.startDrag(function(e) {
+				if( e.kind == EMove )
+					gotoValue(pixelToVal(e));
+			});
+		};
+		input.onRelease = function(_) {
+			input.stopDrag();
+		}
+		value = 0.;
+	}
+	
+	function pixelToVal( e : hxd.Event ) {
+		return (Std.int(e.relX - (style.borderSize + cursor.width * 0.5) ) / (input.width - (style.borderSize * 2 + cursor.width))) * (maxValue - minValue) + minValue;
+	}
+	
+	function gotoValue( v : Float ) {
+		if( v < minValue ) v = minValue;
+		if( v > maxValue ) v = maxValue;
+		if( v == value )
+			return;
+		var dv = Math.abs(value - v);
+		if( style.maxIncrement != null && dv > style.maxIncrement ) {
+			if( v > value )
+				value += style.maxIncrement;
+			else
+				value -= style.maxIncrement;
+		} else if( style.increment != null )
+			value = Math.round(v / style.increment) * style.increment;
+		else
+			value = v;
+		onChange(value);
+	}
+	
+	function set_value(v:Float) {
+		if( v < minValue ) v = minValue;
+		if( v > maxValue ) v = maxValue;
+		value = v;
+		needRebuild = true;
+		return v;
+	}
+
+	override function resize( ctx : Context ) {
+		super.resize(ctx);
+		if( !ctx.measure ) {
+			input.width = width - (style.marginLeft + style.marginRight) + cursor.width;
+			input.height = cursor.height - (cursor.style.marginTop + cursor.style.marginBottom);
+			input.x = cursor.style.marginLeft - style.borderSize - cursor.width * 0.5;
+			input.y = cursor.style.marginTop;
+			cursor.style.offsetX = contentWidth * (value - minValue) / (maxValue - minValue) - cursor.width * 0.5;
+		}
+	}
+
+	public dynamic function onChange( value : Float ) {
+	}
+
+}

+ 67 - 0
h2d/comp/Value.hx

@@ -0,0 +1,67 @@
+package h2d.comp;
+
+class Value extends Interactive {
+
+	var text : Input;
+	public var value(default, set) : Float;
+	public var increment : Float;
+	
+	public function new(?parent) {
+		super("value", parent);
+		text = new Input(this);
+		text.input.cursor = Move;
+		text.onChange = function(v) {
+			var v = Std.parseFloat(v);
+			if( !Math.isNaN(v) ) {
+				var old = text;
+				text = null;
+				value = v;
+				text = old;
+				onChange(value);
+			}
+		};
+		text.input.onPush = function(e1) {
+			text.active = true;
+			if( text.hasClass(":focus") )
+				return;
+			var startVal = value;
+			text.input.startDrag(function(e) {
+				if( e.kind == ERelease )
+					text.input.stopDrag();
+				else {
+					var dx = Math.round(e.relX - e1.relX);
+					value = startVal + dx * increment;
+					onChange(value);
+				}
+			});
+		};
+		text.onFocus = function() {
+			text.input.stopDrag();
+			text.input.cursor = TextInput;
+		};
+		text.onBlur = function() {
+			value = value;
+			text.input.cursor = Move;
+		};
+		value = 0;
+		increment = 0.1;
+	}
+	
+	function set_value(v:Float) {
+		if( text != null ) text.value = ""+hxd.Math.fmt(v);
+		return value = v;
+	}
+	
+	override function resize( ctx : Context ) {
+		if( ctx.measure ) {
+			text.resize(ctx);
+			contentWidth = text.width;
+			contentHeight = text.height;
+		}
+		super.resize(ctx);
+	}
+	
+	public dynamic function onChange( value : Float ) {
+	}
+	
+}

+ 45 - 0
h2d/css/Defs.hx

@@ -0,0 +1,45 @@
+package h2d.css;
+
+enum Unit {
+	Pix( v : Float );
+	Percent( v : Float );
+	EM( v : Float );
+}
+
+enum FillStyle {
+	Transparent;
+	Color( c : Int );
+	Gradient( a : Int, b : Int, c : Int, d : Int );
+}
+
+enum Layout {
+	Horizontal;
+	Vertical;
+	Absolute;
+	Dock;
+	Inline;
+}
+
+enum DockStyle {
+	Top;
+	Left;
+	Right;
+	Bottom;
+	Full;
+}
+
+enum TextAlign {
+	Left;
+	Right;
+	Center;
+}
+
+class CssClass {
+	public var parent : Null<CssClass>;
+	public var node : Null<String>;
+	public var className : Null<String>;
+	public var pseudoClass : Null<String>;
+	public var id : Null<String>;
+	public function new() {
+	}
+}

+ 105 - 0
h2d/css/Engine.hx

@@ -0,0 +1,105 @@
+package h2d.css;
+import h2d.css.Defs;
+
+class Rule {
+	public var id : Int;
+	public var c : CssClass;
+	public var priority : Int;
+	public var s : Style;
+	public function new() {
+	}
+}
+
+@:access(h2d.comp.Component)
+class Engine {
+
+	var rules : Array<Rule>;
+
+	public function new() {
+		rules = [];
+	}
+
+	public function applyClasses( c : h2d.comp.Component ) {
+		var s = new Style();
+		c.style = s;
+		var rules = [];
+		for( r in this.rules ) {
+			if( !ruleMatch(r.c, c) )
+				continue;
+			rules.push(r);
+		}
+		rules.sort(sortByPriority);
+		for( r in rules )
+			s.apply(r.s);
+		if( c.customStyle != null )
+			s.apply(c.customStyle);
+	}
+
+	function sortByPriority(r1:Rule, r2:Rule) {
+		var dp = r1.priority - r2.priority;
+		return dp == 0 ? r1.id - r2.id : dp;
+	}
+
+	public static function ruleMatch( c : CssClass, d : h2d.comp.Component ) {
+		if( c.pseudoClass != null ) {
+			var pc = ":" + c.pseudoClass;
+			var found = false;
+			for( cc in d.classes )
+				if( cc == pc ) {
+					found = true;
+					break;
+				}
+			if( !found )
+				return false;
+		}
+		if( c.className != null ) {
+			if( d.classes == null )
+				return false;
+			var found = false;
+			for( cc in d.classes )
+				if( cc == c.className ) {
+					found = true;
+					break;
+				}
+			if( !found )
+				return false;
+		}
+		if( c.node != null && c.node != d.name )
+			return false;
+		if( c.id != null && c.id != d.id )
+			return false;
+		if( c.parent != null ) {
+			var p = d.parentComponent;
+			while( p != null ) {
+				if( ruleMatch(c.parent, p) )
+					break;
+				p = p.parentComponent;
+			}
+			if( p == null )
+				return false;
+		}
+		return true;
+	}
+
+	public function addRules( text : String ) {
+		for( r in new Parser().parseRules(text) ) {
+			var c = r.c;
+			var imp = r.imp ? 1 : 0;
+			var nids = 0, nothers = 0, nnodes = 0;
+			while( c != null ) {
+				if( c.id != null ) nids++;
+				if( c.node != null ) nnodes++;
+				if( c.pseudoClass != null ) nothers++;
+				if( c.className != null ) nothers++;
+				c = c.parent;
+			}
+			var rule = new Rule();
+			rule.id = rules.length;
+			rule.c = r.c;
+			rule.s = r.s;
+			rule.priority = (imp << 24) | (nids << 16) | (nothers << 8) | nnodes;
+			rules.push(rule);
+		}
+	}
+
+}

+ 63 - 0
h2d/css/Fill.hx

@@ -0,0 +1,63 @@
+package h2d.css;
+import h2d.css.Defs;
+
+class Fill extends h2d.TileColorGroup {
+
+	public function new(?parent) {
+		super(h2d.Tile.fromColor(0xFFFFFFFF), parent);
+	}
+	
+	public inline function fillRectColor(x, y, w, h, c) {
+		content.rectColor(x, y, w, h, c);
+	}
+
+	public inline function fillRectGradient(x, y, w, h, ctl, ctr, cbl, cbr) {
+		content.rectGradient(x, y, w, h, ctl, ctr, cbl, cbr);
+	}
+	
+	public inline function addPoint(x, y, color) {
+		content.addPoint(x, y, color);
+	}
+
+	public function fillRect(fill:FillStyle,x,y,w,h) {
+		switch( fill ) {
+		case Transparent:
+		case Color(c):
+			fillRectColor(x,y,w,h,c);
+		case Gradient(a,b,c,d):
+			fillRectGradient(x,y,w,h,a,b,c,d);
+		}
+	}
+
+	inline function clerp(c1:Int,c2:Int,v:Float) {
+		var a = Std.int( (c1>>>24) * (1-v) + (c2>>>24) * v );
+		var r = Std.int( ((c1>>16)&0xFF) * (1-v) + ((c2>>16)&0xFF) * v );
+		var g = Std.int( ((c1>>8)&0xFF) * (1-v) + ((c2>>8)&0xFF) * v );
+		var b = Std.int( (c1&0xFF) * (1-v) + (c2&0xFF) * v );
+		return (a << 24) | (r << 16) | (g << 8) | b;
+	}
+
+	public function lineRect(fill:FillStyle, x:Float, y:Float, w:Float, h:Float, size:Float) {
+		if( size <= 0 ) return;
+		switch( fill ) {
+		case Transparent:
+		case Color(c):
+			fillRectColor(x,y,w,size,c);
+			fillRectColor(x,y+h-size,w,size,c);
+			fillRectColor(x,y+size,size,h-size*2,c);
+			fillRectColor(x+w-size,y+size,size,h-size*2,c);
+		case Gradient(a,b,c,d):
+			var px = size / w;
+			var py = size / h;
+			var a2 = clerp(a,c,py);
+			var b2 = clerp(b,d,py);
+			var c2 = clerp(a,c,1-py);
+			var d2 = clerp(b,d,1-py);
+			fillRectGradient(x,y,w,size,a,b,a2,b2);
+			fillRectGradient(x,y+h-size,w,size,c2,d2,c,d);
+			fillRectGradient(x,y+size,size,h-size*2,a2,clerp(a2,b2,px),c2,clerp(c2,d2,px));
+			fillRectGradient(x+w-size,y+size,size,h-size*2,clerp(a2,b2,1-px),b2,clerp(c2,d2,1-px),d2);
+		}
+	}
+
+}

+ 1016 - 0
h2d/css/Parser.hx

@@ -0,0 +1,1016 @@
+package h2d.css;
+import h2d.css.Defs;
+
+enum Token {
+	TIdent( i : String );
+	TString( s : String );
+	TInt( i : Int );
+	TFloat( f : Float );
+	TDblDot;
+	TSharp;
+	TPOpen;
+	TPClose;
+	TExclam;
+	TComma;
+	TEof;
+	TPercent;
+	TSemicolon;
+	TBrOpen;
+	TBrClose;
+	TDot;
+	TSpaces;
+	TSlash;
+	TStar;
+}
+
+enum Value {
+	VIdent( i : String );
+	VString( s : String );
+	VUnit( v : Float, unit : String );
+	VFloat( v : Float );
+	VInt( v : Int );
+	VHex( v : String );
+	VList( l : Array<Value> );
+	VGroup( l : Array<Value> );
+	VCall( f : String, vl : Array<Value> );
+	VLabel( v : String, val : Value );
+	VSlash;
+}
+
+class Parser {
+
+	var css : String;
+	var s : Style;
+	var simp : Style;
+	var pos : Int;
+
+	var spacesTokens : Bool;
+	var tokens : Array<Token>;
+
+	public function new() {
+	}
+
+
+	// ----------------- style apply ---------------------------
+
+	#if debug
+	function notImplemented( ?pos : haxe.PosInfos ) {
+		haxe.Log.trace("Not implemented", pos);
+	}
+	#else
+	inline function notImplemented() {
+	}
+	#end
+
+	function applyStyle( r : String, v : Value, s : Style ) : Bool {
+		switch( r ) {
+		case "padding":
+			switch( v ) {
+			case VGroup([a, b]):
+				var a = getVal(a), b = getVal(b);
+				if( a != null && b != null ) {
+					s.paddingTop = s.paddingBottom = a;
+					s.paddingLeft = s.paddingRight = b;
+					return true;
+				}
+			default:
+				var i = getVal(v);
+				if( i != null ) { s.padding(i); return true; }
+			}
+		case "padding-top":
+			var i = getVal(v);
+			if( i != null ) { s.paddingTop = i; return true; }
+		case "padding-left":
+			var i = getVal(v);
+			if( i != null ) { s.paddingLeft = i; return true; }
+		case "padding-right":
+			var i = getVal(v);
+			if( i != null ) { s.paddingRight = i; return true; }
+		case "padding-bottom":
+			var i = getVal(v);
+			if( i != null ) { s.paddingBottom = i; return true; }
+		case "margin":
+			switch( v ) {
+			case VGroup([a, b]):
+				var a = getVal(a), b = getVal(b);
+				if( a != null && b != null ) {
+					s.marginTop = s.marginBottom = a;
+					s.marginLeft = s.marginRight = b;
+					return true;
+				}
+			default:
+				var i = getVal(v);
+				if( i != null ) { s.margin(i); return true; }
+			}
+		case "margin-top":
+			var i = getVal(v);
+			if( i != null ) { s.marginTop = i; return true; }
+		case "margin-left":
+			var i = getVal(v);
+			if( i != null ) { s.marginLeft = i; return true; }
+		case "margin-right":
+			var i = getVal(v);
+			if( i != null ) { s.marginRight = i; return true; }
+		case "margin-bottom":
+			var i = getVal(v);
+			if( i != null ) { s.marginBottom = i; return true; }
+		case "width":
+			var i = getVal(v);
+			if( i != null ) {
+				s.width = i;
+				return true;
+			}
+			if( getIdent(v) == "auto" ) {
+				s.width = null;
+				s.autoWidth = true;
+				return true;
+			}
+		case "height":
+			var i = getVal(v);
+			if( i != null ) {
+				s.height = i;
+				return true;
+			}
+			if( getIdent(v) == "auto" ) {
+				s.height = null;
+				s.autoHeight = true;
+				return true;
+			}
+		case "background-color":
+			var f = getFill(v);
+			if( f != null ) {
+				s.backgroundColor = f;
+				return true;
+			}
+		case "background":
+			return applyComposite(["background-color"], v, s);
+		case "font-family":
+			var l = getFontName(v);
+			if( l != null ) {
+				s.fontName = l;
+				return true;
+			}
+		case "font-size":
+			var i = getUnit(v);
+			if( i != null ) {
+				switch( i ) {
+				case Pix(v):
+					s.fontSize = v;
+				default:
+					notImplemented();
+				}
+				return true;
+			}
+		case "color":
+			var c = getCol(v);
+			if( c != null ) {
+				s.color = c;
+				return true;
+			}
+		case "border":
+			if( applyComposite(["border-width", "border-style", "border-color"], v, s) )
+				return true;
+			if( getIdent(v) == "none" ) {
+				s.borderSize = 0;
+				s.borderColor = Transparent;
+				return true;
+			}
+		case "border-width":
+			var i = getVal(v);
+			if( i != null ) {
+				s.borderSize = i;
+				return true;
+			}
+		case "border-style":
+			if( getIdent(v) == "solid" )
+				return true;
+		case "border-color":
+			var c = getFill(v);
+			if( c != null ) {
+				s.borderColor = c;
+				return true;
+			}
+		case "offset":
+			return applyComposite(["offset-x", "offset-y"], v, s);
+		case "offset-x":
+			var i = getVal(v);
+			if( i != null ) {
+				s.offsetX = i;
+				return true;
+			}
+		case "offset-y":
+			var i = getVal(v);
+			if( i != null ) {
+				s.offsetY = i;
+				return true;
+			}
+		case "layout":
+			var i = mapIdent(v, [Horizontal, Vertical, Absolute, Dock, Inline]);
+			if( i != null ) {
+				s.layout = i;
+				return true;
+			}
+		case "spacing":
+			return applyComposite(["vertical-spacing", "horizontal-spacing"], v, s);
+		case "horizontal-spacing":
+			var i = getVal(v);
+			if( i != null ) {
+				s.horizontalSpacing = i;
+				return true;
+			}
+		case "vertical-spacing":
+			var i = getVal(v);
+			if( i != null ) {
+				s.verticalSpacing = i;
+				return true;
+			}
+		case "increment":
+			var i = getVal(v);
+			if( i != null ) {
+				s.increment = i;
+				return true;
+			}
+		case "max-increment":
+			var i = getVal(v);
+			if( i != null ) {
+				s.maxIncrement = i;
+				return true;
+			}
+		case "tick-color":
+			var i = getFill(v);
+			if( i != null ) {
+				s.tickColor = i;
+				return true;
+			}
+		case "tick-spacing":
+			var i = getVal(v);
+			if( i != null ) {
+				s.tickSpacing = i;
+				return true;
+			}
+		case "dock":
+			var i = mapIdent(v, [Top, Bottom, Left, Right, Full]);
+			if( i != null ) {
+				s.dock = i;
+				return true;
+			}
+		case "cursor-color":
+			var i = getColAlpha(v);
+			if( i != null ) {
+				s.cursorColor = i;
+				return true;
+			}
+		case "selection-color":
+			var i = getColAlpha(v);
+			if( i != null ) {
+				s.selectionColor = i;
+				return true;
+			}
+		case "overflow":
+			switch( getIdent(v) ) {
+			case "hidden":
+				s.overflowHidden = true;
+				return true;
+			case "visible":
+				s.overflowHidden = false;
+				return true;
+			}
+		case "icon":
+			var i = getImage(v);
+			if( i != null ) {
+				s.icon = i;
+				return true;
+			}
+		case "icon-color":
+			var c = getColAlpha(v);
+			if( c != null ) {
+				s.iconColor = c;
+				return true;
+			}
+		case "icon-left":
+			var i = getVal(v);
+			if( i != null ) {
+				s.iconLeft = i;
+				return true;
+			}
+		case "icon-top":
+			var i = getVal(v);
+			if( i != null ) {
+				s.iconTop = i;
+				return true;
+			}
+		case "position":
+			switch( getIdent(v) ) {
+			case "absolute":
+				s.positionAbsolute = true;
+				return true;
+			case "relative":
+				s.positionAbsolute = false;
+				return true;
+			default:
+			}
+		case "text-align":
+			switch( getIdent(v) ) {
+			case "left":
+				s.textAlign = Left;
+				return true;
+			case "right":
+				s.textAlign = Right;
+				return true;
+			case "center":
+				s.textAlign = Center;
+				return true;
+			default:
+			}
+		case "display":
+			switch( getIdent(v) ) {
+			case "none":
+				s.display = false;
+				return true;
+			case "block", "inline-block":
+				s.display = true;
+				return true;
+			default:
+			}
+		default:
+			throw "Not implemented '"+r+"' = "+valueStr(v);
+		}
+		return false;
+	}
+
+	function applyComposite( names : Array<String>, v : Value, s : Style ) {
+		var vl = switch( v ) {
+		case VGroup(l): l;
+		default: [v];
+		};
+		while( vl.length > 0 ) {
+			var found = false;
+			for( n in names ) {
+				var count = 1;
+				if( count > vl.length ) count = vl.length;
+				while( count > 0 ) {
+					var v = (count == 1) ? vl[0] : VGroup(vl.slice(0, count));
+					if( applyStyle(n, v, s) ) {
+						found = true;
+						names.remove(n);
+						for( i in 0...count )
+							vl.shift();
+						break;
+					}
+					count--;
+				}
+				if( found ) break;
+			}
+			if( !found )
+				return false;
+		}
+		return true;
+	}
+
+	function getGroup<T>( v : Value, f : Value -> Null<T> ) : Null<Array<T>> {
+		switch(v) {
+		case VGroup(l):
+			var a = [];
+			for( v in l ) {
+				var v = f(v);
+				if( v == null ) return null;
+				a.push(v);
+			}
+			return a;
+		default:
+			var v = f(v);
+			return (v == null) ? null : [v];
+		}
+	}
+
+	function getList<T>( v : Value, f : Value -> Null<T> ) : Null<Array<T>> {
+		switch(v) {
+		case VList(l):
+			var a = [];
+			for( v in l ) {
+				var v = f(v);
+				if( v == null ) return null;
+				a.push(v);
+			}
+			return a;
+		default:
+			var v = f(v);
+			return (v == null) ? null : [v];
+		}
+	}
+
+	function getInt( v : Value ) : Null<Int> {
+		return switch( v ) {
+		case VUnit(f, u):
+			switch( u ) {
+			case "px": Std.int(f);
+			case "pt": Std.int(f * 4 / 3);
+			default: null;
+			}
+		case VInt(v):
+			Std.int(v);
+		default:
+			null;
+		};
+	}
+
+	function getVal( v : Value ) : Null<Float> {
+		return switch( v ) {
+		case VUnit(f, u):
+			switch( u ) {
+			case "px": f;
+			case "pt": f * 4 / 3;
+			default: null;
+			}
+		case VInt(v):
+			v;
+		case VFloat(v):
+			v;
+		default:
+			null;
+		};
+	}
+
+	function getUnit( v : Value ) : Null<Unit> {
+		return switch( v ) {
+		case VUnit(f, u):
+			switch( u ) {
+			case "px": Pix(f);
+			case "pt": Pix(f * 4 / 3);
+			case "%": Percent(f / 100);
+			default: null;
+			}
+		case VInt(v):
+			Pix(v);
+		case VFloat(v):
+			Pix(v);
+		default:
+			null;
+		};
+	}
+	
+	function mapIdent<T:EnumValue>( v : Value, vals : Array<T> ) : T {
+		var i = getIdent(v);
+		if( i == null ) return null;
+		for( v in vals )
+			if( v.getName().toLowerCase() == i )
+				return v;
+		return null;
+	}
+
+	function getIdent( v : Value ) : Null<String> {
+		return switch( v ) {
+		case VIdent(v): v;
+		default: null;
+		};
+	}
+
+	function getColAlpha( v : Value ) {
+		var c = getCol(v);
+		if( c != null && c >>> 24 == 0 )
+			c |= 0xFF000000;
+		return c;
+	}
+
+	function getFill( v : Value ) {
+		var c = getColAlpha(v);
+		if( c != null )
+			return Color(c);
+		switch( v ) {
+		case VCall("gradient", [a, b, c, d]):
+			var ca = getColAlpha(a);
+			var cb = getColAlpha(b);
+			var cc = getColAlpha(c);
+			var cd = getColAlpha(d);
+			if( ca != null && cb != null && cc != null && cd != null )
+				return Gradient(ca, cb, cc, cd);
+		case VIdent("transparent"):
+			return Transparent;
+		default:
+		}
+		return null;
+	}
+
+	function getCol( v : Value ) : Null<Int> {
+		return switch( v ) {
+		case VHex(v):
+			(v.length == 6) ? Std.parseInt("0x" + v) : ((v.length == 3) ? Std.parseInt("0x"+v.charAt(0)+v.charAt(0)+v.charAt(1)+v.charAt(1)+v.charAt(2)+v.charAt(2)) : null);
+		case VIdent(i):
+			switch( i ) {
+			case "black":	0x000000;
+			case "red": 	0xFF0000;
+			case "lime":	0x00FF00;
+			case "blue":	0x0000FF;
+			case "white":	0xFFFFFF;
+			case "aqua":	0x00FFFF;
+			case "fuchsia":	0xFF00FF;
+			case "yellow":	0xFFFF00;
+			case "maroon":	0x800000;
+			case "green":	0x008000;
+			case "navy":	0x000080;
+			case "olive":	0x808000;
+			case "purple": 	0x800080;
+			case "teal":	0x008080;
+			case "silver":	0xC0C0C0;
+			case "gray", "grey": 0x808080;
+			default: null;
+			}
+		case VCall("rgba", [r, g, b, a]):
+			var r = getVal(r), g = getVal(g), b = getVal(b), a = getVal(a);
+			inline function conv(k:Float) {
+				var v = Std.int(k * 255);
+				if( v < 0 ) v = 0;
+				if( v > 255 ) v = 255;
+				return v;
+			}
+			if( r != null && g != null && b != null && a != null ) {
+				var a = conv(a); if( a == 0 ) a = 1; // prevent setting alpha to FF afterwards
+				(a << 24) | (conv(r) << 16) | (conv(g) << 8) | conv(b);
+			}
+			else
+				null;
+		default:
+			null;
+		};
+	}
+
+	function getFontName( v : Value ) {
+		return switch( v ) {
+		case VString(s): s;
+		case VGroup(_):
+			var g = getGroup(v, getIdent);
+			if( g == null ) null else g.join(" ");
+		case VIdent(i): i;
+		default: null;
+		};
+	}
+	
+	function getImage( v : Value ) {
+		switch( v ) {
+		case VCall("url", [VString(url)]):
+			if( !StringTools.startsWith(url, "data:image/png;base64,") )
+				return null;
+			url = url.substr(22);
+			if( StringTools.endsWith(url, "=") ) url = url.substr(0, -1);
+			var bytes = haxe.crypto.Base64.decode(url);
+			return hxd.res.Any.fromBytes("icon",bytes).toImage().getPixels();
+		default:
+			return null;
+		}
+	}
+
+	// ---------------------- generic parsing --------------------
+
+	function unexpected( t : Token ) : Dynamic {
+		throw "Unexpected " + Std.string(t);
+		return null;
+	}
+
+	function expect( t : Token ) {
+		var tk = readToken();
+		if( tk != t ) unexpected(tk);
+	}
+
+	inline function push( t : Token ) {
+		tokens.push(t);
+	}
+
+	function isToken(t) {
+		var tk = readToken();
+		if( tk == t ) return true;
+		push(tk);
+		return false;
+	}
+
+	public function parse( css : String, s : Style ) {
+		this.css = css;
+		this.s = s;
+		pos = 0;
+		tokens = [];
+		parseStyle(TEof);
+	}
+
+	function valueStr(v) {
+		return switch( v ) {
+		case VIdent(i): i;
+		case VString(s): '"' + s + '"';
+		case VUnit(f, unit): f + unit;
+		case VFloat(f): Std.string(f);
+		case VInt(v): Std.string(v);
+		case VHex(v): "#" + v;
+		case VList(l):
+			[for( v in l ) valueStr(v)].join(", ");
+		case VGroup(l):
+			[for( v in l ) valueStr(v)].join(" ");
+		case VCall(f,args): f+"(" + [for( v in args ) valueStr(v)].join(", ") + ")";
+		case VLabel(label, v): valueStr(v) + " !" + label;
+		case VSlash: "/";
+		}
+	}
+
+	function parseStyle( eof ) {
+		while( true ) {
+			if( isToken(eof) )
+				break;
+			var r = readIdent();
+			expect(TDblDot);
+			var v = readValue();
+			var s = this.s;
+			switch( v ) {
+			case VLabel(label, val):
+				if( label == "important" ) {
+					v = val;
+					if( simp == null ) simp = new Style();
+					s = simp;
+				}
+			default:
+			}
+			if( !applyStyle(r, v, s) )
+				throw "Invalid value " + valueStr(v) + " for css " + r;
+			if( isToken(eof) )
+				break;
+			expect(TSemicolon);
+		}
+	}
+
+	public function parseRules( css : String ) {
+		this.css = css;
+		pos = 0;
+		tokens = [];
+		var rules = [];
+		while( true ) {
+			if( isToken(TEof) )
+				break;
+			var classes = readClasses();
+			expect(TBrOpen);
+			this.s = new Style();
+			this.simp = null;
+			parseStyle(TBrClose);
+			for( c in classes )
+				rules.push( { c : c, s : s, imp : false } );
+			if( this.simp != null )
+				for( c in classes )
+					rules.push( { c : c, s : simp, imp : true } );
+		}
+		return rules;
+	}
+	
+	public function parseClasses( css : String ) {
+		this.css = css;
+		pos = 0;
+		tokens = [];
+		var c = readClasses();
+		expect(TEof);
+		return c;
+	}
+
+	// ----------------- class parser ---------------------------
+
+	function readClasses() {
+		var classes = [];
+		while( true ) {
+			spacesTokens = true;
+			isToken(TSpaces); // skip
+			var c = readClass(null);
+			spacesTokens = false;
+			if( c == null ) break;
+			updateClass(c);
+			classes.push(c);
+			if( !isToken(TComma) )
+				break;
+		}
+		if( classes.length == 0 )
+			unexpected(readToken());
+		return classes;
+	}
+	
+	function updateClass( c : CssClass ) {
+		// map html types to comp ones
+		switch( c.node ) {
+		case "div": c.node = "box";
+		case "span": c.node = "label";
+		case "h1", "h2", "h3", "h4":
+			c.pseudoClass = c.node;
+			c.node = "label";
+		}
+		if( c.parent != null ) updateClass(c.parent);
+	}
+	
+	function readClass( parent ) : CssClass {
+		var c = new CssClass();
+		c.parent = parent;
+		var def = false;
+		var last = null;
+		while( true ) {
+			var t = readToken();
+			if( last == null )
+				switch( t ) {
+				case TStar: def = true;
+				case TDot, TSharp, TDblDot: last = t;
+				case TIdent(i): c.node = i; def = true;
+				case TSpaces:
+					return def ? readClass(c) : null;
+				case TBrOpen, TComma, TEof:
+					push(t);
+					break;
+				default:
+					unexpected(t);
+				}
+			else
+				switch( t ) {
+				case TIdent(i):
+					switch( last ) {
+					case TDot: c.className = i; def = true;
+					case TSharp: c.id = i; def = true;
+					case TDblDot: c.pseudoClass = i; def = true;
+					default: throw "assert";
+					}
+					last = null;
+				default:
+					unexpected(t);
+				}
+		}
+		return def ? c : parent;
+	}
+
+	// ----------------- value parser ---------------------------
+
+	function readIdent() {
+		var t = readToken();
+		return switch( t ) {
+		case TIdent(i): i;
+		default: unexpected(t);
+		}
+	}
+
+	function readValue(?opt)  : Value {
+		var t = readToken();
+		var v = switch( t ) {
+		case TSharp:
+			VHex(readHex());
+		case TIdent(i):
+			VIdent(i);
+		case TString(s):
+			VString(s);
+		case TInt(i):
+			readValueUnit(i, i);
+		case TFloat(f):
+			readValueUnit(f, null);
+		case TSlash:
+			VSlash;
+		default:
+			if( !opt ) unexpected(t);
+			push(t);
+			null;
+		};
+		if( v != null ) v = readValueNext(v);
+		return v;
+	}
+
+	function readHex() {
+		var start = pos;
+		while( true ) {
+			var c = next();
+			if( (c >= "A".code && c <= "F".code) || (c >= "a".code && c <= "f".code) || (c >= "0".code && c <= "9".code) )
+				continue;
+			pos--;
+			break;
+		}
+		return css.substr(start, pos - start);
+	}
+
+	function readValueUnit( f : Float, ?i : Int ) {
+		var t = readToken();
+		return switch( t ) {
+		case TIdent(i):
+			VUnit(f, i);
+		case TPercent:
+			VUnit(f, "%");
+		default:
+			push(t);
+			if( i != null )
+				VInt(i);
+			else
+				VFloat(f);
+		};
+	}
+
+	function readValueNext( v : Value ) : Value {
+		var t = readToken();
+		return switch( t ) {
+		case TPOpen:
+			switch( v ) {
+			case VIdent(i):
+				switch( i ) {
+				case "url":
+					readValueNext(VCall("url",[VString(readUrl())]));
+				default:
+					var args = switch( readValue() ) {
+					case VList(l): l;
+					case x: [x];
+					}
+					expect(TPClose);
+					readValueNext(VCall(i, args));
+				}
+			default:
+				push(t);
+				v;
+			}
+		case TExclam:
+			var t = readToken();
+			switch( t ) {
+			case TIdent(i):
+				VLabel(i, v);
+			default:
+				unexpected(t);
+			}
+		case TComma:
+			loopComma(v, readValue());
+		default:
+			push(t);
+			var v2 = readValue(true);
+			if( v2 == null )
+				v;
+			else
+				loopNext(v, v2);
+		}
+	}
+
+	function loopNext(v, v2) {
+		return switch( v2 ) {
+		case VGroup(l):
+			l.unshift(v);
+			v2;
+		case VList(l):
+			l[0] = loopNext(v, l[0]);
+			v2;
+		case VLabel(lab, v2):
+			VLabel(lab, loopNext(v, v2));
+		default:
+			VGroup([v, v2]);
+		};
+	}
+
+	function loopComma(v,v2) {
+		return switch( v2 ) {
+		case VList(l):
+			l.unshift(v);
+			v2;
+		case VLabel(lab, v2):
+			VLabel(lab, loopComma(v, v2));
+		default:
+			VList([v, v2]);
+		};
+	}
+
+	// ----------------- lexer -----------------------
+
+	inline function isSpace(c) {
+		return (c == " ".code || c == "\n".code || c == "\r".code || c == "\t".code);
+	}
+
+	inline function isIdentChar(c) {
+		return (c >= "a".code && c <= "z".code) || (c >= "A".code && c <= "Z".code) || (c == "-".code) || (c == "_".code);
+	}
+
+	inline function isNum(c) {
+		return c >= "0".code && c <= "9".code;
+	}
+
+	inline function next() {
+		return StringTools.fastCodeAt(css, pos++);
+	}
+
+	function readUrl() {
+		var c0 = next();
+		while( isSpace(c0) )
+			c0 = next();
+		var quote = c0;
+		if( quote == "'".code || quote == '"'.code ) {
+			pos--;
+			switch( readToken() ) {
+			case TString(s):
+				var c0 = next();
+				while( isSpace(c0) )
+					c0 = next();
+				if( c0 != ")".code )
+					throw "Invalid char " + String.fromCharCode(c0);
+				return s;
+			default: throw "assert";
+			}
+
+		}
+		var start = pos - 1;
+		while( true ) {
+			if( StringTools.isEof(c0) )
+				break;
+			c0 = next();
+			if( c0 == ")".code ) break;
+		}
+		return StringTools.trim(css.substr(start, pos - start - 1));
+	}
+
+	#if false
+	function readToken( ?pos : haxe.PosInfos ) {
+		var t = _readToken();
+		haxe.Log.trace(t, pos);
+		return t;
+	}
+
+	function _readToken() {
+	#else
+	function readToken() {
+	#end
+		var t = tokens.pop();
+		if( t != null )
+			return t;
+		while( true ) {
+			var c = next();
+			if( StringTools.isEof(c) )
+				return TEof;
+			if( isSpace(c) ) {
+				if( spacesTokens ) {
+					while( isSpace(next()) ) {
+					}
+					pos--;
+					return TSpaces;
+				}
+
+				continue;
+			}
+			if( isNum(c) || c == '-'.code ) {
+				var i = 0, neg = false;
+				if( c == '-'.code ) { c = "0".code; neg = true; }
+				do {
+					i = i * 10 + (c - "0".code);
+					c = next();
+				} while( isNum(c) );
+				if( c == ".".code ) {
+					var f : Float = i;
+					var k = 0.1;
+					while( isNum(c = next()) ) {
+						f += (c - "0".code) * k;
+						k *= 0.1;
+					}
+					pos--;
+					return TFloat(neg? -f : f);
+				}
+				pos--;
+				return TInt(neg ? -i : i);
+			}
+			if( isIdentChar(c) ) {
+				var pos = pos - 1;
+				do c = next() while( isIdentChar(c) || isNum(c) );
+				this.pos--;
+				return TIdent(css.substr(pos,this.pos - pos));
+			}
+			switch( c ) {
+			case ":".code: return TDblDot;
+			case "#".code: return TSharp;
+			case "(".code: return TPOpen;
+			case ")".code: return TPClose;
+			case "!".code: return TExclam;
+			case "%".code: return TPercent;
+			case ";".code: return TSemicolon;
+			case ".".code: return TDot;
+			case "{".code: return TBrOpen;
+			case "}".code: return TBrClose;
+			case ",".code: return TComma;
+			case "*".code: return TStar;
+			case "/".code:
+				if( (c = next()) != '*'.code ) {
+					pos--;
+					return TSlash;
+				}
+				while( true ) {
+					while( (c = next()) != '*'.code ) {
+						if( StringTools.isEof(c) )
+							throw "Unclosed comment";
+					}
+					c = next();
+					if( c == "/".code ) break;
+					if( StringTools.isEof(c) )
+						throw "Unclosed comment";
+				}
+				return readToken();
+			case "'".code, '"'.code:
+				var pos = pos;
+				var k;
+				while( (k = next()) != c ) {
+					if( StringTools.isEof(k) )
+						throw "Unclosed string constant";
+					if( k == "\\".code ) {
+						throw "todo";
+						continue;
+					}
+				}
+				return TString(css.substr(pos, this.pos - pos - 1));
+			default:
+			}
+			pos--;
+			throw "Invalid char " + css.charAt(pos);
+		}
+		return null;
+	}
+
+}

+ 122 - 0
h2d/css/Style.hx

@@ -0,0 +1,122 @@
+package h2d.css;
+import h2d.css.Defs;
+
+class Style {
+	
+	public var fontName : Null<String>;
+	public var fontSize : Null<Float>;
+	public var color : Null<Int>;
+	public var backgroundColor : Null<FillStyle>;
+	public var borderSize : Null<Float>;
+	public var borderColor : Null<FillStyle>;
+	public var paddingTop : Null<Float>;
+	public var paddingLeft : Null<Float>;
+	public var paddingRight : Null<Float>;
+	public var paddingBottom : Null<Float>;
+	public var width : Null<Float>;
+	public var height : Null<Float>;
+	public var autoWidth : Null<Bool>;
+	public var autoHeight : Null<Bool>;
+	public var offsetX : Null<Float>;
+	public var offsetY : Null<Float>;
+	public var layout : Null<Layout>;
+	public var horizontalSpacing : Null<Float>;
+	public var verticalSpacing : Null<Float>;
+	public var marginTop : Null<Float>;
+	public var marginLeft : Null<Float>;
+	public var marginRight : Null<Float>;
+	public var marginBottom : Null<Float>;
+	public var increment : Null<Float>;
+	public var maxIncrement : Null<Float>;
+	public var tickColor : Null<FillStyle>;
+	public var tickSpacing : Null<Float>;
+	public var dock : Null<DockStyle>;
+	public var cursorColor : Null<Int>;
+	public var selectionColor : Null<Int>;
+	public var overflowHidden : Null<Bool>;
+	public var positionAbsolute : Null<Bool>;
+	public var icon:Null<hxd.Pixels>;
+	public var iconColor : Null<Int>;
+	public var iconLeft : Null<Float>;
+	public var iconTop : Null<Float>;
+	public var textAlign : Null<TextAlign>;
+	public var display : Null<Bool>;
+	
+	public function new() {
+	}
+	
+	public function apply( s : Style ) {
+		if( s.fontName != null ) fontName = s.fontName;
+		if( s.fontSize != null ) fontSize = s.fontSize;
+		if( s.color != null ) color = s.color;
+		if( s.backgroundColor != null ) backgroundColor = s.backgroundColor;
+		if( s.borderSize != null ) borderSize = s.borderSize;
+		if( s.borderColor != null ) borderColor = s.borderColor;
+		if( s.paddingLeft != null ) paddingLeft = s.paddingLeft;
+		if( s.paddingRight != null ) paddingRight = s.paddingRight;
+		if( s.paddingTop != null ) paddingTop = s.paddingTop;
+		if( s.paddingBottom != null ) paddingBottom = s.paddingBottom;
+		if( s.offsetX != null ) offsetX = s.offsetX;
+		if( s.offsetY != null ) offsetY = s.offsetY;
+		if( s.width != null ) width = s.width;
+		if( s.height != null ) height = s.height;
+		if( s.layout != null ) layout = s.layout;
+		if( s.horizontalSpacing != null ) horizontalSpacing = s.horizontalSpacing;
+		if( s.verticalSpacing != null ) verticalSpacing = s.verticalSpacing;
+		if( s.marginLeft != null ) marginLeft = s.marginLeft;
+		if( s.marginRight != null ) marginRight = s.marginRight;
+		if( s.marginTop != null ) marginTop = s.marginTop;
+		if( s.marginBottom != null ) marginBottom = s.marginBottom;
+		if( s.increment != null ) increment = s.increment;
+		if( s.maxIncrement != null ) maxIncrement = s.maxIncrement;
+		if( s.tickColor != null ) tickColor = s.tickColor;
+		if( s.tickSpacing != null ) tickSpacing = s.tickSpacing;
+		if( s.dock != null ) dock = s.dock;
+		if( s.cursorColor != null ) cursorColor = s.cursorColor;
+		if( s.selectionColor != null ) selectionColor = s.selectionColor;
+		if( s.overflowHidden != null ) overflowHidden = s.overflowHidden;
+		if( s.icon != null ) icon = s.icon;
+		if( s.iconColor != null ) iconColor = s.iconColor;
+		if( s.iconLeft != null ) iconLeft = s.iconLeft;
+		if( s.iconTop != null ) iconTop = s.iconTop;
+		if( s.positionAbsolute != null ) positionAbsolute = s.positionAbsolute;
+		if( s.autoWidth != null ) {
+			autoWidth = s.autoWidth;
+			width = s.width;
+		}
+		if( s.autoHeight != null ) {
+			autoHeight = s.autoHeight;
+			height = s.height;
+		}
+		if( s.textAlign != null ) textAlign = s.textAlign;
+		if( s.display != null ) display = s.display;
+	}
+	
+	public function padding( v : Float ) {
+		this.paddingTop = v;
+		this.paddingLeft = v;
+		this.paddingRight = v;
+		this.paddingBottom = v;
+	}
+
+	public function margin( v : Float ) {
+		this.marginTop = v;
+		this.marginLeft = v;
+		this.marginRight = v;
+		this.marginBottom = v;
+	}
+	
+	public function toString() {
+		var fields = [];
+		for( f in Type.getInstanceFields(Style) ) {
+			var v : Dynamic = Reflect.field(this, f);
+			if( v == null || Reflect.isFunction(v) || f == "toString" || f == "apply" )
+				continue;
+			if( f.toLowerCase().indexOf("color") >= 0 && Std.is(v,Int) )
+				v = "#" + StringTools.hex(v, 6);
+			fields.push(f + ": " + v);
+		}
+		return "{" + fields.join(", ") + "}";
+	}
+		
+}

+ 174 - 0
h2d/css/default.css

@@ -0,0 +1,174 @@
+* {
+	font-family : "Arial";
+	font-size : 14px;
+	color : white;
+	offset : 0 0;
+	border : none;
+	background-color : transparent;
+	text-align : left;
+}
+
+div.modal {
+	layout : dock;
+	dock : full;
+	background-color : rgba(0,0,0,0.8);
+}
+
+button, select {
+	background-color : gradient(#434343, #4B4B4B, #383838, #3A3A3A);
+	border : 1px solid gradient(#A0A0A0,#909090,#707070,#606060);
+	padding : 5px;
+}
+
+button:hover {
+	background-color : gradient(#282828,#2A2A2A,#333333,#3B3B3B);
+	border : 1px solid gradient(#606060,#606060,#707070,#606060);
+	margin-top : 1px;
+	padding-bottom : 4px;
+}
+
+:disabled {
+	color : #888;
+}
+
+box {
+	layout : inline;
+	spacing : 5 5;
+}
+
+:inline {
+	layout : inline;
+}
+
+:horizontal {
+	layout : horizontal;
+}
+
+:vertical {
+	layout : vertical;
+}
+
+:absolute {
+	layout : absolute;
+}
+
+:dock {
+	layout : dock;
+}
+
+box.panel {
+	background-color : gradient(#303030,#353535,#202020,#252525);
+}
+
+slider {
+	width : 100px;
+	height : 3px;
+	margin : 7px 5px;
+	background-color : #252525;
+	border : 1px solid gradient(#434343, #4B4B4B, #383838, #3A3A3A);
+}
+
+slider button, slider button:hover {
+	margin-top : -4px;
+	padding : 5px 2px;
+}
+
+checkbox {
+	width : 9px;
+	height : 9px;
+	color : #fff;
+	margin-top : 3px;
+	tick-spacing : 2px;
+	tick-color : gradient(#C0C0C0,#B0B0B0,#888888,#707070);
+	border : 1px solid gradient(#A0A0A0,#909090,#707070,#606060);
+	background-color : gradient(#303030,#252525,#252525,#252525);
+}
+
+itemlist {
+	padding : 5px;
+	vertical-spacing : 3px;
+	cursor-color : #333;
+	selection-color : #000;
+	border : 1px solid gradient(#A0A0A0,#909090,#707070,#606060);
+	background-color : gradient(#303030,#252525,#252525,#252525);
+	overflow : hidden;
+}
+
+input {
+	width : 80px;
+	height : 16px;
+	padding : 3px;
+	border : 1px solid gradient(#A0A0A0,#909090,#707070,#606060);
+	background-color : gradient(#303030,#252525,#252525,#252525);
+	overflow : hidden;
+}
+
+input:focus {
+	border : 1px solid white;
+	cursor-color : white;
+}
+
+color {
+	width : 16px;
+	height : 16px;
+	padding : 1px;
+	border : 1px solid gradient(#A0A0A0,#909090,#707070,#606060);
+}
+
+color:hover {
+	border : 1px solid gradient(#C0C0C0,#B0B0B0,#888888,#707070);
+}
+
+colorpicker {
+	width : 200px;
+	height : 320px;
+	layout : absolute;
+	border : 1px solid gradient(#A0A0A0,#909090,#707070,#606060);
+	background-color : gradient(#303030,#252525,#252525,#252525);
+}
+
+gradienteditor {
+	width : 450px;
+	height : 200px;
+	layout : absolute;
+	border : 1px solid gradient(#A0A0A0,#909090,#707070,#606060);
+	background-color : gradient(#303030,#252525,#252525,#252525);
+}
+
+:close {
+	icon : url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAB3RJTUUH3QsICzInlD+yVAAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAAOklEQVR4nGP4z/CfJMQw2DVAAJoiVEEMG9D0YBiBzUlwRdgsxOGH/9hVU0sDaU4izdPkBCuNY5oWGgC4sxwBPtUQzAAAAABJRU5ErkJggg=');
+	icon-color : #999;
+}
+
+button:close {
+	width : 10px;
+	height : 10px;
+	padding : 2px;
+	icon-left : -1px;
+	icon-top : -1px;
+}
+
+select {
+	padding-left : 20px;
+	icon : url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAAAB3RJTUUH3QsGCg4t5gQ/sQAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAAQElEQVR4nGP4z/CfeMSARRQOCKtGA/hUYwXYVeMBmKr34gZ4fUlamFBfNch9JPmSBNU4NeBxN7oGgr5EaKAwTABmAPc478+46wAAAABJRU5ErkJggg=');
+	icon-left : 3px;
+	icon-top : 5px;
+	icon-color : #999;
+}
+
+select:hover {
+	icon-color : #FFF;
+}
+
+itemlist.popup {
+	position : absolute;
+}
+
+value input {
+	width : 40px;
+	text-align : center;
+}
+
+value input:focus {
+	text-align : left;
+}

+ 214 - 0
h3d/Camera.hx

@@ -0,0 +1,214 @@
+package h3d;
+
+// use left-handed coordinate system, more suitable for 2D games X=0,Y=0 at screen top-left and Z towards user
+
+class Camera {
+	
+	public var zoom : Float;
+	
+	/**
+		The screenRatio represents the W/H screen ratio.
+	 **/
+	public var screenRatio : Float;
+	
+	/**
+		The horizontal FieldOfView, in degrees.
+	**/
+	public var fovX : Float;
+	public var zNear : Float;
+	public var zFar : Float;
+	
+	/**
+		Set orthographic bounds.
+	**/
+	public var orthoBounds : h3d.col.Bounds;
+	
+	public var rightHanded : Bool;
+	
+	public var mproj : Matrix;
+	public var mcam : Matrix;
+	public var m : Matrix;
+	
+	public var pos : Vector;
+	public var up : Vector;
+	public var target : Vector;
+	
+	public var viewX : Float = 0.;
+	public var viewY : Float = 0.;
+	
+	var minv : Matrix;
+	var needInv : Bool;
+
+	public function new( fovX = 60., zoom = 1., screenRatio = 1.333333, zNear = 0.02, zFar = 4000., rightHanded = false ) {
+		this.fovX = fovX;
+		this.zoom = zoom;
+		this.screenRatio = screenRatio;
+		this.zNear = zNear;
+		this.zFar = zFar;
+		this.rightHanded = rightHanded;
+		pos = new Vector(2, 3, 4);
+		up = new Vector(0, 0, 1);
+		target = new Vector(0, 0, 0);
+		m = new Matrix();
+		mcam = new Matrix();
+		mproj = new Matrix();
+		update();
+	}
+	
+	/**
+		Update the fovX value based on the requested fovY value (in degrees) and current screenRatio.
+	**/
+	public function setFovY( value : Float ) {
+		fovX = Math.atan( Math.tan(value * Math.PI / 180) / screenRatio ) * 180 / Math.PI;
+	}
+	
+	public function clone() {
+		var c = new Camera(fovX, zoom, screenRatio, zNear, zFar, rightHanded);
+		c.pos = pos.clone();
+		c.up = up.clone();
+		c.target = target.clone();
+		c.update();
+		return c;
+	}
+
+	/**
+		Returns the inverse of the camera matrix view and projection. Cache the result until the next update().
+	**/
+	public function getInverseViewProj() {
+		if( minv == null ) minv = new h3d.Matrix();
+		if( needInv ) {
+			minv.inverse(m);
+			needInv = false;
+		}
+		return minv;
+	}
+
+	/**
+		Transforms a 2D screen position into the 3D one according to the current camera.
+		The screenX and screenY values must be in the [-1,1] range.
+		The camZ value represents the normalized z in the frustum in the [0,1] range.
+		[unproject] can be used to get the ray from the camera position to a given screen position by using two different camZ values.
+		For instance the 3D ray between unproject(0,0,0) and unproject(0,0,1) is the center axis of the 3D frustum.
+	**/
+	public function unproject( screenX : Float, screenY : Float, camZ ) {
+		var p = new h3d.Vector(screenX, screenY, camZ);
+		p.project(getInverseViewProj());
+		return p;
+	}
+	
+	public function update() {
+		makeCameraMatrix(mcam);
+		makeFrustumMatrix(mproj);
+		m.multiply(mcam, mproj);
+		needInv = true;
+	}
+	
+	public function lostUp() {
+		var p2 = pos.clone();
+		p2.normalize();
+		return Math.abs(p2.dot3(up)) > 0.999;
+	}
+	
+	public function movePosAxis( dx : Float, dy : Float, dz = 0. ) {
+		var p = new Vector(dx, dy, dz);
+		p.project(mcam);
+		pos.x += p.x;
+		pos.y += p.y;
+		pos.z += p.z;
+	}
+
+	public function moveTargetAxis( dx : Float, dy : Float, dz = 0. ) {
+		var p = new Vector(dx, dy, dz);
+		p.project(mcam);
+		target.x += p.x;
+		target.y += p.y;
+		target.z += p.z;
+	}
+	
+	function makeCameraMatrix( m : Matrix ) {
+		// in leftHanded the z axis is positive else it's negative
+		// this way we make sure that our [ax,ay,-az] matrix follow the same handness as our world
+		var az = rightHanded ? pos.sub(target) : target.sub(pos);
+		az.normalize();
+		var ax = up.cross(az);
+		ax.normalize();
+		if( ax.length() == 0 ) {
+			ax.x = az.y;
+			ax.y = az.z;
+			ax.z = az.x;
+		}
+		var ay = az.cross(ax);
+		m._11 = ax.x;
+		m._12 = ay.x;
+		m._13 = az.x;
+		m._14 = 0;
+		m._21 = ax.y;
+		m._22 = ay.y;
+		m._23 = az.y;
+		m._24 = 0;
+		m._31 = ax.z;
+		m._32 = ay.z;
+		m._33 = az.z;
+		m._34 = 0;
+		m._41 = -ax.dot3(pos);
+		m._42 = -ay.dot3(pos);
+		m._43 = -az.dot3(pos);
+		m._44 = 1;
+	}
+	
+	function makeFrustumMatrix( m : Matrix ) {
+		m.zero();
+		
+		// this will take into account the aspect ratio and normalize the z value into [0,1] once it's been divided by w
+		// Matrixes have to solve the following formulaes :
+		//
+		// transform P by Mproj and divide everything by
+		//    [x,y,-zNear,1] => [sx/zNear, sy/zNear, 0, 1]
+		//    [x,y,-zFar,1] => [sx/zFar, sy/zFar, 1, 1]
+		
+		// we apply the screen ratio to the height in order to have the fov being a horizontal FOV. This way we don't have to change the FOV when the screen is enlarged
+		
+		var bounds = orthoBounds;
+		if( bounds != null ) {
+			
+			var w = 1 / (bounds.xMax - bounds.xMin);
+			var h = 1 / (bounds.yMax - bounds.yMin);
+			var d = 1 / (bounds.zMax - bounds.zMin);
+			
+			m._11 = 2 * w;
+			m._22 = 2 * h;
+			m._33 = d;
+			m._41 = -(bounds.xMin + bounds.xMax) * w;
+			m._42 = -(bounds.yMin + bounds.yMax) * h;
+			m._43 = -bounds.zMin * d;
+			m._44 = 1;
+			
+		} else {
+		
+			var scale = zoom / Math.tan(fovX * Math.PI / 360.0);
+			m._11 = scale;
+			m._22 = scale * screenRatio;
+			m._33 = zFar / (zFar - zNear);
+			m._34 = 1;
+			m._43 = -(zNear * zFar) / (zFar - zNear);
+			
+		}
+
+		m._11 += viewX * m._14;
+		m._21 += viewX * m._24;
+		m._31 += viewX * m._34;
+		m._41 += viewX * m._44;
+
+		m._12 += viewY * m._14;
+		m._22 += viewY * m._24;
+		m._32 += viewY * m._34;
+		m._42 += viewY * m._44;
+		
+		// our z is negative in that case
+		if( rightHanded ) {
+			m._33 *= -1;
+			m._34 *= -1;
+		}
+	}
+		
+}

+ 24 - 0
h3d/Drawable.hx

@@ -0,0 +1,24 @@
+package h3d;
+
+/**
+	A core object is a rendering context but completely outside of the 3d scene.
+	It is meant to be able to share a rendering context between several similar physical objects.
+ **/
+class Drawable<S:h3d.impl.Shader> implements IDrawable {
+
+	public var shader : S;
+	public var primitive : h3d.prim.Primitive;
+	public var material : h3d.mat.Material;
+	
+	public function new(prim, shader) {
+		this.primitive = prim;
+		this.shader = shader;
+		this.material = new h3d.mat.Material(shader);
+	}
+	
+	public function render( engine : h3d.Engine ) {
+		engine.selectMaterial(material);
+		primitive.render(engine);
+	}
+	
+}

+ 351 - 0
h3d/Engine.hx

@@ -0,0 +1,351 @@
+package h3d;
+import h3d.mat.Data;
+
+class Engine {
+
+	var driver : h3d.impl.Driver;
+	
+	public var mem(default,null) : h3d.impl.MemoryManager;
+
+	public var hardware(default, null) : Bool;
+	public var width(default, null) : Int;
+	public var height(default, null) : Int;
+	public var debug(default, set) : Bool;
+
+	public var drawTriangles(default, null) : Int;
+	public var drawCalls(default, null) : Int;
+	public var shaderSwitches(default, null) : Int;
+
+	public var backgroundColor : Int;
+	public var autoResize : Bool;
+	public var fullScreen(default, set) : Bool;
+	
+	public var fps(get, never) : Float;
+	public var frameCount : Int = 0;
+	
+	public var forcedMatBits : Int = 0;
+	public var forcedMatMask : Int = 0xFFFFFF;
+	
+	var realFps : Float;
+	var lastTime : Float;
+	var antiAlias : Int;
+	
+	var debugPoint : h3d.Drawable<h3d.impl.Shaders.PointShader>;
+	var debugLine : h3d.Drawable<h3d.impl.Shaders.LineShader>;
+	
+	@:allow(h3d)
+	var curProjMatrix : h3d.Matrix;
+
+	@:access(hxd.Stage)
+	public function new( hardware = true, aa = 0 ) {
+		this.hardware = hardware;
+		this.antiAlias = aa;
+		this.autoResize = true;
+		#if openfl
+		hxd.Stage.openFLBoot(start);
+		#else
+		start();
+		#end
+	}
+	
+	function start() {
+		fullScreen = !hxd.System.isWindowed;
+		var stage = hxd.Stage.getInstance();
+		realFps = stage.getFrameRate();
+		lastTime = haxe.Timer.stamp();
+		stage.addResizeEvent(onStageResize);
+		#if flash
+		driver = new h3d.impl.Stage3dDriver();
+		#elseif (js || cpp)
+		driver = new h3d.impl.GlDriver();
+		#else
+		throw "No driver";
+		#end
+		if( CURRENT == null )
+			CURRENT = this;
+	}
+	
+	static var CURRENT : Engine = null;
+	
+	public static function getCurrent() {
+		return CURRENT;
+	}
+	
+	public function setCurrent() {
+		CURRENT = this;
+	}
+
+	public function init() {
+		driver.init(onCreate, !hardware);
+	}
+
+	public function driverName(details=false) {
+		return driver.getDriverName(details);
+	}
+	
+	public function setCapture( bmp : hxd.BitmapData, callb : Void -> Void ) {
+		driver.setCapture(bmp,callb);
+	}
+
+	public function selectShader( shader : h3d.impl.Shader ) {
+		if( driver.selectShader(shader) )
+			shaderSwitches++;
+	}
+
+	@:access(h3d.mat.Material.bits)
+	public function selectMaterial( m : h3d.mat.Material ) {
+		var mbits = (m.bits & forcedMatMask) | forcedMatBits;
+		driver.selectMaterial(mbits);
+		selectShader(m.shader);
+	}
+
+	function selectBuffer( buf : h3d.impl.MemoryManager.BigBuffer ) {
+		if( buf.isDisposed() )
+			return false;
+		driver.selectBuffer(buf.vbuf);
+		return true;
+	}
+
+	public inline function renderTriBuffer( b : h3d.impl.Buffer, start = 0, max = -1 ) {
+		return renderBuffer(b, mem.indexes, 3, start, max);
+	}
+	
+	public inline function renderQuadBuffer( b : h3d.impl.Buffer, start = 0, max = -1 ) {
+		return renderBuffer(b, mem.quadIndexes, 2, start, max);
+	}
+	
+	public function enableDriver( ?b : Bool ) {
+		if( b == null )
+			b = Std.is(driver, h3d.impl.NullDriver);
+		if( b ) {
+			var d = Std.instance(driver, h3d.impl.NullDriver);
+			if( d != null )
+				driver = d.driver;
+		} else if( !Std.is(driver,h3d.impl.NullDriver) ) {
+			driver = new h3d.impl.NullDriver(driver);
+		}
+	}
+
+	// we use preallocated indexes so all the triangles are stored inside our buffers
+	function renderBuffer( b : h3d.impl.Buffer, indexes : h3d.impl.Indexes, vertPerTri : Int, startTri = 0, drawTri = -1 ) {
+		if( indexes.isDisposed() )
+			return;
+		do {
+			var ntri = Std.int(b.nvert / vertPerTri);
+			var pos = Std.int(b.pos / vertPerTri);
+			if( startTri > 0 ) {
+				if( startTri >= ntri ) {
+					startTri -= ntri;
+					b = b.next;
+					continue;
+				}
+				pos += startTri;
+				ntri -= startTri;
+				startTri = 0;
+			}
+			if( drawTri >= 0 ) {
+				if( drawTri == 0 ) return;
+				drawTri -= ntri;
+				if( drawTri < 0 ) {
+					ntri += drawTri;
+					drawTri = 0;
+				}
+			}
+			if( ntri > 0 && selectBuffer(b.b) ) {
+				// *3 because it's the position in indexes which are always by 3
+				driver.draw(indexes.ibuf, pos * 3, ntri);
+				drawTriangles += ntri;
+				drawCalls++;
+			}
+			b = b.next;
+		} while( b != null );
+	}
+	
+	// we use custom indexes, so the number of triangles is the number of indexes/3
+	public function renderIndexed( b : h3d.impl.Buffer, indexes : h3d.impl.Indexes, startTri = 0, drawTri = -1 ) {
+		if( b.next != null )
+			throw "Buffer is split";
+		if( indexes.isDisposed() )
+			return;
+		var maxTri = Std.int(indexes.count / 3);
+		if( drawTri < 0 ) drawTri = maxTri - startTri;
+		if( drawTri > 0 && selectBuffer(b.b) ) {
+			// *3 because it's the position in indexes which are always by 3
+			driver.draw(indexes.ibuf, startTri * 3, drawTri);
+			drawTriangles += drawTri;
+			drawCalls++;
+		}
+	}
+	
+	public function renderMultiBuffers( buffers : h3d.impl.Buffer.BufferOffset, indexes : h3d.impl.Indexes, startTri = 0, drawTri = -1 ) {
+		var maxTri = Std.int(indexes.count / 3);
+		if( maxTri <= 0 ) return;
+		driver.selectMultiBuffers(buffers);
+		if( indexes.isDisposed() )
+			return;
+		if( drawTri < 0 ) drawTri = maxTri - startTri;
+		if( drawTri > 0 ) {
+			// render
+			driver.draw(indexes.ibuf, startTri * 3, drawTri);
+			drawTriangles += drawTri;
+			drawCalls++;
+		}
+	}
+
+	function set_debug(d) {
+		debug = d;
+		driver.setDebug(debug);
+		return d;
+	}
+
+	function onCreate( disposed ) {
+		if( autoResize ) {
+			width = hxd.System.width;
+			height = hxd.System.height;
+		}
+		if( disposed )
+			mem.onContextLost();
+		else
+			mem = new h3d.impl.MemoryManager(driver, 65400);
+		hardware = driver.isHardware();
+		set_debug(debug);
+		set_fullScreen(fullScreen);
+		resize(width, height);
+		if( disposed )
+			onContextLost();
+		else
+			onReady();
+	}
+	
+	public dynamic function onContextLost() {
+	}
+
+	public dynamic function onReady() {
+	}
+	
+	function onStageResize() {
+		if( autoResize && !driver.isDisposed() ) {
+			var w = hxd.System.width, h = hxd.System.height;
+			if( w != width || h != height )
+				resize(w, h);
+			onResized();
+		}
+	}
+	
+	function set_fullScreen(v) {
+		fullScreen = v;
+		if( mem != null && hxd.System.isWindowed )
+			hxd.Stage.getInstance().setFullScreen(v);
+		return v;
+	}
+	
+	public dynamic function onResized() {
+	}
+
+	public function resize(width, height) {
+		// minimum 32x32 size
+		if( width < 32 ) width = 32;
+		if( height < 32 ) height = 32;
+		this.width = width;
+		this.height = height;
+		if( !driver.isDisposed() ) driver.resize(width, height);
+	}
+
+	public function begin() {
+		if( driver.isDisposed() )
+			return false;
+		driver.clear( ((backgroundColor>>16)&0xFF)/255 , ((backgroundColor>>8)&0xFF)/255, (backgroundColor&0xFF)/255, ((backgroundColor>>>24)&0xFF)/255);
+		// init
+		frameCount++;
+		drawTriangles = 0;
+		shaderSwitches = 0;
+		drawCalls = 0;
+		curProjMatrix = null;
+		driver.reset();
+		return true;
+	}
+
+	function reset() {
+		driver.reset();
+	}
+
+	public function end() {
+		driver.present();
+		reset();
+		curProjMatrix = null;
+	}
+
+	public function setTarget( tex : h3d.mat.Texture, useDepth = false, clearColor = 0 ) {
+		driver.setRenderTarget(tex == null ? null : tex.t, useDepth, clearColor);
+	}
+
+	public function setRenderZone( x = 0, y = 0, width = -1, height = -1 ) {
+		driver.setRenderZone(x, y, width, height);
+	}
+
+	public function render( obj : { function render( engine : Engine ) : Void; } ) {
+		if( !begin() ) return false;
+		obj.render(this);
+		end();
+				
+		var delta = haxe.Timer.stamp() - lastTime;
+		lastTime += delta;
+		if( delta > 0 ) {
+			var curFps = 1. / delta;
+			if( curFps > realFps * 2 ) curFps = realFps * 2 else if( curFps < realFps * 0.5 ) curFps = realFps * 0.5;
+			var f = delta / .5;
+			if( f > 0.3 ) f = 0.3;
+			realFps = realFps * (1 - f) + curFps * f; // smooth a bit the fps
+		}
+		return true;
+	}
+	
+	// debug functions
+	public function point( x : Float, y : Float, z : Float, color = 0x80FF0000, size = 1.0, depth = false ) {
+		if( curProjMatrix == null )
+			return;
+		if( debugPoint == null ) {
+			debugPoint = new Drawable(new h3d.prim.Plan2D(), new h3d.impl.Shaders.PointShader());
+			debugPoint.material.blend(SrcAlpha, OneMinusSrcAlpha);
+			debugPoint.material.depthWrite = false;
+		}
+		debugPoint.material.depthTest = depth ? h3d.mat.Data.Compare.LessEqual : h3d.mat.Data.Compare.Always;
+		debugPoint.shader.mproj = curProjMatrix;
+		debugPoint.shader.delta = new h3d.Vector(x, y, z, 1);
+		var gscale = 1 / 200;
+		debugPoint.shader.size = new h3d.Vector(size * gscale, size * gscale * width / height);
+		debugPoint.shader.color = color;
+		debugPoint.render(h3d.Engine.getCurrent());
+	}
+
+	public function line( x1 : Float, y1 : Float, z1 : Float, x2 : Float, y2 : Float, z2 : Float, color = 0x80FF0000, depth = false ) {
+		if( curProjMatrix == null )
+			return;
+		if( debugLine == null ) {
+			debugLine = new Drawable(new h3d.prim.Plan2D(), new h3d.impl.Shaders.LineShader());
+			debugLine.material.blend(SrcAlpha, OneMinusSrcAlpha);
+			debugLine.material.depthWrite = false;
+			debugLine.material.culling = None;
+		}
+		debugLine.material.depthTest = depth ? h3d.mat.Data.Compare.LessEqual : h3d.mat.Data.Compare.Always;
+		debugLine.shader.mproj = curProjMatrix;
+		debugLine.shader.start = new h3d.Vector(x1, y1, z1);
+		debugLine.shader.end = new h3d.Vector(x2, y2, z2);
+		debugLine.shader.color = color;
+		debugLine.render(h3d.Engine.getCurrent());
+	}
+
+	public function lineP( a : { x : Float, y : Float, z : Float }, b : { x : Float, y : Float, z : Float }, color = 0x80FF0000, depth = false ) {
+		line(a.x, a.y, a.z, b.x, b.y, b.z, color, depth);
+	}
+
+	public function dispose() {
+		driver.dispose();
+		hxd.Stage.getInstance().removeResizeEvent(onStageResize);
+	}
+	
+	function get_fps() {
+		return Math.ceil(realFps * 100) / 100;
+	}
+	
+}

+ 5 - 0
h3d/IDrawable.hx

@@ -0,0 +1,5 @@
+package h3d;
+
+interface IDrawable {
+	public function render( engine : Engine ) : Void;
+}

+ 564 - 0
h3d/Matrix.hx

@@ -0,0 +1,564 @@
+package h3d;
+import hxd.Math;
+
+class Matrix {
+	
+	static var tmp = new Matrix();
+
+	public var _11 : Float;
+	public var _12 : Float;
+	public var _13 : Float;
+	public var _14 : Float;
+	public var _21 : Float;
+	public var _22 : Float;
+	public var _23 : Float;
+	public var _24 : Float;
+	public var _31 : Float;
+	public var _32 : Float;
+	public var _33 : Float;
+	public var _34 : Float;
+	public var _41 : Float;
+	public var _42 : Float;
+	public var _43 : Float;
+	public var _44 : Float;
+
+	public function new() {
+	}
+
+	public function zero() {
+		_11 = 0.0; _12 = 0.0; _13 = 0.0; _14 = 0.0;
+		_21 = 0.0; _22 = 0.0; _23 = 0.0; _24 = 0.0;
+		_31 = 0.0; _32 = 0.0; _33 = 0.0; _34 = 0.0;
+		_41 = 0.0; _42 = 0.0; _43 = 0.0; _44 = 0.0;
+	}
+
+	public function identity() {
+		_11 = 1.0; _12 = 0.0; _13 = 0.0; _14 = 0.0;
+		_21 = 0.0; _22 = 1.0; _23 = 0.0; _24 = 0.0;
+		_31 = 0.0; _32 = 0.0; _33 = 1.0; _34 = 0.0;
+		_41 = 0.0; _42 = 0.0; _43 = 0.0; _44 = 1.0;
+	}
+
+	public function initRotateX( a : Float ) {
+		var cos = Math.cos(a);
+		var sin = Math.sin(a);
+		_11 = 1.0; _12 = 0.0; _13 = 0.0; _14 = 0.0;
+		_21 = 0.0; _22 = cos; _23 = sin; _24 = 0.0;
+		_31 = 0.0; _32 = -sin; _33 = cos; _34 = 0.0;
+		_41 = 0.0; _42 = 0.0; _43 = 0.0; _44 = 1.0;
+	}
+
+	public function initRotateY( a : Float ) {
+		var cos = Math.cos(a);
+		var sin = Math.sin(a);
+		_11 = cos; _12 = 0.0; _13 = -sin; _14 = 0.0;
+		_21 = 0.0; _22 = 1.0; _23 = 0.0; _24 = 0.0;
+		_31 = sin; _32 = 0.0; _33 = cos; _34 = 0.0;
+		_41 = 0.0; _42 = 0.0; _43 = 0.0; _44 = 1.0;
+	}
+
+	public function initRotateZ( a : Float ) {
+		var cos = Math.cos(a);
+		var sin = Math.sin(a);
+		_11 = cos; _12 = sin; _13 = 0.0; _14 = 0.0;
+		_21 = -sin; _22 = cos; _23 = 0.0; _24 = 0.0;
+		_31 = 0.0; _32 = 0.0; _33 = 1.0; _34 = 0.0;
+		_41 = 0.0; _42 = 0.0; _43 = 0.0; _44 = 1.0;
+	}
+
+	public function initTranslate( x = 0., y = 0., z = 0. ) {
+		_11 = 1.0; _12 = 0.0; _13 = 0.0; _14 = 0.0;
+		_21 = 0.0; _22 = 1.0; _23 = 0.0; _24 = 0.0;
+		_31 = 0.0; _32 = 0.0; _33 = 1.0; _34 = 0.0;
+		_41 = x; _42 = y; _43 = z; _44 = 1.0;
+	}
+
+	public function initScale( x = 1., y = 1., z = 1. ) {
+		_11 = x; _12 = 0.0; _13 = 0.0; _14 = 0.0;
+		_21 = 0.0; _22 = y; _23 = 0.0; _24 = 0.0;
+		_31 = 0.0; _32 = 0.0; _33 = z; _34 = 0.0;
+		_41 = 0.0; _42 = 0.0; _43 = 0.0; _44 = 1.0;
+	}
+
+	public function initRotateAxis( axis : Vector, angle : Float ) {
+		var cos = Math.cos(angle), sin = Math.sin(angle);
+		var cos1 = 1 - cos;
+		var x = -axis.x, y = -axis.y, z = -axis.z;
+		var xx = x * x, yy = y * y, zz = z * z;
+		var len = Math.invSqrt(xx + yy + zz);
+		x *= len;
+		y *= len;
+		z *= len;
+		var xcos1 = x * cos1, zcos1 = z * cos1;
+		_11 = cos + x * xcos1;
+		_12 = y * xcos1 - z * sin;
+		_13 = x * zcos1 + y * sin;
+		_14 = 0.;
+		_21 = y * xcos1 + z * sin;
+		_22 = cos + y * y * cos1;
+		_23 = y * zcos1 - x * sin;
+		_24 = 0.;
+		_31 = x * zcos1 - y * sin;
+		_32 = y * zcos1 + x * sin;
+		_33 = cos + z * zcos1;
+		_34 = 0.;
+		_41 = 0.; _42 = 0.; _43 = 0.; _44 = 1.;
+	}
+	
+	public function initRotate( x : Float, y : Float, z : Float ) {
+		var cx = Math.cos(x);
+		var sx = Math.sin(x);
+		var cy = Math.cos(y);
+		var sy = Math.sin(y);
+		var cz = Math.cos(z);
+		var sz = Math.sin(z);
+		var cxsy = cx * sy;
+		var sxsy = sx * sy;
+		_11 = cy * cz;
+		_12 = cy * sz;
+		_13 = -sy;
+		_14 = 0;
+		_21 = sxsy * cz - cx * sz;
+		_22 = sxsy * sz + cx * cz;
+		_23 = sx * cy;
+		_24 = 0;
+		_31 = cxsy * cz + sx * sz;
+		_32 = cxsy * sz - sx * cz;
+		_33 = cx * cy;
+		_34 = 0;
+		_41 = 0;
+		_42 = 0;
+		_43 = 0;
+		_44 = 1;
+	}
+	
+	public function translate( x = 0., y = 0., z = 0. ) {
+		_11 += x * _14;
+		_12 += y * _14;
+		_13 += z * _14;
+		_21 += x * _24;
+		_22 += y * _24;
+		_23 += z * _24;
+		_31 += x * _34;
+		_32 += y * _34;
+		_33 += z * _34;
+		_41 += x * _44;
+		_42 += y * _44;
+		_43 += z * _44;
+	}
+	
+	public function scale( x = 1., y = 1., z = 1. ) {
+		_11 *= x;
+		_21 *= x;
+		_31 *= x;
+		_41 *= x;
+		_12 *= y;
+		_22 *= y;
+		_32 *= y;
+		_42 *= y;
+		_13 *= z;
+		_23 *= z;
+		_33 *= z;
+		_43 *= z;
+	}
+
+	public function rotate( x, y, z ) {
+		var tmp = tmp;
+		tmp.initRotate(x,y,z);
+		multiply(this, tmp);
+	}
+	
+	public function rotateAxis( axis, angle ) {
+		var tmp = tmp;
+		tmp.initRotateAxis(axis, angle);
+		multiply(this, tmp);
+	}
+	
+	public inline function add( m : Matrix ) {
+		multiply(this, m);
+	}
+	
+	public function prependTranslate( x = 0., y = 0., z = 0. ) {
+		var vx = _11 * x + _21 * y + _31 * z + _41;
+		var vy = _12 * x + _22 * y + _32 * z + _42;
+		var vz = _13 * x + _23 * y + _33 * z + _43;
+		var vw = _14 * x + _24 * y + _34 * z + _44;
+		_41 = vx;
+		_42 = vy;
+		_43 = vz;
+		_44 = vw;
+	}
+
+	public function prependRotate( x, y, z ) {
+		var tmp = tmp;
+		tmp.initRotate(x,y,z);
+		multiply(tmp, this);
+	}
+	
+	public function prependRotateAxis( axis, angle ) {
+		var tmp = tmp;
+		tmp.initRotateAxis(axis, angle);
+		multiply(tmp, this);
+	}
+
+	public function prependScale( sx = 1., sy = 1., sz = 1. ) {
+		var tmp = tmp;
+		tmp.initScale(sx,sy,sz);
+		multiply(tmp, this);
+	}
+	
+	public function multiply3x4( a : Matrix, b : Matrix ) {
+		var m11 = a._11; var m12 = a._12; var m13 = a._13;
+		var m21 = a._21; var m22 = a._22; var m23 = a._23;
+		var a31 = a._31; var a32 = a._32; var a33 = a._33;
+		var a41 = a._41; var a42 = a._42; var a43 = a._43;
+		var b11 = b._11; var b12 = b._12; var b13 = b._13;
+		var b21 = b._21; var b22 = b._22; var b23 = b._23;
+		var b31 = b._31; var b32 = b._32; var b33 = b._33;
+		var b41 = b._41; var b42 = b._42; var b43 = b._43;
+
+		_11 = m11 * b11 + m12 * b21 + m13 * b31;
+		_12 = m11 * b12 + m12 * b22 + m13 * b32;
+		_13 = m11 * b13 + m12 * b23 + m13 * b33;
+		_14 = 0;
+
+		_21 = m21 * b11 + m22 * b21 + m23 * b31;
+		_22 = m21 * b12 + m22 * b22 + m23 * b32;
+		_23 = m21 * b13 + m22 * b23 + m23 * b33;
+		_24 = 0;
+
+		_31 = a31 * b11 + a32 * b21 + a33 * b31;
+		_32 = a31 * b12 + a32 * b22 + a33 * b32;
+		_33 = a31 * b13 + a32 * b23 + a33 * b33;
+		_34 = 0;
+
+		_41 = a41 * b11 + a42 * b21 + a43 * b31 + b41;
+		_42 = a41 * b12 + a42 * b22 + a43 * b32 + b42;
+		_43 = a41 * b13 + a42 * b23 + a43 * b33 + b43;
+		_44 = 1;
+	}
+
+	public function multiply( a : Matrix, b : Matrix ) {
+		var a11 = a._11; var a12 = a._12; var a13 = a._13; var a14 = a._14;
+		var a21 = a._21; var a22 = a._22; var a23 = a._23; var a24 = a._24;
+		var a31 = a._31; var a32 = a._32; var a33 = a._33; var a34 = a._34;
+		var a41 = a._41; var a42 = a._42; var a43 = a._43; var a44 = a._44;
+		var b11 = b._11; var b12 = b._12; var b13 = b._13; var b14 = b._14;
+		var b21 = b._21; var b22 = b._22; var b23 = b._23; var b24 = b._24;
+		var b31 = b._31; var b32 = b._32; var b33 = b._33; var b34 = b._34;
+		var b41 = b._41; var b42 = b._42; var b43 = b._43; var b44 = b._44;
+
+		_11 = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
+		_12 = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
+		_13 = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
+		_14 = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;
+
+		_21 = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
+		_22 = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
+		_23 = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
+		_24 = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;
+
+		_31 = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
+		_32 = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
+		_33 = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
+		_34 = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;
+
+		_41 = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
+		_42 = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
+		_43 = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
+		_44 = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;
+	}
+
+	public inline function invert() {
+		inverse(this);
+	}
+
+	public function inverse3x4( m : Matrix ) {
+		var m11 = m._11, m12 = m._12, m13 = m._13;
+		var m21 = m._21, m22 = m._22, m23 = m._23;
+		var m31 = m._31, m32 = m._32, m33 = m._33;
+		var m41 = m._41, m42 = m._42, m43 = m._43;
+		_11 = m22*m33 - m23*m32;
+		_12 = m13*m32 - m12*m33;
+		_13 = m12*m23 - m13*m22;
+		_14 = 0;
+		_21 = m23*m31 - m21*m33;
+		_22 = m11*m33 - m13*m31;
+		_23 = m13*m21 - m11*m23;
+		_24 = 0;
+		_31 = m21*m32 - m22*m31;
+		_32 = m12*m31 - m11*m32;
+		_33 = m11*m22 - m12*m21;
+		_34 = 0;
+		_41 = -m21 * m32 * m43 + m21 * m33 * m42 + m31 * m22 * m43 - m31 * m23 * m42 - m41 * m22 * m33 + m41 * m23 * m32;
+		_42 = m11 * m32 * m43 - m11 * m33 * m42 - m31 * m12 * m43 + m31 * m13 * m42 + m41 * m12 * m33 - m41 * m13 * m32;
+		_43 = -m11 * m22 * m43 + m11 * m23 * m42 + m21 * m12 * m43 - m21 * m13 * m42 - m41 * m12 * m23 + m41 * m13 * m22;
+		_44 = m11 * m22 * m33 - m11 * m23 * m32 - m21 * m12 * m33 + m21 * m13 * m32 + m31 * m12 * m23 - m31 * m13 * m22;
+		_44 = 1;
+		var det = m11 * _11 + m12 * _21 + m13 * _31;
+		if(	Math.abs(det) < Math.EPSILON ) {
+			zero();
+			return;
+		}
+		var invDet = 1.0 / det;
+		_11 *= invDet; _12 *= invDet; _13 *= invDet;
+		_21 *= invDet; _22 *= invDet; _23 *= invDet;
+		_31 *= invDet; _32 *= invDet; _33 *= invDet;
+		_41 *= invDet; _42 *= invDet; _43 *= invDet;
+	}
+	
+	public function inverse( m : Matrix ) {
+		var m11 = m._11; var m12 = m._12; var m13 = m._13; var m14 = m._14;
+		var m21 = m._21; var m22 = m._22; var m23 = m._23; var m24 = m._24;
+		var m31 = m._31; var m32 = m._32; var m33 = m._33; var m34 = m._34;
+		var m41 = m._41; var m42 = m._42; var m43 = m._43; var m44 = m._44;
+
+		_11 = m22 * m33 * m44 - m22 * m34 * m43 - m32 * m23 * m44 + m32 * m24 * m43 + m42 * m23 * m34 - m42 * m24 * m33;
+		_12 = -m12 * m33 * m44 + m12 * m34 * m43 + m32 * m13 * m44 - m32 * m14 * m43 - m42 * m13 * m34 + m42 * m14 * m33;
+		_13 = m12 * m23 * m44 - m12 * m24 * m43 - m22 * m13 * m44 + m22 * m14 * m43 + m42 * m13 * m24 - m42 * m14 * m23;
+		_14 = -m12 * m23 * m34 + m12 * m24 * m33 + m22 * m13 * m34 - m22 * m14 * m33 - m32 * m13 * m24 + m32 * m14 * m23;
+		_21 = -m21 * m33 * m44 + m21 * m34 * m43 + m31 * m23 * m44 - m31 * m24 * m43 - m41 * m23 * m34 + m41 * m24 * m33;
+		_22 = m11 * m33 * m44 - m11 * m34 * m43 - m31 * m13 * m44 + m31 * m14 * m43 + m41 * m13 * m34 - m41 * m14 * m33;
+		_23 = -m11 * m23 * m44 + m11 * m24 * m43 + m21 * m13 * m44 - m21 * m14 * m43 - m41 * m13 * m24 + m41 * m14 * m23;
+		_24 =  m11 * m23 * m34 - m11 * m24 * m33 - m21 * m13 * m34 + m21 * m14 * m33 + m31 * m13 * m24 - m31 * m14 * m23;
+		_31 = m21 * m32 * m44 - m21 * m34 * m42 - m31 * m22 * m44 + m31 * m24 * m42 + m41 * m22 * m34 - m41 * m24 * m32;
+		_32 = -m11 * m32 * m44 + m11 * m34 * m42 + m31 * m12 * m44 - m31 * m14 * m42 - m41 * m12 * m34 + m41 * m14 * m32;
+		_33 = m11 * m22 * m44 - m11 * m24 * m42 - m21 * m12 * m44 + m21 * m14 * m42 + m41 * m12 * m24 - m41 * m14 * m22;
+		_34 =  -m11 * m22 * m34 + m11 * m24 * m32 + m21 * m12 * m34 - m21 * m14 * m32 - m31 * m12 * m24 + m31 * m14 * m22;
+		_41 = -m21 * m32 * m43 + m21 * m33 * m42 + m31 * m22 * m43 - m31 * m23 * m42 - m41 * m22 * m33 + m41 * m23 * m32;
+		_42 = m11 * m32 * m43 - m11 * m33 * m42 - m31 * m12 * m43 + m31 * m13 * m42 + m41 * m12 * m33 - m41 * m13 * m32;
+		_43 = -m11 * m22 * m43 + m11 * m23 * m42 + m21 * m12 * m43 - m21 * m13 * m42 - m41 * m12 * m23 + m41 * m13 * m22;
+		_44 = m11 * m22 * m33 - m11 * m23 * m32 - m21 * m12 * m33 + m21 * m13 * m32 + m31 * m12 * m23 - m31 * m13 * m22;
+
+		var det = m11 * _11 + m12 * _21 + m13 * _31 + m14 * _41;
+		if(	Math.abs(det) < Math.EPSILON ) {
+			zero();
+			return;
+		}
+
+		det = 1.0 / det;
+		_11 *= det;
+		_12 *= det;
+		_13 *= det;
+		_14 *= det;
+		_21 *= det;
+		_22 *= det;
+		_23 *= det;
+		_24 *= det;
+		_31 *= det;
+		_32 *= det;
+		_33 *= det;
+		_34 *= det;
+		_41 *= det;
+		_42 *= det;
+		_43 *= det;
+		_44 *= det;
+	}
+
+	public function transpose() {
+		var tmp;
+		tmp = _12; _12 = _21; _21 = tmp;
+		tmp = _13; _13 = _31; _31 = tmp;
+		tmp = _14; _14 = _41; _41 = tmp;
+		tmp = _23; _23 = _32; _32 = tmp;
+		tmp = _24; _24 = _42; _42 = tmp;
+		tmp = _34; _34 = _43; _43 = tmp;
+	}
+
+	public function clone() {
+		var m = new Matrix();
+		m._11 = _11; m._12 = _12; m._13 = _13; m._14 = _14;
+		m._21 = _21; m._22 = _22; m._23 = _23; m._24 = _24;
+		m._31 = _31; m._32 = _32; m._33 = _33; m._34 = _34;
+		m._41 = _41; m._42 = _42; m._43 = _43; m._44 = _44;
+		return m;
+	}
+
+	public function loadFrom( m : Matrix ) {
+		_11 = m._11; _12 = m._12; _13 = m._13; _14 = m._14;
+		_21 = m._21; _22 = m._22; _23 = m._23; _24 = m._24;
+		_31 = m._31; _32 = m._32; _33 = m._33; _34 = m._34;
+		_41 = m._41; _42 = m._42; _43 = m._43; _44 = m._44;
+	}
+	
+	public function load( a : Array<Float> ) {
+		_11 = a[0]; _12 = a[1]; _13 = a[2]; _14 = a[3];
+		_21 = a[4]; _22 = a[5]; _23 = a[6]; _24 = a[7];
+		_31 = a[8]; _32 = a[9]; _33 = a[10]; _34 = a[11];
+		_41 = a[12]; _42 = a[13]; _43 = a[14]; _44 = a[15];
+	}
+	
+	public function getFloats() {
+		return [_11, _12, _13, _14, _21, _22, _23, _24, _31, _32, _33, _34, _41, _42, _43, _44];
+	}
+	
+	public function toString() {
+		return "MAT=[\n" +
+			"  [ " + Math.fmt(_11) + ", " + Math.fmt(_12) + ", " + Math.fmt(_13) + ", " + Math.fmt(_14) + " ]\n" +
+			"  [ " + Math.fmt(_21) + ", " + Math.fmt(_22) + ", " + Math.fmt(_23) + ", " + Math.fmt(_24) + " ]\n" +
+			"  [ " + Math.fmt(_31) + ", " + Math.fmt(_32) + ", " + Math.fmt(_33) + ", " + Math.fmt(_34) + " ]\n" +
+			"  [ " + Math.fmt(_41) + ", " + Math.fmt(_42) + ", " + Math.fmt(_43) + ", " + Math.fmt(_44) + " ]\n" +
+		"]";
+	}
+	
+	// ---- COLOR MATRIX FUNCTIONS -------
+
+	static inline var lumR = 0.212671;
+	static inline var lumG = 0.71516;
+	static inline var lumB = 0.072169;
+	
+	public function colorHue( hue : Float ) {
+		if( hue == 0. )
+			return;
+		var cv = Math.cos(hue);
+		var sv = Math.sin(hue);
+		tmp._11 = lumR + cv * (1 - lumR) - sv * lumR;
+		tmp._12 = lumR - cv * lumR + sv * 0.143;
+		tmp._13 = lumR - cv * lumR - sv * (1 - lumR);
+		tmp._21 = lumG - cv * lumG - sv * lumG;
+		tmp._22 = lumG + cv * (1 - lumG) + sv * 0.140;
+		tmp._23 = lumG - cv * lumG + sv * lumG;
+		tmp._31 = lumB - cv * lumB - sv * lumB;
+		tmp._32 = lumB - cv * lumB - sv * 0.283;
+		tmp._33 = lumB + cv * (1 - lumB) + sv * lumB;
+		tmp._34 = 0;
+		tmp._41 = 0;
+		tmp._42 = 0;
+		tmp._43 = 0;
+		multiply3x4(this, tmp);
+	}
+	
+	public function colorSaturation( sat : Float ) {
+		var is = 1 - sat;
+		var r = is * lumR;
+		var g = is * lumG;
+		var b = is * lumB;
+		tmp._11 = r + sat;
+		tmp._12 = r;
+		tmp._13 = r;
+		tmp._21 = g;
+		tmp._22 = g + sat;
+		tmp._23 = g;
+		tmp._31 = b;
+		tmp._32 = b;
+		tmp._33 = b + sat;
+		tmp._41 = 0;
+		tmp._42 = 0;
+		tmp._43 = 0;
+		multiply3x4(this, tmp);
+	}
+	
+	public function colorContrast( contrast : Float ) {
+		var v = contrast + 1;
+		tmp._11 = v;
+		tmp._12 = 0;
+		tmp._13 = 0;
+		tmp._21 = 0;
+		tmp._22 = v;
+		tmp._23 = 0;
+		tmp._31 = 0;
+		tmp._32 = 0;
+		tmp._33 = v;
+		tmp._41 = -contrast*0.5;
+		tmp._42 = -contrast*0.5;
+		tmp._43 = -contrast*0.5;
+		multiply3x4(this, tmp);
+	}
+
+	public function colorBrightness( brightness : Float ) {
+		_41 += brightness;
+		_42 += brightness;
+		_43 += brightness;
+	}
+	
+	public static function I() {
+		var m = new Matrix();
+		m.identity();
+		return m;
+	}
+
+	public static function L( a : Array<Float> ) {
+		var m = new Matrix();
+		m.load(a);
+		return m;
+	}
+	
+	public static function T( x = 0., y = 0., z = 0. ) {
+		var m = new Matrix();
+		m.initTranslate(x, y, z);
+		return m;
+	}
+
+	public static function R(x,y,z) {
+		var m = new Matrix();
+		m.initRotate(x,y,z);
+		return m;
+	}
+
+	public static function S( x = 1., y = 1., z = 1.0 ) {
+		var m = new Matrix();
+		m.initScale(x, y, z);
+		return m;
+	}
+
+	//retrieves pos vector from matrix
+	public inline function pos( ? v: Vector)
+	{
+		if( v == null )
+			return new Vector( _41, _42 , _43 , _44  );
+		else
+		{
+			v.x = _41;
+			v.y = _42;
+			v.z = _43;
+			v.w = _44;
+			return v;
+		}
+	}
+	
+	//retrieves at vector from matrix
+	public inline function at( ?v:Vector)
+	{
+		if( v == null )
+			return new Vector( _31, _32 , _33 , _34  );
+		else
+		{
+			v.x = _31;
+			v.y = _32;
+			v.z = _33;
+			v.w = _34;
+			return v;
+		}
+	}
+	
+	//retrieves up vector from matrix
+	public inline function up(?v:Vector)
+	{
+		if( v == null )
+			return new Vector( _21, _22 , _23 , _24  );
+		else
+		{
+			v.x = _21;
+			v.y = _22;
+			v.z = _23;
+			v.w = _24;
+			return v;
+		}
+	}
+	
+	//retrieves right vector from matrix
+	public inline function right(?v:Vector)
+	{
+		if( v == null )
+			return new Vector( _11, _12 , _13 , _14  );
+		else
+		{
+			v.x = _11;
+			v.y = _12;
+			v.z = _13;
+			v.w = _14;
+			return v;
+		}
+	}
+	
+}

+ 257 - 0
h3d/Quat.hx

@@ -0,0 +1,257 @@
+package h3d;
+using hxd.Math;
+
+class Quat {
+	
+	public var x : Float;
+	public var y : Float;
+	public var z : Float;
+	public var w : Float;
+	
+	public inline function new( x = 0., y = 0., z = 0., w = 1. ) {
+		this.x = x;
+		this.y = y;
+		this.z = z;
+		this.w = w;
+	}
+	
+	public inline function set(x, y, z, w) {
+		this.x = x;
+		this.y = y;
+		this.z = z;
+		this.w = w;
+	}
+	
+	public inline function identity() {
+		x = y = z = 0;
+		w = 1;
+	}
+	
+	public inline function lengthSq() {
+		return x * x + y * y + z * z + w * w;
+	}
+	
+	public inline function length() {
+		return lengthSq().sqrt();
+	}
+	
+	public function clone() {
+		return new Quat(x, y, z, w);
+	}
+	
+	public inline function initDirection( dir : Vector, ?up : Vector ) {
+		if( up == null ) {
+			// assume [0,0,1] UP version
+			x = -dir.y;
+			y = dir.x;
+			z = 0;
+			w = dir.z + dir.length();
+			normalize();
+		} else {
+			// LeftHanded (RH might use dir.cross(up) ?)
+			var tmp = up.cross(dir);
+			x = tmp.x;
+			y = tmp.y;
+			z = tmp.z;
+			w = dir.length() + dir.dot3(up);
+			normalize();
+		}
+	}
+	
+	public function initRotateAxis( x : Float, y : Float, z : Float, a : Float ) {
+		var sin = (a / 2).sin();
+		var cos = (a / 2).cos();
+		this.x = x * sin;
+		this.y = y * sin;
+		this.z = z * sin;
+		this.w = cos * (x * x + y * y + z * z).sqrt(); // allow not normalized axis
+		normalize();
+	}
+	
+	public function initRotateMatrix( m : Matrix ) {
+		var tr = m._11 + m._22 + m._33;
+		if( tr > 0 ) {
+			var s = (tr + 1.0).sqrt() * 2;
+			var is = 1 / s;
+			x = (m._23 - m._32) * is;
+			y = (m._31 - m._13) * is;
+			z = (m._12 - m._21) * is;
+			w = 0.25 * s;
+		} else if( m._11 > m._22 && m._11 > m._33 ) {
+			var s = (1.0 + m._11 - m._22 - m._33).sqrt() * 2;
+			var is = 1 / s;
+			x = 0.25 * s;
+			y = (m._21 + m._12) * is;
+			z = (m._31 + m._13) * is;
+			w = (m._23 - m._32) * is;
+		} else if( m._22 > m._33 ) {
+			var s = (1.0 + m._22 - m._11 - m._33).sqrt() * 2;
+			var is = 1 / s;
+			x = (m._21 + m._12) * is;
+			y = 0.25 * s;
+			z = (m._32 + m._23) * is;
+			w = (m._31 - m._13) * is;
+		} else {
+			var s = (1.0 + m._33 - m._11 - m._22).sqrt() * 2;
+			var is = 1 / s;
+			x = (m._31 + m._13) * is;
+			y = (m._32 + m._23) * is;
+			z = 0.25 * s;
+			w = (m._12 - m._21) * is;
+		}
+	}
+	
+	public function normalize() {
+		var len = x * x + y * y + z * z + w * w;
+		if( len < hxd.Math.EPSILON ) {
+			x = y = z = 0;
+			w = 1;
+		} else {
+			var m = len.invSqrt();
+			x *= m;
+			y *= m;
+			z *= m;
+			w *= m;
+		}
+	}
+	
+	public function initRotate( ax : Float, ay : Float, az : Float ) {
+		var sinX = ( ax * 0.5 ).sin();
+		var cosX = ( ax * 0.5 ).cos();
+		var sinY = ( ay * 0.5 ).sin();
+		var cosY = ( ay * 0.5 ).cos();
+		var sinZ = ( az * 0.5 ).sin();
+		var cosZ = ( az * 0.5 ).cos();
+		var cosYZ = cosY * cosZ;
+		var sinYZ = sinY * sinZ;
+		x = sinX * cosYZ - cosX * sinYZ;
+		y = cosX * sinY * cosZ + sinX * cosY * sinZ;
+		z = cosX * cosY * sinZ - sinX * sinY * cosZ;
+		w = cosX * cosYZ + sinX * sinYZ;
+	}
+	
+	public inline function add( q : Quat ) {
+		multiply(this, q);
+	}
+	
+	public function multiply( q1 : Quat, q2 : Quat ) {
+		var x2 = q1.x * q2.w + q1.w * q2.x + q1.y * q2.z - q1.z * q2.y;
+		var y2 = q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x;
+		var z2 = q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w;
+		var w2 = q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z;
+		x = x2;
+		y = y2;
+		z = z2;
+		w = w2;
+	}
+	
+	public function toMatrix() {
+		var m = new Matrix();
+		saveToMatrix(m);
+		return m;
+	}
+	
+	public function toEuler() {
+		return new Vector(
+			hxd.Math.atan2(2 * (y * w + x * z), 1 - 2 * (y * y + z * z)),
+			(2 * (x * y + z * w)).asin(),
+			hxd.Math.atan2(2 * (x * w - y * z), 1 - 2 * (x * x + z * z))
+		);
+	}
+	
+	public inline function lerp( q1 : Quat, q2 : Quat, v : Float, nearest = false ) {
+		var v2;
+		if( nearest && q1.dot(q2) < 0 )
+			v2 = v - 1;
+		else
+			v2 = 1 - v;
+		var x = q1.x * v + q2.x * v2;
+		var y = q1.y * v + q2.y * v2;
+		var z = q1.z * v + q2.z * v2;
+		var w = q1.w * v + q2.w * v2;
+		this.x = x;
+		this.y = y;
+		this.z = z;
+		this.w = w;
+	}
+
+	public function slerp( q1 : Quat, q2 : Quat, v : Float ) {
+		var cosHalfTheta = q1.dot(q2);
+		if( cosHalfTheta.abs() >= 1 ) {
+			this.x = q1.x;
+			this.y = q1.y;
+			this.z = q1.z;
+			this.w = q1.w;
+			return;
+		}
+		var halfTheta = cosHalfTheta.acos();
+		var invSinHalfTheta = (1 - cosHalfTheta * cosHalfTheta).invSqrt();
+		if( invSinHalfTheta.abs() > 1e3 ) {
+			this.lerp(q1, q2, 0.5, true);
+			return;
+		}
+		var a = ((1 - v) * halfTheta).sin() * invSinHalfTheta;
+		var b = (v * halfTheta).sin() * invSinHalfTheta * (cosHalfTheta < 0 ? -1 : 1);
+		this.x = q1.x * a + q2.x * b;
+		this.y = q1.y * a + q2.y * b;
+		this.z = q1.z * a + q2.z * b;
+		this.w = q1.w * a + q2.w * b;
+	}
+	
+	public inline function conjugate() {
+		x *= -1;
+		y *= -1;
+		z *= -1;
+	}
+	
+	/**
+		Negate the quaternion: this will not change the actual angle, use `conjugate` for that.
+	**/
+	public inline function negate() {
+		x *= -1;
+		y *= -1;
+		z *= -1;
+		w *= -1;
+	}
+	
+	public inline function dot( q : Quat ) {
+		return x * q.x + y * q.y + z * q.z + w * q.w;
+	}
+	
+	/**
+		Save to a Left-Handed matrix
+	**/
+	public function saveToMatrix( m : h3d.Matrix ) {
+		var xx = x * x;
+		var xy = x * y;
+		var xz = x * z;
+		var xw = x * w;
+		var yy = y * y;
+		var yz = y * z;
+		var yw = y * w;
+		var zz = z * z;
+		var zw = z * w;
+		m._11 = 1 - 2 * ( yy + zz );
+		m._12 = 2 * ( xy + zw );
+		m._13 = 2 * ( xz - yw );
+		m._14 = 0;
+		m._21 = 2 * ( xy - zw );
+		m._22 = 1 - 2 * ( xx + zz );
+		m._23 = 2 * ( yz + xw );
+		m._24 = 0;
+		m._31 = 2 * ( xz + yw );
+		m._32 = 2 * ( yz - xw );
+		m._33 = 1 - 2 * ( xx + yy );
+		m._34 = 0;
+		m._41 = 0;
+		m._42 = 0;
+		m._43 = 0;
+		m._44 = 1;
+		return m;
+	}
+	
+	public function toString() {
+		return '{${x.fmt()},${y.fmt()},${z.fmt()},${w.fmt()}}';
+	}
+	
+}

+ 163 - 0
h3d/Vector.hx

@@ -0,0 +1,163 @@
+package h3d;
+using hxd.Math;
+
+class Vector {
+
+	public var x : Float;
+	public var y : Float;
+	public var z : Float;
+	public var w : Float;
+
+	public inline function new( x = 0., y = 0., z = 0., w = 1. ) {
+		this.x = x;
+		this.y = y;
+		this.z = z;
+		this.w = w;
+	}
+	
+	public inline function distance( v : Vector ) {
+		return Math.sqrt(distanceSq(v));
+	}
+
+	public inline function distanceSq( v : Vector ) {
+		var dx = v.x - x;
+		var dy = v.y - y;
+		var dz = v.z - z;
+		return dx * dx + dy * dy + dz * dz;
+	}
+
+	public inline function sub( v : Vector ) {
+		return new Vector(x - v.x, y - v.y, z - v.z, w - v.w);
+	}
+
+	public inline function add( v : Vector ) {
+		return new Vector(x + v.x, y + v.y, z + v.z, w + v.w);
+	}
+
+	public inline function cross( v : Vector ) {
+		return new Vector(y * v.z - z * v.y, z * v.x - x * v.z,  x * v.y - y * v.x, 1);
+	}
+	
+	public inline function reflect( n : Vector ) {
+		var k = 2 * this.dot3(n);
+		return new Vector(x - k * n.x, y - k * n.y, z - k * n.z, 1);
+	}
+
+	public inline function dot3( v : Vector ) {
+		return x * v.x + y * v.y + z * v.z;
+	}
+
+	public inline function dot4( v : Vector ) {
+		return x * v.x + y * v.y + z * v.z + w * v.w;
+	}
+
+	public inline function lengthSq() {
+		return x * x + y * y + z * z;
+	}
+
+	public inline function length() {
+		return lengthSq().sqrt();
+	}
+
+	public function normalize() {
+		var k = lengthSq();
+		if( k < hxd.Math.EPSILON ) k = 0 else k = k.invSqrt();
+		x *= k;
+		y *= k;
+		z *= k;
+	}
+
+	public function set(x,y,z,w=1.) {
+		this.x = x;
+		this.y = y;
+		this.z = z;
+		this.w = w;
+	}
+
+	public inline function scale3( f : Float ) {
+		x *= f;
+		y *= f;
+		z *= f;
+	}
+	
+	public inline function project( m : Matrix ) {
+		var px = x * m._11 + y * m._21 + z * m._31 + w * m._41;
+		var py = x * m._12 + y * m._22 + z * m._32 + w * m._42;
+		var pz = x * m._13 + y * m._23 + z * m._33 + w * m._43;
+		var iw = 1 / (x * m._14 + y * m._24 + z * m._34 + w * m._44);
+		x = px * iw;
+		y = py * iw;
+		z = pz * iw;
+		w = 1;
+	}
+	
+	public inline function lerp( v1 : Vector, v2 : Vector, k : Float ) {
+		var x = Math.lerp(v1.x, v2.x, k);
+		var y = Math.lerp(v1.y, v2.y, k);
+		var z = Math.lerp(v1.z, v2.z, k);
+		var w = Math.lerp(v1.w, v2.w, k);
+		this.x = x;
+		this.y = y;
+		this.z = z;
+		this.w = w;
+	}
+	
+	public inline function transform3x4( m : Matrix ) {
+		var px = x * m._11 + y * m._21 + z * m._31 + w * m._41;
+		var py = x * m._12 + y * m._22 + z * m._32 + w * m._42;
+		var pz = x * m._13 + y * m._23 + z * m._33 + w * m._43;
+		x = px;
+		y = py;
+		z = pz;
+	}
+
+	public inline function transform3x3( m : Matrix ) {
+		var px = x * m._11 + y * m._21 + z * m._31;
+		var py = x * m._12 + y * m._22 + z * m._32;
+		var pz = x * m._13 + y * m._23 + z * m._33;
+		x = px;
+		y = py;
+		z = pz;
+	}
+	
+	public inline function transform( m : Matrix ) {
+		var px = x * m._11 + y * m._21 + z * m._31 + w * m._41;
+		var py = x * m._12 + y * m._22 + z * m._32 + w * m._42;
+		var pz = x * m._13 + y * m._23 + z * m._33 + w * m._43;
+		var pw = x * m._14 + y * m._24 + z * m._34 + w * m._44;
+		x = px;
+		y = py;
+		z = pz;
+		w = pw;
+	}
+
+	public inline function loadColor( c : Int, scale : Float = 1.0 ) {
+		var s = scale / 255;
+		x = ((c >> 16) & 0xFF) * s;
+		y = ((c >> 8) & 0xFF) * s;
+		z = (c & 0xFF) * s;
+		w = (c >>> 24) * s;
+	}
+	
+	public inline function toPoint() {
+		return new h3d.col.Point(x, y, z);
+	}
+	
+	public inline function toColor() {
+		return (Std.int(w.clamp() * 255 + 0.499) << 24) | (Std.int(x.clamp() * 255 + 0.499) << 16) | (Std.int(y.clamp() * 255 + 0.499) << 8) | Std.int(z.clamp() * 255 + 0.499);
+	}
+	
+	public static inline function fromColor( c : Int, scale : Float = 1.0 ) {
+		var s = scale / 255;
+		return new Vector(((c>>16)&0xFF)*s,((c>>8)&0xFF)*s,(c&0xFF)*s,(c >>> 24)*s);
+	}
+	
+	public inline function clone() {
+		return new Vector(x,y,z,w);
+	}
+
+	public function toString() {
+		return '{${x.fmt()},${y.fmt()},${z.fmt()},${w.fmt()}}';
+	}
+
+}

+ 225 - 0
h3d/anim/Animation.hx

@@ -0,0 +1,225 @@
+package h3d.anim;
+
+class AnimatedObject {
+	
+	public var objectName : String;
+	public var targetObject : h3d.scene.Object;
+	public var targetSkin : h3d.scene.Skin;
+	public var targetJoint : Int;
+	
+	public function new(name) {
+		this.objectName = name;
+	}
+	
+	public function clone() {
+		return new AnimatedObject(objectName);
+	}
+	
+}
+
+private class AnimWait {
+	public var frame : Float;
+	public var callb : Void -> Void;
+	public var next : AnimWait;
+	public function new(f, c, n) {
+		frame = f;
+		callb = c;
+		next = n;
+	}
+}
+
+class Animation {
+	
+	static inline var EPSILON = 0.000001;
+	
+	public var name : String;
+	public var frameCount(default, null) : Int;
+	public var sampling(default,null) : Float;
+	public var frame(default, null) : Float;
+	
+	public var speed : Float;
+	public var onAnimEnd : Void -> Void;
+	
+	public var pause : Bool;
+	public var loop : Bool;
+	
+	var waits : AnimWait;
+	var isInstance : Bool;
+	var objects : Array<AnimatedObject>;
+	
+	function new(name, frameCount, sampling) {
+		this.name = name;
+		this.frameCount = frameCount;
+		this.sampling = sampling;
+		objects = [];
+		frame = 0.;
+		speed = 1.;
+		loop = true;
+		pause = false;
+	}
+	
+	/**
+		Register a callback function that will be called once when a frame is reached.
+	**/
+	public function waitForFrame( f : Float, callb : Void -> Void ) {
+		// add sorted
+		var prev = null;
+		var cur = waits;
+		while( cur != null ) {
+			if( cur.frame > f )
+				break;
+			prev = cur;
+			cur = cur.next;
+		}
+		if( prev == null )
+			waits = new AnimWait(f, callb, waits);
+		else
+			prev.next = new AnimWait(f, callb, prev.next);
+	}
+	
+	/**
+		Remove all frame listeners
+	**/
+	public function clearWaits() {
+		waits = null;
+	}
+	
+	public function setFrame( f : Float ) {
+		frame = f % frameCount;
+		if( frame < 0 ) frame += frameCount;
+	}
+	
+	function clone( ?a : Animation ) : Animation {
+		if( a == null )
+			a = new Animation(name, frameCount, sampling);
+		a.objects = objects;
+		a.speed = speed;
+		a.loop = loop;
+		a.pause = pause;
+		return a;
+	}
+	
+	function initInstance() {
+		isInstance = true;
+	}
+	
+	public function createInstance( base : h3d.scene.Object ) {
+		var currentSkin : h3d.scene.Skin = null;
+		var objects = [for( a in this.objects ) a.clone()];
+		var a = clone();
+		a.objects = objects;
+		a.bind(base);
+		a.initInstance();
+		return a;
+	}
+	
+	/**
+		If one of the animated object has been changed, it is necessary to call bind() so the animation can keep with the change.
+	**/
+	@:access(h3d.scene.Skin.skinData)
+	public function bind( base : h3d.scene.Object ) {
+		var currentSkin : h3d.scene.Skin = null;
+		for( a in objects ) {
+			if( currentSkin != null ) {
+				// quick lookup for joints (prevent creating a temp object)
+				var j = currentSkin.skinData.namedJoints.get(a.objectName);
+				if( j != null ) {
+					a.targetSkin = currentSkin;
+					a.targetJoint = j.index;
+				}
+			}
+			var obj = base.getObjectByName(a.objectName);
+			if( obj == null )
+				throw a.objectName + " was not found";
+			var joint = Std.instance(obj, h3d.scene.Skin.Joint);
+			if( joint != null ) {
+				currentSkin = cast joint.parent;
+				a.targetSkin = currentSkin;
+				a.targetJoint = joint.index;
+			} else {
+				a.targetObject = obj;
+			}
+		}
+	}
+	
+	/**
+		Synchronize the target object matrix.
+		If decompose is true, then the rotation quaternion is stored in [m12,m13,m21,m23] instead of mixed with the scale.
+	**/
+	public function sync( decompose : Bool = false ) {
+		// should be overridden in subclass
+		throw "assert";
+	}
+	
+	function isPlaying() {
+		return !pause && (speed < 0 ? -speed : speed) > EPSILON;
+	}
+
+	function endFrame() {
+		return frameCount;
+	}
+	
+	public function update(dt:Float) : Float {
+		if( !isInstance )
+			throw "You must instanciate this animation first";
+		
+		if( !isPlaying() )
+			return 0;
+		
+		// check waits
+		var w = waits;
+		var prev = null;
+		while( w != null ) {
+			var wt = (w.frame - frame) / (speed * sampling);
+			// don't run if we're already on the frame (allow to set waitForFrame on the same frame we are)
+			if( wt <= 0 ) {
+				prev = w;
+				w = w.next;
+				continue;
+			}
+			if( wt > dt )
+				break;
+			frame = w.frame;
+			dt -= wt;
+			if( prev == null )
+				waits = w.next;
+			else
+				prev.next = w.next;
+			w.callb();
+			return dt;
+		}
+		
+		// check on anim end
+		if( onAnimEnd != null ) {
+			var end = endFrame();
+			var et = (end - frame) / (speed * sampling);
+			if( et <= dt ) {
+				var f = end - EPSILON;
+				frame = f;
+				dt -= et;
+				onAnimEnd();
+				// if we didn't change the frame or paused the animation, let's end it
+				if( frame == f && isPlaying() ) {
+					if( loop ) {
+						frame = 0;
+					} else {
+						// don't loop infinitely
+						dt = 0;
+					}
+				}
+				return dt;
+			}
+		}
+		
+		// update frame
+		frame += dt * speed * sampling;
+		if( frame >= frameCount ) {
+			if( loop )
+				frame %= frameCount;
+			else
+				frame = frameCount - EPSILON;
+		}
+		return 0;
+	}
+	
+}

+ 80 - 0
h3d/anim/FrameAnimation.hx

@@ -0,0 +1,80 @@
+package h3d.anim;
+import h3d.anim.Animation;
+
+class FrameObject extends AnimatedObject {
+	public var frames : haxe.ds.Vector<h3d.Matrix>;
+	public var alphas : haxe.ds.Vector<Float>;
+	
+	override function clone() : AnimatedObject {
+		var o = new FrameObject(objectName);
+		o.frames = frames;
+		o.alphas = alphas;
+		return o;
+	}
+}
+	
+class FrameAnimation extends Animation {
+
+	var syncFrame : Int;
+
+	public function new(name,frame,sampling) {
+		super(name,frame,sampling);
+		syncFrame = -1;
+	}
+	
+	public function addCurve( objName, frames ) {
+		var f = new FrameObject(objName);
+		f.frames = frames;
+		objects.push(f);
+	}
+	
+	public function addAlphaCurve( objName, alphas ) {
+		var f = new FrameObject(objName);
+		f.alphas = alphas;
+		objects.push(f);
+	}
+	
+	inline function getFrames() : Array<FrameObject> {
+		return cast objects;
+	}
+	
+	override function initInstance() {
+		super.initInstance();
+		for( a in getFrames() )
+			if( a.alphas != null && (a.targetObject == null || !a.targetObject.isMesh()) )
+				throw a.objectName + " should be a mesh";
+	}
+	
+	override function clone(?a:Animation) {
+		if( a == null )
+			a = new FrameAnimation(name, frameCount, sampling);
+		super.clone(a);
+		return a;
+	}
+	
+	@:access(h3d.scene.Skin)
+	override function sync( decompose = false ) {
+		if( decompose ) throw "Decompose not supported on Frame Animation";
+		var frame = Std.int(frame);
+		if( frame < 0 ) frame = 0 else if( frame >= frameCount ) frame = frameCount - 1;
+		if( frame == syncFrame )
+			return;
+		syncFrame = frame;
+		for( o in getFrames() ) {
+			if( o.alphas != null ) {
+				var mat = o.targetObject.toMesh().material;
+				if( mat.colorMul == null ) {
+					mat.colorMul = new Vector(1, 1, 1, 1);
+					if( mat.blendDst == Zero )
+						mat.blend(SrcAlpha, OneMinusSrcAlpha);
+				}
+				mat.colorMul.w = o.alphas[frame];
+			} else if( o.targetSkin != null ) {
+				o.targetSkin.currentRelPose[o.targetJoint] = o.frames[frame];
+				o.targetSkin.jointsUpdated = true;
+			} else
+				o.targetObject.defaultTransform = o.frames[frame];
+		}
+	}
+	
+}

+ 188 - 0
h3d/anim/LinearAnimation.hx

@@ -0,0 +1,188 @@
+package h3d.anim;
+import h3d.anim.Animation;
+
+class LinearFrame {
+	public var tx : Float;
+	public var ty : Float;
+	public var tz : Float;
+	public var qx : Float;
+	public var qy : Float;
+	public var qz : Float;
+	public var qw : Float;
+	public var sx : Float;
+	public var sy : Float;
+	public var sz : Float;
+	public function new() {
+	}
+}
+
+class LinearObject extends AnimatedObject {
+	public var hasRotation : Bool;
+	public var hasScale : Bool;
+	public var frames : haxe.ds.Vector<LinearFrame>;
+	public var alphas : haxe.ds.Vector<Float>;
+	public var matrix : h3d.Matrix;
+	override function clone() : AnimatedObject {
+		var o = new LinearObject(objectName);
+		o.hasRotation = hasRotation;
+		o.hasScale = hasScale;
+		o.frames = frames;
+		o.alphas = alphas;
+		return o;
+	}
+}
+	
+class LinearAnimation extends Animation {
+
+	var syncFrame : Float;
+
+	public function new(name,frame,sampling) {
+		super(name,frame,sampling);
+		syncFrame = -1;
+	}
+	
+	public function addCurve( objName, frames, hasRot, hasScale ) {
+		var f = new LinearObject(objName);
+		f.frames = frames;
+		f.hasRotation = hasRot;
+		f.hasScale = hasScale;
+		objects.push(f);
+	}
+	
+	public function addAlphaCurve( objName, alphas ) {
+		var f = new LinearObject(objName);
+		f.alphas = alphas;
+		objects.push(f);
+	}
+	
+	inline function getFrames() : Array<LinearObject> {
+		return cast objects;
+	}
+	
+	override function initInstance() {
+		super.initInstance();
+		for( a in getFrames() ) {
+			a.matrix = new h3d.Matrix();
+			a.matrix.identity();
+			if( a.alphas != null && (a.targetObject == null || !a.targetObject.isMesh()) )
+				throw a.objectName + " should be a mesh";
+		}
+	}
+	
+	override function clone(?a:Animation) {
+		if( a == null )
+			a = new LinearAnimation(name, frameCount, sampling);
+		super.clone(a);
+		return a;
+	}
+	
+	override function endFrame() {
+		return loop ? frameCount : frameCount - 1;
+	}
+	
+	@:access(h3d.scene.Skin)
+	override function sync( decompose = false ) {
+		if( frame == syncFrame && !decompose )
+			return;
+		var frame1 = Std.int(frame);
+		var frame2 = (frame1 + 1) % frameCount;
+		var k2 = frame - frame1;
+		var k1 = 1 - k2;
+		if( frame1 < 0 ) frame1 = frame2 = 0 else if( frame >= frameCount ) frame1 = frame2 = frameCount - 1;
+		syncFrame = frame;
+		for( o in getFrames() ) {
+			if( o.alphas != null ) {
+				var mat = o.targetObject.toMesh().material;
+				if( mat.colorMul == null ) {
+					mat.colorMul = new Vector(1, 1, 1, 1);
+					if( mat.blendDst == Zero )
+						mat.blend(SrcAlpha, OneMinusSrcAlpha);
+				}
+				mat.colorMul.w = o.alphas[frame1] * k1 + o.alphas[frame2] * k2;
+				continue;
+			}
+			var f1 = o.frames[frame1], f2 = o.frames[frame2];
+			
+			var m = o.matrix;
+			
+			m._41 = f1.tx * k1 + f2.tx * k2;
+			m._42 = f1.ty * k1 + f2.ty * k2;
+			m._43 = f1.tz * k1 + f2.tz * k2;
+			
+			if( o.hasRotation ) {
+				// qlerp nearest
+				var dot = f1.qx * f2.qx + f1.qy * f2.qy + f1.qz * f2.qz + f1.qw * f2.qw;
+				var q2 = dot < 0 ? -k2 : k2;
+				var qx = f1.qx * k1 + f2.qx * q2;
+				var qy = f1.qy * k1 + f2.qy * q2;
+				var qz = f1.qz * k1 + f2.qz * q2;
+				var qw = f1.qw * k1 + f2.qw * q2;
+				// make sure the resulting quaternion is normalized
+				var ql = 1 / Math.sqrt(qx * qx + qy * qy + qz * qz + qw * qw);
+				qx *= ql;
+				qy *= ql;
+				qz *= ql;
+				qw *= ql;
+				
+				if( decompose ) {
+					m._12 = qx;
+					m._13 = qy;
+					m._21 = qz;
+					m._23 = qw;
+					if( o.hasScale ) {
+						m._11 = f1.sx * k1 + f2.sx * k2;
+						m._22 = f1.sy * k1 + f2.sy * k2;
+						m._33 = f1.sz * k1 + f2.sz * k2;
+					}
+				} else {
+					// quaternion to matrix
+					var xx = qx * qx;
+					var xy = qx * qy;
+					var xz = qx * qz;
+					var xw = qx * qw;
+					var yy = qy * qy;
+					var yz = qy * qz;
+					var yw = qy * qw;
+					var zz = qz * qz;
+					var zw = qz * qw;
+					m._11 = 1 - 2 * ( yy + zz );
+					m._12 = 2 * ( xy + zw );
+					m._13 = 2 * ( xz - yw );
+					m._21 = 2 * ( xy - zw );
+					m._22 = 1 - 2 * ( xx + zz );
+					m._23 = 2 * ( yz + xw );
+					m._31 = 2 * ( xz + yw );
+					m._32 = 2 * ( yz - xw );
+					m._33 = 1 - 2 * ( xx + yy );
+					if( o.hasScale ) {
+						var sx = f1.sx * k1 + f2.sx * k2;
+						var sy = f1.sy * k1 + f2.sy * k2;
+						var sz = f1.sz * k1 + f2.sz * k2;
+						m._11 *= sx;
+						m._12 *= sx;
+						m._13 *= sx;
+						m._21 *= sy;
+						m._22 *= sy;
+						m._23 *= sy;
+						m._31 *= sz;
+						m._32 *= sz;
+						m._33 *= sz;
+					}
+				}
+				
+			} else if( o.hasScale ) {
+				m._11 = f1.sx * k1 + f2.sx * k2;
+				m._22 = f1.sy * k1 + f2.sy * k2;
+				m._33 = f1.sz * k1 + f2.sz * k2;
+			}
+			
+			
+			if( o.targetSkin != null ) {
+				o.targetSkin.currentRelPose[o.targetJoint] = o.matrix;
+				o.targetSkin.jointsUpdated = true;
+			} else
+				o.targetObject.defaultTransform = o.matrix;
+		}
+	}
+	
+}

+ 35 - 0
h3d/anim/SimpleBlend.hx

@@ -0,0 +1,35 @@
+package h3d.anim;
+
+class SimpleBlend extends Transition {
+	
+	public var objectsMap : Map<String,Bool>;
+	
+	public function new( anim1 : Animation, anim2 : Animation, objects : Map < String, Bool > ) {
+		super("blend", anim1, anim2);
+		this.objectsMap = objects;
+	}
+	
+	override function clone(?a : Animation) : Animation {
+		var a : SimpleBlend = cast a;
+		if( a == null )
+			a = new SimpleBlend(anim1, anim2, objectsMap);
+		super.clone(a);
+		a.objectsMap = objectsMap;
+		return a;
+	}
+	
+	override function createInstance( base ) {
+		var a = new SimpleBlend(anim1, anim2, objectsMap);
+		a.anim1 = anim1.createInstance(base);
+		a.anim2 = anim2.createInstance(base);
+		for( o in a.anim1.objects.copy() )
+			if( objectsMap.get(o.objectName) )
+				a.anim1.objects.remove(o);
+		for( o in a.anim2.objects.copy() )
+			if( !objectsMap.get(o.objectName) )
+				a.anim2.objects.remove(o);
+		a.isInstance = true;
+		return a;
+	}
+	
+}

+ 201 - 0
h3d/anim/Skin.hx

@@ -0,0 +1,201 @@
+package h3d.anim;
+
+class Joint {
+
+	public var index : Int;
+	public var name : String;
+	public var bindIndex : Int;
+	public var splitIndex : Int;
+	public var defMat : h3d.Matrix; // the default bone matrix
+	public var transPos : h3d.Matrix; // inverse pose matrix
+	public var parent : Joint;
+	public var subs : Array<Joint>;
+	
+	public function new() {
+		bindIndex = -1;
+		subs = [];
+	}
+	
+}
+
+private class Influence {
+	public var j : Joint;
+	public var w : Float;
+	public function new(j, w) {
+		this.j = j;
+		this.w = w;
+	}
+}
+
+class Skin {
+	
+	public var vertexCount(default, null) : Int;
+	public var bonesPerVertex(default,null) : Int;
+	public var vertexJoints : haxe.ds.Vector<Int>;
+	public var vertexWeights : haxe.ds.Vector<Float>;
+	public var rootJoints(default,null) : Array<Joint>;
+	public var namedJoints(default,null) : Map<String,Joint>;
+	public var allJoints(default,null) : Array<Joint>;
+	public var boundJoints(default,null) : Array<Joint>;
+	public var primitive : h3d.prim.Primitive;
+	
+	// spliting
+	public var splitJoints(default, null) : Array<Array<Joint>>;
+	public var triangleGroups : haxe.ds.Vector<Int>;
+	
+	var envelop : Array<Array<Influence>>;
+	
+	public function new( vertexCount, bonesPerVertex ) {
+		this.vertexCount = vertexCount;
+		this.bonesPerVertex = bonesPerVertex;
+		vertexJoints = new haxe.ds.Vector(vertexCount * bonesPerVertex);
+		vertexWeights = new haxe.ds.Vector(vertexCount * bonesPerVertex);
+		envelop = [];
+	}
+	
+	public function setJoints( joints : Array<Joint>, roots : Array<Joint> ) {
+		rootJoints = roots;
+		allJoints = joints;
+		namedJoints = new Map();
+		for( j in joints )
+			if( j.name != null )
+				namedJoints.set(j.name, j);
+	}
+	
+	public inline function addInfluence( vid : Int, j : Joint, w : Float ) {
+		var il = envelop[vid];
+		if( il == null )
+			il = envelop[vid] = [];
+		il.push(new Influence(j,w));
+	}
+
+	function sortInfluences( i1 : Influence, i2 : Influence ) {
+		return i2.w > i1.w ? 1 : -1;
+	}
+	
+	public inline function isSplit() {
+		return splitJoints != null;
+	}
+	
+	public function initWeights() {
+		boundJoints = [];
+		var pos = 0;
+		for( i in 0...vertexCount ) {
+			var il = envelop[i];
+			if( il == null ) il = [];
+			il.sort(sortInfluences);
+			if( il.length > bonesPerVertex )
+				il = il.slice(0, bonesPerVertex);
+			var tw = 0.;
+			for( i in il )
+				tw += i.w;
+			tw = 1 / tw;
+			for( i in 0...bonesPerVertex ) {
+				var i = il[i];
+				if( i == null ) {
+					vertexJoints[pos] = 0;
+					vertexWeights[pos] = 0;
+				} else {
+					if( i.j.bindIndex == -1 ) {
+						i.j.bindIndex = boundJoints.length;
+						boundJoints.push(i.j);
+					}
+					vertexJoints[pos] = i.j.bindIndex;
+					vertexWeights[pos] = i.w * tw;
+				}
+				pos++;
+			}
+		}
+		envelop = null;
+	}
+	
+	public function split( maxBones : Int, index : Array<Int> ) {
+		if( isSplit() )
+			return true;
+		if( boundJoints.length <= maxBones )
+			return false;
+
+		splitJoints = [];
+		triangleGroups = new haxe.ds.Vector(Std.int(index.length / 3));
+		
+		// collect joints groups used by triangles
+		var curGroup = new Array<Joint>(), curJoints = [];
+		var ipos = 0, tpos = 0;
+		while( ipos <= index.length ) {
+			var tjoints = [], flush = false;
+			if( ipos < index.length ) {
+				for( k in 0...3 ) {
+					var vid = index[ipos + k];
+					for( b in 0...bonesPerVertex ) {
+						var bidx = vid * bonesPerVertex + b;
+						if( vertexWeights[bidx] == 0 ) continue;
+						var j = boundJoints[vertexJoints[bidx]];
+						if( curJoints[j.bindIndex] == null ) {
+							curJoints[j.bindIndex] = j;
+							tjoints.push(j);
+						}
+					}
+				}
+			}
+			if( curGroup.length + tjoints.length <= maxBones && ipos < index.length ) {
+				for( j in tjoints )
+					curGroup.push(j);
+				triangleGroups[tpos++] = splitJoints.length;
+				ipos += 3;
+			} else {
+				splitJoints.push(curGroup);
+				curGroup = [];
+				curJoints = [];
+				if( ipos == index.length ) break;
+			}
+		}
+		
+		// assign split indexes to joints
+		var groups = [for( i in 0...splitJoints.length ) { id : i, reserved : [], joints : splitJoints[i] }];
+		var joints = [for( j in boundJoints ) { j : j, groups : [], index : -1 } ];
+		for( g in groups )
+			for( j in g.joints )
+				joints[j.bindIndex].groups.push(g);
+		joints.sort(function(j1, j2) return j2.groups.length - j1.groups.length);
+		for( j in joints ) {
+			for( i in 0...maxBones ) {
+				var ok = true;
+				for( g in j.groups )
+					if( g.reserved[i] != null ) {
+						ok = false;
+						break;
+					}
+				if( ok ) {
+					j.j.splitIndex = i;
+					for( g in j.groups )
+						g.reserved[i] = j.j;
+					break;
+				}
+			}
+			// not very good news if this happen.
+			// It means that we need a smarter way to assign the joint indexes
+			// Maybe by presorting triangles based on bone usage to have more coherent groups
+			if( j.j.splitIndex < 0 ) throw "Bone conflict while spliting groups";
+		}
+
+		// rebuild joints list (and fill holes)
+		splitJoints = [];
+		for( g in groups ) {
+			var jl = [];
+			for( i in 0...g.reserved.length ) {
+				var j = g.reserved[i];
+				if( j == null ) j = boundJoints[0];
+				jl.push(j);
+			}
+			splitJoints.push(jl);
+		}
+		
+		// rebind
+		for( i in 0...vertexJoints.length )
+			vertexJoints[i] = boundJoints[vertexJoints[i]].splitIndex;
+
+		return true;
+	}
+	
+	
+}

+ 127 - 0
h3d/anim/SmoothTransition.hx

@@ -0,0 +1,127 @@
+package h3d.anim;
+
+class SmoothedObject extends Animation.AnimatedObject {
+	public var tmpMatrix : h3d.Matrix;
+	public var outMatrix : h3d.Matrix;
+	public var isAnim1 : Bool;
+	public var isAnim2 : Bool;
+	public function new(name) {
+		super(name);
+		outMatrix = h3d.Matrix.I();
+	}
+}
+
+class SmoothTransition extends Transition {
+	
+	static var MZERO = h3d.Matrix.L([
+		1, 0, 0, 0,
+		0, 1, 1, 0,
+		0, 0, 1, 0,
+		0, 0, 0, 1,
+	]);
+	
+	public var blendFactor : Float;
+	var tspeed : Float;
+	
+	public function new(current, target, speed) {
+		super("smooth", current, target);
+		blendFactor = 0.;
+		this.tspeed = speed;
+		if( !anim1.isInstance || !anim2.isInstance )
+			throw "Both animations must be instances";
+		this.isInstance = true;
+		initObjects();
+	}
+	
+	function initObjects() {
+		var allObjects = new Map();
+		var mzero = MZERO;
+		for( o in anim1.objects ) {
+			var so = new SmoothedObject(o.objectName);
+			so.targetJoint = o.targetJoint;
+			so.targetSkin = o.targetSkin;
+			so.targetObject = o.targetObject;
+			allObjects.set(o.objectName, so);
+			objects.push(so);
+			so.isAnim1 = true;
+		}
+		for( o in anim2.objects ) {
+			var so = allObjects.get(o.objectName);
+			if( so == null ) {
+				so = new SmoothedObject(o.objectName);
+				so.targetJoint = o.targetJoint;
+				so.targetSkin = o.targetSkin;
+				so.targetObject = o.targetObject;
+				so.tmpMatrix = mzero;
+				allObjects.set(o.objectName, so);
+				objects.push(so);
+			}
+			so.isAnim2 = true;
+		}
+	}
+	
+	override function bind( base ) {
+		super.bind(base);
+		this.objects = [];
+		initObjects();
+	}
+	
+	@:access(h3d.scene.Skin)
+	override function sync( decompose = false ) {
+		if( decompose ) throw "assert";
+		var objects : Array<SmoothedObject> = cast objects;
+		anim1.sync(true);
+		for( o in objects ) {
+			if( !o.isAnim1 )
+				continue;
+			o.tmpMatrix = if( o.targetSkin != null ) o.targetSkin.currentRelPose[o.targetJoint] else o.targetObject.defaultTransform;
+		}
+		anim2.sync(true);
+		var a = 1 - blendFactor, b = blendFactor;
+		var mzero = MZERO;
+		var q1 = new h3d.Quat(), q2 = new h3d.Quat(), qout = new h3d.Quat();
+		for( o in objects ) {
+			var m1 = o.tmpMatrix;
+			var m2 = if( !o.isAnim2 ) mzero else if( o.targetSkin != null ) o.targetSkin.currentRelPose[o.targetJoint] else o.targetObject.defaultTransform;
+			var m = o.outMatrix;
+			// interpolate rotation
+			q1.set(m1._12, m1._13, m1._21, m1._23);
+			q2.set(m2._12, m2._13, m2._21, m2._23);
+			// shortest path
+			qout.lerp(q1, q2, a, true);
+			qout.normalize();
+			qout.saveToMatrix(m);
+			// interpolate scale
+			var sx = m1._11 * a + m2._11 * b;
+			var sy = m1._22 * a + m2._22 * b;
+			var sz = m1._33 * a + m2._33 * b;
+			m._11 *= sx;
+			m._12 *= sx;
+			m._13 *= sx;
+			m._21 *= sy;
+			m._22 *= sy;
+			m._23 *= sy;
+			m._31 *= sz;
+			m._32 *= sz;
+			m._33 *= sz;
+			// interpolate translation
+			m._41 = m1._41 * a + m2._41 * b;
+			m._42 = m1._42 * a + m2._42 * b;
+			m._43 = m1._43 * a + m2._43 * b;
+			// save matrix
+			if( o.targetSkin != null ) o.targetSkin.currentRelPose[o.targetJoint] = m else o.targetObject.defaultTransform = m;
+		}
+	}
+	
+	override function update( dt : Float ) : Float {
+		var rt = super.update(dt);
+		var st = dt - rt;
+		blendFactor += st * tspeed;
+		if( blendFactor >= 1 ) {
+			blendFactor = 1;
+			onAnimEnd();
+		}
+		return rt;
+	}
+	
+}

+ 60 - 0
h3d/anim/Transition.hx

@@ -0,0 +1,60 @@
+package h3d.anim;
+
+class Transition extends Animation {
+	
+	public var anim1 : Animation;
+	public var anim2 : Animation;
+	
+	public function new( transitionName : String, anim1 : Animation, anim2 : Animation ) {
+		var r1 = 1, r2 = 1;
+		while( true ) {
+			var d = anim1.frameCount * r1 - anim2.frameCount * r2;
+			if( d == 0 ) break;
+			if( d < 0 ) r1++ else r2++;
+		}
+		super(transitionName+"("+anim1.name+","+anim2.name+")",anim1.frameCount * r1, anim1.sampling);
+		this.anim1 = anim1;
+		this.anim2 = anim2;
+	}
+	
+	override function setFrame( f : Float ) {
+		super.setFrame(f);
+		anim1.setFrame(frame % anim1.frameCount);
+		anim2.setFrame(frame % anim2.frameCount);
+	}
+	
+	override function clone(?a : Animation) : Animation {
+		var a : Transition = cast a;
+		if( a == null )
+			a = new Transition(this.name.split("(")[0], anim1, anim2);
+		super.clone(a);
+		a.anim1 = anim1.clone();
+		a.anim2 = anim2.clone();
+		return a;
+	}
+	
+	override function sync( decompose : Bool = false ) {
+		if( decompose )
+			throw "Decompose not supported on transition";
+		anim1.sync();
+		anim2.sync();
+	}
+	
+	override function bind(base) {
+		anim1.bind(base);
+		anim2.bind(base);
+	}
+	
+	override function update(dt:Float) {
+		var rt = super.update(dt);
+		var st = dt - rt;
+		var tmp = st;
+		while( tmp > 0 )
+			tmp = anim1.update(tmp);
+		var tmp = st;
+		while( tmp > 0 )
+			tmp = anim2.update(tmp);
+		return rt;
+	}
+	
+}

+ 297 - 0
h3d/col/Bounds.hx

@@ -0,0 +1,297 @@
+package h3d.col;
+import hxd.Math;
+
+class Bounds {
+	
+	public var xMin : Float;
+	public var xMax : Float;
+	public var yMin : Float;
+	public var yMax : Float;
+	public var zMin : Float;
+	public var zMax : Float;
+	
+	public inline function new() {
+		empty();
+	}
+	
+	public function inFrustum( mvp : Matrix ) {
+
+		// left
+		if( testPlane(new Plane(mvp._14 + mvp._11, mvp._24 + mvp._21 , mvp._34 + mvp._31, -(mvp._44 + mvp._41))) < 0 )
+			return false;
+		
+		// right
+		if( testPlane(new Plane(mvp._14 - mvp._11, mvp._24 - mvp._21 , mvp._34 - mvp._31, mvp._41 - mvp._44)) < 0 )
+			return false;
+
+		// bottom
+		if( testPlane(new Plane(mvp._14 + mvp._12, mvp._24 + mvp._22 , mvp._34 + mvp._32, -(mvp._44 + mvp._42))) < 0 )
+			return false;
+
+		// top
+		if( testPlane(new Plane(mvp._14 - mvp._12, mvp._24 - mvp._22 , mvp._34 - mvp._32, mvp._42 - mvp._44)) < 0 )
+			return false;
+
+		// near
+		if( testPlane(new Plane(mvp._13, mvp._23, mvp._33, -mvp._43)) < 0 )
+			return false;
+
+		// far
+		if( testPlane(new Plane(mvp._14 - mvp._13, mvp._24 - mvp._23, mvp._34 - mvp._33, mvp._43 - mvp._44)) < 0 )
+			return false;
+			
+		return true;
+	}
+	
+	inline function testPlane( p : Plane ) {
+		var a = p.nx;
+		var b = p.ny;
+		var c = p.nz;
+		var dd = a * (xMax + xMin) + b * (yMax + yMin) + c * (zMax + zMin);
+		if( a < 0 ) a = -a;
+		if( b < 0 ) b = -b;
+		if( c < 0 ) c = -c;
+		var rr = a * (xMax - xMin) + b * (yMax - yMin) + c * (zMax - zMin);
+		return dd + rr - p.d*2;
+	}
+	
+	/**
+	 * Check if the camera model-view-projection Matrix intersects with the Bounds. Returns -1 if outside, 0 if interests and 1 if fully inside.
+	 * @param	mvp : the model-view-projection matrix to test against
+	 * @param	checkZ : tells if we will check against the near/far plane
+	 */
+	public function inFrustumDetails( mvp : Matrix, checkZ = true ) {
+		var ret = 1;
+		
+		// left
+		var p = new Plane(mvp._14 + mvp._11, mvp._24 + mvp._21 , mvp._34 + mvp._31, mvp._44 + mvp._41);
+		var m = p.nx * (p.nx > 0 ? xMax : xMin) + p.ny * (p.ny > 0 ? yMax : yMin) + p.nz * (p.nz > 0 ? zMax : zMin);
+		if( m + p.d < 0 )
+			return -1;
+		var n = p.nx * (p.nx > 0 ? xMin : xMax) + p.ny * (p.ny > 0 ? yMin : yMax) + p.nz * (p.nz > 0 ? zMin : zMax);
+		if( n + p.d < 0 ) ret = 0;
+		// right
+		var p = new Plane(mvp._14 - mvp._11, mvp._24 - mvp._21 , mvp._34 - mvp._31, mvp._44 - mvp._41);
+		var m = p.nx * (p.nx > 0 ? xMax : xMin) + p.ny * (p.ny > 0 ? yMax : yMin) + p.nz * (p.nz > 0 ? zMax : zMin);
+		if( m + p.d < 0 )
+			return -1;
+		var n = p.nx * (p.nx > 0 ? xMin : xMax) + p.ny * (p.ny > 0 ? yMin : yMax) + p.nz * (p.nz > 0 ? zMin : zMax);
+		if( n + p.d < 0 ) ret = 0;
+		// bottom
+		var p = new Plane(mvp._14 + mvp._12, mvp._24 + mvp._22 , mvp._34 + mvp._32, mvp._44 + mvp._42);
+		var m = p.nx * (p.nx > 0 ? xMax : xMin) + p.ny * (p.ny > 0 ? yMax : yMin) + p.nz * (p.nz > 0 ? zMax : zMin);
+		if( m + p.d < 0 )
+			return -1;
+		var n = p.nx * (p.nx > 0 ? xMin : xMax) + p.ny * (p.ny > 0 ? yMin : yMax) + p.nz * (p.nz > 0 ? zMin : zMax);
+		if( n + p.d < 0 ) ret = 0;
+		
+		// top
+		var p = new Plane(mvp._14 - mvp._12, mvp._24 - mvp._22 , mvp._34 - mvp._32, mvp._44 - mvp._42);
+		var m = p.nx * (p.nx > 0 ? xMax : xMin) + p.ny * (p.ny > 0 ? yMax : yMin) + p.nz * (p.nz > 0 ? zMax : zMin);
+		if( m + p.d < 0 )
+			return -1;
+		var n = p.nx * (p.nx > 0 ? xMin : xMax) + p.ny * (p.ny > 0 ? yMin : yMax) + p.nz * (p.nz > 0 ? zMin : zMax);
+		if( n + p.d < 0 ) ret = 0;
+				
+		if( checkZ ) {
+			// nea
+			var p = new Plane(mvp._13, mvp._23, mvp._33, mvp._43);
+			var m = p.nx * (p.nx > 0 ? xMax : xMin) + p.ny * (p.ny > 0 ? yMax : yMin) + p.nz * (p.nz > 0 ? zMax : zMin);
+			if( m + p.d < 0 )
+				return -1;
+			var n = p.nx * (p.nx > 0 ? xMin : xMax) + p.ny * (p.ny > 0 ? yMin : yMax) + p.nz * (p.nz > 0 ? zMin : zMax);
+			if( n + p.d < 0 ) ret = 0;
+
+			var p = new Plane(mvp._14 - mvp._13, mvp._24 - mvp._23, mvp._34 - mvp._33, mvp._44 - mvp._43);
+			var m = p.nx * (p.nx > 0 ? xMax : xMin) + p.ny * (p.ny > 0 ? yMax : yMin) + p.nz * (p.nz > 0 ? zMax : zMin);
+			if( m + p.d < 0 )
+				return -1;
+			var n = p.nx * (p.nx > 0 ? xMin : xMax) + p.ny * (p.ny > 0 ? yMin : yMax) + p.nz * (p.nz > 0 ? zMin : zMax);
+			if( n + p.d < 0 ) ret = 0;
+		}
+		
+		return ret;
+	}
+	
+	public function transform3x4( m : Matrix ) {
+		var xMin = xMin, yMin = yMin, zMin = zMin, xMax = xMax, yMax = yMax, zMax = zMax;
+		empty();
+		var v = new h3d.col.Point();
+		v.set(xMin, yMin, zMin);
+		v.transform(m);
+		addPoint(v);
+		v.set(xMin, yMin, zMax);
+		v.transform(m);
+		addPoint(v);
+		v.set(xMin, yMax, zMin);
+		v.transform(m);
+		addPoint(v);
+		v.set(xMin, yMax, zMax);
+		v.transform(m);
+		addPoint(v);
+		v.set(xMax, yMin, zMin);
+		v.transform(m);
+		addPoint(v);
+		v.set(xMax, yMin, zMax);
+		v.transform(m);
+		addPoint(v);
+		v.set(xMax, yMax, zMin);
+		v.transform(m);
+		addPoint(v);
+		v.set(xMax, yMax, zMax);
+		v.transform(m);
+		addPoint(v);
+	}
+	
+	public inline function collide( b : Bounds ) {
+		return !(xMin > b.xMax || yMin > b.yMax || zMin > b.zMax || xMax < b.xMin || yMax < b.yMin || zMax < b.zMin);
+	}
+	
+	public inline function include( p : Point ) {
+		return p.x >= xMin && p.x < xMax && p.y >= yMin && p.y < yMax && p.z >= zMin && p.z < zMax;
+	}
+	
+	public inline function add( b : Bounds ) {
+		if( b.xMin < xMin ) xMin = b.xMin;
+		if( b.xMax > xMax ) xMax = b.xMax;
+		if( b.yMin < yMin ) yMin = b.yMin;
+		if( b.yMax > yMax ) yMax = b.yMax;
+		if( b.zMin < zMin ) zMin = b.zMin;
+		if( b.zMax > zMax ) zMax = b.zMax;
+	}
+
+	public inline function addPoint( p : Point ) {
+		if( p.x < xMin ) xMin = p.x;
+		if( p.x > xMax ) xMax = p.x;
+		if( p.y < yMin ) yMin = p.y;
+		if( p.y > yMax ) yMax = p.y;
+		if( p.z < zMin ) zMin = p.z;
+		if( p.z > zMax ) zMax = p.z;
+	}
+
+	public inline function addPos( x : Float, y : Float, z : Float ) {
+		if( x < xMin ) xMin = x;
+		if( x > xMax ) xMax = x;
+		if( y < yMin ) yMin = y;
+		if( y > yMax ) yMax = y;
+		if( z < zMin ) zMin = z;
+		if( z > zMax ) zMax = z;
+	}
+	
+	public function intersection( a : Bounds, b : Bounds ) {
+		var xMin = Math.max(a.xMin, b.xMin);
+		var yMin = Math.max(a.yMin, b.yMin);
+		var zMin = Math.max(a.zMin, b.zMin);
+		var xMax = Math.max(a.xMax, b.xMax);
+		var yMax = Math.max(a.yMax, b.yMax);
+		var zMax = Math.max(a.zMax, b.zMax);
+		this.xMin = xMin;
+		this.xMax = xMax;
+		this.yMin = yMin;
+		this.yMax = yMax;
+		this.zMin = zMin;
+		this.zMax = zMax;
+	}
+	
+	public inline function offset( dx : Float, dy : Float, dz : Float ) {
+		xMin += dx;
+		xMax += dx;
+		yMin += dy;
+		yMax += dy;
+		zMin += dz;
+		zMax += dz;
+	}
+	
+	public inline function setMin( p : Point ) {
+		xMin = p.x;
+		yMin = p.y;
+		zMin = p.z;
+	}
+
+	public inline function setMax( p : Point ) {
+		xMax = p.x;
+		yMax = p.y;
+		zMax = p.z;
+	}
+	
+	public function load( b : Bounds ) {
+		xMin = b.xMin;
+		xMax = b.xMax;
+		yMin = b.yMin;
+		yMax = b.yMax;
+		zMin = b.zMin;
+		zMax = b.zMax;
+	}
+	
+	public function scaleCenter( v : Float ) {
+		var dx = (xMax - xMin) * 0.5 * v;
+		var dy = (yMax - yMin) * 0.5 * v;
+		var dz = (zMax - zMin) * 0.5 * v;
+		var mx = (xMax + xMin) * 0.5;
+		var my = (yMax + yMin) * 0.5;
+		var mz = (zMax + zMin) * 0.5;
+		xMin = mx - dx * v;
+		yMin = my - dy * v;
+		zMin = mz - dz * v;
+		xMax = mx + dx * v;
+		yMax = my + dy * v;
+		zMax = mz + dz * v;
+	}
+	
+	public inline function getMin() {
+		return new Point(xMin, yMin, zMin);
+	}
+	
+	public inline function getCenter() {
+		return new Point((xMin + xMax) * 0.5, (yMin + yMax) * 0.5, (zMin + zMax) * 0.5);
+	}
+
+	public inline function getSize() {
+		return new Point(xMax - xMin, yMax - yMin, zMax - zMin);
+	}
+	
+	public inline function getMax() {
+		return new Point(xMax, yMax, zMax);
+	}
+	
+	public inline function empty() {
+		xMin = 1e20;
+		xMax = -1e20;
+		yMin = 1e20;
+		yMax = -1e20;
+		zMin = 1e20;
+		zMax = -1e20;
+	}
+
+	public inline function all() {
+		xMin = -1e20;
+		xMax = 1e20;
+		yMin = -1e20;
+		yMax = 1e20;
+		zMin = -1e20;
+		zMax = 1e20;
+	}
+	
+	public inline function clone() {
+		var b = new Bounds();
+		b.xMin = xMin;
+		b.xMax = xMax;
+		b.yMin = yMin;
+		b.yMax = yMax;
+		b.zMin = zMin;
+		b.zMax = zMax;
+		return b;
+	}
+	
+	public function toString() {
+		return "{" + getMin() + "," + getMax() + "}";
+	}
+	
+	public static inline function fromPoints( min : Point, max : Point ) {
+		var b = new Bounds();
+		b.setMin(min);
+		b.setMax(max);
+		return b;
+	}
+	
+}

+ 92 - 0
h3d/col/Plane.hx

@@ -0,0 +1,92 @@
+package h3d.col;
+import hxd.Math;
+
+@:allow(h3d.col)
+class Plane {
+	
+	// Place equation :  nx.X + ny.Y + nz.Z - d = 0
+	var nx : Float;
+	var ny : Float;
+	var nz : Float;
+	var d : Float;
+	
+	inline function new(nx, ny, nz, d) {
+		this.nx = nx;
+		this.ny = ny;
+		this.nz = nz;
+		this.d = d;
+	}
+	
+	/**
+		Returns the plan normal
+	**/
+	public inline function getNormal() {
+		return new Point(nx, ny, nz);
+	}
+	
+	public inline function getNormalDistance() {
+		return d;
+	}
+	
+	/**
+		Normalize the plan, so we can use distance().
+	**/
+	public inline function normalize() {
+		var len = Math.invSqrt(nx * nx + ny * ny + nz * nz);
+		nx *= len;
+		ny *= len;
+		nz *= len;
+		d *= len;
+	}
+	
+	public function toString() {
+		return "{" + getNormal()+","+ d + "}";
+	}
+	
+	/**
+		Returns the signed distance between a point an the plane. This requires the plan to be normalized. If the distance is negative it means that we are "under" the plan.
+	**/
+	public inline function distance( p : Point ) {
+		return nx * p.x + ny * p.y + nz * p.z - d;
+	}
+	
+	public inline function side( p : Point ) {
+		return distance(p) >= 0;
+	}
+	
+	public inline function project( p : Point ) : Point {
+		var d = distance(p);
+		return new Point(p.x - d * nx, p.y - d * ny, p.z - d * nz);
+	}
+
+	public inline function projectTo( p : Point, out : Point ) {
+		var d = distance(p);
+		out.x = p.x - d * nx;
+		out.y = p.y - d * ny;
+		out.z = p.z - d * nz;
+	}
+	
+	public static inline function fromPoints( p0 : Point, p1 : Point, p2 : Point ) {
+		var d1 = p1.sub(p0);
+		var d2 = p2.sub(p0);
+		var n = d1.cross(d2);
+		return new Plane(n.x,n.y,n.z,n.dot(p0));
+	}
+	
+	public static inline function fromNormalPoint( n : Point, p : Point ) {
+		return new Plane(n.x,n.y,n.z,n.dot(p));
+	}
+	
+	public static inline function X(v:Float) {
+		return new Plane( 1, 0, 0, v );
+	}
+	
+	public static inline function Y(v:Float) {
+		return new Plane( 0, 1, 0, v );
+	}
+
+	public static inline function Z(v:Float) {
+		return new Plane( 0, 0, 1, v );
+	}
+	
+}

+ 115 - 0
h3d/col/Point.hx

@@ -0,0 +1,115 @@
+package h3d.col;
+using hxd.Math;
+
+class Point {
+
+	public var x : Float;
+	public var y : Float;
+	public var z : Float;
+	
+	public inline function new(x=0.,y=0.,z=0.) {
+		this.x = x;
+		this.y = y;
+		this.z = z;
+	}
+	
+	public function inFrustum( mvp : Matrix ) {
+		// left
+		if( !new Plane(mvp._14 + mvp._11, mvp._24 + mvp._21 , mvp._34 + mvp._31, -(mvp._44 + mvp._41)).side(this))
+			return false;
+		
+		// right
+		if( !new Plane(mvp._14 - mvp._11, mvp._24 - mvp._21 , mvp._34 - mvp._31, mvp._41 - mvp._44).side(this) )
+			return false;
+
+		// bottom
+		if( !new Plane(mvp._14 + mvp._12, mvp._24 + mvp._22 , mvp._34 + mvp._32, -(mvp._44 + mvp._42)).side(this) )
+			return false;
+
+		// top
+		if( !new Plane(mvp._14 - mvp._12, mvp._24 - mvp._22 , mvp._34 - mvp._32, mvp._42 - mvp._44).side(this) )
+			return false;
+
+		// near
+		if( !new Plane(mvp._13, mvp._23, mvp._33, -mvp._43).side(this) )
+			return false;
+
+		// far
+		if( !new Plane(mvp._14 - mvp._13, mvp._24 - mvp._23, mvp._34 - mvp._33, mvp._43 - mvp._44).side(this) )
+			return false;
+			
+		return true;
+	}
+	
+	public inline function set(x, y, z) {
+		this.x = x;
+		this.y = y;
+		this.z = z;
+	}
+	
+	public inline function sub( p : Point ) {
+		return new Point(x - p.x, y - p.y, z - p.z);
+	}
+
+	public inline function add( p : Point ) {
+		return new Point(x + p.x, y + p.y, z + p.z);
+	}
+	
+	public inline function cross( p : Point ) {
+		return new Point(y * p.z - z * p.y, z * p.x - x * p.z,  x * p.y - y * p.x);
+	}
+	
+	public inline function lengthSq() {
+		return x * x + y * y + z * z;
+	}
+	
+	public inline function length() {
+		return lengthSq().sqrt();
+	}
+
+	public inline function dot( p : Point ) {
+		return x * p.x + y * p.y + z * p.z;
+	}
+	
+	public inline function distanceSq( p : Point ) {
+		var dx = p.x - x;
+		var dy = p.y - y;
+		var dz = p.z - z;
+		return dx * dx + dy * dy + dz * dz;
+	}
+
+	public inline function distance( p : Point ) {
+		return distanceSq(p).sqrt();
+	}
+
+	
+	public function normalize() {
+		var k = x * x + y * y + z * z;
+		if( k < hxd.Math.EPSILON ) k = 0 else k = k.invSqrt();
+		x *= k;
+		y *= k;
+		z *= k;
+	}
+	
+	public inline function transform( m : Matrix ) {
+		var px = x * m._11 + y * m._21 + z * m._31 + m._41;
+		var py = x * m._12 + y * m._22 + z * m._32 + m._42;
+		var pz = x * m._13 + y * m._23 + z * m._33 + m._43;
+		x = px;
+		y = py;
+		z = pz;
+	}
+	
+	public inline function toVector() {
+		return new Vector(x, y, z);
+	}
+
+	public inline function clone() {
+		return new Point(x,y,z);
+	}
+
+	public function toString() {
+		return '{${x.fmt()},${y.fmt()},${z.fmt()}}';
+	}
+	
+}

+ 85 - 0
h3d/col/Ray.hx

@@ -0,0 +1,85 @@
+package h3d.col;
+import hxd.Math;
+
+@:allow(h3d.col)
+class Ray {
+	
+	var px : Float;
+	var py : Float;
+	var pz : Float;
+	var lx : Float;
+	var ly : Float;
+	var lz : Float;
+	
+	inline function new() {
+	}
+	
+	public function normalize() {
+		var l = lx * lx + ly * ly + lz * lz;
+		if( l < Math.EPSILON ) l = 0 else l = Math.invSqrt(l);
+		lx *= l;
+		ly *= l;
+		lz *= l;
+	}
+	
+	public inline function getPos() {
+		return new Point(px, py, pz);
+	}
+
+	public inline function getDir() {
+		return new Point(lx, ly, lz);
+	}
+	
+	public function toString() {
+		return "{" + getPos() + "," + getDir() + "}";
+	}
+	
+	public inline function intersect( p : Plane ) : Null<Point> {
+		var d = lx * p.nx + ly * p.ny + lz * p.nz;
+		var nd = p.d - (px * p.nx + py * p.ny + pz * p.nz);
+		// line parallel with plane
+		if( Math.abs(d) < Math.EPSILON )
+			return Math.abs(nd) < Math.EPSILON ? new Point(px, py, pz) : null;
+		else {
+			var k = nd / d;
+			return new Point(px + lx * k, py + ly * k, pz + lz * k);
+		}
+	}
+
+	public inline function collide( b : Bounds ) : Bool {
+		var dx = 1 / lx;
+		var dy = 1 / ly;
+		var dz = 1 / lz;
+		var t1 = (b.xMin - px) * dx;
+		var t2 = (b.xMax - px) * dx;
+		var t3 = (b.yMin - py) * dy;
+		var t4 = (b.yMax - py) * dy;
+		var t5 = (b.zMin - pz) * dz;
+		var t6 = (b.zMax - pz) * dz;
+		var tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6));
+		var tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6));
+		if( tmax < 0 ) {
+			// t = tmax;
+			return false;
+		} else if( tmin > tmax ) {
+			// t = tmax;
+			return false;
+		} else {
+			// t = tmin;
+			return true;
+		}
+	}
+	
+	public static function fromPoints( p1 : Point, p2 : Point ) {
+		var r = new Ray();
+		r.px = p1.x;
+		r.py = p1.y;
+		r.pz = p1.z;
+		r.lx = p2.x - p1.x;
+		r.ly = p2.y - p1.y;
+		r.lz = p2.z - p1.z;
+		return r;
+	}
+	
+	
+}

+ 30 - 0
h3d/col/Seg.hx

@@ -0,0 +1,30 @@
+package h3d.col;
+import hxd.Math;
+
+class Seg {
+	
+	public var p1 : Point;
+	public var p2 : Point;
+	public var lenSq : Float;
+	
+	public inline function new( p1 : Point, p2 : Point ) {
+		this.p1 = p1;
+		this.p2 = p2;
+		lenSq = p1.distanceSq(p2);
+	}
+	
+	public inline function distanceSq( p : Point ) {
+		var t = p.sub(p1).dot(p2.sub(p1)) / lenSq;
+		return if( t < 0 )
+			p.distanceSq(p1);
+		else if( t > 1 ) {
+			p.distanceSq(p2);
+		} else
+			p.distanceSq(new Point(p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y)));
+	}
+	
+	public inline function distance( p : Point ) {
+		return Math.sqrt(distanceSq(p));
+	}
+	
+}

+ 144 - 0
h3d/fbx/Data.hx

@@ -0,0 +1,144 @@
+package h3d.fbx;
+
+enum FbxProp {
+	PInt( v : Int );
+	PFloat( v : Float );
+	PString( v : String );
+	PIdent( i : String );
+	PInts( v : Array<Int> );
+	PFloats( v : Array<Float> );
+}
+
+typedef FbxNode = {
+	var name : String;
+	var props : Array<FbxProp>;
+	var childs : Array<FbxNode>;
+}
+
+class FbxTools {
+
+	public static function get( n : FbxNode, path : String, opt = false ) {
+		var parts = path.split(".");
+		var cur = n;
+		for( p in parts ) {
+			var found = false;
+			for( c in cur.childs )
+				if( c.name == p ) {
+					cur = c;
+					found = true;
+					break;
+				}
+			if( !found ) {
+				if( opt )
+					return null;
+				throw n.name + " does not have " + path+" ("+p+" not found)";
+			}
+		}
+		return cur;
+	}
+
+	public static function getAll( n : FbxNode, path : String ) {
+		var parts = path.split(".");
+		var cur = [n];
+		for( p in parts ) {
+			var out = [];
+			for( n in cur )
+				for( c in n.childs )
+					if( c.name == p )
+						out.push(c);
+			cur = out;
+			if( cur.length == 0 )
+				return cur;
+		}
+		return cur;
+	}
+	
+	public static function getInts( n : FbxNode ) {
+		if( n.props.length != 1 )
+			throw n.name + " has " + n.props + " props";
+		switch( n.props[0] ) {
+		case PInts(v):
+			return v;
+		default:
+			throw n.name + " has " + n.props + " props";
+		}
+	}
+
+	public static function getFloats( n : FbxNode ) {
+		if( n.props.length != 1 )
+			throw n.name + " has " + n.props + " props";
+		switch( n.props[0] ) {
+		case PFloats(v):
+			return v;
+		case PInts(i):
+			var fl = new Array<Float>();
+			for( x in i )
+				fl.push(x);
+			return fl;
+		default:
+			throw n.name + " has " + n.props + " props";
+		}
+	}
+	
+	public static function hasProp( n : FbxNode, p : FbxProp ) {
+		for( p2 in n.props )
+			if( Type.enumEq(p, p2) )
+				return true;
+		return false;
+	}
+	
+	public static function toInt( n : FbxProp ) {
+		if( n == null ) throw "null prop";
+		return switch( n ) {
+		case PInt(v): v;
+		case PFloat(f): return Std.int( f );
+		default: throw "Invalid prop " + n;
+		}
+	}
+
+	public static function toFloat( n : FbxProp ) {
+		if( n == null ) throw "null prop";
+		return switch( n ) {
+		case PInt(v): v * 1.0;
+		case PFloat(v): v;
+		default: throw "Invalid prop " + n;
+		}
+	}
+
+	public static function toString( n : FbxProp ) {
+		if( n == null ) throw "null prop";
+		return switch( n ) {
+		case PString(v): v;
+		default: throw "Invalid prop " + n;
+		}
+	}
+	
+	public static function getId( n : FbxNode ) {
+		if( n.props.length != 3 )
+			throw n.name + " is not an object";
+		return switch( n.props[0] ) {
+		case PInt(id): id;
+		case PFloat(id) : Std.int( id );
+		default: throw n.name + " is not an object " + n.props;
+		}
+	}
+
+	public static function getName( n : FbxNode ) {
+		if( n.props.length != 3 )
+			throw n.name + " is not an object";
+		return switch( n.props[1] ) {
+		case PString(n): n.split("::").pop();
+		default: throw n.name + " is not an object";
+		}
+	}
+
+	public static function getType( n : FbxNode ) {
+		if( n.props.length != 3 )
+			throw n.name + " is not an object";
+		return switch( n.props[2] ) {
+		case PString(n): n;
+		default: throw n.name + " is not an object";
+		}
+	}
+	
+}

+ 64 - 0
h3d/fbx/Filter.hx

@@ -0,0 +1,64 @@
+package h3d.fbx;
+using h3d.fbx.Data;
+
+class Filter {
+	
+	var ignoreList : Array<Array<String>>;
+	var removedObjects : Map<Int,Bool>;
+	
+	public function new() {
+		ignoreList = [];
+	}
+	
+	public function ignore( path : String ) {
+		ignoreList.push(path.split("."));
+	}
+	
+	public function filter( f : FbxNode ) : FbxNode {
+		removedObjects = new Map();
+		var f2 = filterRec(f, ignoreList, 0);
+		for( i in 0...f2.childs.length ) {
+			var c = f2.childs[i];
+			if( c.name == "Connections" ) {
+				var cnx = [];
+				for( c in c.childs )
+					if( !removedObjects.exists(c.props[1].toInt()) && !removedObjects.exists(c.props[2].toInt()) )
+						cnx.push(c);
+				f2.childs[i] = {
+					name : c.name,
+					props : c.props,
+					childs : cnx,
+				};
+			}
+		}
+		return f2;
+	}
+	
+	function filterRec( f : FbxNode, match : Array<Array<String>>, index : Int ) {
+		var sub = [];
+		for( m in match )
+			if( m[index] == f.name ) {
+				if( m.length == index + 1 )
+					return null;
+				sub.push(m);
+			}
+		if( sub.length == 0 )
+			return f;
+		var f2 = {
+			name : f.name,
+			props : f.props.copy(),
+			childs : [],
+		};
+		var isObjects = index == 1 && f.name == "Objects";
+		index++;
+		for( c in f.childs ) {
+			var fs = filterRec(c, sub, index);
+			if( fs != null )
+				f2.childs.push(fs);
+			else if( isObjects )
+				removedObjects.set(c.getId(),true);
+		}
+		return f2;
+	}
+
+}

+ 101 - 0
h3d/fbx/Geometry.hx

@@ -0,0 +1,101 @@
+package h3d.fbx;
+using h3d.fbx.Data;
+
+class Geometry {
+
+	var lib : Library;
+	var root : FbxNode;
+	
+	public function new(l, root) {
+		this.lib = l;
+		this.root = root;
+	}
+	
+	public function getVertices() {
+		return root.get("Vertices").getFloats();
+	}
+	
+	public function getPolygons() {
+		return root.get("PolygonVertexIndex").getInts();
+	}
+
+	public function getMaterials() {
+		var mats = root.get("LayerElementMaterial",true);
+		return mats == null ? null : mats.get("Materials").getInts();
+	}
+
+	/**
+		Decode polygon informations into triangle indexes and vertexes indexes.
+		Returns vidx, which is the list of vertices indexes and iout which is the index buffer for the full vertex model
+	**/
+	public function getIndexes() {
+		var count = 0, pos = 0;
+		var index = getPolygons();
+		var vout = [], iout = [];
+		for( i in index ) {
+			count++;
+			if( i < 0 ) {
+				index[pos] = -i - 1;
+				var start = pos - count + 1;
+				for( n in 0...count )
+					vout.push(index[n + start]);
+				for( n in 0...count - 2 ) {
+					iout.push(start + n);
+					iout.push(start + count - 1);
+					iout.push(start + n + 1);
+				}
+				index[pos] = i; // restore
+				count = 0;
+			}
+			pos++;
+		}
+		return { vidx : vout, idx : iout };
+	}
+
+	public function getNormals() {
+		var nrm = root.get("LayerElementNormal.Normals").getFloats();
+		// if by-vertice (Maya in some cases, unless maybe "Split per-Vertex Normals" is checked)
+		// let's reindex based on polygon indexes
+		if( root.get("LayerElementNormal.MappingInformationType").props[0].toString() == "ByVertice" ) {
+			var nout = [];
+			for( i in getPolygons() ) {
+				var vid = i;
+				if( vid < 0 ) vid = -vid - 1;
+				nout.push(nrm[vid * 3]);
+				nout.push(nrm[vid * 3 + 1]);
+				nout.push(nrm[vid * 3 + 2]);
+			}
+			nrm = nout;
+		}
+		return nrm;
+	}
+	
+	public function getColors() {
+		var color = root.get("LayerElementColor",true);
+		return color == null ? null : { values : color.get("Colors").getFloats(), index : color.get("ColorIndex").getInts() };
+	}
+	
+	public function getUVs() {
+		var uvs = [];
+		for( v in root.getAll("LayerElementUV") ) {
+			var index = v.get("UVIndex", true);
+			var values = v.get("UV").getFloats();
+			var index = if( index == null ) {
+				// ByVertice/Direct (Maya sometimes...)
+				[for( i in getPolygons() ) if( i < 0 ) -i - 1 else i];
+			} else index.getInts();
+			uvs.push({ values : values, index : index });
+		}
+		return uvs;
+	}
+	
+	@:access(h3d.fbx.Library.leftHand)
+	public function getGeomTranslate() {
+		for( p in lib.getParent(root, "Model").getAll("Properties70.P") )
+			if( p.props[0].toString() == "GeometricTranslation" )
+				return new h3d.col.Point(p.props[4].toFloat() * (lib.leftHand ? -1 : 1), p.props[5].toFloat(), p.props[6].toFloat());
+		return null;
+	}
+
+
+}

+ 843 - 0
h3d/fbx/Library.hx

@@ -0,0 +1,843 @@
+package h3d.fbx;
+using h3d.fbx.Data;
+import h3d.col.Point;
+
+enum AnimationMode {
+	FrameAnim;
+	LinearAnim;
+}
+
+class DefaultMatrixes {
+	public var trans : Null<Point>;
+	public var scale : Null<Point>;
+	public var rotate : Null<Point>;
+	public var preRot : Null<Point>;
+	public var wasRemoved : Null<Int>;
+	
+	public function new() {
+	}
+
+	public static inline function rightHandToLeft( m : h3d.Matrix ) {
+		// if [x,y,z] is our original point and M the matrix
+		// in right hand we have [x,y,z] * M = [x',y',z']
+		// we need to ensure that left hand matrix convey the x axis flip,
+		// in order to have [-x,y,z] * M = [-x',y',z']
+		m._12 *= -1;
+		m._13 *= -1;
+		m._21 *= -1;
+		m._31 *= -1;
+		m._41 *= -1;
+	}
+	
+	public function toMatrix(leftHand) {
+		var m = new h3d.Matrix();
+		m.identity();
+		if( scale != null ) m.scale(scale.x, scale.y, scale.z);
+		if( rotate != null ) m.rotate(rotate.x, rotate.y, rotate.z);
+		if( preRot != null ) m.rotate(preRot.x, preRot.y, preRot.z);
+		if( trans != null ) m.translate(trans.x, trans.y, trans.z);
+		if( leftHand ) rightHandToLeft(m);
+		return m;
+	}
+	
+}
+
+class Library {
+
+	var root : FbxNode;
+	var ids : Map<Int,FbxNode>;
+	var connect : Map<Int,Array<Int>>;
+	var invConnect : Map<Int,Array<Int>>;
+	var leftHand : Bool;
+	var defaultModelMatrixes : Map<String,DefaultMatrixes>;
+
+	/**
+		Allows to prevent some terminal unskinned joints to be removed, for instance if we want to track their position
+	**/
+	public var keepJoints : Map<String,Bool>;
+	
+	/**
+		Allows to skip some objects from being processed as if they were not part of the FBX
+	**/
+	public var skipObjects : Map<String,Bool>;
+	
+	/**
+		Set how many bones per vertex should be created in skin data in makeObject(). Default is 3
+	**/
+	public var bonesPerVertex = 3;
+	
+	/**
+		If there are too many bones, the model will be split in separate render passes.
+	**/
+	public var maxBonesPerSkin = 34;
+	
+	/**
+		Consider unskinned joints to be simple objects
+	**/
+	public var unskinnedJointsAsObjects : Bool;
+	
+	public function new() {
+		root = { name : "Root", props : [], childs : [] };
+		keepJoints = new Map();
+		skipObjects = new Map();
+		reset();
+	}
+	
+	function reset() {
+		ids = new Map();
+		connect = new Map();
+		invConnect = new Map();
+		defaultModelMatrixes = new Map();
+	}
+	
+	public function loadTextFile( data : String ) {
+		load(Parser.parse(data));
+	}
+	
+	public function load( root : FbxNode ) {
+		reset();
+		this.root = root;
+		for( c in root.childs )
+			init(c);
+	}
+	
+	function convertPoints( a : Array<Float> ) {
+		var p = 0;
+		for( i in 0...Std.int(a.length / 3) ) {
+			a[p] = -a[p]; // inverse X axis
+			p += 3;
+		}
+	}
+	
+	public function leftHandConvert() {
+		if( leftHand ) return;
+		leftHand = true;
+		for( g in root.getAll("Objects.Geometry") ) {
+			for( v in g.getAll("Vertices") )
+				convertPoints(v.getFloats());
+			for( v in g.getAll("LayerElementNormal.Normals") )
+				convertPoints(v.getFloats());
+		}
+	}
+	
+	function init( n : FbxNode ) {
+		switch( n.name ) {
+		case "Connections":
+			for( c in n.childs ) {
+				if( c.name != "C" )
+					continue;
+				var child = c.props[1].toInt();
+				var parent = c.props[2].toInt();
+				
+				var c = connect.get(parent);
+				if( c == null ) {
+					c = [];
+					connect.set(parent, c);
+				}
+				c.push(child);
+
+				if( parent == 0 )
+					continue;
+								
+				var c = invConnect.get(child);
+				if( c == null ) {
+					c = [];
+					invConnect.set(child, c);
+				}
+				c.push(parent);
+			}
+		case "Objects":
+			for( c in n.childs )
+				ids.set(c.getId(), c);
+		default:
+		}
+	}
+	
+	public function getGeometry( name : String = "" ) {
+		var geom = null;
+		for( g in root.getAll("Objects.Geometry") )
+			if( g.hasProp(PString("Geometry::" + name)) ) {
+				geom = g;
+				break;
+			}
+		if( geom == null )
+			throw "Geometry " + name + " not found";
+		return new Geometry(this, geom);
+	}
+
+	public function getParent( node : FbxNode, nodeName : String, ?opt : Bool ) {
+		var p = getParents(node, nodeName);
+		if( p.length > 1 )
+			throw node.getName() + " has " + p.length + " " + nodeName + " parents";
+		if( p.length == 0 && !opt )
+			throw "Missing " + node.getName() + " " + nodeName + " parent";
+		return p[0];
+	}
+
+	public function getChild( node : FbxNode, nodeName : String, ?opt : Bool ) {
+		var c = getChilds(node, nodeName);
+		if( c.length > 1 )
+			throw node.getName() + " has " + c.length + " " + nodeName + " childs";
+		if( c.length == 0 && !opt )
+			throw "Missing " + node.getName() + " " + nodeName + " child";
+		return c[0];
+	}
+	
+	public function getChilds( node : FbxNode, ?nodeName : String ) {
+		var c = connect.get(node.getId());
+		var subs = [];
+		if( c != null )
+			for( id in c ) {
+				var n = ids.get(id);
+				if( n == null ) throw id + " not found";
+				if( nodeName != null && n.name != nodeName ) continue;
+				subs.push(n);
+			}
+		return subs;
+	}
+
+	public function getParents( node : FbxNode, ?nodeName : String ) {
+		var c = invConnect.get(node.getId());
+		var pl = [];
+		if( c != null )
+			for( id in c ) {
+				var n = ids.get(id);
+				if( n == null ) throw id + " not found";
+				if( nodeName != null && n.name != nodeName ) continue;
+				pl.push(n);
+			}
+		return pl;
+	}
+	
+	public function getRoot() {
+		return root;
+	}
+	
+	public function ignoreMissingObject( name : String ) {
+		var def = defaultModelMatrixes.get(name);
+		if( def == null ) {
+			def = new DefaultMatrixes();
+			def.wasRemoved = -1;
+			defaultModelMatrixes.set(name, def);
+		}
+	}
+
+	public function loadAnimation( mode : AnimationMode, ?animName : String, ?root : FbxNode, ?lib : Library ) : h3d.anim.Animation {
+		if( lib != null ) {
+			lib.defaultModelMatrixes = defaultModelMatrixes;
+			return lib.loadAnimation(mode,animName);
+		}
+		if( root != null ) {
+			var l = new Library();
+			l.load(root);
+			if( leftHand ) l.leftHandConvert();
+			l.defaultModelMatrixes = defaultModelMatrixes;
+			return l.loadAnimation(mode,animName);
+		}
+		var animNode = null;
+		for( a in this.root.getAll("Objects.AnimationStack") )
+			if( animName == null || a.getName()	== animName ) {
+				if( animName == null )
+					animName = a.getName();
+				animNode = getChild(a, "AnimationLayer");
+				break;
+			}
+		if( animNode == null ) {
+			if( animName == null ) return null;
+			throw "Animation not found " + animName;
+		}
+
+		var curves = new Map();
+		var P0 = new Point();
+		var P1 = new Point(1, 1, 1);
+		var F = Math.PI / 180;
+		var allTimes = new Map();
+		for( cn in getChilds(animNode, "AnimationCurveNode") ) {
+			var model = getParent(cn, "Model");
+			var c = curves.get(model.getId());
+			if( c == null ) {
+				var name = model.getName();
+				if( skipObjects.get(name) )
+					continue;
+				// if it's an empty model with no sub nodes, let's ignore it (ex : Camera)
+				if( model.getType() == "Null" && getChilds(model, "Model").length == 0 )
+					continue;
+				var def = defaultModelMatrixes.get(name);
+				if( def == null )
+					throw "Object "+name+" used in anim "+animName+" was not found in library";
+				// if it's a move animation on a terminal unskinned joint, let's skip it
+				if( def.wasRemoved != null ) {
+					if( cn.getName() != "Visibility" )
+						continue;
+					// apply it on the skin instead
+					model = ids.get(def.wasRemoved);
+					name = model.getName();
+					c = curves.get(def.wasRemoved);
+					def = defaultModelMatrixes.get(name);
+					// todo : change behavior not to remove the mesh but the skin instead!
+					if( def == null ) throw "assert";
+				}
+				if( c == null ) {
+					c = { def : def, t : null, r : null, s : null, a : null, name : name };
+					curves.set(model.getId(), c);
+				}
+			}
+			var data = getChilds(cn, "AnimationCurve");
+			var cname = cn.getName();
+			// collect all the timestamps
+			var times = data[0].get("KeyTime").getFloats();
+			for( i in 0...times.length ) {
+				var t = times[i];
+				// fix rounding error
+				if( t % 100 != 0 ) {
+					t += 100 - (t % 100);
+					times[i] = t;
+				}
+				// this should give significant-enough key
+				var it = Std.int(t / 200000);
+				allTimes.set(it, t);
+			}
+			// handle special curves
+			if( data.length != 3 ) {
+				switch( cname ) {
+				case "Visibility":
+					c.a = {
+						v : data[0].get("KeyValueFloat").getFloats(),
+						t : times,
+					};
+					continue;
+				default:
+				}
+				throw model.getName()+"."+cname + " has " + data.length + " curves";
+			}
+			// handle TRS curves
+			var data = {
+				x : data[0].get("KeyValueFloat").getFloats(),
+				y : data[1].get("KeyValueFloat").getFloats(),
+				z : data[2].get("KeyValueFloat").getFloats(),
+				t : times,
+			};
+			// this can happen when resampling anims due to rounding errors, let's ignore it for now
+			//if( data.y.length != times.length || data.z.length != times.length )
+			//	throw "Unsynchronized curve components on " + model.getName()+"."+cname+" (" + data.x.length + "/" + data.y.length + "/" + data.z.length + ")";
+			// optimize empty animations out
+			var E = 1e-10, M = 1.0;
+			var def = switch( cname ) {
+			case "T": if( c.def.trans == null ) P0 else c.def.trans;
+			case "R": M = F; if( c.def.rotate == null ) P0 else c.def.rotate;
+			case "S": if( c.def.scale == null ) P1 else c.def.scale;
+			default:
+				throw "Unknown curve " + model.getName()+"."+cname;
+			}
+			var hasValue = false;
+			for( v in data.x )
+				if( v*M < def.x-E || v*M > def.x+E ) {
+					hasValue = true;
+					break;
+				}
+			if( !hasValue ) {
+				for( v in data.y )
+					if( v*M < def.y-E || v*M > def.y+E ) {
+						hasValue = true;
+						break;
+					}
+			}
+			if( !hasValue ) {
+				for( v in data.z )
+					if( v*M < def.z-E || v*M > def.z+E ) {
+						hasValue = true;
+						break;
+					}
+			}
+			// no meaningful value found
+			if( !hasValue )
+				continue;
+			switch( cname ) {
+			case "T": c.t = data;
+			case "R": c.r = data;
+			case "S": c.s = data;
+			default: throw "assert";
+			}
+		}
+		
+		var times = [];
+		for( a in allTimes )
+			times.push(a);
+		var allTimes = times;
+		allTimes.sort(sortDistinctFloats);
+		var maxTime = allTimes[allTimes.length - 1];
+		var minDT = maxTime;
+		var curT = allTimes[0];
+		for( i in 1...allTimes.length ) {
+			var t = allTimes[i];
+			var dt = t - curT;
+			if( dt < minDT ) minDT = dt;
+			curT = t;
+		}
+		var numFrames = maxTime == 0 ? 1 : 1 + Std.int((maxTime - allTimes[0]) / minDT);
+		var sampling = 15.0 / (minDT / 3079077200); // this is the DT value we get from Max when using 15 FPS export
+		
+		switch( mode ) {
+		case FrameAnim:
+			var anim = new h3d.anim.FrameAnimation(animName, numFrames, sampling);
+		
+			for( c in curves ) {
+				var frames = c.t == null && c.r == null && c.s == null ? null : new haxe.ds.Vector(numFrames);
+				var alpha = c.a == null ? null : new haxe.ds.Vector(numFrames);
+				// skip empty curves
+				if( frames == null && alpha == null )
+					continue;
+				var ctx = c.t == null ? null : c.t.x;
+				var cty = c.t == null ? null : c.t.y;
+				var ctz = c.t == null ? null : c.t.z;
+				var ctt = c.t == null ? [-1.] : c.t.t;
+				var crx = c.r == null ? null : c.r.x;
+				var cry = c.r == null ? null : c.r.y;
+				var crz = c.r == null ? null : c.r.z;
+				var crt = c.r == null ? [-1.] : c.r.t;
+				var csx = c.s == null ? null : c.s.x;
+				var csy = c.s == null ? null : c.s.y;
+				var csz = c.s == null ? null : c.s.z;
+				var cst = c.s == null ? [ -1.] : c.s.t;
+				var cav = c.a == null ? null : c.a.v;
+				var cat = c.a == null ? null : c.a.t;
+				var def = c.def;
+				var tp = 0, rp = 0, sp = 0, ap = 0;
+				var curMat = null;
+				for( f in 0...numFrames ) {
+					var changed = curMat == null;
+					if( allTimes[f] == ctt[tp] ) {
+						changed = true;
+						tp++;
+					}
+					if( allTimes[f] == crt[rp] ) {
+						changed = true;
+						rp++;
+					}
+					if( allTimes[f] == cst[sp] ) {
+						changed = true;
+						sp++;
+					}
+					if( changed ) {
+						var m = new h3d.Matrix();
+						m.identity();
+						if( c.s == null || sp == 0 ) {
+							if( def.scale != null )
+								m.scale(def.scale.x, def.scale.y, def.scale.z);
+						} else
+							m.scale(csx[sp-1], csy[sp-1], csz[sp-1]);
+
+						if( c.r == null || rp == 0 ) {
+							if( def.rotate != null )
+								m.rotate(def.rotate.x, def.rotate.y, def.rotate.z);
+						} else
+							m.rotate(crx[rp-1] * F, cry[rp-1] * F, crz[rp-1] * F);
+							
+						if( def.preRot != null )
+							m.rotate(def.preRot.x, def.preRot.y, def.preRot.z);
+
+						if( c.t == null || tp == 0 ) {
+							if( def.trans != null )
+								m.translate(def.trans.x, def.trans.y, def.trans.z);
+						} else
+							m.translate(ctx[tp-1], cty[tp-1], ctz[tp-1]);
+
+						if( leftHand )
+							DefaultMatrixes.rightHandToLeft(m);
+							
+						curMat = m;
+					}
+					if( frames != null )
+						frames[f] = curMat;
+					if( alpha != null ) {
+						if( allTimes[f] == cat[ap] )
+							ap++;
+						alpha[f] = cav[ap - 1];
+					}
+				}
+				
+				if( frames != null )
+					anim.addCurve(c.name, frames);
+				if( alpha != null )
+					anim.addAlphaCurve(c.name, alpha);
+			}
+			return anim;
+			
+		case LinearAnim:
+			
+			var anim = new h3d.anim.LinearAnimation(animName, numFrames, sampling);
+			var q = new h3d.Quat(), q2 = new h3d.Quat();
+
+			for( c in curves ) {
+				var frames = c.t == null && c.r == null && c.s == null ? null : new haxe.ds.Vector(numFrames);
+				var alpha = c.a == null ? null : new haxe.ds.Vector(numFrames);
+				// skip empty curves
+				if( frames == null && alpha == null )
+					continue;
+				var ctx = c.t == null ? null : c.t.x;
+				var cty = c.t == null ? null : c.t.y;
+				var ctz = c.t == null ? null : c.t.z;
+				var ctt = c.t == null ? [-1.] : c.t.t;
+				var crx = c.r == null ? null : c.r.x;
+				var cry = c.r == null ? null : c.r.y;
+				var crz = c.r == null ? null : c.r.z;
+				var crt = c.r == null ? [-1.] : c.r.t;
+				var csx = c.s == null ? null : c.s.x;
+				var csy = c.s == null ? null : c.s.y;
+				var csz = c.s == null ? null : c.s.z;
+				var cst = c.s == null ? [ -1.] : c.s.t;
+				var cav = c.a == null ? null : c.a.v;
+				var cat = c.a == null ? null : c.a.t;
+				var def = c.def;
+				var tp = 0, rp = 0, sp = 0, ap = 0;
+				var curFrame = null;
+				for( f in 0...numFrames ) {
+					var changed = curFrame == null;
+					if( allTimes[f] == ctt[tp] ) {
+						changed = true;
+						tp++;
+					}
+					if( allTimes[f] == crt[rp] ) {
+						changed = true;
+						rp++;
+					}
+					if( allTimes[f] == cst[sp] ) {
+						changed = true;
+						sp++;
+					}
+					if( changed ) {
+						var f = new h3d.anim.LinearAnimation.LinearFrame();
+						if( c.s == null || sp == 0 ) {
+							if( def.scale != null ) {
+								f.sx = def.scale.x;
+								f.sy = def.scale.y;
+								f.sz = def.scale.z;
+							} else {
+								f.sx = 1;
+								f.sy = 1;
+								f.sx = 1;
+							}
+						} else {
+							f.sx = csx[sp - 1];
+							f.sy = csy[sp - 1];
+							f.sz = csz[sp - 1];
+						}
+
+						if( c.r == null || rp == 0 ) {
+							if( def.rotate != null ) {
+								q.initRotate(def.rotate.x, def.rotate.y, def.rotate.z);
+							} else
+								q.identity();
+						} else
+							q.initRotate(crx[rp-1] * F, cry[rp-1] * F, crz[rp-1] * F);
+							
+						if( def.preRot != null ) {
+							q2.initRotate(def.preRot.x, def.preRot.y, def.preRot.z);
+							q.multiply(q,q2);
+						}
+						
+						f.qx = q.x;
+						f.qy = q.y;
+						f.qz = q.z;
+						f.qw = q.w;
+
+						if( c.t == null || tp == 0 ) {
+							if( def.trans != null ) {
+								f.tx = def.trans.x;
+								f.ty = def.trans.y;
+								f.tz = def.trans.z;
+							} else {
+								f.tx = 0;
+								f.ty = 0;
+								f.tz = 0;
+							}
+						} else {
+							f.tx = ctx[tp - 1];
+							f.ty = cty[tp - 1];
+							f.tz = ctz[tp - 1];
+						}
+
+						if( leftHand ) {
+							f.tx *= -1;
+							f.qy *= -1;
+							f.qz *= -1;
+						}
+						
+						curFrame = f;
+					}
+					if( frames != null )
+						frames[f] = curFrame;
+					if( alpha != null ) {
+						if( allTimes[f] == cat[ap] )
+							ap++;
+						alpha[f] = cav[ap - 1];
+					}
+				}
+				
+				if( frames != null )
+					anim.addCurve(c.name, frames, c.r != null || def.rotate != null, c.s != null || def.scale != null);
+				if( alpha != null )
+					anim.addAlphaCurve(c.name, alpha);
+			}
+			return anim;
+			
+		}
+	}
+	
+	function sortDistinctFloats( a : Float, b : Float ) {
+		return if( a > b ) 1 else -1;
+	}
+	
+	function isNullJoint( model : FbxNode ) {
+		if( getParent(model, "Deformer", true) != null )
+			return false;
+		var parent = getParent(model, "Model", true);
+		if( parent == null )
+			return true;
+		var t = parent.getType();
+		if( t == "LimbNode" || t == "Root" )
+			return false;
+		return true;
+	}
+
+	public function makeObject( ?textureLoader : String -> FbxNode -> h3d.mat.MeshMaterial ) : h3d.scene.Object {
+		var scene = new h3d.scene.Object();
+		var hobjects = new Map();
+		var hgeom = new Map();
+		var objects = new Array();
+		var hjoints = new Map();
+		var joints = new Array();
+		var hskins = new Map();
+		
+		if( textureLoader == null ) {
+			var tmpTex = null;
+			textureLoader = function(_,_) {
+				if( tmpTex == null )
+					tmpTex = h3d.mat.Texture.fromColor(0xFFFF00FF);
+				return new h3d.mat.MeshMaterial(tmpTex);
+			}
+		}
+		// create all models
+		for( model in root.getAll("Objects.Model") ) {
+			var o : h3d.scene.Object;
+			var name = model.getName();
+			if( skipObjects.get(name) )
+				continue;
+			var mtype = model.getType();
+			if( unskinnedJointsAsObjects && mtype == "LimbNode" && isNullJoint(model) )
+				mtype = "Null";
+			switch( mtype ) {
+			case "Null", "Root", "Camera":
+				var hasJoint = false;
+				for( c in getChilds(model, "Model") )
+					if( c.getType() == "LimbNode" ) {
+						if( unskinnedJointsAsObjects && isNullJoint(c) ) continue;
+						hasJoint = true;
+						break;
+					}
+				if( hasJoint )
+					o = new h3d.scene.Skin(null, null, scene);
+				else
+					o = new h3d.scene.Object(scene);
+			case "LimbNode":
+				var j = new h3d.anim.Skin.Joint();
+				getDefaultMatrixes(model); // store for later usage in animation
+				j.index = model.getId();
+				j.name = model.getName();
+				hjoints.set(j.index, j);
+				joints.push({ model : model, joint : j });
+				continue;
+			case "Mesh":
+				// load geometry
+				var g = getChild(model, "Geometry");
+				var prim = hgeom.get(g.getId());
+				if( prim == null ) {
+					prim = new h3d.prim.FBXModel(new Geometry(this, g));
+					hgeom.set(g.getId(), prim);
+				}
+				// load materials
+				var mats = getChilds(model, "Material");
+				var tmats = [];
+				var vcolor = prim.geom.getColors() != null;
+				var lastAdded = 0;
+				for( mat in mats ) {
+					var tex = getChilds(mat, "Texture")[0];
+					if( tex == null ) {
+						tmats.push(null);
+						continue;
+					}
+					var mat = textureLoader(tex.get("FileName").props[0].toString(),mat);
+					if( vcolor )
+						mat.hasVertexColor = true;
+					tmats.push(mat);
+					lastAdded = tmats.length;
+				}
+				while( tmats.length > lastAdded )
+					tmats.pop();
+				if( tmats.length == 0 )
+					tmats.push(new h3d.mat.MeshMaterial(h2d.Tile.fromColor(0xFFFF00FF).getTexture()));
+				// create object
+				if( tmats.length == 1 )
+					o = new h3d.scene.Mesh(prim, tmats[0], scene);
+				else {
+					prim.multiMaterial = true;
+					o = new h3d.scene.MultiMaterial(prim, tmats, scene);
+				}
+			case type:
+				throw "Unknown model type " + type+" for "+model.getName();
+			}
+			o.name = name;
+			var m = getDefaultMatrixes(model);
+			if( m.trans != null || m.rotate != null || m.scale != null || m.preRot != null )
+				o.defaultTransform = m.toMatrix(leftHand);
+			hobjects.set(model.getId(), o);
+			objects.push( { model : model, obj : o } );
+		}
+		// rebuild joints hierarchy
+		for( j in joints ) {
+			var p = getParent(j.model, "Model");
+			var jparent = hjoints.get(p.getId());
+			if( jparent != null ) {
+				jparent.subs.push(j.joint);
+				j.joint.parent = jparent;
+			} else if( p.getType() != "Root" && p.getType() != "Null" )
+				throw "Parent joint not found " + p.getName();
+		}
+		// rebuild model hierarchy and additional inits
+		for( o in objects ) {
+			var rootJoints = [];
+			for( sub in getChilds(o.model, "Model") ) {
+				var sobj = hobjects.get(sub.getId());
+				if( sobj == null ) {
+					if( sub.getType() == "LimbNode" ) {
+						var j = hjoints.get(sub.getId());
+						if( j == null ) throw "Missing sub joint " + sub.getName();
+						rootJoints.push(j);
+						continue;
+					}
+					throw "Missing sub " + sub.getName();
+				}
+				o.obj.addChild(sobj);
+			}
+			if( rootJoints.length != 0 ) {
+				if( !Std.is(o.obj,h3d.scene.Skin) )
+					throw o.obj.name + ":" + o.model.getType() + " should be a skin";
+				var skin : h3d.scene.Skin = cast o.obj;
+				var skinData = createSkin(hskins, hgeom, rootJoints, bonesPerVertex);
+				// if we have a skinned object, remove it (only keep the skin) and set the material
+				for( osub in objects ) {
+					if( !osub.obj.isMesh() ) continue;
+					var m = osub.obj.toMesh();
+					if( m.primitive != skinData.primitive || m == skin )
+						continue;
+					skin.material = m.material;
+					m.remove();
+					// ignore key frames for this object
+					defaultModelMatrixes.get(osub.obj.name).wasRemoved = o.model.getId();
+				}
+				// set the skin data
+				if( skinData.boundJoints.length > maxBonesPerSkin )
+					skinData.split(maxBonesPerSkin, Std.instance(skinData.primitive,h3d.prim.FBXModel).geom.getIndexes().vidx);
+				skin.setSkinData(skinData);
+			}
+		}
+		return scene.numChildren == 1 ? scene.getChildAt(0) : scene;
+	}
+	
+	function keepJoint( j : h3d.anim.Skin.Joint ) {
+		return keepJoints.get(j.name);
+	}
+	
+	function createSkin( hskins : Map<Int,h3d.anim.Skin>, hgeom : Map<Int,h3d.prim.FBXModel>, rootJoints : Array<h3d.anim.Skin.Joint>, bonesPerVertex ) {
+		var allJoints = [];
+		function collectJoints(j:h3d.anim.Skin.Joint) {
+			// collect subs first (allow easy removal of terminal unskinned joints)
+			for( j in j.subs )
+				collectJoints(cast j);
+			allJoints.push(j);
+		}
+		for( j in rootJoints )
+			collectJoints(j);
+		var skin = null;
+		var geomTrans = null;
+		var iterJoints = allJoints.copy();
+		for( j in iterJoints ) {
+			var jModel = ids.get(j.index);
+			var subDef = getParent(jModel, "Deformer", true);
+			var defMat = defaultModelMatrixes.get(jModel.getName());
+			j.defMat = defMat.toMatrix(leftHand);
+			
+			if( subDef == null ) {
+				// if we have skinned subs, we need to keep in joint hierarchy
+				if( j.subs.length > 0 || keepJoint(j) )
+					continue;
+				// otherwise we're an ending bone, we can safely be removed
+				if( j.parent == null )
+					rootJoints.remove(j);
+				else
+					j.parent.subs.remove(j);
+				allJoints.remove(j);
+				// ignore key frames for this joint
+				defMat.wasRemoved = -1;
+				continue;
+			}
+			// create skin
+			if( skin == null ) {
+				var def = getParent(subDef, "Deformer");
+				skin = hskins.get(def.getId());
+				// shared skin between same instances
+				if( skin != null )
+					return skin;
+				var geom = hgeom.get(getParent(def, "Geometry").getId());
+				skin = new h3d.anim.Skin(geom.getVerticesCount(), bonesPerVertex);
+				geom.skin = skin;
+				skin.primitive = geom;
+				hskins.set(def.getId(), skin);
+			}
+			j.transPos = h3d.Matrix.L(subDef.get("Transform").getFloats());
+			if( leftHand ) DefaultMatrixes.rightHandToLeft(j.transPos);
+			
+			var weights = subDef.getAll("Weights");
+			if( weights.length > 0 ) {
+				var weights = weights[0].getFloats();
+				var vertex = subDef.get("Indexes").getInts();
+				for( i in 0...vertex.length ) {
+					var w = weights[i];
+					if( w < 0.01 )
+						continue;
+					skin.addInfluence(vertex[i], j, w);
+				}
+			}
+		}
+		if( skin == null )
+			throw "No joint is skinned ("+[for( j in iterJoints ) j.name].join(",")+")";
+		allJoints.reverse();
+		for( i in 0...allJoints.length )
+			allJoints[i].index = i;
+		skin.setJoints(allJoints, rootJoints);
+		skin.initWeights();
+		return skin;
+	}
+	
+	function getDefaultMatrixes( model : FbxNode ) {
+		var d = new DefaultMatrixes();
+		var F = Math.PI / 180;
+		for( p in model.getAll("Properties70.P") )
+			switch( p.props[0].toString() ) {
+			case "GeometricTranslation":
+				// handle in Geometry directly
+			case "PreRotation":
+				d.preRot = new Point(p.props[4].toFloat() * F, p.props[5].toFloat() * F, p.props[6].toFloat() * F);
+			case "Lcl Rotation":
+				d.rotate = new Point(p.props[4].toFloat() * F, p.props[5].toFloat() * F, p.props[6].toFloat() * F);
+			case "Lcl Translation":
+				d.trans = new Point(p.props[4].toFloat(), p.props[5].toFloat(), p.props[6].toFloat());
+			case "Lcl Scaling":
+				d.scale = new Point(p.props[4].toFloat(), p.props[5].toFloat(), p.props[6].toFloat());
+			default:
+			}
+		defaultModelMatrixes.set(model.getName(), d);
+		return d;
+	}
+	
+}

+ 273 - 0
h3d/fbx/Parser.hx

@@ -0,0 +1,273 @@
+package h3d.fbx;
+using h3d.fbx.Data;
+
+private enum Token {
+	TIdent( s : String );
+	TNode( s : String );
+	TInt( s : String );
+	TFloat( s : String );
+	TString( s : String );
+	TLength( v : Int );
+	TBraceOpen;
+	TBraceClose;
+	TColon;
+	TEof;
+}
+
+class Parser {
+
+	var line : Int;
+	var buf : String;
+	var pos : Int;
+	var token : Null<Token>;
+
+	function new() {
+	}
+
+	function parseText( str ) : FbxNode {
+		this.buf = str;
+		this.pos = 0;
+		this.line = 1;
+		token = null;
+		return {
+			name : "Root",
+			props : [PInt(0),PString("Root"),PString("Root")],
+			childs : parseNodes(),
+		};
+	}
+
+	function parseNodes() {
+		var nodes = [];
+		while( true ) {
+			switch( peek() ) {
+			case TEof, TBraceClose:
+				return nodes;
+			default:
+			}
+			nodes.push(parseNode());
+		}
+		return nodes;
+	}
+
+	function parseNode() : FbxNode {
+		var t = next();
+		var name = switch( t ) {
+		case TNode(n): n;
+		default: unexpected(t);
+		};
+		var props = [], childs = null;
+		while( true ) {
+			t = next();
+			switch( t ) {
+			case TFloat(s):
+				props.push(PFloat(Std.parseFloat(s)));
+			case TInt(s):
+				props.push(PInt(Std.parseInt(s)));
+			case TString(s):
+				props.push(PString(s));
+			case TIdent(s):
+				props.push(PIdent(s));
+			case TBraceOpen, TBraceClose:
+				token = t;
+			case TLength(v):
+				except(TBraceOpen);
+				except(TNode("a"));
+				var ints = [];
+				var floats : Array<Float> = null;
+				var i = 0;
+				while( i < v ) {
+					t = next();
+					switch( t ) {
+					case TColon:
+						continue;
+					case TInt(s):
+						i++;
+						if( floats == null )
+							ints.push(Std.parseInt(s));
+						else
+							floats.push(Std.parseInt(s));
+					case TFloat(s):
+						i++;
+						if( floats == null ) {
+							floats = [];
+							for( i in ints )
+								floats.push(i);
+							ints = null;
+						}
+						floats.push(Std.parseFloat(s));
+					default:
+						unexpected(t);
+					}
+				}
+				props.push(floats == null ? PInts(ints) : PFloats(floats));
+				except(TBraceClose);
+				break;
+			default:
+				unexpected(t);
+			}
+			t = next();
+			switch( t ) {
+			case TNode(_), TBraceClose:
+				token = t; // next
+				break;
+			case TColon:
+				// next prop
+			case TBraceOpen:
+				childs = parseNodes();
+				except(TBraceClose);
+				break;
+			default:
+				unexpected(t);
+			}
+		}
+		if( childs == null ) childs = [];
+		return { name : name, props : props, childs : childs };
+	}
+
+	function except( except : Token ) {
+		var t = next();
+		if( !Type.enumEq(t, except) )
+			error("Unexpected '" + tokenStr(t) + "' (" + tokenStr(except) + " expected)");
+	}
+
+	function peek() {
+		if( token == null )
+			token = nextToken();
+		return token;
+	}
+
+	function next() {
+		if( token == null )
+			return nextToken();
+		var tmp = token;
+		token = null;
+		return tmp;
+	}
+
+	function error( msg : String ) : Dynamic {
+		throw msg + " (line " + line + ")";
+		return null;
+	}
+
+	function unexpected( t : Token ) : Dynamic {
+		return error("Unexpected "+tokenStr(t));
+	}
+
+	function tokenStr( t : Token ) {
+		return switch( t ) {
+		case TEof: "<eof>";
+		case TBraceOpen: '{';
+		case TBraceClose: '}';
+		case TIdent(i): i;
+		case TNode(i): i+":";
+		case TFloat(f): f;
+		case TInt(i): i;
+		case TString(s): '"' + s + '"';
+		case TColon: ',';
+		case TLength(l): '*' + l;
+		};
+	}
+
+	inline function nextChar() {
+		return StringTools.fastCodeAt(buf, pos++);
+	}
+
+	inline function getBuf( pos : Int, len : Int ) {
+		return buf.substr(pos, len);
+	}
+
+	inline function isIdentChar(c) {
+		return (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || (c >= '0'.code && c <= '9'.code) || c == '_'.code || c == '-'.code;
+	}
+
+	@:noDebug
+	function nextToken() {
+		var start = pos;
+		while( true ) {
+			var c = nextChar();
+			switch( c ) {
+			case ' '.code, '\r'.code, '\t'.code:
+				start++;
+			case '\n'.code:
+				line++;
+				start++;
+			case ';'.code:
+				while( true ) {
+					var c = nextChar();
+					if( StringTools.isEof(c) || c == '\n'.code ) {
+						pos--;
+						break;
+					}
+				}
+				start = pos;
+			case ','.code:
+				return TColon;
+			case '{'.code:
+				return TBraceOpen;
+			case '}'.code:
+				return TBraceClose;
+			case '"'.code:
+				start = pos;
+				while( true ) {
+					c = nextChar();
+					if( c == '"'.code )
+						break;
+					if( StringTools.isEof(c) || c == '\n'.code )
+						error("Unclosed string");
+				}
+				return TString(getBuf(start, pos - start - 1));
+			case '*'.code:
+				start = pos;
+				do {
+					c = nextChar();
+				} while( c >= '0'.code && c <= '9'.code );
+				pos--;
+				return TLength(Std.parseInt(getBuf(start, pos - start)));
+			default:
+				if( (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || c == '_'.code ) {
+					do {
+						c = nextChar();
+					} while( isIdentChar(c) );
+					if( c == ':'.code )
+						return TNode(getBuf(start, pos - start - 1));
+					pos--;
+					return TIdent(getBuf(start, pos - start));
+				}
+				if( (c >= '0'.code && c <= '9'.code) || c == '-'.code ) {
+					do {
+						c = nextChar();
+					} while( c >= '0'.code && c <= '9'.code );
+					if( c != '.'.code && c != 'E'.code && c != 'e'.code && pos - start < 10 ) {
+						pos--;
+						return TInt(getBuf(start, pos - start));
+					}
+					if( c == '.'.code ) {
+						do {
+							c = nextChar();
+						} while( c >= '0'.code && c <= '9'.code );
+					}
+					if( c == 'e'.code || c == 'E'.code ) {
+						c = nextChar();
+						if( c != '-'.code && c != '+'.code )
+							pos--;
+						do {
+							c = nextChar();
+						} while( c >= '0'.code && c <= '9'.code );
+					}
+					pos--;
+					return TFloat(getBuf(start, pos - start));
+				}
+				if( StringTools.isEof(c) ) {
+					pos--;
+					return TEof;
+				}
+				error("Unexpected char '" + String.fromCharCode(c) + "'");
+			}
+		}
+	}
+
+	public static function parse( text : String ) {
+		return new Parser().parseText(text);
+	}
+
+}

+ 104 - 0
h3d/fbx/XBXReader.hx

@@ -0,0 +1,104 @@
+package h3d.fbx;
+import h3d.fbx.Data;
+
+class XBXReader
+{
+	var i : haxe.io.Input;
+	var version : Int;
+
+	public function new(i) {
+		this.i = i;
+	}
+
+	function error(?msg) {
+		throw "Invalid XBX data"+((null!=msg)? (": "+msg) : "");
+	}
+
+	function readString() {
+		var len = i.readByte();
+		if( len >= 0x80 )
+			len = (i.readUInt24() << 7) | (len & 0x7F);
+		return i.readString(len);
+	}
+
+	public function read() : FbxNode {
+		if( i.readString(3) != "XBX" )
+			error("no XBX sig");
+		version = i.readByte();
+		if( version > 0 )
+			error("version err "+version);
+		return readNode();
+	}
+
+	public function readNode() : FbxNode
+	{
+		return {
+			name: readString(),
+			props: {
+				var a = [];
+				var l = i.readByte();
+				a[l-1] = null;
+				for( i in 0...l )
+					a[i] = readProp();
+				a;
+			},
+			childs: {
+				var a = [];
+				var l = i.readInt24();
+				a[l - 1] = null;
+				for( i in 0...l)
+					a[i] = readNode();
+				a;
+			}
+		};
+	}
+
+	inline function readInt() {
+		#if haxe3
+		return i.readInt32();
+		#else
+		return i.readInt31();
+		#end
+	}
+
+	public function readProp()
+	{
+		var b = i.readByte();
+		var t = switch( b )
+		{
+			case 0: PInt( readInt());
+			case 1: PFloat( i.readDouble() );
+			case 2: PString( readString() );
+			case 3: PIdent( readString() );
+			case 4:
+				var l = readInt();
+				var a = [];
+				var tmp = hxd.impl.Tmp.getBytes(l * 4);
+				i.readFullBytes(tmp, 0, l * 4);
+				var r = hxd.impl.Memory.select(tmp);
+				a[l - 1] = 0;
+				for( idx in 0...l )
+					a[idx] = r.i32(idx << 2);
+				r.end();
+				hxd.impl.Tmp.saveBytes(tmp);
+				PInts( a );
+			case 5:
+				var l = readInt();
+				var a = [];
+				var tmp = hxd.impl.Tmp.getBytes(l * 8);
+				i.readFullBytes(tmp, 0, l * 8);
+				var r = hxd.impl.Memory.select(tmp);
+				a[l - 1] = 0.;
+				for( idx in 0...l)
+					a[idx] = r.double(idx << 3);
+				r.end();
+				hxd.impl.Tmp.saveBytes(tmp);
+				PFloats( a );
+			default:
+				error( "unknown prop " + b);
+				null;
+		}
+
+		return t;
+	}
+}

+ 76 - 0
h3d/fbx/XBXWriter.hx

@@ -0,0 +1,76 @@
+package h3d.fbx;
+import h3d.fbx.Data;
+
+/**
+ * ...
+ * @author de
+ */
+class XBXWriter
+{
+	var o : haxe.io.Output;
+	public function new(o) {
+		this.o = o;
+	}
+
+	public function write( n : FbxNode )
+	{
+		o.writeString("XBX");
+		o.writeByte(0); // version
+		writeNode(n);
+	}
+
+	function writeString( s : String ) {
+		if( s.length < 0x80 )
+			o.writeByte(s.length);
+		else {
+			o.writeByte(0x80 | (s.length & 0x7F));
+			o.writeUInt24(s.length >> 7);
+		}
+		o.writeString(s);
+	}
+
+	public function writeNode( n : FbxNode)
+	{
+		writeString( n.name);
+		o.writeByte( n.props.length );
+		for ( p in n.props)
+			writeProperty( p );
+
+		o.writeInt24( n.childs.length );
+		for ( c in n.childs )
+			writeNode( c );
+	}
+
+	inline function writeInt(v) {
+		#if haxe3
+		o.writeInt32(v);
+		#else
+		o.writeInt31(v);
+		#end
+	}
+
+
+	public function writeProperty( p : FbxProp )
+	{
+		o.writeByte( Type.enumIndex( p ) );
+
+		switch( p )
+		{
+			case PInt( v ):		writeInt( v );
+			case PFloat( v ):	o.writeDouble(v);
+			case PString( v ):	writeString( v );
+			case PIdent( v ): 	writeString( v );
+			case PInts( va ):
+				writeInt( va.length );
+				for ( i in va ) writeInt( i );
+			case PFloats( va ):
+				#if haxe3
+				o.writeInt32( va.length );
+				#else
+				o.writeInt31( va.length );
+				#end
+				for ( i in va ) o.writeDouble(i);
+		}
+	}
+
+}

+ 3 - 0
h3d/impl/AllocPos.hx

@@ -0,0 +1,3 @@
+package h3d.impl;
+
+typedef AllocPos = #if debug haxe.PosInfos #else { __alloc : Int } #end

+ 91 - 0
h3d/impl/Buffer.hx

@@ -0,0 +1,91 @@
+package h3d.impl;
+
+class Buffer {
+
+	public var b : MemoryManager.BigBuffer;
+	public var pos : Int;
+	public var nvert : Int;
+	public var next : Buffer;
+	#if debug
+	public var allocPos : AllocPos;
+	public var allocNext : Buffer;
+	public var allocPrev : Buffer;
+	#end
+	
+	public function new(b, pos, nvert) {
+		this.b = b;
+		this.pos = pos;
+		this.nvert = nvert;
+	}
+
+	public function isDisposed() {
+		return b == null || b.isDisposed();
+	}
+	
+	public function dispose() {
+		if( b != null ) {
+			b.freeCursor(pos, nvert);
+			#if debug
+			if( allocNext != null )
+				allocNext.allocPrev = allocPrev;
+			if( allocPrev != null )
+				allocPrev.allocNext = allocNext;
+			if( b.allocHead == this )
+				b.allocHead = allocNext;
+			#end
+			b = null;
+			if( next != null ) next.dispose();
+		}
+	}
+	
+	public function uploadVector( data : hxd.FloatBuffer, dataPos : Int, nverts : Int ) {
+		var cur = this;
+		while( nverts > 0 ) {
+			if( cur == null ) throw "Too many vertexes";
+			var count = nverts > cur.nvert ? cur.nvert : nverts;
+			cur.b.mem.driver.uploadVertexBuffer(cur.b.vbuf, cur.pos, count, data, dataPos);
+			dataPos += count * b.stride;
+			nverts -= count;
+			cur = cur.next;
+		}
+	}
+	
+	public function uploadBytes( data : haxe.io.Bytes, dataPos : Int, nverts : Int ) {
+		var cur = this;
+		while( nverts > 0 ) {
+			if( cur == null ) throw "Too many vertexes";
+			var count = nverts > cur.nvert ? cur.nvert : nverts;
+			cur.b.mem.driver.uploadVertexBytes(cur.b.vbuf, cur.pos, count, data, dataPos);
+			dataPos += count * b.stride * 4;
+			nverts -= count;
+			cur = cur.next;
+		}
+	}
+	
+}
+
+class BufferOffset {
+	public var id : Int;
+	public var b : Buffer;
+	public var offset : Int;
+	
+	/*
+		This is used to return a list of BufferOffset without allocating an array
+	*/
+	public var next : BufferOffset;
+	
+	static var UID = 0;
+	
+	public function new(b, offset) {
+		this.id = UID++;
+		this.b = b;
+		this.offset = offset;
+	}
+	public function dispose() {
+		if( b != null ) {
+			b.dispose();
+			b = null;
+		}
+		next = null;
+	}
+}

+ 68 - 0
h3d/impl/DebugGL.hx

@@ -0,0 +1,68 @@
+package h3d.impl;
+
+#if js
+private typedef GL = js.html.webgl.GL;
+#elseif cpp
+import openfl.gl.GL;
+#end
+
+#if (!macro && (js || cpp))
+@:build(h3d.impl.DebugGL.buildProxy())
+#end
+class DebugGL {
+	
+	
+	#if macro
+	static function buildProxy() {
+		var gl = haxe.macro.Context.getType("GL");
+		var fields = [];
+		var pos = haxe.macro.Context.currentPos();
+		switch( gl ) {
+		case TInst(cl, _):
+			for( f in cl.get().statics.get() ) {
+				var fname = f.name;
+				if( !f.isPublic ) continue;
+				switch( f.kind ) {
+				case FVar(_):
+					if( fname.toUpperCase() == fname )
+						fields.push((macro class { public static var $fname = GL.$fname; } ).fields[0] );
+				case FMethod(_):
+					switch( haxe.macro.Context.follow(f.type) ) {
+					case TFun(args, ret):
+						var eargs = [for( a in args ) macro $i { a.name } ];
+						var expr = if( haxe.macro.TypeTools.toString(haxe.macro.Context.follow(ret)) == "Void" )
+							macro {
+								GL.$fname($a{eargs});
+								haxe.Log.trace($v { fname } + ([$a { eargs } ] : Array<Dynamic>), _pos);
+							};
+						else
+							macro {
+								var r = GL.$fname($a { eargs } );
+								haxe.Log.trace($v{fname} + ([$a{eargs}] : Array<Dynamic>) + "=" + r, _pos);
+								return r;
+							};
+						var fargs = [for( a in args ) { name : a.name, opt : false, type : null, value : null } ];
+						fargs.push( { name : "_pos", opt : true, type : macro : haxe.PosInfos, value : null } );
+						fields.push({
+							name : fname,
+							access : [APublic, AStatic],
+							meta : [],
+							pos : pos,
+							kind : FFun( {
+								ret : null,
+								args : fargs,
+								params : [],
+								expr : expr,
+							}),
+						});
+					default:
+					}
+				}
+			}
+		default:
+		}
+		return fields;
+	}
+	#end
+	
+}

+ 125 - 0
h3d/impl/Driver.hx

@@ -0,0 +1,125 @@
+package h3d.impl;
+
+#if flash
+typedef IndexBuffer = flash.display3D.IndexBuffer3D;
+typedef VertexBuffer = Stage3dDriver.VertexWrapper;
+typedef Texture = flash.display3D.textures.TextureBase;
+#elseif js
+typedef IndexBuffer = js.html.webgl.Buffer;
+typedef VertexBuffer = { b : js.html.webgl.Buffer, stride : Int };
+typedef Texture = js.html.webgl.Texture;
+#elseif cpp
+typedef IndexBuffer = openfl.gl.GLBuffer;
+typedef VertexBuffer = { b : openfl.gl.GLBuffer, stride : Int };
+typedef Texture = openfl.gl.GLTexture;
+#else
+typedef IndexBuffer = Int;
+typedef VertexBuffer = Int;
+typedef Texture = Int;
+#end
+
+class Driver {
+	
+	public function isDisposed() {
+		return true;
+	}
+	
+	public function dispose() {
+	}
+	
+	public function clear( r : Float, g : Float, b : Float, a : Float ) {
+	}
+	
+	public function setCapture( bmp : hxd.BitmapData, callb : Void -> Void ) {
+	}
+	
+	public function reset() {
+	}
+	
+	public function getDriverName( details : Bool ) {
+		return "Not available";
+	}
+	
+	public function init( onCreate : Bool -> Void, forceSoftware = false ) {
+	}
+	
+	public function resize( width : Int, height : Int ) {
+	}
+	
+	public function selectMaterial( mbits : Int ) {
+	}
+	
+	/** return value tells if we have shader shader **/
+	public function selectShader( shader : Shader ) : Bool {
+		return false;
+	}
+	
+	public function getShaderInputNames() : Array<String> {
+		return null;
+	}
+	
+	public function selectBuffer( buffer : VertexBuffer ) {
+	}
+	
+	public function selectMultiBuffers( buffers : Buffer.BufferOffset ) {
+	}
+	
+	public function draw( ibuf : IndexBuffer, startIndex : Int, ntriangles : Int ) {
+	}
+	
+	public function setRenderZone( x : Int, y : Int, width : Int, height : Int ) {
+	}
+	
+	public function setRenderTarget( tex : Null<Texture>, useDepth : Bool, clearColor : Int ) {
+	}
+	
+	public function present() {
+	}
+	
+	public function isHardware() {
+		return true;
+	}
+	
+	public function setDebug( b : Bool ) {
+	}
+	
+	public function allocTexture( t : h3d.mat.Texture ) : Texture {
+		return null;
+	}
+
+	public function allocIndexes( count : Int ) : IndexBuffer {
+		return null;
+	}
+
+	public function allocVertex( count : Int, stride : Int ) : VertexBuffer {
+		return null;
+	}
+	
+	public function disposeTexture( t : Texture ) {
+	}
+	
+	public function disposeIndexes( i : IndexBuffer ) {
+	}
+	
+	public function disposeVertex( v : VertexBuffer ) {
+	}
+	
+	public function uploadIndexesBuffer( i : IndexBuffer, startIndice : Int, indiceCount : Int, buf : hxd.IndexBuffer, bufPos : Int ) {
+	}
+
+	public function uploadIndexesBytes( i : IndexBuffer, startIndice : Int, indiceCount : Int, buf : haxe.io.Bytes , bufPos : Int ) {
+	}
+	
+	public function uploadVertexBuffer( v : VertexBuffer, startVertex : Int, vertexCount : Int, buf : hxd.FloatBuffer, bufPos : Int ) {
+	}
+
+	public function uploadVertexBytes( v : VertexBuffer, startVertex : Int, vertexCount : Int, buf : haxe.io.Bytes, bufPos : Int ) {
+	}
+	
+	public function uploadTextureBitmap( t : h3d.mat.Texture, bmp : hxd.BitmapData, mipLevel : Int, side : Int ) {
+	}
+
+	public function uploadTexturePixels( t : h3d.mat.Texture, pixels : hxd.Pixels, mipLevel : Int, side : Int ) {
+	}
+	
+}

+ 546 - 0
h3d/impl/GlDriver.hx

@@ -0,0 +1,546 @@
+package h3d.impl;
+import h3d.impl.Driver;
+
+#if (js||cpp)
+
+#if js
+import js.html.Uint16Array;
+import js.html.Uint8Array;
+import js.html.Float32Array;
+private typedef GL = js.html.webgl.GL;
+#elseif cpp
+import openfl.gl.GL;
+private typedef Uint16Array = openfl.utils.Int16Array;
+private typedef Uint8Array = openfl.utils.UInt8Array;
+private typedef Float32Array = openfl.utils.Float32Array;
+#end
+
+@:access(h3d.impl.Shader)
+class GlDriver extends Driver {
+
+	#if js
+	var canvas : js.html.CanvasElement;
+	public var gl : js.html.webgl.RenderingContext;
+	#elseif cpp
+	static var gl = GL;
+	var fixMult : Bool;
+	#end
+	
+	var curAttribs : Int;
+	var curShader : Shader.ShaderInstance;
+	var curMatBits : Int;
+	
+	public function new() {
+		#if js
+		canvas = cast js.Browser.document.getElementById("webgl");
+		if( canvas == null ) throw "Canvas #webgl not found";
+		gl = canvas.getContextWebGL();
+		if( gl == null ) throw "Could not acquire GL context";
+		// debug if webgl_debug.js is included
+		untyped if( __js__('typeof')(WebGLDebugUtils) != "undefined" ) gl = untyped WebGLDebugUtils.makeDebugContext(gl);
+		#elseif cpp
+		// check for a bug in HxCPP handling of sub buffers
+		var tmp = new Float32Array(8);
+		var sub = new Float32Array(tmp.buffer, 0, 4);
+		fixMult = sub.length == 1; // should be 4
+		#end
+
+		curAttribs = 0;
+		curMatBits = -1;
+		selectMaterial(0);
+	}
+	
+	override function reset() {
+		curShader = null;
+		gl.useProgram(null);
+	}
+	
+	override function selectMaterial( mbits : Int ) {
+		var diff = curMatBits ^ mbits;
+		if( diff == 0 )
+			return;
+		if( diff & 3 != 0 ) {
+			if( mbits & 3 == 0 )
+				gl.disable(GL.CULL_FACE);
+			else {
+				if( curMatBits & 3 == 0 ) gl.enable(GL.CULL_FACE);
+				gl.cullFace(FACES[mbits&3]);
+			}
+		}
+		if( diff & (0xFF << 6) != 0 ) {
+			var src = (mbits >> 6) & 15;
+			var dst = (mbits >> 10) & 15;
+			if( src == 0 && dst == 1 )
+				gl.disable(GL.BLEND);
+			else {
+				if( curMatBits < 0 || (curMatBits >> 6) & 0xFF == 0x10 ) gl.enable(GL.BLEND);
+				gl.blendFunc(BLEND[src], BLEND[dst]);
+			}
+		}
+	
+		if( diff & (15 << 2) != 0 ) {
+			var write = (mbits >> 2) & 1 == 1;
+			if( curMatBits < 0 || diff & 4 != 0 ) gl.depthMask(write);
+			var cmp = (mbits >> 3) & 7;
+			if( cmp == 0 )
+				gl.disable(GL.DEPTH_TEST);
+			else {
+				if( curMatBits < 0 || (curMatBits >> 3) & 7 == 0 ) gl.enable(GL.DEPTH_TEST);
+				gl.depthFunc(COMPARE[cmp]);
+			}
+		}
+			
+		if( diff & (15 << 14) != 0 )
+			gl.colorMask((mbits >> 14) & 1 != 0, (mbits >> 14) & 2 != 0, (mbits >> 14) & 4 != 0, (mbits >> 14) & 8 != 0);
+			
+		curMatBits = mbits;
+	}
+	
+	override function clear( r : Float, g : Float, b : Float, a : Float ) {
+		gl.clearColor(r, g, b, a);
+		gl.clearDepth(1);
+		gl.clear(GL.COLOR_BUFFER_BIT|GL.DEPTH_BUFFER_BIT);
+	}
+	
+	override function resize(width, height) {
+		#if js
+		canvas.width = width;
+		canvas.height = height;
+		#elseif cpp
+		// resize window
+		#end
+		gl.viewport(0, 0, width, height);
+	}
+	
+	override function allocTexture( t : h3d.mat.Texture ) : Texture {
+		var tt = gl.createTexture();
+		gl.bindTexture(GL.TEXTURE_2D, tt);
+		gl.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, t.width, t.height, 0, GL.RGBA, GL.UNSIGNED_BYTE, null);
+		gl.bindTexture(GL.TEXTURE_2D, null);
+		return tt;
+	}
+	
+	override function allocVertex( count : Int, stride : Int ) : VertexBuffer {
+		var b = gl.createBuffer();
+		#if js
+		gl.bindBuffer(GL.ARRAY_BUFFER, b);
+		gl.bufferData(GL.ARRAY_BUFFER, count * stride * 4, GL.STATIC_DRAW);
+		gl.bindBuffer(GL.ARRAY_BUFFER, null);
+		#else
+		var tmp = new Uint8Array(count * stride * 4);
+		gl.bindBuffer(GL.ARRAY_BUFFER, b);
+		gl.bufferData(GL.ARRAY_BUFFER, tmp, GL.STATIC_DRAW);
+		gl.bindBuffer(GL.ARRAY_BUFFER, null);
+		#end
+		return { b : b, stride : stride };
+	}
+	
+	override function allocIndexes( count : Int ) : IndexBuffer {
+		var b = gl.createBuffer();
+		#if js
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, b);
+		gl.bufferData(GL.ELEMENT_ARRAY_BUFFER, count * 2, GL.STATIC_DRAW);
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, null);
+		#else
+		var tmp = new Uint16Array(count);
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, b);
+		gl.bufferData(GL.ELEMENT_ARRAY_BUFFER, tmp, GL.STATIC_DRAW);
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, null);
+		#end
+		return b;
+	}
+
+	override function disposeTexture( t : Texture ) {
+		gl.deleteTexture(t);
+	}
+
+	override function disposeIndexes( i : IndexBuffer ) {
+		gl.deleteBuffer(i);
+	}
+	
+	override function disposeVertex( v : VertexBuffer ) {
+		gl.deleteBuffer(v.b);
+	}
+	
+	override function uploadTexturePixels( t : h3d.mat.Texture, pixels : hxd.Pixels, mipLevel : Int, side : Int ) {
+		gl.bindTexture(GL.TEXTURE_2D, t.t);
+		pixels.convert(RGBA);
+		var pixels = new Uint8Array(pixels.bytes.getData());
+		gl.texImage2D(GL.TEXTURE_2D, mipLevel, GL.RGBA, t.width, t.height, 0, GL.RGBA, GL.UNSIGNED_BYTE, pixels);
+		gl.bindTexture(GL.TEXTURE_2D, null);
+	}
+	
+	override function uploadVertexBuffer( v : VertexBuffer, startVertex : Int, vertexCount : Int, buf : hxd.FloatBuffer, bufPos : Int ) {
+		var stride : Int = v.stride;
+		var buf = new Float32Array(buf.getNative());
+		var sub = new Float32Array(buf.buffer, bufPos, vertexCount * stride #if cpp * (fixMult?4:1) #end);
+		gl.bindBuffer(GL.ARRAY_BUFFER, v.b);
+		gl.bufferSubData(GL.ARRAY_BUFFER, startVertex * stride * 4, sub);
+		gl.bindBuffer(GL.ARRAY_BUFFER, null);
+	}
+
+	override function uploadVertexBytes( v : VertexBuffer, startVertex : Int, vertexCount : Int, buf : haxe.io.Bytes, bufPos : Int ) {
+		var stride : Int = v.stride;
+		var buf = new Uint8Array(buf.getData());
+		var sub = new Uint8Array(buf.buffer, bufPos, vertexCount * stride * 4);
+		gl.bindBuffer(GL.ARRAY_BUFFER, v.b);
+		gl.bufferSubData(GL.ARRAY_BUFFER, startVertex * stride * 4, sub);
+		gl.bindBuffer(GL.ARRAY_BUFFER, null);
+	}
+
+	override function uploadIndexesBuffer( i : IndexBuffer, startIndice : Int, indiceCount : Int, buf : hxd.IndexBuffer, bufPos : Int ) {
+		var buf = new Uint16Array(buf.getNative());
+		var sub = new Uint16Array(buf.buffer, bufPos, indiceCount #if cpp * (fixMult?2:1) #end);
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, i);
+		gl.bufferSubData(GL.ELEMENT_ARRAY_BUFFER, startIndice * 2, sub);
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, null);
+	}
+
+	override function uploadIndexesBytes( i : IndexBuffer, startIndice : Int, indiceCount : Int, buf : haxe.io.Bytes , bufPos : Int ) {
+		var buf = new Uint8Array(buf.getData());
+		var sub = new Uint8Array(buf.buffer, bufPos, indiceCount * 2);
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, i);
+		gl.bufferSubData(GL.ELEMENT_ARRAY_BUFFER, startIndice * 2, sub);
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, null);
+	}
+	
+	function decodeType( t : String ) : Shader.ShaderType {
+		return switch( t ) {
+		case "float": Float;
+		case "vec2": Vec2;
+		case "vec3": Vec3;
+		case "vec4": Vec4;
+		case "mat4": Mat4;
+		default: throw "Unknown type " + t;
+		}
+	}
+	
+	function decodeTypeInt( t : Int ) : Shader.ShaderType {
+		return switch( t ) {
+		case GL.SAMPLER_2D:	Tex2d;
+		case GL.SAMPLER_CUBE: TexCube;
+		case GL.FLOAT: Float;
+		case GL.FLOAT_VEC2: Vec2;
+		case GL.FLOAT_VEC3: Vec3;
+		case GL.FLOAT_VEC4: Vec4;
+		case GL.FLOAT_MAT2: Mat2;
+		case GL.FLOAT_MAT3: Mat3;
+		case GL.FLOAT_MAT4: Mat4;
+		default:
+			gl.pixelStorei(t, 0); // get DEBUG value
+			throw "Unknown type " + t;
+		}
+	}
+	
+	function typeSize( t : Shader.ShaderType ) {
+		return switch( t ) {
+		case Float, Byte4, Byte3: 1;
+		case Vec2: 2;
+		case Vec3: 3;
+		case Vec4: 4;
+		case Mat2: 4;
+		case Mat3: 9;
+		case Mat4: 16;
+		case Tex2d, TexCube, Struct(_), Index(_): throw "Unexpected " + t;
+		}
+	}
+	
+	function buildShaderInstance( shader : Shader ) {
+		var cl = Type.getClass(shader);
+		function compileShader(type) {
+			var vertex = type == GL.VERTEX_SHADER;
+			var name = vertex ? "VERTEX" : "FRAGMENT";
+			var code = Reflect.field(cl, name);
+			if( code == null ) throw "Missing " + Type.getClassName(cl) + "." + name + " shader source";
+			var cst = shader.getConstants(vertex);
+			code = StringTools.trim(cst + code);
+			#if cpp
+			code = "#define lowp\n#define mediump\n#define highp\n"+code;
+			#end
+			// replace haxe-like #if/#else/#end by GLSL ones
+			code = ~/#if ([A-Za-z0-9_]+)/g.replace(code, "#if defined($1)");
+			code = ~/#elseif ([A-Za-z0-9_]+)/g.replace(code, "#elif defined($1)");
+			code = code.split("#end").join("#endif");
+			var s = gl.createShader(type);
+			gl.shaderSource(s, code);
+			gl.compileShader(s);
+			if( gl.getShaderParameter(s, GL.COMPILE_STATUS) != cast 1 ) {
+				var log = gl.getShaderInfoLog(s);
+				var line = code.split("\n")[Std.parseInt(log.substr(9)) - 1];
+				if( line == null ) line = "" else line = "(" + StringTools.trim(line) + ")";
+				throw "An error occurred compiling the shaders: " + log + line;
+			}
+			return s;
+		}
+		var vs = compileShader(GL.VERTEX_SHADER);
+		var fs = compileShader(GL.FRAGMENT_SHADER);
+		
+		var p = gl.createProgram();
+		gl.attachShader(p, vs);
+		gl.attachShader(p, fs);
+		gl.linkProgram(p);
+		if( gl.getProgramParameter(p, GL.LINK_STATUS) != cast 1 ) {
+			var log = gl.getProgramInfoLog(p);
+			throw "Program linkage failure: "+log;
+		}
+	
+		var inst = new Shader.ShaderInstance();
+			
+		var nattr = gl.getProgramParameter(p, GL.ACTIVE_ATTRIBUTES);
+		inst.attribs = [];
+		
+		var amap = new Map();
+		for( k in 0...nattr ) {
+			var inf = gl.getActiveAttrib(p, k);
+			amap.set(inf.name, { index : k, inf : inf });
+		}
+		
+		
+		var code = gl.getShaderSource(vs);
+
+		// remove (and save) all #define's
+		var rdef = ~/#define ([A-Za-z0-9_]+)/;
+		var defs = new Map();
+		while( rdef.match(code) ) {
+			defs.set(rdef.matched(1), true);
+			code = rdef.matchedLeft() + rdef.matchedRight();
+		}
+		
+		// remove parts of the codes that are undefined
+		var rif = ~/#if defined\(([A-Za-z0-9_]+)\)([^#]+)#endif/;
+		while( rif.match(code) ) {
+			if( defs.get(rif.matched(1)) )
+				code = rif.matchedLeft() + rif.matched(2) + rif.matchedRight();
+			else
+				code = rif.matchedLeft() + rif.matchedRight();
+		}
+		
+		// extract attributes from code (so we know the offset and stride)
+		var r = ~/attribute[ \t\r\n]+([A-Za-z0-9_]+)[ \t\r\n]+([A-Za-z0-9_]+)/;
+		var offset = 0;
+		var ccode = code;
+		while( r.match(ccode) ) {
+			var aname = r.matched(2);
+			var atype = decodeType(r.matched(1));
+			var a = amap.get(aname);
+			var size = typeSize(atype);
+			if( a != null )
+				inst.attribs.push( { name : aname, type : atype, etype : GL.FLOAT, size : size, index : a.index, offset : offset } );
+			offset += size;
+			ccode = r.matchedRight();
+		}
+		inst.stride = offset;
+		
+		// list uniforms needed by shader
+		var allCode = code + gl.getShaderSource(fs);
+		var nuni = gl.getProgramParameter(p, GL.ACTIVE_UNIFORMS);
+		inst.uniforms = [];
+		var texIndex = -1;
+		var r_array = ~/\[([0-9]+)\]$/;
+		for( k in 0...nuni ) {
+			var inf = gl.getActiveUniform(p, k);
+			if( inf.name.substr(0, 6) == "webgl_" )
+				continue; // skip native uniforms
+			var t = decodeTypeInt(inf.type);
+			switch( t ) {
+			case Tex2d, TexCube:
+				texIndex++;
+			case Vec3:
+				var r = new EReg(inf.name + "[ \\t]*\\/\\*([A-Za-z0-9_]+)\\*\\/", "");
+				if( r.match(allCode) )
+					switch( r.matched(1) ) {
+					case "byte4":
+						t = Byte3;
+					default:
+					}
+			case Vec4:
+				var r = new EReg(inf.name + "[ \\t]*\\/\\*([A-Za-z0-9_]+)\\*\\/", "");
+				if( r.match(allCode) )
+					switch( r.matched(1) ) {
+					case "byte4":
+						t = Byte4;
+					default:
+					}
+			default:
+			}
+			var name = inf.name;
+			while( true ) {
+				if( r_array.match(name) ) {
+					name = r_array.matchedLeft();
+					t = Index(Std.parseInt(r_array.matched(1)), t);
+					continue;
+				}
+				var c = name.lastIndexOf(".");
+				if( c > 0 ) {
+					var field = name.substr(c + 1);
+					name = name.substr(0, c);
+					t = Struct(field, t);
+				}
+				break;
+			}
+			inst.uniforms.push( {
+				name : name,
+				type : t,
+				loc : gl.getUniformLocation(p, inf.name),
+				index : texIndex,
+			});
+		}
+		inst.program = p;
+		return inst;
+	}
+
+	override function selectShader( shader : Shader ) : Bool {
+		var change = false;
+		if( shader.instance == null )
+			shader.instance = buildShaderInstance(shader);
+		if( shader.instance != curShader ) {
+			curShader = shader.instance;
+			gl.useProgram(curShader.program);
+			for( i in curAttribs...curShader.attribs.length ) {
+				gl.enableVertexAttribArray(i);
+				curAttribs++;
+			}
+			while( curAttribs > curShader.attribs.length )
+				gl.disableVertexAttribArray(--curAttribs);
+			change = true;
+		}
+			
+		
+		for( u in curShader.uniforms ) {
+			var val : Dynamic = Reflect.field(shader, u.name);
+			if( val == null ) throw "Missing shader value " + u.name;
+			setUniform(val, u, u.type);
+		}
+		shader.customSetup(this);
+		
+		return change;
+	}
+	
+	public function setupTexture( t : h3d.mat.Texture, mipMap : h3d.mat.Data.MipMap, filter : h3d.mat.Data.Filter, wrap : h3d.mat.Data.Wrap ) {
+		gl.bindTexture(GL.TEXTURE_2D, t.t);
+		var flags = TFILTERS[Type.enumIndex(mipMap)][Type.enumIndex(filter)];
+		gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, flags[0]);
+		gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, flags[1]);
+		var w = TWRAP[Type.enumIndex(wrap)];
+		gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, w);
+		gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, w);
+	}
+	
+	function setUniform( val : Dynamic, u : Shader.Uniform, t : Shader.ShaderType ) {
+		switch( t ) {
+		case Mat4:
+			var m : Matrix = val;
+			gl.uniformMatrix4fv(u.loc, false, new Float32Array(m.getFloats()));
+		case Tex2d:
+			var t : h3d.mat.Texture = val;
+			setupTexture(t, t.mipMap, t.filter, t.wrap);
+			gl.activeTexture(GL.TEXTURE0 + u.index);
+			gl.uniform1i(u.loc, u.index);
+		case Float:
+			gl.uniform1f(u.loc, val);
+		case Vec2:
+			var v : h3d.Vector = val;
+			gl.uniform2f(u.loc, v.x, v.y);
+		case Vec3:
+			var v : h3d.Vector = val;
+			gl.uniform3f(u.loc, v.x, v.y, v.z);
+		case Vec4:
+			var v : h3d.Vector = val;
+			gl.uniform4f(u.loc, v.x, v.y, v.z, v.w);
+		case Struct(field, t):
+			var v = Reflect.field(val, field);
+			if( v == null ) throw "Missing shader field " + field;
+			setUniform(v, u, t);
+		case Index(index, t):
+			var v = val[index];
+			if( v == null ) throw "Missing shader index " + index;
+			setUniform(v, u, t);
+		case Byte4:
+			var v : Int = val;
+			gl.uniform4f(u.loc, ((v >> 16) & 0xFF) / 255, ((v >> 8) & 0xFF) / 255, (v & 0xFF) / 255, (v >>> 24) / 255);
+		case Byte3:
+			var v : Int = val;
+			gl.uniform3f(u.loc, ((v >> 16) & 0xFF) / 255, ((v >> 8) & 0xFF) / 255, (v & 0xFF) / 255);
+		default:
+			throw "Unsupported uniform " + u.type;
+		}
+		
+	}
+	
+	override function selectBuffer( v : VertexBuffer ) {
+		var stride : Int = v.stride;
+		if( stride < curShader.stride )
+			throw "Buffer stride (" + stride + ") and shader stride (" + curShader.stride + ") mismatch";
+		gl.bindBuffer(GL.ARRAY_BUFFER, v.b);
+		for( a in curShader.attribs )
+			gl.vertexAttribPointer(a.index, a.size, a.etype, false, stride * 4, a.offset * 4);
+	}
+	
+	override function draw( ibuf : IndexBuffer, startIndex : Int, ntriangles : Int ) {
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, ibuf);
+		gl.drawElements(GL.TRIANGLES, ntriangles * 3, GL.UNSIGNED_SHORT, startIndex * 2);
+		gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, null);
+	}
+	
+	override function present() {
+		gl.finish();
+	}
+
+	override function isDisposed() {
+		return false;
+	}
+
+	override function init( onCreate : Bool -> Void, forceSoftware = false ) {
+		haxe.Timer.delay(onCreate.bind(false), 1);
+	}
+	
+	static var TFILTERS = [
+		[[GL.NEAREST,GL.NEAREST],[GL.LINEAR,GL.LINEAR]],
+		[[GL.NEAREST,GL.NEAREST_MIPMAP_NEAREST],[GL.LINEAR,GL.LINEAR_MIPMAP_NEAREST]],
+		[[GL.NEAREST,GL.NEAREST_MIPMAP_LINEAR],[GL.LINEAR,GL.LINEAR_MIPMAP_LINEAR]],
+	];
+	
+	static var TWRAP = [
+		GL.CLAMP_TO_EDGE,
+		GL.REPEAT,
+	];
+	
+	static var FACES = [
+		0,
+		GL.FRONT, // front/back reversed wrt stage3d
+		GL.BACK,
+		GL.FRONT_AND_BACK,
+	];
+	
+	static var BLEND = [
+		GL.ONE,
+		GL.ZERO,
+		GL.SRC_ALPHA,
+		GL.SRC_COLOR,
+		GL.DST_ALPHA,
+		GL.DST_COLOR,
+		GL.ONE_MINUS_SRC_ALPHA,
+		GL.ONE_MINUS_SRC_COLOR,
+		GL.ONE_MINUS_DST_ALPHA,
+		GL.ONE_MINUS_DST_COLOR,
+		GL.CONSTANT_COLOR,
+		GL.CONSTANT_ALPHA,
+		GL.ONE_MINUS_CONSTANT_COLOR,
+		GL.ONE_MINUS_CONSTANT_ALPHA,
+		GL.SRC_ALPHA_SATURATE,
+	];
+	
+	static var COMPARE = [
+		GL.ALWAYS,
+		GL.NEVER,
+		GL.EQUAL,
+		GL.NOTEQUAL,
+		GL.GREATER,
+		GL.GEQUAL,
+		GL.LESS,
+		GL.LEQUAL,
+	];
+
+}
+
+#end

+ 30 - 0
h3d/impl/Indexes.hx

@@ -0,0 +1,30 @@
+package h3d.impl;
+
+@:allow(h3d.impl.MemoryManager)
+@:allow(h3d.Engine)
+class Indexes {
+
+	var mem : MemoryManager;
+	var ibuf : Driver.IndexBuffer;
+	public var count(default,null) : Int;
+	
+	function new(mem, ibuf, count) {
+		this.mem = mem;
+		this.ibuf = ibuf;
+		this.count = count;
+	}
+	
+	public function isDisposed() {
+		return ibuf == null;
+	}
+	
+	public function upload( indexes : hxd.IndexBuffer, pos : Int, count : Int, bufferPos = 0 ) {
+		mem.driver.uploadIndexesBuffer(this.ibuf, pos, count, indexes, bufferPos);
+	}
+	
+	public function dispose() {
+		if( ibuf != null )
+			mem.deleteIndexes(this);
+	}
+
+}

+ 566 - 0
h3d/impl/MemoryManager.hx

@@ -0,0 +1,566 @@
+package h3d.impl;
+
+#if flash
+private typedef WeakMap<K,T> = haxe.ds.WeakMap<K,T>;
+#else
+private typedef WeakMap<K,T> = haxe.ds.ObjectMap<K,T>;
+#end
+
+@:allow(h3d)
+class FreeCell {
+	var pos : Int;
+	var count : Int;
+	var next : FreeCell;
+	function new(pos,count,next) {
+		this.pos = pos;
+		this.count = count;
+		this.next = next;
+	}
+}
+
+@:allow(h3d)
+class BigBuffer {
+
+	var mem : MemoryManager;
+	var stride : Int;
+	var size : Int;
+	
+	var vbuf : Driver.VertexBuffer;
+	var free : FreeCell;
+	var next : BigBuffer;
+	#if debug
+	public var allocHead : Buffer;
+	#end
+	
+	function new(mem, v, stride, size) {
+		this.mem = mem;
+		this.size = size;
+		this.stride = stride;
+		this.vbuf = v;
+		this.free = new FreeCell(0,size,null);
+	}
+
+	function freeCursor( pos:Int, nvect:Int ) {
+		var prev : FreeCell = null;
+		var f = free;
+		var end = pos + nvect;
+		while( f != null ) {
+			if( f.pos == end ) {
+				f.pos -= nvect;
+				f.count += nvect;
+				if( prev != null && prev.pos + prev.count == f.pos ) {
+					prev.count += f.count;
+					prev.next = f.next;
+				}
+				return;
+			}
+			if( f.pos > end ) {
+				if( prev != null && prev.pos + prev.count == pos )
+					prev.count += nvect;
+				else {
+					var n = new FreeCell(pos, nvect, f);
+					if( prev == null ) free = n else prev.next = n;
+				}
+				return;
+			}
+			prev = f;
+			f = f.next;
+		}
+		if( nvect != 0 )
+			throw "assert";
+	}
+
+	function dispose() {
+		mem.driver.disposeVertex(vbuf);
+		vbuf = null;
+	}
+	
+	inline function isDisposed() {
+		return vbuf == null;
+	}
+
+}
+
+class MemoryManager {
+
+	static inline var MAX_MEMORY = 250 << 20; // MB
+	static inline var MAX_BUFFERS = 4096;
+
+	@:allow(h3d)
+	var driver : Driver;
+	var buffers : Array<BigBuffer>;
+	var idict : Map<Indexes,Bool>;
+	
+	var tdict : WeakMap<h3d.mat.Texture,Driver.Texture>;
+	var textures : Array<Driver.Texture>;
+	
+	public var indexes(default,null) : Indexes;
+	public var quadIndexes(default,null) : Indexes;
+	public var usedMemory(default,null) : Int;
+	public var bufferCount(default,null) : Int;
+	public var allocSize(default,null) : Int;
+
+	public function new(driver,allocSize) {
+		this.driver = driver;
+		this.allocSize = allocSize;
+
+		idict = new Map();
+		tdict = new WeakMap();
+		textures = new Array();
+		buffers = new Array();
+		
+		initIndexes();
+	}
+	
+	function initIndexes() {
+		var indices = new hxd.IndexBuffer();
+		for( i in 0...allocSize ) indices.push(i);
+		indexes = allocIndex(indices);
+
+		var indices = new hxd.IndexBuffer();
+		var p = 0;
+		for( i in 0...allocSize >> 2 ) {
+			var k = i << 2;
+			indices.push(k);
+			indices.push(k + 1);
+			indices.push(k + 2);
+			indices.push(k + 2);
+			indices.push(k + 1);
+			indices.push(k + 3);
+		}
+		quadIndexes = allocIndex(indices);
+	}
+
+	/**
+		Call user-defined garbage function that will cleanup some unused allocated objects.
+		Might be called several times if we need to allocate a lot of memory
+	**/
+	public dynamic function garbage() {
+	}
+
+	/**
+		Clean empty (unused) buffers
+	**/
+	public function cleanBuffers() {
+		for( i in 0...buffers.length ) {
+			var b = buffers[i], prev : BigBuffer = null;
+			while( b != null ) {
+				if( b.free.count == b.size ) {
+					b.dispose();
+					bufferCount--;
+					usedMemory -= b.size * b.stride * 4;
+					if( prev == null )
+						buffers[i] = b.next;
+					else
+						prev.next = b.next;
+				} else
+					prev = b;
+				b = b.next;
+			}
+		}
+	}
+
+	public function stats() {
+		var total = 0, free = 0, count = 0;
+		for( b in buffers ) {
+			var b = b;
+			while( b != null ) {
+				total += b.stride * b.size * 4;
+				var f = b.free;
+				while( f != null ) {
+					free += f.count * b.stride * 4;
+					f = f.next;
+				}
+				count++;
+				b = b.next;
+			}
+		}
+		freeTextures();
+		var tcount = 0, tmem = 0;
+		for( t in tdict.keys() ) {
+			tcount++;
+			tmem += t.width * t.height * 4;
+		}
+		return {
+			bufferCount : count,
+			freeMemory : free,
+			totalMemory : total,
+			textureCount : tcount,
+			textureMemory : tmem,
+		};
+	}
+
+	public function allocStats() : Array<{ file : String, line : Int, count : Int, tex : Bool, size : Int }> {
+		#if !debug
+		return [];
+		#else
+		var h = new Map();
+		var all = [];
+		for( buf in buffers ) {
+			var buf = buf;
+			while( buf != null ) {
+				var b = buf.allocHead;
+				while( b != null ) {
+					var key = b.allocPos.fileName + ":" + b.allocPos.lineNumber;
+					var inf = h.get(key);
+					if( inf == null ) {
+						inf = { file : b.allocPos.fileName, line : b.allocPos.lineNumber, count : 0, size : 0, tex : false };
+						h.set(key, inf);
+						all.push(inf);
+					}
+					inf.count++;
+					inf.size += b.nvert * b.b.stride * 4;
+					b = b.allocNext;
+				}
+				buf = buf.next;
+			}
+		}
+		for( t in tdict.keys() ) {
+			var key = "$"+t.allocPos.fileName + ":" + t.allocPos.lineNumber;
+			var inf = h.get(key);
+			if( inf == null ) {
+				inf = { file : t.allocPos.fileName, line : t.allocPos.lineNumber, count : 0, size : 0, tex : true };
+				h.set(key, inf);
+				all.push(inf);
+			}
+			inf.count++;
+			inf.size += t.width * t.height * 4;
+		}
+		all.sort(function(a, b) return a.size == b.size ? a.line - b.line : b.size - a.size);
+		return all;
+		#end
+	}
+	
+	function newTexture(fmt, w, h, cubic, target, mm, allocPos) {
+		var t = new h3d.mat.Texture(this, fmt, w, h, cubic, target, mm);
+		#if debug
+		t.allocPos = allocPos;
+		#end
+		initTexture(t);
+		return t;
+	}
+	
+	function initTexture( t : h3d.mat.Texture ) {
+		t.t = driver.allocTexture(t);
+		tdict.set(t, t.t);
+		textures.push(t.t);
+	}
+
+	@:allow(h3d.impl.Indexes.dispose)
+	function deleteIndexes( i : Indexes ) {
+		idict.remove(i);
+		driver.disposeIndexes(i.ibuf);
+		i.ibuf = null;
+		usedMemory -= i.count * 2;
+	}
+	
+	@:allow(h3d.mat.Texture.dispose)
+	function deleteTexture( t : h3d.mat.Texture ) {
+		textures.remove(t.t);
+		tdict.remove(t);
+		driver.disposeTexture(t.t);
+		t.t = null;
+	}
+
+	@:allow(h3d.mat.Texture.resize)
+	function resizeTexture( t : h3d.mat.Texture, width, height ) {
+		t.dispose();
+		t.width = width;
+		t.height = height;
+		initTexture(t);
+	}
+	
+	public function readAtfHeader( data : haxe.io.Bytes ) {
+		var cubic = (data.get(6) & 0x80) != 0;
+		var alpha = false, compress = false;
+		switch( data.get(6) & 0x7F ) {
+		case 0:
+		case 1: alpha = true;
+		case 2: compress = true;
+		case 3, 4: alpha = true; compress = true;
+		case f: throw "Invalid ATF format " + f;
+		}
+		var width = 1 << data.get(7);
+		var height = 1 << data.get(8);
+		var mips = data.get(9) - 1;
+		return {
+			width : width,
+			height : height,
+			cubic : cubic,
+			alpha : alpha,
+			compress : compress,
+			mips : mips,
+		};
+	}
+
+	public function allocCustomTexture( fmt : h3d.mat.Data.TextureFormat, width : Int, height : Int, mipLevels : Int = 0, cubic : Bool = false, target : Bool = false, ?allocPos : AllocPos ) {
+		freeTextures();
+		return newTexture(fmt, width, height, cubic, target, mipLevels, allocPos);
+	}
+	
+	public function allocTexture( width : Int, height : Int, ?mipMap = false, ?allocPos : AllocPos ) {
+		freeTextures();
+		var levels = 0;
+		if( mipMap ) {
+			while( width > (1 << levels) && height > (1 << levels) )
+				levels++;
+		}
+		return newTexture(Rgba, width, height, false, false, levels, allocPos);
+	}
+	
+	public function allocTargetTexture( width : Int, height : Int, ?allocPos : AllocPos ) {
+		freeTextures();
+		return newTexture(Rgba, width, height, false, true, 0, allocPos);
+	}
+
+	public function allocCubeTexture( size : Int, ?mipMap = false, ?allocPos : AllocPos ) {
+		freeTextures();
+		var levels = 0;
+		if( mipMap ) {
+			while( size > (1 << levels) )
+				levels++;
+		}
+		return newTexture(Rgba, size, size, true, false, levels, allocPos);
+	}
+
+	public function allocIndex( indices : hxd.IndexBuffer, pos = 0, count = -1 ) {
+		if( count < 0 ) count = indices.length;
+		var ibuf = driver.allocIndexes(count);
+		var idx = new Indexes(this, ibuf, count);
+		idx.upload(indices, 0, count);
+		idict.set(idx, true);
+		usedMemory += idx.count * 2;
+		return idx;
+	}
+
+	public function allocBytes( bytes : haxe.io.Bytes, stride : Int, align, ?allocPos : AllocPos ) {
+		var count = Std.int(bytes.length / (stride * 4));
+		var b = alloc(count, stride, align, allocPos);
+		b.uploadBytes(bytes, 0, count);
+		return b;
+	}
+
+	public function allocVector( v : hxd.FloatBuffer, stride, align, ?allocPos : AllocPos ) {
+		var nvert = Std.int(v.length / stride);
+		var b = alloc(nvert, stride, align, allocPos);
+		b.uploadVector(v, 0, nvert);
+		return b;
+	}
+	
+	/**
+		This will automatically free all textures which are no longer referenced / have been GC'ed.
+		This is called before each texture allocation as well.
+		Returns the number of textures freed that way.
+	 **/
+	public function freeTextures() {
+		var tall = new Map();
+		for( t in textures )
+			tall.set(t, true);
+		for( t in tdict )
+			tall.remove(t);
+		var count = 0;
+		for( t in tall.keys() ) {
+			driver.disposeTexture(t);
+			textures.remove(t);
+			count++;
+		}
+		return count;
+	}
+
+	/**
+		The amount of free buffers memory
+	 **/
+	function freeMemory() {
+		var size = 0;
+		for( b in buffers ) {
+			var b = b;
+			while( b != null ) {
+				var free = b.free;
+				while( free != null ) {
+					size += free.count * b.stride * 4;
+					free = free.next;
+				}
+				b = b.next;
+			}
+		}
+		return size;
+	}
+
+	/**
+		Allocate a vertex buffer.
+		Align represent the number of vertex that represent a single primitive : 3 for triangles, 4 for quads
+		You can use 0 to allocate your own buffer but in that case you can't use pre-allocated indexes/quadIndexes
+	 **/
+	public function alloc( nvect : Int, stride, align, ?allocPos : AllocPos ) {
+		var b = buffers[stride], free = null;
+		if( nvect == 0 && align == 0 )
+			align = 3;
+		while( b != null ) {
+			free = b.free;
+			while( free != null ) {
+				if( free.count >= nvect ) {
+					// align 0 must be on first index
+					if( align == 0 ) {
+						if( free.pos != 0 )
+							free = null;
+						break;
+					} else {
+						// we can't alloc into a smaller buffer because we might use preallocated indexes
+						if( b.size != allocSize ) {
+							free = null;
+							break;
+						}
+						var d = (align - (free.pos % align)) % align;
+						if( d == 0 )
+							break;
+							
+						// insert some padding
+						if( free.count >= nvect + d ) {
+							free.next = new FreeCell(free.pos + d, free.count - d, free.next);
+							free.count = d;
+							free = free.next;
+							break;
+						}
+					}
+					break;
+				}
+				free = free.next;
+			}
+			if( free != null ) break;
+			b = b.next;
+		}
+		// try splitting big groups
+		if( b == null && align > 0 ) {
+			var size = nvect;
+			while( size > 1000 ) {
+				b = buffers[stride];
+				size >>= 1;
+				size -= size % align;
+				while( b != null ) {
+					free = b.free;
+					// skip not aligned buffers
+					if( b.size != allocSize )
+						free = null;
+					while( free != null ) {
+						if( free.count >= size ) {
+							// check alignment
+							var d = (align - (free.pos % align)) % align;
+							if( d == 0 )
+								break;
+							// insert some padding
+							if( free.count >= size + d ) {
+								free.next = new FreeCell(free.pos + d, free.count - d, free.next);
+								free.count = d;
+								free = free.next;
+								break;
+							}
+						}
+						free = free.next;
+					}
+					if( free != null ) break;
+					b = b.next;
+				}
+				if( b != null ) break;
+			}
+		}
+		// buffer not found : allocate a new one
+		if( b == null ) {
+			var size;
+			if( align == 0 ) {
+				size = nvect;
+				if( size > 0xFFFF ) throw "Too many vertex to allocate "+size;
+			} else
+				size = allocSize; // group allocations together to minimize buffer count
+			var mem = size * stride * 4, v = null;
+			if( usedMemory + mem > MAX_MEMORY || bufferCount >= MAX_BUFFERS || (v = driver.allocVertex(size,stride)) == null ) {
+				var size = usedMemory - freeMemory();
+				garbage();
+				cleanBuffers();
+				if( usedMemory - freeMemory() == size ) {
+					if( bufferCount >= MAX_BUFFERS )
+						throw "Too many buffer";
+					throw "Memory full";
+				}
+				return alloc(nvect, stride, align, allocPos);
+			}
+			usedMemory += mem;
+			bufferCount++;
+			b = new BigBuffer(this, v, stride, size);
+			#if flash
+			untyped v.b = b;
+			#end
+			b.next = buffers[stride];
+			buffers[stride] = b;
+			free = b.free;
+		}
+		// always alloc multiples of 4 (prevent quad split)
+		var alloc = nvect > free.count ? free.count - (free.count%align) : nvect;
+		var fpos = free.pos;
+		free.pos += alloc;
+		free.count -= alloc;
+		var b = new Buffer(b, fpos, alloc);
+		nvect -= alloc;
+		#if debug
+		var head = b.b.allocHead;
+		b.allocPos = allocPos;
+		b.allocNext = head;
+		if( head != null ) head.allocPrev = b;
+		b.b.allocHead = b;
+		#end
+		if( nvect > 0 )
+			b.next = this.alloc(nvect, stride, align #if debug, allocPos #end);
+		return b;
+	}
+
+	public function onContextLost() {
+		indexes.dispose();
+		quadIndexes.dispose();
+		var tkeys = Lambda.array({ iterator : tdict.keys });
+		for( t in tkeys ) {
+			if( !tdict.exists(t) )
+				continue;
+			if( t.onContextLost == null )
+				t.dispose();
+			else {
+				textures.remove(t.t);
+				initTexture(t);
+				t.onContextLost();
+			}
+		}
+		for( b in buffers ) {
+			var b = b;
+			while( b != null ) {
+				b.dispose();
+				b = b.next;
+			}
+		}
+		for( i in idict.keys() )
+			i.dispose();
+		buffers = [];
+		bufferCount = 0;
+		usedMemory = 0;
+		initIndexes();
+	}
+
+	public function dispose() {
+		indexes.dispose();
+		indexes = null;
+		quadIndexes.dispose();
+		quadIndexes = null;
+		for( t in tdict.keys() )
+			t.dispose();
+		for( b in buffers ) {
+			var b = b;
+			while( b != null ) {
+				b.dispose();
+				b = b.next;
+			}
+		}
+		for( i in idict.keys() )
+			i.dispose();
+		buffers = [];
+		bufferCount = 0;
+		usedMemory = 0;
+	}
+
+}

+ 120 - 0
h3d/impl/NullDriver.hx

@@ -0,0 +1,120 @@
+package h3d.impl;
+import h3d.impl.Driver;
+
+class NullDriver extends Driver {
+
+	public var driver : Driver;
+	
+	public function new( driver ) {
+		this.driver = driver;
+	}
+	
+	override function isDisposed() {
+		return driver.isDisposed();
+	}
+	
+	override function dispose() {
+		driver.dispose();
+	}
+
+	override function reset() {
+		driver.reset();
+	}
+
+	override function present() {
+		driver.present();
+	}
+
+	override function clear( r : Float, g : Float, b : Float, a : Float ) {
+		driver.clear(r, g, b, a);
+	}
+	
+	override function getDriverName( details : Bool ) {
+		return "Null Driver - " + driver.getDriverName(details);
+	}
+	
+	override function init( onCreate : Bool -> Void, forceSoftware = false ) {
+		driver.init(onCreate, forceSoftware);
+	}
+
+	override function resize( width : Int, height : Int ) {
+		driver.resize(width, height);
+	}
+
+	override function selectMaterial( mbits : Int ) {
+		driver.selectMaterial(mbits);
+	}
+	
+	override function selectShader( shader : Shader ) : Bool {
+		return driver.selectShader(shader);
+	}
+	
+	override function getShaderInputNames() : Array<String> {
+		return driver.getShaderInputNames();
+	}
+	
+	override function isHardware() {
+		return driver.isHardware();
+	}
+	
+	override function setDebug( b : Bool ) {
+		driver.setDebug(b);
+	}
+	
+	override function allocTexture( t : h3d.mat.Texture ) : Texture {
+		return driver.allocTexture(t);
+	}
+
+	override function allocIndexes( count : Int ) : IndexBuffer {
+		return driver.allocIndexes(count);
+	}
+
+	override function allocVertex( count : Int, stride : Int ) : VertexBuffer {
+		return driver.allocVertex(count,stride);
+	}
+	
+	override function disposeTexture( t : Texture ) {
+		driver.disposeTexture(t);
+	}
+	
+	override function disposeIndexes( i : IndexBuffer ) {
+		driver.disposeIndexes(i);
+	}
+	
+	override function disposeVertex( v : VertexBuffer ) {
+		driver.disposeVertex(v);
+	}
+	
+	override function uploadIndexesBuffer( i : IndexBuffer, startIndice : Int, indiceCount : Int, buf : hxd.IndexBuffer, bufPos : Int ) {
+		driver.uploadIndexesBuffer(i, startIndice, indiceCount, buf, bufPos);
+	}
+
+	override function uploadIndexesBytes( i : IndexBuffer, startIndice : Int, indiceCount : Int, buf : haxe.io.Bytes , bufPos : Int ) {
+		driver.uploadIndexesBytes(i, startIndice, indiceCount, buf, bufPos);
+	}
+	
+	override function uploadVertexBuffer( v : VertexBuffer, startVertex : Int, vertexCount : Int, buf : hxd.FloatBuffer, bufPos : Int ) {
+		driver.uploadVertexBuffer(v, startVertex, vertexCount, buf, bufPos);
+	}
+
+	override function uploadVertexBytes( v : VertexBuffer, startVertex : Int, vertexCount : Int, buf : haxe.io.Bytes, bufPos : Int ) {
+		driver.uploadVertexBytes(v, startVertex, vertexCount, buf, bufPos);
+	}
+	
+	override function uploadTextureBitmap( t : h3d.mat.Texture, bmp : hxd.BitmapData, mipLevel : Int, side : Int ) {
+		driver.uploadTextureBitmap(t, bmp, mipLevel, side);
+	}
+
+	override function uploadTexturePixels( t : h3d.mat.Texture, pixels : hxd.Pixels, mipLevel : Int, side : Int ) {
+		driver.uploadTexturePixels(t, pixels, mipLevel, side);
+	}
+
+	/*
+	public function selectBuffer( buffer : VertexBuffer )
+	public function selectMultiBuffers( buffers : Array<Buffer.BufferOffset> )
+	public function draw( ibuf : IndexBuffer, startIndex : Int, ntriangles : Int )
+	public function setRenderZone( x : Int, y : Int, width : Int, height : Int )
+	public function setRenderTarget( tex : Null<Texture>, useDepth : Bool, clearColor : Int )
+	*/
+	
+}

+ 121 - 0
h3d/impl/Shader.hx

@@ -0,0 +1,121 @@
+package h3d.impl;
+#if macro
+import haxe.macro.Context;
+#end
+
+#if flash
+typedef Shader = hxsl.Shader;
+#elseif (js || cpp)
+
+enum ShaderType {
+	Float;
+	Vec2;
+	Vec3;
+	Vec4;
+	Mat2;
+	Mat3;
+	Mat4;
+	Tex2d;
+	TexCube;
+	Byte3;
+	Byte4;
+	Struct( field : String, t : ShaderType );
+	Index( index : Int, t : ShaderType );
+}
+
+typedef Uniform = { name : String, loc : #if js js.html.webgl.UniformLocation #else openfl.gl.GLUniformLocation #end, type : ShaderType, index : Int }
+
+class ShaderInstance {
+
+	public var program : #if js js.html.webgl.Program #else openfl.gl.GLProgram #end;
+	public var attribs : Array<{ name : String, type : ShaderType, etype : Int, offset : Int, index : Int, size : Int }>;
+	public var uniforms : Array<Uniform>;
+	public var stride : Int;
+	public function new() {
+	}
+
+}
+
+@:autoBuild(h3d.impl.Shader.ShaderMacros.buildGLShader())
+class Shader {
+	
+	var instance : ShaderInstance;
+	
+	public function new() {
+	}
+	
+	function customSetup( driver : h3d.impl.GlDriver ) {
+	}
+	
+	function getConstants( vertex : Bool ) {
+		return "";
+	}
+
+}
+
+#else
+
+class Shader implements Dynamic {
+	public function new() {
+	}
+}
+
+#end
+
+#if macro
+class ShaderMacros {
+	
+	public static function buildGLShader() {
+		var pos = Context.getLocalClass().get().pos;
+		var fields = Context.getBuildFields();
+		var hasVertex = false, hasFragment = false;
+		var r_uni = ~/uniform[ \t]+((lowp|mediump|highp)[ \t]+)?([A-Za-z0-9_]+)[ \t]+([A-Za-z0-9_]+)[ \t]*(\/\*([A-Za-z0-9_]+)\*\/)?/;
+		function addUniforms( code : String ) {
+			while( r_uni.match(code) ) {
+				var name = r_uni.matched(4);
+				var type = r_uni.matched(3);
+				var hint = r_uni.matched(6);
+				code = r_uni.matchedRight();
+				var t = switch( type ) {
+				case "float": macro : Float;
+				case "vec4", "vec3" if( hint == "byte4" ): macro : Int;
+				case "vec2", "vec3", "vec4": macro : h3d.Vector;
+				case "mat3", "mat4": macro : h3d.Matrix;
+				case "sampler2D", "samplerCube": macro : h3d.mat.Texture;
+				default:
+					// most likely a struct, handle it manually
+					if( type.charCodeAt(0) >= 'A'.code && type.charCodeAt(0) <= 'Z'.code )
+						continue;
+					throw "Unsupported type " + type;
+				}
+				if( code.charCodeAt(0) == '['.code )
+					t = macro : Array<$t>;
+				fields.push( {
+					name : name,
+					kind : FVar(t),
+					pos : pos,
+					access : [APublic],
+				});
+			}
+		}
+		for( f in fields )
+			switch( [f.name, f.kind] ) {
+			case ["VERTEX", FVar(_,{ expr : EConst(CString(code)) }) ]:
+				hasVertex = true;
+				addUniforms(code);
+				f.meta.push( { name : ":keep", params : [], pos : pos } );
+			case ["FRAGMENT", FVar(_,{ expr : EConst(CString(code)) })]:
+				hasFragment = true;
+				addUniforms(code);
+				f.meta.push( { name : ":keep", params : [], pos : pos } );
+			default:
+			}
+		if( !hasVertex )
+			haxe.macro.Context.error("Missing VERTEX shader", pos);
+		if( !hasFragment )
+			haxe.macro.Context.error("Missing FRAGMENT shader", pos);
+		return fields;
+	}
+	
+}
+#end

+ 88 - 0
h3d/impl/Shaders.hx

@@ -0,0 +1,88 @@
+package h3d.impl;
+
+class PointShader extends h3d.impl.Shader {
+
+#if flash
+	static var SRC = {
+		var input : {
+			pos : Float2,
+		};
+		var tuv : Float2;
+		function vertex( mproj : Matrix, delta : Float4, size : Float2 ) {
+			var p = delta * mproj;
+			p.xy += input.pos.xy * size * p.z;
+			out = p;
+			tuv = input.pos;
+		}
+		function fragment( color : Color ) {
+			kill( 1 - (tuv.x * tuv.x + tuv.y * tuv.y) );
+			out = color;
+		}
+	}
+#elseif (js || cpp)
+
+	static var VERTEX = "
+		attribute vec2 pos;
+		varying mediump tuv;
+		uniform mat4 mproj;
+		uniform vec4 delta;
+		uniform vec2 size;
+		
+		void main(void) {
+			vec4 p = mproj * delta;
+			p.xy += pos.xy * size * p.z;
+			gl_Position = p;
+			tuv = pos;
+		}
+	";
+	static var FRAGMENT = "
+		varying mediump tuv;
+		uniform vec4 color /*byte4*/;
+		
+		void main(void) {
+			if( 1 - dot(tuv, tuv) < 0 ) discard;
+			gl_FragColor = color;
+		}
+	";
+
+#end
+	
+}
+
+class LineShader extends h3d.impl.Shader {
+
+#if flash
+	static var SRC = {
+		var input : {
+			pos : Float2,
+		};
+
+		function vertex( mproj : Matrix, start : Float4, end : Float4 ) {
+			var spos = start * mproj;
+			var epos = end * mproj;
+			var delta = epos.xy  - spos.xy;
+			delta.xy *= 1 / sqrt(delta.x * delta.x + delta.y * delta.y);
+			
+			
+			var p = (epos - spos) * (input.pos.x + 1) * 0.5 + spos;
+			p.xy += delta.yx * input.pos.y * p.z / 400;
+			out = p;
+		}
+		function fragment( color : Color ) {
+			out = color;
+		}
+	}
+	
+#elseif (js || cpp)
+
+	public var mproj : Matrix;
+	public var start : Vector;
+	public var end : Vector;
+	public var color : Int;
+	
+	static var VERTEX = "TODO";
+	static var FRAGMENT = "TODO";
+	
+#end
+
+}

+ 475 - 0
h3d/impl/Stage3dDriver.hx

@@ -0,0 +1,475 @@
+package h3d.impl;
+import h3d.impl.Driver;
+
+#if flash
+
+@:allow(h3d.impl.Stage3dDriver)
+class VertexWrapper {
+	var vbuf : flash.display3D.VertexBuffer3D;
+	var stride : Int;
+	var written : Bool;
+	var b : MemoryManager.BigBuffer;
+	
+	function new(vbuf, stride) {
+		this.vbuf = vbuf;
+		this.stride = stride;
+	}
+	
+	function finalize( driver : Stage3dDriver ) {
+		if( written ) return;
+		written = true;
+		// fill all the free positions that were unwritten with zeroes (necessary for flash)
+		var f = b.free;
+		while( f != null ) {
+			if( f.count > 0 ) {
+				var mem : UInt = f.count * b.stride * 4;
+				if( driver.empty.length < mem ) driver.empty.length = mem;
+				driver.uploadVertexBytes(b.vbuf, f.pos, f.count, haxe.io.Bytes.ofData(driver.empty), 0);
+			}
+			f = f.next;
+		}
+	}
+
+}
+
+class Stage3dDriver extends Driver {
+	
+	var s3d : flash.display.Stage3D;
+	var ctx : flash.display3D.Context3D;
+	var onCreateCallback : Bool -> Void;
+	
+	var curMatBits : Int;
+	var curShader : hxsl.Shader.ShaderInstance;
+	var curBuffer : VertexBuffer;
+	var curMultiBuffer : Array<Int>;
+	var curAttributes : Int;
+	var curTextures : Array<h3d.mat.Texture>;
+	var curSamplerBits : Array<Int>;
+	var inTarget : Texture;
+	var antiAlias : Int;
+	var width : Int;
+	var height : Int;
+	var enableDraw : Bool;
+	var capture : { bmp : hxd.BitmapData, callb : Void -> Void };
+
+	@:allow(h3d.impl.VertexWrapper)
+	var empty : flash.utils.ByteArray;
+	
+	public function new() {
+		empty = new flash.utils.ByteArray();
+		s3d = flash.Lib.current.stage.stage3Ds[0];
+		curTextures = [];
+		curMultiBuffer = [];
+	}
+	
+	override function getDriverName(details:Bool) {
+		return ctx == null ? "None" : (details ? ctx.driverInfo : ctx.driverInfo.split(" ")[0]);
+	}
+	
+	override function reset() {
+		enableDraw = true;
+		curMatBits = -1;
+		curShader = null;
+		curBuffer = null;
+		curMultiBuffer[0] = -1;
+		for( i in 0...curAttributes )
+			ctx.setVertexBufferAt(i, null);
+		curAttributes = 0;
+		for( i in 0...curTextures.length )
+			ctx.setTextureAt(i, null);
+		curTextures = [];
+		curSamplerBits = [];
+	}
+	
+	override function init( onCreate, forceSoftware = false ) {
+		this.onCreateCallback = onCreate;
+		s3d.addEventListener(flash.events.Event.CONTEXT3D_CREATE, this.onCreate);
+		s3d.requestContext3D( forceSoftware ? "software" : "auto" );
+	}
+	
+	function onCreate(_) {
+		var old = ctx;
+		if( old != null ) {
+			if( old.driverInfo != "Disposed" ) throw "Duplicate onCreate()";
+			old.dispose();
+			hxsl.Shader.ShaderGlobals.disposeAll();
+			ctx = s3d.context3D;
+			onCreateCallback(true);
+		} else {
+			ctx = s3d.context3D;
+			onCreateCallback(false);
+		}
+	}
+	
+	override function isHardware() {
+		return ctx != null && ctx.driverInfo.toLowerCase().indexOf("software") == -1;
+	}
+	
+	override function resize(width, height) {
+		ctx.configureBackBuffer(width, height, antiAlias);
+		this.width = width;
+		this.height = height;
+	}
+	
+	override function clear(r, g, b, a) {
+		ctx.clear(r, g, b, a);
+	}
+	
+	override function setCapture( bmp : hxd.BitmapData, onCapture : Void -> Void ) {
+		capture = { bmp : bmp, callb : onCapture };
+	}
+	
+	override function dispose() {
+		s3d.removeEventListener(flash.events.Event.CONTEXT3D_CREATE, onCreate);
+		if( ctx != null ) ctx.dispose();
+		ctx = null;
+	}
+	
+	override function isDisposed() {
+		return ctx == null || ctx.driverInfo == "Disposed";
+	}
+	
+	override function present() {
+		if( capture != null ) {
+			ctx.drawToBitmapData(capture.bmp.toNative());
+			ctx.present();
+			var callb = capture.callb;
+			capture = null;
+			callb();
+			return;
+		}
+		ctx.present();
+	}
+	
+	override function disposeTexture( t : Texture ) {
+		t.dispose();
+	}
+	
+	override function allocVertex( count : Int, stride : Int ) : VertexBuffer {
+		var v;
+		try {
+			v = ctx.createVertexBuffer(count, stride);
+		} catch( e : flash.errors.Error ) {
+			// too many resources / out of memory
+			if( e.errorID == 3691 )
+				return null;
+			throw e;
+		}
+		return new VertexWrapper(v, stride);
+	}
+
+	override function allocIndexes( count : Int ) : IndexBuffer {
+		return ctx.createIndexBuffer(count);
+	}
+	
+	override function allocTexture( t : h3d.mat.Texture ) : Texture {
+		var fmt = switch( t.format ) {
+		case Rgba, Atf:
+			flash.display3D.Context3DTextureFormat.BGRA;
+		case AtfCompressed(alpha):
+			alpha ? flash.display3D.Context3DTextureFormat.COMPRESSED_ALPHA : flash.display3D.Context3DTextureFormat.COMPRESSED;
+		}
+		return if( t.isCubic )
+			ctx.createCubeTexture(t.width, fmt, t.isTarget, t.mipLevels);
+		else
+			ctx.createTexture(t.width, t.height, fmt, t.isTarget, t.mipLevels);
+	}
+
+	override function uploadTextureBitmap( t : h3d.mat.Texture, bmp : hxd.BitmapData, mipLevel : Int, side : Int ) {
+		if( t.isCubic ) {
+			var t = flash.Lib.as(t.t, flash.display3D.textures.CubeTexture);
+			t.uploadFromBitmapData(bmp.toNative(), side, mipLevel);
+		}
+		else {
+			var t = flash.Lib.as(t.t, flash.display3D.textures.Texture);
+			t.uploadFromBitmapData(bmp.toNative(), mipLevel);
+		}
+	}
+
+	override function uploadTexturePixels( t : h3d.mat.Texture, pixels : hxd.Pixels, mipLevel : Int, side : Int ) {
+		pixels.convert(BGRA);
+		var data = pixels.bytes.getData();
+		switch( t.format ) {
+		case Atf, AtfCompressed(_):
+			if( t.isCubic ) {
+				var t = flash.Lib.as(t.t, flash.display3D.textures.CubeTexture);
+				t.uploadCompressedTextureFromByteArray(data, 0);
+			}
+			else {
+				var t = flash.Lib.as(t.t,  flash.display3D.textures.Texture);
+				t.uploadCompressedTextureFromByteArray(data, 0);
+			}
+		default:
+			if( t.isCubic ) {
+				var t = flash.Lib.as(t.t, flash.display3D.textures.CubeTexture);
+				t.uploadFromByteArray(data, 0, side, mipLevel);
+			}
+			else {
+				var t = flash.Lib.as(t.t,  flash.display3D.textures.Texture);
+				t.uploadFromByteArray(data, 0, mipLevel);
+			}
+		}
+	}
+	
+	override function disposeVertex( v : VertexBuffer ) {
+		v.vbuf.dispose();
+		v.b = null;
+	}
+	
+	override function disposeIndexes( i : IndexBuffer ) {
+		i.dispose();
+	}
+	
+	override function setDebug( d : Bool ) {
+		if( ctx != null ) ctx.enableErrorChecking = d && isHardware();
+	}
+	
+	override function uploadVertexBuffer( v : VertexBuffer, startVertex : Int, vertexCount : Int, buf : hxd.FloatBuffer, bufPos : Int ) {
+		var data = buf.getNative();
+		v.vbuf.uploadFromVector( bufPos == 0 ? data : data.slice(bufPos, vertexCount * v.stride + bufPos), startVertex, vertexCount );
+	}
+
+	override function uploadVertexBytes( v : VertexBuffer, startVertex : Int, vertexCount : Int, bytes : haxe.io.Bytes, bufPos : Int ) {
+		v.vbuf.uploadFromByteArray( bytes.getData(), bufPos, startVertex, vertexCount );
+	}
+
+	override function uploadIndexesBuffer( i : IndexBuffer, startIndice : Int, indiceCount : Int, buf : hxd.IndexBuffer, bufPos : Int ) {
+		var data = buf.getNative();
+		i.uploadFromVector( bufPos == 0 ? data : data.slice(bufPos, indiceCount + bufPos), startIndice, indiceCount );
+	}
+
+	override function uploadIndexesBytes( i : IndexBuffer, startIndice : Int, indiceCount : Int, buf : haxe.io.Bytes, bufPos : Int ) {
+		i.uploadFromByteArray(buf.getData(), bufPos, startIndice, indiceCount );
+	}
+	
+	override function selectMaterial( mbits : Int ) {
+		var diff = curMatBits ^ mbits;
+		if( diff != 0 ) {
+			if( curMatBits < 0 || diff&3 != 0 )
+				ctx.setCulling(FACE[mbits&3]);
+			if( curMatBits < 0 || diff & (0xFF << 6) != 0 )
+				ctx.setBlendFactors(BLEND[(mbits>>6)&15], BLEND[(mbits>>10)&15]);
+			if( curMatBits < 0 || diff & (15 << 2) != 0 )
+				ctx.setDepthTest((mbits >> 2) & 1 == 1, COMPARE[(mbits>>3)&7]);
+			if( curMatBits < 0 || diff & (15 << 14) != 0 )
+				ctx.setColorMask((mbits >> 14) & 1 != 0, (mbits >> 14) & 2 != 0, (mbits >> 14) & 4 != 0, (mbits >> 14) & 8 != 0);
+			curMatBits = mbits;
+		}
+	}
+
+	override function selectShader( shader : Shader ) {
+		var shaderChanged = false;
+		var s = shader.getInstance();
+		if( s.program == null ) {
+			s.program = ctx.createProgram();
+			var vdata = s.vertexBytes.getData();
+			var fdata = s.fragmentBytes.getData();
+			vdata.endian = flash.utils.Endian.LITTLE_ENDIAN;
+			fdata.endian = flash.utils.Endian.LITTLE_ENDIAN;
+			s.program.upload(vdata, fdata);
+			curShader = null; // in case we had the same shader and it was disposed
+		}
+		if( s != curShader ) {
+			ctx.setProgram(s.program);
+			shaderChanged = true;
+			s.varsChanged = true;
+			// unbind extra textures
+			var tcount : Int = s.textures.length;
+			while( curTextures.length > tcount ) {
+				curTextures.pop();
+				ctx.setTextureAt(curTextures.length, null);
+			}
+			// force remapping of vertex buffer
+			curBuffer = null;
+			curMultiBuffer[0] = -1;
+			curShader = s;
+		}
+		if( s.varsChanged ) {
+			s.varsChanged = false;
+			ctx.setProgramConstantsFromVector(flash.display3D.Context3DProgramType.VERTEX, 0, s.vertexVars.toData());
+			ctx.setProgramConstantsFromVector(flash.display3D.Context3DProgramType.FRAGMENT, 0, s.fragmentVars.toData());
+			for( i in 0...s.textures.length ) {
+				var t = s.textures[i];
+				if( t == null || t.isDisposed() )
+					t = h2d.Tile.fromColor(0xFFFF00FF).getTexture();
+				var cur = curTextures[i];
+				if( t != cur ) {
+					ctx.setTextureAt(i, t.t);
+					curTextures[i] = t;
+				}
+				// if we have set one of the texture flag manually or if the shader does not configure the texture flags
+				if( !t.hasDefaultFlags() || !s.texHasConfig[s.textureMap[i]] ) {
+					if( cur == null || t.bits != curSamplerBits[i] ) {
+						ctx.setSamplerStateAt(i, WRAP[t.wrap.getIndex()], FILTER[t.filter.getIndex()], MIP[t.mipMap.getIndex()]);
+						curSamplerBits[i] = t.bits;
+					}
+				} else {
+					// the texture flags has been set by the shader, so we are in an unkown state
+					curSamplerBits[i] = -1;
+				}
+			}
+		}
+		return shaderChanged;
+	}
+	
+	override function selectBuffer( v : VertexBuffer ) {
+		if( v == curBuffer )
+			return;
+		curBuffer = v;
+		curMultiBuffer[0] = -1;
+		if( v.stride < curShader.stride )
+			throw "Buffer stride (" + v.stride + ") and shader stride (" + curShader.stride + ") mismatch";
+		if( !v.written )
+			v.finalize(this);
+		var pos = 0, offset = 0;
+		var bits = curShader.bufferFormat;
+		while( offset < curShader.stride ) {
+			var size = bits & 7;
+			ctx.setVertexBufferAt(pos++, v.vbuf, offset, FORMAT[size]);
+			offset += size == 0 ? 1 : size;
+			bits >>= 3;
+		}
+		for( i in pos...curAttributes )
+			ctx.setVertexBufferAt(i, null);
+		curAttributes = pos;
+	}
+	
+	override function getShaderInputNames() {
+		return curShader.bufferNames;
+	}
+	
+	override function selectMultiBuffers( buffers : Buffer.BufferOffset ) {
+		// select the multiple buffers elements
+		var changed = false;
+		var b = buffers;
+		var i = 0;
+		while( b != null || i < curAttributes ) {
+			if( b == null || b.id != curMultiBuffer[i] ) {
+				changed = true;
+				break;
+			}
+			b = b.next;
+			i++;
+		}
+		if( changed ) {
+			var pos = 0, offset = 0;
+			var bits = curShader.bufferFormat;
+			var b = buffers;
+			while( offset < curShader.stride ) {
+				var size = bits & 7;
+				if( b.b.next != null )
+					throw "Buffer is split";
+				if( !b.b.b.vbuf.written )
+					b.b.b.vbuf.finalize(this);
+				ctx.setVertexBufferAt(pos, b.b.b.vbuf.vbuf, b.offset, FORMAT[size]);
+				curMultiBuffer[pos] = b.id;
+				offset += size == 0 ? 1 : size;
+				bits >>= 3;
+				pos++;
+				b = b.next;
+			}
+			for( i in pos...curAttributes )
+				ctx.setVertexBufferAt(i, null);
+			curAttributes = pos;
+			curBuffer = null;
+		}
+	}
+	
+	override function draw( ibuf : IndexBuffer, startIndex : Int, ntriangles : Int ) {
+		if( enableDraw ) ctx.drawTriangles(ibuf, startIndex, ntriangles);
+	}
+
+	override function setRenderZone( x : Int, y : Int, width : Int, height : Int ) {
+		if( x == 0 && y == 0 && width < 0 && height < 0 ) {
+			enableDraw = true;
+			ctx.setScissorRectangle(null);
+		} else {
+			if( x < 0 ) {
+				width += x;
+				x = 0;
+			}
+			if( y < 0 ) {
+				height += y;
+				y = 0;
+			}
+			var tw = inTarget == null ? this.width : 9999;
+			var th = inTarget == null ? this.height : 9999;
+			if( x + width > tw ) width = tw - x;
+			if( y + height > th ) height = th - y;
+			enableDraw = width > 0 && height > 0;
+			if( enableDraw )
+				ctx.setScissorRectangle(new flash.geom.Rectangle(x, y, width, height));
+		}
+	}
+
+	override function setRenderTarget( tex : Null<Texture>, useDepth : Bool, clearColor : Int ) {
+		if( tex == null ) {
+			ctx.setRenderToBackBuffer();
+			inTarget = null;
+		} else {
+			if( inTarget != null )
+				throw "Calling setTarget() while already set";
+			ctx.setRenderToTexture(tex, useDepth);
+			inTarget = tex;
+			reset();
+			ctx.clear( ((clearColor>>16)&0xFF)/255 , ((clearColor>>8)&0xFF)/255, (clearColor&0xFF)/255, ((clearColor>>>24)&0xFF)/255);
+		}
+	}
+	
+	static var BLEND = [
+		flash.display3D.Context3DBlendFactor.ONE,
+		flash.display3D.Context3DBlendFactor.ZERO,
+		flash.display3D.Context3DBlendFactor.SOURCE_ALPHA,
+		flash.display3D.Context3DBlendFactor.SOURCE_COLOR,
+		flash.display3D.Context3DBlendFactor.DESTINATION_ALPHA,
+		flash.display3D.Context3DBlendFactor.DESTINATION_COLOR,
+		flash.display3D.Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA,
+		flash.display3D.Context3DBlendFactor.ONE_MINUS_SOURCE_COLOR,
+		flash.display3D.Context3DBlendFactor.ONE_MINUS_DESTINATION_ALPHA,
+		flash.display3D.Context3DBlendFactor.ONE_MINUS_DESTINATION_COLOR
+	];
+
+	static var FACE = [
+		flash.display3D.Context3DTriangleFace.NONE,
+		flash.display3D.Context3DTriangleFace.BACK,
+		flash.display3D.Context3DTriangleFace.FRONT,
+		flash.display3D.Context3DTriangleFace.FRONT_AND_BACK,
+	];
+
+	static var COMPARE = [
+		flash.display3D.Context3DCompareMode.ALWAYS,
+		flash.display3D.Context3DCompareMode.NEVER,
+		flash.display3D.Context3DCompareMode.EQUAL,
+		flash.display3D.Context3DCompareMode.NOT_EQUAL,
+		flash.display3D.Context3DCompareMode.GREATER,
+		flash.display3D.Context3DCompareMode.GREATER_EQUAL,
+		flash.display3D.Context3DCompareMode.LESS,
+		flash.display3D.Context3DCompareMode.LESS_EQUAL,
+	];
+
+	static var FORMAT = [
+		flash.display3D.Context3DVertexBufferFormat.BYTES_4,
+		flash.display3D.Context3DVertexBufferFormat.FLOAT_1,
+		flash.display3D.Context3DVertexBufferFormat.FLOAT_2,
+		flash.display3D.Context3DVertexBufferFormat.FLOAT_3,
+		flash.display3D.Context3DVertexBufferFormat.FLOAT_4,
+	];
+	
+	static var WRAP = [
+		flash.display3D.Context3DWrapMode.CLAMP,
+		flash.display3D.Context3DWrapMode.REPEAT,
+	];
+	
+	static var FILTER = [
+		flash.display3D.Context3DTextureFilter.NEAREST,
+		flash.display3D.Context3DTextureFilter.LINEAR,
+	];
+	
+	static var MIP = [
+		flash.display3D.Context3DMipFilter.MIPNONE,
+		flash.display3D.Context3DMipFilter.MIPNEAREST,
+		flash.display3D.Context3DMipFilter.MIPLINEAR,
+	];
+	
+}
+#end

+ 15 - 0
h3d/mat/Bitmap.hx

@@ -0,0 +1,15 @@
+package h3d.mat;
+
+class Bitmap {
+
+	public var bytes : haxe.io.Bytes;
+	public var width : Int;
+	public var height : Int;
+	
+	public function new(w,h,b) {
+		this.width = w;
+		this.height = h;
+		this.bytes = b;
+	}
+	
+}

+ 60 - 0
h3d/mat/Data.hx

@@ -0,0 +1,60 @@
+package h3d.mat;
+
+enum Face {
+	None;
+	Back;
+	Front;
+	Both;
+}
+
+enum Blend {
+	One;
+	Zero;
+	SrcAlpha;
+	SrcColor;
+	DstAlpha;
+	DstColor;
+	OneMinusSrcAlpha;
+	OneMinusSrcColor;
+	OneMinusDstAlpha;
+	OneMinusDstColor;
+	// only supported on WebGL
+	ConstantColor;
+	ConstantAlpha;
+	OneMinusConstantColor;
+	OneMinusConstantAlpha;
+	SrcAlphaSaturate;
+}
+
+enum Compare {
+	Always;
+	Never;
+	Equal;
+	NotEqual;
+	Greater;
+	GreaterEqual;
+	Less;
+	LessEqual;
+}
+
+enum MipMap {
+	None;
+	Nearest;
+	Linear;
+}
+
+enum Filter {
+	Nearest;
+	Linear;
+}
+
+enum Wrap {
+	Clamp;
+	Repeat;
+}
+
+enum TextureFormat {
+	Rgba;
+	Atf;
+	AtfCompressed( alpha : Bool );
+}

+ 93 - 0
h3d/mat/Material.hx

@@ -0,0 +1,93 @@
+package h3d.mat;
+import h3d.mat.Data;
+
+class Material {
+	
+	var bits : Int;
+	public var culling(default,set) : Face;
+	public var depthWrite(default,set) : Bool;
+	public var depthTest(default,set) : Compare;
+	public var blendSrc(default,set) : Blend;
+	public var blendDst(default,set) : Blend;
+	public var colorMask(default,set) : Int;
+	public var shader : h3d.impl.Shader;
+	public var renderPass : Int;
+	
+	public function new(shader) {
+		bits = 0;
+		renderPass = 0;
+		this.shader = shader;
+		this.culling = Face.Back;
+		this.depthWrite = true;
+		this.depthTest = Compare.Less;
+		this.blendSrc = Blend.One;
+		this.blendDst = Blend.Zero;
+		this.colorMask = 15;
+	}
+	
+	public function setup( ctx : h3d.scene.RenderContext ) {
+	}
+	
+	public function blend(src, dst) {
+		blendSrc = src;
+		blendDst = dst;
+	}
+	
+	public function clone( ?m : Material ) {
+		if( m == null ) m = new Material(null);
+		m.culling = culling;
+		m.depthWrite = depthWrite;
+		m.depthTest = depthTest;
+		m.blendSrc = blendSrc;
+		m.blendDst = blendDst;
+		m.colorMask = colorMask;
+		return m;
+	}
+	
+	public function depth( write, test ) {
+		this.depthWrite = write;
+		this.depthTest = test;
+	}
+	
+	public function setColorMask(r, g, b, a) {
+		this.colorMask = (r?1:0) | (g?2:0) | (b?4:0) | (a?8:0);
+	}
+
+	function set_culling(f) {
+		culling = f;
+		bits = (bits & ~(3 << 0)) | (Type.enumIndex(f) << 0);
+		return f;
+	}
+	
+	function set_depthWrite(b) {
+		depthWrite = b;
+		bits = (bits & ~(1 << 2)) | ((b ? 1 : 0) << 2);
+		return b;
+	}
+	
+	function set_depthTest(c) {
+		depthTest = c;
+		bits = (bits & ~(7 << 3)) | (Type.enumIndex(c) << 3);
+		return c;
+	}
+	
+	function set_blendSrc(b) {
+		blendSrc = b;
+		bits = (bits & ~(15 << 6)) | (Type.enumIndex(b) << 6);
+		return b;
+	}
+
+	function set_blendDst(b) {
+		blendDst = b;
+		bits = (bits & ~(15 << 10)) | (Type.enumIndex(b) << 10);
+		return b;
+	}
+	
+	function set_colorMask(m) {
+		m &= 15;
+		colorMask = m;
+		bits = (bits & ~(15 << 14)) | (m << 14);
+		return m;
+	}
+
+}

部分文件因文件數量過多而無法顯示