Ver código fonte

Merge branch 'fxeditor'
- Extract SceneEditor functionality into separate component
- Add Curve and CurveEditor
- FXScene WIP

trethaller 7 anos atrás
pai
commit
f3ec380fd3

+ 2 - 1
bin/package.json

@@ -7,5 +7,6 @@
 		"height" : 600,
 		"show" : true
 	},
-	"main" : "app.html"
+	"main" : "app.html",
+	"js-flags": "--expose-gc"
 }

+ 69 - 0
bin/style.css

@@ -440,6 +440,75 @@ input[type=checkbox]:checked:after {
 .hide-properties .group > .content {
   margin-bottom: 5px;
 }
+/* Curve editor */
+.hide-curve-editor {
+  border: 1px solid black;
+}
+.hide-curve-editor line {
+  stroke-width: 1px;
+}
+.hide-curve-editor rect {
+  shape-rendering: crispEdges;
+}
+.hide-curve-editor .grid line {
+  shape-rendering: crispEdges;
+}
+.hide-curve-editor .grid .hgrid line {
+  stroke: #444;
+}
+.hide-curve-editor .grid .vgrid line {
+  stroke: #444;
+}
+.hide-curve-editor .grid line.axis {
+  stroke: #747474;
+  shape-rendering: crispEdges;
+}
+.hide-curve-editor .graph .selection rect {
+  fill: #ffffff;
+  fill-opacity: 0.1;
+  stroke: #ffffff;
+  opacity: 0.3;
+  mix-blend-mode: screen;
+}
+.hide-curve-editor .graph .curve line,
+.hide-curve-editor .graph path {
+  stroke: #ff1f1f;
+  stroke-width: 1px;
+  fill: none;
+}
+.hide-curve-editor .graph .vectors line {
+  stroke: rgba(207, 207, 207, 0.603);
+}
+.hide-curve-editor .graph .vectors line.selected {
+  stroke: #ffffff;
+}
+.hide-curve-editor .graph .handles circle,
+.hide-curve-editor .graph .handles rect {
+  fill: rgba(187, 187, 187, 0.644);
+}
+.hide-curve-editor .graph .handles circle:hover,
+.hide-curve-editor .graph .handles rect:hover {
+  fill: #fff;
+  stroke: #fff;
+  stroke-width: 2px;
+}
+.hide-curve-editor .graph .handles circle.selected,
+.hide-curve-editor .graph .handles circle:hover.selected,
+.hide-curve-editor .graph .handles rect.selected,
+.hide-curve-editor .graph .handles rect:hover.selected {
+  fill: #ffffff;
+}
+.hide-curve-editor .selection-overlay rect {
+  mix-blend-mode: multiply;
+  fill: #888;
+  fill-opacity: 20px;
+  stroke: #000;
+  opacity: 50%;
+}
+/* FX Editor */
+.fx-animpanel {
+  flex-basis: 300px;
+}
 /* Golden Layout Fixes */
 .lm_header .lm_tabs {
   z-index: 1;

+ 81 - 0
bin/style.less

@@ -481,6 +481,87 @@ input[type=checkbox] {
 
 }
 
+/* Curve editor */
+.hide-curve-editor {
+	@selectCol: rgb(255, 255, 255);
+	@lineCol: rgb(255, 31, 31);
+
+	border: 1px solid black;
+	line {
+		stroke-width: 1px;
+	}
+
+	rect {
+		shape-rendering: crispEdges;
+	}
+
+	.grid {
+		line {
+			shape-rendering: crispEdges;
+		}
+		.hgrid line { stroke: #444; }
+		.vgrid line { stroke: #444; }
+		line.axis {
+			stroke: rgb(116, 116, 116);
+			shape-rendering: crispEdges;
+		}
+	}
+
+	.graph {
+
+		.selection rect {
+			fill: @selectCol;
+			fill-opacity: 0.1;
+			stroke: rgb(255, 255, 255);
+			opacity: 0.3;
+			mix-blend-mode: screen;
+		}
+
+		.curve line, path {
+			stroke: @lineCol;
+			stroke-width: 1px;
+			fill: none;
+		}
+
+		.vectors {
+			line {
+				stroke: rgba(207, 207, 207, 0.603);
+			}
+
+			line.selected {
+				stroke: @selectCol;
+			}
+		}
+
+		.handles {	
+			circle, rect {
+				fill: rgba(187, 187, 187, 0.644);
+			}
+			circle:hover, rect:hover {
+				fill: #fff;
+				stroke: #fff;
+				stroke-width: 2px;
+			}
+			circle.selected, circle:hover.selected, rect.selected, rect:hover.selected {
+				fill: @selectCol;
+			}
+		}
+	}
+
+	.selection-overlay rect {
+		mix-blend-mode: multiply;
+		fill: #888;
+		fill-opacity: 20px;
+		stroke: #000;
+		opacity: 50%;
+	}
+}
+
+/* FX Editor */
+.fx-animpanel {
+	flex-basis: 300px;
+}
+
 /* Golden Layout Fixes */
 
 .lm_header .lm_tabs {

+ 475 - 0
hide/comp/CurveEditor.hx

@@ -0,0 +1,475 @@
+package hide.comp;
+
+class CurveEditor extends Component {
+
+	public var xScale = 200.;
+	public var yScale = 30.;
+	public var xOffset = 0.;
+	public var yOffset = 0.;
+
+	public var curve : hide.prefab.Curve;
+	public var undo : hide.ui.UndoHistory;
+
+	var svg : hide.comp.SVG;
+	var width = 0;
+	var height = 0;
+	var gridGroup : Element;
+	var graphGroup : Element;
+	var selectGroup : Element;
+
+	var refreshTimer : haxe.Timer = null;
+	var lastValue : Dynamic;
+
+	var selectedKeys: Array<hide.prefab.Curve.CurveKey> = [];
+
+	public function new(parent, curve : hide.prefab.Curve, undo) {
+		super(parent);
+		this.undo = undo;
+		this.curve = curve;
+		var div = new Element("<div></div>");
+		div.attr({ tabindex: "1" });
+		div.css({ width: "100%", height: "100%" });
+
+		div.appendTo(parent);
+		div.focus();
+		svg = new hide.comp.SVG(div);
+		var root = svg.element;
+
+		lastValue = haxe.Json.parse(haxe.Json.stringify(curve.save()));
+
+		gridGroup = svg.group(root, "grid");
+		graphGroup = svg.group(root, "graph");
+		selectGroup = svg.group(root, "selection-overlay");
+
+		root.resize((e) -> refresh());
+		root.addClass("hide-curve-editor");
+		root.mousedown(function(e) {
+			var offset = root.offset();
+			var px = e.clientX - offset.left;
+			var py = e.clientY - offset.top;
+			e.preventDefault();
+			e.stopPropagation();
+			div.focus();
+			if(e.which == 1) {
+				if(e.ctrlKey) {
+					addPoint(ixt(px), iyt(py));
+				}
+				else {
+					startSelectRect(px, py);
+				}
+			}
+			else if(e.which == 2) {
+				// Pan
+				startPan(e);
+			}
+		});
+		root.contextmenu(function(e) {
+			e.preventDefault();
+			return false;
+		});
+		root.on("mousewheel", function(e) {
+			var step = e.originalEvent.wheelDelta > 0 ? 1.0 : -1.0;
+			if(hxd.Key.isDown(hxd.Key.SHIFT))
+				yScale *= Math.pow(1.125, step);
+			else
+				xScale *= Math.pow(1.125, step);
+			refresh();
+		});
+		div.keydown(function(e) {
+			if(e.keyCode == 46) {
+				var newVal = [for(k in curve.keys) if(selectedKeys.indexOf(k) < 0) k];
+				curve.keys = newVal;
+				selectedKeys = [];
+				e.preventDefault();
+				e.stopPropagation();
+				afterChange();
+			}
+		});
+	}
+
+	function addPoint(time: Float, ?val: Float) {
+		var index = 0;
+		for(ik in 0...curve.keys.length) {
+			var key = curve.keys[ik];
+			if(time > key.time)
+				index = ik + 1;
+		}
+
+		if(val == null)
+			val = curve.getVal(time);
+
+		var key : hide.prefab.Curve.CurveKey = {
+			time: time,
+			value: val,
+			mode: Linear
+		};
+		curve.keys.insert(index, key);
+		afterChange();
+	}
+
+	function fixKey(key : hide.prefab.Curve.CurveKey) {
+		var index = curve.keys.indexOf(key);
+		var prev = curve.keys[index-1];
+		var next = curve.keys[index+1];
+
+		inline function addPrevH() {
+			if(key.prevHandle == null)
+				key.prevHandle = { dt: prev != null ? (prev.time - key.time) / 3 : -0.5, dv: 0};
+		}
+		inline function addNextH() {
+			if(key.nextHandle == null)
+				key.nextHandle = { dt: next != null ? (next.time - key.time) / 3 : -0.5, dv: 0};
+		}
+		switch(key.mode) {
+			case Aligned:
+				addPrevH();
+				addNextH();
+				var pa = hxd.Math.atan2(key.prevHandle.dv, key.prevHandle.dt);
+				var na = hxd.Math.atan2(key.nextHandle.dv, key.nextHandle.dt);
+				if(hxd.Math.abs(hxd.Math.angle(pa - na)) < Math.PI - (1./180.)) {
+					key.nextHandle.dt = -key.prevHandle.dt;
+					key.nextHandle.dv = -key.prevHandle.dv;
+				}
+			case Free:
+				addPrevH();
+				addNextH();
+			case Linear:
+				key.nextHandle = null;
+				key.prevHandle = null;
+			case Constant:
+				key.nextHandle = null;
+				key.prevHandle = null;
+		}
+
+		if(key.time < 0)
+			key.time = 0;
+
+		if(prev != null && key.time < prev.time)
+			key.time = prev.time + 0.01;
+		if(next != null && key.time > next.time)
+			key.time = next.time - 0.01;
+
+		if(next != null && key.nextHandle != null) {
+			var slope = key.nextHandle.dv / key.nextHandle.dt;
+			slope = hxd.Math.clamp(slope, -1000, 1000);
+			if(key.nextHandle.dt + key.time > next.time) {
+				key.nextHandle.dt = next.time - key.time;
+				key.nextHandle.dv = slope * key.nextHandle.dt;
+			}
+		}
+		if(prev != null && key.prevHandle != null) {
+			var slope = key.prevHandle.dv / key.prevHandle.dt;
+			slope = hxd.Math.clamp(slope, -1000, 1000);
+			if(key.prevHandle.dt + key.time < prev.time) {
+				key.prevHandle.dt = prev.time - key.time;
+				key.prevHandle.dv = slope * key.prevHandle.dt;
+			}
+		}
+	}
+
+	function startSelectRect(p1x: Float, p1y: Float) {
+		var offset = root.offset();
+		var selX = p1x;
+		var selY = p1y;
+		var selW = 0.;
+		var selH = 0.;
+		startDrag(root, function(e) {
+			var p2x = e.clientX - offset.left;
+			var p2y = e.clientY - offset.top;
+			selX = hxd.Math.min(p1x, p2x);
+			selY = hxd.Math.min(p1y, p2y);
+			selW = hxd.Math.abs(p2x-p1x);
+			selH = hxd.Math.abs(p2y-p1y);
+			selectGroup.empty();
+			svg.rect(selectGroup, selX, selY, selW, selH);
+		}, function(e) {
+			selectGroup.empty();
+			var minT = ixt(selX);
+			var minV = iyt(selY);
+			var maxT = ixt(selX + selW);
+			var maxV = iyt(selY + selH);
+			selectedKeys = [for(key in curve.keys)
+				if(key.time >= minT && key.time <= maxT && key.value >= minV && key.value <= maxV) key];
+			refresh();
+		});
+	}
+
+	function startPan(e) {
+		var lastX = e.clientX;
+		var lastY = e.clientY;
+		startDrag(root, function(e) {
+			var dt = (e.clientX - lastX) / xScale;
+			var dv = (e.clientY - lastY) / yScale;
+			xOffset += dt;
+			yOffset += dv;
+			lastX = e.clientX;
+			lastY = e.clientY;
+			refresh(true);
+		}, function(e) {
+			refresh();
+		});
+	}
+
+	inline function xt(x: Float) return Math.round((x + xOffset) * xScale);
+	inline function yt(y: Float) return Math.round((y + yOffset) * yScale + height/2);
+	inline function ixt(px: Float) return px / xScale - xOffset;
+	inline function iyt(py: Float) return (py - height/2) / yScale - yOffset;
+
+	function startDrag(el: Element, onMove, onStop) {
+		el.mousemove(onMove);
+		el.mouseup(function(e) {
+			el.off("mousemove");
+			el.off("mouseup");
+			e.preventDefault();
+			e.stopPropagation();
+			onStop(e);
+		});
+	}
+
+	function copyKey(key: hide.prefab.Curve.CurveKey): hide.prefab.Curve.CurveKey {
+		return cast haxe.Json.parse(haxe.Json.stringify(key));
+	}
+
+	function afterChange() {
+		var newVal = haxe.Json.parse(haxe.Json.stringify(curve.save()));
+		var oldVal = lastValue;
+		lastValue = newVal;
+		undo.change(Custom(function(undo) {
+			if(undo) {
+				curve.load(oldVal);
+			}
+			else {
+				curve.load(newVal);
+			}
+			lastValue = haxe.Json.parse(haxe.Json.stringify(curve.save()));
+			selectedKeys = [];
+			refresh();
+		}));
+		refresh();
+	}
+
+	public function resetView() {
+		// var margin = 20;
+		// var minT = ixt(-margin);
+		// var maxT = ixt(width + margin);
+		// var minV = iyt(0);
+		// var maxV = iyt(height);
+		// xOffset = minT;
+		// xScale = (maxT - minT)
+		// TODO
+	}
+
+	public function refresh(?anim: Bool = false, ?animKey: hide.prefab.Curve.CurveKey) {
+		width = Math.round(svg.element.width());
+		height = Math.round(svg.element.height());
+		gridGroup.empty();
+		graphGroup.empty();
+		selectGroup.empty();
+
+		if(refreshTimer != null)
+			refreshTimer.stop();
+		if(!anim) {
+			refreshTimer = haxe.Timer.delay(function() {
+				refreshTimer = null;
+				untyped window.gc();
+			}, 100);
+		}
+
+		var minX = Math.floor(ixt(0));
+		var maxX = Math.ceil(ixt(width));
+		var hgrid = svg.group(gridGroup, "hgrid");
+		for(ix in minX...(maxX+1)) {
+			var l = svg.line(hgrid, xt(ix), 0, xt(ix), height).attr({
+				"shape-rendering": "crispEdges"
+			});
+			if(ix == 0)
+				l.addClass("axis");
+		}
+
+		var minY = Math.floor(iyt(0));
+		var maxY = Math.ceil(iyt(height));
+		var vgrid = svg.group(gridGroup, "vgrid");
+		for(iy in minY...(maxY+1)) {
+			var l = svg.line(vgrid, 0, yt(iy), width, yt(iy)).attr({
+				"shape-rendering": "crispEdges"
+			});
+			if(iy == 0)
+				l.addClass("axis");
+		}
+
+		var curveGroup = svg.group(graphGroup, "curve");
+		var vectorsGroup = svg.group(graphGroup, "vectors");
+		var handlesGroup = svg.group(graphGroup, "handles");
+		var tangentsHandles = svg.group(handlesGroup, "tangents");
+		var keyHandles = svg.group(handlesGroup, "keys");
+		var selection = svg.group(graphGroup, "selection");
+		var size = 7;
+
+		// Draw curve
+		{
+			var keys = curve.keys;
+			var lines = ['M ${xt(keys[0].time)},${yt(keys[0].value)}'];
+			for(ik in 1...keys.length) {
+				var prev = keys[ik-1];
+				var cur = keys[ik];
+				lines.push('C
+					${xt(prev.time + (prev.nextHandle != null ? prev.nextHandle.dt : 0.))}, ${yt(prev.value + (prev.nextHandle != null ? prev.nextHandle.dv : 0.))}
+					${xt(cur.time + (cur.prevHandle != null ? cur.prevHandle.dt : 0.))}, ${yt(cur.value + (cur.prevHandle != null ? cur.prevHandle.dv : 0.))}
+					${xt(cur.time)}, ${yt(cur.value)} ');
+			}
+			svg.make(curveGroup, "path", {d: lines.join("")});
+			// var pts = curve.sample(200);
+			// var poly = [];
+			// for(i in 0...pts.length) {
+			// 	var x = xt(curve.duration * i / (pts.length - 1));
+			// 	var y = yt(pts[i]);
+			// 	poly.push(new h2d.col.Point(x, y));
+			// }
+			// svg.polygon(curveGroup, poly);
+		}
+
+
+		function addRect(group, x: Float, y: Float) {
+			return svg.rect(group, x - Math.floor(size/2), y - Math.floor(size/2), size, size).attr({
+				"shape-rendering": "crispEdges"
+			});
+		}
+
+		for(key in curve.keys) {
+			var kx = xt(key.time);
+			var ky = yt(key.value);
+			var keyHandle = addRect(keyHandles, kx, ky);
+			var selected = selectedKeys.indexOf(key) >= 0;
+			if(selected)
+				keyHandle.addClass("selected");
+			if(!anim) {
+				keyHandle.mousedown(function(e) {
+					if(e.which != 1) return;
+					e.preventDefault();
+					e.stopPropagation();
+					var offx = e.clientX - keyHandle.offset().left;
+					var offy = e.clientY - keyHandle.offset().top;
+					var offset = svg.element.offset();
+					startDrag(root, function(e) {
+						var lx = e.clientX - offset.left - offx;
+						var ly = e.clientY - offset.top - offy;
+						var nkx = ixt(lx);
+						var nky = iyt(ly);
+						key.time = nkx;
+						key.value = nky;
+						fixKey(key);
+						refresh(true, key);
+					}, function(e) {
+						selectedKeys = [key];
+						fixKey(key);
+						afterChange();
+					});
+					selectedKeys = [key];
+					refresh();
+				});
+				keyHandle.contextmenu(function(e) {
+					e.preventDefault();
+					function setMode(m: hide.prefab.Curve.CurveKeyMode) {
+						key.mode = m;
+						fixKey(key);
+						refresh();
+					}
+					new ContextMenu([
+						{ label : "Mode", menu :[
+							{ label : "Aligned", checked: key.mode == Aligned, click : setMode.bind(Aligned) },
+							{ label : "Free", checked: key.mode == Free, click : setMode.bind(Free) },
+							{ label : "Linear", checked: key.mode == Linear, click : setMode.bind(Linear) },
+							{ label : "Constant", checked: key.mode == Constant, click : setMode.bind(Constant) },
+						] }
+					]);
+					return false;
+				});
+			}
+			function addHandle(next: Bool) {
+				var handle = next ? key.nextHandle : key.prevHandle;
+				var other = next ? key.prevHandle : key.nextHandle;
+				if(handle == null) return null;
+				var px = xt(key.time + handle.dt);
+				var py = yt(key.value + handle.dv);
+				var line = svg.line(vectorsGroup, kx, ky, px, py);
+				var circle = svg.circle(tangentsHandles, px, py, size/2);
+				if(selected) {
+					line.addClass("selected");
+					circle.addClass("selected");
+				}
+				if(anim)
+					return circle;
+				circle.mousedown(function(e) {
+					if(e.which != 1) return;
+					e.preventDefault();
+					e.stopPropagation();
+					var offx = e.clientX - circle.offset().left;
+					var offy = e.clientY - circle.offset().top;
+					var offset = svg.element.offset();
+					var otherLen = hxd.Math.distance(other.dt * xScale, other.dv * yScale);
+					startDrag(root, function(e) {
+						var lx = e.clientX - offset.left - offx;
+						var ly = e.clientY - offset.top - offy;
+						if(next && lx < kx || !next && lx > kx)
+							lx = kx;
+						var ndt = ixt(lx) - key.time;
+						var ndv = iyt(ly) - key.value;
+						handle.dt = ndt;
+						handle.dv = ndv;
+						if(key.mode == Aligned) {
+							var angle = Math.atan2(ly - ky, lx - kx);
+							other.dt = Math.cos(angle + Math.PI) * otherLen / xScale;
+							other.dv = Math.sin(angle + Math.PI) * otherLen / yScale;
+						}
+						fixKey(key);
+						refresh(true, key);
+					}, function(e) {
+						afterChange();
+					});
+				});
+				return circle;
+			}
+			if(!anim || animKey == key) {
+				var pHandle = addHandle(false);
+				var nHandle = addHandle(true);
+			}
+		}
+
+		if(selectedKeys.length > 1) {
+			var bounds = new h2d.col.Bounds();
+			for(key in selectedKeys)
+				bounds.addPoint(new h2d.col.Point(xt(key.time), yt(key.value)));
+			var margin = 12.5;
+			bounds.xMin -= margin;
+			bounds.yMin -= margin;
+			bounds.xMax += margin;
+			bounds.yMax += margin;
+			var rect = svg.rect(selection, bounds.x, bounds.y, bounds.width, bounds.height).attr({
+				"shape-rendering": "crispEdges"
+			});
+			if(!anim) {
+				rect.mousedown(function(e) {
+					if(e.which != 1) return;
+					e.preventDefault();
+					e.stopPropagation();
+					var lastX = e.clientX;
+					var lastY = e.clientY;
+					startDrag(root, function(e) {
+						var dx = e.clientX - lastX;
+						var dy = e.clientY - lastY;
+						for(key in selectedKeys) {
+							key.time += dx / xScale;
+							key.value += dy / yScale;
+						}
+						lastX = e.clientX;
+						lastY = e.clientY;
+						refresh(true);
+					}, function(e) {
+						afterChange();
+					});
+					refresh();
+				});
+			}
+		}
+	}
+}

+ 73 - 0
hide/comp/SVG.hx

@@ -0,0 +1,73 @@
+package hide.comp;
+import hide.Element;
+
+class SVG extends Component {
+
+	var document = null;
+	public var element(default, null) : hide.Element = null;
+
+	public function new(root: hide.Element) {
+		super(root);
+		document = root[0].ownerDocument;
+
+		var e = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+		element = new Element(e);
+		element.attr("width", "100%");
+		element.attr("height", "100%");
+		root.append(element);
+	}
+
+	public function clear() {
+		element.empty();
+	}
+
+	public function add(el: Element) {
+		element.append(el);
+	}
+
+	public function make(?parent: Element, name: String, ?attr: Dynamic, ?style: Dynamic) {
+		var e = document.createElementNS('http://www.w3.org/2000/svg', name);
+		var el = new Element(e);
+		if(attr != null)
+			el.attr(attr);
+		if(style != null)
+			el.css(style);
+		if(parent != null)
+			parent.append(el);
+		return el;
+	}
+
+	public function circle(?parent: Element, x:Float, y:Float, radius:Float, ?style:Dynamic) {
+		return make(parent, "circle", {cx:x, cy:y, r:radius}, style);
+	}
+
+	public function rect(?parent: Element, x:Float, y:Float, width:Float, height:Float, ?style:Dynamic) {
+		return make(parent, "rect", {x:x, y:y, width:width, height:height}, style);
+	}
+
+	public function line(?parent: Element, x1:Float, y1:Float, x2:Float, y2:Float, ?style:Dynamic) {
+		return make(parent, "line", {x1:x1, y1:y1, x2:x2, y2:y2}, style);
+	}
+
+	public function polygon(?parent: Element, points: Array<h2d.col.Point>, ?style:Dynamic) {
+		// TODO: Use https://www.w3schools.com/graphics/svg_polygon.asp
+		var lines = ['M${points[0].x},${points[0].y} '];
+		for(i in 1...points.length) {
+			lines.push('L${points[i].x},${points[i].y} ');
+		}
+		return make(parent, "path", {d: lines.join("")}, style);
+	}
+
+	public function group(?parent: Element, ?className: String, ?attr: Dynamic) {
+		var g = make(parent, "g", attr);
+		if(className != null)
+			g.addClass(className);
+		return g;
+	}
+	
+	// public function text(x: Float, y: Float, text: String, ?style: Dynamic) {
+	// 	var e = make("text", {x:x, y:y}, style);
+	// 	e.text(text);
+	// 	return e;
+	// }
+}

+ 1013 - 0
hide/comp/SceneEditor.hx

@@ -0,0 +1,1013 @@
+package hide.comp;
+
+import hxd.Key as K;
+
+import hide.prefab.Prefab as PrefabElement;
+import hide.prefab.Object3D;
+import h3d.scene.Object;
+
+class SceneEditorContext extends hide.prefab.EditContext {
+
+	public var editor : SceneEditor;
+	public var elements : Array<PrefabElement>;
+	public var rootObjects(default, null): Array<Object>;
+	public var rootElements(default, null): Array<PrefabElement>;
+
+	public function new(ctx, elts, editor) {
+		super(ctx);
+		this.editor = editor;
+		this.elements = elts;
+		rootObjects = [];
+		rootElements = [];
+		cleanups = [];
+		for(elt in elements) {
+			var obj3d = elt.to(Object3D);
+			if(obj3d == null) continue;
+			if(!SceneEditor.hasParent(elt, elements)) {
+				rootElements.push(elt);
+				var obj = getContext(elt).local3d;
+				if(obj != null) 
+					rootObjects.push(obj);
+			}
+		}
+	}
+
+	public function objects3D() {
+		var ret = [];
+		for(e in elements) {
+			var obj = e.to(Object3D);
+			if(obj != null) ret.push(obj);
+		}
+		return ret;
+	}
+
+	override function rebuild() {
+		properties.clear();
+		cleanup();
+		if(elements.length > 0)
+			elements[0].edit(this);
+	}
+
+	public function cleanup() {
+		for( c in cleanups.copy() )
+			c();
+		cleanups = [];
+	}
+
+	override function onChange(p : PrefabElement, pname: String) {
+		editor.onPrefabChange(p, pname);
+	}
+}
+
+class SceneEditor {
+
+	public var tree : hide.comp.IconTree<PrefabElement>;
+	public var scene : hide.comp.Scene;
+	public var properties : hide.comp.PropsEditor;
+
+	var searchBox : Element;
+	var curEdit : SceneEditorContext;
+
+	var cameraController : h3d.scene.CameraController;
+	var gizmo : hide.view.l3d.Gizmo;
+	var interactives : Map<PrefabElement, h3d.scene.Interactive>;
+	var ide : hide.Ide;
+
+	var undo(get, null):hide.ui.UndoHistory;
+	function get_undo() { return view.undo; }	
+
+	var view : hide.view.FileView;
+	var context : hide.prefab.Context;
+	var sceneData : PrefabElement;
+	
+	public function new(view, context, data) {
+		ide = hide.Ide.inst;
+		this.view = view;
+		this.context = context;
+		this.sceneData = data;
+
+		var propsEl = new Element('<div class="props"></div>');
+		properties = new hide.comp.PropsEditor(propsEl, undo);
+
+		var treeEl = new Element('<div class="tree"></div>');
+		tree = new hide.comp.IconTree(treeEl);
+		tree.async = false;
+
+		var sceneEl = new Element('<div class="scene"></div>');
+		scene = new hide.comp.Scene(sceneEl);
+		scene.onReady = onSceneReady;
+
+		view.keys.register("copy", onCopy);
+		view.keys.register("paste", onPaste);
+		view.keys.register("cancel", deselect);
+		view.keys.register("selectAll", selectAll);
+		view.keys.register("duplicate", duplicate);
+		view.keys.register("group", groupSelection);
+		view.keys.register("delete", () -> deleteElements(curEdit.rootElements));
+		view.keys.register("search", function() {
+			if(searchBox != null) {
+				searchBox.show();
+				searchBox.find("input").focus().select();
+			}
+		});
+	}
+
+	public function addSearchBox(parent : Element) {
+		searchBox = new Element("<div>").addClass("searchBox").appendTo(parent);
+		new Element("<input type='text'>").appendTo(searchBox).keydown(function(e) {
+			if( e.keyCode == 27 ) {
+				searchBox.find("i").click();
+				return;
+			}
+		}).keyup(function(e) {
+			tree.searchFilter(e.getThis().val());
+		});
+		new Element("<i>").addClass("fa fa-times-circle").appendTo(searchBox).click(function(_) {
+			tree.searchFilter(null);
+			searchBox.toggle();
+		});
+	}
+
+	function onSceneReady() {
+
+		tree.saveDisplayKey = view.saveDisplayKey + '/tree';
+		
+		scene.s2d.addChild(context.shared.root2d);
+		scene.s3d.addChild(context.shared.root3d);
+
+		gizmo = new hide.view.l3d.Gizmo(scene);
+
+		cameraController = new h3d.scene.CameraController(scene.s3d);
+		cameraController.friction = 0.9;
+		cameraController.panSpeed = 0.6;
+		cameraController.zoomAmount = 1.05;
+		cameraController.smooth = 0.7;
+
+		resetCamera();
+
+		var cam = @:privateAccess view.getDisplayState("Camera");
+		if( cam != null ) {
+			scene.s3d.camera.pos.set(cam.x, cam.y, cam.z);
+			scene.s3d.camera.target.set(cam.tx, cam.ty, cam.tz);
+		}
+		cameraController.loadFromCamera();
+		scene.onUpdate = update;
+
+		// BUILD scene tree
+
+		function makeItem(o:PrefabElement) : hide.comp.IconTree.IconTreeItem<PrefabElement> {
+			var p = o.getHideProps();
+			var r : hide.comp.IconTree.IconTreeItem<PrefabElement> = {
+				value : o,
+				text : o.name,
+				icon : "fa fa-"+p.icon,
+				children : o.children.length > 0
+			};
+			return r;
+		}
+		tree.get = function(o:PrefabElement) {
+			var objs = o == null ? sceneData.children : Lambda.array(o);
+			var out = [for( o in objs ) makeItem(o)];
+			return out;
+		};
+		tree.root.parent().contextmenu(function(e) {
+			e.preventDefault();
+			var current = tree.getCurrentOver();
+			if(current != null && (curEdit == null || curEdit.elements.indexOf(current) < 0)) {
+				selectObjects([current]);
+			}
+
+			var newItems = getNewContextMenu();
+			var menuItems : Array<hide.comp.ContextMenu.ContextMenuItem> = [
+				{ label : "New...", menu : newItems },
+				{ label : "Rename", enabled : current != null, click : function() tree.editNode(current) },
+				{ label : "Delete", enabled : current != null, click : function() deleteElements(curEdit.rootElements) },
+				{ label : "Select all", click : selectAll },
+				{ label : "Select children", enabled : current != null, click : function() selectObjects(current.flatten()) },
+				{ label : "Show", enabled : curEdit != null && curEdit.elements.length > 0, click : function() setVisible(curEdit.elements, true) },
+				{ label : "Hide", enabled : curEdit != null && curEdit.elements.length > 0, click : function() setVisible(curEdit.elements, false) },
+				{ label : "Isolate", enabled : curEdit != null && curEdit.elements.length > 0, click : function() isolate(curEdit.elements) },
+				{ label : "Group", enabled : curEdit != null && canGroupSelection(), click : groupSelection },
+			];
+
+			new hide.comp.ContextMenu(menuItems);
+		});
+		tree.allowRename = true;
+		tree.init();
+		tree.onClick = function(e) {
+			selectObjects(tree.getSelection(), false);
+			if(curEdit.rootObjects.length > 0) {
+				cameraController.set(curEdit.rootObjects[0].getAbsPos().pos().toPoint());
+			}
+		}
+		tree.onRename = function(e, name) {
+			var oldName = e.name;
+			e.name = name;
+			undo.change(Field(e, "name", oldName), function() tree.refresh());
+			return true;
+		};
+		tree.onAllowMove = function(_, _) {
+			return true;
+		};
+
+		// Batch tree.onMove, which is called for every node moved, causing problems with undo and refresh
+		{
+			var movetimer : haxe.Timer = null;
+			var moved = [];
+			tree.onMove = function(e, to, idx) {
+				if(movetimer != null) {
+					movetimer.stop();
+				}
+				moved.push(e);
+				movetimer = haxe.Timer.delay(function() {
+					reparentElement(moved, to, idx);
+					movetimer = null;
+					moved = [];
+				}, 50);
+			}
+		}
+		tree.applyStyle = updateTreeStyle;
+		refresh();
+	}
+
+	public function refresh( ?callb ) {
+		var sh = context.shared;
+		sh.root3d.remove();
+		sh.root2d.remove();
+		for( f in sh.cleanups )
+			f();
+		sh.root3d = new h3d.scene.Object();
+		scene.s3d.addChild(sh.root3d);
+		sh.root2d = new h2d.Sprite();
+		scene.s2d.addChild(sh.root2d);
+		sh.cleanups = [];
+		context.init();
+		sceneData.makeInstance(context);
+		scene.init(view.props);
+		refreshInteractives();
+		tree.refresh(function() {
+			for(elt in sh.contexts.keys()) {
+				onPrefabChange(elt);
+			}
+			if(callb != null) callb();
+		});
+	}
+
+	function refreshProps() {
+		properties.clear();
+		if(curEdit != null && curEdit.elements != null && curEdit.elements.length > 0) {
+			curEdit.elements[0].edit(curEdit);
+		}
+	}
+
+	function refreshInteractives() {
+		var contexts = context.shared.contexts;
+		interactives = new Map();
+		var all = contexts.keys();
+		for(elt in all) {
+			if(elt.to(Object3D) == null)
+				continue;
+			var ctx = contexts[elt];
+			var o = ctx.local3d;
+			if(o == null)
+				continue;
+			var meshes = getSelfMeshes(elt);
+			var invRootMat = o.getAbsPos().clone();
+			invRootMat.invert();
+			var bounds = new h3d.col.Bounds();
+			for(mesh in meshes) {
+				var localMat = mesh.getAbsPos().clone();
+				localMat.multiply(localMat, invRootMat);
+				var lb = mesh.primitive.getBounds().clone();
+				lb.transform(localMat);
+				bounds.add(lb);
+			}
+			var meshCollider = new h3d.col.Collider.GroupCollider([for(m in meshes) m.getGlobalCollider()]);
+			var boundsCollider = new h3d.col.ObjectCollider(o, bounds);
+			var int = new h3d.scene.Interactive(boundsCollider, o);
+			interactives.set(elt, int);
+			int.ignoreParentTransform = true;
+			int.preciseShape = meshCollider;
+			int.propagateEvents = true;
+			var startDrag = null;
+			int.onPush = function(e) {
+				startDrag = [scene.s2d.mouseX, scene.s2d.mouseY];
+				e.propagate = false;
+				if(K.isDown(K.CTRL) && curEdit != null) {
+					var list = curEdit.elements.copy();
+					if(list.indexOf(elt) < 0) {
+							list.push(elt);
+						selectObjects(list);
+					}
+				}
+				else {
+					selectObjects([elt]);
+				}
+			}
+			int.onRelease = function(e) {
+				startDrag = null;
+			}
+			int.onMove = function(e) {
+				if(startDrag != null) {
+					if((hxd.Math.abs(startDrag[0] - scene.s2d.mouseX) + hxd.Math.abs(startDrag[1] - scene.s2d.mouseY)) > 5) {
+						startDrag = null;
+						moveGizmoToSelection();
+						gizmo.startMove(MoveXY);
+					}
+				}
+			}
+		}
+	}
+
+	function setupGizmo() {
+		if(curEdit == null) return;
+		gizmo.onStartMove = function(mode) {
+			var objects = curEdit.rootObjects;
+			var pivotPt = getPivot(objects);
+			var pivot = new h3d.Matrix();
+			pivot.initTranslate(pivotPt.x, pivotPt.y, pivotPt.z);
+			var invPivot = pivot.clone();
+			invPivot.invert();
+
+			var localMats = [for(o in objects) {
+				var m = worldMat(o);
+				m.multiply(m, invPivot);
+				m;
+			}];
+
+			var objects3d = curEdit.objects3D();
+			var prevState = [for(o in objects3d) o.save()];
+			var snapGround = mode == MoveXY;
+			gizmo.onMove = function(translate: h3d.Vector, rot: h3d.Quat, scale: h3d.Vector) {
+				var transf = new h3d.Matrix();
+				transf.identity();
+				if(rot != null)
+					rot.saveToMatrix(transf);
+				if(translate != null)
+					transf.translate(translate.x, translate.y, translate.z);
+				for(i in 0...objects.length) {
+					var newMat = localMats[i].clone();
+					newMat.multiply(newMat, transf);
+					newMat.multiply(newMat, pivot);
+					if(snapGround) {
+						newMat.tz = getZ(newMat.tx, newMat.ty);
+					}
+					var invParent = objects[i].parent.getAbsPos().clone();
+					invParent.invert();
+					newMat.multiply(newMat, invParent);
+					if(scale != null) {
+						newMat.prependScale(scale.x, scale.y, scale.z);
+					}
+					var obj3d = objects3d[i];
+					obj3d.setTransform(newMat);
+					obj3d.applyPos(objects[i]);
+				}
+			}
+
+			gizmo.onFinishMove = function() {
+				var newState = [for(o in objects3d) o.save()];
+				refreshProps();
+				undo.change(Custom(function(undo) {
+					if( undo ) {
+						for(i in 0...objects3d.length) {
+							objects3d[i].load(prevState[i]);
+							objects3d[i].applyPos(objects[i]);
+						}
+						refreshProps();
+					}
+					else {
+						for(i in 0...objects3d.length) {
+							objects3d[i].load(newState[i]);
+							objects3d[i].applyPos(objects[i]);
+						}
+						refreshProps();
+					}
+				}));
+			}
+		}
+	}
+
+	function moveGizmoToSelection() {
+		// Snap Gizmo at center of objects
+		gizmo.getRotationQuat().identity();
+		if(curEdit != null && curEdit.rootObjects.length > 0) {
+			var pos = getPivot(curEdit.rootObjects);
+			gizmo.visible = true;
+			gizmo.setPos(pos.x, pos.y, pos.z);
+
+			if(curEdit.rootObjects.length == 1 && K.isDown(K.ALT)) {
+				var obj = curEdit.rootObjects[0];
+				var mat = worldMat(obj);
+				var s = mat.getScale();
+				mat.prependScale(1.0 / s.x, 1.0 / s.y, 1.0 / s.z);
+				gizmo.getRotationQuat().initRotateMatrix(mat);
+			}
+		}
+		else {
+			gizmo.visible = false;
+		}
+	}
+
+	public function onPrefabChange(p: PrefabElement, ?pname: String) {
+		var model = p.to(hide.prefab.Model);
+		if(model != null && pname == "source") {
+			refresh();
+			return;
+		}
+
+		var el = tree.getElement(p);
+		updateTreeStyle(p, el);
+	}
+
+	function updateTreeStyle(p: PrefabElement, el: Element) {
+		var obj3d  = p.to(Object3D);
+		if(obj3d != null) {
+			if(obj3d.visible) {
+				el.removeClass("jstree-invisible");
+			}
+			else {
+				el.addClass("jstree-invisible");
+			}
+		}
+	}
+
+	function getContext(elt : PrefabElement) {
+		if(elt != null) {
+			return context.shared.contexts.get(elt);
+		}
+		return null;
+	}
+
+	function getObject(elt: PrefabElement) {
+		var ctx = getContext(elt);
+		if(ctx != null)
+			return ctx.local3d;
+		return context.shared.root3d;
+	}
+
+	function getSelfMeshes(p : PrefabElement) {
+		var childObjs = [for(c in p.children) getContext(c).local3d];
+		var ret = [];
+		function rec(o : Object) {
+			var m = Std.instance(o, h3d.scene.Mesh);
+			if(m != null) ret.push(m);
+			for(i in 0...o.numChildren) {
+				var child = o.getChildAt(i);
+				if(childObjs.indexOf(child) < 0) {
+					rec(child);
+				}
+			}
+		}
+		rec(getContext(p).local3d);
+		return ret;
+	}
+
+	public function addObject( e : PrefabElement ) {
+		var roots = e.parent.children;
+		undo.change(Custom(function(undo) {
+			if( undo )
+				roots.remove(e);
+			else
+				roots.push(e);
+			refresh();
+		}));
+		refresh(function() {
+			selectObjects([e]);
+		});
+		if( e.parent == sceneData && sceneData.children.length == 1 )
+			resetCamera();
+	}
+
+	public function selectObjects( elts : Array<PrefabElement>, ?includeTree=true) {
+		if( curEdit != null )
+			curEdit.cleanup();
+		var edit = makeEditContext(elts);
+		edit.rebuild();
+
+		if(includeTree) {
+			tree.setSelection(elts);
+		}
+
+		var objects = edit.rootObjects;
+		addOutline(objects);
+		edit.cleanups.push(function() {
+			cleanOutline(objects);
+		});
+
+		curEdit = edit;
+		setupGizmo();
+	}
+
+	public function resetCamera(?top = false) {
+		var targetPt = new h3d.col.Point(0, 0, 0);
+		if(curEdit != null && curEdit.rootObjects.length > 0) {
+			targetPt = curEdit.rootObjects[0].getAbsPos().pos().toPoint();
+		}
+		if(top)
+			cameraController.set(200, Math.PI/2, 0.001, targetPt);
+		else
+			cameraController.set(200, -4.7, 0.8, targetPt);
+		cameraController.toTarget();
+	}
+
+	public function dropModels(paths: Array<String>) {
+		var proj = screenToWorld(scene.s2d.mouseX, scene.s2d.mouseY);
+		if(proj == null) return;
+
+		var parent = curEdit != null && curEdit.elements.length > 0 ? curEdit.elements[0] : sceneData;
+		var parentMat = worldMat(getObject(parent));
+		parentMat.invert();
+
+		var localMat = new h3d.Matrix();
+		localMat.initTranslate(proj.x, proj.y, proj.z);
+		localMat.multiply(localMat, parentMat);
+
+		var models: Array<PrefabElement> = [];
+		for(path in paths) {
+			var model = new hide.prefab.Model(parent);
+			model.setTransform(localMat);
+			var relative = ide.makeRelative(path);
+			model.source = relative;
+			autoName(model);
+			models.push(model);
+		}
+
+		refresh();
+	}
+
+	function canGroupSelection() {
+		var elts = curEdit.rootElements;
+		if(elts.length == 0)
+			return false;
+
+		if(elts.length == 1)
+			return true;
+
+		// Only allow grouping of sibling elements
+		var parent = elts[0].parent;
+		for(e in elts)
+			if(e.parent != parent)
+				return false;
+
+		return true;
+	}
+
+	function groupSelection() {
+		if(!canGroupSelection())
+			return;
+
+		var elts = curEdit.rootElements;
+		var parent = elts[0].parent;
+		var parentMat = getObject(parent).getAbsPos();
+		var invParentMat = parentMat.clone();
+		invParentMat.invert();
+
+		var pivot = getPivot(curEdit.rootObjects);
+		var local = new h3d.Matrix();
+		local.initTranslate(pivot.x, pivot.y, pivot.z);
+		local.multiply(local, invParentMat);
+		var group = new hide.prefab.Object3D(parent);
+		@:privateAccess group.type = "object";
+		autoName(group);
+		group.x = local.tx;
+		group.y = local.ty;
+		group.z = local.tz;
+		var parentCtx = getContext(parent);
+		if(parentCtx == null)
+			parentCtx = context;
+		group.makeInstance(parentCtx);
+		var groupCtx = getContext(group);
+
+		var reparentUndo = reparentImpl(elts, group, 0);
+		undo.change(Custom(function(undo) {
+			if(undo) {
+				group.parent = null;
+				context.shared.contexts.remove(group);
+				reparentUndo(true);
+			}
+			else {
+				group.parent = parent;
+				context.shared.contexts.set(group, groupCtx);
+				reparentUndo(false);
+			}
+			if(undo)
+				refresh(deselect);
+			else
+				refresh(()->selectObjects([group]));
+		}));
+		refresh(() -> selectObjects([group]));
+	}
+
+	function onCopy() {
+		if(curEdit == null) return;
+		if(curEdit.rootElements.length == 1) {
+			view.setClipboard(curEdit.rootElements[0].saveRec(), "prefab");
+		}
+		else {
+			var lib = new hide.prefab.Library();
+			for(e in curEdit.rootElements) {
+				lib.children.push(e);
+			}
+			view.setClipboard(lib.saveRec(), "library");
+		}
+	}
+
+	function onPaste() {
+		var parent : PrefabElement = sceneData;
+		if(curEdit != null && curEdit.elements.length > 0) {
+			parent = curEdit.elements[0];
+		}
+		var obj: PrefabElement = view.getClipboard("prefab");
+		if(obj != null) {
+			var p = hide.prefab.Prefab.loadRec(obj, parent);
+			autoName(p);
+			refresh();
+		}
+		else {
+			obj = view.getClipboard("library");
+			if(obj != null) {
+				var lib = hide.prefab.Prefab.loadRec(obj);
+				for(c in lib.children) {
+					autoName(c);
+					c.parent = parent;
+				}
+				refresh();
+			}
+		}
+	}
+
+	public function selectAll() {
+		selectObjects([for(e in context.shared.contexts.keys()) e]);
+	}
+
+	public function deselect() {
+		selectObjects([]);
+	}
+
+	public function setVisible(elements : Array<PrefabElement>, visible: Bool) {
+		var cache = [];
+		for(e in elements) {
+			var o = e.to(Object3D);
+			if(o != null) {
+				cache.push({o: o, vis: o.visible});
+			}
+		}
+
+		function apply(b) {
+			for(c in cache) {
+				c.o.visible = b ? visible : c.vis;
+				var obj = getContext(c.o).local3d;
+				if(obj != null) {
+					c.o.applyPos(obj);
+				}
+				onPrefabChange(c.o);
+			}
+		}
+
+		apply(true);
+		undo.change(Custom(function(undo) {
+			if(undo)
+				apply(false);
+			else
+				apply(true);
+		}));
+	}
+
+	function isolate(elts : Array<PrefabElement>) {
+		var toShow = elts.copy();
+		var toHide = [];
+		function hideSiblings(elt: PrefabElement) {
+			var p = elt.parent;
+			for(c in p.children) {
+				var needsVisible = c == elt
+					|| toShow.indexOf(c) >= 0
+					|| hasChild(c, toShow);
+				if(!needsVisible) {
+					toHide.push(c);
+				}
+			}
+			if(p != sceneData) {
+				hideSiblings(p);
+			}
+		}
+		for(e in toShow) {
+			hideSiblings(e);
+		}
+		setVisible(toHide, false);
+	}
+
+	function duplicate() {
+		if(curEdit == null) return;
+		var elements = curEdit.rootElements;
+		if(elements == null || elements.length == 0)
+			return;
+		var contexts = context.shared.contexts;
+		var oldContexts = contexts.copy();
+		var newElements = [for(elt in elements) {
+			var clone = hide.prefab.Prefab.loadRec(elt.saveRec());
+			autoName(clone);
+			var index = elt.parent.children.indexOf(elt);
+			clone.parent = elt.parent;
+			elt.parent.children.remove(clone);
+			elt.parent.children.insert(index+1, clone);
+			{ elt: clone, idx: index };
+		}];
+		var newContexts = contexts.copy();
+		refresh(function() {
+			var all = [for(e in newElements) e.elt];
+			selectObjects(all);
+			tree.setSelection(all);
+			gizmo.startMove(MoveXY, true);
+			gizmo.onFinishMove = function() {
+				undo.change(Custom(function(undo) {
+					for(e in newElements) {
+						if(undo) {
+							e.elt.parent.children.remove(e.elt);
+						}
+						else {
+							e.elt.parent.children.insert(e.idx, e.elt);
+						}
+					}
+					if(undo)
+						context.shared.contexts = oldContexts;
+					else
+						context.shared.contexts = newContexts;
+					refresh();
+					deselect();
+				}));
+			}
+		});
+	}
+
+	function deleteElements(elts : Array<PrefabElement>) {
+		var contexts = context.shared.contexts;
+		var list = [for(e in elts) {
+			elt: e,
+			parent: e.parent,
+			index: e.parent.children.indexOf(e)
+		}];
+		var oldContexts = contexts.copy();
+		for(e in elts) {
+			for(c in e.flatten())
+				contexts.remove(c);
+		}
+		var newContexts = contexts.copy();
+		function action(undo) {
+			if( undo ) {
+				for(o in list)
+					o.parent.children.insert(o.index, o.elt);
+				context.shared.contexts = oldContexts;
+			}
+			else {
+				for(o in list)
+					o.parent.children.remove(o.elt);
+				context.shared.contexts = newContexts;
+			}
+			deselect();
+			refresh();
+		}
+		action(false);
+		undo.change(Custom(action));
+	}
+
+	function reparentElement(e : Array<PrefabElement>, to : PrefabElement, index : Int) {
+		if( to == null )
+			to = sceneData;
+
+		var undoFunc = reparentImpl(e, to, index);
+		undo.change(Custom(function(undo) {
+			undoFunc(undo);
+			refresh();
+		}));
+		refresh();
+	}
+
+	function reparentImpl(elts : Array<PrefabElement>, to: PrefabElement, index: Int) {
+		var undoes = [];
+		for(e in elts) {
+			var prev = e.parent;
+			var prevIndex = prev.children.indexOf(e);
+			e.parent = to;
+			to.children.remove(e);
+			to.children.insert(index, e);
+
+			var obj3d = e.to(Object3D);
+			var toObj = getObject(to);
+			var obj = getObject(e);
+			if(obj3d != null && toObj != null && obj != null) {
+				var mat = worldMat(obj);
+				var parentMat = worldMat(toObj);
+				parentMat.invert();
+				mat.multiply(mat, parentMat);
+				var prevState = obj3d.save();
+				obj3d.setTransform(mat);
+				var newState = obj3d.save();
+
+				undoes.push(function(undo) {
+					if( undo ) {
+						e.parent = prev;
+						prev.children.remove(e);
+						prev.children.insert(prevIndex, e);
+						obj3d.load(prevState);
+					} else {
+						e.parent = to;
+						to.children.remove(e);
+						to.children.insert(index, e);
+						obj3d.load(newState);
+					};
+				});
+			}
+		}
+		return function(undo) {
+			for(f in undoes) {
+				f(undo);
+			}
+		}
+	}
+
+	function autoName(p : PrefabElement) {
+		var prefix = p.type;
+		if(prefix == "object")
+			prefix = "group";
+		if(p.name != null && p.name.length > 0) {
+			prefix = p.name.split("_")[0].split(" ")[0].split("-")[0];
+		}
+
+		var model = p.to(hide.prefab.Model);
+		if(model != null && model.source != null) {
+			var path = new haxe.io.Path(model.source);
+			prefix = path.file;
+		}
+
+		prefix += "_";
+		var id = 0;
+		while( sceneData.getPrefabByName(prefix + id) != null )
+			id++;
+
+		p.name = prefix + id;
+
+		for(c in p.children) {
+			autoName(c);
+		}
+	}
+
+	function update(dt:Float) {
+		var cam = scene.s3d.camera;
+		@:privateAccess view.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(gizmo != null) {
+			if(!gizmo.moving) {
+				moveGizmoToSelection();
+			}
+			gizmo.update(dt);
+		}
+	}
+
+	// Override
+	function makeEditContext(elts : Array<PrefabElement>) : SceneEditorContext {
+		var edit = new SceneEditorContext(context, elts, this);
+		edit.prefabPath = view.state.path;
+		edit.properties = properties;
+		edit.scene = scene;
+		edit.editor = this;
+		return edit;
+	}
+
+	// Override
+	function getNewContextMenu() : Array<hide.comp.ContextMenu.ContextMenuItem> {
+		var current = tree.getCurrentOver();
+		var newItems = new Array<hide.comp.ContextMenu.ContextMenuItem>();
+		var allRegs = @:privateAccess hide.prefab.Library.registeredElements;
+		var allowed = ["model", "object"];
+		for( ptype in allowed ) {
+			var pcl = allRegs.get(ptype);
+			var props = Type.createEmptyInstance(pcl).getHideProps();
+			newItems.push({
+				label : props.name,
+				click : function() {
+
+					function make(?path) {
+						var p = Type.createInstance(pcl, [current == null ? sceneData : current]);
+						@:privateAccess p.type = ptype;
+						if(path != null)
+							p.source = path;
+						autoName(p);
+						return p;
+					}
+
+					if( props.fileSource != null )
+						ide.chooseFile(props.fileSource, function(path) {
+							if( path == null ) return;
+							var p = make(path);
+							addObject(p);
+						});
+					else
+						addObject(make());
+				}
+			});
+		}
+		return newItems;
+	}
+
+	function getZ(x: Float, y: Float) {
+		var offset = 1000;
+		var ray = h3d.col.Ray.fromValues(x, y, offset, 0, 0, -1);
+		var dist = projectToGround(ray);
+		if(dist >= 0) {
+			return offset - dist;
+		}
+		return 0.;
+	}
+
+	function projectToGround(ray: h3d.col.Ray) {
+		var minDist = -1.;
+		var zPlane = h3d.col.Plane.Z(0);
+		var pt = ray.intersect(zPlane);
+		if(pt != null) {
+			minDist = pt.sub(ray.getPos()).length();
+		}
+		return minDist;
+	}
+
+	function screenToWorld(sx: Float, sy: Float) {
+		var camera = scene.s3d.camera;
+		var ray = camera.rayFromScreen(sx, sy);
+		var dist = projectToGround(ray);
+		if(dist >= 0) {
+			return ray.getPoint(dist);
+		}
+		return null;
+	}
+
+	static function worldMat(obj: Object) {
+		if(obj.defaultTransform != null) {
+			var m = obj.defaultTransform.clone();
+			m.invert();
+			m.multiply(m, obj.getAbsPos());
+			return m;
+		}
+		else {
+			return obj.getAbsPos().clone();
+		}
+	}
+
+	static function getPivot(objects: Array<Object>) {
+		var pos = new h3d.Vector();
+		for(o in objects) {
+			pos = pos.add(o.getAbsPos().pos());
+		}
+		pos.scale3(1.0 / objects.length);
+		return pos;
+	}
+
+	static function addOutline(objects: Array<Object>) {
+		var outlineShader = new h3d.shader.Outline();
+		outlineShader.size = 0.12;
+		outlineShader.distance = 0;
+		outlineShader.color.setColor(0xffffff);
+		for(obj in objects) {
+			for( m in obj.getMaterials() ) {
+				var p = m.allocPass("outline");
+				p.culling = None;
+				p.depthWrite = false;
+				p.addShader(outlineShader);
+				if( m.mainPass.name == "default" )
+					m.mainPass.setPassName("outlined");
+			}
+		}
+	}
+
+	static function cleanOutline(objects: Array<Object>) {
+		for(obj in objects) {
+			for( m in obj.getMaterials() ) {
+				if( m.mainPass != null && m.mainPass.name == "outlined" )
+					m.mainPass.setPassName("default");
+				m.removePass(m.getPass("outline"));
+			}
+		}
+	}
+
+	public static function hasParent(elt: PrefabElement, list: Array<PrefabElement>) {
+		for(p in list) {
+			if(isParent(elt, p))
+				return true;
+		}
+		return false;
+	}
+
+	public static function hasChild(elt: PrefabElement, list: Array<PrefabElement>) {
+		for(p in list) {
+			if(isParent(p, elt))
+				return true;
+		}
+		return false;
+	}
+
+	public static function isParent(elt: PrefabElement, parent: PrefabElement) {
+		var p = elt.parent;
+		while(p != null) {
+			if(p == parent) return true;
+			p = p.parent;
+		}
+		return false;
+	}
+}

+ 1 - 1
hide/comp/Tabs.hx

@@ -20,7 +20,7 @@ class Tabs extends Component {
 		return currentTab = e;
 	}
 
-	function getTabs() : Element {
+	public function getTabs() : Element {
 		return root.children(".tab");
 	}
 

+ 122 - 0
hide/prefab/Curve.hx

@@ -0,0 +1,122 @@
+package hide.prefab;
+
+typedef CurveHandle = {
+	dt: Float,
+	dv: Float
+}
+
+@:enum abstract CurveKeyMode(Int) {
+	var Aligned = 0;
+	var Free = 1;
+	var Linear = 2;
+	var Constant = 3;
+}
+
+typedef CurveKey = {
+	time: Float,
+	value: Float,
+	mode: CurveKeyMode,	
+	?prevHandle: CurveHandle,
+	?nextHandle: CurveHandle,
+}
+
+typedef CurveKeys = Array<CurveKey>;
+
+class Curve extends Prefab {
+
+	public var duration : Float = 0.;
+	public var keys : CurveKeys = [];
+
+   	public function new(?parent) {
+		super(parent);
+		this.type = "curve";
+	}
+
+	override function load(o:Dynamic) {
+		duration = o.duration;
+		keys = o.keys;
+	}
+
+	override function save() {
+		return {
+			duration: duration,
+			keys: keys
+		};
+	}
+	
+	static inline function bezier(c0: Float, c1:Float, c2:Float, c3: Float, t:Float) {
+		var u = 1 - t;
+		return u * u * u * c0 + c1 * 3 * t * u * u + c2 * 3 * t * t * u + t * t * t * c3;
+	}
+
+	public function getVal(time: Float) : Float {
+		switch(keys.length) {
+			case 0: return 0;
+			case 1: return keys[0].value;
+			default:
+		}
+
+		var idx = -1;
+		for(ik in 0...keys.length) {
+			var key = keys[ik];
+			if(time > key.time)
+				idx = ik;
+		}
+
+		if(idx < 0)
+			return keys[0].value;
+
+		var cur = keys[idx];
+		var next = keys[idx + 1];
+		if(next == null || cur.mode == Constant)
+			return cur.value;
+		
+		var minT = 0.;
+		var maxT = 1.;
+		var maxDelta = 1./ 25.;
+
+		inline function sampleTime(t) {
+			return bezier(
+				cur.time,
+				cur.time + (cur.nextHandle != null ? cur.nextHandle.dt : 0.),
+				next.time + (next.prevHandle != null ? next.prevHandle.dt : 0.),
+				next.time, t);
+		}
+
+		inline function sampleVal(t) {
+			return bezier(
+				cur.value,
+				cur.value + (cur.nextHandle != null ? cur.nextHandle.dv : 0.),
+				next.value + (next.prevHandle != null ? next.prevHandle.dv : 0.),
+				next.value, t);
+		}
+
+		while( maxT - minT > maxDelta ) {
+			var t = (maxT + minT) * 0.5;
+			var x = sampleTime(t);
+			if( x > time )
+				maxT = t;
+			else
+				minT = t;
+		}
+
+		var x0 = sampleTime(minT);
+		var x1 = sampleTime(maxT);
+		var dx = x1 - x0;
+		var xfactor = dx == 0 ? 0.5 : (time - x0) / dx;
+
+		var y0 = sampleVal(minT);
+		var y1 = sampleVal(maxT);
+		var y = y0 + (y1 - y0) * xfactor;
+		return y;
+	}
+
+	public function sample(numPts: Int) {
+		var vals = [];
+		for(i in 0...numPts) {
+			var v = getVal(duration * i/(numPts-1));
+			vals.push(v);
+		}
+		return vals;
+	}
+}

+ 36 - 0
hide/prefab/fx/FXScene.hx

@@ -0,0 +1,36 @@
+package hide.prefab.fx;
+
+class FXScene extends Library {
+
+	public function new() {
+		super();
+		type = "fx";
+	}
+
+	override function save() {
+		var obj : Dynamic = super.save();
+		return obj;
+	}
+
+	override function load( obj : Dynamic ) {
+		super.load(obj);
+	}
+
+	override function edit( ctx : EditContext ) {
+		#if editor
+		var props = new hide.Element('
+			<div class="group" name="Level">
+			</div>
+		');
+		ctx.properties.add(props, this, function(pname) {
+			ctx.onChange(this, pname);
+		});
+		#end
+	}
+
+	override function getHideProps() {
+		return { icon : "cube", name : "FX", fileSource : ["fx"] };
+	}
+
+	static var _ = Library.register("fx", FXScene);
+}

+ 4 - 4
hide/ui/View.hx

@@ -13,11 +13,11 @@ typedef ViewOptions = { ?position : DisplayPosition, ?width : Int }
 class View<T> extends hide.comp.Component {
 
 	var container : golden.Container;
-	var state : T;
-	var keys(get,null) : Keys;
-	var props(get,null) : Props;
-	var undo = new hide.ui.UndoHistory();
 	var watches : Array<{ keep : Bool, path : String, callb : Void -> Void }> = [];
+	public var keys(get,null) : Keys;
+	public var state(default, null) : T;
+	public var undo(default, null) = new hide.ui.UndoHistory();
+	public var props(get, null) : Props;
 	public var viewClass(get, never) : String;
 	public var defaultOptions(get,never) : ViewOptions;
 

+ 238 - 0
hide/view/FXScene.hx

@@ -0,0 +1,238 @@
+package hide.view;
+
+import hide.Element;
+import hide.prefab.Prefab in PrefabElement;
+
+@:access(hide.view.FXScene)
+private class FXSceneEditor extends hide.comp.SceneEditor {
+	var parent : hide.view.FXScene;
+	public function new(view, context, data) {
+		super(view, context, data);
+		parent = cast view;
+	}
+
+	override function onSceneReady() {
+		super.onSceneReady();
+		parent.onSceneReady();
+	}
+
+	override function update(dt) {
+		super.update(dt);
+		parent.onUpdate(dt);
+	}
+
+	override function selectObjects( elts, ?includeTree) {
+		super.selectObjects(elts, includeTree);
+		parent.onSelect(elts);
+	}
+
+	override function getNewContextMenu() {
+		var current = tree.getCurrentOver();
+		var registered = new Array<hide.comp.ContextMenu.ContextMenuItem>();
+		var allRegs = @:privateAccess hide.prefab.Library.registeredElements;
+		var allowed = ["model", "object"];
+		for( ptype in allowed ) {
+			var pcl = allRegs.get(ptype);
+			var props = Type.createEmptyInstance(pcl).getHideProps();
+			registered.push({
+				label : props.name,
+				click : function() {
+
+					function make() {
+						var p = Type.createInstance(pcl, [current == null ? sceneData : current]);
+						@:privateAccess p.type = ptype;
+						autoName(p);
+						return p;
+					}
+
+					if( props.fileSource != null )
+						ide.chooseFile(props.fileSource, function(path) {
+							if( path == null ) return;
+							var p = make();
+							p.source = path;
+							addObject(p);
+						});
+					else
+						addObject(make());
+				}
+			});
+		}
+		return registered;
+	}
+}
+
+class FXScene extends FileView {
+
+	var sceneEditor : FXSceneEditor;
+	var data : hide.prefab.fx.FXScene;
+	var context : hide.prefab.Context;
+	var tabs : hide.comp.Tabs;
+
+	var tools : hide.comp.Toolbar;
+	var light : h3d.scene.DirLight;
+	var lightDirection = new h3d.Vector( 1, 2, -4 );
+
+
+	var scene(get, null):  hide.comp.Scene;
+	function get_scene() return sceneEditor.scene;
+	var properties(get, null):  hide.comp.PropsEditor;
+	function get_properties() return sceneEditor.properties;
+
+	// autoSync
+	var autoSync : Bool;
+	var currentVersion : Int = 0;
+	var lastSyncChange : Float = 0.;
+	var currentSign : String;
+
+	override function getDefaultContent() {
+		return haxe.io.Bytes.ofString(ide.toJSON(new hide.prefab.fx.FXScene().save()));
+	}
+
+	override function onFileChanged(wasDeleted:Bool) {
+		if( !wasDeleted ) {
+			// double check if content has changed
+			var content = sys.io.File.getContent(getPath());
+			var sign = haxe.crypto.Md5.encode(content);
+			if( sign == currentSign )
+				return;
+		}
+		super.onFileChanged(wasDeleted);
+	}
+
+	override function save() {
+		var content = ide.toJSON(data.save());
+		currentSign = haxe.crypto.Md5.encode(content);
+		sys.io.File.saveContent(getPath(), content);
+	}
+
+	override function onDisplay() {
+		saveDisplayKey = "FX:" + getPath().split("\\").join("/").substr(0,-1);
+		data = new hide.prefab.fx.FXScene();
+		var content = sys.io.File.getContent(getPath());
+		data.load(haxe.Json.parse(content));
+		currentSign = haxe.crypto.Md5.encode(content);
+
+		context = new hide.prefab.Context();
+		context.onError = function(e) {
+			ide.error(e);
+		};
+		context.init();
+
+		root.html('
+			<div class="flex vertical">
+				<div class="toolbar"></div>
+				<div class="flex">
+					<div class="scene">
+					</div>
+					<div class="tabs">
+						<div class="tab" name="Scene" icon="sitemap">
+							<div class="hide-block">
+								<div class="hide-list hide-scene-tree">
+								</div>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="fx-animpanel">
+				</div>
+			</div>
+		');
+		tools = new hide.comp.Toolbar(root.find(".toolbar"));
+		tabs = new hide.comp.Tabs(root.find(".tabs"));
+		sceneEditor = new FXSceneEditor(this, context, data);
+		root.find(".hide-scene-tree").first().append(sceneEditor.tree.root);
+		root.find(".tab").first().append(sceneEditor.properties.root);
+		root.find(".scene").first().append(sceneEditor.scene.root);
+		currentVersion = undo.currentID;
+
+		var animPanel = root.find(".fx-animpanel");
+		var curve = new hide.prefab.Curve();
+		curve.duration = 3.;
+		curve.keys.push({
+			time: 0.1,
+			value: 1.0,
+			prevHandle: {
+				dt: -0.09,
+				dv: -0.1
+			},
+			nextHandle: {
+				dt: 0.12,
+				dv: -0.1
+			},
+		});
+		for(i in 0...2) {
+			curve.keys.push({
+				time: i + 1.0,
+				value: -1.0,
+				prevHandle: {
+					dt: -0.09,
+					dv: -0.1
+				},
+				nextHandle: {
+					dt: 0.3,
+					dv: -0.1
+				},
+			});
+		}
+		var curveAnim = new hide.comp.CurveEditor(animPanel, curve, this.undo);
+	}
+
+	public function onSceneReady() {
+		light = sceneEditor.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;
+
+
+		this.saveDisplayKey = "Scene:" + state.path;
+
+		tools.saveDisplayKey = "Prefab/tools";
+		tools.addButton("video-camera", "Perspective camera", () -> sceneEditor.resetCamera(false));
+		tools.addToggle("sun-o", "Enable Lights/Shadows", function(v) {
+			if( !v ) {
+				for( m in context.shared.root3d.getMaterials() ) {
+					m.mainPass.enableLights = false;
+					m.shadows = false;
+				}
+			} else {
+				for( m in context.shared.root3d.getMaterials() )
+					h3d.mat.MaterialSetup.current.initModelMaterial(m);
+			}
+		},true);
+
+		tools.addColor("Background color", function(v) {
+			scene.engine.backgroundColor = v;
+		}, scene.engine.backgroundColor);
+		tools.addToggle("refresh", "Auto synchronize", function(b) {
+			autoSync = b;
+		});
+		tools.addRange("Speed", function(v) {
+			scene.speed = v;
+		}, scene.speed);
+	}
+
+	function onSelect(elts : Array<PrefabElement>) {
+
+	}
+
+	function onUpdate(dt:Float) {
+		var cam = scene.s3d.camera;
+		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
+			);
+		}
+		if( autoSync && (currentVersion != undo.currentID || lastSyncChange != properties.lastChange) ) {
+			save();
+			lastSyncChange = properties.lastChange;
+			currentVersion = undo.currentID;
+		}
+	}
+
+	static var _ = FileTree.registerExtension(FXScene,["fx"], { icon : "sitemap", createNew : "FX" });
+}

+ 75 - 269
hide/view/Prefab.hx

@@ -2,50 +2,73 @@ package hide.view;
 
 import hide.prefab.Prefab in PrefabElement;
 
-class EditContext extends hide.prefab.EditContext {
-
-	var view : FileView;
-	public var elt : PrefabElement;
-
-	public function new(ctx, elt, view) {
-		super(ctx);
-		this.elt = elt;
-		this.view = view;
+@:access(hide.view.Prefab)
+private class PrefabSceneEditor extends hide.comp.SceneEditor {
+	var parent : Prefab;
+	public function new(view, context, data) {
+		super(view, context, data);
+		parent = cast view;
 	}
-
-	override function rebuild() {
-		properties.clear();
-		cleanup();
-		elt.edit(this);
+	override function onSceneReady() {
+		super.onSceneReady();
+		parent.onSceneReady();
 	}
-
-	public function cleanup() {
-		for( c in cleanups.copy() )
-			c();
-		cleanups = [];
+	override function update(dt) {
+		super.update(dt);
+		parent.onUpdate(dt);
 	}
 
-	override function onChange(p:hide.prefab.Prefab, propName:String) {
-		view.modified = true;
-	}
+	override function getNewContextMenu() {
+		var current = tree.getCurrentOver();
+		var registered = new Array<hide.comp.ContextMenu.ContextMenuItem>();
+		var allRegs = @:privateAccess hide.prefab.Library.registeredElements;
+		for( ptype in allRegs.keys() ) {
+			if( ptype == "prefab" ) continue;
+			var pcl = allRegs.get(ptype);
+			var props = Type.createEmptyInstance(pcl).getHideProps();
+			registered.push({
+				label : props.name,
+				click : function() {
+
+					function make() {
+						var p = Type.createInstance(pcl, [current == null ? sceneData : current]);
+						@:privateAccess p.type = ptype;
+						autoName(p);
+						return p;
+					}
 
+					if( props.fileSource != null )
+						ide.chooseFile(props.fileSource, function(path) {
+							if( path == null ) return;
+							var p = make();
+							p.source = path;
+							addObject(p);
+						});
+					else
+						addObject(make());
+				}
+			});
+		}
+		return registered;
+	}
 }
 
 class Prefab extends FileView {
 
+	var sceneEditor : PrefabSceneEditor;
 	var data : hide.prefab.Library;
 	var context : hide.prefab.Context;
 	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<PrefabElement>;
 
-	var curEdit : EditContext;
+
+	var scene(get, null):  hide.comp.Scene;
+	function get_scene() return sceneEditor.scene;
+	var properties(get, null):  hide.comp.PropsEditor;
+	function get_properties() return sceneEditor.properties;
 
 	// autoSync
 	var autoSync : Bool;
@@ -75,6 +98,18 @@ class Prefab extends FileView {
 	}
 
 	override function onDisplay() {
+		saveDisplayKey = "Prefab:" + getPath().split("\\").join("/").substr(0,-1);
+		data = new hide.prefab.Library();
+		var content = sys.io.File.getContent(getPath());
+		data.load(haxe.Json.parse(content));
+		currentSign = haxe.crypto.Md5.encode(content);
+
+		context = new hide.prefab.Context();
+		context.onError = function(e) {
+			ide.error(e);
+		};
+		context.init();
+
 		root.html('
 			<div class="flex vertical">
 				<div class="toolbar"></div>
@@ -84,11 +119,9 @@ class Prefab extends FileView {
 					<div class="tabs">
 						<div class="tab" name="Scene" icon="sitemap">
 							<div class="hide-block">
-								<div class="hide-list">
-									<div class="tree"></div>
+								<div class="hide-list hide-scene-tree">
 								</div>
 							</div>
-							<div class="props"></div>
 						</div>
 					</div>
 				</div>
@@ -96,114 +129,26 @@ class Prefab extends FileView {
 		');
 		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"));
+		sceneEditor = new PrefabSceneEditor(this, context, data);
+		root.find(".hide-scene-tree").first().append(sceneEditor.tree.root);
+		root.find(".tab").first().append(sceneEditor.properties.root);
+		root.find(".scene").first().append(sceneEditor.scene.root);
 		currentVersion = undo.currentID;
 	}
 
-	function refresh( ?callb ) {
-		var sh = context.shared;
-		sh.root2d.remove();
-		sh.root3d.remove();
-		for( f in sh.cleanups )
-			f();
-		sh.root2d = new h2d.Sprite();
-		sh.root3d = new h3d.scene.Object();
-		sh.cleanups = [];
-		context.init();
-		data.makeInstance(context);
-		scene.s2d.addChild(sh.root2d);
-		scene.s3d.addChild(sh.root3d);
-		scene.init(props);
-		tree.refresh(callb);
-	}
-
-	function allocName( prefix : String ) {
-		var id = 0;
-		while( data.getPrefabByName(prefix + id) != null )
-			id++;
-		return prefix + id;
-	}
-
-	function selectObject( elt : PrefabElement ) {
-		if( curEdit != null )
-			curEdit.cleanup();
-		var edit = new EditContext(context, elt, this);
-		edit.prefabPath = state.path;
-		edit.properties = properties;
-		edit.scene = scene;
-		edit.cleanups = [];
-		edit.rebuild();
-		curEdit = edit;
-	}
-
-	function resetCamera() {
-		var bounds = context.shared.root2d.getBounds();
-		context.shared.root2d.x = -Std.int(bounds.xMin + bounds.width * 0.5);
-		context.shared.root2d.y = -Std.int(bounds.yMin + bounds.height * 0.5);
-		scene.resetCamera(context.shared.root3d, 1.5);
-		control.loadFromCamera();
-	}
-
-	function addObject( e : PrefabElement ) {
-		var roots = e.parent.children;
-		undo.change(Custom(function(undo) {
-			if( undo )
-				roots.remove(e);
-			else
-				roots.push(e);
-			refresh();
-		}));
-		refresh(function() {
-			tree.setSelection([e]);
-			selectObject(e);
-		});
-		if( e.parent == data && data.children.length == 1 )
-			resetCamera();
-	}
-
-	function init() {
-		data = new hide.prefab.Library();
-		var content = sys.io.File.getContent(getPath());
-		data.load(haxe.Json.parse(content));
-		currentSign = haxe.crypto.Md5.encode(content);
-
-		context = new hide.prefab.Context();
-		context.onError = function(e) {
-			ide.error(e);
-		};
-		context.init();
-		scene.s2d.addChild(context.shared.root2d);
-		scene.s3d.addChild(context.shared.root3d);
-
-		data.makeInstance(context);
-
-		light = scene.s3d.find(function(o) return Std.instance(o, h3d.scene.DirLight));
+	public function onSceneReady() {
+		light = sceneEditor.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;
 
-		resetCamera();
-		var cam = getDisplayState("Camera");
-		if( cam != null ) {
-			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", resetCamera);
+		tools.saveDisplayKey = "Prefab/tools";
+		tools.addButton("video-camera", "Perspective camera", () -> sceneEditor.resetCamera(false));
 		tools.addToggle("sun-o", "Enable Lights/Shadows", function(v) {
 			if( !v ) {
 				for( m in context.shared.root3d.getMaterials() ) {
@@ -219,154 +164,16 @@ class Prefab extends FileView {
 		tools.addColor("Background color", function(v) {
 			scene.engine.backgroundColor = v;
 		}, scene.engine.backgroundColor);
-
-		tools.addRange("Speed", function(v) {
-			scene.speed = v;
-		}, scene.speed);
-
 		tools.addToggle("refresh", "Auto synchronize", function(b) {
 			autoSync = b;
 		});
-
-		// BUILD scene tree
-
-		function makeItem(o:PrefabElement) : hide.comp.IconTree.IconTreeItem<PrefabElement> {
-			var p = o.getHideProps();
-			return {
-				value : o,
-				text : o.name,
-				icon : "fa fa-"+p.icon,
-				children : o.children.length > 0,
-				state : { opened : true },
-			};
-		}
-		tree.get = function(o:PrefabElement) {
-			var objs = o == null ? data.children : Lambda.array(o);
-			var out = [for( o in objs ) makeItem(o)];
-			return out;
-		};
-		tree.root.parent().contextmenu(function(e) {
-			e.preventDefault();
-			var current = tree.getCurrentOver();
-			tree.setSelection(current == null ? [] : [current]);
-
-			var registered = new Array<hide.comp.ContextMenu.ContextMenuItem>();
-			var allRegs = @:privateAccess hide.prefab.Library.registeredElements;
-			for( ptype in allRegs.keys() ) {
-				if( ptype == "prefab" ) continue;
-				var pcl = allRegs.get(ptype);
-				var props = Type.createEmptyInstance(pcl).getHideProps();
-				registered.push({
-					label : props.name,
-					click : function() {
-
-						function make() {
-							var p = Type.createInstance(pcl, [current == null ? data : current]);
-							@:privateAccess p.type = ptype;
-							p.name = allocName(ptype);
-							return p;
-						}
-
-						if( props.fileSource != null )
-							ide.chooseFile(props.fileSource, function(path) {
-								if( path == null ) return;
-								var p = make();
-								p.source = path;
-								addObject(p);
-							});
-						else
-							addObject(make());
-					}
-				});
-			}
-
-
-			new hide.comp.ContextMenu([
-				{ label : "New...", menu : registered },
-				{ label : "Rename", enabled : current != null, click : function() tree.editNode(current) },
-				{ label : "Delete", enabled : current != null, click : function() {
-					function deleteRec(roots:Array<PrefabElement>) {
-						for( o in roots ) {
-							if( o == current ) {
-								properties.clear();
-								var index = roots.indexOf(o);
-								var contexts = context.shared.contexts;
-								var ctx = contexts.get(o);
-								contexts.remove(o);
-								roots.remove(o);
-								undo.change(Custom(function(undo) {
-									if( undo ) {
-										roots.insert(index, o);
-										contexts.set(o, ctx);
-									}
-									else {
-										roots.remove(o);
-										contexts.remove(o);
-									}
-									refresh();
-								}));
-								refresh();
-								return;
-							}
-							@:privateAccess deleteRec(o.children);
-						}
-					}
-					deleteRec(data.children);
-				} },
-			]);
-		});
-		tree.allowRename = true;
-		tree.init();
-		tree.onClick = selectObject;
-		tree.onRename = function(e, name) {
-			var oldName = e.name;
-			e.name = name;
-			undo.change(Field(e, "name", oldName), function() tree.refresh());
-			return true;
-		};
-		tree.onAllowMove = function(_, _) {
-			return true;
-		};
-		tree.onMove = function(e, to, index) {
-			if( to == null ) to = data;
-			var prev = e.parent;
-			var prevIndex = prev.children.indexOf(e);
-			e.parent = to;
-			to.children.remove(e);
-			to.children.insert(index, e);
-			undo.change(Custom(function(undo) {
-				if( undo ) {
-					e.parent = prev;
-					prev.children.remove(e);
-					prev.children.insert(prevIndex, e);
-				} else {
-					e.parent = to;
-					to.children.remove(e);
-					to.children.insert(index, e);
-				}
-				refresh();
-			}));
-			refresh();
-			return true;
-		};
-
-		scene.onResize = function() {
-			scene.s2d.x = scene.s2d.width >> 1;
-			scene.s2d.y = scene.s2d.height >> 1;
-		};
-		scene.onResize();
-
-		if( curEdit != null ) {
-			curEdit.cleanup();
-			var e = curEdit.elt.name;
-			var elt = data.getPrefabByName(e);
-			if( elt != null ) selectObject(elt);
-		}
+		tools.addRange("Speed", function(v) {
+			scene.speed = v;
+		}, scene.speed);
 	}
 
-	function update(dt:Float) {
+	function onUpdate(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(
@@ -383,5 +190,4 @@ class Prefab extends FileView {
 	}
 
 	static var _ = FileTree.registerExtension(Prefab,["prefab"],{ icon : "sitemap", createNew : "Prefab" });
-
 }

Diferenças do arquivo suprimidas por serem muito extensas
+ 227 - 692
hide/view/l3d/Level3D.hx


+ 0 - 42
hide/view/l3d/LevelEditContext.hx

@@ -1,42 +0,0 @@
-package hide.view.l3d;
-import hide.prefab.Prefab as PrefabElement;
-import h3d.scene.Object;
-
-class LevelEditContext extends hide.prefab.EditContext {
-
-	public var view : Level3D;
-	public var elements : Array<PrefabElement>;
-	public var rootObjects(default, null): Array<Object>;
-	public var rootElements(default, null): Array<PrefabElement>;
-
-	public function new(ctx, elts, view) {
-		super(ctx);
-		this.view = view;
-		this.elements = elts;
-		rootObjects = [];
-		rootElements = [];
-		for(elt in elements) {
-			if(!Level3D.hasParent(elt, elements)) {
-				rootElements.push(elt);
-				rootObjects.push(getContext(elt).local3d);
-			}
-		}
-	}
-
-	override function rebuild() {
-		properties.clear();
-		cleanup();
-		if(elements.length > 0)
-			elements[0].edit(this);
-	}
-
-	public function cleanup() {
-		for( c in cleanups.copy() )
-			c();
-		cleanups = [];
-	}
-
-	override function onChange(p : PrefabElement, pname: String) {
-		view.onPrefabChange(p, pname);
-	}
-}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff