瀏覽代碼

started scene editor

ncannasse 8 年之前
父節點
當前提交
2364864f07
共有 9 個文件被更改,包括 582 次插入45 次删除
  1. 33 0
      bin/style.css
  2. 37 0
      bin/style.less
  3. 57 31
      hide/comp/IconTree.hx
  4. 4 1
      hide/comp/Scene.hx
  5. 4 4
      hide/comp/SceneTree.hx
  6. 42 0
      hide/comp/Tabs.hx
  7. 4 1
      hide/ui/Ide.hx
  8. 16 8
      hide/view/FileTree.hx
  9. 385 0
      hide/view/SceneEditor.hx

+ 33 - 0
bin/style.css

@@ -261,6 +261,39 @@ input[type=checkbox]:checked:after {
   margin-left: 5px;
   min-width: 100px;
 }
+.hide-list {
+  width: 100%;
+  height: 200px;
+  background-color: #111;
+  border: 1px solid #444;
+  overflow: auto;
+}
+.hide-block {
+  padding: 10px;
+}
+.hide-tabs {
+  flex: 0 0 320px;
+}
+.hide-tabs > .tabs-header {
+  background-color: #111;
+  padding: 2px;
+  padding-bottom: 0px;
+}
+.hide-tabs > .tabs-header > div {
+  cursor: pointer;
+  display: inline-block;
+  background-color: #202020;
+  padding: 2px 5px;
+  border: 1px solid #444;
+  margin-left: -1px;
+}
+.hide-tabs > .tabs-header > div.active {
+  background-color: #222;
+  border-bottom-color: #222;
+}
+.hide-tabs > .tabs-header > div:hover {
+  background-color: #282828;
+}
 /* Properties */
 .hide-properties {
   flex: 0 0 300px;

+ 37 - 0
bin/style.less

@@ -284,6 +284,43 @@ input[type=checkbox] {
 	}
 }
 
