From 12d599f570980d87e565adb33e9cb4df82c9adae Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sun, 21 Aug 2016 17:52:29 +0200 Subject: [PATCH] Allow dragging of keyframes in simplepathing --- .../gui/GuiKeyframeTimeline.java | 209 +++++++++++++++++- .../simplepathing/gui/GuiPathing.java | 128 +++++++---- 2 files changed, 297 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java index b26b966b..b632a6af 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java @@ -6,23 +6,73 @@ import com.replaymod.core.ReplayMod; import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; +import com.replaymod.replay.ReplayModReplay; +import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.PathSegment; +import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.pathing.property.Property; import com.replaymod.simplepathing.ReplayModSimplePathing; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline; +import de.johni0702.minecraft.gui.function.Draggable; +import net.minecraft.client.Minecraft; +import org.apache.commons.lang3.tuple.Pair; +import org.lwjgl.util.Point; import org.lwjgl.util.ReadableDimension; +import org.lwjgl.util.ReadablePoint; import javax.annotation.Nullable; +import java.util.Comparator; +import java.util.Optional; -public class GuiKeyframeTimeline extends AbstractGuiTimeline { +public class GuiKeyframeTimeline extends AbstractGuiTimeline implements Draggable { protected static final int KEYFRAME_SIZE = 5; protected static final int KEYFRAME_TEXTURE_X = 74; protected static final int KEYFRAME_TEXTURE_Y = 20; + private static final int DOUBLE_CLICK_INTERVAL = 250; + private static final int DRAGGING_THRESHOLD = KEYFRAME_SIZE; private final GuiPathing gui; + /** + * The keyframe that was last clicked on using the left mouse button. + */ + private Keyframe lastClickedKeyframe; + + /** + * Id of the path of {@link #lastClickedKeyframe}. + */ + private int lastClickedPath; + + /** + * The time at which {@link #lastClickedKeyframe} was updated. + * According to {@link Minecraft#getSystemTime()}. + */ + private long lastClickedTime; + + /** + * Whether to handle dragging events. + */ + private boolean dragging; + + /** + * Whether we have surpassed the initial threshold and are actually dragging the keyframe. + */ + private boolean actuallyDragging; + + /** + * Where the mouse was when {@link #dragging} started. + */ + private int draggingStartX; + + /** + * Change caused by dragging. Whenever the user moves the keyframe further, the previous change is undone + * and a new one is created. This way when the mouse is released, only one change is in the undo history. + */ + private Change draggingChange; + public GuiKeyframeTimeline(GuiPathing gui) { this.gui = gui; } @@ -91,6 +141,163 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline getKeyframe(ReadablePoint position) { + int time = getTimeAt(position.getX(), position.getY()); + if (time != -1) { + Point mouse = new Point(position); + getContainer().convertFor(this, mouse); + int mouseY = mouse.getY(); + if (mouseY > BORDER_TOP && mouseY < BORDER_TOP + 2 * KEYFRAME_SIZE) { + Timeline timeline = gui.getMod().getCurrentTimeline(); + int path; + if (mouseY <= BORDER_TOP + KEYFRAME_SIZE) { + // Position keyframe + path = GuiPathing.POSITION_PATH; + } else { + // Time keyframe + path = GuiPathing.TIME_PATH; + } + int visibleTime = (int) (getZoom() * getLength()); + int tolerance = visibleTime * KEYFRAME_SIZE / (size.getWidth() - BORDER_LEFT - BORDER_RIGHT) / 2; + Optional keyframe = timeline.getPaths().get(path).getKeyframes().stream() + .filter(k -> Math.abs(k.getTime() - time) <= tolerance) + .sorted(Comparator.comparing(k -> Math.abs(k.getTime() - time))) + .findFirst(); + if (keyframe.isPresent()) { + return Pair.of(path, keyframe.get()); + } + } + } + return null; + } + + @Override + public boolean mouseClick(ReadablePoint position, int button) { + int time = getTimeAt(position.getX(), position.getY()); + Pair pathKeyframePair = getKeyframe(position); + if (pathKeyframePair != null) { + // Clicked on keyframe + Keyframe keyframe = pathKeyframePair.getRight(); + if (button == 0) { // Left click + long now = Minecraft.getSystemTime(); + if (lastClickedKeyframe == keyframe) { + // Clicked the same keyframe again, potentially a double click + if (now - lastClickedTime < DOUBLE_CLICK_INTERVAL) { + // Yup, double click, open the edit keyframe gui + gui.openEditKeyframePopup(keyframe); + return true; + } + } + // Not a double click, just update the click time and selection + lastClickedTime = now; + lastClickedKeyframe = keyframe; + lastClickedPath = pathKeyframePair.getLeft(); + selectKeyframe(lastClickedPath, lastClickedKeyframe); + // We might be dragging + draggingStartX = position.getX(); + dragging = true; + } else if (button == 1) { // Right click + for (Property property : keyframe.getProperties()) { + applyPropertyToGame(property, keyframe); + } + } + return true; + } else if (time != -1) { + // Clicked on timeline but not on any keyframe + setCursorPosition(time); + return true; + } + // Missed timeline + return false; + } + + // Helper method because generics cannot be defined on blocks + private void applyPropertyToGame(Property property, Keyframe keyframe) { + Optional value = keyframe.getValue(property); + if (value.isPresent()) { + property.applyToGame(value.get(), ReplayModReplay.instance.getReplayHandler()); + } + } + + private void selectKeyframe(int path, Keyframe keyframe) { + if (path == GuiPathing.POSITION_PATH) { + gui.getMod().setSelectedPositionKeyframe(keyframe); + gui.getMod().setSelectedTimeKeyframe(null); + } else { + gui.getMod().setSelectedPositionKeyframe(null); + gui.getMod().setSelectedTimeKeyframe(keyframe); + } + } + + @Override + public boolean mouseDrag(ReadablePoint position, int button, long timeSinceLastCall) { + if (!dragging) { + return false; + } + + if (!actuallyDragging) { + // Check if threshold has been passed by now + if (Math.abs(position.getX() - draggingStartX) >= DRAGGING_THRESHOLD) { + actuallyDragging = true; + } + } + if (actuallyDragging) { + // Threshold passed + Path path = gui.getMod().getCurrentTimeline().getPaths().get(lastClickedPath); + Point mouse = new Point(position); + getContainer().convertFor(this, mouse); + int mouseX = mouse.getX(); + int width = size.getWidth(); + int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; + double segmentLength = getLength() * getZoom(); + double segmentTime = segmentLength * (mouseX - BORDER_LEFT) / bodyWidth; + int newTime = Math.min(Math.max((int) Math.round(getOffset() + segmentTime), 0), getLength()); + if (newTime < 0) { + return true; + } + + // If there already is a keyframe at the target time, then increase the time by one until there is none + while (path.getKeyframe(newTime) != null) { + newTime++; + } + + // First undo any previous changes + if (draggingChange != null) { + draggingChange.undo(gui.getMod().getCurrentTimeline()); + } + + // Move keyframe to new position and + // store change for later undoing / pushing to history + draggingChange = gui.moveKeyframe(lastClickedPath, lastClickedKeyframe, newTime); + + // Selected keyframe has been replaced + selectKeyframe(lastClickedPath, path.getKeyframe(newTime)); + + // Path has been changed + path.updateAll(); + } + return true; + } + + @Override + public boolean mouseRelease(ReadablePoint position, int button) { + if (dragging) { + if (actuallyDragging) { + gui.getMod().getCurrentTimeline().pushChange(draggingChange); + draggingChange = null; + actuallyDragging = false; + } + dragging = false; + return true; + } + return false; + } + @Override protected GuiKeyframeTimeline getThis() { return this; diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index 68fba27a..d3c1f2ea 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -22,6 +22,7 @@ import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.PathSegment; import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.pathing.property.Property; import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.replaystudio.util.Location; import com.replaymod.simplepathing.ReplayModSimplePathing; @@ -32,7 +33,6 @@ import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.element.*; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime; -import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; @@ -91,15 +91,7 @@ public class GuiPathing { } super.draw(renderer, size, renderInfo); } - }.onClick(new IGuiTimeline.OnClick() { - @Override - public void run(int time) { - timeline.setCursorPosition(time); - // TODO: Keyframe selection - mod.setSelectedPositionKeyframe(null); - mod.setSelectedTimeKeyframe(null); - } - }).setSize(Integer.MAX_VALUE, 20).setLength(30 * 60 * 1000).setMarkers(); + }.setSize(Integer.MAX_VALUE, 20).setLength(30 * 60 * 1000).setMarkers(); public final GuiHorizontalScrollbar scrollbar = new GuiHorizontalScrollbar().setSize(Integer.MAX_VALUE, 9); {scrollbar.onValueChanged(new Runnable() { @@ -399,7 +391,7 @@ public class GuiPathing { Timeline timeline = mod.getCurrentTimeline(); Path path = timeline.getPaths().get(isTime ? TIME_PATH : POSITION_PATH); - Keyframe keyframe = path.getKeyframe(time); + Keyframe keyframe = isTime ? mod.getSelectedTimeKeyframe() : mod.getSelectedPositionKeyframe(); Change change; if (keyframe == null) { change = AddKeyframe.create(path, time); @@ -442,33 +434,9 @@ public class GuiPathing { // Update interpolators for spectator keyframes // while this is overkill, it is far simpler than updating differently for every possible case if (!isTime) { - Interpolator interpolator = null; - boolean isSpectatorInterpolator = false; - for (PathSegment segment : path.getSegments()) { - if (segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent() - && segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()) { - // Spectator segment - if (!isSpectatorInterpolator) { - isSpectatorInterpolator = true; - interpolator = new LinearInterpolator(); - interpolator.registerProperty(SpectatorProperty.PROPERTY); - } - SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator); - setInterpolator.apply(timeline); - change = CombinedChange.createFromApplied(change, setInterpolator); - } else { - // Normal segment - if (isSpectatorInterpolator || interpolator == null) { - isSpectatorInterpolator = false; - interpolator = new CubicSplineInterpolator(); - interpolator.registerProperty(CameraProperties.POSITION); - interpolator.registerProperty(CameraProperties.ROTATION); - } - SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator); - setInterpolator.apply(timeline); - change = CombinedChange.createFromApplied(change, setInterpolator); - } - } + Change interpolators = updateInterpolators(); + interpolators.apply(timeline); + change = CombinedChange.createFromApplied(change, interpolators); } Change specPosUpdate = updateSpectatorPositions(); @@ -484,7 +452,35 @@ public class GuiPathing { } } - private Change updateSpectatorPositions() { + public Change updateInterpolators() { + List changes = new ArrayList<>(); + Interpolator interpolator = null; + boolean isSpectatorInterpolator = false; + for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getSegments()) { + if (segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent() + && segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()) { + // Spectator segment + if (!isSpectatorInterpolator) { + isSpectatorInterpolator = true; + interpolator = new LinearInterpolator(); + interpolator.registerProperty(SpectatorProperty.PROPERTY); + } + changes.add(SetInterpolator.create(segment, interpolator)); + } else { + // Normal segment + if (isSpectatorInterpolator || interpolator == null) { + isSpectatorInterpolator = false; + interpolator = new CubicSplineInterpolator(); + interpolator.registerProperty(CameraProperties.POSITION); + interpolator.registerProperty(CameraProperties.ROTATION); + } + changes.add(SetInterpolator.create(segment, interpolator)); + } + } + return CombinedChange.create(changes.toArray(new Change[changes.size()])); + } + + public Change updateSpectatorPositions() { List changes = new ArrayList<>(); Path positionPath = mod.getCurrentTimeline().getPaths().get(POSITION_PATH); Path timePath = mod.getCurrentTimeline().getPaths().get(TIME_PATH); @@ -514,6 +510,56 @@ public class GuiPathing { return CombinedChange.create(changes.toArray(new Change[changes.size()])); } + public Change moveKeyframe(int pathId, Keyframe keyframe, long newTime) { + Timeline timeline = mod.getCurrentTimeline(); + Path path = timeline.getPaths().get(pathId); + // Interpolator might be required later (only if path is the time path) + Optional interpolator = + path.getSegments().stream().findFirst().map(PathSegment::getInterpolator); + + // First remove the old keyframe + Change removeChange = RemoveKeyframe.create(path, keyframe); + removeChange.apply(timeline); + + // and add a new one at the correct time + Change addChange = AddKeyframe.create(path, newTime); + addChange.apply(timeline); + path.getKeyframe(newTime); + + // Then copy over all properties + UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime)); + for (Property property : keyframe.getProperties()) { + copyProperty(property, keyframe, builder); + } + Change propertyChange = builder.done(); + propertyChange.apply(timeline); + + // Finally set the interpolators + Change interpolatorChange; + if (pathId == GuiPathing.POSITION_PATH) { + // Position / Spectator keyframes need special handling + interpolatorChange = updateInterpolators(); + } else { + // Time keyframes only need updating when only one segment of them exists + if (path.getSegments().size() == 1) { + interpolatorChange = SetInterpolator.create(path.getSegments().iterator().next(), interpolator.get()); + } else { + interpolatorChange = CombinedChange.create(); // Noop change + } + } + interpolatorChange.apply(timeline); + // and update spectator positions + Change spectatorChange = updateSpectatorPositions(); + spectatorChange.apply(timeline); + + return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, interpolatorChange, spectatorChange); + } + + // Helper method because generics cannot be defined on blocks + private void copyProperty(Property property, Keyframe from, UpdateKeyframeProperties.Builder to) { + from.getValue(property).ifPresent(value -> to.setValue(property, value)); + } + public ReplayModSimplePathing getMod() { return mod; } @@ -522,6 +568,10 @@ public class GuiPathing { return entityTracker; } + public void openEditKeyframePopup(Keyframe keyframe) { + + } + private class LoadEntityTrackerPopup extends AbstractGuiPopup { private final GuiProgressBar progressBar = new GuiProgressBar(popup).setSize(300, 20) .setI18nLabel("replaymod.gui.loadentitytracker");