Преглед на файлове

[cdb] Gradient editor can have a scene preview

Clément Espeute преди 11 месеца
родител
ревизия
2a2fe45e17
променени са 9 файла, в които са добавени 538 реда и са изтрити 135 реда
  1. 5 0
      bin/style.css
  2. 8 0
      bin/style.less
  3. 272 3
      hide/comp/GradientEditor.hx
  4. 21 3
      hide/comp/Popup.hx
  5. 186 2
      hide/comp/Scene.hx
  6. 33 4
      hide/comp/cdb/Cell.hx
  7. 2 0
      hide/comp/cdb/Editor.hx
  8. 9 0
      hide/view/CdbTable.hx
  9. 2 123
      hide/view/shadereditor/ShaderEditor.hx

+ 5 - 0
bin/style.css

@@ -2968,6 +2968,7 @@ div.gradient-box {
 .gradient-editor {
   background-color: #282829;
   border: 1px solid #333333;
+  display: flex;
 }
 .gradient-editor .color-box {
   border: 3px solid #333333;
@@ -2993,6 +2994,10 @@ div.gradient-box {
 .gradient-editor summary:focus {
   outline: none;
 }
+.gradient-editor #preview {
+  width: 256px;
+  height: 256px;
+}
 .gradient-container {
   height: 100%;
   position: relative;

+ 8 - 0
bin/style.less

@@ -3430,6 +3430,9 @@ div.gradient-box {
 	background-color : rgb(40, 40, 41);
 	border : 1px solid rgb(51, 51, 51);
 
+	display: flex;
+
+
 	.color-box {
 		border : 3px solid rgb(51, 51, 51);
 	}
@@ -3461,6 +3464,11 @@ div.gradient-box {
 	summary:focus {
 		outline: none;
 	}
+
+	#preview {
+		width: 256px;
+		height: 256px;
+	}
 }
 
 .gradient-container {

+ 272 - 3
hide/comp/GradientEditor.hx

@@ -11,6 +11,7 @@ import js.html.SelectElement;
 import js.html.PointerEvent;
 import h3d.Vector;
 import js.html.CanvasElement;
+using hide.tools.Extensions;
 
 class GradientBox extends Component {
     public var value(get, set) : GradientData;
@@ -130,6 +131,17 @@ class GradientEditor extends Popup {
     var interpolation : Element;
     var colorMode : Element;
 
+    var previewDiv : Element;
+    var previewScene : hide.comp.Scene = null;
+    var previewRoot2d : h2d.Object;
+    var previewRoot3d : h3d.scene.Object;
+    var previewPrefab : hrt.prefab.Prefab = null;
+    var previewSettings : PreviewSettings = null;
+    var previewBaseData: Dynamic = null;
+    var previewNeedRebuild : Bool = false;
+    var previewCurrentPath: String = null; // The path that this gradient editor is currently updating
+
+	public var keepPreviewAlive : Bool = false;
 
     var keys : hide.ui.Keys;
 
@@ -153,6 +165,8 @@ class GradientEditor extends Popup {
 
         element.addClass("gradient-editor");
 
+        var firstGrid = new Element("<div>").appendTo(element);
+
         // Allows the popup to become focusable,
         // allowing the handling of keyboard events
         element.attr("tabindex","-1");
@@ -178,7 +192,7 @@ class GradientEditor extends Popup {
             }
         });
 
-        gradientView = new GradientView(element);
+        gradientView = new GradientView(firstGrid);
         gradientView.element.height(90).width(256);
 
         var elem = gradientView.element.get(0);
@@ -214,7 +228,7 @@ class GradientEditor extends Popup {
         </filter>
         </defs>');
 
-        var editor = new Element("<div>").addClass("editor").appendTo(element);
+        var editor = new Element("<div>").addClass("editor").appendTo(firstGrid);
         stopEditor = new Element("<div>").addClass("stop-editor").appendTo(editor);
 
         stopLabel = new Element("<p>").appendTo(stopEditor);
@@ -326,6 +340,9 @@ class GradientEditor extends Popup {
         if(colorbox.isPickerOpen()) {
             colorbox.picker.close();
         }
+        if (!keepPreviewAlive) {
+            cleanupPreview();
+        }
         super.close();
     }
 
@@ -436,6 +453,22 @@ class GradientEditor extends Popup {
         isVerticalCheckbox.val('${innerValue.isVertical ? 1 : 0}');
         interpolation.val(innerValue.interpolation);
         colorMode.val('${innerValue.colorMode}');
+
+        if (previewCurrentPath != null) {
+            var customOverrides : Array<DataOverride> = [];
+            if (previewSettings.overrides != null) {
+                for (over in previewSettings.overrides) {
+                    if (over.cdbPath == previewCurrentPath) {
+                        customOverrides.push({
+                            cdbPath: "",
+                            targetPath: over.targetPath,
+                            type: "gradient",
+                        });
+                    }
+                }
+                updateOverrides(customOverrides, innerValue);
+            }
+        }
     }
 
     function removeStop(element : Element) {
@@ -484,6 +517,233 @@ class GradientEditor extends Popup {
             stopMarquers[i] = arr[i].marquer;
         }
     }
+
+    public function setPreview(previewSettings: PreviewSettings, baseData: Dynamic, currentPath: String) {
+        previewCurrentPath = currentPath;
+        if (previewDiv == null) {
+            initPreview();
+        }
+        setPreviewInternal(previewSettings, baseData);
+    }
+
+    function setPreviewInternal(newPreviewSettings: PreviewSettings, baseData: Dynamic) {
+        previewSettings = haxe.Json.parse(haxe.Json.stringify(newPreviewSettings));
+        this.previewBaseData = baseData;
+        if (previewScene?.s3d == null)
+            return;
+
+        previewRoot3d?.removeChildren();
+        previewRoot2d?.removeChildren();
+
+
+        @:privateAccess
+        {
+            previewScene.s3d.renderer.props = previewScene.s3d.renderer.getDefaultProps();
+
+            for(fx in previewScene.s3d.renderer.effects)
+                if ( fx != null )
+                    fx.dispose();
+            previewScene.s3d.renderer.effects = [];
+            var pbr = Std.downcast(previewScene.s3d.renderer, h3d.scene.pbr.Renderer);
+            pbr?.env = h3d.scene.pbr.Environment.getDefault();
+            previewScene.s3d.renderer.refreshProps();
+        }
+
+        previewRoot3d = previewRoot3d ?? new h3d.scene.Object(previewScene.s3d);
+        previewRoot2d = previewRoot2d ?? new h2d.Object(previewScene.s2d);
+
+        try {
+            if (StringTools.startsWith(previewSettings.prefab, "cdb@")) {
+                var namePath = StringTools.replace(previewSettings.prefab, "cdb@", "").split(".");
+                var currentProps = getObjectPathRec(namePath, baseData, true);
+                previewSettings.prefab = currentProps;
+            }
+            var res = hxd.res.Loader.currentInstance.load(previewSettings.prefab);
+            res.watch(() -> previewNeedRebuild = true);
+            var prefab = res.toPrefab().load();
+            prefab.shared.scene = previewScene;
+            var shared = new hrt.prefab.ContextShared(previewSettings.prefab, previewRoot2d, previewRoot3d);
+            previewPrefab = prefab.make(shared);
+
+
+            if (previewSettings.renderProps != null) {
+                var renderPropsPrefab = hxd.res.Loader.currentInstance.load(previewSettings.renderProps).toPrefab().load();
+                var shared = new hrt.prefab.ContextShared(previewSettings.renderProps, previewRoot2d, previewRoot3d);
+                renderPropsPrefab.shared.scene = previewScene;
+                renderPropsPrefab = renderPropsPrefab.make(shared);
+                var renderProps = @:privateAccess renderPropsPrefab.getOpt(hrt.prefab.RenderProps, true);
+			    if( renderProps != null ) {
+                    renderProps.applyProps(previewScene.s3d.renderer);
+                }
+            }
+        } catch(e) {
+            ide.error("Couln't load preview " + previewSettings.prefab + ", " + e.toString());
+            cleanupPreview();
+            return;
+        }
+
+        // Clenup scene if no prefab were created
+        // This allow the camera controllers to not update on envents
+        // if there is no prefab from the given scene
+        if (previewRoot3d.numChildren == 0) {
+            previewRoot3d.remove();
+            previewRoot3d = null;
+        }
+        if (previewRoot2d.numChildren == 0) {
+            previewRoot2d.remove();
+            previewRoot2d = null;
+        }
+        updateOverrides(this.previewSettings.overrides, this.previewBaseData);
+        previewNeedRebuild = false;
+    }
+
+    function updateOverrides(overrides: Array<DataOverride>, source: Dynamic) {
+        if (this.previewPrefab == null)
+            return;
+        previewScene.setCurrent();
+
+        var prefabToRefresh : Map<hrt.prefab.Prefab, Bool> = [];
+
+        for (dataOverride in overrides) {
+            var sourcePath = dataOverride.cdbPath;
+            var paths = sourcePath.split(".");
+            var current = source;
+            while(paths.length > 0) {
+                var name = paths.shift();
+                if (name.length <= 0)
+                    continue;
+                current = Reflect.getProperty(current, name);
+            }
+            var value : Dynamic = current;
+
+            var paths = dataOverride.targetPath.split(".");
+            var prefabPath = paths.shift().split("/");
+            var propPath = paths;
+
+            var errMessage = "Invalid prefab path " + dataOverride.targetPath + "( correct syntax : 'parent/child/subChild.prop.gradientProperty')";
+
+            var currentPrefabs = [previewPrefab];
+
+
+            while(prefabPath.length > 0) {
+                var nextCurrent = [];
+                var searchName = prefabPath.shift();
+
+                for (prefab in currentPrefabs) {
+                    if (searchName == "*") {
+                        nextCurrent = nextCurrent.concat(prefab.children);
+                    }
+                    else if (searchName == "**") {
+                        nextCurrent = nextCurrent.concat(prefab.flatten());
+                    }
+                    else {
+                        for (child in prefab) {
+                            if (child.name == searchName) {
+                                nextCurrent.push(child);
+                            }
+                        }
+                    }
+                }
+                currentPrefabs = nextCurrent;
+            }
+
+            for (prefab in currentPrefabs) {
+                prefabToRefresh.set(prefab, true);
+                var currentProps = getObjectPathRec(propPath, prefab, false);
+
+                if (value != null) {
+                    switch (dataOverride.type) {
+                        case null:
+                            Reflect.setField(currentProps, propPath[0], value);
+                        case "gradient":
+
+                            // convert from cdb to prefab gradient
+                            if (Reflect.hasField(value, "colors")) {
+                                var cdbGradient = value;
+                                var gradient = hrt.impl.Gradient.getDefaultGradientData();
+                                if (cdbGradient != null && cdbGradient.colors != null && cdbGradient.colors.length >= 1) {
+                                    gradient.stops.clear();
+                                    for (i in 0...cdbGradient.colors.length) {
+                                        gradient.stops[i] = {color: cdbGradient.colors[i], position: cdbGradient.positions[i]};
+                                    }
+                                }
+                                value = gradient;
+                            }
+                            Reflect.setField(currentProps, propPath[0], {type: hrt.impl.TextureType.gradient, data: value});
+                        case "tile":
+                            Reflect.setField(currentProps, "src", value.file);
+                            Reflect.setField(currentProps, "size", value.size);
+                            Reflect.setField(currentProps, "posX", value.x);
+                            Reflect.setField(currentProps, "posY", value.y);
+                            Reflect.setField(currentProps, "sizeX", value.width ?? 1);
+                            Reflect.setField(currentProps, "sizeY", value.height ?? 1);
+                    }
+                }
+            }
+        }
+
+        for (refresh => _ in prefabToRefresh) {
+            refresh.updateInstance();
+        }
+    }
+
+    // modifies path in place
+    static function getObjectPathRec(path: Array<String>, obj: Dynamic, includeLast: Bool) {
+        var obj : Dynamic = obj;
+        while (path.length > (includeLast ? 0 : 1) && obj != null) {
+            var prev = obj;
+            var key = path.shift();
+            obj = Reflect.getProperty(obj, key);
+            if (obj == null) {
+                obj = {}
+                Reflect.setProperty(prev, key, obj);
+            }
+        }
+        return obj;
+    }
+
+    function initPreview() {
+        previewDiv = new Element('<div id="preview">').appendTo(element);
+        previewScene = new hide.comp.Scene(ide.config.current, previewDiv, null);
+        previewScene.autoDisposeOutOfDocument = !keepPreviewAlive;
+        previewScene.onReady = onPreviewReady;
+        previewScene.onUpdate = onPreviewUpdate;
+    }
+
+    public function cleanupPreview() {
+        if (previewScene != null) {
+            previewScene.dispose();
+        }
+        if (previewDiv != null) {
+            previewDiv.remove();
+        }
+        previewScene = null;
+        previewDiv = null;
+        previewSettings = null;
+        previewPrefab = null;
+        previewCurrentPath = null;
+    }
+
+    function onPreviewReady() {
+        setPreviewInternal(this.previewSettings, this.previewBaseData);
+
+        previewScene.s2d.camera.anchorX = 0.5;
+        previewScene.s2d.camera.anchorY = 0.5;
+        previewScene.s2d.camera.viewportWidth = 256;
+        previewScene.s2d.camera.viewportHeight = 256;
+
+        new hide.comp.Scene.PreviewCamController(previewScene.s3d);
+        new hide.comp.Scene.Preview2DCamController(previewScene.s2d);
+
+
+    }
+
+    function onPreviewUpdate(dt: Float) {
+        if (previewNeedRebuild) {
+            setPreviewInternal(this.previewSettings, this.previewBaseData);
+        }
+    }
+
 }
 
 // Displays a GradientData inside a canvas
@@ -530,4 +790,13 @@ class GradientView extends Component {
         canvas = cast(canvasElement.get(0),CanvasElement);
         canvas.height = 1;
     }
-}
+}
+
+typedef DataOverride = {cdbPath: String, targetPath: String, type: String};
+
+typedef PreviewSettings = {
+    cdbPaths: Array<String>, // table.column names to match to apply the preview setting
+    prefab: String, // prefab path or `cdb@column_with_the_path`
+    ?renderProps: String, // render props for the preview
+    ?overrides: Array<DataOverride>, // Overrides to load with the prefab data
+};

+ 21 - 3
hide/comp/Popup.hx

@@ -8,6 +8,7 @@ import js.Browser;
 class Popup extends Component {
 	var timer : haxe.Timer;
 	var isSearchable:Bool;
+	public var anchor : Element;
 
 	function onMouseDown(e : js.html.MouseEvent) {
 		originalTarget = e.target;
@@ -57,10 +58,26 @@ class Popup extends Component {
         reflow();
 	}
 
+	public function open() {
+		untyped element.get(0).showPopover();
+		element.show();
+		element.get(0).addEventListener("toggle", onToggle);
+		element.parent()?.get(0)?.addEventListener("scroll", onScroll);
+        reflow();
+	}
+
+	public function onScroll(e: js.html.MouseScrollEvent) {
+		reflow();
+	}
+
 	function onToggle(e: Dynamic) {
 		if (e.newState == "closed") {
 			timer.stop();
-			element.remove();
+			element.parent()?.get(0)?.removeEventListener("scroll", onScroll);
+			if (anchor == null) {
+				element.remove();
+			}
+			element.hide();
 			onClose();
 		}
 	}
@@ -81,14 +98,15 @@ class Popup extends Component {
 	}
 
 	function reflow() {
-		var offset = element.parent().offset();
+		var refElement = if (anchor != null) anchor else element.parent();
+		var offset = refElement.offset();
 		var popupHeight = element.get(0).offsetHeight;
 		var popupWidth = element.get(0).offsetWidth;
 
 		var clientHeight = Browser.document.documentElement.clientHeight;
 		var clientWidth = Browser.document.documentElement.clientWidth;
 
-		offset.top += element.parent().get(0).offsetHeight;
+		offset.top += refElement.get(0).offsetHeight;
 		offset.top = Math.min(offset.top,  clientHeight - popupHeight - 32);
 
 		//offset.left += element.get(0).offsetWidth;

+ 186 - 2
hide/comp/Scene.hx

@@ -23,6 +23,7 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 	public var speed : Float = 1.0;
 	public var visible(default, null) : Bool = true;
 	public var editor : hide.comp.SceneEditor;
+	public var autoDisposeOutOfDocument : Bool = true;
 	var unFocusedTime = 0.;
 
 	public static var cache : h3d.prim.ModelCache = new h3d.prim.ModelCache();
@@ -161,8 +162,10 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 	}
 
 	function sync() {
-		if( new Element(canvas).parents("html").length == 0 ) {
-			dispose();
+		if( new Element(canvas).parents("html").length == 0) {
+			if (autoDisposeOutOfDocument) {
+				dispose();
+			}
 			return;
 		}
 		if( !visible || pendingCount > 0)
@@ -557,4 +560,185 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 		materials.sort((m1, m2) -> { return (m1.mat.name > m2.mat.name ? 1 : -1); });
 		return materials;
 	}
+}
+
+class PreviewCamController extends h3d.scene.Object {
+	var target : h3d.Vector = new h3d.Vector();
+	var targetInterp : h3d.Vector = new h3d.Vector();
+	var pos : h3d.Vector = new h3d.Vector();
+
+	var phi : Float = 0.4;
+	var theta : Float = 0.4;
+	var r : Float = 4.0;
+
+	var phiInterp : Float = 0.4;
+	var thetaInterp : Float = 0.4;
+	var rInterp : Float = 4.0;
+
+	function computePos() {
+		pos.x = rInterp * hxd.Math.sin(thetaInterp) * hxd.Math.cos(phiInterp);
+		pos.y = rInterp * hxd.Math.sin(thetaInterp) * hxd.Math.sin(phiInterp);
+		pos.z = rInterp * hxd.Math.cos(thetaInterp);
+		pos += targetInterp;
+	}
+
+	public function set(r: Float, phi: Float, theta: Float, target: h3d.Vector) {
+		this.r = r;
+		this.phi = phi;
+		this.theta = theta;
+		this.target.load(target);
+	}
+
+	var pushing : Int = -1;
+	var ignoreNext : Bool = false;
+	function onEvent(e : hxd.Event) {
+		if (getScene().children.length <= 1)
+			return;
+		switch (e.kind) {
+			case EPush: {
+				if (pushing != -1)
+					return;
+				pushing = e.button;
+				var win = hxd.Window.getInstance();
+				ignoreNext = true;
+				win.mouseMode = Relative(onCapture, true);
+			}
+			case EWheel: {
+				r *= hxd.Math.pow(2, e.wheelDelta * 0.5);
+			}
+			case ERelease, EReleaseOutside:
+				if (pushing != e.button) {
+					return;
+				}
+				var win = hxd.Window.getInstance();
+				win.mouseMode = Absolute;
+				pushing = -1;
+			default:
+		}
+	}
+
+	function pan(dx, dy, dz = 0.) {
+		var v = new h3d.Vector(dx, dy, dz);
+		var cam = getScene().camera;
+		cam.update();
+		v.transform3x3(cam.getInverseView());
+		target = target.add(v);
+	}
+
+	override function sync(ctx: h3d.scene.RenderContext) {
+		var cam = getScene().camera;
+		if (cam == null)
+			return;
+
+
+		var dt = hxd.Math.min(1, 1 - Math.pow(0.6, ctx.elapsedTime * 60));
+		var dt2 = hxd.Math.min(1, 1 - Math.pow(0.4, ctx.elapsedTime * 60));
+
+		thetaInterp = hxd.Math.lerp(thetaInterp, theta, dt2);
+		phiInterp = hxd.Math.lerp(phiInterp, phi, dt2);
+		rInterp = hxd.Math.lerp(rInterp, r, dt);
+		targetInterp.lerp(targetInterp, target, dt);
+
+		computePos();
+
+		cam.target.load(targetInterp);
+		cam.pos.load(pos);
+	}
+
+	override function onAdd() {
+		getScene().addEventListener(onEvent);
+	}
+
+	override function onRemove() {
+		getScene().removeEventListener(onEvent);
+	}
+
+	function onCapture(e: hxd.Event) {
+		// For some reason sometimes the first
+		// input from a capture has extreme values.
+		// We just filter out all first events from a capture to mitigate this
+		if (ignoreNext) {
+			ignoreNext = false;
+			return;
+		}
+
+		switch (e.kind) {
+			case EMove:
+				switch (pushing) {
+					case 1:
+						pan(-e.relX * 0.01, e.relY * 0.01);
+					case 2:
+						var dx = e.relX;
+						var dy = e.relY;
+						phi += dx * 0.01;
+						theta -= dy * 0.01;
+						theta = hxd.Math.clamp(theta, 0, hxd.Math.PI);
+				}
+			default:
+		}
+
+	}
+
+	function onCleanup() {
+		pushing = -1;
+	}
+}
+
+class Preview2DCamController extends h2d.Object {
+	var pushing : Int;
+	var zoom: Float = 1.0;
+	var ignoreNext: Bool;
+
+	function onEvent(e : hxd.Event) {
+		if (getScene().children.length <= 1)
+			return;
+		switch (e.kind) {
+			case EPush: {
+				if (pushing != -1)
+					return;
+				pushing = e.button;
+				var win = hxd.Window.getInstance();
+				ignoreNext = true;
+				win.mouseMode = Relative(onCapture, true);
+			}
+			case EWheel: {
+				zoom *= hxd.Math.pow(2, -e.wheelDelta * 0.5);
+			}
+			case ERelease, EReleaseOutside:
+				if (pushing != e.button) {
+					return;
+				}
+				var win = hxd.Window.getInstance();
+				win.mouseMode = Absolute;
+				pushing = -1;
+			default:
+		}
+	}
+
+	function onCapture(e: hxd.Event) {
+		// For some reason sometimes the first
+		// input from a capture has extreme values.
+		// We just filter out all first events from a capture to mitigate this
+		if (ignoreNext) {
+			ignoreNext = false;
+			return;
+		}
+
+	}
+
+	override function onAdd() {
+		getScene().addEventListener(onEvent);
+	}
+
+	override function onRemove() {
+		getScene().removeEventListener(onEvent);
+	}
+
+	override function sync(ctx: h2d.RenderContext) {
+		var cam = getScene().camera;
+		if (cam == null)
+			return;
+
+		cam.setScale(zoom, zoom);
+	}
 }

+ 33 - 4
hide/comp/cdb/Cell.hx

@@ -1195,7 +1195,12 @@ class Cell {
 			#if js
 			var e = new Element(elementHtml);
 			e.addClass("edit");
-			var gradientEditor = new GradientEditor(e , false);
+			if (editor.gradientEditor == null) {
+				editor.gradientEditor = new GradientEditor(editor.element.parents(".hide-scroll").first(), false);
+				editor.gradientEditor.keepPreviewAlive = true;
+			}
+			editor.gradientEditor.anchor = e;
+			editor.gradientEditor.open();
 
 			var gradient = hrt.impl.Gradient.getDefaultGradientData();
 			if (value != null && value.colors != null && value.colors.length >= 1) {
@@ -1205,10 +1210,10 @@ class Cell {
 				}
 			}
 
-			gradientEditor.value = gradient;
-			gradientEditor.onClose = function() {
+			editor.gradientEditor.value = gradient;
+			editor.gradientEditor.onClose = function() {
 				var grad : cdb.Types.Gradient = {colors: [], positions: []};
-				for (i => stop in gradientEditor.value.stops) {
+				for (i => stop in editor.gradientEditor.value.stops) {
 					grad.data.colors[i] = stop.color;
 					grad.data.positions[i] = stop.position;
 				}
@@ -1219,6 +1224,30 @@ class Cell {
 				refresh();
 				focus();
 			}
+
+			var customPreviews : Array<hide.comp.GradientEditor.PreviewSettings> = editor.config.get("cdb.gradientCustomPreviews");
+			var paths = table.sheet.name.split("@");
+			paths.push(column.name);
+			var path = paths.join(".");
+
+			var previewSettings = null;
+			for (config in customPreviews) {
+				for (cdbPath in config.cdbPaths) {
+					if (cdbPath == path) {
+						previewSettings = config;
+						break;
+					}
+				}
+			}
+
+			if (previewSettings != null) {
+				editor.gradientEditor.setPreview(previewSettings, line.obj, column.name);
+			}
+			else {
+				editor.gradientEditor.cleanupPreview();
+			}
+
+
 			#end
 		case TCurve:
 			var e = new Element(elementHtml);

+ 2 - 0
hide/comp/cdb/Editor.hx

@@ -83,6 +83,8 @@ class Editor extends Component {
 	public var cursorIndex : Int = 0;
 	public var formulas : Formulas;
 
+	public var gradientEditor: GradientEditor;
+
 	public function new(config, api, ?cdbTable) {
 		super(null,null);
 		this.api = api;

+ 9 - 0
hide/view/CdbTable.hx

@@ -24,6 +24,15 @@ class CdbTable extends hide.ui.View<{}> {
 		view = cast this.config.get("cdb.view");
 	}
 
+	override function destroy() {
+		if (editor != null) {
+			@:privateAccess editor.gradientEditor?.cleanupPreview();
+			editor.gradientEditor.remove();
+			editor.gradientEditor = null;
+		}
+		super.destroy();
+	}
+
 	public function goto2(rootSheet : cdb.Sheet, path: hide.comp.cdb.Editor.Path) {
 		var sheets = [for( s in getSheets() ) s.name];
 		var index = sheets.indexOf(rootSheet.name);

+ 2 - 123
hide/view/shadereditor/ShaderEditor.hx

@@ -30,127 +30,6 @@ typedef SavedClipboard = {
 	edges : Array<{ fromIdx : Int, fromOutputId : Int, toIdx : Int, toInputId : Int }>,
 }
 
-class PreviewCamController extends h3d.scene.Object {
-	var target : Vector = new h3d.Vector();
-	var targetInterp : Vector = new h3d.Vector();
-	var pos : Vector = new h3d.Vector();
-
-	var phi : Float = 0.4;
-	var theta : Float = 0.4;
-	var r : Float = 4.0;
-
-	var phiInterp : Float = 0.4;
-	var thetaInterp : Float = 0.4;
-	var rInterp : Float = 4.0;
-
-	function computePos() {
-		pos.x = rInterp * hxd.Math.sin(thetaInterp) * hxd.Math.cos(phiInterp);
-		pos.y = rInterp * hxd.Math.sin(thetaInterp) * hxd.Math.sin(phiInterp);
-		pos.z = rInterp * hxd.Math.cos(thetaInterp);
-		pos += targetInterp;
-	}
-
-	public function set(r: Float, phi: Float, theta: Float, target: Vector) {
-		this.r = r;
-		this.phi = phi;
-		this.theta = theta;
-		this.target.load(target);
-	}
-
-	var pushing : Int = -1;
-	var ignoreNext : Bool = false;
-	function onEvent(e : hxd.Event) {
-		switch (e.kind) {
-			case EPush: {
-				if (pushing != -1)
-					return;
-				pushing = e.button;
-				var win = hxd.Window.getInstance();
-				ignoreNext = true;
-				win.mouseMode = Relative(onCapture, true);
-			}
-			case EWheel: {
-				r += e.wheelDelta;
-				r = hxd.Math.max(r, 0.0001);
-			}
-			case ERelease, EReleaseOutside:
-				if (pushing != e.button) {
-					return;
-				}
-				var win = hxd.Window.getInstance();
-				win.mouseMode = Absolute;
-				pushing = -1;
-			default:
-		}
-	}
-
-	function pan(dx, dy, dz = 0.) {
-		var v = new h3d.Vector(dx, dy, dz);
-		var cam = getScene().camera;
-		cam.update();
-		v.transform3x3(cam.getInverseView());
-		target = target.add(v);
-	}
-
-	override function sync(ctx: h3d.scene.RenderContext) {
-		var cam = getScene().camera;
-		if (cam == null)
-			return;
-
-
-		var dt = hxd.Math.min(1, 1 - Math.pow(0.6, ctx.elapsedTime * 60));
-		var dt2 = hxd.Math.min(1, 1 - Math.pow(0.4, ctx.elapsedTime * 60));
-
-		thetaInterp = hxd.Math.lerp(thetaInterp, theta, dt2);
-		phiInterp = hxd.Math.lerp(phiInterp, phi, dt2);
-		rInterp = hxd.Math.lerp(rInterp, r, dt);
-		targetInterp.lerp(targetInterp, target, dt);
-
-		computePos();
-
-		cam.target.load(targetInterp);
-		cam.pos.load(pos);
-	}
-
-	override function onAdd() {
-		getScene().addEventListener(onEvent);
-	}
-
-	override function onRemove() {
-		getScene().removeEventListener(onEvent);
-	}
-
-	function onCapture(e: hxd.Event) {
-		// For some reason sometimes the first
-		// input from a capture has extreme values.
-		// We just filter out all first events from a capture to mitigate this
-		if (ignoreNext) {
-			ignoreNext = false;
-			return;
-		}
-
-		switch (e.kind) {
-			case EMove:
-				switch (pushing) {
-					case 1:
-						pan(-e.relX * 0.01, e.relY * 0.01);
-					case 2:
-						var dx = e.relX;
-						var dy = e.relY;
-						phi += dx * 0.01;
-						theta -= dy * 0.01;
-						theta = hxd.Math.clamp(theta, 0, hxd.Math.PI);
-				}
-			default:
-		}
-
-	}
-
-	function onCleanup() {
-		pushing = -1;
-	}
-}
-
 class PreviewShaderBase extends hxsl.Shader {
 	static var SRC = {
 
@@ -269,7 +148,7 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 	var meshPreviewMeshes : Array<h3d.scene.Mesh> = [];
 	var meshPreviewRoot3d : h3d.scene.Object;
 	var meshPreviewShader : hxsl.DynamicShader;
-	var meshPreviewCameraController : PreviewCamController;
+	var meshPreviewCameraController : hide.comp.Scene.PreviewCamController;
 	var previewSettings : PreviewSettings;
 	var meshPreviewPrefab : hrt.prefab.Prefab;
 	var meshPreviewprefabWatch : hide.tools.FileWatcher.FileWatchEvent;
@@ -1331,7 +1210,7 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 			throw "meshPreviewScene not ready";
 
 		var moved = false;
-		meshPreviewCameraController = new PreviewCamController(meshPreviewScene.s3d);
+		meshPreviewCameraController = new hide.comp.Scene.PreviewCamController(meshPreviewScene.s3d);
 		meshPreviewRoot3d = new h3d.scene.Object(meshPreviewScene.s3d);
 
 		loadMeshPreviewFromString(previewSettings.meshPath);