Browse Source

inspector rearchitecture

Nicolas Cannasse 9 years ago
parent
commit
a393c90327

+ 1 - 0
.gitignore

@@ -5,3 +5,4 @@
 bin
 bin
 .tmp
 .tmp
 /hxd/net/inspect.min.css
 /hxd/net/inspect.min.css
+/hxd/inspect/inspect.min.css

+ 67 - 0
hxd/inspect/Node.hx

@@ -0,0 +1,67 @@
+package hxd.inspect;
+
+class Node {
+
+	public var name(default,set) : String;
+	public var parent(default, set) : Node;
+	public var props : Void -> Array<Property>;
+	public var j : cdb.jq.JQuery;
+	var childs : Array<Node>;
+
+	public function new( name, ?parent ) {
+		childs = [];
+		initContent();
+		this.name = name;
+		this.parent = parent;
+	}
+
+	function getJRoot() {
+		return @:privateAccess SceneInspector.current.jroot;
+	}
+
+	function initContent() {
+	}
+
+	function removeChild(n:Node) {
+		childs.remove(n);
+	}
+
+	function addChild(n:Node) {
+		childs.push(n);
+	}
+
+	function set_parent(p:Node) {
+		if( parent != null )
+			parent.removeChild(this);
+		if( p != null )
+			p.addChild(this);
+		return parent = p;
+	}
+
+	public function getPathName() {
+		return name;
+	}
+
+	public function getFullPath() {
+		var n = getPathName();
+		if( parent == null )
+			return n;
+		return parent.getFullPath() + "." + n;
+	}
+
+	function dispose() {
+		if( j != null ) j.dispose();
+	}
+
+	public function remove() {
+		parent = null;
+		dispose();
+		for( c in childs )
+			c.remove();
+	}
+
+	function set_name(v) {
+		return name = v;
+	}
+
+}

+ 67 - 0
hxd/inspect/Panel.hx

@@ -0,0 +1,67 @@
+package hxd.inspect;
+
+class Panel extends Node {
+
+	var inspect : SceneInspector;
+	public var visible(default, null) : Bool;
+	public var caption(default, set) : String;
+	public var content : cdb.jq.JQuery;
+
+	public function new( name, caption, ?panelGroup ) {
+		super(name, panelGroup);
+		this.caption = caption;
+		@:privateAccess {
+			inspect = SceneInspector.current;
+			inspect.panels.push(this);
+			inspect.rootNodes.push(this);
+		}
+		if( name == null ) onClose = remove;
+	}
+
+	public function dock( to : cdb.jq.JQuery, align : cdb.jq.Message.DockDirection, ?size : Float ) {
+		j.dock(to.get(), align, size);
+		visible = true;
+	}
+
+	function set_caption(c) {
+		j.attr("caption", c);
+		return caption = c;
+	}
+
+	override function initContent() {
+		j = getJRoot().query('<div>');
+		j.addClass("panel");
+	}
+
+	override function addChild(n:Node)	{
+		super.addChild(n);
+		n.j.appendTo(content);
+	}
+
+	override function removeChild(n:Node) {
+		super.removeChild(n);
+		n.j.detach();
+	}
+
+	override function set_parent(p:Node) {
+		// no physical attachment
+		if( parent != null )
+			parent.childs.remove(this);
+		if( p != null )
+			p.childs.push(this);
+		return parent = p;
+	}
+
+	public dynamic function onClose() {
+		visible = false;
+	}
+
+	public function show() {
+		trace("TODO");
+		visible = true;
+	}
+
+	public function sync() {
+	}
+
+}

+ 5 - 15
hxd/net/PropInspector.hx → hxd/inspect/PropInspector.hx

@@ -1,6 +1,6 @@
-package hxd.net;
+package hxd.inspect;
 import cdb.jq.JQuery;
 import cdb.jq.JQuery;
-import hxd.net.Property;
+import hxd.inspect.Property;
 
 
 private typedef History = { path : String, oldV : Dynamic, newV : Dynamic };
 private typedef History = { path : String, oldV : Dynamic, newV : Dynamic };
 
 
@@ -194,16 +194,6 @@ class PropInspector extends cdb.jq.Client {
 		return t;
 		return t;
 	}
 	}
 
 
-	public function createPanel( name : String, ?dock : cdb.jq.Message.DockDirection, ?size) {
-		var panel = J('<div>');
-		panel.addClass("panel");
-		panel.attr("caption", ""+name);
-		panel.appendTo(j);
-		if( dock == null ) dock = Fill;
-		panel.dock(root, dock, size);
-		return panel;
-	}
-
 	public function sameValue( v1 : Dynamic, v2 : Dynamic ) {
 	public function sameValue( v1 : Dynamic, v2 : Dynamic ) {
 		if( v1 == v2 ) return true;
 		if( v1 == v2 ) return true;
 		if( Std.is(v1, Array) && Std.is(v2, Array) ) {
 		if( Std.is(v1, Array) && Std.is(v2, Array) ) {
@@ -510,14 +500,14 @@ class PropInspector extends cdb.jq.Client {
 					else {
 					else {
 						jprop.html(StringTools.htmlEscape("" + t) + " <button>View</button>");
 						jprop.html(StringTools.htmlEscape("" + t) + " <button>View</button>");
 						jprop.find("button").click(function(_) {
 						jprop.find("button").click(function(_) {
-							var p = createPanel("" + t);
-							p.html("Loading...");
+							var p = new Panel(null, "" + t);
+							p.j.html("Loading...");
 							haxe.Timer.delay(function() {
 							haxe.Timer.delay(function() {
 								var bmp = t.capturePixels();
 								var bmp = t.capturePixels();
 								var png = bmp.toPNG();
 								var png = bmp.toPNG();
 								bmp.dispose();
 								bmp.dispose();
 								var pngBase64 = new haxe.crypto.BaseCode(haxe.io.Bytes.ofString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")).encodeBytes(png).toString();
 								var pngBase64 = new haxe.crypto.BaseCode(haxe.io.Bytes.ofString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")).encodeBytes(png).toString();
-								p.html('<img src="data:image/png;base64,$pngBase64" style="background:#696969"/>');
+								p.j.html('<img src="data:image/png;base64,$pngBase64" style="background:#696969"/>');
 							},0);
 							},0);
 						});
 						});
 					}
 					}

+ 14 - 14
hxd/net/Property.hx → hxd/inspect/Property.hx

@@ -1,15 +1,15 @@
-package hxd.net;
-
-enum Property {
-	PBool( name : String, get : Void -> Bool, set : Bool -> Void );
-	PInt( name : String, get : Void -> Int, set : Int -> Void );
-	PFloat( name : String, get : Void -> Float, set : Float -> Void );
-	PString( name : String, get : Void -> String, set : String -> Void );
-	PEnum( name : String, e : Enum<Dynamic>, get : Void -> Dynamic, set : Dynamic -> Void );
-	PColor( name : String, hasAlpha : Bool, get : Void -> h3d.Vector, set : h3d.Vector -> Void );
-	PGroup( name : String, props : Array<Property> );
-	PTexture( name : String, get : Void -> h3d.mat.Texture, set : h3d.mat.Texture -> Void );
-	PFloats( name : String, get : Void -> Array<Float>, set : Array<Float> -> Void );
-	PPopup( p : Property, menu : Array<String>, click : cdb.jq.JQuery -> Int -> Void );
-	PCustom( name : String, content : Void -> cdb.jq.JQuery, ?set : Dynamic -> Void );
+package hxd.inspect;
+
+enum Property {
+	PBool( name : String, get : Void -> Bool, set : Bool -> Void );
+	PInt( name : String, get : Void -> Int, set : Int -> Void );
+	PFloat( name : String, get : Void -> Float, set : Float -> Void );
+	PString( name : String, get : Void -> String, set : String -> Void );
+	PEnum( name : String, e : Enum<Dynamic>, get : Void -> Dynamic, set : Dynamic -> Void );
+	PColor( name : String, hasAlpha : Bool, get : Void -> h3d.Vector, set : h3d.Vector -> Void );
+	PGroup( name : String, props : Array<Property> );
+	PTexture( name : String, get : Void -> h3d.mat.Texture, set : h3d.mat.Texture -> Void );
+	PFloats( name : String, get : Void -> Array<Float>, set : Array<Float> -> Void );
+	PPopup( p : Property, menu : Array<String>, click : cdb.jq.JQuery -> Int -> Void );
+	PCustom( name : String, content : Void -> cdb.jq.JQuery, ?set : Dynamic -> Void );
 }
 }

+ 312 - 0
hxd/inspect/SceneInspector.hx

@@ -0,0 +1,312 @@
+package hxd.inspect;
+import cdb.jq.JQuery;
+import hxd.inspect.Property;
+
+private class DrawEvent implements h3d.IDrawable {
+	var i : SceneInspector;
+	public function new(i) {
+		this.i = i;
+	}
+	public function render( engine : h3d.Engine ) {
+		i.sync();
+	}
+}
+
+class Tool {
+	public var name(default,set) : String;
+	public var icon(default,set) : String;
+	public var title(default,set) : String;
+	public var click : Void -> Void;
+	public var j : JQuery;
+	public var jicon : JQuery;
+	public var active(get, set) : Bool;
+	public function new() {
+	}
+	function get_active() {
+		return j.hasClass("active");
+	}
+	function set_active(v) {
+		j.toggleClass("active", v);
+		return v;
+	}
+	function set_name(v) {
+		jicon.attr("alt", v);
+		return name = v;
+	}
+	function set_icon(v) {
+		jicon.attr("class", "fa fa-"+v);
+		return icon = v;
+	}
+	function set_title(v) {
+		j.attr("title", v);
+		return title = v;
+	}
+}
+
+class SceneInspector {
+
+	static var CSS = hxd.res.Embed.getFileContent("hxd/inspect/inspect.css");
+	static var current : SceneInspector;
+
+	public var scene(default, set) : h3d.scene.Scene;
+	public var connected(get, never): Bool;
+
+	var inspect : PropInspector;
+	var jroot : JQuery;
+	var event : DrawEvent;
+	var oldLog : Dynamic -> haxe.PosInfos -> Void;
+	var savedFile : String;
+	var oldLoop : Void -> Void;
+	var state : Map<String,{ original : Dynamic, current : Dynamic }>;
+
+	var panels : Array<Panel>;
+	var rootNodes : Array<Node>;
+
+	public var scenePanel : ScenePanel;
+	var propsPanel : Panel;
+	var logPanel : Panel;
+	var statsPanel : Panel;
+
+	public function new( scene, ?host, ?port ) {
+
+		current = this;
+
+		event = new DrawEvent(this);
+		savedFile = "sceneProps.js";
+		state = new Map();
+		oldLog = haxe.Log.trace;
+		//haxe.Log.trace = onTrace;
+		inspect = new PropInspector(host, port);
+		inspect.resolveProps = resolveProps;
+		inspect.onChange = onChange;
+		inspect.handleKey = onKey;
+		this.scene = scene;
+		panels = [];
+		rootNodes = [];
+
+		init();
+
+		scenePanel.addNode("Renderer", "object-group", scenePanel.getRendererProps);
+
+		addTool("Load...", "download", load, "Load settings");
+		addTool("Save...", "save", save, "Save settings");
+		addTool("Undo", "undo", inspect.undo, "Undo");
+		addTool("Repeat", "repeat", inspect.redo, "Redo");
+		var pause : Tool = null;
+		pause = addTool("Pause", "pause", function() {
+			if( oldLoop != null ) {
+				hxd.System.setLoop(oldLoop);
+				oldLoop = null;
+			} else {
+				oldLoop = hxd.System.getCurrentLoop();
+				hxd.System.setLoop(pauseLoop);
+			}
+			pause.active = oldLoop != null;
+		}, "Pause scene");
+		addTool("Statistics", "bar-chart", function() {
+			if( statsPanel == null ) {
+				statsPanel = new StatsPanel();
+				statsPanel.dock(jroot, Right, 0.35);
+			}
+			statsPanel.show();
+		}, "Open statistics");
+	}
+
+	public function dispose() {
+		if( current == this ) current = null;
+		inspect.dispose();
+		scene = null;
+		if( oldLoop != null ) {
+			hxd.System.setLoop(oldLoop);
+			oldLoop = null;
+		}
+	}
+
+	inline function get_connected() {
+		return inspect.connected;
+	}
+
+	public inline function J( ?elt : cdb.jq.Dom, ?query : String ) {
+		return inspect.J(elt,query);
+	}
+
+	public function addTool( name : String, icon : String, click : Void -> Void, ?title : String = "" ) {
+		var t = new Tool();
+		t.j = J("<li>");
+		t.jicon = J("<i>").appendTo(t.j);
+		t.j.click(function(_) t.click());
+		t.name = name;
+		t.icon = icon;
+		t.title = title;
+		t.click = click;
+		t.j.appendTo(jroot.find("#toolbar"));
+		return t;
+	}
+
+	function onKey( e : cdb.jq.Event ) {
+		switch( e.keyCode ) {
+		case 'S'.code if( e.ctrlKey ):
+			save();
+		case hxd.Key.F1:
+			load();
+		default:
+		}
+	}
+
+	function onTrace( v : Dynamic, ?pos : haxe.PosInfos ) {
+		oldLog(v, pos);
+		if( inspect.connected ) {
+			var vstr = null;
+			if( pos.customParams != null ) {
+				pos.customParams.unshift(v);
+				vstr = [for( v in pos.customParams ) Std.string(v)].join(",");
+			} else
+				vstr = Std.string(v);
+			J("<pre>").addClass("line").text(pos.fileName+"(" + pos.lineNumber + ") : " + vstr).appendTo(J("#log"));
+		}
+	}
+
+	function set_scene(s:h3d.scene.Scene) {
+		if( scene != null )
+			scene.removePass(event);
+		if( s != null )
+			s.addPass(event);
+		return scene = s;
+	}
+
+	function pauseLoop() {
+		scene.setElapsedTime(0);
+		h3d.Engine.getCurrent().render(scene);
+	}
+
+	function init() {
+		jroot = J(inspect.getRoot());
+		jroot.html('
+			<style>$CSS</style>
+			<ul id="toolbar" class="toolbar">
+			</ul>
+		');
+		jroot.attr("title", "Inspect");
+
+		scenePanel = new ScenePanel("s3d",scene);
+		propsPanel = new Panel("props","Properties");
+		logPanel = new Panel("log", "Log");
+
+		scenePanel.dock(jroot, Left, 0.2);
+		logPanel.dock(jroot, Down, 0.3);
+		propsPanel.dock(scenePanel.j, Down, 0.5);
+	}
+
+	function load() {
+		try {
+		hxd.File.browse(function(b) {
+			savedFile = b.fileName;
+			b.load(function(bytes) {
+
+				// reset to default
+				for( s in state.keys() )
+					inspect.setPathPropValue(s, state.get(s).original);
+				state = new Map();
+
+				var o : Dynamic = haxe.Json.parse(bytes.toString());
+				function browseRec( path : Array<String>, v : Dynamic ) {
+					switch( Type.typeof(v) ) {
+					case TNull, TInt, TFloat, TBool, TClass(_):
+						var path = path.join(".");
+						state.set(path, { original : null, current : v });
+					case TUnknown, TFunction, TEnum(_):
+						throw "Invalid value " + v;
+					case TObject:
+						for( f in Reflect.fields(v) ) {
+							var fv = Reflect.field(v, f);
+							path.push(f);
+							browseRec(path, fv);
+							path.pop();
+						}
+					}
+				}
+				browseRec([], o);
+				for( s in state.keys() )
+					inspect.setPathPropValue(s, state.get(s).current);
+			});
+
+		},{ defaultPath : savedFile, fileTypes : [ { name:"Scene Props", extensions:["js"] } ] } );
+		} catch( e : Dynamic ) {
+			// already open
+		}
+	}
+
+	function save() {
+		var o : Dynamic = { };
+		for( s in state.keys() ) {
+			var path = s.split(".");
+			var o = o;
+			while( path.length > 1 ) {
+				var name = path.shift();
+				var s = Reflect.field(o, name);
+				if( s == null ) {
+					s = { };
+					Reflect.setField(o, name, s);
+				}
+				o = s;
+			}
+			Reflect.setField(o, path[0], state.get(s).current);
+		}
+		var js = haxe.Json.stringify(o, null, "\t");
+		try {
+			hxd.File.saveAs(haxe.io.Bytes.ofString(js), { defaultPath : savedFile, saveFileName : function(name) savedFile = name } );
+		} catch( e : Dynamic ) {
+			// already open
+		}
+	}
+
+	public function sync() {
+		if( scene == null || !inspect.connected ) return;
+		for( p in panels )
+			p.sync();
+	}
+
+	function resolveProps( path : Array<String> ) {
+		var cur = null;
+		var nodes = rootNodes;
+		while( true ) {
+			var k = path.shift();
+			if( k == null ) break;
+			var found = false;
+			for( n in nodes ) {
+				if( n.getPathName() == k ) {
+					found = true;
+					cur = n;
+					nodes = @:privateAccess n.childs;
+					break;
+				}
+			}
+			if( !found ) {
+				path.unshift(k);
+				break;
+			}
+		}
+		return cur == null || cur.props == null ? null : cur.props();
+	}
+
+	function onChange( path : String, oldV : Dynamic, newV : Dynamic ) {
+		var s = state.get(path);
+		if( s == null )
+			state.set(path, { original : oldV, current : newV } );
+		else {
+			if( inspect.sameValue(s.original,newV) )
+				state.remove(path);
+			else
+				s.current = newV;
+		}
+	}
+
+	function fillProps( n : TreeNode ) {
+		var t = inspect.makeProps(n.getFullPath(), n.props());
+		propsPanel.j.text("");
+		propsPanel.parent = n;
+		t.appendTo(propsPanel.j);
+		propsPanel.show();
+	}
+
+}

+ 406 - 0
hxd/inspect/ScenePanel.hx

@@ -0,0 +1,406 @@
+package hxd.inspect;
+import hxd.inspect.Property;
+
+private class SceneObject extends TreeNode {
+
+	public var o : h3d.scene.Object;
+	public var visible = true;
+	public var culled = false;
+
+	public function new(o, p) {
+		this.o = o;
+		super(o.name != null ? o.name : o.toString(), p);
+	}
+
+	function objectName( o : h3d.scene.Object ) {
+		var name = o.name != null ? o.name : o.toString();
+		return name.split(".").join("_");
+	}
+
+	override function getPathName() {
+		var name = objectName(o);
+		if( o.parent == null )
+			return name;
+		var idx = Lambda.indexOf(@:privateAccess o.parent.childs, o);
+		var count = 0;
+		for( i in 0...idx )
+			if( objectName(o) == name )
+				count++;
+		if( count > 0 )
+			name += "@" + count;
+		return name;
+	}
+}
+
+class ScenePanel extends Panel {
+
+	var scene : h3d.scene.Scene;
+	var sceneObjects : Array<SceneObject>;
+	var scenePosition = 0;
+
+	public function new(name, scene) {
+		super(name, "Scene");
+		sceneObjects = [];
+		this.scene = scene;
+	}
+
+	override function initContent() {
+		super.initContent();
+		j.addClass("scene");
+		j.html('
+			<ul class="buttons">
+				<li class="bt_hide" title="Show/Hide invisible objects">
+					<i class="fa fa-eye" />
+				</li>
+				<li class="bt_highlight" title="[TODO] Auto highlight in scene selected object">
+					<i class="fa fa-cube" />
+				</li>
+			</ul>
+			<div class="scrollable">
+				<ul class="elt root">
+				</ul>
+			</div>
+		');
+		content = j.find(".root");
+
+		var bt = j.find(".bt_hide");
+		bt.addClass("active");
+		bt.click(function(_) {
+			bt.toggleClass("active");
+			content.toggleClass("masked");
+		});
+
+		var bt = j.find(".bt_highlight");
+		bt.click(function(_) {
+			bt.toggleClass("active");
+			// TODO
+		});
+	}
+
+	public function addNode( name : String, icon : String, ?getProps : Void -> Array<Property>, ?parent : TreeNode ) {
+		var n = new TreeNode(name, parent == null ? this : parent);
+		n.icon = icon;
+		n.props = getProps;
+		if( getProps != null )
+			n.onSelect = function() @:privateAccess SceneInspector.current.fillProps(n);
+		return n;
+	}
+
+	function getObjectIcon( o : h3d.scene.Object) {
+		if( Std.is(o, h3d.scene.Skin) )
+			return "child";
+		if( Std.is(o, h3d.scene.Mesh) )
+			return "cube";
+		if( Std.is(o, h3d.scene.CustomObject) )
+			return "globe";
+		if( Std.is(o, h3d.scene.Scene) )
+			return "picture-o";
+		if( Std.is(o, h3d.scene.Light) )
+			return "lightbulb-o";
+		return null;
+	}
+
+	override function sync() {
+		scenePosition = 0;
+		syncRec(scene, this);
+		while( sceneObjects.length > scenePosition )
+			sceneObjects.pop().remove();
+	}
+
+	@:access(hxd.inspect.TreeNode)
+	function syncRec( o : h3d.scene.Object, p : Node ) {
+		var so = sceneObjects[scenePosition];
+		if( so != null && so.o != o ) {
+			for( i in scenePosition + 1...sceneObjects.length )
+				if( sceneObjects[i].o == o ) {
+					var tmp = sceneObjects[i];
+					sceneObjects[i] = so;
+					sceneObjects[scenePosition] = tmp;
+					so = tmp;
+					break;
+				}
+		}
+		if( so == null || so.o != o ) {
+			so = new SceneObject(o, p);
+			sceneObjects.insert(scenePosition, so);
+			var icon = getObjectIcon(o);
+			so.icon = icon == null ? "circle-o" : getObjectIcon(o);
+			so.props = function() return getObjectProps(o);
+			so.onSelect = function() @:privateAccess SceneInspector.current.fillProps(so);
+		}
+
+		if( so.parent != p ) so.parent = p;
+
+		if( o.visible != so.visible ) {
+			so.visible = o.visible;
+			so.j.toggleClass("hidden");
+		}
+		@:privateAccess if( o.culled != so.culled ) {
+			so.culled = o.culled;
+			so.j.toggleClass("culled");
+		}
+
+		scenePosition++;
+		if( o.numChildren > 0 ) {
+			for( c in o )
+				syncRec(c, so);
+		} else if( so.jchild != null ) {
+			so.jchild.remove();
+			so.jchild = null;
+			for( o in so.childs )
+				o.remove();
+		}
+	}
+
+
+	function getShaderProps( s : hxsl.Shader ) {
+		var props = [];
+		var data = @:privateAccess s.shader;
+		var vars = data.data.vars.copy();
+		vars.sort(function(v1, v2) return Reflect.compare(v1.name, v2.name));
+		for( v in vars ) {
+			switch( v.kind ) {
+			case Param:
+				var name = v.name+"__";
+				function set(val:Dynamic) {
+					Reflect.setField(s, name, val);
+					if( hxsl.Ast.Tools.isConst(v) )
+						@:privateAccess s.constModified = true;
+				}
+				switch( v.type ) {
+				case TBool:
+					props.push(PBool(v.name, function() return Reflect.field(s,name), set ));
+				case TInt:
+					props.push(PInt(v.name, function() return Reflect.field(s,name), set ));
+				case TFloat:
+					props.push(PFloat(v.name, function() return Reflect.field(s, name), set));
+				case TVec(size = (3 | 4), VFloat) if( v.name.toLowerCase().indexOf("color") >= 0 ):
+					props.push(PColor(v.name, size == 4, function() return Reflect.field(s, name), set));
+				case TSampler2D, TSamplerCube:
+					props.push(PTexture(v.name, function() return Reflect.field(s, name), set));
+				case TVec(size, VFloat):
+					props.push(PFloats(v.name, function() {
+						var v : h3d.Vector = Reflect.field(s, name);
+						var vl = [v.x, v.y];
+						if( size > 2 ) vl.push(v.z);
+						if( size > 3 ) vl.push(v.w);
+						return vl;
+					}, function(vl) {
+						set(new h3d.Vector(vl[0], vl[1], vl[2], vl[3]));
+					}));
+				case TArray(_):
+					props.push(PString(v.name, function() {
+						var a : Array<Dynamic> = Reflect.field(s, name);
+						return a == null ? "NULL" : "(" + a.length + " elements)";
+					}, function(val) {}));
+				default:
+					props.push(PString(v.name, function() return ""+Reflect.field(s,name), function(val) { } ));
+				}
+			default:
+			}
+		}
+
+		var name = data.data.name;
+		if( StringTools.startsWith(name, "h3d.shader.") )
+			name = name.substr(11);
+		name = name.split(".").join(" "); // no dot in prop name !
+
+		return PGroup("shader "+name, props);
+	}
+
+	function colorize( code : String ) {
+		code = code.split("\t").join("    ");
+		code = StringTools.htmlEscape(code);
+		code = ~/\b((var)|(function)|(if)|(else)|(for)|(while))\b/g.replace(code, "<span class='kwd'>$1</span>");
+		code = ~/(@[A-Za-z]+)/g.replace(code, "<span class='meta'>$1</span>");
+		return code;
+	}
+
+	function getMaterialProps( mat : h3d.mat.Material ) {
+		var props = [];
+		props.push(PString("name", function() return mat.name == null ? "" : mat.name, function(n) mat.name = n == "" ? null : n));
+		for( pass in mat.getPasses() ) {
+			var pl = [
+				PBool("Lights", function() return pass.enableLights, function(v) pass.enableLights = v),
+				PEnum("Cull", h3d.mat.Data.Face, function() return pass.culling, function(v) pass.culling = v),
+				PEnum("BlendSrc", h3d.mat.Data.Blend, function() return pass.blendSrc, function(v) pass.blendSrc = pass.blendAlphaSrc = v),
+				PEnum("BlendDst", h3d.mat.Data.Blend, function() return pass.blendDst, function(v) pass.blendDst = pass.blendAlphaDst = v),
+				PBool("DepthWrite", function() return pass.depthWrite, function(b) pass.depthWrite = b),
+				PEnum("DepthTest", h3d.mat.Data.Compare, function() return pass.depthTest, function(v) pass.depthTest = v)
+			];
+
+			var shaders = [for( s in pass.getShaders() ) s];
+			shaders.reverse();
+			for( index in 0...shaders.length ) {
+				var s = shaders[index];
+				var p = getShaderProps(s);
+				p = PPopup(p, ["Toggle", "View Source"], function(j, i) {
+					switch( i ) {
+					case 0:
+						if( index == 0 ) return; // Don't allow toggle base shader
+						if( !pass.removeShader(s) )
+							pass.addShaderAt(s, shaders.length - (index + 1));
+						j.toggleClass("disable");
+					case 1:
+						var shader = @:privateAccess s.shader;
+						var p = new Panel(null, shader.data.name+" shader");
+						var toString = hxsl.Printer.shaderToString;
+						var code = toString(shader.data);
+						p.j.html("<pre class='code'>"+colorize(code)+"</pre>");
+					}
+
+				});
+				pl.push(p);
+			}
+
+			var p = PGroup("pass " + pass.name, pl);
+			p = PPopup(p, ["Toggle", "View Shader"], function(j, i) {
+				switch( i ) {
+				case 0:
+					if( !mat.removePass(pass) )
+						mat.addPass(pass);
+					j.toggleClass("disable");
+				case 1:
+					var p = new Panel(null,pass.name+" shader");
+					var shader = scene.renderer.compileShader(pass);
+					var toString = hxsl.Printer.shaderToString;
+					var code = toString(shader.vertex.data) + "\n\n" + toString(shader.fragment.data);
+					p.j.html("<pre class='code'>" + colorize(code) + "</pre>");
+				}
+			});
+			props.push(p);
+		}
+		return PGroup("Material",props);
+	}
+
+	function getLightProps( l : h3d.scene.Light ) {
+		var props = [];
+		props.push(PColor("color", false, function() return l.color, function(c) l.color.load(c)));
+		props.push(PInt("priority", function() return l.priority, function(p) l.priority = p));
+		props.push(PBool("enableSpecular", function() return l.enableSpecular, function(b) l.enableSpecular = b));
+		var dl = Std.instance(l, h3d.scene.DirLight);
+		if( dl != null )
+			props.push(PFloats("direction", function() return [dl.direction.x, dl.direction.y, dl.direction.z], function(fl) dl.direction.set(fl[0], fl[1], fl[2])));
+		var pl = Std.instance(l, h3d.scene.PointLight);
+		if( pl != null )
+			props.push(PFloats("params", function() return [pl.params.x, pl.params.y, pl.params.z], function(fl) pl.params.set(fl[0], fl[1], fl[2], fl[3])));
+		return PGroup("Light", props);
+	}
+
+	function getObjectProps( o : h3d.scene.Object ) {
+		var props = [];
+		props.push(PString("name", function() return o.name == null ? "" : o.name, function(v) o.name = v == "" ? null : v));
+		props.push(PFloat("x", function() return o.x, function(v) o.x = v));
+		props.push(PFloat("y", function() return o.y, function(v) o.y = v));
+		props.push(PFloat("z", function() return o.z, function(v) o.z = v));
+		props.push(PBool("visible", function() return o.visible, function(v) o.visible = v));
+
+		if( o.isMesh() ) {
+			var multi = Std.instance(o, h3d.scene.MultiMaterial);
+			if( multi != null && multi.materials.length > 1 ) {
+				for( m in multi.materials )
+					props.push(getMaterialProps(m));
+			} else
+				props.push(getMaterialProps(o.toMesh().material));
+		} else {
+			var c = Std.instance(o, h3d.scene.CustomObject);
+			if( c != null )
+				props.push(getMaterialProps(c.material));
+			var l = Std.instance(o, h3d.scene.Light);
+			if( l != null )
+				props.push(getLightProps(l));
+		}
+		return props;
+	}
+
+	function getDynamicProps( v : Dynamic ) {
+		var fx = Std.instance(v,h3d.pass.ScreenFx);
+		if( fx != null )
+			return [getShaderProps(fx.shader)];
+		var s = Std.instance(v, hxsl.Shader);
+		if( s != null )
+			return [getShaderProps(s)];
+		return null;
+	}
+
+	function addDynamicProps( props : Array<Property>, o : Dynamic ) {
+		var cl = Type.getClass(o);
+		var meta = haxe.rtti.Meta.getFields(cl);
+		var fields = Type.getInstanceFields(cl);
+		fields.sort(Reflect.compare);
+		for( f in fields ) {
+			var v = Reflect.field(o, f);
+			var pl = getDynamicProps(v);
+			if( pl != null )
+				props.push(PGroup(f, pl));
+			else {
+				// @inspect metadata
+				var m = Reflect.field(meta, f);
+				if( m != null && Reflect.hasField(m, "inspect") ) {
+					if( Std.is(v, Bool) )
+						props.unshift(PBool(f, function() return Reflect.getProperty(o, f), function(v) Reflect.setProperty(o, f, v)));
+				}
+			}
+		}
+	}
+
+	function getPassProps( p : h3d.pass.Base ) {
+		var props = [];
+		var def = Std.instance(p, h3d.pass.Default);
+		if( def == null ) return props;
+
+		addDynamicProps(props, p);
+
+		for( t in getTextures(@:privateAccess def.tcache) )
+			props.push(t);
+
+		return props;
+	}
+
+	public function getRendererProps() {
+		var props = [];
+		var ls = scene.lightSystem;
+		props.push(PGroup("LightSystem", [
+			PInt("maxLightsPerObject", function() return ls.maxLightsPerObject, function(s) ls.maxLightsPerObject = s),
+			PColor("ambientLight", false, function() return ls.ambientLight, function(v) ls.ambientLight = v),
+			PBool("perPixelLighting", function() return ls.perPixelLighting, function(b) ls.perPixelLighting = b),
+		]));
+
+		var s = Std.instance(scene.renderer.getPass("shadow", false),h3d.pass.ShadowMap);
+		if( s != null ) {
+			props.push(PGroup("Shadows", [
+				PInt("size", function() return s.size, function(sz) s.size = sz),
+				PColor("color", false, function() return s.color, function(v) s.color = v),
+				PFloat("power", function() return s.power, function(v) s.power = v),
+				PFloat("bias", function() return s.bias, function(v) s.bias = v),
+			]));
+		}
+
+		var r = scene.renderer;
+
+		var tex = getTextures(@:privateAccess r.tcache);
+		if( tex.length > 0 )
+			props.push( PGroup("Textures", tex) );
+
+		var pmap = new Map();
+		for( p in @:privateAccess r.allPasses ) {
+			if( pmap.exists(p.p) ) continue;
+			pmap.set(p.p, true);
+			props.push(PGroup("Pass " + p.name, getPassProps(p.p)));
+		}
+
+		addDynamicProps(props, r);
+		return props;
+	}
+
+	function getTextures( t : h3d.impl.TextureCache ) {
+		var cache = @:privateAccess t.cache;
+		var props = [];
+		for( i in 0...cache.length ) {
+			var t = cache[i];
+			props.push(PTexture(t.name, function() return t, null));
+		}
+		return props;
+	}
+
+}

+ 182 - 0
hxd/inspect/StatsPanel.hx

@@ -0,0 +1,182 @@
+package hxd.inspect;
+
+class StatsPanel extends Panel {
+
+	public function new() {
+		super("stats", "Statistics");
+	}
+
+	override function initContent() {
+		super.initContent();
+		j.html('
+			<table>
+				<tr>
+					<th class="title" colspan="2">Renderer</th>
+				</tr>
+				<tr>
+					<th>Framerate</th>
+					<td id="fps">0</td>
+				</tr>
+				<tr>
+					<th>Draw Calls</th>
+					<td id="calls">0</td>
+				</tr>
+				<tr>
+					<th>Drawn Triangles</th>
+					<td id="tris">0</td>
+				</tr>
+
+				<tr>
+					<th class="title" colspan="2">Memory</th>
+				</tr>
+				<tr>
+					<th>
+						<span>Total</span>
+						<div id="totMemCount"></div>
+					</th>
+					<td id="totMem"></td>
+				</tr>
+				<tr>
+					<th class="button hidden">
+						<i class="fa fa-arrow-right"/>
+						<span id="bufMemTitle">Buffers</span>
+						<div id="bufMemCount"></div>
+					</th>
+					<td id="bufMem"></td>
+				</tr>
+				<tr>
+					<th class="button hidden">
+						<i class="fa fa-arrow-right"/>
+						<span id="texMemTitle">Textures</span>
+						<div id="texMemCount"></div>
+					</th>
+					<td id="texMem"></td>
+				</tr>
+			</table>
+		');
+
+		for( b in j.find("th.button").elements() ) {
+			b.click(function(_) {
+				b.toggleClass("hidden");
+				var i = b.children("i");
+				i.toggleClass("fa-arrow-right");
+				i.toggleClass("fa-arrow-down");
+			});
+		}
+	}
+
+	function showMemoryDetails( button : cdb.jq.JQuery ) {
+		var id = button.find("span").getAttr("id");
+		button.parent().parent().find(".detail_" + id).remove();
+
+		#if !debug
+			if(!button.hasClass("hidden")) {
+				var newElement = J("<tr>");
+				newElement.addClass("detail_" + id);
+				newElement.html("<th class='debug' colspan='2'>(Debug mode only)</th>");
+				newElement.insertAfter(button.parent());
+			}
+		#else
+			if(!button.hasClass("hidden")) {
+				var engine = h3d.Engine.getCurrent();
+				var m = new Map();
+				@:privateAccess switch(id) {
+					case "bufMemTitle":
+						for( b in engine.mem.buffers ) {
+							var b = b;
+							while( b != null ) {
+								var buf = b.allocHead;
+								while( buf != null ) {
+									var mem = buf.buffer.stride * buf.vertices * 4;
+									var name = buf.allocPos.className + ":" + buf.allocPos.lineNumber;
+									var p = m.get(name);
+									if( p == null ) {
+										p = { count : 0, mem : 0, name : name };
+										m.set(name, p);
+									}
+									p.count++;
+									p.mem += mem;
+									buf = buf.allocNext;
+								}
+								b = b.next;
+							}
+						}
+					case "texMemTitle":
+						for( t in engine.mem.textures ) {
+							var mem = t.width * t.height * 4;
+							var name = t.allocPos.fileName + ":" + t.allocPos.lineNumber;
+							var p = m.get(name);
+							if( p == null ) {
+								p = { count : 0, mem : 0, name : name };
+								m.set(name, p);
+							}
+							p.count++;
+							p.mem += mem;
+						}
+					default: null;
+				}
+
+				var elements = [for( k in m ) k];
+				elements.sort(function(e1, e2) return e1.mem - e2.mem);
+				for( e in elements) {
+					e.mem >>= 10;
+					var newElement = j.query("<tr>");
+					newElement.addClass("subMem");
+					newElement.addClass("detail_" + id);
+					newElement.html("<th>" + e.name + "<div>[" + e.count + "]</div></th><td>" + (e.mem > 1024 ? Math.fmt(e.mem / 1024) + " MB" : e.mem + " KB") + "</td>");
+					newElement.insertAfter(button.parent());
+				}
+			}
+		#end
+	}
+
+	inline function numberFormat(v : Int) {
+		var tmp = Std.string(v);
+		var n = Math.ceil(tmp.length / 3);
+		var str = "";
+		for( i in 0...n) {
+			if(str != "") str = " " + str;
+			var start = tmp.length - 3 * (i + 1);
+			str = Std.string(tmp.substring(Math.imax(0, start), start + 3)) + str;
+		}
+		return Std.string(str);
+	}
+
+	override function sync() {
+		var engine = h3d.Engine.getCurrent();
+		var p = j;
+
+		p.find("#fps").text(Std.string(engine.fps));
+		p.find("#calls").text(numberFormat(engine.drawCalls));
+		p.find("#tris").text(numberFormat(engine.drawTriangles));
+
+		var bufMem = p.find("#bufMem");
+		var texMem = p.find("#texMem");
+		var totMem = p.find("#totMem");
+		var bufMemTitle = p.find("#bufMemTitle");
+		var texMemTitle = p.find("#texMemTitle");
+		var bufMemCount = p.find("#bufMemCount");
+		var texMemCount = p.find("#texMemCount");
+		var totMemCount = p.find("#totMemCount");
+
+		var stats = engine.mem.stats();
+		var idx = (stats.totalMemory - (stats.textureMemory + stats.managedMemory));
+		var sum : Float = (idx + stats.managedMemory) >> 10;
+		var freeMem : Float = stats.freeManagedMemory >> 10;
+		var totTex : Float = stats.textureMemory >> 10;
+		var totalMem : Float = stats.totalMemory >> 10;
+
+		bufMem.text((sum > 1024 ?  Math.fmt(sum / 1024) + " MB" : totTex + " KB") + " (" + (freeMem > 1024 ?  Math.fmt(freeMem / 1024) + " MB" : freeMem + " KB") + " free)");
+		texMem.text(totTex > 1024 ?  Math.fmt(totTex / 1024) + " MB" : totTex + " KB");
+		totMem.text(totalMem > 1024 ?  Math.fmt(totalMem / 1024) + " MB" : totTex + " KB");
+		bufMemTitle.text("Buffers");
+		bufMemCount.text("[" + Std.string(stats.bufferCount) + "]");
+		texMemTitle.text("Textures");
+		texMemCount.text("[" + Std.string(stats.textureCount) + "]");
+		totMemCount.text("[" + Std.string(stats.bufferCount + stats.textureCount) + "]");
+
+		for( b in p.find("th.button").elements() )
+			showMemoryDetails(b);
+	}
+
+}

+ 64 - 0
hxd/inspect/TreeNode.hx

@@ -0,0 +1,64 @@
+package hxd.inspect;
+
+class TreeNode extends Node {
+
+	public var icon(default, set) : String;
+	var jchild : cdb.jq.JQuery;
+
+	override function initContent() {
+		j = getJRoot().query("<li>");
+		j.html('<i/><div class="content"></div>');
+		j.children("i").click(function(_) {
+			if( jchild != null ) {
+				j.toggleClass("expand");
+				jchild.slideToggle(50);
+			}
+		});
+		j.children(".content").click(function(_) {
+			getJRoot().find(".selected").removeClass("selected");
+			j.addClass("selected");
+			onSelect();
+		});
+	}
+
+	override function getJRoot() {
+		return parent == null ? super.getJRoot() : parent.getJRoot();
+	}
+
+	override function removeChild(n:Node) {
+		super.removeChild(n);
+		n.j.detach();
+		if( jchild.get().numChildren == 0 ) {
+			jchild.remove();
+			jchild = null;
+		}
+	}
+
+	override function addChild(n:Node) {
+		super.addChild(n);
+		if( jchild == null ) {
+			jchild = j.query("<ul>");
+			jchild.addClass("elt");
+			jchild.appendTo(j);
+		}
+		n.j.appendTo(jchild);
+	}
+
+	override function getPathName() {
+		return name.split(".").join(" ");
+	}
+
+	public dynamic function onSelect() {
+	}
+
+	override function set_name(v) {
+		j.children(".content").text(v);
+		return name = v;
+	}
+
+	function set_icon(v) {
+		j.children("i").attr("class", "fa fa-"+v);
+		return icon = v;
+	}
+
+}

+ 17 - 17
hxd/net/inspect.css → hxd/inspect/inspect.css

@@ -51,54 +51,54 @@
 .jqpage ul.toolbar li.active, .dialog-floating ul.toolbar li.active {
 .jqpage ul.toolbar li.active, .dialog-floating ul.toolbar li.active {
 	color : white;
 	color : white;
 }
 }
-.jqpage #scene .scrollable, .dialog-floating #scene .scrollable {
+.jqpage .panel.scene .scrollable, .dialog-floating .panel.scene .scrollable {
 	height : 100%;
 	height : 100%;
 	overflow : auto;
 	overflow : auto;
 }
 }
-.jqpage #scene ul.elt.root, .dialog-floating #scene ul.elt.root {
+.jqpage .panel.scene ul.elt.root, .dialog-floating .panel.scene ul.elt.root {
 	padding : 5px;
 	padding : 5px;
 }
 }
-.jqpage #scene ul.elt, .dialog-floating #scene ul.elt {
+.jqpage .panel.scene ul.elt, .dialog-floating .panel.scene ul.elt {
 	background-color : transparent;
 	background-color : transparent;
 }
 }
-.jqpage #scene ul.elt li, .dialog-floating #scene ul.elt li {
+.jqpage .panel.scene ul.elt li, .dialog-floating .panel.scene ul.elt li {
 	cursor : pointer;
 	cursor : pointer;
 }
 }
-.jqpage #scene ul.elt li>i, .dialog-floating #scene ul.elt li>i {
+.jqpage .panel.scene ul.elt li>i, .dialog-floating .panel.scene ul.elt li>i {
 	text-align : center;
 	text-align : center;
 	width : 16px;
 	width : 16px;
 }
 }
-.jqpage #scene ul.elt li>.content, .dialog-floating #scene ul.elt li>.content {
+.jqpage .panel.scene ul.elt li>.content, .dialog-floating .panel.scene ul.elt li>.content {
 	margin-left : 2px;
 	margin-left : 2px;
 	display : inline-block;
 	display : inline-block;
 	zoom : 1;
 	zoom : 1;
 	*display : inline;
 	*display : inline;
 	height : 18px;
 	height : 18px;
 }
 }
-.jqpage #scene ul.elt li .content:hover, .dialog-floating #scene ul.elt li .content:hover {
+.jqpage .panel.scene ul.elt li .content:hover, .dialog-floating .panel.scene ul.elt li .content:hover {
 	background-color : #eee;
 	background-color : #eee;
 }
 }
-.jqpage #scene ul.elt ul, .dialog-floating #scene ul.elt ul {
+.jqpage .panel.scene ul.elt ul, .dialog-floating .panel.scene ul.elt ul {
 	padding-left : 20px;
 	padding-left : 20px;
 }
 }
-.jqpage #scene ul.elt li.selected>.content, .dialog-floating #scene ul.elt li.selected>.content {
+.jqpage .panel.scene ul.elt li.selected>.content, .dialog-floating .panel.scene ul.elt li.selected>.content {
 	background-color : #eee;
 	background-color : #eee;
 }
 }
-.jqpage #scene ul.elt li.hidden, .dialog-floating #scene ul.elt li.hidden {
+.jqpage .panel.scene ul.elt li.hidden, .dialog-floating .panel.scene ul.elt li.hidden {
 	opacity : 0.5;
 	opacity : 0.5;
 	filter : alpha(opacity=50);
 	filter : alpha(opacity=50);
 	zoom : 1;
 	zoom : 1;
 }
 }
