ソースを参照

Re-merged ref-overrides2 with fixes for crash

Clément Espeute 4 ヶ月 前
コミット
4700951358

+ 14 - 5
bin/style.css

@@ -1128,11 +1128,6 @@ input[type=checkbox].indeterminate:after {
   word-break: break-word;
   margin-top: 4px;
 }
-.hide-properties dd span,
-.hide-properties dt span {
-  display: inline-block;
-  width: 20px;
-}
 .hide-properties dt {
   width: calc(80px - var(--level) * 2%);
   text-align: right;
@@ -1166,6 +1161,9 @@ input[type=checkbox].indeterminate:after {
 .hide-properties input[type=checkbox] {
   width: 13px;
 }
+.hide-properties .warning {
+  color: red;
+}
 .hide-properties dt input[type=button] {
   width: 100%;
   text-align: right;
@@ -2733,6 +2731,17 @@ body.hide-subview .lm_controls {
 .jstree .inRef {
   background: #444444 !important;
 }
+.jstree .isOverride {
+  background: #25346d !important;
+}
+.jstree .isOverride.isOverriden > i,
+.jstree .isOverride.isOverriden > a {
+  color: cyan !important;
+}
+.jstree .isOverride.isOverriden.isOverridenNew i,
+.jstree .isOverride.isOverriden.isOverridenNew a {
+  color: #26c726 !important;
+}
 .jstree a.locked {
   color: #666 !important;
   font-style: italic;

+ 20 - 5
bin/style.less

@@ -1231,11 +1231,6 @@ input[type=checkbox] {
 		text-wrap: wrap;
 		word-break: break-word;
 		margin-top:4px;
-
-		span {
-			display: inline-block;
-			width: 20px;
-		}
 	}
 
 	dt {
@@ -1278,6 +1273,10 @@ input[type=checkbox] {
 		width:13px;
 	}
 
+	.warning {
+		color: red;
+	}
+
 	dt {
 		input[type=button] {
 			width: 100%;
@@ -3165,6 +3164,22 @@ body.hide-subview {
 		background: rgb(68, 68, 68) !important;
 	}
 
+	.isOverride {
+		background: #25346d !important;
+	}
+
+	.isOverride.isOverriden {
+		> i, > a {
+			color: cyan !important;
+		}
+	}
+
+	.isOverride.isOverriden.isOverridenNew {
+		i, a {
+			color: rgb(38, 199, 38) !important;
+		}
+	}
+
 	a.locked {
 		color: #666 !important;
 		font-style: italic;

+ 249 - 152
hide/comp/SceneEditor.hx

@@ -238,10 +238,6 @@ class ViewportOverlaysPopup extends hide.comp.Popup {
 			var btn = addButton("Scene Info", "info-circle", "sceneInformationToggle", () -> editor.updateStatusTextVisibility()).appendTo(group);
 			addButton("Wireframe", "connectdevelop", "wireframeToggle", () -> editor.updateWireframe()).appendTo(group);
 			addButton("Disable Scene Render", "eye-slash", "tog-scene-render", () -> {}).appendTo(group);
-
-			//btn.contextmenu(() -> {
-			//
-			//})
 		}
 
 
@@ -717,46 +713,46 @@ class ViewModePopup extends hide.comp.Popup {
 }
 
 class IconVisibilityPopup extends hide.comp.Popup {
-    var editor : SceneEditor;
+	 var editor : SceneEditor;
 
-    public function new(?parent : Element, editor: SceneEditor) {
-        super(parent);
-        this.editor = editor;
+	 public function new(?parent : Element, editor: SceneEditor) {
+		  super(parent);
+		  this.editor = editor;
 
-        element.append(new Element("<p>Icon Visibility</p>"));
-        element.addClass("settings-popup");
-        element.css("max-width", "300px");
+		  element.append(new Element("<p>Icon Visibility</p>"));
+		  element.addClass("settings-popup");
+		  element.css("max-width", "300px");
 
-        var form_div = new Element("<div>").addClass("form-grid").appendTo(element);
+		  var form_div = new Element("<div>").addClass("form-grid").appendTo(element);
 
-        var editMode : hrt.tools.Gizmo.EditMode = @:privateAccess editor.gizmo.editMode;
+		  var editMode : hrt.tools.Gizmo.EditMode = @:privateAccess editor.gizmo.editMode;
 
 		var ide = hide.Ide.inst;
-        for (k => v in ide.show3DIconsCategory) {
-            var input = new Element('<input type="checkbox" name="snap" id="$k" value="$k"/>');
-            if (v)
-                input.get(0).toggleAttribute("checked", true);
-            input.change((e) -> {
+		  for (k => v in ide.show3DIconsCategory) {
+				var input = new Element('<input type="checkbox" name="snap" id="$k" value="$k"/>');
+				if (v)
+					 input.get(0).toggleAttribute("checked", true);
+				input.change((e) -> {
 				var val = !ide.show3DIconsCategory.get(k);
 				ide.show3DIconsCategory.set(k, val);
 				js.Browser.window.localStorage.setItem(hrt.impl.EditorTools.iconVisibilityKey(k), val ? "true" : "false");
-            });
-            form_div.append(input);
-            form_div.append(new Element('<label for="$k" class="left">$k</label>'));
-        }
-    }
+				});
+				form_div.append(input);
+				form_div.append(new Element('<label for="$k" class="left">$k</label>'));
+		  }
+	 }
 }
 
 class HelpPopup extends hide.comp.Popup {
 	var editor : SceneEditor;
 
 	public function new(?parent : Element, editor: SceneEditor, ?shortcuts: Array<{name:String, shortcut:String}>) {
-        super(parent);
-        this.editor = editor;
+		  super(parent);
+		  this.editor = editor;
 
-        element.append(new Element("<p>Shortcuts</p>"));
-        element.addClass("settings-popup");
-        element.css("max-width", "300px");
+		  element.append(new Element("<p>Shortcuts</p>"));
+		  element.addClass("settings-popup");
+		  element.css("max-width", "300px");
 
 		var form_div = new Element("<div>").addClass("form-grid").appendTo(element);
 
@@ -916,9 +912,9 @@ class RenderPropsPopup extends Popup {
 @:access(hide.comp.SceneEditor)
 class CustomEditor {
 
-    var ide(get, never) : hide.Ide;
+	 var ide(get, never) : hide.Ide;
 	function get_ide() { return editor.ide; }
-    var editor : SceneEditor;
+	 var editor : SceneEditor;
 
 	var element : hide.Element;
 
@@ -926,9 +922,9 @@ class CustomEditor {
 		this.editor = editor;
 	}
 
-    public function setElementSelected( p : hrt.prefab.Prefab, b : Bool ) {
+	 public function setElementSelected( p : hrt.prefab.Prefab, b : Bool ) {
 		return true;
-    }
+	 }
 
 	public function update( dt : Float ) {
 
@@ -991,11 +987,11 @@ class SceneEditor {
 	public var curEdit(default, null) : SceneEditorContext;
 	public var snapToGround = false;
 
-    public var snapToggle = false;
-    public var snapMoveStep = 1.0;
-    public var snapRotateStep = 15.0;
-    public var snapScaleStep = 1.0;
-    public var snapForceOnGrid = false;
+	 public var snapToggle = false;
+	 public var snapMoveStep = 1.0;
+	 public var snapRotateStep = 15.0;
+	 public var snapScaleStep = 1.0;
+	 public var snapForceOnGrid = false;
 
 	public var localTransform = true;
 	public var selfOnlyTransform = false;
@@ -1082,11 +1078,17 @@ class SceneEditor {
 
 	public var lastFocusObjects : Array<Object> = [];
 
-	public function new(view, data) {
+
+	// Called when the sceneEditor scene has finished loading
+	// Use it to call setPrefab() to set the content of the scene
+	dynamic public function onSceneReady() {
+
+	}
+
+	public function new(view) {
 		ready = false;
 		ide = hide.Ide.inst;
 		this.view = view;
-		this.sceneData = data;
 
 		event = new hxd.WaitEvent();
 
@@ -1106,7 +1108,7 @@ class SceneEditor {
 		var sceneEl = new Element('<div class="heaps-scene"></div>');
 		scene = new hide.comp.Scene(view.config, null, sceneEl);
 		scene.editor = this;
-		scene.onReady = onSceneReady;
+		scene.onReady = onSceneReadyInternal;
 		scene.onResize = function() {
 			if( cameraController2D != null ) cameraController2D.toTarget();
 			onResize();
@@ -1307,23 +1309,23 @@ class SceneEditor {
 		grid.scale(1);
 		grid.material.mainPass.setPassName("overlay");
 
-        if (snapToggle) {
-    		gridStep = snapMoveStep;
-        }
-        else {
-            gridStep = ide.currentConfig.get("sceneeditor.gridStep");
-        }
+		  if (snapToggle) {
+	 		gridStep = snapMoveStep;
+		  }
+		  else {
+				gridStep = ide.currentConfig.get("sceneeditor.gridStep");
+		  }
 		gridSize = ide.currentConfig.get("sceneeditor.gridSize");
 
 		var col = h3d.Vector.fromColor(scene?.engine?.backgroundColor ?? 0);
 		var hsl = col.toColorHSL();
 
-        var mov = 0.1;
+		  var mov = 0.1;
 
-        if (snapToggle) {
-            mov = 0.2;
-            hsl.y += (1.0-hsl.y) * 0.2;
-        }
+		  if (snapToggle) {
+				mov = 0.2;
+				hsl.y += (1.0-hsl.y) * 0.2;
+		  }
 		if(hsl.z > 0.5) hsl.z -= mov;
 		else hsl.z += mov;
 
@@ -1348,12 +1350,12 @@ class SceneEditor {
 
 		var hsl = color.toColorHSL();
 
-        var mov = 0.1;
+		  var mov = 0.1;
 
-        if (snapToggle) {
-            mov = 0.2;
-            hsl.y += (1.0-hsl.y) * 0.2;
-        }
+		  if (snapToggle) {
+				mov = 0.2;
+				hsl.y += (1.0-hsl.y) * 0.2;
+		  }
 		if(hsl.z > 0.5) hsl.z -= mov;
 		else hsl.z += mov;
 
@@ -1428,16 +1430,16 @@ class SceneEditor {
 		haxe.Timer.delay(function() event.wait(0.5, updateStats), 0);
 	}
 
-    public function getSnapStatus() : Bool {
-        var ctrl = K.isDown(K.CTRL);
-        return (snapToggle && !ctrl) || (!snapToggle && ctrl);
-    };
+	 public function getSnapStatus() : Bool {
+		  var ctrl = K.isDown(K.CTRL);
+		  return (snapToggle && !ctrl) || (!snapToggle && ctrl);
+	 };
 
-    public function snap(value: Float, step:Float) : Float {
-        if (step > 0.0 && getSnapStatus())
-            value = hxd.Math.round(value / step) * step;
-        return value;
-    }
+	 public function snap(value: Float, step:Float) : Float {
+		  if (step > 0.0 && getSnapStatus())
+				value = hxd.Math.round(value / step) * step;
+		  return value;
+	 }
 
 	public function gizmoSnap(value: Float, mode: hrt.tools.Gizmo.EditMode) : Float {
 		switch(mode) {
@@ -1522,7 +1524,7 @@ class SceneEditor {
 		if (tree == null)
 			tree = this.tree;
 
-        focusObjects(getSelectedLocal3D());
+		  focusObjects(getSelectedLocal3D());
 		var selected3d = getSelectedLocal3D();
 		for(obj in selectedPrefabs)
 			tree.revealNode(obj);
@@ -1712,9 +1714,9 @@ class SceneEditor {
 			return;
 
 		var id = Std.parseInt(settings.camTypeIndex) ?? 0;
-        var newClass = CameraControllerEditor.controllersClasses[id];
-        if (Type.getClass(cameraController) != newClass.cl)
-            switchCamController(newClass.cl);
+		  var newClass = CameraControllerEditor.controllersClasses[id];
+		  if (Type.getClass(cameraController) != newClass.cl)
+				switchCamController(newClass.cl);
 
 		scene.s3d.camera.pos.set(settings.x, settings.y, settings.z);
 		scene.s3d.camera.target.set(settings.tx, settings.ty, settings.tz);
@@ -1769,53 +1771,45 @@ class SceneEditor {
 		}
 	}
 
-    function loadSnapSettings() {
-        function sanitize(value:Dynamic, def: Dynamic) {
-            if (value == null || value == 0.0)
-                return def;
-            return value;
-        }
-        @:privateAccess snapMoveStep = sanitize(view.getDisplayState("snapMoveStep"), snapMoveStep);
-        @:privateAccess snapRotateStep = sanitize(view.getDisplayState("snapRotateStep"), snapRotateStep);
-        @:privateAccess snapScaleStep = sanitize(view.getDisplayState("snapScaleStep"), snapScaleStep);
-        @:privateAccess snapForceOnGrid = view.getDisplayState("snapForceOnGrid");
-    }
-
-    public function saveSnapSettings() {
-        @:privateAccess view.saveDisplayState("snapMoveStep", snapMoveStep);
-        @:privateAccess view.saveDisplayState("snapRotateStep", snapRotateStep);
-        @:privateAccess view.saveDisplayState("snapScaleStep", snapScaleStep);
-        @:privateAccess view.saveDisplayState("snapForceOnGrid", snapForceOnGrid);
-    }
-
-    function toggleSnap(?force: Bool) {
-        if (force != null)
-            snapToggle = force;
-        else
-            snapToggle = !snapToggle;
-
-        var snap = new Element("#snap").get(0);
-        if (snap != null) {
-            snap.toggleAttribute("checked", snapToggle);
-        }
-
-        updateGrid();
-    }
-
-	function onSceneReady() {
-		// Load display state
-		{
-			var all = sceneData.flatten(PrefabElement, null);
-			var list = @:privateAccess view.getDisplayState("hideList");
-			if(list != null) {
-				var m = [for(i in (list:Array<Dynamic>)) i => true];
-				for(p in all) {
-					if(m.exists(p.getAbsPath(true, true)))
-						hideList.set(p, true);
-				}
-			}
+	function loadSnapSettings() {
+		function sanitize(value:Dynamic, def: Dynamic) {
+			if (value == null || value == 0.0)
+					return def;
+			return value;
 		}
+		@:privateAccess snapMoveStep = sanitize(view.getDisplayState("snapMoveStep"), snapMoveStep);
+		@:privateAccess snapRotateStep = sanitize(view.getDisplayState("snapRotateStep"), snapRotateStep);
+		@:privateAccess snapScaleStep = sanitize(view.getDisplayState("snapScaleStep"), snapScaleStep);
+		@:privateAccess snapForceOnGrid = view.getDisplayState("snapForceOnGrid");
+	}
+
+	public function saveSnapSettings() {
+		@:privateAccess view.saveDisplayState("snapMoveStep", snapMoveStep);
+		@:privateAccess view.saveDisplayState("snapRotateStep", snapRotateStep);
+		@:privateAccess view.saveDisplayState("snapScaleStep", snapScaleStep);
+		@:privateAccess view.saveDisplayState("snapForceOnGrid", snapForceOnGrid);
+	}
 
+	function toggleSnap(?force: Bool) {
+		if (force != null)
+			snapToggle = force;
+		else
+			snapToggle = !snapToggle;
+
+		var snap = new Element("#snap").get(0);
+		if (snap != null) {
+			snap.toggleAttribute("checked", snapToggle);
+		}
+
+		updateGrid();
+	}
+
+	public function setPrefab(prefab: hrt.prefab.Prefab) {
+		sceneData = prefab;
+		refreshScene();
+	}
+
+	function onSceneReadyInternal() {
 		tree.saveDisplayKey = view.saveDisplayKey + '/tree';
 		renderPropsTree.saveDisplayKey = view.saveDisplayKey + '/renderPropsTree';
 
@@ -1900,7 +1894,7 @@ class SceneEditor {
 				value : o,
 				text : o.name,
 				icon : "ico ico-"+icon,
-				children : o.children.length > 0 || (ref != null && @:privateAccess ref.editMode),
+				children : o.children.length > 0 || (ref != null && ref.editMode != None),
 				state: state
 			};
 			return r;
@@ -1918,7 +1912,7 @@ class SceneEditor {
 				objs = visibleObjs;
 			}
 			var ref = o == null ? null : o.to(Reference);
-			@:privateAccess if( ref != null && ref.editMode && ref.refInstance != null ) {
+			@:privateAccess if( ref != null && ref.editMode != None && ref.refInstance != null ) {
 				for( c in ref.refInstance )
 					objs.push(c);
 			}
@@ -1927,6 +1921,8 @@ class SceneEditor {
 		};
 
 		tree.get = function(o:PrefabElement) {
+			if (sceneData == null)
+				return [];
 			var objs = o == null ? sceneData.children : Lambda.array(o);
 			return getFunc(objs, o);
 		};
@@ -2137,17 +2133,21 @@ class SceneEditor {
 		tree.applyStyle = function(p, el) applyTreeStyle(p, el);
 		renderPropsTree.applyStyle = function(p, el) applyTreeStyle(p, el, renderPropsTree);
 
-		selectElements([]);
-		refreshScene();
+		ready = true;
+
+		onSceneReady();
+
+		selectElements([], NoHistory);
 		this.camera2D = camera2D;
 
 		updateViewportOverlays();
 
-		ready = true;
+
 		for (callback in readyDelayed) {
 			callback();
 		}
 		readyDelayed.empty();
+
 	}
 
 	function checkAllowParent(prefabInf:hrt.prefab.Prefab.PrefabInfo, prefabParent : PrefabElement) : Bool {
@@ -2171,7 +2171,17 @@ class SceneEditor {
 		tree.collapseAll();
 	}
 
+	var treeRefreshing = false;
+	var queueRefresh : Array<() -> Void> = null;
+
 	function refreshTree( ?callb ) {
+		if (treeRefreshing) {
+			queueRefresh ??= [];
+			if (callb != null)
+				queueRefresh.push(callb);
+			return;
+		}
+		treeRefreshing = true;
 		tree.refresh(function() {
 			var all = sceneData.flatten(PrefabElement);
 			for(elt in all) {
@@ -2183,6 +2193,13 @@ class SceneEditor {
 			tree.setSelection(selectedPrefabs);
 
 			if(callb != null) callb();
+
+			treeRefreshing = false;
+			if (queueRefresh != null) {
+				var list = queueRefresh;
+				queueRefresh = null;
+				refreshTree(() -> for (cb in list) cb());
+			}
 		});
 
 		renderPropsTree.refresh(function() {
@@ -2359,7 +2376,7 @@ class SceneEditor {
 		if (renderPropsRoot == null && path != null) {
 			renderPropsRoot = new hrt.prefab.Reference(null, new ContextShared());
 			renderPropsRoot.setEditor(this, this.scene);
-			renderPropsRoot.editMode = Ide.inst.currentConfig.get("sceneeditor.renderprops.edit", false);
+			renderPropsRoot.editMode = Ide.inst.currentConfig.get("sceneeditor.renderprops.edit", false) ? Edit : None;
 			renderPropsRoot.name = "Render Props";
 			renderPropsRoot.source = path;
 
@@ -2398,13 +2415,16 @@ class SceneEditor {
 	}
 
 	public function refreshScene() {
+
 		clearWatches();
 
 		if (root2d != null) root2d.remove();
 		if (root3d != null) root3d.remove();
 
-		if (sceneData != null)
-			sceneData.dispose();
+		if (sceneData == null)
+			return;
+
+		sceneData.dispose();
 
 		hrt.impl.Gradient.purgeEditorCache();
 
@@ -2449,6 +2469,19 @@ class SceneEditor {
 		scene.init();
 		scene.engine.backgroundColor = bgcol;
 
+		// Load display state
+		{
+			var all = sceneData.flatten(PrefabElement, null);
+			var list = @:privateAccess view.getDisplayState("hideList");
+			if(list != null) {
+				var m = [for(i in (list:Array<Dynamic>)) i => true];
+				for(p in all) {
+					if(m.exists(p.getAbsPath(true, true)))
+						hideList.set(p, true);
+				}
+			}
+		}
+
 		rebuild(sceneData);
 
 		var all = sceneData.all();
@@ -2460,6 +2493,8 @@ class SceneEditor {
 		setRenderProps();
 
 		onRefresh();
+
+
 	}
 
 	function getAllWithRefs<T:PrefabElement>( p : PrefabElement, cl : Class<T>, ?arr : Array<T>, forceLoad: Bool = false ) : Array<T> {
@@ -2490,10 +2525,6 @@ class SceneEditor {
 			if( isLocked(elt) ) toggleInteractive(elt, false);
 		}
 		var ref = Std.downcast(elt,Reference);
-		@:privateAccess if( ref != null && ref.editMode && ref.refInstance != null ) {
-			for( p in ref.refInstance.flatten() )
-				makeInteractive(p);
-		}
 	}
 
 	function toggleInteractive( e : PrefabElement, visible : Bool ) {
@@ -2660,8 +2691,10 @@ class SceneEditor {
 	}
 
 	public function refreshInteractive(elt : PrefabElement) {
-		removeInteractive(elt);
-		makeInteractive(elt);
+		for (p in elt.flatten(null, null)) {
+			removeInteractive(p);
+			makeInteractive(p);
+		}
 	}
 
 	public function removeInteractive(elt: PrefabElement) {
@@ -2728,7 +2761,7 @@ class SceneEditor {
 				if(rot != null) {
 					rot.toMatrix(transf);
 
-                }
+					 }
 				if(translate != null)
 					transf.translate(translate.x, translate.y, translate.z);
 				for(i in 0...sceneObjs.length) {
@@ -2762,21 +2795,21 @@ class SceneEditor {
 					var obj3d = objects3d[i];
 					var obj3dPrevTransform = obj3d.getTransform();
 					var euler = newMat.getEulerAngles();
-                    if (translate != null && translate.length() > 0.0001 && snapForceOnGrid) {
-                        obj3d.x = snap(quantize(newMat.tx, posQuant), snapMoveStep);
-                        obj3d.y = snap(quantize(newMat.ty, posQuant), snapMoveStep);
-                        obj3d.z = snap(quantize(newMat.tz, posQuant), snapMoveStep);
-                    }
-                    else { // Don't snap translation if the primary action wasn't a translation (i.e. Rotation around a pivot)
+						  if (translate != null && translate.length() > 0.0001 && snapForceOnGrid) {
+								obj3d.x = snap(quantize(newMat.tx, posQuant), snapMoveStep);
+								obj3d.y = snap(quantize(newMat.ty, posQuant), snapMoveStep);
+								obj3d.z = snap(quantize(newMat.tz, posQuant), snapMoveStep);
+						  }
+						  else { // Don't snap translation if the primary action wasn't a translation (i.e. Rotation around a pivot)
 						obj3d.x = quantize(newMat.tx, posQuant);
 						obj3d.y = quantize(newMat.ty, posQuant);
 						obj3d.z = quantize(newMat.tz, posQuant);
 					}
 
-                    if (rot != null) {
-                        obj3d.rotationX = quantize(M.radToDeg(euler.x), rotQuant);
-                        obj3d.rotationY = quantize(M.radToDeg(euler.y), rotQuant);
-                        obj3d.rotationZ = quantize(M.radToDeg(euler.z), rotQuant);
+						  if (rot != null) {
+								obj3d.rotationX = quantize(M.radToDeg(euler.x), rotQuant);
+								obj3d.rotationY = quantize(M.radToDeg(euler.y), rotQuant);
+								obj3d.rotationZ = quantize(M.radToDeg(euler.z), rotQuant);
 					}
 
 					if(scale != null) {
@@ -2941,10 +2974,10 @@ class SceneEditor {
 			var engine = h3d.Engine.getCurrent();
 			var ratio = 150 / engine.height;
 
-            var scale = ratio * distToCam * Math.tan(cam.fovY * 0.5 * Math.PI / 180.0);
-            if (cam.orthoBounds != null) {
-                scale = ratio *  (cam.orthoBounds.xSize) * 0.5;
-            }
+				var scale = ratio * distToCam * Math.tan(cam.fovY * 0.5 * Math.PI / 180.0);
+				if (cam.orthoBounds != null) {
+					 scale = ratio *  (cam.orthoBounds.xSize) * 0.5;
+				}
 			basis.setScale(scale);
 
 		} else {
@@ -3158,15 +3191,21 @@ class SceneEditor {
 		}
 
 		var modifiedRef = Std.downcast(p.shared.parentPrefab, hrt.prefab.Reference);
-		if (modifiedRef != null) {
+		if (modifiedRef != null && modifiedRef.editMode == Edit) {
 			var path = modifiedRef.source;
 
 			var others = sceneData.findAll(Reference, (r) -> r.source == path && r != modifiedRef, true);
 			@:privateAccess
-			for (ref in others) {
-				removeInstance(ref.refInstance, false);
-				ref.refInstance = modifiedRef.refInstance.clone();
-				queueRebuild(ref);
+			if (others.length > 0) {
+				var data = modifiedRef.refInstance.serialize();
+				beginRebuild();
+				for (ref in others) {
+					removeInstance(ref.refInstance, false);
+					@:privateAccess ref.setRef(data);
+					queueRebuild(ref);
+				}
+				endRebuild();
+				refreshTree();
 			}
 		}
 
@@ -3181,8 +3220,59 @@ class SceneEditor {
 		var obj3d  = p.to(Object3D);
 		el.toggleClass("disabled", !p.enabled);
 		var aEl = el.find("a").first();
-		var root = p.getRoot();
-		el.toggleClass("inRef", root != sceneData);
+
+		// reference
+		var isOverride = false;
+		var isOverriden = false;
+		var isOverridenNew = false;
+		var inRef = false;
+		if (p.shared.parentPrefab != null) {
+			var parentRef = Std.downcast(p.shared.parentPrefab, Reference);
+			if (parentRef != null) {
+				if (parentRef.editMode == Override) {
+					isOverride = true;
+
+					var path = [];
+					var current = p;
+					while (current != null) {
+						path.push(current);
+						current = current.parent;
+					}
+
+					var currentOverride = @:privateAccess parentRef.computeDiffFromSource();
+
+					// skip first item in the path
+					path.pop();
+					while(currentOverride != null && path.length > 0) {
+						var current = path.pop();
+						if (currentOverride.children != null) {
+							currentOverride = Reflect.field(currentOverride.children, current.name);
+						}
+					}
+
+					if (currentOverride != null) {
+						var overridenFields = Reflect.fields(currentOverride);
+						overridenFields.remove("children");
+						if (overridenFields.length > 0) {
+							isOverriden = true;
+							if (currentOverride.type != null) {
+								isOverridenNew = true;
+							}
+						}
+					}
+
+				} else {
+					inRef = true;
+				}
+			}
+		}
+
+
+		el.toggleClass("inRef", inRef);
+		el.toggleClass("isOverride", isOverride);
+		el.toggleClass("isOverriden", isOverriden);
+		el.toggleClass("isOverridenNew", isOverridenNew);
+
 
 		var tag = getTag(p);
 
@@ -4049,7 +4139,7 @@ class SceneEditor {
 	function groupSelection() {
 		if(!canGroupSelection()) {
 			return;
-        }
+		  }
 
 		// Sort the selection to match the scene order
 		var elts : Array<hrt.prefab.Prefab> = [];
@@ -4571,7 +4661,7 @@ class SceneEditor {
 			return;
 
 		var ref = Std.downcast(to, Reference);
-		@:privateAccess if( ref != null && ref.editMode ) to = ref.refInstance;
+		@:privateAccess if( ref != null && ref.editMode != None ) to = ref.refInstance;
 
 		// Sort node based on where they appear in the scene tree
 		var flat = sceneData.flatten();
@@ -4756,12 +4846,19 @@ class SceneEditor {
 		}
 	}
 
+	var beginRebuildStack = 0;
 	function beginRebuild() {
+		beginRebuildStack++;
+		if (beginRebuildStack > 1)
+			return;
 		rebuildQueue = [];
 		rebuildEndCallbacks = [];
 	}
 
 	function endRebuild() {
+		beginRebuildStack --;
+		if (beginRebuildStack > 0)
+			return;
 		for (prefab => want in rebuildQueue) {
 			switch (want) {
 				case Skip:

+ 34 - 41
hide/view/FXEditor.hx

@@ -43,16 +43,11 @@ private class FXSceneEditor extends hide.comp.SceneEditor {
 	public var is2D : Bool = false;
 
 
-	public function new(view,  data) {
-		super(view, data);
+	public function new(view) {
+		super(view);
 		parent = cast view;
 	}
 
-	override function onSceneReady() {
-		super.onSceneReady();
-		parent.onSceneReady();
-	}
-
 	override function onPrefabChange(p: PrefabElement, ?pname: String) {
 		super.onPrefabChange(p, pname);
 		parent.onPrefabChange(p, pname);
@@ -345,6 +340,8 @@ class FXEditor extends hide.view.FileView {
 	var xOffset = 0.;
 	var tlKeys: Array<{name:String, shortcut:String}> = [];
 
+	var fxprops : hide.comp.PropsEditor;
+
 	var pauseButton : hide.comp.Toolbar.ToolToggle;
 	@:isVar var currentTime(get, set) : Float;
 	var selectMin : Float;
@@ -355,8 +352,6 @@ class FXEditor extends hide.view.FileView {
 	var afterPanRefreshes : Array<Bool->Void> = [];
 	var statusText : h2d.Text;
 
-	var scriptEditor : hide.comp.ScriptEditor;
-	//var fxScriptParser : hrt.prefab.fx.FXScriptParser;
 	var cullingPreview : h3d.scene.Sphere;
 
     var viewModes : Array<String>;
@@ -393,8 +388,6 @@ class FXEditor extends hide.view.FileView {
 		var content = sys.io.File.getContent(getPath());
 		var json = haxe.Json.parse(content);
 
-
-		data = cast(PrefabElement.createFromDynamic(json), hrt.prefab.fx.BaseFX);
 		currentSign = ide.makeSignature(content);
 
 		element.html('
@@ -440,16 +433,13 @@ class FXEditor extends hide.view.FileView {
 						<div class="tab expand" name="Properties" icon="cog">
 							<div class="fx-props"></div>
 						</div>
-						<div class="tab expand" name="Script" icon="cog">
-							<div class="fx-script"></div>
-							<div class="fx-scriptParams"></div>
-						</div>
 					</div>
 				</div>
 			</div>');
 		tools = new hide.comp.Toolbar(null,element.find(".tools-buttons"));
 		var tabs = new hide.comp.Tabs(null,element.find(".tabs"));
-		sceneEditor = new FXSceneEditor(this, cast(data, hrt.prefab.Prefab));
+		sceneEditor = new FXSceneEditor(this);
+		sceneEditor.onSceneReady = onSceneReady;
 
 		for (callback in sceneReadyDelayed) {
 			sceneEditor.delayReady(callback);
@@ -515,42 +505,19 @@ class FXEditor extends hide.view.FileView {
 		element.find(".collapse-btn").click(function(e) {
 			sceneEditor.collapseTree();
 		});
-		var fxprops = new hide.comp.PropsEditor(undo,null,element.find(".fx-props"));
-		{
-			var edit = new FXEditContext(this);
-			edit.properties = fxprops;
-			edit.scene = sceneEditor.scene;
-			edit.cleanups = [];
-			cast(data, hrt.prefab.Prefab).edit(edit);
-		}
+		fxprops = new hide.comp.PropsEditor(undo,null,element.find(".fx-props"));
+
 
 		if (is2D) {
 			sceneEditor.camera2D = true;
 		}
 
-		var scriptElem = element.find(".fx-script");
-		scriptEditor = new hide.comp.ScriptEditor(data.scriptCode, null, scriptElem, scriptElem);
-		function onSaveScript() {
-			data.scriptCode = scriptEditor.code;
-			save();
-			skipNextChange = true;
-			modified = false;
-		}
-		scriptEditor.onSave = onSaveScript;
-		//fxScriptParser = new hrt.prefab.fx.FXScriptParser();
-		data.scriptCode = scriptEditor.code;
-
 		keys.register("playPause", function() { pauseButton.toggle(!pauseButton.isDown()); });
 
 		currentVersion = undo.currentID;
 		sceneEditor.tree.element.addClass("small");
 		sceneEditor.renderPropsTree.element.addClass("small");
 
-		selectMin = 0.0;
-		selectMax = 0.0;
-		previewMin = 0.0;
-		previewMax = data.duration == 0 ? 5000 : data.duration;
-
 		var rpEditionvisible = Ide.inst.currentConfig.get("sceneeditor.renderprops.edit", false);
 		setRenderPropsEditionVisibility(rpEditionvisible);
 	}
@@ -583,6 +550,27 @@ class FXEditor extends hide.view.FileView {
 		setRenderPropsEditionVisibility(Ide.inst.currentConfig.get("sceneeditor.renderprops.edit", false));
 	}
 	public function onSceneReady() {
+		data = cast(hxd.res.Loader.currentInstance.load(state.path).toPrefab().load().clone(), hrt.prefab.fx.BaseFX);
+		if (data == null) {
+			throw "Prefab is not a FX";
+			return;
+		}
+
+		sceneEditor.setPrefab(cast data);
+
+		selectMin = 0.0;
+		selectMax = 0.0;
+		previewMin = 0.0;
+		previewMax = data.duration == 0 ? 5000 : data.duration;
+
+		{
+			var edit = new FXEditContext(this);
+			edit.properties = fxprops;
+			edit.scene = sceneEditor.scene;
+			edit.cleanups = [];
+			cast(data, hrt.prefab.Prefab).edit(edit);
+		}
+
 		var axis = new h3d.scene.Graphics(scene.s3d);
 		axis.z = 0.001;
 		axis.lineStyle(2,0xFF0000); axis.lineTo(1,0,0);
@@ -667,6 +655,8 @@ class FXEditor extends hide.view.FileView {
 
 		statusText = new h2d.Text(hxd.res.DefaultFont.get(), scene.s2d);
 		statusText.setPosition(5, 5);
+
+		rebuildAnimPanel();
 	}
 
 	function onPrefabChange(p: PrefabElement, ?pname: String) {
@@ -1259,6 +1249,9 @@ class FXEditor extends hide.view.FileView {
 	}
 
 	function rebuildAnimPanel() {
+		if (@:privateAccess !sceneEditor.ready)
+			return;
+
 		if(element == null)
 			return;
 

+ 8 - 2
hide/view/Model.hx

@@ -80,11 +80,12 @@ class Model extends FileView {
 		tabs = new hide.comp.Tabs(null,element.find(".tabs"));
 		eventList = element.find(".event-editor");
 
-		root = new hrt.prefab.Prefab(null, null);
 		var def = new hrt.prefab.Prefab(null, null);
 		new hrt.prefab.RenderProps(def, null).name = "renderer";
 		var l = new hrt.prefab.Light(def, null);
-		sceneEditor = new hide.comp.SceneEditor(this, root);
+		sceneEditor = new hide.comp.SceneEditor(this);
+		sceneEditor.onSceneReady = onSceneReady;
+
 		sceneEditor.editorDisplay = false;
 		sceneEditor.onRefresh = onRefresh;
 		sceneEditor.onUpdate = onUpdate;
@@ -1200,6 +1201,11 @@ class Model extends FileView {
 	// Scene editor bindings
 	inline function get_scene() return sceneEditor.scene;
 
+	function onSceneReady() {
+		root = new hrt.prefab.Prefab(null, null);
+		sceneEditor.setPrefab(root);
+	}
+
 	function onRefresh() {
 		this.saveDisplayKey = "Model:" + state.path;
 

+ 8 - 10
hide/view/Prefab.hx

@@ -52,8 +52,8 @@ class FiltersPopup extends hide.comp.Popup {
 class PrefabSceneEditor extends hide.comp.SceneEditor {
 	var parent : Prefab;
 
-	public function new(view, data) {
-		super(view, data);
+	public function new(view) {
+		super(view);
 		parent = cast view;
 		this.localTransform = false; // TODO: Expose option
 	}
@@ -63,11 +63,6 @@ class PrefabSceneEditor extends hide.comp.SceneEditor {
 		parent.onUpdate(dt);
 	}
 
-	override function onSceneReady() {
-		super.onSceneReady();
-		parent.onSceneReady();
-	}
-
 	override function applyTreeStyle(p: PrefabElement, el: Element, ?pname: String, ?tree: hide.comp.IconTree<PrefabElement>) {
 		super.applyTreeStyle(p, el, pname, tree);
 		parent.applyTreeStyle(p, el, pname);
@@ -239,7 +234,8 @@ class Prefab extends hide.view.FileView {
 	}
 
 	function createEditor() {
-		sceneEditor = new PrefabSceneEditor(this, data);
+		sceneEditor = new PrefabSceneEditor(this);
+		sceneEditor.onSceneReady = onSceneReady;
 		for (callback in sceneReadyDelayed) {
 			sceneEditor.delayReady(callback);
 		}
@@ -248,10 +244,8 @@ class Prefab extends hide.view.FileView {
 
 	override function onDisplay() {
 		if( sceneEditor != null ) sceneEditor.dispose();
-
 		createData();
 		var content = sys.io.File.getContent(getPath());
-		data = hrt.prefab.Prefab.createFromDynamic(haxe.Json.parse(content));
 		currentSign = ide.makeSignature(content);
 
 
@@ -439,6 +433,9 @@ class Prefab extends hide.view.FileView {
 	}
 
 	public function onSceneReady() {
+		data = hxd.res.Loader.currentInstance.load(state.path).toPrefab().load().clone();
+		sceneEditor.setPrefab(cast data);
+
 		refreshSceneFilters();
 		refreshGraphicsFilters();
 		refreshViewModes();
@@ -578,6 +575,7 @@ class Prefab extends hide.view.FileView {
 			initGraphicsFilters();
 			initSceneFilters();
 		}
+
 	}
 
 	function resetCamera( top : Bool ) {

+ 341 - 0
hrt/prefab/Diff.hx

@@ -0,0 +1,341 @@
+package hrt.prefab;
+
+enum DiffResult {
+	/**The two object are identical, don't save anything**/
+	Skip;
+
+	/**The two objects are different, save the result as diff**/
+	Set(diff: Dynamic);
+}
+
+/**
+	Utility class to get the difference between two dynamics
+
+	There are two main functions : diff and apply, and they are reciprocal
+	diff(A,B) = D
+	apply(A,D) = B
+
+	the diff has a special support for prefab if the diffPrefab function is used :
+	the children array is handled as a special case, and prefabs that change type are
+	fully serialized in the diff instead of just the delta
+
+	Special fields prefixed by an @ can appear in the diff, they are as follow :
+
+	@removed : an array of keys name that are present in A but were removed in B
+	@index : in the prefab children data, indicate that this child has changed index between the A.children and B.children array
+
+**/
+class Diff {
+
+	/**
+		Add or Set a key/value pair to a DiffResult. If "diff" was a Skip, it will become a Set({key: value})
+	**/
+	public static function addToDiff(diff: DiffResult, key: String, value: Dynamic) : DiffResult{
+		var v = switch(diff) {
+			case Skip:
+				var v = {};
+				Reflect.setField(v, key, value);
+				return Set(v);
+			case Set(v):
+				Reflect.setField(v, key, value);
+				return diff;
+		}
+	}
+
+	public static function deepCopy(v:Dynamic) : Dynamic {
+		return haxe.Json.parse(haxe.Json.stringify(v));
+	}
+
+	/**
+		Returns the difference of two values together
+	**/
+	public static function diff(originalValue: Dynamic, modifiedValue: Dynamic) : DiffResult {
+		var originalType = Type.typeof(originalValue);
+		var modifiedType = Type.typeof(modifiedValue);
+
+		if (!originalType.equals(modifiedType)) {
+			return Set(modifiedValue);
+		}
+
+		switch (modifiedType) {
+			case TNull:
+				// The only way we get here is if both types are null, so by definition they are both null and so there is no diff
+				return Skip;
+			case TInt | TFloat | TBool:
+				if (originalValue == modifiedValue) {
+					return Skip;
+				}
+			case TObject:
+				return diffObject(originalValue, modifiedValue);
+			case TClass(subClass): {
+				switch (subClass) {
+					case String:
+						if (originalValue == modifiedValue) {
+							return Skip;
+						}
+					case Array:
+						return diffArray(originalValue, modifiedValue);
+					default:
+						throw "Can't diff class " + subClass;
+				}
+			}
+			default:
+				throw "Unhandled type " + modifiedType;
+		}
+		return Set(modifiedValue);
+	}
+
+
+	/**
+		Same as diffObject, but handles `type` and `children` fields as a special case :
+		children is serialized as an object with prefabName: diffPrefab(prefab)
+		and if original.type != modified.type, the whole modified object is copied as is
+		(because we consider that changing the type of a prefab in a diff means the prefab was destroyed then re-created)
+	**/
+	public static function diffPrefab(original: Dynamic, modified: Dynamic) : DiffResult {
+		if (original == null || modified == null) {
+			if (original == modified)
+				return Skip;
+			return Set(deepCopy(modified));
+		}
+
+		if (original.type != modified.type)
+			return Set(deepCopy(modified));
+
+		var result = diffObject(original, modified, ["children"]); // we could skip "type" but because we are sure that the type are equals they will never be serialised
+
+		var resultChildren = {};
+
+		var originalChildren = original.children ?? [];
+		var modifiedChildren = modified.children ?? [];
+
+		var childrenMap : Map<String, {originals: Array<Dynamic>, modifieds: Array<Dynamic>}> = [];
+
+		for (index => child in originalChildren) {
+			hrt.tools.MapUtils.getOrPut(childrenMap, child.name ?? "", {originals: [], modifieds: []}).originals.push({index: index, child: child});
+		}
+
+		for (index => child in modifiedChildren) {
+			hrt.tools.MapUtils.getOrPut(childrenMap, child.name ?? "", {originals: [], modifieds: []}).modifieds.push({index: index, child: child});
+		}
+
+		for (name => data in childrenMap) {
+			for (index in 0...hxd.Math.imax(data.originals.length, data.modifieds.length)) {
+				var originalChild = data.originals[index];
+				var modifiedChild = data.modifieds[index];
+				var key = name;
+
+				#if editor
+				if ((originalChild?.child != null && originalChild.child.type == null) || (modifiedChild?.child != null && modifiedChild.child.type == null)) {
+					throw "can't diff child that have a missing `type`";
+				}
+				#end
+				if (index > 0)
+					key += '@$index';
+
+				var diff = diffPrefab(originalChild?.child, modifiedChild?.child);
+
+				if (originalChild?.index != modifiedChild?.index) {
+					if (modifiedChild?.index != null) {
+						diff = addToDiff(diff, "@index", modifiedChild.index);
+					}
+				}
+
+				switch(diff) {
+					case Skip:
+					case Set(value):
+						Reflect.setField(resultChildren, key, value);
+				}
+			}
+		}
+
+		if (Reflect.fields(resultChildren).length > 0) {
+			result = addToDiff(result, "children", resultChildren);
+		}
+
+		return result;
+	}
+
+	/**
+		Returns the difference between two dynamic objects
+	**/
+	public static function diffObject(original: Dynamic, modified: Dynamic, skipFields: Array<String> = null) : DiffResult {
+		skipFields ??= [];
+		var result = {};
+		var removedFields : Array<String> = [];
+
+		if (original == null || modified == null) {
+			if (original == modified)
+				return Skip;
+			return Set(deepCopy(modified));
+		}
+
+		// Mark fields as removed
+		for (originalField in Reflect.fields(original)) {
+			if (skipFields.contains(originalField))
+				continue;
+
+			if (!Reflect.hasField(modified, originalField)) {
+				removedFields.push(originalField);
+				continue;
+			}
+		}
+
+		for (modifiedField in Reflect.fields(modified)) {
+			if (skipFields.contains(modifiedField))
+				continue;
+
+			var originalValue = Reflect.getProperty(original, modifiedField);
+			var modifiedValue = Reflect.getProperty(modified, modifiedField);
+
+			switch(diff(originalValue, modifiedValue)) {
+				case Skip:
+				case Set(v):
+					Reflect.setField(result, modifiedField, v);
+			}
+		}
+
+		if (removedFields.length > 0) {
+			Reflect.setField(result, "@removed", removedFields);
+		}
+
+		if (Reflect.fields(result).length == 0)
+			return Skip;
+		return Set(result);
+	}
+
+	/**
+		Returns the difference between two arrays. If the arrays are found to be different, a full copy of
+		modified will be returned as a Set()
+	**/
+	public static function diffArray(original: Array<Dynamic>, modified: Dynamic) : DiffResult {
+		if (original.length != modified.length) {
+			return Set(deepCopy(modified));
+		}
+
+		for (index in 0...original.length) {
+			var originalValue = original[index];
+			var modifiedValue = modified[index];
+
+			switch(diff(originalValue, modifiedValue)) {
+				case Set(_):
+					// return the whole modified object when any field is different than the original
+					return Set(deepCopy(modified));
+				case Skip:
+			}
+		}
+		return Skip;
+	}
+
+	/**
+		Modifies `target` dynamic so `apply(a, diffObject(a, b)) == b`
+	**/
+	public static function apply(target: Dynamic, diff: Dynamic) : Dynamic {
+		if (diff == null)
+			return null;
+
+		if (target == null)
+			target = {};
+
+		if (diff.type != null && diff.type != target.type) {
+			return diff;
+		}
+
+		for (field in Reflect.fields(diff)) {
+			if (field == "children")
+			{
+				var targetChildren = Reflect.field(target, "children") ?? [];
+				var diffChildren = Reflect.field(diff, "children");
+
+				for (fields in Reflect.fields(diffChildren)) {
+					var diffChild = Reflect.field(diffChildren, fields);
+					var name = fields;
+					var split = name.split("@");
+					var nthChild = 0;
+					if (split.length == 2) {
+						name = split[0];
+						nthChild = Std.parseInt(split[1]);
+					}
+
+					var targetChild = null;
+					var originalIndex = targetChildren.length; // if we don't found any children with the right name in the array, this will make sure we add the newly created children at the end of the array
+					for (index => child in targetChildren) {
+						if (name == child.name) {
+							if (nthChild == 0) {
+								targetChild = child;
+								originalIndex = index;
+								break;
+							} else {
+								nthChild --;
+							}
+						}
+					}
+
+					// Remove child if null
+					if (diffChild == null) {
+						targetChildren[originalIndex] = null;
+						continue;
+					}
+
+					// Skip diff children that don't have type if they don't
+					// modify a prefab from target object (because we can't create a prefab without a type)
+					if (targetChild == null && diffChild.type == null) {
+							continue;
+					}
+
+					targetChildren[originalIndex] = apply(targetChild, diffChild);
+				}
+
+				// Reorder the targetChildren array based on @indexes.
+				// if the @index point to a slot already taken, find the next free slot
+				// This should ensure that arrays are somewhat coherent in bad situation like
+				// the target children array has been modified since the last diff
+				var finalChildren : Array<Dynamic> = [];
+				for (index => child in targetChildren) {
+					if (child == null) continue;
+					var changedIndex = Reflect.field(child, "@index");
+					var targetIndex = if (changedIndex != null) {
+						Reflect.deleteField(child, "@index");
+						changedIndex;
+					} else {
+						index;
+					}
+					while (finalChildren[targetIndex] != null) {
+						targetIndex ++;
+					}
+					finalChildren[targetIndex] = child;
+				}
+				// If a prefab has been removed, it get inserted as a null in the childrenArray
+				// we fix that here
+				finalChildren = finalChildren.filter((f) -> f != null);
+
+				Reflect.setField(target, "children", finalChildren);
+				continue;
+			}
+
+			if (field == "@removed") {
+				var removed = Reflect.field(diff, "@removed");
+				for (field in (removed:Array<String>)) {
+					Reflect.deleteField(target, field);
+				}
+				continue;
+			}
+
+			var targetValue = Reflect.getProperty(target, field);
+			var diffValue = Reflect.getProperty(diff, field);
+
+			var targetType = Type.typeof(targetValue);
+			var diffType = Type.typeof(diffValue);
+
+			switch (targetType) {
+				case TNull | TInt | TFloat | TBool | TClass(Array) | TClass(String):
+					Reflect.setField(target, field, diffValue);
+				case TObject:
+					apply(targetValue, diffValue);
+				default:
+					throw "unhandeld type " + targetType;
+			}
+		}
+		return target;
+	}
+}

+ 1 - 1
hrt/prefab/Light.hx

@@ -179,7 +179,7 @@ class Light extends Object3D {
 		}
 
 		#if editor
-		if (shared.parentPrefab == null || (Std.downcast(shared.parentPrefab, Reference)?.editMode && shared.parentPrefab != shared.editor?.renderPropsRoot)) {
+		if (shared.parentPrefab == null || (Std.downcast(shared.parentPrefab, Reference)?.editMode != None && shared.parentPrefab != shared.editor?.renderPropsRoot)) {
 			icon = hrt.impl.EditorTools.create3DIcon(object, hide.Ide.inst.getHideResPath("icons/PointLight.png"), 0.5, Light);
 		}
 		#end

+ 51 - 17
hrt/prefab/Prefab.hx

@@ -63,7 +63,11 @@ class Prefab {
 	/**
 		The associated source file (an image, a 3D model, etc.) if the prefab type needs it.
 	**/
-	@:s public var source : String;
+	@:s public var source(default, set) : String;
+
+	public function set_source(newSource: String) {
+		return source = newSource;
+	}
 
 	/**
 		The parent of the prefab in the tree view
@@ -516,10 +520,39 @@ class Prefab {
 		return [].iterator();
 	}
 
+
+	/**
+		Returns a name that will allow to disambiguate this
+		prefabs from siblings with the same name
+		Prefab with more than one sibling with the same name
+		will have their name formated as `name-<index>` unless their index is 0.
+	**/
+	public function getUniqueName() {
+		if (parent == null) {
+			return "";
+		}
+
+		var path = name;
+		var suffix = 0;
+		for(i => c in parent.children) {
+			if(c == this)
+				break;
+			else {
+				var cname = c.name ?? "";
+				if(cname == path)
+					++suffix;
+			}
+		}
+		if(suffix > 0)
+			path += "-" + suffix;
+		return path;
+	}
+
 	/**
 		Returns the absolute name path for this prefab
 	**/
 	public function getAbsPath(unique=false, followRef : Bool = false) {
+		var origParent = parent;
 		var parent = parent;
 		if (parent != null && followRef) {
 			var ref = Std.downcast(parent.shared.parentPrefab, Reference);
@@ -532,25 +565,14 @@ class Prefab {
 		if (path == "")
 			path = hrt.prefab.Prefab.emptyNameReplacement;
 		if(unique) {
-			var suffix = 0;
-			for(i in 0...parent.children.length) {
-				var c = parent.children[i];
-				if(c == this)
-					break;
-				else {
-					var cname = c.name ?? "";
-					if(cname == path)
-						++suffix;
-				}
-			}
-			if(suffix > 0)
-				path += "-" + suffix;
+			path = getUniqueName();
 		}
 		if(parent.parent != null)
 			path = parent.getAbsPath(unique) + "." + path;
 		return path;
 	}
 
+
 	/**
 		If the prefab `props` represent CDB data, returns the sheet name of it, or null.
 	 **/
@@ -727,20 +749,32 @@ class Prefab {
 	/**
 		Finds a prefab by folowing a dot separated path like this one : `parent.child.grandchild`.
 		Returns null if the path is invalid or does not match any prefabs in the hierarchy
+		If the path contains many prefabs with the same name, they can be disambiguated in the path with `name-index`
 	**/
-	function locatePrefab(path: String) : Null<Prefab> {
+	public function locatePrefab(path: String) : Null<Prefab> {
 		if (path == null)
 			return null;
 		var parts = path.split(".");
 		var p = this;
 		while (parts.length > 0 && p != null) {
 			var name = parts.shift();
+			var subIndex = name.split("-");
+			var chooseNth = 0;
+			if (subIndex.length > 1) {
+				chooseNth = Std.parseInt(subIndex.pop()) ?? 0;
+				name = subIndex[0];
+			}
 			var found = null;
+			var currentNth = 0;
 			for (o in p.children) {
 				if (o.name == name)
 				{
-					found = o;
-					break;
+					if (currentNth == chooseNth) {
+						found = o;
+						break;
+					} else {
+						currentNth ++;
+					}
 				}
 			}
 			p = found;

+ 258 - 71
hrt/prefab/Reference.hx

@@ -1,31 +1,62 @@
 package hrt.prefab;
 
-class Reference extends Object3D {
-	@:s public var editMode : Bool = false;
+enum EditMode {
+	/** The reference can't be edited in the editor **/
+	None;
+
+	/** The reference can be edited in the editor, and saving it will update the referenced prefab file on disk **/
+	Edit;
 
+	/** The reference can be edited, and saving it will save a diff between the original prefab and this in the `overrides` field **/
+	Override;
+}
+class Reference extends Object3D {
+	/**
+		The referenced prefab loaded by this reference
+	**/
 	public var refInstance : Prefab;
 
+	/**
+		How the reference can be edited in the editor
+	**/
+	@:s public var editMode : EditMode = None;
+
+	/**
+		List of all the properties that differs between this reference
+		and the original prefab data. Use the format defined by
+		hrt.prefab.Diff.diffPrefab
+	**/
+	@:s public var overrides : Dynamic = null;
+
 	#if editor
 	var wasMade : Bool = false;
+
+	/**
+		Copy of the original data to use as a reference on save for overrides
+	**/
+	public var originalSource : Dynamic;
 	#end
 
-	public static function copy_overrides(from:Dynamic) : haxe.ds.StringMap<Dynamic> {
-		if (Std.isOfType(from, haxe.ds.StringMap)) {
-			return from != null ? cast(from, haxe.ds.StringMap<Dynamic>).copy() : new haxe.ds.StringMap<Dynamic>();
-		}
-		else {
-			var m = new haxe.ds.StringMap<Dynamic>();
-			for (f in Reflect.fields(from)) {
-				m.set(f, Reflect.getProperty(from ,f));
-			}
-			return m;
+	override function set_source(newSource:String):String {
+		if (newSource != source) {
+			resetRefInstance();
 		}
+		return source = newSource;
 	}
 
 	override function save() {
+		#if editor
+		if (editMode == Override && refInstance != null) {
+			this.overrides = computeDiffFromSource();
+		} else if (editMode == Edit && refInstance != null) {
+			this.overrides = null;
+		}
+		#end
+
 		var obj : Dynamic = super.save();
+
 		#if editor
-		if( editMode && refInstance != null ) {
+		if( editMode == Edit && refInstance != null ) {
 			var sheditor = Std.downcast(shared, hide.prefab.ContextShared);
 			if( sheditor.editor != null ) sheditor.editor.watchIgnoreChanges(source);
 
@@ -33,41 +64,111 @@ class Reference extends Object3D {
 			sys.io.File.saveContent(hide.Ide.inst.getPath(source), hide.Ide.inst.toJSON(s));
 		}
 		#end
+
 		return obj;
 	}
 
-	#if editor
-	override function setEditorChildren(sceneEditor:hide.comp.SceneEditor, scene: hide.comp.Scene) {
-		super.setEditorChildren(sceneEditor, scene);
+	override function load(obj: Dynamic) {
+		// Backward compatibility between old bool editMode and new enum based editMode
+		if (Type.typeof(obj.editMode) == TBool) {
+			obj.editMode = "Edit";
+		}
 
-		if (refInstance != null) {
-			refInstance.setEditor(sceneEditor, scene);
+		super.load(obj);
+
+		if (source != null && shouldBeInstanciated() && hxd.res.Loader.currentInstance.exists(source)) {
+			#if editor
+			// we need the resCache to exist or we'll have an error in Ide.CustomeLoader.loadCache
+			if (@:privateAccess h3d.Engine.getCurrent()?.resCache == null)
+				return;
+
+			if (hasCycle())
+				return;
+			#end
+			initRefInstance();
+		}
+	}
+
+	override function copy(obj: Prefab) {
+		super.copy(obj);
+		var otherRef : Reference = cast obj;
+
+		#if editor
+		originalSource = @:privateAccess hxd.res.Loader.currentInstance.load(source).toPrefab().loadData();
+		#end
+
+		// Clone the refInstance from the original prefab on copy
+		if (source != null && shouldBeInstanciated()) {
+			if (otherRef.refInstance != null) {
+				refInstance = otherRef.refInstance.clone(new ContextShared(source, null, null, true));
+			} else {
+				initRefInstance();
+			}
+			if (refInstance != null) {
+				refInstance.shared.parentPrefab = this;
+			}
+		}
+	}
+
+	#if editor
+	function computeDiffFromSource() : Dynamic {
+		var orig = originalSource;
+		var ref = refInstance?.serialize() ?? null;
+		var diff = hrt.prefab.Diff.diffPrefab(orig, ref);
+		switch (diff) {
+			case Skip:
+				return null;
+			case Set(v):
+				return hrt.prefab.Diff.deepCopy(v);
 		}
 	}
 	#end
 
-	function resolveRef() : Prefab {
-		if(source == null)
-			return null;
-		if (refInstance != null)
-			return refInstance;
+	function initRefInstance() {
+		var refInstanceData = null;
 		#if editor
 		try {
 		#end
-			var refInstance = hxd.res.Loader.currentInstance.load(source).to(hrt.prefab.Resource).load().clone();
-			refInstance.shared.parentPrefab = this;
-			this.refInstance = refInstance;
-			return refInstance;
+			refInstanceData = @:privateAccess hxd.res.Loader.currentInstance.load(source).toPrefab().loadData();
 		#if editor
-		} catch (_) {
-			return null;
+			originalSource = @:privateAccess hxd.res.Loader.currentInstance.load(source).toPrefab().loadData();
+		} catch (e) {
+			return;
+		}
+		#end
+
+		if (overrides != null) {
+			refInstanceData = hrt.prefab.Diff.apply(refInstanceData, overrides);
 		}
+
+		refInstance = hrt.prefab.Prefab.createFromDynamic(refInstanceData, null, new ContextShared(source, null, null, true));
+		refInstance.shared.parentPrefab = this;
+	}
+
+	function resolveRef() : Prefab {
+		var shouldLoad = refInstance == null && source != null && shouldBeInstanciated();
+
+		#if editor
+		if (hasCycle())
+			shouldLoad = false;
 		#end
+
+		if (shouldLoad) {
+			initRefInstance();
+		}
+		return refInstance;
 	}
 
 	override function makeInstance() {
 		if( source == null )
 			return;
+
+
+		// in the case source has changed since the last load (can happen when creating references manually)
+		if (refInstance?.shared.currentPath != source) {
+			initRefInstance();
+			refInstance = refInstance.clone();
+		}
 		#if editor
 		if (hasCycle()) {
 			hide.Ide.inst.quickError('Reference ${getAbsPath()} to $source is creating a cycle. Please fix the reference.');
@@ -76,32 +177,31 @@ class Reference extends Object3D {
 		}
 		#end
 
-		var p = resolveRef();
 		var refLocal3d : h3d.scene.Object = null;
 
-		if (Std.downcast(p, Object3D) != null) {
+		if (Std.downcast(refInstance, Object3D) != null) {
 			refLocal3d = shared.current3d;
 		} else {
 			super.makeInstance();
 			refLocal3d = local3d;
 		}
 
-		if (p == null) {
-			refInstance = null;
+		if (refInstance == null) {
 			return;
 		}
 
-		var sh = p.shared;
+		var sh = refInstance.shared;
 		@:privateAccess sh.root3d = sh.current3d = refLocal3d;
 		@:privateAccess sh.root2d = sh.current2d = findFirstLocal2d();
 
 		#if editor
 		sh.editor = this.shared.editor;
 		sh.scene = this.shared.scene;
+		if (sh.isInstance == false)
+			throw "isInstance should be true";
 		#end
 		sh.parentPrefab = this;
 		sh.customMake = this.shared.customMake;
-		refInstance = p;
 
 		if (refInstance.to(Object3D) != null) {
 			var obj3d = refInstance.to(Object3D);
@@ -120,7 +220,6 @@ class Reference extends Object3D {
 		#end
 	}
 
-
 	override public function findRec<T:Prefab>(?cl: Class<T>, ?filter : T -> Bool, followRefs : Bool = false, includeDisabled: Bool = true) : Null<T> {
 		if (!includeDisabled && !enabled)
 			return null;
@@ -143,7 +242,7 @@ class Reference extends Object3D {
 
 	override public function flatten<T:Prefab>( ?cl : Class<T>, ?arr: Array<T>) : Array<T> {
 		arr = super.flatten(cl, arr);
-		if (editMode && resolveRef() != null) {
+		if (editMode != None && resolveRef() != null) {
 			arr = refInstance.flatten(cl, arr);
 		}
 		return arr;
@@ -155,8 +254,24 @@ class Reference extends Object3D {
 			refInstance.dispose();
 	}
 
+	function resetRefInstance() {
+		#if editor
+		editorRemoveObjects();
+		#end
+
+		refInstance = null;
+	}
+
 	#if editor
 
+	override function setEditorChildren(sceneEditor:hide.comp.SceneEditor, scene: hide.comp.Scene) {
+		super.setEditorChildren(sceneEditor, scene);
+
+		if (refInstance != null) {
+			refInstance.setEditor(sceneEditor, scene);
+		}
+	}
+
 	override public function editorRemoveObjects() : Void {
 		if (refInstance != null && wasMade) {
 			for (child in refInstance.flatten()) {
@@ -168,42 +283,76 @@ class Reference extends Object3D {
 		super.editorRemoveObjects();
 	}
 
-	public function hasCycle(?seenPaths: Map<String, Bool>) : Bool {
-		if (editorOnly)
-			return false;
-		var oldEditMode = editMode;
-		editMode = false;
-		seenPaths = seenPaths?.copy() ?? [];
-		var curPath = this.shared.currentPath;
-		if (seenPaths.get(curPath) != null) {
-			editMode = oldEditMode;
-			return true;
+	/**
+		Updates the original reference data to be equal to `data`.
+		If the ref is an override, the override will be kept as is
+	**/
+	function setRef(data: Dynamic) {
+		if (data == null)
+			throw "Null data";
+
+		if (refInstance == null)
+			return;
+
+		var currentSerialization = refInstance.serialize();
+		var pristineData = hrt.prefab.Diff.deepCopy(data);
+
+		// we might have unsaved changes
+		if (editMode == Override) {
+			switch(hrt.prefab.Diff.diffPrefab(originalSource, currentSerialization)) {
+				case Skip:
+				case Set(diff):
+					pristineData = hrt.prefab.Diff.apply(pristineData, diff);
+			}
+		}
+		else if (overrides != null) {
+			pristineData = hrt.prefab.Diff.apply(pristineData, overrides);
 		}
-		seenPaths.set(curPath, true);
-
-		if (source != null) {
-			var ref = resolveRef();
-			if (ref != null) {
-				var root = ref;
-				if (Std.isOfType(root, hrt.prefab.fx.BaseFX)) {
-					root = hrt.prefab.fx.BaseFX.BaseFXTools.getFXRoot(root) ?? root;
-				}
 
-				var allRefs = root.flatten(Reference);
-				for (r in allRefs) {
-					if (r.hasCycle(seenPaths)){
-						editMode = oldEditMode;
-						return true;
-					}
+		originalSource = hrt.prefab.Diff.deepCopy(data);
+
+		refInstance = Prefab.createFromDynamic(pristineData, new ContextShared(source, true));
+		refInstance.shared.parentPrefab = this;
+	}
+
+	/**
+		Returns true if this reference has a cycle,
+		meaning that references depends on each other
+	**/
+	public function hasCycle() : Bool {
+		var map : Map<String, Bool> = [];
+		map.set(shared.currentPath, true);
+		return hasCycleDynamic(serialize(), map);
+	}
+
+	static function hasCyclePath(path: String, seenPaths: Map<String, Bool>) : Bool {
+		if (seenPaths.get(path) == true)
+			return true;
+		if (!hxd.res.Loader.currentInstance.exists(path))
+			return false;
+
+		seenPaths.set(path, true);
+		var data = @:privateAccess hxd.res.Loader.currentInstance.load(path).toPrefab().loadData();
+		return hasCycleDynamic(data, seenPaths);
+	}
+
+	static function hasCycleDynamic(data: Dynamic, seenPaths: Map<String, Bool>) : Bool {
+		if (data.source != null && (data.type == "reference" || data.type == "subFX")) {
+			if (hasCyclePath(data.source, seenPaths.copy()))
+				return true;
+		}
+		if (data.children) {
+			for (child in (data.children:Array<Dynamic>)) {
+				if (hasCycleDynamic(child, seenPaths)) {
+					return true;
 				}
 			}
 		}
-		editMode = oldEditMode;
 		return false;
 	}
 
 	override function makeInteractive() {
-		if( editMode )
+		if( editMode != None )
 			return null;
 		return super.makeInteractive();
 	}
@@ -213,28 +362,29 @@ class Reference extends Object3D {
 			<div class="group" name="Reference">
 			<dl>
 				<dt>Reference</dt><dd><input type="fileselect" extensions="prefab l3d fx" field="source"/></dd>
-				<dt>Edit</dt><dd><input type="checkbox" field="editMode"/></dd>
+				<dt>Edit</dt><dd><select field="editMode" class="monSelector"></select></dd>
+				<p class="warning">Warning : Edit mode enabled while there are override on this reference. Saving will cause the overrides to be applied to the original reference !</p>
 			</dl>
 			</div>');
 
+
+		var warning = element.find(".warning");
+
 		function updateProps() {
 			var input = element.find("input");
 			var found = resolveRef() != null;
 			input.toggleClass("error", !found);
+			warning.toggle(overrides != null && editMode == Edit);
 		}
 		updateProps();
 
 		var props = ctx.properties.add(element, this, function(pname) {
 			ctx.onChange(this, pname);
 			if(pname == "source" || pname == "editMode") {
-				if (pname == "source") {
-					editorRemoveObjects();
-					refInstance = null;
-				}
 				if (hasCycle()) {
 					hide.Ide.inst.quickError('Reference to $source would create a cycle. The reference change was aborted.');
-					ctx.properties.undo.undo();
-					@:privateAccess ctx.properties.undo.redoElts.pop();
+					source = null;
+					ctx.rebuildProperties();
 					return;
 				}
 				updateProps();
@@ -257,6 +407,43 @@ class Reference extends Object3D {
 		});
 
 		super.edit(ctx);
+
+		var over = new hide.Element('
+			<div class="group">
+				<dl>
+					<dt>Overrides</dt><dd><p class="override-infos"></p><fancy-button><span class="label">Clear Overrides</span></fancy-button></dd>
+				</dl>
+			</div>
+		');
+
+		var overInfos = over.find(".override-infos");
+		function refreshOverrideInfos() {
+			if (computeDiffFromSource() == null) {
+				overInfos.text("No overrides");
+			}
+			else {
+				overInfos.text("This reference has overrides");
+			}
+		}
+		refreshOverrideInfos();
+
+		over.find("fancy-button").click((_) -> {
+			var old = overrides;
+			this.overrides = null;
+			var refresh = () -> {
+				if (originalSource != null) {
+					@:privateAccess shared.editor.removeInstance(refInstance, false);
+					originalSource = null;
+					refInstance = null;
+					ctx.rebuildPrefab(this);
+					refreshOverrideInfos();
+				}
+			};
+			@:privateAccess ctx.properties.undo.change(Field(this, "overrides", old), refresh);
+			refresh();
+			//ctx.rebuildPrefab(this);
+		});
+		ctx.properties.add(over);
 	}
 
 	override function getHideProps() : hide.prefab.HideProps {

+ 4 - 2
hrt/prefab/Shader.hx

@@ -203,8 +203,10 @@ class Shader extends Prefab {
 	#if editor
 
 	override function editorRemoveInstanceObjects() : Void {
-		shared.editor.queueRebuild(parent);
-		super.editorRemoveInstanceObjects();
+		if (shared?.editor != null) {
+			shared.editor.queueRebuild(parent);
+			super.editorRemoveInstanceObjects();
+		}
 	}
 
 	function getEditProps(shaderDef: hxsl.SharedShader) : Array<hrt.prefab.Props.PropDef> {

+ 1 - 1
hrt/tools/MapUtils.hx

@@ -3,7 +3,7 @@ import haxe.macro.Expr;
 
 class MapUtils {
 	/**
-		Returns map[key] if key if present, else execute def and puts it into map[key]
+		Returns `map[key]` if key if present, else evaluate `def` and puts the result into `map[key]`, then retunrs `map[key]`
 	**/
 	macro public static function getOrPut<K, V>(map:ExprOf<Map<K, V>>, key:ExprOf<K>, def:ExprOf<V>):Expr {
 		return macro {