فهرست منبع

FX event tracks support

trethaller 6 سال پیش
والد
کامیت
6fdde03d26
7فایلهای تغییر یافته به همراه324 افزوده شده و 36 حذف شده
  1. 26 3
      bin/style.css
  2. 29 3
      bin/style.less
  3. 10 3
      hide/comp/SceneEditor.hx
  4. 117 0
      hide/prefab/fx/AnimEvent.hx
  5. 31 0
      hide/prefab/fx/Event.hx
  6. 12 17
      hide/prefab/fx/FX.hx
  7. 99 10
      hide/view/FXEditor.hx

+ 26 - 3
bin/style.css

@@ -749,7 +749,7 @@ input[type=checkbox]:checked:after {
   color: white;
 }
 .fx-animpanel .track .track-header {
-  height: 20px;
+  min-height: 20px;
   display: flex;
   user-select: none;
 }
@@ -800,13 +800,36 @@ input[type=checkbox]:checked:after {
 }
 .fx-animpanel .track .curve {
   margin-left: 120px;
-  border-bottom: 1px solid black;
-  border-left: 1px solid black;
   position: relative;
 }
 .fx-animpanel .track .curves.hidden {
   display: none;
 }
+.fx-animpanel .track .events {
+  position: relative;
+  overflow-x: hidden;
+  flex-grow: 1;
+  box-shadow: 0px -1px 0px black inset;
+}
+.fx-animpanel .track .events .event {
+  cursor: pointer;
+  min-width: 10px;
+  display: inline-block;
+  height: 17px;
+  user-select: none;
+  position: absolute;
+  background: #202;
+  overflow-x: hidden;
+  padding-top: 3px;
+}
+.fx-animpanel .track .events .event label,
+.fx-animpanel .track .events .event i {
+  margin-left: 3px;
+  display: inline;
+}
+.fx-animpanel .track .events .event:hover {
+  color: white;
+}
 .fx-animpanel .track label.curve-label {
   user-select: none;
   pointer-events: none;

+ 29 - 3
bin/style.less

@@ -829,7 +829,7 @@ input[type=checkbox] {
 
 	.track {
 		.track-header {
-			height: 20px;
+			min-height: 20px;
 			display: flex;
 			user-select: none;
 
@@ -883,13 +883,39 @@ input[type=checkbox] {
 		}
 		.curve {
 			margin-left: @leftPanelWidth;
-			border-bottom: 1px solid black;
-			border-left: 1px solid black;
 			position: relative;
 		}
 		.curves.hidden {
 			display: none;
 		}
+
+		.events {
+			position: relative;
+			overflow-x: hidden;
+			flex-grow: 1;
+			box-shadow: 0px -1px 0px black inset;
+
+			.event {
+				cursor: pointer;
+				min-width: 10px;
+				display: inline-block;
+				height: 17px;
+				user-select: none;
+				position: absolute;
+				background: #202;
+				overflow-x: hidden;
+				padding-top: 3px;
+
+				label, i {
+					margin-left: 3px;
+					display: inline;
+				}
+			}
+
+			.event:hover {
+				color: white;
+			}
+		}
 		label.curve-label {
 			user-select: none;
 			pointer-events: none;

+ 10 - 3
hide/comp/SceneEditor.hx

@@ -857,6 +857,13 @@ class SceneEditor {
 		e.edit(edit);
 	}
 
+	public function showProps(e: PrefabElement) {
+		scene.setCurrent();
+		var edit = makeEditContext([e]);
+		properties.clear();
+		fillProps(edit, e);
+	}
+
 	public function selectObjects( elts : Array<PrefabElement>, ?includeTree=true) {
 		scene.setCurrent();
 		if( curEdit != null )
@@ -1296,7 +1303,7 @@ class SceneEditor {
 			obj3d.updateInstance(ctx);
 	}
 
-	function deleteElements(elts : Array<PrefabElement>) {
+	public function deleteElements(elts : Array<PrefabElement>, ?then: Void->Void) {
 		var fullRefresh = false;
 		var undoes = [];
 		for(elt in elts) {
@@ -1314,7 +1321,7 @@ class SceneEditor {
 			refresh(fullRefresh ? Full : Partial, then);
 		}
 
-		refreshFunc(deselect);
+		refreshFunc(then != null ? then : deselect);
 
 		undo.change(Custom(function(undo) {
 			if(!undo && !fullRefresh)
@@ -1325,7 +1332,7 @@ class SceneEditor {
 			if(undo)
 				for(e in elts) makeInstance(e);
 
-			refreshFunc(selectObjects.bind(undo ? elts : []));
+			refreshFunc(then != null ? then : selectObjects.bind(undo ? elts : []));
 		}));
 	}
 

+ 117 - 0
hide/prefab/fx/AnimEvent.hx

@@ -0,0 +1,117 @@
+package hide.prefab.fx;
+
+class AnimEvent extends hide.prefab.fx.Event {
+
+	public var animation: String;
+	public var speed : Float = 1.0;
+	public var duration : Float = 0.0;
+	public var offset : Float = 0.0;
+
+	public function new(?parent) {
+		super(parent);
+		this.type = "animEvent";
+	}
+
+	override function save() {
+		var obj : Dynamic = super.save();
+		obj.animation = animation;
+		if(speed != 1.0) obj.speed = speed;
+		if(duration > 0) obj.duration = duration;
+		if(offset > 0) obj.offset = offset;
+		return obj;
+	}
+
+	override function load(obj:Dynamic) {
+		super.load(obj);
+		this.animation = obj.animation;
+		if(obj.speed != null) speed = obj.speed;
+		if(obj.duration != null) duration = obj.duration;
+		if(obj.offset != null) offset = obj.offset;
+	}
+
+	override function prepare(ctx: Context) : Event.EventInstance {
+		var obj = ctx.local3d;
+		var anim = ctx.loadAnimation(animation);
+		var lastTime = -1.0;
+		var inst = null;
+		if(anim == null) { return null; }
+		return {
+			evt: this,
+			play: function() {
+			},
+			setTime: function(localTime) {
+				var duration = duration > 0 ? duration : anim.getDuration();
+				if(localTime > 0 && localTime < duration) {
+					if(inst == null) {
+						inst = obj.playAnimation(anim);
+						inst.pause = true;
+						inst.loop = false;
+					}
+					inst.setFrame(hxd.Math.clamp((localTime + offset) * anim.sampling * anim.speed * speed, 0, anim.frameCount));
+				}
+				else inst = null;
+				lastTime = localTime;
+			}
+		}
+	}
+
+	#if editor
+	override function edit( ctx : EditContext ) {
+		super.edit(ctx);
+		var props = ctx.properties.add(new hide.Element('
+			<div class="group" name="Event">
+				<dl>
+					<dt>Time</dt><dd><input type="number" value="0" field="time"/></dd>
+					<dt>Animation</dt><dd><select><option value="">-- Choose --</option></select></dd>
+					<dt>Speed</dt><dd><input type="number" value="0" field="speed"/></dd>
+					<dt>Duration</dt><dd><input type="number" value="0" field="duration"/></dd>
+					<dt>Offset</dt><dd><input type="number" value="0" field="offset"/></dd>
+				</dl>
+			</div>
+		'),this, function(pname) {
+			ctx.onChange(this, pname);
+		});
+
+		if(parent.source != null) {
+			var select = props.find("select");
+			var anims = try ctx.scene.listAnims(parent.source) catch(e: Dynamic) [];
+			for( a in anims )
+				new hide.Element('<option>').attr("value", ctx.ide.makeRelative(a)).text(ctx.scene.animationName(a)).appendTo(select);
+			if( animation != null )
+				select.val(animation);
+			select.change(function(_) {
+				var v = select.val();
+				var prev = animation;
+				if( v == "" ) {
+					animation = null;
+				} else {
+					animation = v;
+				}
+				ctx.properties.undo.change(Field(this, "animation", prev));
+			});
+		}
+	}
+
+	override function getHideProps() : HideProps {
+		return {
+			icon : "play-circle", name : "AnimEvent",
+			allowParent : (p) -> p.to(Model) != null };
+	}
+
+	override function getDisplayInfo(ctx: EditContext) {
+		var anim = null;
+		if(animation != null) {
+			try {
+				anim = ctx.rootContext.loadAnimation(animation);
+			} catch(e : hxd.res.NotFound) { }
+		}
+		return {
+			label: anim != null ? ctx.scene.animationName(animation) : "null",
+			length: duration > 0 ? duration : anim != null ? anim.getDuration() : 1.0
+		}
+	}
+	#end
+
+	static var _ = hxd.prefab.Library.register("animEvent", AnimEvent);
+
+}

+ 31 - 0
hide/prefab/fx/Event.hx

@@ -0,0 +1,31 @@
+package hide.prefab.fx;
+
+typedef EventInstance = {
+	evt: Event,
+	play: Void->Void,
+	setTime: Float->Void
+};
+
+class Event extends hxd.prefab.Prefab {
+	public var time: Float = 0.0;
+
+	override function save() : {} {
+		return {
+			time: time
+		};
+	}
+
+	override function load(obj: Dynamic) {
+		this.time = obj.time;
+	}
+
+	public function prepare(ctx: Context) : EventInstance {
+		return null;
+	}
+
+	#if editor
+	public function getDisplayInfo(ctx: EditContext) : { label: String, length: Float } {
+		throw "Not implemented";
+	}
+	#end
+}

+ 12 - 17
hide/prefab/fx/FX.hx

@@ -38,6 +38,7 @@ class ShaderAnimation extends Evaluator {
 typedef ObjectAnimation = {
 	elt: hide.prefab.Object3D,
 	obj: h3d.scene.Object,
+	events: Array<hide.prefab.fx.Event.EventInstance>,
 	?position: Value,
 	?scale: Value,
 	?rotation: Value,
@@ -166,29 +167,18 @@ class FXAnimation extends h3d.scene.Object {
 					}
 				}
 			}
+
+			if(anim.events != null) {
+				for(evt in anim.events) {
+					evt.setTime(localTime - evt.evt.time);
+				}
+			}
 		}
 
 		for(anim in shaderAnims) {
 			anim.setTime(localTime);
 		}
 
-		function setAnimFrame(object : h3d.scene.Object, time : Float){
-			var anim = object.currentAnimation;
-			if(object.currentAnimation != null){
-				anim.loop = false;
-				anim.pause = true;
-				if(loopAnims){
-					var frameTime = time * anim.sampling * anim.speed;
-					var frameIndex = frameTime - hxd.Math.floor(frameTime / anim.frameCount) * anim.frameCount;
-					anim.setFrame( frameIndex );
-				}else
-					anim.setFrame( hxd.Math.clamp(time * anim.sampling * anim.speed, 0, anim.frameCount));
-			}
-			for(i in 0...object.numChildren)
-				setAnimFrame(object.getChildAt(i), time);
-		}
-		setAnimFrame(this, localTime);
-
 		for(em in emitters) {
 			if(em.visible)
 				em.setTime(worldTime);
@@ -298,9 +288,14 @@ class FX extends hxd.prefab.Library {
 			return hide.prefab.Curve.getColorValue(curves);
 		}
 
+		var events = [for(evt in elt.getAll(Event)) evt.prepare(objCtx)];
+		if(events.length > 0)
+			anyFound = true;
+
 		var anim : ObjectAnimation = {
 			elt: obj3d,
 			obj: objCtx.local3d,
+			events: events.length > 0 ? events : null,
 			position: makeVector("position", 0.0),
 			scale: makeVector("scale", 1.0, true),
 			rotation: makeVector("rotation", 0.0, 360.0),

+ 99 - 10
hide/view/FXEditor.hx

@@ -4,6 +4,7 @@ using Lambda;
 import hide.Element;
 import hide.prefab.Prefab in PrefabElement;
 import hide.prefab.Curve;
+import hide.prefab.fx.Event;
 
 typedef PropTrackDef = {
 	name: String,
@@ -110,7 +111,7 @@ class FXEditor extends FileView {
 	var previewMax : Float;
 	var curveEdits : Array<hide.comp.CurveEditor>;
 	var timeLineEl : Element;
-	var refreshDopesheetKeys : Array<Bool->Void> = [];
+	var afterPanRefreshes : Array<Bool->Void> = [];
 	var statusText : h2d.Text;
 
 	var scriptEditor : hide.comp.ScriptEditor;
@@ -377,6 +378,10 @@ class FXEditor extends FileView {
 			previewMax = hxd.Math.min(data.duration == 0 ? 5000 : data.duration, previewMax);
 			refreshTimeline(false);
 		}
+
+		if(p.to(Event) != null) {
+			afterPan(false);
+		}
 	}
 
 	override function onDragDrop(items : Array<String>, isDrop : Bool) {
@@ -437,12 +442,12 @@ class FXEditor extends FileView {
 		for(curve in curveEdits) {
 			curve.setPan(xOffset, curve.yOffset);
 		}
-		for(clb in refreshDopesheetKeys) {
+		for(clb in afterPanRefreshes) {
 			clb(anim);
 		}
 	}
 
-	function addTrackEdit(trackName: String, curves: Array<Curve>, tracksEl: Element) {
+	function addCurvesTrack(trackName: String, curves: Array<Curve>, tracksEl: Element) {
 		var keyTimeTolerance = 0.05;
 		var trackEdits : Array<hide.comp.CurveEditor> = [];
 		var trackEl = new Element('<div class="track">
@@ -645,7 +650,7 @@ class FXEditor extends FileView {
 						});
 					}
 				});
-				refreshDopesheetKeys.push(function(anim) {
+				afterPanRefreshes.push(function(anim) {
 					updatePos();
 				});
 				refreshKey(key, keyEl);
@@ -693,16 +698,94 @@ class FXEditor extends FileView {
 		updateExpanded();
 	}
 
+	function addEventsTrack(events: Array<Event>, tracksEl: Element) {
+		var trackEl = new Element('<div class="track">
+			<div class="track-header">
+				<div class="track-prop">
+					<label>Events</label>
+				</div>
+				<div class="events"></div>
+			</div>
+		</div>');
+		var eventsEl = trackEl.find(".events");
+		var items : Array<{el: Element, event: Event }> = [];
+		function refreshItems() {
+			var yoff = 1;
+			for(item in items) {
+				var info = item.event.getDisplayInfo(sceneEditor.curEdit);
+				item.el.css({left: xt(item.event.time), top: yoff});
+				item.el.width(info.length * xScale);
+				yoff += 21;
+			}
+			eventsEl.css("height", yoff + 1);
+		}
+
+		function refreshTrack() {
+			trackEl.remove();
+			trackEl = addEventsTrack(events, tracksEl);
+		}
+
+		for(event in events) {
+			var info = event.getDisplayInfo(sceneEditor.curEdit);
+			var evtEl = new Element('<div class="event">
+				<i class="icon fa fa-play-circle"></i><label>${info.label}</label>
+			</div>').appendTo(eventsEl);
+			items.push({el: evtEl, event: event });
+
+			evtEl.click(function(e) {
+				sceneEditor.showProps(event);
+			});
+
+			evtEl.contextmenu(function(e) {
+				e.preventDefault();
+				e.stopPropagation();
+				new hide.comp.ContextMenu([
+					{
+						label: "Delete", click: function() {
+							events.remove(event);
+							sceneEditor.deleteElements([event], refreshTrack);
+						}
+					}
+				]);
+			});
+
+			evtEl.mousedown(function(e) {
+				var offsetX = e.clientX - xt(event.time);
+				e.preventDefault();
+				e.stopPropagation();
+				if(e.button == 2) {
+				}
+				else {
+					var prevVal = event.time;
+					startDrag(function(e) {
+						var x = ixt(e.clientX - offsetX);
+						x = hxd.Math.max(0, x);
+						x = untyped parseFloat(x.toFixed(5));
+						event.time = x;
+						refreshItems();
+					}, function(e) {
+						undo.change(Field(event, "time", prevVal), refreshItems);
+					});
+				}
+			});
+		}
+		refreshItems();
+		afterPanRefreshes.push(function(anim) refreshItems());
+		tracksEl.append(trackEl);
+		return trackEl;
+	}
+
 	function rebuildAnimPanel() {
 		var selection = sceneEditor.getSelection();
 		var scrollPanel = element.find(".anim-scroll");
 		scrollPanel.empty();
 		curveEdits = [];
-		refreshDopesheetKeys = [];
+		afterPanRefreshes = [];
 
 		var sections : Array<{
 			elt: PrefabElement,
-			curves: Array<Curve>
+			curves: Array<Curve>,
+			events: Array<Event>
 		}> = [];
 
 		for(elt in selection) {
@@ -712,13 +795,16 @@ class FXEditor extends FileView {
 			}
 			var sect = sections.find(s -> s.elt == root);
 			if(sect == null) {
-				sect = {elt: root, curves: []};
+				sect = {elt: root, curves: [], events: []};
 				sections.push(sect);
 			}
 			var curves = elt.flatten(hide.prefab.Curve);
-			for(c in curves) {
+			for(c in curves)
 				sect.curves.push(c);
-			}
+
+			var events = elt.flatten(Event);
+			for(e in events)
+				sect.events.push(e);
 		}
 
 		for(sec in sections) {
@@ -735,9 +821,12 @@ class FXEditor extends FileView {
 				new hide.comp.ContextMenu(menuItems);
 			});
 			var tracksEl = objPanel.find(".tracks");
+
+			addEventsTrack(sec.events, tracksEl);
+
 			var groups = hide.prefab.Curve.getGroups(sec.curves);
 			for(group in groups) {
-				addTrackEdit(group.name, group.items, tracksEl);
+				addCurvesTrack(group.name, group.items, tracksEl);
 			}
 		}
 	}