-.jqpage #scene ul.elt li.culled>.content, .dialog-floating #scene ul.elt li.culled>.content {
+.jqpage .panel.scene ul.elt li.culled>.content, .dialog-floating .panel.scene ul.elt li.culled>.content {
 	opacity : 0.5;
 	opacity : 0.5;
 	filter : alpha(opacity=50);
 	filter : alpha(opacity=50);
 	zoom : 1;
 	zoom : 1;
 	font-style : italic;
 	font-style : italic;
 }
 }
-.jqpage #scene #scontent.masked li.hidden, .jqpage #scene #scontent.masked li.culled, .dialog-floating #scene #scontent.masked li.hidden, .dialog-floating #scene #scontent.masked li.culled {
+.jqpage .panel.scene .elt.root.masked li.hidden, .jqpage .panel.scene .elt.root.masked li.culled, .dialog-floating .panel.scene .elt.root.masked li.hidden, .dialog-floating .panel.scene .elt.root.masked li.culled {
 	display : none;
 	display : none;
 }
 }
-.jqpage #scene>ul.buttons, .dialog-floating #scene>ul.buttons {
+.jqpage .panel.scene>ul.buttons, .dialog-floating .panel.scene>ul.buttons {
 	color : #AAA;
 	color : #AAA;
 	background-color : #333;
 	background-color : #333;
 	border-right : 1px solid black;
 	border-right : 1px solid black;
@@ -106,21 +106,21 @@
 	width : 25px;
 	width : 25px;
 	height : 100%;
 	height : 100%;
 }
 }
