Просмотр исходного кода

Merge branch 'master' of https://github.com/HeapsIO/hide

LeoVgr 8 месяцев назад
Родитель
Сommit
5002595370

+ 64 - 0
bin/style.css

@@ -4176,6 +4176,12 @@ button-2 {
 button-2:hover {
 button-2:hover {
   background-color: var(--hover);
   background-color: var(--hover);
 }
 }
+button-2 value {
+  flex-grow: 1;
+  overflow-x: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
 button-2:active {
 button-2:active {
   box-shadow: inset var(--sublte-shadow);
   box-shadow: inset var(--sublte-shadow);
 }
 }
@@ -4401,6 +4407,7 @@ blend-space-2d-root main-panel preview-container .hide-toolbar2 {
 }
 }
 blend-space-2d-root main-panel graph-container {
 blend-space-2d-root main-panel graph-container {
   height: 40%;
   height: 40%;
+  position: relative;
   background-color: var(--bg-1);
   background-color: var(--bg-1);
   margin: 16px;
   margin: 16px;
 }
 }
@@ -4440,6 +4447,14 @@ blend-space-2d-root main-panel graph-container svg .preview-axis {
   stroke: #008a00;
   stroke: #008a00;
   stroke-width: 2;
   stroke-width: 2;
 }
 }
+blend-space-2d-root main-panel graph-container drag-handler {
+  z-index: 1;
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  top: 0;
+}
 blend-space-2d-root properties-container {
 blend-space-2d-root properties-container {
   padding: var(--basic-padding);
   padding: var(--basic-padding);
   display: flex;
   display: flex;
@@ -4451,6 +4466,55 @@ blend-space-2d-root properties-container {
 blend-space-2d-root properties-container parameters-container {
 blend-space-2d-root properties-container parameters-container {
   flex-grow: 1;
   flex-grow: 1;
 }
 }
+blend-space-2d-root properties-container .hide-properties {
+  padding: 1em;
+  display: flex;
+  flex-direction: column;
+  gap: 0.5em;
+}
+blend-space-2d-root properties-container .hide-properties input {
+  min-width: 0;
+  width: 0;
+}
+blend-space-2d-root properties-container .hide-properties dl {
+  display: grid;
+  grid-template-columns: 6em minmax(0, 1fr);
+  row-gap: 1px;
+  column-gap: 0.5em;
+  width: 100%;
+}
+blend-space-2d-root properties-container .hide-properties dl > div {
+  display: grid;
+  grid-column: 1 / -1;
+  grid-template-columns: subgrid;
+  align-items: center;
+}
+blend-space-2d-root properties-container .hide-properties dl > div > dt {
+  width: unset;
+  text-align: right;
+}
+blend-space-2d-root properties-container .hide-properties dl > div > dd {
+  width: unset;
+  margin-left: 0;
+  display: flex;
+  justify-content: stretch;
+}
+blend-space-2d-root properties-container .hide-properties dl > div > dd > * {
+  flex: 1 1;
+}
+blend-space-2d-root properties-container .hide-properties dl > div .hide-range {
+  display: flex;
+}
+blend-space-2d-root properties-container .hide-properties dl > div .hide-range input[type="range"] {
+  min-width: 0;
+  width: 0;
+  flex: 1 1;
+}
+blend-space-2d-root properties-container .hide-properties dl > div .hide-range input[type="text"] {
+  min-width: 0;
+  width: 0;
+  flex: 0 0 4em;
+}
 .basic-border {
 .basic-border {
   padding: var(--basic-padding);
   padding: var(--basic-padding);
   border: var(--basic-border);
   border: var(--basic-border);

+ 78 - 0
bin/style.less

@@ -4934,6 +4934,13 @@ button-2 {
 		background-color: var(--hover);
 		background-color: var(--hover);
 	}
 	}
 
 
+	value {
+		flex-grow: 1;
+		overflow-x: hidden;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+	}
+
 	&:active {
 	&:active {
 		box-shadow: inset var(--sublte-shadow);
 		box-shadow: inset var(--sublte-shadow);
 	}
 	}
@@ -5212,6 +5219,8 @@ blend-space-2d-root {
 		graph-container {
 		graph-container {
 			height: 40%;
 			height: 40%;
 
 
+			position: relative;
+
 			background-color: var(--bg-1);
 			background-color: var(--bg-1);
 			margin: 16px;
 			margin: 16px;
 
 
@@ -5260,6 +5269,15 @@ blend-space-2d-root {
 					stroke-width: 2;
 					stroke-width: 2;
 				}
 				}
 			}
 			}
+
+			drag-handler {
+				z-index: 1;
+				position: absolute;
+				left: 0;
+				bottom: 0;
+				right: 0;
+				top: 0;
+			}
 		}
 		}
 	}
 	}
 
 