+.hide-list {
+	width : 100%;
+	height : 200px;
+	background-color : #111;
+	border : 1px solid #444;
+	overflow : auto;
+}
+
+.hide-block {
+	padding : 10px;
+}
+
+.hide-tabs {
+	flex : 0 0 320px;
+	&>.tabs-header {
+		background-color : #111;
+		padding : 2px;
+		padding-bottom : 0px;
+		&>div {
+			cursor : pointer;
+			display : inline-block;
+			background-color : #202020;
+			padding : 2px 5px;
+			border : 1px solid #444;
+			margin-left : -1px;
+			&.active {
+				background-color : #222;
+				border-bottom-color : #222;
+			}
+			&:hover {
+				background-color : #282828;
+			}
+		}
+
+	}
+}
+
 /* Properties */
 
 .hide-properties {

+ 57 - 31
hide/comp/IconTree.hx

@@ -1,7 +1,7 @@
 package hide.comp;
 
-typedef IconTreeItem = {
-	var id : String;
+typedef IconTreeItem<T> = {
+	var data : T;
 	var text : String;
 	@:optional var children : Bool;
 	@:optional var icon : String;
@@ -10,23 +10,32 @@ typedef IconTreeItem = {
 		@:optional var selected : Bool;
 		@:optional var disabled : Bool;
 	};
+	@:optional private var id : String; // internal usage
+	@:optional private var absKey : String; // internal usage
 }
 
-class IconTree extends Component {
+class IconTree<T:{}> extends Component {
+
+	static var UID = 0;
 
 	var waitRefresh = new Array<Void->Void>();
+	var map : Map<String, IconTreeItem<T>> = new Map();
+	var revMapString : haxe.ds.StringMap<IconTreeItem<T>> = new haxe.ds.StringMap();
+	var revMap : haxe.ds.ObjectMap<T, IconTreeItem<T>> = new haxe.ds.ObjectMap();
+
+	public var onMenu : Void -> Void;
 
-	public dynamic function get( id : String ) : Array<IconTreeItem> {
-		return [{ id : id+"0", text : "get()", children : true }];
+	public dynamic function get( parent : Null<T> ) : Array<IconTreeItem<T>> {
+		return [{ data : null, text : "get()", children : true }];
 	}
 
-	public dynamic function onClick( id : String ) : Void {
+	public dynamic function onClick( e : T ) : Void {
 	}
 
-	public dynamic function onDblClick( id : String ) : Void {
+	public dynamic function onDblClick( e : T ) : Void {
 	}
 
-	public dynamic function onToggle( id : String, isOpen : Bool ) : Void {
+	public dynamic function onToggle( e : T, isOpen : Bool ) : Void {
 	}
 
 	public function init() {
@@ -38,16 +47,22 @@ class IconTree extends Component {
 					icons: true
             	},
 				data : function(obj, callb) {
-					var parent = obj.parent == null ? null : obj.id;
-					var content : Array<IconTreeItem> = get(parent);
-					for( c in content )
+					var parent = obj.parent == null ? null : map.get(obj.id);
+					var content : Array<IconTreeItem<T>> = get(parent == null ? null : parent.data);
+					for( c in content ) {
+						var key = (parent == null ? "" : parent.absKey + "/") + c.text;
+						if( c.absKey == null ) c.absKey = key;
+						c.id = "titem$" + (UID++);
+						map.set(c.id, c);
+						if( Std.is(c.data, String) )
+							revMapString.set(cast c.data, c);
+						else
+							revMap.set(c.data, c);
 						if( c.state == null ) {
-							var s = getDisplayState((parent == null ? "" : parent + "/") + c.id);
-							if( s != null ) {
-								if( c.state == null ) c.state = {};
-								c.state.opened = s;
-							}
+							var s = getDisplayState(key);
+							if( s != null ) c.state = { opened : s };
 						}
+					}
 					callb.call(this,content);
 				}
 			},
@@ -55,21 +70,23 @@ class IconTree extends Component {
 		});
 		root.on("click.jstree", function (event) {
 			var node = new Element(event.target).closest("li");
-   			var data = node[0].id;
-			onClick(data);
+   			var i = map.get(node[0].id);
+			onClick(i.data);
 		});
 		root.on("dblclick.jstree", function (event) {
 			var node = new Element(event.target).closest("li");
-   			var data = node[0].id;
-			onDblClick(data);
+   			var i = map.get(node[0].id);
+			onDblClick(i.data);
 		});
 		root.on("open_node.jstree", function(event, e) {
-			saveDisplayState(e.node.id, true);
-			onToggle(e.node.id, true);
+			var i = map.get(e.node.id);
+			saveDisplayState(i.absKey, true);
+			onToggle(i.data, true);
 		});
 		root.on("close_node.jstree", function(event,e) {
-			saveDisplayState(e.node.id, false);
-			onToggle(e.node.id, false);
+			var i = map.get(e.node.id);
+			saveDisplayState(i.absKey, false);
+			onToggle(i.data, false);
 		});
 		root.on("refresh.jstree", function(_) {
 			var old = waitRefresh;
@@ -78,15 +95,23 @@ class IconTree extends Component {
 		});
 	}
 
-	public function getCurrentOver() : Null<String> {
+	function getRev( o : T ) {
+		if( Std.is(o, String) )
+			return revMapString.get(cast o);
+		return revMap.get(o);
+	}
+
+	public function getCurrentOver() : Null<T> {
 		var id = root.find(":focus").attr("id");
-		if( id != null )
-			id = id.substr(0, -7); // remove _anchor
-		return id;
+		if( id == null )
+			return null;
+		var i = map.get(id.substr(0, -7)); // remove _anchor
+		return i == null ? null : i.data;
 	}
 
-	public function setSelection( ids : Array<String> ) {
+	public function setSelection( objects : Array<T> ) {
 		(untyped root.jstree)('deselect_all');
+		var ids = [for( o in objects ) { var v = getRev(o); if( v != null ) v.id; }];
 		(untyped root.jstree)('select_node',ids);
 	}
 
@@ -95,8 +120,9 @@ class IconTree extends Component {
 		(untyped root.jstree)('refresh',true);
 	}
 
-	public function getSelection() : Array<String> {
-		return (untyped root.jstree)('get_selected');
+	public function getSelection() : Array<T> {
+		var ids : Array<String> = (untyped root.jstree)('get_selected');
+		return [for( id in ids ) map.get(id).data];
 	}
 
 }

+ 4 - 1
hide/comp/Scene.hx

@@ -267,10 +267,13 @@ class Scene extends Component implements h3d.IDrawable {
 		if( lib.header.animations.length > 0 )
 			anims.push(ide.getPath(path));
 
-		for( dir in dirs )
+		for( dir in dirs ) {
+			var dir = dir;
+			if( StringTools.endsWith(dir, "/") ) dir = dir.substr(0,-1);
 			for( f in try sys.FileSystem.readDirectory(dir) catch( e : Dynamic ) [] )
 				if( StringTools.startsWith(f,"Anim_") )
 					anims.push(dir+"/"+f);
+		}
 		return anims;
 	}
 

+ 4 - 4
hide/comp/SceneTree.hx

@@ -1,6 +1,6 @@
 package hide.comp;
 
-class SceneTree extends IconTree {
+class SceneTree extends IconTree<String> {
 
 	var showRoot : Bool;
 	public var obj : h3d.scene.Object;
@@ -67,11 +67,11 @@ class SceneTree extends IconTree {
 			for( p in parts )
 				root = root.getChildAt(p);
 		}
-		var elements : Array<IconTree.IconTreeItem> = [
+		var elements : Array<IconTree.IconTreeItem<String>> = [
 			for( i in 0...root.numChildren ) {
 				var c = root.getChildAt(i);
 				{
-					id : path+i,
+					data : path+i,
 					text : c.name == null ? c.toString()+"@"+i : c.name,
 					icon : "fa fa-" + getIcon(c),
 					children : c.isMesh() || c.numChildren > 0,
@@ -84,7 +84,7 @@ class SceneTree extends IconTree {
 			for( i in 0...materials.length ) {
 				var m = materials[i];
 				elements.push({
-					id : path+"mat:"+i,
+					data : path+"mat:"+i,
 					text : m.name == null ? "Material@"+i : m.name,
 					icon : "fa fa-photo",
 				});

+ 42 - 0
hide/comp/Tabs.hx

@@ -0,0 +1,42 @@
+package hide.comp;
+
+class Tabs extends Component {
+
+	public var currentTab(default, set) : Element;
+	var header : Element;
+
+	public function new(root) {
+		super(root);
+		root.addClass("hide-tabs");
+		header = new Element("<div>").addClass("tabs-header").prependTo(root);
+		syncTabs();
+		currentTab = new Element(getTabs()[0]);
+	}
+
+	function set_currentTab( e : Element ) {
+		getTabs().hide();
+		e.show();
+		header.children().removeClass("active").filter("[index=" + e.attr("index") + "]").addClass("active");
+		return currentTab = e;
+	}
+
+	function getTabs() : Element {
+		return root.children(".tab");
+	}
+
+	function syncTabs() {
+		header.html("");
+		var index = 0;
+		for( t in getTabs().elements() ) {
+			var icon = t.attr("icon");
+			var title = t.attr("tabtitle");
+			var index = index++;
+			var tab = new Element("<div>").html( (icon != null ? '<div class="fa fa-$icon"></div> ' : '') + (title != null ? title : '') );
+			t.attr("index", index);
+			tab.attr("index", index);
+			tab.appendTo(header);
+			tab.click(function(_) currentTab = t);
+		}
+	}
+
+}

+ 4 - 1
hide/ui/Ide.hx

@@ -323,7 +323,10 @@ class Ide {
 	}
 
 	public function toJSON( v : Dynamic ) {
-		return haxe.Json.stringify(v, "\t");
+		var str = haxe.Json.stringify(v, "\t");
+		str = ~/,\n\t+"__id__": [0-9]+/g.replace(str, "");
+		str = ~/\t+"__id__": [0-9]+,\n/g.replace(str, "");
+		return str;
 	}
 
 	function initMenu() {

+ 16 - 8
hide/view/FileTree.hx

@@ -13,7 +13,7 @@ typedef ExtensionDesc = {
 
 class FileTree extends FileView {
 
-	var tree : hide.comp.IconTree;
+	var tree : hide.comp.IconTree<String>;
 	var lastOpen : hide.ui.View<Dynamic>;
 
 	public function new(state) {
@@ -66,7 +66,7 @@ class FileTree extends FileView {
 		tree.get = function(path) {
 			if( path == null ) path = "";
 			var basePath = getFilePath(path);
-			var content = new Array<hide.comp.IconTree.IconTreeItem>();
+			var content = new Array<hide.comp.IconTree.IconTreeItem<String>>();
 			for( c in sys.FileSystem.readDirectory(basePath) ) {
 				if( isIgnored(basePath, c) ) continue;
 				var fullPath = basePath + "/" + c;
@@ -74,7 +74,7 @@ class FileTree extends FileView {
 				var ext = getExtension(fullPath);
 				var id = ( path == "" ? c : path+"/"+c );
 				content.push({
-					id : id,
+					data : id,
 					text : c,
 					icon : "fa fa-" + (isDir ? "folder" : (ext != null && ext.options.icon != null ? ext.options.icon : "file-text")),
 					children : isDir,
@@ -102,12 +102,12 @@ class FileTree extends FileView {
 		});
 		root.contextmenu(function(e) {
 			var current = tree.getCurrentOver();
-			if( current == null ) return;
-			tree.setSelection([current]);
+			if( current != null )
+				tree.setSelection([current]);
 			e.preventDefault();
 			new hide.comp.ContextMenu([
 				{ label : "New..", menu:[for( e in EXTENSIONS ) if( e.options.createNew != null ) { label : e.options.createNew, click : createNew.bind(current, e) }] },
-				{ label : "Delete", click : function() if( js.Browser.window.confirm("Delete " + current + "?") ) { onDeleteFile(current); tree.refresh(); } },
+				{ label : "Delete", enabled : current != null, click : function() if( js.Browser.window.confirm("Delete " + current + "?") ) { onDeleteFile(current); tree.refresh(); } },
 			]);
 		});
 		tree.onDblClick = onOpenFile;
@@ -125,6 +125,8 @@ class FileTree extends FileView {
 	}
 
 	function getFilePath(path:String) {
+		if( path == "" )
+			return ide.getPath(state.path).substr(0, -1);
 		return ide.getPath(state.path) + path;
 	}
 
@@ -144,11 +146,15 @@ class FileTree extends FileView {
 	}
 
 	function createNew( basePath : String, ext : ExtensionDesc ) {
+		if( basePath == null )
+			basePath = "";
 		var fullPath = getFilePath(basePath);
 		if( !sys.FileSystem.isDirectory(fullPath) ) {
 			basePath = new haxe.io.Path(basePath).dir;
+			if( basePath == null ) basePath = "";
 			fullPath = getFilePath(basePath);
 		}
+
 		var file = ide.ask(ext.options.createNew + " name:");
 		if( file == null ) return;
 		if( file.indexOf(".") < 0 ) file += "." + ext.extensions[0].split(".").shift();
@@ -162,8 +168,10 @@ class FileTree extends FileView {
 		var view : hide.view.FileView = Type.createEmptyInstance(Type.resolveClass(ext.component));
 		view.ide = ide;
 		sys.io.File.saveBytes(fullPath + "/" + file, view.getDefaultContent());
-		tree.refresh(function() tree.setSelection([basePath + "/" + file]));
-		onOpenFile(basePath+"/"+file);
+
+		var fpath = basePath == "" ? file : basePath + "/" + file;
+		tree.refresh(function() tree.setSelection([fpath]));
+		onOpenFile(fpath);
 	}
 
 	function isIgnored( path : String, file : String ) {

+ 385 - 0
hide/view/SceneEditor.hx

@@ -0,0 +1,385 @@
+package hide.view;
+import hxd.fmt.s3d.Data;
+
+class SceneEditor extends FileView {
+
+	var content : hxd.fmt.s3d.Library;
+	var objRoot : h3d.scene.Object;
+	var tabs : hide.comp.Tabs;
+
+	var tools : hide.comp.Toolbar;
+	var scene : hide.comp.Scene;
+	var control : h3d.scene.CameraController;
+	var properties : hide.comp.PropsEditor;
+	var light : h3d.scene.DirLight;
+	var lightDirection = new h3d.Vector( 1, 2, -4 );
+	var tree : hide.comp.IconTree<BaseObject>;
+
+	override function getDefaultContent() {
+		return haxe.io.Bytes.ofString(ide.toJSON(new hxd.fmt.s3d.Library().save()));
+	}
+
+	override function save() {
+		sys.io.File.saveContent(getPath(), ide.toJSON(content.save()));
+	}
+
+	override function onDisplay() {
+
+		root.html('
+			<div class="flex vertical">
+				<div class="toolbar"></div>
+				<div class="flex">
+					<div class="scene">
+					</div>
+					<div class="tabs">
+						<div class="tab" tabtitle="Scene" icon="sitemap">
+							<div class="hide-block">
+								<div class="hide-list">
+									<div class="tree"></div>
+								</div>
+							</div>
+							<div class="props"></div>
+						</div>
+						<div class="tab" tabtitle="Properties" icon="gears">
+						</div>
+					</div>
+				</div>
+			</div>
+		');
+		tools = new hide.comp.Toolbar(root.find(".toolbar"));
+		tabs = new hide.comp.Tabs(root.find(".tabs"));
+		properties = new hide.comp.PropsEditor(root.find(".props"), undo);
+		scene = new hide.comp.Scene(root.find(".scene"));
+		scene.onReady = init;
+		tree = new hide.comp.IconTree(root.find(".tree"));
+	}
+
+	function refresh( ?callb ) {
+		objRoot.remove();
+		objRoot = content.makeInstance();
+		scene.s3d.addChild(objRoot);
+		scene.init(props);
+		tree.refresh(callb);
+	}
+
+	function allocName( prefix : String ) {
+		var id = 0;
+		while( objRoot.getObjectByName(prefix + id) != null )
+			id++;
+		return prefix + id;
+	}
+
+	function selectObject( elt : BaseObject ) {
+		var obj = objRoot.getObjectByName(elt.name);
+
+		properties.clear();
+
+		if( obj != null )
+			properties.add(new Element('
+				<dl>
+					<dt>Name</dt><dd><input field="name"></dd>
+					<dt>X</dt><dd><input field="x"/></dd>
+					<dt>Y</dt><dd><input field="y"/></dd>
+					<dt>Z</dt><dd><input field="z"/></dd>
+					<dt>ScaleX</dt><dd><input field="scaleX"/></dd>
+					<dt>ScaleY</dt><dd><input field="scaleY"/></dd>
+					<dt>ScaleZ</dt><dd><input field="scaleZ"/></dd>
+					<dt>Visible</dt><dd><input type="checkbox" field="visible"/></dd>
+				</dl>
+			'),obj, function(name) {
+				elt.x = obj.x;
+				elt.y = obj.y;
+				elt.z = obj.z;
+				elt.name = obj.name;
+				elt.scaleX = obj.scaleX;
+				elt.scaleY = obj.scaleY;
+				elt.scaleZ = obj.scaleZ;
+				if( elt.x == 0 ) Reflect.deleteField(elt,"x");
+				if( elt.y == 0 ) Reflect.deleteField(elt,"y");
+				if( elt.z == 0 ) Reflect.deleteField(elt,"z");
+				if( elt.scaleX == 1 ) Reflect.deleteField(elt, "scaleX");
+				if( elt.scaleY == 1 ) Reflect.deleteField(elt, "scaleY");
+				if( elt.scaleZ == 1 ) Reflect.deleteField(elt, "scaleZ");
+				if( name == "name" )
+					tree.refresh();
+			});
+
+		switch( elt.type ) {
+		case Object:
+			var elt : ObjectProperties = cast elt;
+			var props = properties.add(new Element('
+				<dl>
+					<dt>Animation</dt><dd><select><option value="">-- Choose --</option></select>
+					<dt title="Don\'t save animation changes">Lock</dt><dd><input type="checkbox" field="lock"></select>
+				</dl>
+			'),elt);
+
+			var select = props.find("select");
+			var anims = scene.listAnims(elt.modelPath);
+			for( a in anims )
+				new Element('<option>').attr("value", a).text(scene.animationName(a)).appendTo(select);
+			if( elt.animationPath != null ) select.val(ide.getPath(elt.animationPath));
+			select.change(function(_) {
+				var v = select.val();
+				var prev = elt.animationPath;
+				if( v == "" ) {
+					elt.animationPath = null;
+					obj.stopAnimation();
+				} else {
+					obj.playAnimation(scene.loadAnimation(v)).loop = true;
+					if( elt.lock ) return;
+					elt.animationPath = ide.makeRelative(v);
+				}
+				var newValue = elt.animationPath;
+				undo.change(Custom(function(undo) {
+					elt.animationPath = undo ? prev : newValue;
+					if( elt.animationPath == null ) {
+						obj.stopAnimation();
+						select.val("");
+					} else {
+						obj.playAnimation(scene.loadAnimation(v)).loop = true;
+						select.val(v);
+					}
+				}));
+			});
+
+		case Constraint:
+			var elt : ConstraintProperties = cast elt;
+			var props = properties.add(new Element('
+				<dl>
+					<dt>Name</dt><dd><input field="name"/></dd>
+					<dt>Source</dt><dd><select field="source"><option value="">-- Choose --</option></select>
+					<dt>Target</dt><dd><select field="attach"><option value="">-- Choose --</option></select>
+				</dl>
+			'),elt, function(field) if( field == "name" ) tree.refresh() else refresh());
+
+			for( select in [props.find("[field=source]"), props.find("[field=attach]")] ) {
+				for( path in getNamedObjects() ) {
+					var parts = path.split(".");
+					var opt = new Element("<option>").attr("value", path).html([for( p in 1...parts.length ) "&nbsp; "].join("") + parts.pop());
+					select.append(opt);
+				}
+				select.val(Reflect.field(elt, select.attr("field")));
+			}
+
+		default:
+		}
+	}
+
+	function getNamedObjects( ?exclude : h3d.scene.Object ) {
+		var out = [];
+
+		function getJoint(path:Array<String>,j:h3d.anim.Skin.Joint) {
+			path.push(j.name);
+			out.push(path.join("."));
+			for( j in j.subs )
+				getJoint(path, j);
+			path.pop();
+		}
+
+		function getRec(path:Array<String>, o:h3d.scene.Object) {
+			if( o == exclude || o.name == null ) return;
+			path.push(o.name);
+			out.push(path.join("."));
+			for( c in o )
+				getRec(path, c);
+			var sk = Std.instance(o, h3d.scene.Skin);
+			if( sk != null ) {
+				var j = sk.getSkinData();
+				for( j in j.rootJoints )
+					getJoint(path, j);
+			}
+			path.pop();
+		}
+
+		for( o in objRoot )
+			getRec([], o);
+
+		return out;
+	}
+
+	function init() {
+		content = new hxd.fmt.s3d.Library();
+		content.load(haxe.Json.parse(sys.io.File.getContent(getPath())));
+		objRoot = content.makeInstance();
+		scene.s3d.addChild(objRoot);
+
+		light = scene.s3d.find(function(o) return Std.instance(o, h3d.scene.DirLight));
+		if( light == null ) {
+			light = new h3d.scene.DirLight(new h3d.Vector(), scene.s3d);
+			light.enableSpecular = true;
+		} else
+			light = null;
+
+		control = new h3d.scene.CameraController(scene.s3d);
+
+		this.saveDisplayKey = "Scene:" + state.path;
+
+		var cam = getDisplayState("Camera");
+		if( cam == null )
+			scene.resetCamera(scene.s3d, 1.5);
+		else {
+			scene.s3d.camera.pos.set(cam.x, cam.y, cam.z);
+			scene.s3d.camera.target.set(cam.tx, cam.ty, cam.tz);
+		}
+		control.loadFromCamera();
+
+		scene.onUpdate = update;
+		scene.init(props);
+		tools.saveDisplayKey = "SceneTools";
+
+		tools.addButton("video-camera", "Reset Camera", function() {
+			scene.resetCamera(objRoot,1.5);
+			control.loadFromCamera();
+		});
+
+		tools.addToggle("sun-o", "Enable Lights/Shadows", function(v) {
+			if( !v ) {
+				for( m in objRoot.getMaterials() ) {
+					m.mainPass.enableLights = false;
+					m.shadows = false;
+				}
+			} else {
+				for( m in objRoot.getMaterials() )
+					h3d.mat.MaterialSetup.current.initModelMaterial(m);
+			}
+		},true);
+
+		tools.addColor("Background color", function(v) {
+			scene.engine.backgroundColor = v;
+		}, scene.engine.backgroundColor);
+
+		// BUILD scene tree
+
+		function makeItem(o:BaseObject) : hide.comp.IconTree.IconTreeItem<BaseObject> {
+			return {
+				data : o,
+				text : o.name,
+				icon : "fa fa-"+switch( o.type ) {
+				case Object: "cube";
+				case Constraint: "lock";
+				case Particles: "snowflake-o";
+				case Trail: "toggle-on";
+				},
+				children : o.children != null && o.children.length > 0,
+				state : { opened : true },
+			};
+		}
+		tree.get = function(o:BaseObject) {
+			var objs = o == null ? content.data.content : o.children;
+			return [for( o in objs ) makeItem(o)];
+		};
+		tree.root.parent().contextmenu(function(e) {
+			e.preventDefault();
+			var current = tree.getCurrentOver();
+			tree.setSelection(current == null ? [] : [current]);
+
+			new hide.comp.ContextMenu([
+				{ label : "New...", menu : [
+					{ label : "Model", click : function() {
+						ide.chooseFile(["fbx", "hmd"], function(path) {
+							if( path == null ) return;
+							var props : ObjectProperties = {
+								type : Object,
+								name : allocName("Object"),
+								modelPath : path,
+							};
+							addObject(props, current);
+						});
+					} },
+					{ label : "Particles", click : function() {
+						var parts = new h3d.parts.GpuParticles();
+						parts.addGroup();
+						var props : ExtraProperties = {
+							type : Particles,
+							name : allocName("Particles"),
+							data : parts.save(),
+						};
+						addObject(props, current);
+					} },
+					{ label : "Trail", click : function() {
+						var props : ExtraProperties = {
+							type : Trail,
+							name : allocName("Trail"),
+							data : new h3d.scene.Trail().save(),
+						};
+						addObject(props, current);
+					} },
+					{ label : "Constraint", click : function() {
+						var props : ConstraintProperties = {
+							type : Constraint,
+							name : allocName("Constraint"),
+							source : "",
+							attach : "",
+						};
+						addObject(props, current);
+					} },
+				] },
+				{ label : "Delete", enabled : current != null, click : function() {
+					function deleteRec(roots:Array<BaseObject>) {
+						for( o in roots ) {
+							if( o == current ) {
+								properties.clear();
+								var index = roots.indexOf(o);
+								roots.remove(o);
+								undo.change(Custom(function(undo) {
+									if( undo ) roots.insert(index, o) else roots.remove(o);
+									refresh();
+								}));
+								refresh();
+								return;
+							}
+							if( o.children != null ) deleteRec(o.children);
+						}
+					}
+					deleteRec(content.data.content);
+				} },
+			]);
+		});
+		tree.init();
+		tree.onClick = selectObject;
+	}
+
+	function addObject( props : BaseObject, parent : BaseObject ) {
+		var roots = content.data.content;
+		if( parent != null ) {
+			roots = parent.children;
+			if( roots == null ) parent.children = roots = [];
+		}
+		roots.push(props);
+		undo.change(Custom(function(undo) {
+			if( undo ) {
+				roots.remove(props);
+				if( roots.length == 0 && parent != null ) Reflect.deleteField(parent, "children");
+			} else {
+				roots.push(props);
+				if( parent != null ) parent.children = roots;
+			}
+			refresh();
+		}));
+		refresh(function() {
+			tree.setSelection([props]);
+			selectObject(props);
+		});
+		if( parent == null && roots.length == 1 ) {
+			scene.resetCamera(objRoot, 1.5);
+			control.loadFromCamera();
+		}
+	}
+
+	function update(dt:Float) {
+		var cam = scene.s3d.camera;
+		saveDisplayState("Camera", { x : cam.pos.x, y : cam.pos.y, z : cam.pos.z, tx : cam.target.x, ty : cam.target.y, tz : cam.target.z });
+		if( light != null ) {
+			var angle = Math.atan2(cam.target.y - cam.pos.y, cam.target.x - cam.pos.x);
+			light.direction.set(
+				Math.cos(angle) * lightDirection.x - Math.sin(angle) * lightDirection.y,
+				Math.sin(angle) * lightDirection.x + Math.cos(angle) * lightDirection.y,
+				lightDirection.z
+			);
+		}
+	}
+
+	static var _ = FileTree.registerExtension(SceneEditor,["s3d"],{ icon : "sitemap", createNew : "Scene 3D" });
+
+}