-.jqpage #scene>ul.buttons li, .dialog-floating #scene>ul.buttons li {
+.jqpage .panel.scene>ul.buttons li, .dialog-floating .panel.scene>ul.buttons li {
 	padding : 5px;
 	padding : 5px;
 	display : inline-block;
 	display : inline-block;
 	zoom : 1;
 	zoom : 1;
 	*display : inline;
 	*display : inline;
 	border : 1px solid #555;
 	border : 1px solid #555;
 }
 }
-.jqpage #scene>ul.buttons li:hover, .dialog-floating #scene>ul.buttons li:hover {
+.jqpage .panel.scene>ul.buttons li:hover, .dialog-floating .panel.scene>ul.buttons li:hover {
 	background-color : #555;
 	background-color : #555;
 	border-color : white;
 	border-color : white;
 }
 }
-.jqpage #scene>ul.buttons li.active, .dialog-floating #scene>ul.buttons li.active {
+.jqpage .panel.scene>ul.buttons li.active, .dialog-floating .panel.scene>ul.buttons li.active {
 	color : white;
 	color : white;
 }
 }
-.jqpage #scene>ul.elt, .dialog-floating #scene>ul.elt {
+.jqpage .panel.scene>ul.elt, .dialog-floating .panel.scene>ul.elt {
 	padding : 5px;
 	padding : 5px;
 }
 }
 .jqpage table.props tr.pgroup, .dialog-floating table.props tr.pgroup {
 .jqpage table.props tr.pgroup, .dialog-floating table.props tr.pgroup {

+ 4 - 7
hxd/net/inspect.hss → hxd/inspect/inspect.hss

@@ -51,7 +51,7 @@
 	}
 	}
 
 
 
 
