From c0f013cfa5cd5786442aa74159be0dc33afbe4f3 Mon Sep 17 00:00:00 2001 From: Yanbing Zhao Date: Sun, 12 Sep 2021 07:21:59 +0800 Subject: [PATCH] Add polynomial conformal mappings between actual and render locations --- .../teacon/signin/client/GuideMapScreen.java | 58 ++++- .../signin/client/PolynomialMapping.java | 235 ++++++++++++++++++ .../teacon/signin/data/GuideMapManager.java | 2 +- .../signup_guides/points/point_1.json | 3 +- 4 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/teacon/signin/client/PolynomialMapping.java diff --git a/src/main/java/org/teacon/signin/client/GuideMapScreen.java b/src/main/java/org/teacon/signin/client/GuideMapScreen.java index 95ebd20..e1f8e68 100644 --- a/src/main/java/org/teacon/signin/client/GuideMapScreen.java +++ b/src/main/java/org/teacon/signin/client/GuideMapScreen.java @@ -18,6 +18,10 @@ import net.minecraft.util.math.vector.Vector3i; import net.minecraft.util.text.ITextComponent; import net.minecraft.util.text.TranslationTextComponent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; import org.teacon.signin.SignMeUp; import org.teacon.signin.data.GuideMap; import org.teacon.signin.data.Trigger; @@ -32,6 +36,9 @@ @ParametersAreNonnullByDefault public final class GuideMapScreen extends Screen { + private static final Logger LOGGER = LogManager.getLogger("SignMeUp"); + private static final Marker MARKER = MarkerManager.getMarker("GuideMapScreen"); + private static final int X_SIZE = 385; private static final int Y_SIZE = 161; @@ -46,7 +53,7 @@ public final class GuideMapScreen extends Screen { private ResourceLocation selectedWaypoint; private int ticksAfterWaypointTextureChanged = 0; - private Deque lastWaypointTextures = Queues.newArrayDeque(); + private final Deque lastWaypointTextures = Queues.newArrayDeque(); private boolean hasWaypointTrigger = false; @@ -54,6 +61,8 @@ public final class GuideMapScreen extends Screen { private final Vector3d playerLocation; private final List waypointIds; + private PolynomialMapping mapping; + private ImageButton leftFlip, rightFlip; private ImageButton mapTriggerPrev, mapTriggerNext; @@ -106,20 +115,47 @@ protected void init() { ++j; } - // Setup Waypoints + // Collect Waypoints this.waypointTriggers.clear(); int mapCanvasX = x0 + 78, mapCanvasY = y0 + 23; + final List waypoints = new ArrayList<>(this.waypointIds.size()); + final List waypointIds = new ArrayList<>(this.waypointIds.size()); for (ResourceLocation wpId : this.waypointIds) { Waypoint wp = SignMeUpClient.MANAGER.findWaypoint(wpId); - if (wp == null) { - continue; + if (wp != null) { + waypoints.add(wp); + waypointIds.add(wpId); } - // For map_icons.png, each icon is 4x4 pixels, so after we get the center coordinate, - // we also need to shift left/up by 2 pixels to center the icon. - final Vector3i center = this.map.center; + } + + // Setup Mapping + final int waypointSize = waypoints.size(); + final double[] inputX = new double[waypointSize], inputY = new double[waypointSize]; + final double[] outputX = new double[waypointSize], outputY = new double[waypointSize]; + final Vector3i center = this.map.center; + for (int i = 0; i < waypointSize; ++i) { + final Waypoint wp = waypoints.get(i); + final Vector3i actualLocation = wp.getActualLocation(); final Vector3i renderLocation = wp.getRenderLocation(); - final int wpX = Math.round(((float) (renderLocation.getX() - center.getX())) / this.map.radius * 64) + 64; - final int wpY = Math.round(((float) (renderLocation.getZ() - center.getZ())) / this.map.radius * 64) + 64; + inputX[i] = actualLocation.getX() - center.getX(); + inputY[i] = actualLocation.getZ() - center.getZ(); + outputX[i] = renderLocation.getX() - center.getX(); + outputY[i] = renderLocation.getZ() - center.getZ(); + } + try { + this.mapping = new PolynomialMapping(inputX, inputY, outputX, outputY); + LOGGER.info(MARKER, "Current mapping for {} waypoint(s): {}", waypointSize, this.mapping); + } catch (IllegalArgumentException e) { + this.mapping = new PolynomialMapping(new double[0], new double[0], new double[0], new double[0]); + LOGGER.warn(MARKER, "Unable to generate mapping for the map.", e); + } + + // Setup Waypoints + for (int i = 0; i < waypointSize; ++i) { + final Waypoint wp = waypoints.get(i); + final ResourceLocation wpId = waypointIds.get(i); + final int wpX = Math.round((float) outputX[i] / this.map.radius * 64) + 64; + final int wpY = Math.round((float) outputY[i] / this.map.radius * 64) + 64; if (wpX >= 1 && wpX <= 127 && wpY >= 1 && wpY <= 127) { // Setup Waypoints as ImageButtons this.addButton(new ImageButton(mapCanvasX + wpX - 2, mapCanvasY + wpY - 2, 4, 4, 58, 2, 0, MAP_ICONS, @@ -133,8 +169,8 @@ protected void init() { }, wp.getTitle())); // Setup trigger buttons from Waypoints List wpTriggerIds = wp.getTriggerIds(); - for (int i = 0, j = 0; i < wpTriggerIds.size() && j < 7; ++i) { - ResourceLocation triggerId = wpTriggerIds.get(i); + for (int j = 0, k = 0; k < wpTriggerIds.size() && j < 7; ++k) { + ResourceLocation triggerId = wpTriggerIds.get(k); final Trigger trigger = SignMeUpClient.MANAGER.findTrigger(triggerId); if (trigger == null) { continue; diff --git a/src/main/java/org/teacon/signin/client/PolynomialMapping.java b/src/main/java/org/teacon/signin/client/PolynomialMapping.java new file mode 100644 index 0000000..702395d --- /dev/null +++ b/src/main/java/org/teacon/signin/client/PolynomialMapping.java @@ -0,0 +1,235 @@ +package org.teacon.signin.client; + +import java.util.*; + +/** + * Polynomial conformal mapping from wn to zn: f(wn) = zn, 0 <= n < N. + *

+ * The interpolation is based on newton polynomial: + *

+ * w = f(z) = [w0] + [w1, w0](z - z0) + [w2, w1, w0](z - z0)(z - z1) + ... +
+ *            [wN, ..., w1, w0](z - z0)(z - z1)...(z - zN-1) + A(z - z0)(z - z1)...(z - zN)
+ * 
+ * Extra condition: the highest factor A should fit: f'(0) = 1 + *

+ */ +public final class PolynomialMapping { + private final List inputParameters; + private final Collection outputDifferences; + + public PolynomialMapping(double[] inputX, double[] inputY, double[] outputX, double[] outputY) { + int degree = this.ensureSize(inputX, inputY, outputX, outputY); + + Complex zero = new Complex(0.0), one = new Complex(1.0); + + LinkedHashSet inputs = new LinkedHashSet<>(degree); + for (int i = 0; i < degree; ++i) { + if (!inputs.add(new Complex(inputX[i], inputY[i]))) { + throw new IllegalArgumentException("Duplicate input for x = " + inputX[i] + " and y = " + inputY[i]); + } + } + + if (degree > 0) { + this.inputParameters = new ArrayList<>(inputs); + } else { + this.inputParameters = new ArrayList<>(); + this.inputParameters.add(zero); + } + + ArrayDeque outputs = new ArrayDeque<>(degree); + for (int i = degree - 1; i >= 0; --i) { + Complex input = this.inputParameters.get(i); // z_i + Complex current = new Complex(outputX[i], outputY[i]); // [w_i] + + for (int j = i + 1; j < degree; ++j) { + // current = [w_{j-1}, w_{j-2}, ..., w_i] + // outputs.remove() = [w_j, w_{j-1}, ..., w_{i+1}] + // result = [w_j, w_{j-1}, ..., w_{i+1}, w_i] = (outputs.remove() - current) / (z_j - z_i) + Complex result = outputs.remove().sub(current).div(this.inputParameters.get(j).sub(input)); + outputs.offer(current); + current = result; + } + + // left = [ w_i, w_{i-1}, ..., w_1, w_0 ] + outputs.offer(current); + } + + if (degree > 0) { + Iterator currentOutput = outputs.iterator(); + currentOutput.next(); // skip the first value since it is not needed for derivative + + Complex currentDerivative = one, resultDerivative = zero; + Complex currentValue = zero.sub(this.inputParameters.get(0)); + + for (int i = 1; i < degree; ++i) { + Complex inputFactor = zero.sub(this.inputParameters.get(i)); + + resultDerivative = resultDerivative.add(currentDerivative.mul(currentOutput.next())); + currentDerivative = currentDerivative.mul(inputFactor).add(currentValue); + + currentValue = currentValue.mul(inputFactor); + } + + if (!zero.equals(currentDerivative)) { + outputs.add(one.sub(resultDerivative).div(currentDerivative)); // decide the highest factor + } else { + // one example: f(-1) = -i, f(1) = i + // then for every A, f(z) = -i + i(z + 1) + A(z + 1)(z - 1) makes f'(0) = i != 1 + // so we should ensure f'(0) = 1 by adding two degrees of freedom to decide the highest factor + // then it becomes: f(z) = -i + i(z + 1) + (-1 + i)z(z + 1)(z - 1) = (-1 + i)z^3 + z + outputs.add(zero); + this.inputParameters.add(zero); + outputs.add(one.sub(resultDerivative).div(currentValue)); + } + + this.outputDifferences = outputs; + } else { + this.outputDifferences = outputs; + this.outputDifferences.add(zero); + this.outputDifferences.add(one); + } + } + + private void interpolate(double[] inputX, double[] inputY, double[] outputX, double[] outputY) { + int size = this.ensureSize(inputX, inputY, outputX, outputY); + double[] dummyInputs = new double[size], dummyOutputs = new double[size]; + interpolate(inputX, inputY, dummyInputs, dummyInputs, outputX, outputY, dummyOutputs, dummyOutputs); + } + + private void interpolate(double[] inputX, double[] inputY, double[] inputDX, double[] inputDY, + double[] outputX, double[] outputY, double[] outputDX, double[] outputDY) { + int size = this.ensureSize(inputX, inputY, outputX, outputY); + int sizeDerivative = this.ensureSize(inputDX, inputDY, outputDX, outputDY); + + if (size != sizeDerivative) { + throw new IllegalArgumentException("Mismatched value/derivative size: " + size + " != " + sizeDerivative); + } + + Complex zero = new Complex(0.0), one = new Complex(1.0); + + int degree = this.inputParameters.size(); + + for (int i = 0; i < size; ++i) { + Complex current = new Complex(inputX[i], inputY[i]); + + Iterator currentOutput = this.outputDifferences.iterator(); + + Complex currentDerivative = one, resultDerivative = zero; + Complex currentValue = current.sub(this.inputParameters.get(0)), resultValue = currentOutput.next(); + + for (int j = 1; j < degree; ++j) { + Complex inputFactor = current.sub(this.inputParameters.get(i)); + + resultDerivative = resultDerivative.add(currentDerivative.mul(currentOutput.next())); + currentDerivative = currentDerivative.mul(inputFactor).add(currentValue); + + resultValue = resultValue.add(currentValue.mul(currentOutput.next())); + currentValue = currentValue.mul(inputFactor); + } + + resultDerivative = resultDerivative.add(currentDerivative.mul(currentOutput.next())); + resultDerivative = new Complex(inputDX[i], inputDY[i]).mul(resultDerivative); + resultValue = resultValue.add(currentValue.mul(currentOutput.next())); + + outputDX[i] = resultDerivative.real; + outputDY[i] = resultDerivative.imag; + outputX[i] = resultValue.real; + outputY[i] = resultValue.imag; + } + } + + private int ensureSize(double[] inputX, double[] inputY, double[] outputX, double[] outputY) { + if (inputX.length != inputY.length) { + throw new IllegalArgumentException("Mismatched input size: " + inputX.length + " != " + inputY.length); + } + if (outputX.length != outputY.length) { + throw new IllegalArgumentException("Mismatched output size: " + outputX.length + " != " + outputY.length); + } + if (inputX.length != outputX.length) { + throw new IllegalArgumentException("Mismatched input/output size: " + inputX.length + " != " + outputX.length); + } + return inputX.length; + } + + @Override + public String toString() { + Complex zero = new Complex(0.0); + + StringBuilder result = new StringBuilder(); + StringBuilder current = new StringBuilder(); + + Iterator currentOutput = this.outputDifferences.iterator(); + + result.append("(").append(currentOutput.next().toString()).append(")"); + current.append("*(#").append(zero.sub(this.inputParameters.get(0)).toSignedString()).append(")"); + + int degree = this.inputParameters.size(); + + for (int i = 1; i < degree; ++i) { + result.append("+(").append(currentOutput.next().toString()).append(")").append(current); + current.append("*(#").append(zero.sub(this.inputParameters.get(i)).toSignedString()).append(")"); + } + + return result.append("+(").append(currentOutput.next().toString()).append(")").append(current).toString(); + } + + private static final class Complex { + public final double real, imag; + + public Complex(double real) { + this.real = real; + this.imag = 0.0; + } + + public Complex(double real, double imag) { + this.real = real; + this.imag = imag; + } + + public Complex add(Complex that) { + double real = this.real + that.real; + double imag = this.imag + that.imag; + return new Complex(real, imag); + } + + public Complex sub(Complex that) { + double real = this.real - that.real; + double imag = this.imag - that.imag; + return new Complex(real, imag); + } + + public Complex mul(Complex that) { + double real = this.real * that.real - this.imag * that.imag; + double imag = this.imag * that.real + this.real * that.imag; + return new Complex(real, imag); + } + + public Complex div(Complex that) { + double factor = 1.0 / (that.real * that.real + that.imag * that.imag); + double real = factor * (this.real * that.real + this.imag * that.imag); + double imag = factor * (this.imag * that.real - this.real * that.imag); + return new Complex(real, imag); + } + + public String toSignedString() { + return String.format("%+.15f%+.15fi", this.real, this.imag); + } + + @Override + public String toString() { + return String.format("%.15f%+.15fi", this.real, this.imag); + } + + @Override + public boolean equals(Object o) { + return this == o || o instanceof Complex + && Double.compare(this.real, ((Complex) o).real) == 0 + && Double.compare(this.imag, ((Complex) o).imag) == 0; + } + + @Override + public int hashCode() { + return Double.hashCode(this.real) ^ Double.hashCode(this.imag); + } + } +} diff --git a/src/main/java/org/teacon/signin/data/GuideMapManager.java b/src/main/java/org/teacon/signin/data/GuideMapManager.java index e0c77f1..a1cee7a 100644 --- a/src/main/java/org/teacon/signin/data/GuideMapManager.java +++ b/src/main/java/org/teacon/signin/data/GuideMapManager.java @@ -37,7 +37,7 @@ public final class GuideMapManager extends JsonReloadListener { - private static final Logger LOGGER = LogManager.getLogger("SignMeIn"); + private static final Logger LOGGER = LogManager.getLogger("SignMeUp"); private static final Marker MARKER = MarkerManager.getMarker("GuideMapManager"); private static final Gson GSON = new GsonBuilder().setLenient() diff --git a/src/test/resources/data/sign_up_test/signup_guides/points/point_1.json b/src/test/resources/data/sign_up_test/signup_guides/points/point_1.json index 5bbd406..1707945 100644 --- a/src/test/resources/data/sign_up_test/signup_guides/points/point_1.json +++ b/src/test/resources/data/sign_up_test/signup_guides/points/point_1.json @@ -3,7 +3,8 @@ "title": "Test Point 1", "selector": "@e[gamemode=creative]", "location": { - "actual": [ 10, 70, 10 ] + "actual": [ 10, 70, 10 ], + "render": [ 0, 70, 0 ] }, "triggers": [ "sign_up_test:triggers/teleport",