From cd162113094b9e4c575788a934b7a2d5f4bc84b9 Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Fri, 13 Jan 2017 12:05:07 +0100 Subject: [PATCH] Register new Property to imply that the path segment following a keyframe has a user-set interpolator instead of the default Modified Keyframe GUI to allow for custom Path interpolator --- .../ExplicitInterpolationProperty.java | 43 ++++ .../simplepathing/ReplayModSimplePathing.java | 37 ++- .../com/replaymod/simplepathing/Setting.java | 16 +- .../simplepathing/gui/GuiEditKeyframe.java | 220 +++++++++++++++++- .../simplepathing/gui/GuiPathing.java | 123 ++++++++-- .../assets/replaymod/lang/en_US.lang | 9 + 6 files changed, 417 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java diff --git a/src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java b/src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java new file mode 100644 index 00000000..6443e91b --- /dev/null +++ b/src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java @@ -0,0 +1,43 @@ +package com.replaymod.pathing.properties; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.replaymod.replaystudio.pathing.property.AbstractProperty; +import com.replaymod.replaystudio.pathing.property.PropertyPart; +import lombok.NonNull; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +/** + * Property indicating that the next path segment's interpolator is fixed + */ +public class ExplicitInterpolationProperty extends AbstractProperty { + public static final ExplicitInterpolationProperty PROPERTY = new ExplicitInterpolationProperty(); + + private ExplicitInterpolationProperty() { + super("interpolationFixed", "", null, new Object()); + } + + @Override + public Collection> getParts() { + return Collections.emptyList(); + } + + @Override + public void applyToGame(Object value, @NonNull Object replayHandler) { + // dummy property, do nothing + } + + @Override + public void toJson(JsonWriter writer, Object value) throws IOException { + writer.nullValue(); + } + + @Override + public Void fromJson(JsonReader reader) throws IOException { + reader.nextNull(); + return null; + } +} diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index 8a884a04..d55ae73c 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -5,6 +5,7 @@ import com.google.gson.stream.JsonWriter; import com.replaymod.core.ReplayMod; import com.replaymod.core.events.SettingsChangedEvent; import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.events.ReplayCloseEvent; @@ -16,6 +17,7 @@ import com.replaymod.replaystudio.pathing.interpolation.Interpolator; import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.pathing.property.Property; import com.replaymod.simplepathing.gui.GuiPathing; import com.replaymod.simplepathing.preview.PathPreview; import net.minecraftforge.fml.common.FMLCommonHandler; @@ -24,7 +26,10 @@ import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.apache.logging.log4j.Logger; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; @Mod(modid = ReplayModSimplePathing.MOD_ID, version = "@MOD_VERSION@", @@ -68,7 +73,7 @@ public class ReplayModSimplePathing implements PathingRegistry { @SubscribeEvent public void onSettingsChanged(SettingsChangedEvent event) { - if (event.getKey() == Setting.LINEAR_INTERPOLATION) { + if (event.getKey() == Setting.DEFAULT_INTERPOLATION) { if (currentTimeline != null && guiPathing != null) { currentTimeline.applyChange(guiPathing.updateInterpolators()); } @@ -105,6 +110,7 @@ public class ReplayModSimplePathing implements PathingRegistry { timeline.registerProperty(CameraProperties.POSITION); timeline.registerProperty(CameraProperties.ROTATION); timeline.registerProperty(SpectatorProperty.PROPERTY); + timeline.registerProperty(ExplicitInterpolationProperty.PROPERTY); return timeline; } @@ -134,6 +140,35 @@ public class ReplayModSimplePathing implements PathingRegistry { } } + /** + * Clones an interpolator by de- and reserializing it. + * @param interpolator The interpolator to clone + * @return The cloned interpolator + * @throws IOException + */ + public Interpolator cloneInterpolator(Interpolator interpolator) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter printWriter = new PrintWriter(baos); + + JsonWriter jsonWriter = new JsonWriter(printWriter); + jsonWriter.beginArray(); + serializeInterpolator(jsonWriter, interpolator); + jsonWriter.endArray(); + jsonWriter.flush(); + + String json = baos.toString(); + + JsonReader jsonReader = new JsonReader(new StringReader(json)); + jsonReader.beginArray(); + Interpolator cloned = deserializeInterpolator(jsonReader); + + for (Property p : interpolator.getKeyframeProperties()) { + cloned.registerProperty(p); + } + + return cloned; + } + public ReplayMod getCore() { return core; } diff --git a/src/main/java/com/replaymod/simplepathing/Setting.java b/src/main/java/com/replaymod/simplepathing/Setting.java index b1e99e17..328714b5 100644 --- a/src/main/java/com/replaymod/simplepathing/Setting.java +++ b/src/main/java/com/replaymod/simplepathing/Setting.java @@ -2,9 +2,23 @@ package com.replaymod.simplepathing; import com.replaymod.core.SettingsRegistry; +import java.util.ArrayList; + public final class Setting extends SettingsRegistry.SettingKeys { public static final Setting PATH_PREVIEW = make("pathpreview", "pathpreview", true); - public static final Setting LINEAR_INTERPOLATION = make("linear_interpolation", "linearinterpolation", false); + public static final SettingsRegistry.MultipleChoiceSettingKeys DEFAULT_INTERPOLATION = + new SettingsRegistry.MultipleChoiceSettingKeys<>( + "simplepathing", "interpolator", "replaymod.gui.settings.interpolator", + "replaymod.gui.editkeyframe.interpolator.cubic.name"); + + static { + DEFAULT_INTERPOLATION.setChoices(new ArrayList() { + { + add("replaymod.gui.editkeyframe.interpolator.cubic.name"); + add("replaymod.gui.editkeyframe.interpolator.linear.name"); + } + }); + } private static Setting make(String key, String displayName, T defaultValue) { return new Setting<>(key, displayName, defaultValue); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index bc61f618..d1c57a8f 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -1,18 +1,24 @@ package com.replaymod.simplepathing.gui; import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.change.CombinedChange; +import com.replaymod.replaystudio.pathing.change.SetInterpolator; import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties; +import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; +import com.replaymod.replaystudio.pathing.interpolation.Interpolator; +import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; +import com.replaymod.replaystudio.pathing.path.PathSegment; +import com.replaymod.simplepathing.Setting; +import com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorType; import de.johni0702.minecraft.gui.container.GuiPanel; -import de.johni0702.minecraft.gui.element.GuiButton; -import de.johni0702.minecraft.gui.element.GuiLabel; -import de.johni0702.minecraft.gui.element.GuiNumberField; -import de.johni0702.minecraft.gui.element.IGuiLabel; +import de.johni0702.minecraft.gui.element.*; +import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; @@ -20,10 +26,19 @@ import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.minecraft.client.resources.I18n; import org.apache.commons.lang3.tuple.Triple; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.lwjgl.input.Keyboard; import org.lwjgl.util.ReadablePoint; +import java.util.Map; + +import static com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorSettingsPanel.CubicInterpolatorSettingsPanel; +import static com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorSettingsPanel.LinearInterpolatorSettingsPanel; import static de.johni0702.minecraft.gui.utils.Utils.link; public abstract class GuiEditKeyframe> extends AbstractGuiPopup implements Typeable { @@ -31,6 +46,10 @@ public abstract class GuiEditKeyframe> extends Abst return new GuiNumberField().setPrecision(0).setValidateOnFocusChange(true); } + protected static final Logger logger = LogManager.getLogger(); + + protected final GuiPathing guiPathing; + protected final Keyframe keyframe; protected final Path path; @@ -68,6 +87,7 @@ public abstract class GuiEditKeyframe> extends Abst public GuiEditKeyframe(GuiPathing gui, Path path, Keyframe keyframe, String type) { super(ReplayModReplay.instance.getReplayHandler().getOverlay()); + this.guiPathing = gui; this.keyframe = keyframe; this.path = path; @@ -187,8 +207,11 @@ public abstract class GuiEditKeyframe> extends Abst public final GuiNumberField pitchField = newGuiNumberField().setSize(60, 20).setPrecision(5); public final GuiNumberField rollField = newGuiNumberField().setSize(60, 20).setPrecision(5); + public final InterpolationPanel interpolationPanel = new InterpolationPanel(guiPathing); + { - inputs.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5)) + GuiPanel positionInputs = new GuiPanel() + .setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5)) .addElements(new GridLayout.Data(1, 0.5), new GuiLabel().setI18nText("replaymod.gui.editkeyframe.xpos"), xField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camyaw"), yawField, @@ -196,6 +219,9 @@ public abstract class GuiEditKeyframe> extends Abst new GuiLabel().setI18nText("replaymod.gui.editkeyframe.campitch"), pitchField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.zpos"), zField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camroll"), rollField); + + inputs.setLayout(new VerticalLayout().setSpacing(10)).addElements(new VerticalLayout.Data(0.5, false), + positionInputs, interpolationPanel); } public Position(GuiPathing gui, Path path, Keyframe keyframe) { @@ -219,17 +245,193 @@ public abstract class GuiEditKeyframe> extends Abst @Override protected Change save() { - Change change = UpdateKeyframeProperties.create(path, keyframe) + Change setInterpolatorChange = null; + + UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe) .setValue(CameraProperties.POSITION, Triple.of(xField.getDouble(), yField.getDouble(), zField.getDouble())) .setValue(CameraProperties.ROTATION, Triple.of(yawField.getFloat(), pitchField.getFloat(), rollField.getFloat())) - .done(); - change.apply(path.getTimeline()); - return change; + .removeProperty(ExplicitInterpolationProperty.PROPERTY); + + // if the interpolator is not the default, set the ExplicitInterpolationProperty flag + if (interpolationPanel.getInterpolatorType() != InterpolatorType.DEFAULT) { + PathSegment toModify = null; + for (PathSegment segment : path.getSegments()) { + if (segment.getStartKeyframe() == keyframe) { + toModify = segment; + break; + } + } + + if (toModify != null) { + builder.setValue(ExplicitInterpolationProperty.PROPERTY, new Object()); + + Interpolator interpolator = interpolationPanel.getInterpolatorSettingsPanel().createInterpolator(); + interpolator.registerProperty(CameraProperties.POSITION); + interpolator.registerProperty(CameraProperties.ROTATION); + + setInterpolatorChange = SetInterpolator.create(toModify, interpolator); + } else { + logger.warn("The Path segment to modify was not found. Setting interpolator to default."); + } + } + + Change keyframePropertiesChange = builder.done(); + keyframePropertiesChange.apply(path.getTimeline()); + + if (setInterpolatorChange == null) { + return keyframePropertiesChange; + } else { + setInterpolatorChange.apply(path.getTimeline()); + guiPathing.updateInterpolators(); + path.updateAll(); + } + + return CombinedChange.createFromApplied(keyframePropertiesChange, setInterpolatorChange); } @Override protected Position getThis() { return this; } + + public static class InterpolationPanel extends de.johni0702.minecraft.gui.container.AbstractGuiContainer { + + private final GuiPathing guiPathing; + + @Getter + private InterpolatorSettingsPanel interpolatorSettingsPanel; + + private GuiDropdownMenu dropdown; + + @AllArgsConstructor + public enum InterpolatorType { + DEFAULT("default", null), + CUBIC("cubic", CubicSplineInterpolator.class), + LINEAR("linear", LinearInterpolator.class); + + private String localizationKey; + + @Getter + private Class interpolatorClass; + + @Override + public String toString() { + return I18n.format(String.format("replaymod.gui.editkeyframe.interpolator.%1$s.name", localizationKey)); + } + + public String getI18nDescription() { + return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.desc", localizationKey); + } + + public static InterpolatorType fromString(String string) { + for (InterpolatorType t : values()) { + if (t.toString().equals(string)) return t; + } + return CUBIC; //the default + } + + } + + public InterpolationPanel(GuiPathing guiPathing) { + this.guiPathing = guiPathing; + + setLayout(new VerticalLayout()); + + dropdown = new GuiDropdownMenu().setValues(InterpolatorType.values()).setHeight(20); + dropdown.onSelection((index) -> setSettingsPanel(dropdown.getSelectedValue())); + + // set hover tooltips + for (Map.Entry e : dropdown.getDropdownEntries().entrySet()) { + e.getValue().setTooltip(new GuiTooltip().setI18nText(e.getKey().getI18nDescription())); + } + + GuiPanel dropdownPanel = new GuiPanel() + .setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(3).setSpacingY(5)) + .addElements(new GridLayout.Data(1, 0.5), + new GuiLabel().setI18nText("replaymod.gui.editkeyframe.interpolator"), dropdown); + + + addElements(new VerticalLayout.Data(0.5, false), dropdownPanel); + + dropdown.onSelection(0); // trigger the callback once to display settings panel + } + + public void setSettingsPanel(InterpolatorType type) { + removeElement(this.interpolatorSettingsPanel); + + InterpolatorSettingsPanel settingsPanel = null; + switch (getInterpolatorTypeNoDefault(type)) { + case CUBIC: + settingsPanel = new CubicInterpolatorSettingsPanel(); + break; + case LINEAR: + settingsPanel = new LinearInterpolatorSettingsPanel(); + break; + } + + addElements(new GridLayout.Data(0.5, 0.5), settingsPanel); + + this.interpolatorSettingsPanel = settingsPanel; + } + + protected InterpolatorType getInterpolatorTypeNoDefault(InterpolatorType interpolatorType) { + if (interpolatorType == InterpolatorType.DEFAULT || interpolatorType == null) { + InterpolatorType defaultType = InterpolatorType.fromString( + guiPathing.getMod().getCore().getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION)); + return defaultType; + } + return interpolatorType; + } + + public InterpolatorType getInterpolatorType() { + return dropdown.getSelectedValue(); + } + + @Override + protected InterpolationPanel getThis() { + return this; + } + + public static abstract class InterpolatorSettingsPanel extends de.johni0702.minecraft.gui.container.GuiPanel { + + public abstract void loadSettings(I interpolator); + + public abstract I createInterpolator(); + + public static class CubicInterpolatorSettingsPanel extends InterpolatorSettingsPanel { + + @Override + public void loadSettings(CubicSplineInterpolator interpolator) { + } + + @Override + public CubicSplineInterpolator createInterpolator() { + return new CubicSplineInterpolator(); + } + + @Override + protected InterpolatorSettingsPanel getThis() { + return this; + } + } + + public static class LinearInterpolatorSettingsPanel extends InterpolatorSettingsPanel { + + @Override + public void loadSettings(LinearInterpolator interpolator) { + } + + @Override + public LinearInterpolator createInterpolator() { + return new LinearInterpolator(); + } + + @Override + protected InterpolatorSettingsPanel 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 8eb27501..ed3a1134 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -5,9 +5,11 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; import com.replaymod.pathing.gui.GuiKeyframeRepository; import com.replaymod.pathing.player.RealtimeTimelinePlayer; import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.render.gui.GuiRenderSettings; @@ -28,6 +30,7 @@ import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.replaystudio.util.Location; import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.Setting; +import com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorType; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.GuiContainer; @@ -57,10 +60,7 @@ import org.lwjgl.util.WritablePoint; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.function.Consumer; import static com.replaymod.core.utils.Utils.error; @@ -637,11 +637,48 @@ public class GuiPathing { } public Change updateInterpolators() { - boolean linearInterpolation = mod.getCore().getSettingsRegistry().get(Setting.LINEAR_INTERPOLATION); + InterpolatorType type = InterpolatorType.fromString(mod.getCore().getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION)); + + Interpolator defaultInterpolator = null; + + // Iterate over all existing segments until the first interpolator + // of the same type without the fixed flag is found and clone it. + // This way, the default interpolator's settings can be saved on a per-camera-path basis. + // This is useful for interpolators that have customizable properties. + Collection pathSegments = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getSegments(); + + for (PathSegment segment : pathSegments) { + if (segment.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) continue; + if (segment.getInterpolator() == null) continue; + if (type == InterpolatorType.DEFAULT || type.getInterpolatorClass().equals(segment.getInterpolator().getClass())) { + try { + defaultInterpolator = mod.cloneInterpolator(segment.getInterpolator()); + break; + } catch (IOException e) { + Utils.error(LOGGER, replayHandler.getOverlay(), + CrashReport.makeCrashReport(e, "Cloning fixed interpolator"), null); + } + } + } + + // if no segment with the default interpolator was found, + // create a new one with default settings + if (defaultInterpolator == null) { + switch (type) { + case LINEAR: + defaultInterpolator = new LinearInterpolator(); + break; + case CUBIC: + default: + defaultInterpolator = new CubicSplineInterpolator(); + break; + } + } + List changes = new ArrayList<>(); Interpolator interpolator = null; boolean isSpectatorInterpolator = false; - for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getSegments()) { + for (PathSegment segment : pathSegments) { if (segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent() && segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()) { // Spectator segment @@ -653,11 +690,23 @@ public class GuiPathing { changes.add(SetInterpolator.create(segment, interpolator)); } else { // Normal segment - if (isSpectatorInterpolator || interpolator == null) { + boolean explicit = segment.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent(); + if (isSpectatorInterpolator || interpolator == null || explicit) { isSpectatorInterpolator = false; - interpolator = linearInterpolation ? new LinearInterpolator() : new CubicSplineInterpolator(); - interpolator.registerProperty(CameraProperties.POSITION); - interpolator.registerProperty(CameraProperties.ROTATION); + + if (segment.getInterpolator() == null || !explicit) { + interpolator = defaultInterpolator; + interpolator.registerProperty(CameraProperties.POSITION); + interpolator.registerProperty(CameraProperties.ROTATION); + } else { + try { + interpolator = mod.cloneInterpolator(segment.getInterpolator()); + } catch (IOException e) { + Utils.error(LOGGER, replayHandler.getOverlay(), + CrashReport.makeCrashReport(e, "Cloning fixed interpolator"), null); + interpolator = defaultInterpolator; + } + } } changes.add(SetInterpolator.create(segment, interpolator)); } @@ -697,10 +746,18 @@ public class GuiPathing { public Change moveKeyframe(Path path, Keyframe keyframe, long newTime) { Timeline timeline = mod.getCurrentTimeline(); - // Interpolator might be required later (only if path is the time path) - Optional interpolator = + // InterpolatorType might be required later (only if path is the time path) + Optional firstInterpolator = path.getSegments().stream().findFirst().map(PathSegment::getInterpolator); + // The interpolator before the segment + Optional interpolatorBefore = + path.getSegments().stream().filter(s -> s.getEndKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator); + + // The interpolator that follows the segment + Optional interpolatorAfter = + path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator); + // First remove the old keyframe Change removeChange = RemoveKeyframe.create(path, keyframe); removeChange.apply(timeline); @@ -708,7 +765,7 @@ public class GuiPathing { // and add a new one at the correct time Change addChange = AddKeyframe.create(path, newTime); addChange.apply(timeline); - path.getKeyframe(newTime); + Keyframe newKeyframe = path.getKeyframe(newTime); // Then copy over all properties UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime)); @@ -718,25 +775,51 @@ public class GuiPathing { Change propertyChange = builder.done(); propertyChange.apply(timeline); - // Finally set the interpolators - Change interpolatorChange; + // Set the interpolator of the segment before the keyframe to what it was + Change interpolatorBeforeChange; + + Optional segmentBefore = path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKeyframe).findFirst(); + if (segmentBefore.isPresent() && interpolatorBefore.isPresent()) { + interpolatorBeforeChange = SetInterpolator.create(segmentBefore.get(), interpolatorBefore.get()); + } else { + interpolatorBeforeChange = CombinedChange.create(); + } + + interpolatorBeforeChange.apply(timeline); + + // Set the interpolator of the segment after the keyframe to what it was + Change interpolatorAfterChange; + + Optional segmentAfter = path.getSegments().stream().filter(s -> s.getStartKeyframe() == newKeyframe).findFirst(); + if (segmentAfter.isPresent() && interpolatorAfter.isPresent()) { + interpolatorAfterChange = SetInterpolator.create(segmentAfter.get(), interpolatorAfter.get()); + } else { + interpolatorAfterChange = CombinedChange.create(); + } + + interpolatorAfterChange.apply(timeline); + + // Finally update the interpolators + Change interpolatorUpdateChange; if (path.getTimeline().getPaths().indexOf(path) == GuiPathing.POSITION_PATH) { // Position / Spectator keyframes need special handling - interpolatorChange = updateInterpolators(); + interpolatorUpdateChange = 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()); + interpolatorUpdateChange = SetInterpolator.create(path.getSegments().iterator().next(), firstInterpolator.get()); } else { - interpolatorChange = CombinedChange.create(); // Noop change + interpolatorUpdateChange = CombinedChange.create(); // Noop change } } - interpolatorChange.apply(timeline); + + interpolatorUpdateChange.apply(timeline); // and update spectator positions Change spectatorChange = updateSpectatorPositions(); spectatorChange.apply(timeline); - return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, interpolatorChange, spectatorChange); + return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, + interpolatorBeforeChange, interpolatorAfterChange, interpolatorUpdateChange, spectatorChange); } // Helper method because generics cannot be defined on blocks diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 3c85d8d8..e68f9897 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -309,6 +309,7 @@ replaymod.gui.settings.keyframecleancallback=Clear Confirmation replaymod.gui.settings.renderinvisible=Render invisible Entities replaymod.gui.settings.camera=Camera replaymod.gui.settings.showchat=Show Chat +replaymod.gui.settings.interpolator=Default Interpolator replaymod.gui.settings.warning.linea=WARNING: Recording settings will be replaymod.gui.settings.warning.lineb=applied the next time you join a world. @@ -372,6 +373,14 @@ replaymod.gui.editkeyframe.spec.method.shoulder.pitch=Pitch Offset replaymod.gui.editkeyframe.spec.method.shoulder.yaw=Rotation Angle replaymod.gui.editkeyframe.spec.method.shoulder.smoothness=Path Smoothness +replaymod.gui.editkeyframe.interpolator=Interpolator +replaymod.gui.editkeyframe.interpolator.default.name=Default Interpolator +replaymod.gui.editkeyframe.interpolator.default.desc=Uses the default interpolator defined in the Replay Mod Settings. +replaymod.gui.editkeyframe.interpolator.cubic.name=Cubic Spline Interpolator +replaymod.gui.editkeyframe.interpolator.cubic.desc=Calculates a cubic equation matrix for all points for a smooth camera path. +replaymod.gui.editkeyframe.interpolator.linear.name=Linear Interpolator +replaymod.gui.editkeyframe.interpolator.linear.desc=Draws straight lines between the keyframes. + #Render Settings GUI replaymod.gui.rendersettings.title=Rendering Options replaymod.gui.rendersettings.renderer=Rendering Method