Переглянути джерело

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

LeoVgr 8 місяців тому
батько
коміт
5002595370

+ 64 - 0
bin/style.css

@@ -4176,6 +4176,12 @@ button-2 {
 button-2:hover {
   background-color: var(--hover);
 }
+button-2 value {
+  flex-grow: 1;
+  overflow-x: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
 button-2:active {
   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 {
   height: 40%;
+  position: relative;
   background-color: var(--bg-1);
   margin: 16px;
 }
@@ -4440,6 +4447,14 @@ blend-space-2d-root main-panel graph-container svg .preview-axis {
   stroke: #008a00;
   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 {
   padding: var(--basic-padding);
   display: flex;
@@ -4451,6 +4466,55 @@ blend-space-2d-root properties-container {
 blend-space-2d-root properties-container parameters-container {
   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 {
   padding: var(--basic-padding);
   border: var(--basic-border);

+ 78 - 0
bin/style.less

@@ -4934,6 +4934,13 @@ button-2 {
 		background-color: var(--hover);
 	}
 
+	value {
+		flex-grow: 1;
+		overflow-x: hidden;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+	}
+
 	&:active {
 		box-shadow: inset var(--sublte-shadow);
 	}
@@ -5212,6 +5219,8 @@ blend-space-2d-root {
 		graph-container {
 			height: 40%;
 
+			position: relative;
+
 			background-color: var(--bg-1);
 			margin: 16px;
 
@@ -5260,6 +5269,15 @@ blend-space-2d-root {
 					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 {
 			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;
 	}
 
+
+	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 }> ) {
+
 		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} {

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

@@ -180,7 +180,22 @@ class ModalColumnForm extends Modal {
 		for( i in 0...base.sheets.length ) {
 			var s = base.sheets[i];
 			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.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 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)
 						return;
 
@@ -115,11 +118,14 @@ class BlendSpace2DEditor extends hide.view.FileView {
 							return;
 					}
 
-					svg.setPointerCapture(e.pointerId);
+					dragHandler.setPointerCapture(e.pointerId);
 					e.preventDefault();
 				}
 
-				svg.onpointermove = (e:js.html.PointerEvent) -> {
+
+
+
+				dragHandler.onpointermove = (e:js.html.PointerEvent) -> {
 					if (movingPreview) {
 						var pt = getPointPos(e.clientX, e.clientY, false);
 
@@ -161,7 +167,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 					refreshGraph();
 				}
 
-				svg.onpointerup = (e:js.html.PointerEvent) -> {
+				dragHandler.onpointerup = (e:js.html.PointerEvent) -> {
 					if (movingPreview) {
 						movingPreview = false;
 						refreshPropertiesPannel();
@@ -199,7 +205,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 					movedPoint = -1;
 				}
 
-				svg.oncontextmenu = (e:js.html.MouseEvent) -> {
+				dragHandler.oncontextmenu = (e:js.html.MouseEvent) -> {
 					e.preventDefault();
 
 					var options : Array<hide.comp.ContextMenu.MenuItem> = [];
@@ -238,18 +244,48 @@ class BlendSpace2DEditor extends hide.view.FileView {
 					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)) {
 						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)) {
+
+						var path = e.dataTransfer.getData(AnimList.dragEventKey);
+						if (path.length <= 0)
+							return;
+
 						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);
 		{
-			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);
 			propsEditor = new hide.comp.PropsEditor(undo, paramContainer);
 			refreshPropertiesPannel();
@@ -346,6 +382,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 	function refreshPropertiesPannel() {
 		propsEditor.clear();
 
+
 		propsEditor.add(new hide.Element('
 		<div class="group" name="BlendSpace">
 			<dl>
@@ -358,20 +395,68 @@ class BlendSpace2DEditor extends hide.view.FileView {
 		});
 
 		if (selectedPoint != -1) {
-			propsEditor.add(new hide.Element('
+			var editor = new hide.Element('
 				<div class="group" name="Point">
 					<dl>
 						<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>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>
 				</div>
-			'), blendSpace2D.points[selectedPoint], (_) -> {
+			');
+
+			propsEditor.add(editor, blendSpace2D.points[selectedPoint], (_) -> {
 				blendSpace2D.triangulate();
 				refreshGraph();
 				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('
@@ -450,7 +535,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 			} else {
 				blendSpace2D.points.splice(index, 1);
 				blendSpace2D.triangulate();
-				if (select)
+				if (select || selectedPoint == index)
 					setSelection(prevSelection);
 			}
 			refreshGraph();

+ 3 - 2
hrt/animgraph/AnimGraph.hx

@@ -16,6 +16,7 @@ typedef SerializedEdge = {
 	outputId: Int,
 };
 
+
 @:access(hrt.animgraph.AnimGraphInstance)
 class AnimGraph extends hrt.prefab.Prefab {
 
@@ -34,8 +35,8 @@ class AnimGraph extends hrt.prefab.Prefab {
 		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)
 	**/
-	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() {

+ 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.Node)
 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 defaultPoseNode = new hrt.animgraph.nodes.DefaultPose();
 
+	var resolver : AnimResolver = null;
+
 	#if editor
 	var editorSkipClone : Bool = false;
 	#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);
 		if (outputNode == null)
 			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;
 	}
 
-	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
 		super(name, framesCount, sampling);
 		this.rootNode = rootNode;
-
+		this.resolver = resolver ?? defaultResolver;
 		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) {
 		var param = parameterMap.get(name);
 		if (param != null) {
@@ -78,7 +85,7 @@ class AnimGraphInstance extends h3d.anim.Animation {
 		#end
 		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);
 		super.clone(inst);
 		return inst;
@@ -127,6 +134,7 @@ class AnimGraphInstance extends h3d.anim.Animation {
 
 		var ctx = new hrt.animgraph.nodes.AnimNode.GetBoneContext();
 		ctx.targetObject = base;
+		ctx.resolver = resolver.bind(this, base);
 
 		var bones = getBones(ctx);
 		if (bones != null) {
@@ -223,4 +231,9 @@ class AnimGraphInstance extends h3d.anim.Animation {
 		node.tick(dt);
 		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 resolver : (path: String) -> Null<String>;
 }
 
 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) {
 				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) {
 					trace('Couldn\'t load anim ${blendSpacePoint.animPath} : ${e.toString()}');
 				}
@@ -121,11 +125,13 @@ class BlendSpace2D extends AnimNode {
 	override function tick(dt:Float) {
 		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";
 
 			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]) {
-				currentAnimLenght += pt.animInfo.anim.getDuration()/pt.speed * weights[i];
+				if(pt.animInfo != null) {
+					currentAnimLenght += pt.animInfo.anim.getDuration()/pt.speed * weights[i] + nullWeights;
+				}
 			}
 		}