-	#scene {
+	.panel.scene {
 
 
 		.scrollable {
 		.scrollable {
 			height:100%;
 			height:100%;
@@ -96,14 +96,14 @@
 			}
 			}
 		}
 		}
 
 
-		#scontent.masked {
+		.elt.root.masked {
 			li.hidden, li.culled {
 			li.hidden, li.culled {
 				display : none;
 				display : none;
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	#scene > ul.buttons {
+	.panel.scene > ul.buttons {
 			color : #AAA;
 			color : #AAA;
 			background-color : #333;
 			background-color : #333;
 			border-right : 1px solid black;
 			border-right : 1px solid black;
@@ -124,13 +124,10 @@
 			}
 			}
 		}
 		}
 
 
-	#scene > ul.elt {
+	.panel.scene > ul.elt {
 		padding : 5px;
 		padding : 5px;
 	}
 	}
 
 
-	#props {
-	}
-
 	table.props {
 	table.props {
 		tr.pgroup {
 		tr.pgroup {
 			cursor : pointer;
 			cursor : pointer;

+ 0 - 959
hxd/net/SceneInspector.hx

@@ -1,959 +0,0 @@
-package hxd.net;
-import cdb.jq.JQuery;
-import hxd.net.Property;
-
-private class DrawEvent implements h3d.IDrawable {
-	var i : SceneInspector;
-	public function new(i) {
-		this.i = i;
-	}
-	public function render( engine : h3d.Engine ) {
-		i.sync();
-	}
-}
-
-class Node {
-
-	public var name(default,set) : String;
-	public var icon(default, set) : String;
-	public var parent(default, set) : Node;
-
-	public var props : Void -> Array<Property>;
-
-	var root : JQuery;
-	var childs : Array<Node>;
-	var j : cdb.jq.JQuery;
-	var jchild : cdb.jq.JQuery;
-
-	public function new( root : JQuery, ?parent) {
-
-		this.root = root;
-		j = root.query("<li>");
-		j.html('<i/><div class="content"></div>');
-
-		childs = [];
-
-		this.parent = parent;
-
-		j.children("i").click(function(_) {
-			if( childs.length > 0 ) {
-				j.toggleClass("expand");
-				jchild.slideToggle(50);
-			}
-		});
-		j.children(".content").click(function(_) {
-			root.find(".selected").removeClass("selected");
-			j.addClass("selected");
-			onSelect();
-		});
-	}
-
-	function set_parent(p:Node) {
-		j.detach();
-		if( parent != null ) {
-			parent.childs.remove(this);
-			if( parent.jchild.get().numChildren == 0 ) {
-				parent.jchild.remove();
-				parent.jchild = null;
-			}
-		}
-		if( p != null ) {
-			if( p.jchild == null ) {
-				p.jchild = root.query("<ul>");
-				p.jchild.addClass("elt");
-				p.jchild.appendTo(p.j);
-			}
-			j.appendTo(p.jchild);
-			p.childs.push(this);
-		} else
-			j.appendTo(root);
-		return parent = p;
-	}
-
-	public function getPathName() {
-		return name.split(".").join(" ");
-	}
-
-	public function getFullPath() {
-		var n = getPathName();
-		if( parent == null )
-			return n;
-		return parent.getFullPath() + "." + n;
-	}
-
-	public dynamic function onSelect() {
-	}
-
-	public function remove() {
-		j.dispose();
-		for( c in childs )
-			c.remove();
-		if( parent != null )
-			parent.childs.remove(this);
-	}
-
-	function set_name(v) {
-		j.children(".content").text(v);
-		return name = v;
-	}
-
-	function set_icon(v) {
-		j.children("i").attr("class", "fa fa-"+v);
-		return icon = v;
-	}
-
-}
-
-private class SceneObject extends Node {
-
-	public var o : h3d.scene.Object;
-	public var visible = true;
-	public var culled = false;
-
-	public function new(i, o, p) {
-		super(i,p);
-		this.o = o;
-	}
-
-	function objectName( o : h3d.scene.Object ) {
-		var name = o.name != null ? o.name : o.toString();
-		return name.split(".").join("_");
-	}
-
-	override public function getPathName() {
-		var name = objectName(o);
-		if( o.parent == null )
-			return name;
-		var idx = Lambda.indexOf(@:privateAccess o.parent.childs, o);
-		var count = 0;
-		for( i in 0...idx )
-			if( objectName(o) == name )
-				count++;
-		if( count > 0 )
-			name += "@" + count;
-		return name;
-	}
-
-}
-
-class Tool {
-	public var name(default,set) : String;
-	public var icon(default,set) : String;
-	public var title(default,set) : String;
-	public var click : Void -> Void;
-	public var j : JQuery;
-	public var jicon : JQuery;
-	public var active(get, set) : Bool;
-	public function new() {
-	}
-	function get_active() {
-		return j.hasClass("active");
-	}
-	function set_active(v) {
-		j.toggleClass("active", v);
-		return v;
-	}
-	function set_name(v) {
-		jicon.attr("alt", v);
-		return name = v;
-	}
-	function set_icon(v) {
-		jicon.attr("class", "fa fa-"+v);
-		return icon = v;
-	}
-	function set_title(v) {
-		j.attr("title", v);
-		return title = v;
-	}
-}
-
-class SceneInspector {
-
-	static var CSS = hxd.res.Embed.getFileContent("hxd/net/inspect.css");
-
-	public var scene(default, set) : h3d.scene.Scene;
-	public var connected(get, never): Bool;
-
-	var inspect : PropInspector;
-	var jroot : JQuery;
-	var event : DrawEvent;
-	var oldLog : Dynamic -> haxe.PosInfos -> Void;
-	var savedFile : String;
-	var sceneObjects : Array<SceneObject> = [];
-	var scenePosition = 0;
-	var oldLoop : Void -> Void;
-	var state : Map<String,{ original : Dynamic, current : Dynamic }>;
-	var activeTool : JQuery;
-	var rootNodes : Array<Node>;
-
-
-	public function new( scene, ?host, ?port ) {
-		event = new DrawEvent(this);
-		savedFile = "sceneProps.js";
-		state = new Map();
-		oldLog = haxe.Log.trace;
-		haxe.Log.trace = onTrace;
-		inspect = new PropInspector(host, port);
-		inspect.resolveProps = resolveProps;
-		inspect.onChange = onChange;
-		inspect.handleKey = onKey;
-		this.scene = scene;
-		rootNodes = [];
-		sceneObjects = [];
-
-		init();
-
-		addNode("Renderer", "object-group", getRendererProps);
-		addTool("Load...", "download", load, "Load settings");
-		addTool("Save...", "save", save, "Save settings");
-		addTool("Undo", "undo", inspect.undo, "Undo");
-		addTool("Repeat", "repeat", inspect.redo, "Redo");
-		var pause : Tool = null;
-		pause = addTool("Pause", "pause", function() {
-			if( oldLoop != null ) {
-				hxd.System.setLoop(oldLoop);
-				oldLoop = null;
-			} else {
-				oldLoop = hxd.System.getCurrentLoop();
-				hxd.System.setLoop(pauseLoop);
-			}
-			pause.active = oldLoop != null;
-		}, "Pause scene");
-		addTool("Statistics", "bar-chart", getStats, "Open statistics");
-	}
-
-	public function dispose() {
-		inspect.dispose();
-		scene = null;
-		if( oldLoop != null ) {
-			hxd.System.setLoop(oldLoop);
-			oldLoop = null;
-		}
-	}
-
-	inline function get_connected() {
-		return inspect.connected;
-	}
-
-	public inline function J( ?elt : cdb.jq.Dom, ?query : String ) {
-		return inspect.J(elt,query);
-	}
-
-	public function getActiveTool() {
-		return activeTool;
-	}
-
-	public function createPanel( title ) {
-		return inspect.createPanel(title);
-	}
-
-	public function addTool( name : String, icon : String, click : Void -> Void, ?title : String = "" ) {
-		var t = new Tool();
-		t.j = J("<li>");
-		t.jicon = J("<i>").appendTo(t.j);
-		t.j.click(function(_) t.click());
-		t.name = name;
-		t.icon = icon;
-		t.title = title;
-		t.click = click;
-		t.j.appendTo(jroot.find("#toolbar"));
-		return t;
-	}
-
-	function onKey( e : cdb.jq.Event ) {
-		switch( e.keyCode ) {
-		case 'S'.code if( e.ctrlKey ):
-			save();
-		case hxd.Key.F1:
-			load();
-		default:
-		}
-	}
-
-	function onTrace( v : Dynamic, ?pos : haxe.PosInfos ) {
-		oldLog(v, pos);
-		if( inspect.connected ) {
-			var vstr = null;
-			if( pos.customParams != null ) {
-				pos.customParams.unshift(v);
-				vstr = [for( v in pos.customParams ) Std.string(v)].join(",");
-			} else
-				vstr = Std.string(v);
-			J("<pre>").addClass("line").text(pos.fileName+"(" + pos.lineNumber + ") : " + vstr).appendTo(J("#log"));
-		}
-	}
-
-	function set_scene(s:h3d.scene.Scene) {
-		if( scene != null )
-			scene.removePass(event);
-		if( s != null )
-			s.addPass(event);
-		return scene = s;
-	}
-
-	function pauseLoop() {
-		scene.setElapsedTime(0);
-		h3d.Engine.getCurrent().render(scene);
-	}
-
-	function init() {
-		jroot = J(inspect.getRoot());
-		jroot.html('
-			<style>$CSS</style>
-			<ul id="toolbar" class="toolbar">
-			</ul>
-			<div id="scene" class="panel" caption="Scene">
-				<ul class="buttons">
-					<li id="bt_hide" title="Show/Hide invisible objects">
-						<i class="fa fa-eye" />
-					</li>
-					<li id="bt_highlight" title="[TODO] Auto highlight in scene selected object">
-						<i class="fa fa-cube" />
-					</li>
-				</ul>
-				<div class="scrollable">
-					<ul id="scontent" class="elt root">
-					</ul>
-				</div>
-			</div>
-			<div id="props" class="panel" caption="Properties">
-			</div>
-			<div id="log" class="panel" caption="Log">
-			</div>
-		');
-		jroot.attr("title", "Inspect");
-		var scene = J("#scene");
-		scene.dock(jroot.get(), Left, 0.2);
-		J("#log").dock(jroot.get(), Down, 0.3);
-		J("#props").dock(scene.get(), Down, 0.5);
-
-		var bt = J("#bt_hide");
-		bt.addClass("active");
-		bt.click(function(_) {
-			bt.toggleClass("active");
-			J("#scontent").toggleClass("masked");
-		});
-
-		var bt = J("#bt_highlight");
-		bt.click(function(_) {
-			bt.toggleClass("active");
-		});
-	}
-
-	function load() {
-		try {
-		hxd.File.browse(function(b) {
-			savedFile = b.fileName;
-			b.load(function(bytes) {
-
-				// reset to default
-				for( s in state.keys() )
-					inspect.setPathPropValue(s, state.get(s).original);
-				state = new Map();
-
-				var o : Dynamic = haxe.Json.parse(bytes.toString());
-				function browseRec( path : Array<String>, v : Dynamic ) {
-					switch( Type.typeof(v) ) {
-					case TNull, TInt, TFloat, TBool, TClass(_):
-						var path = path.join(".");
-						state.set(path, { original : null, current : v });
-					case TUnknown, TFunction, TEnum(_):
-						throw "Invalid value " + v;
-					case TObject:
-						for( f in Reflect.fields(v) ) {
-							var fv = Reflect.field(v, f);
-							path.push(f);
-							browseRec(path, fv);
-							path.pop();
-						}
-					}
-				}
-				browseRec([], o);
-				for( s in state.keys() )
-					inspect.setPathPropValue(s, state.get(s).current);
-			});
-
-		},{ defaultPath : savedFile, fileTypes : [ { name:"Scene Props", extensions:["js"] } ] } );
-		} catch( e : Dynamic ) {
-			// already open
-		}
-	}
-
-	function save() {
-		var o : Dynamic = { };
-		for( s in state.keys() ) {
-			var path = s.split(".");
-			var o = o;
-			while( path.length > 1 ) {
-				var name = path.shift();
-				var s = Reflect.field(o, name);
-				if( s == null ) {
-					s = { };
-					Reflect.setField(o, name, s);
-				}
-				o = s;
-			}
-			Reflect.setField(o, path[0], state.get(s).current);
-		}
-		var js = haxe.Json.stringify(o, null, "\t");
-		try {
-			hxd.File.saveAs(haxe.io.Bytes.ofString(js), { defaultPath : savedFile, saveFileName : function(name) savedFile = name } );
-		} catch( e : Dynamic ) {
-			// already open
-		}
-	}
-
-	public function addNode( name : String, icon : String, ?getProps : Void -> Array<Property>, ?parent : Node ) : Node {
-		var n = new Node(J("#scontent"), parent);
-		n.name = name;
-		n.icon = icon;
-		n.props = getProps;
-		if( getProps != null )
-			n.onSelect = function() fillProps(n.getFullPath(), getProps());
-		if( parent == null )
-			rootNodes.push(n);
-		return n;
-	}
-
-	public function sync() {
-		if( scene == null || !inspect.connected ) return;
-		scenePosition = 0;
-		syncRec(scene, null);
-		while( sceneObjects.length > scenePosition )
-			sceneObjects.pop().remove();
-		syncStats();
-	}
-
-	function resolveProps( path : Array<String> ) {
-		var cur = null;
-		var nodes = rootNodes;
-		while( true ) {
-			var k = path.shift();
-			if( k == null ) break;
-			var found = false;
-			for( n in nodes ) {
-				if( n.getPathName() == k ) {
-					found = true;
-					cur = n;
-					nodes = @:privateAccess n.childs;
-					break;
-				}
-			}
-			if( !found ) {
-				path.unshift(k);
-				break;
-			}
-		}
-		return cur == null || cur.props == null ? null : cur.props();
-	}
-
-	function getObjectIcon( o : h3d.scene.Object) {
-		if( Std.is(o, h3d.scene.Skin) )
-			return "child";
-		if( Std.is(o, h3d.scene.Mesh) )
-			return "cube";
-		if( Std.is(o, h3d.scene.CustomObject) )
-			return "globe";
-		if( Std.is(o, h3d.scene.Scene) )
-			return "picture-o";
-		if( Std.is(o, h3d.scene.Light) )
-			return "lightbulb-o";
-		return null;
-	}
-
-	@:access(hxd.net.Node)
-	function syncRec( o : h3d.scene.Object, p : SceneObject ) {
-		var so = sceneObjects[scenePosition];
-		if( so != null && so.o != o ) {
-			for( i in scenePosition + 1...sceneObjects.length )
-				if( sceneObjects[i].o == o ) {
-					var tmp = sceneObjects[i];
-					sceneObjects[i] = so;
-					sceneObjects[scenePosition] = tmp;
-					so = tmp;
-					break;
-				}
-		}
-		if( so == null || so.o != o ) {
-			so = new SceneObject(J("#scontent"), o, p);
-			so.name = o.name != null ? o.name : o.toString();
-			sceneObjects.insert(scenePosition, so);
-			var icon = getObjectIcon(o);
-			so.icon = icon == null ? "circle-o" : getObjectIcon(o);
-			so.props = function() return getObjectProps(o);
-			so.onSelect = function() fillProps(so.getFullPath(), getObjectProps(o));
-			if( p == null )
-				rootNodes.push(so);
-		}
-
-		if( so.parent != p ) so.parent = p;
-
-		if( o.visible != so.visible ) {
-			so.visible = o.visible;
-			so.j.toggleClass("hidden");
-		}
-		@:privateAccess if( o.culled != so.culled ) {
-			so.culled = o.culled;
-			so.j.toggleClass("culled");
-		}
-
-		scenePosition++;
-		if( o.numChildren > 0 ) {
-			for( c in o )
-				syncRec(c, so);
-		} else if( so.jchild != null ) {
-			so.jchild.remove();
-			so.jchild = null;
-			for( o in so.childs )
-				o.remove();
-		}
-	}
-
-	function onChange( path : String, oldV : Dynamic, newV : Dynamic ) {
-		var s = state.get(path);
-		if( s == null )
-			state.set(path, { original : oldV, current : newV } );
-		else {
-			if( inspect.sameValue(s.original,newV) )
-				state.remove(path);
-			else
-				s.current = newV;
-		}
-	}
-
-	function getShaderProps( s : hxsl.Shader ) {
-		var props = [];
-		var data = @:privateAccess s.shader;
-		var vars = data.data.vars.copy();
-		vars.sort(function(v1, v2) return Reflect.compare(v1.name, v2.name));
-		for( v in vars ) {
-			switch( v.kind ) {
-			case Param:
-				var name = v.name+"__";
-				function set(val:Dynamic) {
-					Reflect.setField(s, name, val);
-					if( hxsl.Ast.Tools.isConst(v) )
-						@:privateAccess s.constModified = true;
-				}
-				switch( v.type ) {
-				case TBool:
-					props.push(PBool(v.name, function() return Reflect.field(s,name), set ));
-				case TInt:
-					props.push(PInt(v.name, function() return Reflect.field(s,name), set ));
-				case TFloat:
-					props.push(PFloat(v.name, function() return Reflect.field(s, name), set));
-				case TVec(size = (3 | 4), VFloat) if( v.name.toLowerCase().indexOf("color") >= 0 ):
-					props.push(PColor(v.name, size == 4, function() return Reflect.field(s, name), set));
-				case TSampler2D, TSamplerCube:
-					props.push(PTexture(v.name, function() return Reflect.field(s, name), set));
-				case TVec(size, VFloat):
-					props.push(PFloats(v.name, function() {
-						var v : h3d.Vector = Reflect.field(s, name);
-						var vl = [v.x, v.y];
-						if( size > 2 ) vl.push(v.z);
-						if( size > 3 ) vl.push(v.w);
-						return vl;
-					}, function(vl) {
-						set(new h3d.Vector(vl[0], vl[1], vl[2], vl[3]));
-					}));
-				case TArray(_):
-					props.push(PString(v.name, function() {
-						var a : Array<Dynamic> = Reflect.field(s, name);
-						return a == null ? "NULL" : "(" + a.length + " elements)";
-					}, function(val) {}));
-				default:
-					props.push(PString(v.name, function() return ""+Reflect.field(s,name), function(val) { } ));
-				}
-			default:
-			}
-		}
-
-		var name = data.data.name;
-		if( StringTools.startsWith(name, "h3d.shader.") )
-			name = name.substr(11);
-		name = name.split(".").join(" "); // no dot in prop name !
-
-		return PGroup("shader "+name, props);
-	}
-
-	function colorize( code : String ) {
-		code = code.split("\t").join("    ");
-		code = StringTools.htmlEscape(code);
-		code = ~/\b((var)|(function)|(if)|(else)|(for)|(while))\b/g.replace(code, "<span class='kwd'>$1</span>");
-		code = ~/(@[A-Za-z]+)/g.replace(code, "<span class='meta'>$1</span>");
-		return code;
-	}
-
-	function getMaterialProps( mat : h3d.mat.Material ) {
-		var props = [];
-		props.push(PString("name", function() return mat.name == null ? "" : mat.name, function(n) mat.name = n == "" ? null : n));
-		for( pass in mat.getPasses() ) {
-			var pl = [
-				PBool("Lights", function() return pass.enableLights, function(v) pass.enableLights = v),
-				PEnum("Cull", h3d.mat.Data.Face, function() return pass.culling, function(v) pass.culling = v),
-				PEnum("BlendSrc", h3d.mat.Data.Blend, function() return pass.blendSrc, function(v) pass.blendSrc = pass.blendAlphaSrc = v),
-				PEnum("BlendDst", h3d.mat.Data.Blend, function() return pass.blendDst, function(v) pass.blendDst = pass.blendAlphaDst = v),
-				PBool("DepthWrite", function() return pass.depthWrite, function(b) pass.depthWrite = b),
-				PEnum("DepthTest", h3d.mat.Data.Compare, function() return pass.depthTest, function(v) pass.depthTest = v)
-			];
-
-			var shaders = [for( s in pass.getShaders() ) s];
-			shaders.reverse();
-			for( index in 0...shaders.length ) {
-				var s = shaders[index];
-				var p = getShaderProps(s);
-				p = PPopup(p, ["Toggle", "View Source"], function(j, i) {
-					switch( i ) {
-					case 0:
-						if( index == 0 ) return; // Don't allow toggle base shader
-						if( !pass.removeShader(s) )
-							pass.addShaderAt(s, shaders.length - (index + 1));
-						j.toggleClass("disable");
-					case 1:
-						var shader = @:privateAccess s.shader;
-						var p = inspect.createPanel(shader.data.name+" shader");
-						var toString = hxsl.Printer.shaderToString;
-						var code = toString(shader.data);
-						p.html("<pre class='code'>"+colorize(code)+"</pre>");
-					}
-
-				});
-				pl.push(p);
-			}
-
-			var p = PGroup("pass " + pass.name, pl);
-			p = PPopup(p, ["Toggle", "View Shader"], function(j, i) {
-				switch( i ) {
-				case 0:
-					if( !mat.removePass(pass) )
-						mat.addPass(pass);
-					j.toggleClass("disable");
-				case 1:
-					var p = inspect.createPanel(pass.name+" shader");
-
-					var shader = scene.renderer.compileShader(pass);
-					var toString = hxsl.Printer.shaderToString;
-					var code = toString(shader.vertex.data) + "\n\n" + toString(shader.fragment.data);
-					p.html("<pre class='code'>"+colorize(code)+"</pre>");
-				}
-			});
-			props.push(p);
-		}
-		return PGroup("Material",props);
-	}
-
-	function getLightProps( l : h3d.scene.Light ) {
-		var props = [];
-		props.push(PColor("color", false, function() return l.color, function(c) l.color.load(c)));
-		props.push(PInt("priority", function() return l.priority, function(p) l.priority = p));
-		props.push(PBool("enableSpecular", function() return l.enableSpecular, function(b) l.enableSpecular = b));
-		var dl = Std.instance(l, h3d.scene.DirLight);
-		if( dl != null )
-			props.push(PFloats("direction", function() return [dl.direction.x, dl.direction.y, dl.direction.z], function(fl) dl.direction.set(fl[0], fl[1], fl[2])));
-		var pl = Std.instance(l, h3d.scene.PointLight);
-		if( pl != null )
-			props.push(PFloats("params", function() return [pl.params.x, pl.params.y, pl.params.z], function(fl) pl.params.set(fl[0], fl[1], fl[2], fl[3])));
-		return PGroup("Light", props);
-	}
-
-	function getObjectProps( o : h3d.scene.Object ) {
-		var props = [];
-		props.push(PString("name", function() return o.name == null ? "" : o.name, function(v) o.name = v == "" ? null : v));
-		props.push(PFloat("x", function() return o.x, function(v) o.x = v));
-		props.push(PFloat("y", function() return o.y, function(v) o.y = v));
-		props.push(PFloat("z", function() return o.z, function(v) o.z = v));
-		props.push(PBool("visible", function() return o.visible, function(v) o.visible = v));
-
-		if( o.isMesh() ) {
-			var multi = Std.instance(o, h3d.scene.MultiMaterial);
-			if( multi != null && multi.materials.length > 1 ) {
-				for( m in multi.materials )
-					props.push(getMaterialProps(m));
-			} else
-				props.push(getMaterialProps(o.toMesh().material));
-		} else {
-			var c = Std.instance(o, h3d.scene.CustomObject);
-			if( c != null )
-				props.push(getMaterialProps(c.material));
-			var l = Std.instance(o, h3d.scene.Light);
-			if( l != null )
-				props.push(getLightProps(l));
-		}
-		return props;
-	}
-
-	function fillProps( basePath : String, props : Array<Property> ) {
-		var j = J("#props");
-		j.text("");
-		var t = inspect.makeProps(basePath, props);
-		t.appendTo(j);
-	}
-
-	function getTextures( t : h3d.impl.TextureCache ) {
-		var cache = @:privateAccess t.cache;
-		var props = [];
-		for( i in 0...cache.length ) {
-			var t = cache[i];
-			props.push(PTexture(t.name, function() return t, null));
-		}
-		return props;
-	}
-
-	function getDynamicProps( v : Dynamic ) {
-		var fx = Std.instance(v,h3d.pass.ScreenFx);
-		if( fx != null )
-			return [getShaderProps(fx.shader)];
-		var s = Std.instance(v, hxsl.Shader);
-		if( s != null )
-			return [getShaderProps(s)];
-		return null;
-	}
-
-	function addDynamicProps( props : Array<Property>, o : Dynamic ) {
-		var cl = Type.getClass(o);
-		var meta = haxe.rtti.Meta.getFields(cl);
-		var fields = Type.getInstanceFields(cl);
-		fields.sort(Reflect.compare);
-		for( f in fields ) {
-			var v = Reflect.field(o, f);
-			var pl = getDynamicProps(v);
-			if( pl != null )
-				props.push(PGroup(f, pl));
-			else {
-				// @inspect metadata
-				var m = Reflect.field(meta, f);
-				if( m != null && Reflect.hasField(m, "inspect") ) {
-					if( Std.is(v, Bool) )
-						props.unshift(PBool(f, function() return Reflect.getProperty(o, f), function(v) Reflect.setProperty(o, f, v)));
-				}
-			}
-		}
-	}
-
-	function getPassProps( p : h3d.pass.Base ) {
-		var props = [];
-		var def = Std.instance(p, h3d.pass.Default);
-		if( def == null ) return props;
-
-		addDynamicProps(props, p);
-
-		for( t in getTextures(@:privateAccess def.tcache) )
-			props.push(t);
-
-		return props;
-	}
-
-	function getRendererProps() {
-		var props = [];
-		var ls = scene.lightSystem;
-		props.push(PGroup("LightSystem", [
-			PInt("maxLightsPerObject", function() return ls.maxLightsPerObject, function(s) ls.maxLightsPerObject = s),
-			PColor("ambientLight", false, function() return ls.ambientLight, function(v) ls.ambientLight = v),
-			PBool("perPixelLighting", function() return ls.perPixelLighting, function(b) ls.perPixelLighting = b),
-		]));
-
-		var s = Std.instance(scene.renderer.getPass("shadow", false),h3d.pass.ShadowMap);
-		if( s != null ) {
-			props.push(PGroup("Shadows", [
-				PInt("size", function() return s.size, function(sz) s.size = sz),
-				PColor("color", false, function() return s.color, function(v) s.color = v),
-				PFloat("power", function() return s.power, function(v) s.power = v),
-				PFloat("bias", function() return s.bias, function(v) s.bias = v),
-			]));
-		}
-
-		var r = scene.renderer;
-
-		var tex = getTextures(@:privateAccess r.tcache);
-		if( tex.length > 0 )
-			props.push( PGroup("Textures", tex) );
-
-		var pmap = new Map();
-		for( p in @:privateAccess r.allPasses ) {
-			if( pmap.exists(p.p) ) continue;
-			pmap.set(p.p, true);
-			props.push(PGroup("Pass " + p.name, getPassProps(p.p)));
-		}
-
-		addDynamicProps(props, r);
-		return props;
-	}
-
-	function getStats() {
-		var p = inspect.createPanel("Statistics", Right, 0.35);
-		p.html('
-			<style>$CSS</style>
-			<div id="stats" class="panel">
-				<table>
-					<tr>
-						<th class="title" colspan="2">Renderer</th>
-					</tr>
-					<tr>
-						<th>Framerate</th>
-						<td id="fps">0</td>
-					</tr>
-					<tr>
-						<th>Draw Calls</th>
-						<td id="calls">0</td>
-					</tr>
-					<tr>
-						<th>Drawn Triangles</th>
-						<td id="tris">0</td>
-					</tr>
-
-					<tr>
-						<th class="title" colspan="2">Memory</th>
-					</tr>
-					<tr>
-						<th>
-							<span>Total</span>
-							<div id="totMemCount"></div>
-						</th>
-						<td id="totMem"></td>
-					</tr>
-					<tr>
-						<th class="button hidden">
-							<i class="fa fa-arrow-right"/>
-							<span id="bufMemTitle">Buffers</span>
-							<div id="bufMemCount"></div>
-						</th>
-						<td id="bufMem"></td>
-					</tr>
-					<tr>
-						<th class="button hidden">
-							<i class="fa fa-arrow-right"/>
-							<span id="texMemTitle">Textures</span>
-							<div id="texMemCount"></div>
-						</th>
-						<td id="texMem"></td>
-					</tr>
-				</table>
-			</div>
-		');
-
-		for( b in p.find("th.button").elements() ) {
-			b.click(function(_) {
-				b.toggleClass("hidden");
-				var i = b.children("i");
-				i.toggleClass("fa-arrow-right");
-				i.toggleClass("fa-arrow-down");
-			});
-		}
-	}
-
-	function showMemoryDetails(button : JQuery) {
-		var id = button.find("span").getAttr("id");
-		button.parent().parent().find(".detail_" + id).remove();
-
-		#if !debug
-			if(!button.hasClass("hidden")) {
-				var newElement = J("<tr>");
-				newElement.addClass("detail_" + id);
-				newElement.html("<th class='debug' colspan='2'>(Debug mode only)</th>");
-				newElement.insertAfter(button.parent());
-			}
-		#else
-			if(!button.hasClass("hidden")) {
-				var engine = h3d.Engine.getCurrent();
-				var m = new Map();
-				@:privateAccess switch(id) {
-					case "bufMemTitle":
-						for( b in engine.mem.buffers ) {
-							var b = b;
-							while( b != null ) {
-								var buf = b.allocHead;
-								while( buf != null ) {
-									var mem = buf.buffer.stride * buf.vertices * 4;
-									var name = buf.allocPos.className + ":" + buf.allocPos.lineNumber;
-									var p = m.get(name);
-									if( p == null ) {
-										p = { count : 0, mem : 0, name : name };
-										m.set(name, p);
-									}
-									p.count++;
-									p.mem += mem;
-									buf = buf.allocNext;
-								}
-								b = b.next;
-							}
-						}
-					case "texMemTitle":
-						for( t in engine.mem.textures ) {
-							var mem = t.width * t.height * 4;
-							var name = t.allocPos.fileName + ":" + t.allocPos.lineNumber;
-							var p = m.get(name);
-							if( p == null ) {
-								p = { count : 0, mem : 0, name : name };
-								m.set(name, p);
-							}
-							p.count++;
-							p.mem += mem;
-						}
-					default: null;
-				}
-
-				var elements = [for( k in m ) k];
-				elements.sort(function(e1, e2) return e1.mem - e2.mem);
-				for( e in elements) {
-					e.mem >>= 10;
-					var newElement = J("<tr>");
-					newElement.addClass("subMem");
-					newElement.addClass("detail_" + id);
-					newElement.html("<th>" + e.name + "<div>[" + e.count + "]</div></th><td>" + (e.mem > 1024 ? Math.fmt(e.mem / 1024) + " MB" : e.mem + " KB") + "</td>");
-					newElement.insertAfter(button.parent());
-				}
-			}
-		#end
-	}
-
-	inline function numberFormat(v : Int) {
-		var tmp = Std.string(v);
-		var n = Math.ceil(tmp.length / 3);
-		var str = "";
-		for( i in 0...n) {
-			if(str != "") str = " " + str;
-			var start = tmp.length - 3 * (i + 1);
-			str = Std.string(tmp.substring(Math.imax(0, start), start + 3)) + str;
-		}
-		return Std.string(str);
-	}
-
-	function syncStats() {
-		var p = J("#stats");
-		if(!p.hasClass("panel")) return;
-
-		var engine = h3d.Engine.getCurrent();
-		p.find("#fps").text(Std.string(engine.fps));
-		p.find("#calls").text(numberFormat(engine.drawCalls));
-		p.find("#tris").text(numberFormat(engine.drawTriangles));
-
-		var bufMem = p.find("#bufMem");
-		var texMem = p.find("#texMem");
-		var totMem = p.find("#totMem");
-		var bufMemTitle = p.find("#bufMemTitle");
-		var texMemTitle = p.find("#texMemTitle");
-		var bufMemCount = p.find("#bufMemCount");
-		var texMemCount = p.find("#texMemCount");
-		var totMemCount = p.find("#totMemCount");
-
-		var stats = engine.mem.stats();
-		var idx = (stats.totalMemory - (stats.textureMemory + stats.managedMemory));
-		var sum : Float = (idx + stats.managedMemory) >> 10;
-		var freeMem : Float = stats.freeManagedMemory >> 10;
-		var totTex : Float = stats.textureMemory >> 10;
-		var totalMem : Float = stats.totalMemory >> 10;
-
-		bufMem.text((sum > 1024 ?  Math.fmt(sum / 1024) + " MB" : totTex + " KB") + " (" + (freeMem > 1024 ?  Math.fmt(freeMem / 1024) + " MB" : freeMem + " KB") + " free)");
-		texMem.text(totTex > 1024 ?  Math.fmt(totTex / 1024) + " MB" : totTex + " KB");
-		totMem.text(totalMem > 1024 ?  Math.fmt(totalMem / 1024) + " MB" : totTex + " KB");
-		bufMemTitle.text("Buffers");
-		bufMemCount.text("[" + Std.string(stats.bufferCount) + "]");
-		texMemTitle.text("Textures");
-		texMemCount.text("[" + Std.string(stats.textureCount) + "]");
-		totMemCount.text("[" + Std.string(stats.bufferCount + stats.textureCount) + "]");
-
-		for( b in p.find("th.button").elements() )
-			showMemoryDetails(b);
-	}
-
-}