@@ -5277,6 +5295,66 @@ blend-space-2d-root {
 		parameters-container {
 		parameters-container {
 			flex-grow: 1;
 			flex-grow: 1;
 		}
 		}
+
+		.hide-properties {
+			padding: 1em;
+			display: flex;
+			flex-direction: column;
+
+			gap: 0.5em;
+
+			input {
+				min-width: 0;
+				width: 0;
+			}
+
+			dl {
+				display: grid;
+				grid-template-columns: 6em minmax(0, 1fr);
+				row-gap: 1px;
+				column-gap: 0.5em;
+				width: 100%;
+
+				> div {
+					display: grid;
+					grid-column: 1 / -1;
+					grid-template-columns: subgrid;
+					align-items: center;
+					> dt {
+						width: unset;
+						text-align: right;
+					}
+
+					> dd {
+						width: unset;
+						margin-left: 0;
+
+						display: flex;
+						justify-content: stretch;
+
+						> * {
+							flex: 1 1;
+						}
+					}
+
+					.hide-range {
+						display: flex;
+						input[type="range"] {
+							min-width: 0;
+							width: 0;
+							flex: 1 1;
+						}
+
+						input[type="text"] {
+							min-width: 0;
+							width: 0;
+							flex: 0 0 4em;
+						}
+					}
+				}
+			}
+		}
+
 	}
 	}
 
 
 }
 }

+ 55 - 6
hide/comp/cdb/Cell.hx

@@ -310,14 +310,63 @@ class Cell {
 		return view == null || view.show == null || view.show.indexOf(column) >= 0;
 		return view == null || view.show == null || view.show.indexOf(column) >= 0;
 	}
 	}
 
 
+
+	public static function isIdScope( origin : cdb.Sheet, sheet : cdb.Sheet, ?scope ) {
+		if( origin.idCol == null ) return false;
+		if( scope == null ) scope = origin.idCol.scope;
+		return StringTools.startsWith(sheet.name,origin.name.split("@").slice(0,-scope).join("@"));
+	}
+
 	function refScope( targetSheet : cdb.Sheet, currentSheet : cdb.Sheet, obj : Dynamic, localScope : Array<{ s : cdb.Sheet, obj : Dynamic }> ) {
 	function refScope( targetSheet : cdb.Sheet, currentSheet : cdb.Sheet, obj : Dynamic, localScope : Array<{ s : cdb.Sheet, obj : Dynamic }> ) {
+
 		var targetDepth = targetSheet.name.split("@").length;
 		var targetDepth = targetSheet.name.split("@").length;
-		var scope = table.getScope().concat(localScope);
-		if( scope.length < targetDepth )
-			scope.push({ s : currentSheet, obj : obj });
-		while( scope.length >= targetDepth )
-			scope.pop();
-		return scope;
+		var fullScope;
+		if( !isIdScope(targetSheet,currentSheet) ) {
+			// look into other fields at same level for contextual references
+			var newScope = [];
+			var scope = targetSheet.idCol.scope;
+			while( scope > 0 ) {
+				var found = null;
+				for( c in currentSheet.columns ) {
+					switch( c.type ) {
+					case TRef(s):
+						var sheet = editor.base.getSheet(s);
+						if( isIdScope(targetSheet,sheet,scope) ) {
+							found = { col : c, sheet : sheet };
+							break;
+						}
+					default:
+					}
+				}
+				if( found == null )
+					return [];
+				scope--;
+				var refId = Reflect.field(obj, found.col.name);
+				if( refId == null )
+					return [];
+				newScope.unshift({ sheet : found.sheet, ref : refId });
+			}
+			var s0 = newScope.shift();
+			var obj = s0.sheet.index.get(s0.ref)?.obj;
+			if( obj == null )
+				return [];
+			fullScope = [{ obj : obj, s : s0.sheet }];
+			for( s in newScope ) {
+				var sub = Reflect.field(obj, s.sheet.getParent().c);
+				if( sub == null ) return [];
+				fullScope.push({ obj : sub, s : s.sheet });
+				obj = sub;
+			}
+			obj = fullScope[0].obj;
+			currentSheet = fullScope[0].s;
+		} else {
+			fullScope = table.getScope().concat(localScope);
+		}
+		if( fullScope.length < targetDepth )
+			fullScope.push({ s : currentSheet, obj : obj });
+		while( fullScope.length >= targetDepth )
+			fullScope.pop();
+		return fullScope;
 	}
 	}
 
 
 	function valueHtml( c : cdb.Data.Column, v : Dynamic, sheet : cdb.Sheet, obj : Dynamic, scope : Array<{ s : cdb.Sheet, obj : Dynamic }> ) : {str: String, containsHtml: Bool} {
 	function valueHtml( c : cdb.Data.Column, v : Dynamic, sheet : cdb.Sheet, obj : Dynamic, scope : Array<{ s : cdb.Sheet, obj : Dynamic }> ) : {str: String, containsHtml: Bool} {

+ 16 - 1
hide/comp/cdb/ModalColumnForm.hx

@@ -180,7 +180,22 @@ class ModalColumnForm extends Modal {
 		for( i in 0...base.sheets.length ) {
 		for( i in 0...base.sheets.length ) {
 			var s = base.sheets[i];
 			var s = base.sheets[i];
 			if( s.idCol == null ) continue;
 			if( s.idCol == null ) continue;
-			if( s.idCol.scope != null && !StringTools.startsWith(sheet.name,s.name.split("@").slice(0,-s.idCol.scope).join("@")) ) continue;
+			if( s.idCol.scope != null ) {
+				var checkSheets = [sheet];
+				for( c in sheet.columns )
+					switch( c.type ) {
+					case TRef(s): checkSheets.push(base.getSheet(s));
+					default:
+					}
+				var found = false;
+				for( sscope in checkSheets )
+					if( Cell.isIdScope(s, sscope) ) {
+						found = true;
+						break;
+					}
+				if( !found )
+					continue;
+			}
 			options.push(new Element("<option>").attr("value", "" + i).text(s.name)); // .appendTo(sheets);
 			options.push(new Element("<option>").attr("value", "" + i).text(s.name)); // .appendTo(sheets);
 		}
 		}
 		options.sort((e1, e2) -> e1.text() > e2.text() ? 1 : -1);
 		options.sort((e1, e2) -> e1.text() > e2.text() ? 1 : -1);

+ 99 - 14
hide/view/animgraph/BlendSpace2DEditor.hx

@@ -92,7 +92,10 @@ class BlendSpace2DEditor extends hide.view.FileView {
 				var movingPreview = false;
 				var movingPreview = false;
 				var svg: js.html.svg.SVGElement = cast graph.element.get(0);
 				var svg: js.html.svg.SVGElement = cast graph.element.get(0);
 
 
-				svg.onpointerdown = (e:js.html.PointerEvent) -> {
+				// We use a div instead to catch the drag event instead of the svg because the svg getting repainted messes with the mouse cursor drag preview
+				var dragHandler = new Element("<drag-handler>").appendTo(graphContainer).get(0);
+
+				dragHandler.onpointerdown = (e:js.html.PointerEvent) -> {
 					if (e.button != 0)
 					if (e.button != 0)
 						return;
 						return;
 
 
@@ -115,11 +118,14 @@ class BlendSpace2DEditor extends hide.view.FileView {
 							return;
 							return;
 					}
 					}
 
 
-					svg.setPointerCapture(e.pointerId);
+					dragHandler.setPointerCapture(e.pointerId);
 					e.preventDefault();
 					e.preventDefault();
 				}
 				}
 
 
-				svg.onpointermove = (e:js.html.PointerEvent) -> {
+
+
+
+				dragHandler.onpointermove = (e:js.html.PointerEvent) -> {
 					if (movingPreview) {
 					if (movingPreview) {
 						var pt = getPointPos(e.clientX, e.clientY, false);
 						var pt = getPointPos(e.clientX, e.clientY, false);
 
 
@@ -161,7 +167,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 					refreshGraph();
 					refreshGraph();
 				}
 				}
 
 
-				svg.onpointerup = (e:js.html.PointerEvent) -> {
+				dragHandler.onpointerup = (e:js.html.PointerEvent) -> {
 					if (movingPreview) {
 					if (movingPreview) {
 						movingPreview = false;
 						movingPreview = false;
 						refreshPropertiesPannel();
 						refreshPropertiesPannel();
@@ -199,7 +205,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 					movedPoint = -1;
 					movedPoint = -1;
 				}
 				}
 
 
-				svg.oncontextmenu = (e:js.html.MouseEvent) -> {
+				dragHandler.oncontextmenu = (e:js.html.MouseEvent) -> {
 					e.preventDefault();
 					e.preventDefault();
 
 
 					var options : Array<hide.comp.ContextMenu.MenuItem> = [];
 					var options : Array<hide.comp.ContextMenu.MenuItem> = [];
@@ -238,18 +244,48 @@ class BlendSpace2DEditor extends hide.view.FileView {
 					hide.comp.ContextMenu.createFromEvent(e, options);
 					hide.comp.ContextMenu.createFromEvent(e, options);
 				}
 				}
 
 
-				svg.ondragover = (e:js.html.DragEvent) -> {
+				dragHandler.ondragover = (e:js.html.DragEvent) -> {
 					if (e.dataTransfer.types.contains(AnimList.dragEventKey)) {
 					if (e.dataTransfer.types.contains(AnimList.dragEventKey)) {
 						e.preventDefault();
 						e.preventDefault();
+
+						var mouse = inline new h2d.col.Point(e.clientX - cachedRect.x, e.clientY - cachedRect.y);
+
+						hoverPoint = -1;
+						for (id => point in blendSpace2D.points) {
+							var pt = inline new h2d.col.Point(localXToGraph(point.x), localYToGraph(point.y));
+							if (mouse.distanceSq(pt) < pointRadius * pointRadius) {
+								hoverPoint = id;
+								break;
+							}
+						}
+
+						refreshGraph();
 					}
 					}
 				}
 				}
 
 
-				svg.ondrop = (e:js.html.DragEvent) -> {
+				dragHandler.ondrop = (e:js.html.DragEvent) -> {
 					if (e.dataTransfer.types.contains(AnimList.dragEventKey)) {
 					if (e.dataTransfer.types.contains(AnimList.dragEventKey)) {
+
+						var path = e.dataTransfer.getData(AnimList.dragEventKey);
+						if (path.length <= 0)
+							return;
+
 						e.preventDefault();
 						e.preventDefault();
 
 
-							var pos = getPointPos(e.clientX, e.clientY, true);
-							addPoint({x: pos.x, y: pos.y, animPath: e.dataTransfer.getData(AnimList.dragEventKey)}, true);
+						if (hoverPoint >= 0) {
+							var old = blendSpace2D.points[hoverPoint].animPath;
+							blendSpace2D.points[hoverPoint].animPath = path;
+							undo.change(Field(blendSpace2D.points[hoverPoint], "animPath", old), () -> {
+								refreshPropertiesPannel();
+								refreshPreviewAnimation();
+							});
+							refreshPropertiesPannel();
+							refreshPreviewAnimation();
+						}
+						else {
+							var pos = getPointPos(e.clientX, e.clientY, false);
+							addPoint({x: pos.x, y: pos.y, animPath: path}, true);
+						}
 					}
 					}
 				}
 				}
 			}
 			}
@@ -277,7 +313,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 
 
 		propertiesContainer = new hide.Element("<properties-container></properties-container>").appendTo(root);
 		propertiesContainer = new hide.Element("<properties-container></properties-container>").appendTo(root);
 		{
 		{
-			var paramContainer = new Element('<parameters-container></parameters-container>').appendTo(propertiesContainer).addClass("hide-properties");
+			var paramContainer = new Element('<parameters-container></parameters-container>').appendTo(propertiesContainer);
 			new Element("<h1>Parameters</h1>").appendTo(paramContainer);
 			new Element("<h1>Parameters</h1>").appendTo(paramContainer);
 			propsEditor = new hide.comp.PropsEditor(undo, paramContainer);
 			propsEditor = new hide.comp.PropsEditor(undo, paramContainer);
 			refreshPropertiesPannel();
 			refreshPropertiesPannel();
@@ -346,6 +382,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 	function refreshPropertiesPannel() {
 	function refreshPropertiesPannel() {
 		propsEditor.clear();
 		propsEditor.clear();
 
 
+
 		propsEditor.add(new hide.Element('
 		propsEditor.add(new hide.Element('
 		<div class="group" name="BlendSpace">
 		<div class="group" name="BlendSpace">
 			<dl>
 			<dl>
@@ -358,20 +395,68 @@ class BlendSpace2DEditor extends hide.view.FileView {
 		});
 		});
 
 
 		if (selectedPoint != -1) {
 		if (selectedPoint != -1) {
-			propsEditor.add(new hide.Element('
+			var editor = new hide.Element('
 				<div class="group" name="Point">
 				<div class="group" name="Point">
 					<dl>
 					<dl>
 						<dt>X</dt><dd><input type="range" min="0.0" max="1.0" field="x"/></dd>
 						<dt>X</dt><dd><input type="range" min="0.0" max="1.0" field="x"/></dd>
 						<dt>Y</dt><dd><input type="range" min="0.0" max="1.0" field="y"/></dd>
 						<dt>Y</dt><dd><input type="range" min="0.0" max="1.0" field="y"/></dd>
 						<dt>Anim speed</dt><dd><input type="range" min="0.1" max="2.0" field="speed"/></dd>
 						<dt>Anim speed</dt><dd><input type="range" min="0.1" max="2.0" field="speed"/></dd>
-						<dt>Anim</dt><dd><input type="fileselect" extensions="fbx" field="animPath"/></dd>
 					</dl>
 					</dl>
 				</div>
 				</div>
-			'), blendSpace2D.points[selectedPoint], (_) -> {
+			');
+
+			propsEditor.add(editor, blendSpace2D.points[selectedPoint], (_) -> {
 				blendSpace2D.triangulate();
 				blendSpace2D.triangulate();
 				refreshGraph();
 				refreshGraph();
 				refreshPreviewAnimation();
 				refreshPreviewAnimation();
 			});
 			});
+
+			var div = new Element("<div></div>").appendTo(editor.find("dl"));
+			new Element("<dt>Anim</dt>").appendTo(div);
+			var dd = new Element("<dd>").appendTo(div);
+			var button = new hide.comp.Button(dd, null, "", {hasDropdown: true});
+			button.label = blendSpace2D.points[selectedPoint].animPath;
+			button.onClick = () -> {
+				hide.comp.ContextMenu.createDropdown(button.element.get(0), [
+					{
+						label: "Choose File ...",
+						click: () -> {
+						ide.chooseFile(["fbx"], (path) -> {
+								var old = blendSpace2D.points[selectedPoint].animPath;
+								blendSpace2D.points[selectedPoint].animPath = path;
+								undo.change(Field(blendSpace2D.points[selectedPoint], "animPath", old), () -> {
+									button.label = blendSpace2D.points[selectedPoint].animPath;
+									refreshPreviewAnimation();
+								});
+								button.label = blendSpace2D.points[selectedPoint].animPath;
+								refreshPreviewAnimation();
+							}, true);
+						}
+					}
+				], {search: Visible, autoWidth: true});
+			};
+
+			button.element.get(0).ondragover = (e:js.html.DragEvent) -> {
+				if (e.dataTransfer.types.contains(AnimList.dragEventKey))
+					e.preventDefault();
+			};
+
+			button.element.get(0).ondrop = (e:js.html.DragEvent) -> {
+				var data = e.dataTransfer.getData(AnimList.dragEventKey);
+				if (data.length == 0)
+					return;
+				e.preventDefault();
+
+				var old = blendSpace2D.points[selectedPoint].animPath;
+				blendSpace2D.points[selectedPoint].animPath = data;
+				undo.change(Field(blendSpace2D.points[selectedPoint], "animPath", old), () -> {
+					button.label = blendSpace2D.points[selectedPoint].animPath;
+					refreshPreviewAnimation();
+				});
+
+				button.label = blendSpace2D.points[selectedPoint].animPath;
+				refreshPreviewAnimation();
+			};
 		}
 		}
 
 
 		propsEditor.add(new hide.Element('
 		propsEditor.add(new hide.Element('
@@ -450,7 +535,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 			} else {
 			} else {
 				blendSpace2D.points.splice(index, 1);
 				blendSpace2D.points.splice(index, 1);
 				blendSpace2D.triangulate();
 				blendSpace2D.triangulate();
-				if (select)
+				if (select || selectedPoint == index)
 					setSelection(prevSelection);
 					setSelection(prevSelection);
 			}
 			}
 			refreshGraph();
 			refreshGraph();

+ 3 - 2
hrt/animgraph/AnimGraph.hx

@@ -16,6 +16,7 @@ typedef SerializedEdge = {
 	outputId: Int,
 	outputId: Int,
 };
 };
 
 
+
 @:access(hrt.animgraph.AnimGraphInstance)
 @:access(hrt.animgraph.AnimGraphInstance)
 class AnimGraph extends hrt.prefab.Prefab {
 class AnimGraph extends hrt.prefab.Prefab {
 
 
@@ -34,8 +35,8 @@ class AnimGraph extends hrt.prefab.Prefab {
 		Get the animation "template" for this AnimGraph.
 		Get the animation "template" for this AnimGraph.
 		This anim should be instanciated using getInstance() after that (or use the h3d.scene.Object.playAnimation() function that does this for you)
 		This anim should be instanciated using getInstance() after that (or use the h3d.scene.Object.playAnimation() function that does this for you)
 	**/
 	**/
-	public function getAnimation(previewNode: hrt.animgraph.nodes.AnimNode = null) : AnimGraphInstance {
-		return AnimGraphInstance.fromAnimGraph(this, previewNode);
+	public function getAnimation(previewNode: hrt.animgraph.nodes.AnimNode = null, resolver: hrt.animgraph.AnimGraphInstance.AnimResolver = null) : AnimGraphInstance {
+		return AnimGraphInstance.fromAnimGraph(this, previewNode, resolver);
 	}
 	}
 
 
 	override function save() {
 	override function save() {

+ 19 - 6
hrt/animgraph/AnimGraphInstance.hx

@@ -10,6 +10,8 @@ class AnimGraphAnimatedObject extends h3d.anim.Animation.AnimatedObject {
 	}
 	}
 }
 }
 
 
+typedef AnimResolver = (instance: AnimGraphInstance, target: h3d.scene.Object, path: String ) -> Null<String>;
+
 @:access(hrt.animgraph.AnimGraph)
 @:access(hrt.animgraph.AnimGraph)
 @:access(hrt.animgraph.Node)
 @:access(hrt.animgraph.Node)
 class AnimGraphInstance extends h3d.anim.Animation {
 class AnimGraphInstance extends h3d.anim.Animation {
@@ -24,28 +26,33 @@ class AnimGraphInstance extends h3d.anim.Animation {
 	var syncCtx = new hrt.animgraph.nodes.AnimNode.GetBoneTransformContext();
 	var syncCtx = new hrt.animgraph.nodes.AnimNode.GetBoneTransformContext();
 	var defaultPoseNode = new hrt.animgraph.nodes.DefaultPose();
 	var defaultPoseNode = new hrt.animgraph.nodes.DefaultPose();
 
 
+	var resolver : AnimResolver = null;
+
 	#if editor
 	#if editor
 	var editorSkipClone : Bool = false;
 	var editorSkipClone : Bool = false;
 	#end
 	#end
 
 
-	static function fromAnimGraph(animGraph:AnimGraph, outputNode: hrt.animgraph.nodes.AnimNode = null) : AnimGraphInstance {
+	static function fromAnimGraph(animGraph:AnimGraph, outputNode: hrt.animgraph.nodes.AnimNode = null, resolver: AnimResolver) : AnimGraphInstance {
 		outputNode ??= cast animGraph.nodes.find((node) -> Std.downcast(node, hrt.animgraph.nodes.Output) != null);
 		outputNode ??= cast animGraph.nodes.find((node) -> Std.downcast(node, hrt.animgraph.nodes.Output) != null);
 		if (outputNode == null)
 		if (outputNode == null)
 			throw "Animgraph has no output node";
 			throw "Animgraph has no output node";
 
 
-		var inst = new AnimGraphInstance(outputNode, animGraph.name, 1000, 1/60.0);
-
+		var inst = new AnimGraphInstance(outputNode, resolver, animGraph.name, 1000, 1/60.0);
 		return inst;
 		return inst;
 	}
 	}
 
 
-	public function new(rootNode: hrt.animgraph.nodes.AnimNode, name: String, framesCount: Int, sampling: Float) {
+	public function new(rootNode: hrt.animgraph.nodes.AnimNode, resolver: AnimResolver = null, name: String, framesCount: Int, sampling: Float) {
 		// Todo : Define a true length for the animation OR make so animations can have an undefined length
 		// Todo : Define a true length for the animation OR make so animations can have an undefined length
 		super(name, framesCount, sampling);
 		super(name, framesCount, sampling);
 		this.rootNode = rootNode;
 		this.rootNode = rootNode;
-
+		this.resolver = resolver ?? defaultResolver;
 		defaultPoseNode = new hrt.animgraph.nodes.DefaultPose();
 		defaultPoseNode = new hrt.animgraph.nodes.DefaultPose();
 	}
 	}
 
 
+	static function defaultResolver(instance: AnimGraphInstance, target: h3d.scene.Object, path: String) : Null<String> {
+		return path;
+	}
+
 	public function setParam(name: String, value: Float) {
 	public function setParam(name: String, value: Float) {
 		var param = parameterMap.get(name);
 		var param = parameterMap.get(name);
 		if (param != null) {
 		if (param != null) {
@@ -78,7 +85,7 @@ class AnimGraphInstance extends h3d.anim.Animation {
 		#end
 		#end
 		if (target != null) throw "Unexpected";
 		if (target != null) throw "Unexpected";
 
 
-		var inst = new AnimGraphInstance(null, name, frameCount, sampling);
+		var inst = new AnimGraphInstance(null, resolver, name, frameCount, sampling);
 		inst.rootNode = cast cloneRec(rootNode, inst);
 		inst.rootNode = cast cloneRec(rootNode, inst);
 		super.clone(inst);
 		super.clone(inst);
 		return inst;
 		return inst;
@@ -127,6 +134,7 @@ class AnimGraphInstance extends h3d.anim.Animation {
 
 
 		var ctx = new hrt.animgraph.nodes.AnimNode.GetBoneContext();
 		var ctx = new hrt.animgraph.nodes.AnimNode.GetBoneContext();
 		ctx.targetObject = base;
 		ctx.targetObject = base;
+		ctx.resolver = resolver.bind(this, base);
 
 
 		var bones = getBones(ctx);
 		var bones = getBones(ctx);
 		if (bones != null) {
 		if (bones != null) {
@@ -223,4 +231,9 @@ class AnimGraphInstance extends h3d.anim.Animation {
 		node.tick(dt);
 		node.tick(dt);
 		node.tickedThisFrame = true;
 		node.tickedThisFrame = true;
 	}
 	}
+
+	/**
+		Set this function in your project to programaticaly return an animation path
+	**/
+	static var customAnimResolver : (instance: AnimGraphInstance, name: String) -> Null<String> = null;
 }
 }

+ 1 - 0
hrt/animgraph/nodes/AnimNode.hx

@@ -9,6 +9,7 @@ class GetBoneContext {
 	}
 	}
 
 
 	public var targetObject:h3d.scene.Object;
 	public var targetObject:h3d.scene.Object;
+	public var resolver : (path: String) -> Null<String>;
 }
 }
 
 
 class GetBoneTransformContext {
 class GetBoneTransformContext {

+ 44 - 21
hrt/animgraph/nodes/BlendSpace2D.hx

@@ -71,26 +71,30 @@ class BlendSpace2D extends AnimNode {
 			if (blendSpacePoint.animPath != null && blendSpacePoint.animPath.length > 0) {
 			if (blendSpacePoint.animPath != null && blendSpacePoint.animPath.length > 0) {
 				try
 				try
 				{
 				{
-					var animIndex = animMap.getOrPut(blendSpacePoint.animPath, {
-						// Create a new animation
-						var index = animInfos.length;
-						var animBase = hxd.res.Loader.currentInstance.load(blendSpacePoint.animPath).toModel().toHmd().loadAnimation();
+					var path = ctx.resolver(blendSpacePoint.animPath);
+					if (path != null) {
+						var animIndex = animMap.getOrPut(path, {
+							// Create a new animation
+							var index = animInfos.length;
+							var animBase = hxd.res.Loader.currentInstance.load(path).toModel().toHmd().loadAnimation();
 
 
-						var proxy = new hrt.animgraph.nodes.Input.AnimProxy(null);
-						var animInstance = animBase.createInstance(proxy);
+							var proxy = new hrt.animgraph.nodes.Input.AnimProxy(null);
+							var animInstance = animBase.createInstance(proxy);
 
 
-						var indexRemap : Array<Null<Int>> = [];
+							var indexRemap : Array<Null<Int>> = [];
 
 
-						for (boneId => obj in animInstance.getObjects()) {
-							var ourId = boneMap.getOrPut(obj.objectName, curOurBoneId++);
-							indexRemap[ourId] = boneId;
-						}
+							for (boneId => obj in animInstance.getObjects()) {
+								var ourId = boneMap.getOrPut(obj.objectName, curOurBoneId++);
+								indexRemap[ourId] = boneId;
+							}
+
+							animInfos.push({anim: animInstance, proxy: proxy, indexRemap: indexRemap});
+							index;
+						});
 
 
-						animInfos.push({anim: animInstance, proxy: proxy, indexRemap: indexRemap});
-						index;
-					});
+						point.animInfo = animInfos[animIndex];
+					}
 
 
-					point.animInfo = animInfos[animIndex];
 				} catch (e) {
 				} catch (e) {
 					trace('Couldn\'t load anim ${blendSpacePoint.animPath} : ${e.toString()}');
 					trace('Couldn\'t load anim ${blendSpacePoint.animPath} : ${e.toString()}');
 				}
 				}
@@ -121,11 +125,13 @@ class BlendSpace2D extends AnimNode {
 	override function tick(dt:Float) {
 	override function tick(dt:Float) {
 		super.tick(dt);
 		super.tick(dt);
 
 
-		for (animInfo in animInfos) {
-			// keep all the animations in sync
-			var scale = (animInfo.anim.getDuration()) / currentAnimLenght;
-			animInfo.anim.update(dt * scale);
-			@:privateAccess animInfo.anim.isSync = false;
+		if (currentAnimLenght > 0) {
+			for (animInfo in animInfos) {
+				// keep all the animations in sync
+				var scale = (animInfo.anim.getDuration()) / currentAnimLenght;
+				animInfo.anim.update(dt * scale);
+				@:privateAccess animInfo.anim.isSync = false;
+			}
 		}
 		}
 	}
 	}
 
 
@@ -190,8 +196,25 @@ class BlendSpace2D extends AnimNode {
 				throw "assert";
 				throw "assert";
 
 
 			currentAnimLenght = 0.0;
 			currentAnimLenght = 0.0;
+
+			// Compensate for null animations that don't have lenght
+			var nulls = 0;
+			var nullWeights: Float = 0;
+			for (i => pt in triangles[currentTriangle]) {
+				if (pt.animInfo == null) {
+					nulls ++;
+					nullWeights += weights[i];
+				}
+			}
+
+			if (nulls < 3) {
+				nullWeights /= (3 - nulls);
+			}
+
 			for (i => pt in triangles[currentTriangle]) {
 			for (i => pt in triangles[currentTriangle]) {
-				currentAnimLenght += pt.animInfo.anim.getDuration()/pt.speed * weights[i];
+				if(pt.animInfo != null) {
+					currentAnimLenght += pt.animInfo.anim.getDuration()/pt.speed * weights[i] + nullWeights;
+				}
 			}
 			}
 		}
 		}