diff --git a/.classpath b/.classpath
index dfc7bed..790d0fb 100644
--- a/.classpath
+++ b/.classpath
@@ -1,23 +1,33 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 618cdf0..42ac2a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,11 +28,6 @@
-
org.mapdbmapdb
diff --git a/src/main/java/org/joml/ImmutableVector2i.java b/src/main/java/org/joml/ImmutableVector2i.java
new file mode 100644
index 0000000..653a4b7
--- /dev/null
+++ b/src/main/java/org/joml/ImmutableVector2i.java
@@ -0,0 +1,427 @@
+/* (C) Copyright 2015-2017 Richard Greenlees
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+package org.joml;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+
+/**
+ * Represents a 2D vector with single-precision. The values of this class are set in the constructor and cannot be changed. Operations that modify this vector
+ * either take in a destination vector that will be changed or return a changed immutable copy of this vector with the computation's result.
+ *
+ * @author RGreenlees
+ * @author Kai Burjack
+ * @author Hans Uhlig
+ * @author Julian Fischer
+ */
+public class ImmutableVector2i implements Vector2ic {
+
+ /**
+ * The x component of the vector.
+ */
+ public final int x;
+ /**
+ * The y component of the vector.
+ */
+ public final int y;
+
+ /**
+ * Create a new {@link ImmutableVector2i} and initialize its components to zero.
+ *
+ * @Deprecated Use {@link #ZERO} instead.
+ */
+ @Deprecated
+ public ImmutableVector2i() {
+ this(0);
+ }
+
+ /**
+ * Create a new {@link ImmutableVector2i} and initialize both of its components with the given value.
+ *
+ * @param s the value of both components
+ */
+ public ImmutableVector2i(int s) {
+ this(s, s);
+ }
+
+ /**
+ * Create a new {@link ImmutableVector2i} and initialize its components to the given values.
+ *
+ * @param x the x component
+ * @param y the y component
+ */
+ public ImmutableVector2i(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Create a new {@link ImmutableVector2i} and initialize its components to the one of the given vector.
+ *
+ * @param v the {@link Vector2ic} to copy the values from
+ */
+ public ImmutableVector2i(Vector2ic v) {
+ x = v.x();
+ y = v.y();
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#x() */
+ @Override
+ public int x() {
+ return this.x;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#y() */
+ @Override
+ public int y() {
+ return this.y;
+ }
+
+ /**
+ * Set the value of the specified component of this vector and returns it in a new vector.
+ *
+ * @param component the component whose value to set, within [0..1]
+ * @param value the value to set
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ * @throws IllegalArgumentException if component is not within [0..1]
+ */
+ public ImmutableVector2i setComponent(int component, int value) throws IllegalArgumentException {
+ int x = this.x;
+ int y = this.y;
+ switch (component) {
+ case 0:
+ x = value;
+ break;
+ case 1:
+ y = value;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ return new ImmutableVector2i(x, y);
+ }
+
+ // #ifdef __HAS_NIO__
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#get(java.nio.ByteBuffer) */
+ @Override
+ public ByteBuffer get(ByteBuffer buffer) {
+ return get(buffer.position(), buffer);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#get(int, java.nio.ByteBuffer) */
+ @Override
+ public ByteBuffer get(int index, ByteBuffer buffer) {
+ MemUtil.INSTANCE.put(this.toMutableVector(), index, buffer);
+ return buffer;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#get(java.nio.IntBuffer) */
+ @Override
+ public IntBuffer get(IntBuffer buffer) {
+ return get(buffer.position(), buffer);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#get(int, java.nio.IntBuffer) */
+ @Override
+ public IntBuffer get(int index, IntBuffer buffer) {
+ MemUtil.INSTANCE.put(this.toMutableVector(), index, buffer);
+ return buffer;
+ }
+ // #endif
+
+ public Vector2i toMutableVector() {
+ return new Vector2i(this);
+ }
+
+ /**
+ * Subtract the supplied vector from this one and store the result in a new vector.
+ *
+ * @param v the vector to subtract
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector2i sub(Vector2ic v) {
+ return new ImmutableVector2i(x - v.x(), y - v.y());
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#sub(org.joml.Vector2ic, org.joml.Vector2i) */
+ @Override
+ public Vector2i sub(Vector2ic v, Vector2i dest) {
+ dest.x = x - v.x();
+ dest.y = y - v.y();
+ return dest;
+ }
+
+ /**
+ * Decrement the components of this vector by the given values.
+ *
+ * @param x the x component to subtract
+ * @param y the y component to subtract
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector2i sub(int x, int y) {
+ return new ImmutableVector2i(this.x - x, this.y - y);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#sub(int, int, org.joml.Vector2i) */
+ @Override
+ public Vector2i sub(int x, int y, Vector2i dest) {
+ dest.x = this.x - x;
+ dest.y = this.y - y;
+ return dest;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#lengthSquared() */
+ @Override
+ public long lengthSquared() {
+ return x * x + y * y;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#length() */
+ @Override
+ public double length() {
+ return Math.sqrt(lengthSquared());
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#distance(org.joml.Vector2ic) */
+ @Override
+ public double distance(Vector2ic v) {
+ return Math.sqrt(distanceSquared(v));
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#distance(int, int) */
+ @Override
+ public double distance(int x, int y) {
+ return Math.sqrt(distanceSquared(x, y));
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#distanceSquared(org.joml.Vector2ic) */
+ @Override
+ public long distanceSquared(Vector2ic v) {
+ int dx = this.x - v.x();
+ int dy = this.y - v.y();
+ return dx * dx + dy * dy;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#distanceSquared(int, int) */
+ @Override
+ public long distanceSquared(int x, int y) {
+ int dx = this.x - x;
+ int dy = this.y - y;
+ return dx * dx + dy * dy;
+ }
+
+ /**
+ * Add the supplied vector to this one.
+ *
+ * @param v the vector to add
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector2i add(Vector2ic v) {
+ return add(v.x(), v.y());
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#add(org.joml.Vector2ic, org.joml.Vector2i) */
+ @Override
+ public Vector2i add(Vector2ic v, Vector2i dest) {
+ dest.x = x + v.x();
+ dest.y = y + v.y();
+ return dest;
+ }
+
+ /**
+ * Increment the components of this vector by the given values.
+ *
+ * @param x the x component to add
+ * @param y the y component to add
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector2i add(int x, int y) {
+ return new ImmutableVector2i(this.x + x, this.y + y);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#add(int, int, int, org.joml.Vector2i) */
+ @Override
+ public Vector2i add(int x, int y, Vector2i dest) {
+ dest.x = this.x + x;
+ dest.y = this.y + y;
+ return dest;
+ }
+
+ /**
+ * Multiply all components of this vector by the given scalar value.
+ *
+ * @param scalar the scalar to multiply this vector by
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector2i mul(int scalar) {
+ return new ImmutableVector2i(x * scalar, y * scalar);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#mul(int, org.joml.Vector2i) */
+ @Override
+ public Vector2i mul(int scalar, Vector2i dest) {
+ dest.x = x * scalar;
+ dest.y = y * scalar;
+ return dest;
+ }
+
+ /**
+ * Multiply the componentes of this vector by the values given in v.
+ *
+ * @param v the vector to multiply
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector2i mul(Vector2ic v) {
+ return new ImmutableVector2i(this.x * v.x(), this.y * v.y());
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#mul(org.joml.Vector2ic, org.joml.Vector2i) */
+ @Override
+ public Vector2i mul(Vector2ic v, Vector2i dest) {
+ dest.x = x * v.x();
+ dest.y = y * v.y();
+ return dest;
+ }
+
+ /**
+ * Multiply the components of this vector by the given values.
+ *
+ * @param x the x component to multiply
+ * @param y the y component to multiply
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector2i mul(int x, int y) {
+ return new ImmutableVector2i(this.x * x, this.y * y);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#mul(int, int, int, org.joml.Vector2i) */
+ @Override
+ public Vector2i mul(int x, int y, Vector2i dest) {
+ dest.x = this.x * x;
+ dest.y = this.y * y;
+ return dest;
+ }
+
+ public static final ImmutableVector2i ZERO = new ImmutableVector2i();
+
+ /**
+ * Negate this vector.
+ *
+ * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector2i negate() {
+ return new ImmutableVector2i(-x, -y);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector2ic#negate(org.joml.Vector2i) */
+ @Override
+ public Vector2i negate(Vector2i dest) {
+ dest.x = -x;
+ dest.y = -y;
+ return dest;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + x;
+ result = prime * result + y;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ImmutableVector2i other = (ImmutableVector2i) obj;
+ if (x != other.x) {
+ return false;
+ }
+ if (y != other.y) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return a string representation of this vector.
+ *
+ * This method creates a new {@link DecimalFormat} on every invocation with the format string "0.000E0;-".
+ *
+ * @return the string representation
+ */
+ @Override
+ public String toString() {
+ return Runtime.formatNumbers(toString(Options.NUMBER_FORMAT));
+ }
+
+ /**
+ * Return a string representation of this vector by formatting the vector components with the given {@link NumberFormat}.
+ *
+ * @param formatter the {@link NumberFormat} used to format the vector components with
+ * @return the string representation
+ */
+ public String toString(NumberFormat formatter) {
+ return "(" + formatter.format(x) + " " + formatter.format(y) + ")";
+ }
+
+}
diff --git a/src/main/java/org/joml/ImmutableVector3i.java b/src/main/java/org/joml/ImmutableVector3i.java
new file mode 100644
index 0000000..74f249e
--- /dev/null
+++ b/src/main/java/org/joml/ImmutableVector3i.java
@@ -0,0 +1,475 @@
+/* (C) Copyright 2015-2017 Richard Greenlees
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+package org.joml;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+
+/**
+ * Contains the definition of a Vector comprising 3 ints and associated transformations. The values of this class are set in the constructor and cannot be
+ * changed. Operations that modify this vector either take in a destination vector that will be changed or return a changed immutable copy of this vector with
+ * the computation's result.
+ *
+ * @author Richard Greenlees
+ * @author Kai Burjack
+ * @author Hans Uhlig
+ * @author Julian Fischer
+ */
+public class ImmutableVector3i implements Vector3ic {
+
+ /**
+ * The x component of the vector.
+ */
+ public final int x;
+ /**
+ * The y component of the vector.
+ */
+ public final int y;
+ /**
+ * The z component of the vector.
+ */
+ public final int z;
+
+ /**
+ * Create a new {@link Vector3i} of (0, 0, 0).
+ *
+ * @Deprecated Use {@link #ZERO} instead.
+ */
+ @Deprecated
+ public ImmutableVector3i() {
+ this(0);
+ }
+
+ /**
+ * Create a new {@link Vector3i} and initialize all three components with the given value.
+ *
+ * @param d the value of all three components
+ */
+ public ImmutableVector3i(int d) {
+ this(d, d, d);
+ }
+
+ /**
+ * Create a new {@link Vector3i} with the given component values.
+ *
+ * @param x the value of x
+ * @param y the value of y
+ * @param z the value of z
+ */
+ public ImmutableVector3i(int x, int y, int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ /**
+ * Create a new {@link Vector3i} with the same values as v.
+ *
+ * @param v the {@link Vector3ic} to copy the values from
+ */
+ public ImmutableVector3i(Vector3ic v) {
+ this.x = v.x();
+ this.y = v.y();
+ this.z = v.z();
+ }
+
+ /**
+ * Create a new {@link Vector3i} with the first two components from the given v and the given z
+ *
+ * @param v the {@link Vector2ic} to copy the values from
+ * @param z the z component
+ */
+ public ImmutableVector3i(Vector2ic v, int z) {
+ this.x = v.x();
+ this.y = v.y();
+ this.z = z;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#x() */
+ @Override
+ public int x() {
+ return this.x;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#y() */
+ @Override
+ public int y() {
+ return this.y;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#z() */
+ @Override
+ public int z() {
+ return this.z;
+ }
+
+ /**
+ * Set the value of the specified component of this vector and returns it in a new vector.
+ *
+ * @param component the component whose value to set, within [0..2]
+ * @param value the value to set
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ * @throws IllegalArgumentException if component is not within [0..2]
+ */
+ public ImmutableVector3i setComponent(int component, int value) throws IllegalArgumentException {
+ int x = this.x;
+ int y = this.y;
+ int z = this.z;
+ switch (component) {
+ case 0:
+ x = value;
+ break;
+ case 1:
+ y = value;
+ break;
+ case 2:
+ z = value;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ return new ImmutableVector3i(x, y, z);
+ }
+
+ // #ifdef __HAS_NIO__
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#get(java.nio.IntBuffer) */
+ @Override
+ public IntBuffer get(IntBuffer buffer) {
+ return get(buffer.position(), buffer);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#get(int, java.nio.IntBuffer) */
+ @Override
+ public IntBuffer get(int index, IntBuffer buffer) {
+ MemUtil.INSTANCE.put(this.toMutableVector(), index, buffer);
+ return buffer;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#get(java.nio.ByteBuffer) */
+ @Override
+ public ByteBuffer get(ByteBuffer buffer) {
+ return get(buffer.position(), buffer);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#get(int, java.nio.ByteBuffer) */
+ @Override
+ public ByteBuffer get(int index, ByteBuffer buffer) {
+ MemUtil.INSTANCE.put(this.toMutableVector(), index, buffer);
+ return buffer;
+ }
+ // #endif
+
+ public Vector3i toMutableVector() {
+ return new Vector3i(this);
+ }
+
+ /**
+ * Subtract the supplied vector from this one and store the result in a new vector.
+ *
+ * @param v the vector to subtract
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector3i sub(Vector3ic v) {
+ return new ImmutableVector3i(x - v.x(), y - v.y(), z - v.z());
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#sub(org.joml.Vector3ic, org.joml.Vector3i) */
+ @Override
+ public Vector3i sub(Vector3ic v, Vector3i dest) {
+ dest.x = x - v.x();
+ dest.y = y - v.y();
+ dest.z = z - v.z();
+ return dest;
+ }
+
+ /**
+ * Decrement the components of this vector by the given values.
+ *
+ * @param x the x component to subtract
+ * @param y the y component to subtract
+ * @param z the z component to subtract
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector3i sub(int x, int y, int z) {
+ return new ImmutableVector3i(this.x - x, this.y - y, this.z - z);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#sub(int, int, int, org.joml.Vector3i) */
+ @Override
+ public Vector3i sub(int x, int y, int z, Vector3i dest) {
+ dest.x = this.x - x;
+ dest.y = this.y - y;
+ dest.z = this.z - z;
+ return dest;
+ }
+
+ /**
+ * Add the supplied vector to this one.
+ *
+ * @param v the vector to add
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector3i add(Vector3ic v) {
+ return add(v.x(), v.y(), v.z());
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#add(org.joml.Vector3ic, org.joml.Vector3i) */
+ @Override
+ public Vector3i add(Vector3ic v, Vector3i dest) {
+ dest.x = x + v.x();
+ dest.y = y + v.y();
+ dest.z = z + v.z();
+ return dest;
+ }
+
+ /**
+ * Increment the components of this vector by the given values.
+ *
+ * @param x the x component to add
+ * @param y the y component to add
+ * @param z the z component to add
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector3i add(int x, int y, int z) {
+ return new ImmutableVector3i(this.x + x, this.y + y, this.z + z);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#add(int, int, int, org.joml.Vector3i) */
+ @Override
+ public Vector3i add(int x, int y, int z, Vector3i dest) {
+ dest.x = this.x + x;
+ dest.y = this.y + y;
+ dest.z = this.z + z;
+ return dest;
+ }
+
+ /**
+ * Multiply all components of this vector by the given scalar value.
+ *
+ * @param scalar the scalar to multiply this vector by
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector3i mul(int scalar) {
+ return new ImmutableVector3i(x * scalar, y * scalar, z * scalar);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#mul(int, org.joml.Vector3i) */
+ @Override
+ public Vector3i mul(int scalar, Vector3i dest) {
+ dest.x = x * scalar;
+ dest.y = y * scalar;
+ dest.y = z * scalar;
+ return dest;
+ }
+
+ /**
+ * Multiply the componentes of this vector by the values given in v.
+ *
+ * @param v the vector to multiply
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector3i mul(Vector3ic v) {
+ return new ImmutableVector3i(this.x * v.x(), this.y * v.y(), this.z * v.z());
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#mul(org.joml.Vector3ic, org.joml.Vector3i) */
+ @Override
+ public Vector3i mul(Vector3ic v, Vector3i dest) {
+ dest.x = x * v.x();
+ dest.y = y * v.y();
+ dest.z = z * v.z();
+ return dest;
+ }
+
+ /**
+ * Multiply the components of this vector by the given values.
+ *
+ * @param x the x component to multiply
+ * @param y the y component to multiply
+ * @param z the z component to multiply
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector3i mul(int x, int y, int z) {
+ return new ImmutableVector3i(this.x * x, this.y * y, this.z * z);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#mul(int, int, int, org.joml.Vector3i) */
+ @Override
+ public Vector3i mul(int x, int y, int z, Vector3i dest) {
+ dest.x = this.x * x;
+ dest.y = this.y * y;
+ dest.z = this.z * z;
+ return dest;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#lengthSquared() */
+ @Override
+ public long lengthSquared() {
+ return x * x + y * y + z * z;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#length() */
+ @Override
+ public double length() {
+ return Math.sqrt(lengthSquared());
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#distance(org.joml.Vector3ic) */
+ @Override
+ public double distance(Vector3ic v) {
+ return Math.sqrt(distanceSquared(v));
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#distance(int, int, int) */
+ @Override
+ public double distance(int x, int y, int z) {
+ return Math.sqrt(distanceSquared(x, y, z));
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#distanceSquared(org.joml.Vector3ic) */
+ @Override
+ public long distanceSquared(Vector3ic v) {
+ int dx = this.x - v.x();
+ int dy = this.y - v.y();
+ int dz = this.z - v.z();
+ return dx * dx + dy * dy + dz * dz;
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#distanceSquared(int, int, int) */
+ @Override
+ public long distanceSquared(int x, int y, int z) {
+ int dx = this.x - x;
+ int dy = this.y - y;
+ int dz = this.z - z;
+ return dx * dx + dy * dy + dz * dz;
+ }
+
+ public static final ImmutableVector3i ZERO = new ImmutableVector3i();
+
+ /**
+ * Return a string representation of this vector.
+ *
+ * This method creates a new {@link DecimalFormat} on every invocation with the format string "0.000E0;-".
+ *
+ * @return the string representation
+ */
+ @Override
+ public String toString() {
+ return Runtime.formatNumbers(toString(Options.NUMBER_FORMAT));
+ }
+
+ /**
+ * Return a string representation of this vector by formatting the vector components with the given {@link NumberFormat}.
+ *
+ * @param formatter the {@link NumberFormat} used to format the vector components with
+ * @return the string representation
+ */
+ public String toString(NumberFormat formatter) {
+ return "(" + formatter.format(x) + " " + formatter.format(y) + " " + formatter.format(z) + ")";
+ }
+
+ /**
+ * Negate this vector.
+ *
+ * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation
+ */
+ public ImmutableVector3i negate() {
+ return new ImmutableVector3i(-x, -y, -z);
+ }
+
+ /* (non-Javadoc)
+ *
+ * @see org.joml.Vector3ic#negate(org.joml.Vector3i) */
+ @Override
+ public Vector3i negate(Vector3i dest) {
+ dest.x = -x;
+ dest.y = -y;
+ dest.z = -z;
+ return dest;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + x;
+ result = prime * result + y;
+ result = prime * result + z;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Vector3i other = (Vector3i) obj;
+ if (x != other.x) {
+ return false;
+ }
+ if (y != other.y) {
+ return false;
+ }
+ if (z != other.z) {
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/DotMinecraft.java b/src/main/java/togos/minecraft/maprend/DotMinecraft.java
new file mode 100644
index 0000000..a698e99
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/DotMinecraft.java
@@ -0,0 +1,27 @@
+package togos.minecraft.maprend;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public final class DotMinecraft {
+
+ private DotMinecraft() {
+ }
+
+ public static final Path DOTMINECRAFT;
+
+ static {
+ Path mc = null;
+ String OS = System.getProperty("os.name").toUpperCase();
+ if (OS.contains("WIN")) {
+ mc = Paths.get(System.getenv("APPDATA")).resolve(".minecraft");
+ } else if (OS.contains("MAC")) {
+ mc = Paths.get(System.getProperty("user.home") + "/Library/Application Support").resolve("minecraft");
+ } else if (OS.contains("NUX")) {
+ mc = Paths.get("~/.minecraft");
+ } else {
+ mc = Paths.get(System.getProperty("user.dir"));
+ }
+ DOTMINECRAFT = mc;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/RegionMap.java b/src/main/java/togos/minecraft/maprend/RegionMap.java
index 196b53e..e34ed85 100644
--- a/src/main/java/togos/minecraft/maprend/RegionMap.java
+++ b/src/main/java/togos/minecraft/maprend/RegionMap.java
@@ -8,7 +8,8 @@
public class RegionMap
{
- static class Region {
+
+ public static class Region {
public int rx, rz;
public File regionFile;
public File imageFile;
diff --git a/src/main/java/togos/minecraft/maprend/RegionRenderer.java b/src/main/java/togos/minecraft/maprend/RegionRenderer.java
index 34425a1..1595957 100644
--- a/src/main/java/togos/minecraft/maprend/RegionRenderer.java
+++ b/src/main/java/togos/minecraft/maprend/RegionRenderer.java
@@ -3,28 +3,10 @@
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
-import java.io.DataInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
+import java.io.*;
+import java.util.*;
import javax.imageio.ImageIO;
-
-import org.jnbt.ByteArrayTag;
-import org.jnbt.ByteTag;
-import org.jnbt.CompoundTag;
-import org.jnbt.ListTag;
-import org.jnbt.NBTInputStream;
-import org.jnbt.Tag;
-
+import org.jnbt.*;
import togos.minecraft.maprend.BiomeMap.Biome;
import togos.minecraft.maprend.BlockMap.Block;
import togos.minecraft.maprend.RegionMap.Region;
@@ -61,6 +43,7 @@ class RenderThread extends Thread {
this.force = force;
}
+ @Override
public void run() {
try {
for( Region reg : regions ) renderRegion(reg, outputDir, force);
@@ -74,47 +57,25 @@ public void run() {
public final Set defaultedBlockIds = new HashSet();
public final Set defaultedBlockIdDataValues = new HashSet();
public final Set defaultedBiomeIds = new HashSet();
- public final boolean debug;
public final BlockMap blockMap;
public final BiomeMap biomeMap;
public final int air16Color; // Color of 16 air blocks stacked
- public final int minHeight;
- public final int maxHeight;
- public final int shadingReferenceAltitude;
- public final int altitudeShadingFactor;
- public final int minAltitudeShading;
- public final int maxAltitudeShading;
- public final String mapTitle;
- public final int[] mapScales;
/**
* Alpha below which blocks are considered transparent for purposes of shading
* (i.e. blocks with alpha < this will not be shaded, but blocks below them will be)
*/
private int shadeOpacityCutoff = 0x20;
-
- public RegionRenderer(
- BlockMap blockMap, BiomeMap biomeMap, boolean debug, int minHeight, int maxHeight,
- int shadingRefAlt, int minAltShading, int maxAltShading, int altShadingFactor,
- String mapTitle, int[] mapScales
- ) {
- assert blockMap != null;
- assert biomeMap != null;
-
- this.blockMap = blockMap;
- this.biomeMap = biomeMap;
- this.air16Color = Color.overlay( 0, getColor(0, 0, 0), 16 );
- this.debug = debug;
-
- this.minHeight = minHeight;
- this.maxHeight = maxHeight;
- this.shadingReferenceAltitude = shadingRefAlt;
- this.minAltitudeShading = minAltShading;
- this.maxAltitudeShading = maxAltShading;
- this.altitudeShadingFactor = altShadingFactor;
-
- this.mapTitle = mapTitle;
- this.mapScales = mapScales;
+
+ public final RenderSettings settings;
+
+ public RegionRenderer(RenderSettings settings) throws IOException {
+ this.settings = settings;
+
+ blockMap = settings.colorMapFile == null ? BlockMap.loadDefault() : BlockMap.load(settings.colorMapFile);
+ biomeMap = settings.biomeMapFile == null ? BiomeMap.loadDefault() : BiomeMap.load(settings.biomeMapFile);
+
+ this.air16Color = Color.overlay(0, getColor(0, 0, 0), 16);
}
/**
@@ -249,9 +210,11 @@ protected void shade( short[] height, int[] color ) {
if( shade > 10 ) shade = 10;
if( shade < -10 ) shade = -10;
- int altShade = altitudeShadingFactor * (height[idx] - shadingReferenceAltitude) / 255;
- if( altShade < minAltitudeShading ) altShade = minAltitudeShading;
- if( altShade > maxAltitudeShading ) altShade = maxAltitudeShading;
+ int altShade = settings.altitudeShadingFactor * (height[idx] - settings.shadingReferenceAltitude) / 255;
+ if (altShade < settings.minAltitudeShading)
+ altShade = settings.minAltitudeShading;
+ if (altShade > settings.maxAltitudeShading)
+ altShade = settings.maxAltitudeShading;
shade += altShade;
@@ -309,15 +272,18 @@ protected void preRender( RegionFile rf, int[] colors, short[] heights ) {
for( int s=0; s= maxHeight ) continue;
- if( absY+16 <= minHeight ) continue;
+ if (absY >= settings.maxHeight)
+ continue;
+ if (absY + 16 <= settings.minHeight)
+ continue;
if( usedSections[s] ) {
short[] blockIds = sectionBlockIds[s];
byte[] blockData = sectionBlockData[s];
for( int idx=z*16+x, y=0; y<16; ++y, idx+=256, ++absY ) {
- if( absY < minHeight || absY >= maxHeight ) continue;
+ if (absY < settings.minHeight || absY >= settings.maxHeight)
+ continue;
final short blockId = blockIds[idx];
final byte blockDatum = blockData[idx];
@@ -328,7 +294,7 @@ protected void preRender( RegionFile rf, int[] colors, short[] heights ) {
}
}
} else {
- if( minHeight <= absY && maxHeight >= absY+16 ) {
+ if (settings.minHeight <= absY && settings.maxHeight >= absY + 16) {
// Optimize the 16-blocks-of-air case:
pixelColor = Color.overlay( pixelColor, air16Color );
} else {
@@ -412,7 +378,8 @@ public void renderAll( RegionMap rm, File outputDir, boolean force, int threadCo
renderThreads.add(renderThread);
}
- if( debug ) System.err.println("Using "+renderThreads.size()+" render threads");
+ if (settings.debug)
+ System.err.println("Using " + renderThreads.size() + " render threads");
for( RenderThread renderThread : renderThreads ) renderThread.start();
@@ -424,7 +391,8 @@ public void renderAll( RegionMap rm, File outputDir, boolean force, int threadCo
public void renderRegion( Region r, File outputDir, boolean force ) throws IOException {
if( r == null ) return;
- if( debug ) System.err.print("Region "+pad(r.rx, 4)+", "+pad(r.rz, 4)+"...");
+ if (settings.debug)
+ System.err.print("Region " + pad(r.rx, 4) + ", " + pad(r.rz, 4) + "...");
String imageFilename = "tile."+r.rx+"."+r.rz+".png";
File fullSizeImageFile = r.imageFile = new File( outputDir, imageFilename );
@@ -433,11 +401,12 @@ public void renderRegion( Region r, File outputDir, boolean force ) throws IOExc
if( force || !fullSizeImageFile.exists() || fullSizeImageFile.lastModified() < r.regionFile.lastModified() ) {
fullSizeNeedsReRender = true;
} else {
- if( debug ) System.err.println("image already up-to-date");
+ if (settings.debug)
+ System.err.println("image already up-to-date");
}
boolean anyScalesNeedReRender = false;
- for( int scale : mapScales ) {
+ for (int scale : settings.mapScales) {
if( scale == 1 ) continue;
File f = new File( outputDir, "tile."+r.rx+"."+r.rz+".1-"+scale+".png" );
if( force || !f.exists() || f.lastModified() < r.regionFile.lastModified() ) {
@@ -448,7 +417,8 @@ public void renderRegion( Region r, File outputDir, boolean force ) throws IOExc
BufferedImage fullSize;
if( fullSizeNeedsReRender ) {
fullSizeImageFile.delete();
- if( debug ) System.err.println("generating "+imageFilename+"...");
+ if (settings.debug)
+ System.err.println("generating " + imageFilename + "...");
RegionFile rf = new RegionFile( r.regionFile );
try {
@@ -472,10 +442,11 @@ public void renderRegion( Region r, File outputDir, boolean force ) throws IOExc
return;
}
- for( int scale : mapScales ) {
+ for (int scale : settings.mapScales) {
if( scale == 1 ) continue; // Already wrote!
File f = new File( outputDir, "tile."+r.rx+"."+r.rz+".1-"+scale+".png" );
- if( debug ) System.err.println("generating "+f+"...");
+ if (settings.debug)
+ System.err.println("generating " + f + "...");
int size = 512 / scale;
BufferedImage rescaled = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = rescaled.createGraphics();
@@ -492,8 +463,9 @@ public void renderRegion( Region r, File outputDir, boolean force ) throws IOExc
* within the given bounds (inclusive)
*/
public void createTileHtml( int minX, int minZ, int maxX, int maxZ, File outputDir ) {
- if( debug ) System.err.println("Writing HTML tiles...");
- for( int scale : mapScales ) {
+ if (settings.debug)
+ System.err.println("Writing HTML tiles...");
+ for (int scale : settings.mapScales) {
int regionSize = 512 / scale;
try {
@@ -522,7 +494,7 @@ public void createTileHtml( int minX, int minZ, int maxX, int maxZ, File outputD
)));
try {
w.write("\n");
- w.write(""+mapTitle+" - 1:"+scale+"\n");
+ w.write("" + settings.mapTitle + " - 1:" + scale + "\n");
w.write("\n");
w.write("\n");
w.write("
");
@@ -554,11 +526,11 @@ public void createTileHtml( int minX, int minZ, int maxX, int maxZ, File outputD
}
w.write("
");
} else {
@@ -583,15 +555,17 @@ public void createTileHtml( int minX, int minZ, int maxX, int maxZ, File outputD
}
public void createImageTree( RegionMap rm ) {
- if( debug ) System.err.println("Composing image tree...");
+ if (settings.debug)
+ System.err.println("Composing image tree...");
ImageTreeComposer itc = new ImageTreeComposer(new ContentStore());
System.out.println( itc.compose( rm ) );
}
public void createBigImage( RegionMap rm, File outputDir) {
- if( debug ) System.err.println( "Creating big image..." );
+ if (settings.debug)
+ System.err.println("Creating big image...");
BigImageMerger bic = new BigImageMerger();
- bic.createBigImage( rm, outputDir, debug );
+ bic.createBigImage(rm, outputDir, settings.debug);
}
public static final String USAGE =
@@ -762,18 +736,12 @@ public int run() throws IOException, InterruptedException {
return 0;
}
- final BlockMap colorMap = colorMapFile == null ? BlockMap.loadDefault() :
- BlockMap.load(colorMapFile);
-
- final BiomeMap biomeMap = biomeMapFile == null ? BiomeMap.loadDefault() :
- BiomeMap.load( biomeMapFile );
-
RegionMap rm = RegionMap.load(regionFiles, regionLimitRect);
- RegionRenderer rr = new RegionRenderer(
- colorMap, biomeMap, debug, minHeight, maxHeight,
+ RegionRenderer rr = new RegionRenderer(new RenderSettings(
+ colorMapFile, biomeMapFile, debug, minHeight, maxHeight,
shadingReferenceAltitude, minAltitudeShading, maxAltitudeShading, altitudeShadingFactor,
mapTitle, mapScales
- );
+ ));
rr.renderAll(rm, outputDir, forceReRender, threadCount);
diff --git a/src/main/java/togos/minecraft/maprend/RenderSettings.java b/src/main/java/togos/minecraft/maprend/RenderSettings.java
new file mode 100644
index 0000000..d791481
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/RenderSettings.java
@@ -0,0 +1,48 @@
+package togos.minecraft.maprend;
+
+import java.io.File;
+
+public class RenderSettings {
+
+ public boolean debug = false;
+ public File colorMapFile = null;
+ public File biomeMapFile = null;
+ public int minHeight = Integer.MIN_VALUE;
+ public int maxHeight = Integer.MAX_VALUE;
+
+ /**
+ * Above = brighter, below = darker TODO affect grass color in a more native way
+ */
+ public int shadingReferenceAltitude = 64;
+ /** Maximum brightness difference through shading */
+ public int altitudeShadingFactor = 50;
+
+ public int minAltitudeShading = -20;
+ public int maxAltitudeShading = +20;
+
+ public String mapTitle = "Regions";
+ public int[] mapScales = { 1 };
+
+ public RenderSettings() {
+ }
+
+ public RenderSettings(
+ File colorMapFile, File biomeMapFile, boolean debug, int minHeight, int maxHeight,
+ int shadingRefAlt, int minAltShading, int maxAltShading, int altShadingFactor,
+ String mapTitle, int[] mapScales) {
+
+ this.colorMapFile = colorMapFile;
+ this.biomeMapFile = biomeMapFile;
+ this.debug = debug;
+
+ this.minHeight = minHeight;
+ this.maxHeight = maxHeight;
+ this.shadingReferenceAltitude = shadingRefAlt;
+ this.minAltitudeShading = minAltShading;
+ this.maxAltitudeShading = maxAltShading;
+ this.altitudeShadingFactor = altShadingFactor;
+
+ this.mapTitle = mapTitle;
+ this.mapScales = mapScales;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/CanvasContainer.java b/src/main/java/togos/minecraft/maprend/gui/CanvasContainer.java
new file mode 100644
index 0000000..5b9b856
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/CanvasContainer.java
@@ -0,0 +1,27 @@
+package togos.minecraft.maprend.gui;
+
+import javafx.scene.canvas.Canvas;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.BorderPane;
+
+/**
+ *
+ * Wraps a {@link Canvas} into an AnchorPane and a BorderPane. This is because a Canvas needs to have a preferred size set to properly work in many layouts.
+ * This wrapper will make sure that the canvas will always use exactly as much of the space available - no less and no more. It is recommended to use this class
+ * in most layout situations when working with canvases.
+ *
+ */
+public class CanvasContainer extends AnchorPane {
+
+ public CanvasContainer(Canvas canvas) {
+ BorderPane wrapper = new BorderPane(canvas);
+ wrapper.setPickOnBounds(true);
+ canvas.widthProperty().bind(wrapper.widthProperty());
+ canvas.heightProperty().bind(wrapper.heightProperty());
+ AnchorPane.setTopAnchor(wrapper, 0D);
+ AnchorPane.setBottomAnchor(wrapper, 0D);
+ AnchorPane.setLeftAnchor(wrapper, 0D);
+ AnchorPane.setRightAnchor(wrapper, 0D);
+ getChildren().add(wrapper);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/CanvasHelper.java b/src/main/java/togos/minecraft/maprend/gui/CanvasHelper.java
new file mode 100644
index 0000000..1ca10d7
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/CanvasHelper.java
@@ -0,0 +1,31 @@
+package togos.minecraft.maprend.gui;
+
+import javafx.application.Platform;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+
+public abstract class CanvasHelper extends CanvasContainer {
+
+ protected Canvas canvas;
+ protected GraphicsContext gc;
+
+ public CanvasHelper() {
+ this(null);
+ }
+
+ /* A little hack to pass arguments in super() and also store them as field. Java is weird sometimes */
+ private CanvasHelper(Canvas canvas) {
+ super(canvas = new Canvas());
+ this.canvas = canvas;
+ gc = canvas.getGraphicsContext2D();
+ }
+
+ public final void repaint() {
+ if (Platform.isFxApplicationThread())
+ render();
+ else
+ Platform.runLater(this::render);
+ }
+
+ protected abstract void render();
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java b/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java
new file mode 100644
index 0000000..645ae65
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java
@@ -0,0 +1,18 @@
+package togos.minecraft.maprend.gui;
+
+import org.joml.AABBd;
+
+public class DisplaySettings {
+ // public int threadCount;
+ // public int overDraw;
+ // public int diskCacheSize;
+ // public int memoryCacheSize;
+ // public int maxZoomInLevel;
+ // public int maxZoomOutLevel;
+ // public float loadingFrustum;
+ public AABBd viewRestrictionInner;
+ public AABBd viewRestrictionOuter;
+
+ public DisplaySettings() {
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/DisplayViewport.java b/src/main/java/togos/minecraft/maprend/gui/DisplayViewport.java
new file mode 100644
index 0000000..8603cf5
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/DisplayViewport.java
@@ -0,0 +1,148 @@
+package togos.minecraft.maprend.gui;
+
+import org.joml.AABBd;
+import org.joml.Vector2d;
+import org.joml.Vector2dc;
+import org.joml.Vector3d;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.*;
+import javafx.util.Duration;
+
+
+/**
+ * An object of this class represents the camera viewport of a {@link WorldRendererCanvas}. All units are in world coordinates by default, meaning one unit is
+ * equal to one block.
+ * Warning: The properties used here may use mutable objects. It is strongly discouraged to modify their inner state; treat them as immutable. The system
+ * does not check whether the values are mutable or not. Note that checking is important when reading as well as when writing the respecting property. Modifying
+ * objects will result in wrong calculations of the viewport and may lead to undefined behaviour on third-party components using this.
+ */
+public class DisplayViewport {
+
+ // up to 9 without rounding errors
+ public static final int MAX_ZOOM_LEVEL = 7;
+
+ /** The size of the viewport is needed to calculate the frustum's size in world coordinates. */
+ public final DoubleProperty widthProperty = new SimpleDoubleProperty(800);
+ /** The size of the viewport is needed to calculate the frustum's size in world coordinates. */
+ public final DoubleProperty heightProperty = new SimpleDoubleProperty(500);
+
+ /**
+ * A linear scaling factor to zoom in or out on the map. Larger values mean zoom in, smaller values mean zoom out. Avoid changing this property directly
+ * because it leads to inconsistency between scale and zoom. Use {@link #zoomProperty} instead.
+ * A change in this value will scale the map to the new scaling value accordingly. The center of the scaling operation will be the mouse's current position.
+ */
+ public final DoubleProperty scaleProperty = new SimpleDoubleProperty(1);
+ /**
+ * The zoom is unlike the scale not linear, but on an exponential basis. It is defined as scale=Math.pow(2, zoom). A zoom of zero means a scale
+ * of 1:1, each unit increase means zooming out by a factor of 2, a each unit decrease results in zooming in by a factor of 2.
+ */
+ public final DoubleProperty zoomProperty = new SimpleDoubleProperty(0);
+
+ /** The current mouse position in local coordinates. Used for calculations when zooming around that point. */
+ public final ObjectProperty mousePosProperty = new SimpleObjectProperty<>(new Vector2d());
+ private final ReadOnlyObjectWrapper mouseWorld = new ReadOnlyObjectWrapper<>(new Vector2d());
+ /** The current mouse position in world coordinates (1 unit == 1 block). */
+ public final ReadOnlyObjectProperty mouseWorldProperty = mouseWorld.getReadOnlyProperty();
+ /** The translation of the map relative to its origin, in blocks/pixels. */
+ public final ObjectProperty translationProperty = new SimpleObjectProperty<>(new Vector2d());
+ private final ReadOnlyObjectWrapper frustum = new ReadOnlyObjectWrapper<>(new AABBd());
+ /**
+ * The camera frustum of this viewport. It represents the area of the world visible to the current camera. Translating, zooming or resizing the map result
+ * in an update of this object.
+ */
+ public final ReadOnlyObjectProperty frustumProperty = frustum.getReadOnlyProperty();
+ protected Timeline zoomTimeline;
+
+ public DisplayViewport() {
+ scaleProperty.bind(Bindings.createDoubleBinding(() -> Math.pow(2, zoomProperty.get()), zoomProperty));
+ scaleProperty.addListener((e, oldScale, newScale) -> {
+ // In world coordinates
+ Vector2d cursorPos = new Vector2d(mousePosProperty.get()).mul(1d / oldScale.doubleValue()).sub(translationProperty.get());
+ Vector2d translation = new Vector2d(this.translationProperty.get());
+ translation.add(cursorPos);
+ translation.mul(oldScale.doubleValue() / newScale.doubleValue());
+ translation.sub(cursorPos);
+ this.translationProperty.set(translation);
+ });
+
+ // Use bindings because they are executed lazily
+ frustum.bind(Bindings.createObjectBinding(() -> {
+ Vector2dc translation = this.translationProperty.get();
+ return new AABBd(// TODO optimise
+ new Vector3d(-translation.x(), -translation.y(), 0),
+ new Vector3d(widthProperty.get(), heightProperty.get(), 0).div(scaleProperty.get()).sub(translation.x(), translation.y(), 0));
+ }, translationProperty, widthProperty, heightProperty));
+ mouseWorld.bind(Bindings.createObjectBinding(() -> mousePosProperty.get().mul(1d / scaleProperty.get(), new Vector2d()).sub(translationProperty.get()), mousePosProperty, scaleProperty));
+ }
+
+ /**
+ * Offsets the view of the map by the given amount of pixels in the respective direction.
+ *
+ * @param dx how many pixels the map should move horizontally
+ * @param dy how many pixels the map should move vertically
+ */
+ public void pan(double dx, double dy) {
+ double scale = scaleProperty.get();
+ translationProperty.set(new Vector2d(dx / scale, dy / scale).add(translationProperty.get()));
+ }
+
+ /**
+ * Zooms in or out around the position currently shown by the mouse cursor. The zooming will be animated with an eased interpolation for 100 milliseconds.
+ * The resulting scale is clamped to [-MAX_ZOOM_LEVEL, MAX_ZOOM_LEVEL]. If called while the animation from a previous call has not finished,
+ * that animation is stopped in its current state and both are combined to a new animation that is slightly faster than executing both animations
+ * sequentially.
+ *
+ * @param deltaZoom The amount of change to the zoom property. -1 means zoom out by a factor of 2, 1 means zoom in by a factor of 2.
+ * @see #zoomProperty
+ */
+ public void mouseScroll(double deltaZoom) {
+ double currentValue = zoomProperty.get();
+ double missingTime = 0;
+ if (zoomTimeline != null) {
+ // Divide by 2 to fasten things up
+ missingTime = (zoomTimeline.getTotalDuration().toMillis() - zoomTimeline.getCurrentTime().toMillis()) / 2;
+ zoomTimeline.jumpTo("end");
+ }
+ double scale = zoomProperty.get();
+ zoomProperty.set(currentValue);
+ scale += deltaZoom;
+ // Clamp
+ if (scale > MAX_ZOOM_LEVEL)
+ scale = MAX_ZOOM_LEVEL;
+ if (scale < -MAX_ZOOM_LEVEL)
+ scale = -MAX_ZOOM_LEVEL;
+ zoomTimeline = new Timeline(new KeyFrame(Duration.millis(100 + missingTime), new KeyValue(zoomProperty, scale, Interpolator.EASE_BOTH)));
+ zoomTimeline.play();
+ }
+
+ /**
+ * Returns the currently visible part of the world. The coordinates are world coordinates, relative to the world's origin with one unit corresponding to one
+ * block. The vertical range of this AABBd (the z axis) is from zero to zero. This object is not immutable and should not be changed.
+ */
+ public AABBd getFrustum() {
+ return frustum.get();
+ }
+
+ /**
+ * The zoom level is a rounded version of {@link #zoomProperty}. Use this to control the level of detail used for MIP-mapping.
+ */
+ public int getZoomLevel() {
+ return (int) Math.ceil(zoomProperty.get());
+ }
+
+ public Vector2dc getMousePos() {
+ return mousePosProperty.get();
+ }
+
+ public Vector2dc getMouseInWorld() {
+ return mouseWorldProperty.get();
+ }
+
+ public Vector2dc getTranslation() {
+ return translationProperty.get();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/MapPane.java b/src/main/java/togos/minecraft/maprend/gui/MapPane.java
new file mode 100644
index 0000000..5377ad0
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/MapPane.java
@@ -0,0 +1,44 @@
+package togos.minecraft.maprend.gui;
+
+import java.util.LinkedList;
+import java.util.Objects;
+import com.sun.javafx.collections.ObservableListWrapper;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.EventType;
+import javafx.scene.Node;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+
+/***/
+@SuppressWarnings("restriction")
+public class MapPane extends StackPane {
+
+ /***/
+ public final ObservableList settingsLayers = new ObservableListWrapper(new LinkedList<>());
+ /***/
+ public final ObservableList decorationLayers = new ObservableListWrapper(new LinkedList<>());
+
+ protected Region catchEvents;
+
+ public final WorldRendererCanvas renderer;
+
+ public MapPane(WorldRendererCanvas renderer) {
+ this.renderer = Objects.requireNonNull(renderer);
+ catchEvents = new Region();
+ ListChangeListener l = c -> {
+ getChildren().clear();
+ getChildren().addAll(decorationLayers);
+ getChildren().add(catchEvents);
+ getChildren().addAll(settingsLayers);
+ // System.out.println(Arrays.toString(getChildren().toArray()));
+ };
+ settingsLayers.addListener(l);
+ decorationLayers.addListener(l);
+
+ catchEvents.setPickOnBounds(true);
+ catchEvents.addEventFilter(EventType.ROOT, event -> decorationLayers.forEach(l1 -> l1.fireEvent(event)));
+
+ decorationLayers.add(new CanvasContainer(renderer));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedImage.java b/src/main/java/togos/minecraft/maprend/gui/RenderedImage.java
new file mode 100644
index 0000000..6521239
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/RenderedImage.java
@@ -0,0 +1,49 @@
+package togos.minecraft.maprend.gui;
+
+import java.util.Objects;
+import org.joml.Vector3ic;
+import org.mapdb.HTreeMap;
+import javafx.scene.image.WritableImage;
+
+public class RenderedImage {
+
+ // private WritableImage image;
+ private final RenderedMap map;
+ private final Vector3ic key;
+ private final HTreeMap cache;
+ private volatile boolean hasValue = false;
+
+ public RenderedImage(RenderedMap map, HTreeMap cache, Vector3ic key) {
+ this.map = Objects.requireNonNull(map);
+ this.cache = Objects.requireNonNull(cache);
+ this.key = Objects.requireNonNull(key);
+ // System.out.println(cache + " " + key);
+ }
+
+ public void setImage(WritableImage image) {
+ // this.image = image;
+ // System.out.println(cache + " " + key + " " + image);
+ if (image == null)
+ cache.remove(key);
+ else
+ cache.put(key, image);
+ hasValue = image != null;
+ }
+
+ /** Returns if the last {@link #setImage(WritableImage)} call was with a non-null value. */
+ public boolean isImageSet() {
+ return hasValue;
+ }
+
+ /**
+ * Returns true if the image is loaded in RAM, causing {@link #getImage(boolean)} to return immediately. Returns false if the image got written to disk to
+ * save memory.
+ */
+ public boolean isImageLoaded() {
+ return map.isImageLoaded(key);// cache.containsKey(key);
+ }
+
+ public WritableImage getImage(boolean force) {
+ return (force || isImageLoaded()) ? cache.get(key) : null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java b/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java
new file mode 100644
index 0000000..a8fb128
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java
@@ -0,0 +1,383 @@
+package togos.minecraft.maprend.gui;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import org.joml.*;
+import org.mapdb.*;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.PixelReader;
+import javafx.scene.image.PixelWriter;
+import javafx.scene.image.WritableImage;
+import javafx.scene.paint.Color;
+import togos.minecraft.maprend.RegionMap.Region;
+
+public class RenderedMap {
+
+ /** https://github.com/jankotek/mapdb/issues/839 */
+ protected Set unloaded = ConcurrentHashMap.newKeySet();
+ public final Serializer VECTOR_SERIALIZER = new Serializer() {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void serialize(DataOutput2 out, Vector3ic value) throws IOException {
+ unloaded.add(value);
+ Serializer.JAVA.serialize(out, value);
+ }
+
+ @Override
+ public Vector3ic deserialize(DataInput2 input, int available) throws IOException {
+ Vector3ic value = (Vector3ic) Serializer.JAVA.deserialize(input, available);
+ unloaded.remove(value);
+ return value;
+ }
+
+ };
+
+ /**
+ * Thank the JavaFX guys who a) Made WriteableImage not Serializable b) Didn't include any serialization except by converting to BufferedImage for this ugly
+ * mess.
+ */
+ public final Serializer IMAGE_SERIALIZER = new Serializer() {
+
+ @Override
+ public boolean isTrusted() {
+ return true;
+ }
+
+ @Override
+ public void serialize(DataOutput2 out, WritableImage value) throws IOException {
+ byte[] data = new byte[512 * 512 * 4];
+ value.getPixelReader().getPixels(0, 0, 512, 512,
+ PixelFormat.getByteBgraInstance(), data, 0, 512 * 4);
+ out.write(data);
+ }
+
+ @Override
+ public WritableImage deserialize(DataInput2 input, int available) throws IOException {
+ byte[] data = new byte[available];
+ input.readFully(data);
+ WritableImage ret = new WritableImage(512, 512);
+ ret.getPixelWriter().setPixels(0, 0, 512, 512,
+ PixelFormat.getByteBgraInstance(), data, 0, 512 * 4);
+ return ret;
+ }
+ };
+
+ // Disk for overflow
+ private static final DB cacheDBDisk = DBMaker.tempFileDB().fileDeleteAfterClose().closeOnJvmShutdown().make();
+ // Fast memory cache
+ private static final DB cacheDBMem = DBMaker.heapDB().closeOnJvmShutdown().make();
+
+ private final HTreeMap cacheMapDisk, cacheMapDiskMem, cacheMapMem;
+
+ private Map plainRegions = new HashMap<>();
+ private Map> regions = new HashMap<>();
+
+ @SuppressWarnings("unchecked")
+ public RenderedMap(ScheduledExecutorService executor) {
+ cacheMapDisk = cacheDBDisk.hashMap("OnDisk" + System.identityHashCode(this), VECTOR_SERIALIZER, IMAGE_SERIALIZER).create();
+ cacheMapDisk.clear();
+ cacheMapDiskMem = cacheDBMem
+ .hashMap("RenderedRegionCache" + System.identityHashCode(this), Serializer.JAVA, Serializer.JAVA)
+ // .expireStoreSize(1)
+ .expireMaxSize(1024)
+ .expireAfterCreate()
+ .expireAfterUpdate()
+ // .expireAfterGet()
+ .expireAfterCreate(30, TimeUnit.SECONDS)
+ .expireAfterUpdate(30, TimeUnit.SECONDS)
+ .expireAfterGet(60, TimeUnit.SECONDS)
+ .expireOverflow(cacheMapDisk)
+ .expireExecutor(executor)
+ .expireExecutorPeriod(10000)
+ .create();
+ // cacheMapMem = cacheMapDiskMem;
+ cacheMapMem = cacheDBMem.hashMap("ScaledRegionCache" + System.identityHashCode(this), Serializer.JAVA, Serializer.JAVA)
+ // .expireStoreSize(1)
+ .expireMaxSize(512)
+ .expireAfterCreate()
+ .expireAfterUpdate()
+ // .expireAfterGet()
+ .expireAfterCreate(30, TimeUnit.SECONDS)
+ .expireAfterUpdate(30, TimeUnit.SECONDS)
+ .expireAfterGet(60, TimeUnit.SECONDS)
+ // .expireOverflow(cacheMapDisk)
+ .expireExecutor(executor)
+ .expireExecutorPeriod(10000)
+ .create();
+ cacheMapDisk.checkThreadSafe();
+ cacheMapDiskMem.checkThreadSafe();
+ cacheMapMem.checkThreadSafe();
+ clearReload(Collections.emptyList());
+ }
+
+ public RenderedMap(ScheduledExecutorService executor, Collection positions) {
+ this(executor);
+ clearReload(positions);
+ }
+
+ public void close() {
+ clearReload(Collections.emptyList());
+ cacheMapDiskMem.close();
+ cacheMapMem.close();
+ cacheMapDisk.close();
+ cacheDBMem.close();
+ cacheDBDisk.close();
+ }
+
+ public void evictCache() {
+ cacheMapDiskMem.expireEvict();
+ cacheMapMem.expireEvict();
+ cacheMapDisk.expireEvict();
+ }
+
+ public void clearReload(Collection positions) {
+ cacheMapDisk.clear();
+ cacheMapDiskMem.clear();
+ cacheMapMem.clear();
+ regions.clear();
+ plainRegions.clear();
+ regions.put(0, plainRegions);
+ positions.stream().map(r -> new RenderedRegion(this, r)).forEach(r -> plainRegions.put(r.position, r));
+ }
+
+ public void invalidateAll() {
+ // System.out.println("Invalidate all");
+ plainRegions.values().forEach(r -> r.invalidateTree(true));
+ }
+
+ public boolean isNothingLoaded() {
+ return get(0).isEmpty();
+ }
+
+ public void draw(GraphicsContext gc, int level, AABBd frustum, double scale) {
+ Map map = get(level > 0 ? 0 : level);
+ gc.setFill(new Color(0.3f, 0.3f, 0.9f, 1.0f)); // Background color
+ plainRegions.values()
+ .stream()
+ .filter(r -> r.isVisible(frustum))
+ .forEach(r -> r.drawBackground(gc, scale));
+ try {
+ map
+ .entrySet()
+ .stream()
+ .filter(e -> RenderedRegion.isVisible(e.getKey(), level > 0 ? 0 : level, frustum))
+ .map(e -> {
+ RenderedRegion r = e.getValue();
+ if (e.getValue() == null)
+ r = get(level, e.getKey(), true);
+ return r;
+ })
+ .forEach(r -> r.draw(gc, level, frustum, scale));
+ } catch (NullPointerException e) {
+ // This seems to be a pretty rare exception that might be linked to some race hazard when daw() is called during the reloading of the map.
+ // Nonetheless this should not happen because every time it happens on level 0 and at no point in time a level 0 region has a key associated with a
+ // non-null value.
+ System.out.println(level);
+ System.out.println(plainRegions.entrySet());
+ System.out.println(regions.entrySet());
+ throw e;
+ }
+ plainRegions.values()
+ .stream()
+ .filter(r -> r.isVisible(frustum))
+ .forEach(r -> r.drawForeground(gc, frustum, scale));
+ }
+
+ public boolean updateImage(int level, AABBd frustum) {
+ try {
+ Thread current = Thread.currentThread();
+ if (regions.isEmpty())
+ // Race hazard: updateImage() is called while clearReload() is reloading all the chunks
+ return false;
+ return get(level)
+ .entrySet()
+ .stream()
+ .map(e -> e.getValue())
+ .filter(r -> r != null)
+ // .filter(e -> e.getValue().needsUpdate())
+ .filter(r -> r.isVisible(frustum))
+ // .sorted(comp)
+ .filter(r -> current.isInterrupted() ? false : r.updateImage())
+ .limit(10)
+ .collect(Collectors.toList())
+ .size() > 0;
+ } catch (ConcurrentModificationException e) {
+ // System.out.println(e);
+ return true;
+ }
+ }
+
+ public Map get(int level) {
+ // if (new Error().getStackTrace().length > 180)
+ // Sometimes, this throws a StackOverflowError, but Eclipse doesn't show when this method got called before the recursion in the stack trace. This
+ // is to catch the error prematurely in the hope of getting a full stack trace
+ // TODO this is really bad for performance, remove ASAP the bug is found and fixed
+ // throw new InternalError("Stack overflow.");
+ Map ret = regions.get(level);
+ try {
+ // the bug might be when requesting values on level 0 that are null (not calculated)
+ if (ret == null && level != 0) {
+ Map ret2 = new HashMap<>();
+ ret = ret2;
+ // the bug might be to using abovePos() regardless of the level being zoomed in or out
+ get(level < 0 ? level + 1 : level - 1).keySet().stream().map(RenderedMap::abovePos).map(v -> new ImmutableVector2i(v)).distinct().forEach(v -> ret2.put(v, null));
+
+ regions.put(level, ret);
+ }
+ } catch (StackOverflowError e) {
+ System.out.println(level + " " + ret);
+ throw e;
+ }
+ return ret;
+ }
+
+ public void putImage(Vector2ic pos, WritableImage image) {
+ if (!plainRegions.containsKey(pos))
+ throw new IllegalArgumentException("Position out of bounds");
+ plainRegions.get(pos).setImage(image);
+ }
+
+ public RenderedImage createImage(RenderedRegion r) {
+ return new RenderedImage(this, r.level <= 0 ? cacheMapDiskMem : cacheMapMem, new Vector3i(r.position.x(), r.position.y(), r.level).toImmutable());
+ }
+
+ protected boolean isImageLoaded(Vector3ic key) {
+ return !unloaded.contains(key);
+ }
+
+ public RenderedRegion get(int level, ImmutableVector2i position, boolean create) {
+ Map map = get(level);
+ RenderedRegion r = map.get(new ImmutableVector2i(position));
+ if (create && r == null && level != 0 && ((level > 0 /* && plainRegions.containsKey(new Vector2i(position.x() >> level, position.y() >>
+ * level).toImmutable()) */) || map.containsKey(position))) {
+ r = new RenderedRegion(this, level, position);
+ if (level < 0)
+ Arrays.stream(belowPos(position)).forEach(pos -> get(level + 1, position, true));
+ if (level > 0)
+ get(level - 1, abovePos(position), true);
+ map.put(position, r);
+ }
+ return r;
+ }
+
+ public RenderedRegion[] get(int level, ImmutableVector2i[] belowPos, boolean create) {
+ RenderedRegion[] ret = new RenderedRegion[belowPos.length];
+ for (int i = 0; i < belowPos.length; i++)
+ ret[i] = get(level, belowPos[i], create);
+ return ret;
+ }
+
+ public static ImmutableVector2i abovePos(Vector2ic pos) {
+ return groundPos(pos, 1);
+ }
+
+ public static ImmutableVector2i groundPos(Vector2ic pos, int levelDiff) {
+ return new ImmutableVector2i(pos.x() >> levelDiff, pos.y() >> levelDiff);
+ }
+
+ public static ImmutableVector2i[] belowPos(Vector2ic pos) {
+ ImmutableVector2i belowPos = new ImmutableVector2i(pos.x() << 1, pos.y() << 1);
+ return new ImmutableVector2i[] {
+ new ImmutableVector2i(0, 0).add(belowPos),
+ new ImmutableVector2i(1, 0).add(belowPos),
+ new ImmutableVector2i(0, 1).add(belowPos),
+ new ImmutableVector2i(1, 1).add(belowPos)
+ };
+ }
+
+ public static WritableImage halfSize(WritableImage old, WritableImage... corners) {
+ return halfSize(corners[0], corners[1], corners[2], corners[3]);
+ }
+
+ public static WritableImage halfSize(WritableImage old, WritableImage topLeft, WritableImage topRight, WritableImage bottomLeft, WritableImage bottomRight) {
+ WritableImage output = old != null ? old : new WritableImage(512, 512);
+
+ PixelReader topLeftReader = topLeft != null ? topLeft.getPixelReader() : null;
+ PixelReader topRightReader = topRight != null ? topRight.getPixelReader() : null;
+ PixelReader bottomLeftReader = bottomLeft != null ? bottomLeft.getPixelReader() : null;
+ PixelReader bottomRightReader = bottomRight != null ? bottomRight.getPixelReader() : null;
+
+ int[] topLeftPixels = genBuffer(512 * 512);
+ int[] topRightPixels = genBuffer(512 * 512);
+ int[] bottomLeftPixels = genBuffer(512 * 512);
+ int[] bottomRightPixels = genBuffer(512 * 512);
+
+ if (topLeftReader != null)
+ topLeftReader.getPixels(0, 0, 512, 512, PixelFormat.getIntArgbInstance(), topLeftPixels, 0, 512);
+ if (topRightReader != null)
+ topRightReader.getPixels(0, 0, 512, 512, PixelFormat.getIntArgbInstance(), topRightPixels, 0, 512);
+ if (bottomLeftReader != null)
+ bottomLeftReader.getPixels(0, 0, 512, 512, PixelFormat.getIntArgbInstance(), bottomLeftPixels, 0, 512);
+ if (bottomRightReader != null)
+ bottomRightReader.getPixels(0, 0, 512, 512, PixelFormat.getIntArgbInstance(), bottomRightPixels, 0, 512);
+
+ PixelWriter writer = output.getPixelWriter();
+
+ // TODO optimize with buffers
+ for (int y = 0; y < 256; y++) {
+ for (int x = 0; x < 256; x++) {
+ int rx = x * 2;
+ int ry = y * 2;
+ writer.setArgb(x, y, topLeftReader != null ? sampleColor(rx, ry, topLeftPixels) : 0);
+ writer.setArgb(x + 256, y, topRightReader != null ? sampleColor(rx, ry, topRightPixels) : 0);
+ writer.setArgb(x, y + 256, bottomLeftReader != null ? sampleColor(rx, ry, bottomLeftPixels) : 0);
+ writer.setArgb(x + 256, y + 256, bottomRightReader != null ? sampleColor(rx, ry, bottomRightPixels) : 0);
+ }
+ }
+ return output;
+ }
+
+ private static int sampleColor(int x, int y, int[] colors) {
+ // Image image = new Image(is, requestedWidth, requestedHeight, preserveRatio, smooth)
+ // int[] colors = genBuffer(4);
+ // reader.getPixels(x, y, 2, 2, WritablePixelFormat.getIntArgbInstance(), colors, 0, 2);
+
+ int c1 = colors[y * 512 + x], c2 = colors[y * 512 + x + 1], c3 = colors[y * 512 + x + 512], c4 = colors[y * 512 + x + 513];
+ // TODO premultiply alpha to avoid dark edges
+ long ret = 0;// use long against overflow
+ long a1 = c1 >>> 24, a2 = c2 >>> 24, a3 = c3 >>> 24, a4 = c4 >>> 24;
+ // alpha
+ ret |= ((a1 + a2 + a3 + a4) << 22) & 0xFF000000;
+ // red
+ ret |= ((((c1 & 0x00FF0000) * a1 + (c2 & 0x00FF0000) * a2 + (c3 & 0x00FF0000) * a3 + (c4 & 0x00FF0000) * a4) / 255) >> 2) & 0x00FF0000;
+ // green
+ ret |= ((((c1 & 0x0000FF00) * a1 + (c2 & 0x0000FF00) * a2 + (c3 & 0x0000FF00) * a3 + (c4 & 0x0000FF00) * a4) / 255) >> 2) & 0x0000FF00;
+ // blue
+ ret |= ((((c1 & 0x000000FF) * a1 + (c2 & 0x000000FF) * a2 + (c3 & 0x000000FF) * a3 + (c4 & 0x000000FF) * a4) / 255) >> 2) & 0x000000FF;
+ return (int) ret;
+ }
+
+ public static WritableImage doubleSize(WritableImage old, WritableImage input, int levelDiff, Vector2i subTile) {
+ WritableImage output = old != null ? old : new WritableImage(512, 512);
+
+ PixelReader reader = input.getPixelReader();
+ PixelWriter writer = output.getPixelWriter();
+
+ int tileSize = 512 >> levelDiff;
+ int scaleFactor = 1 << levelDiff;
+
+ int[] pixels = genBuffer(tileSize * tileSize);
+ int[] pixel = genBuffer(scaleFactor * scaleFactor);
+ reader.getPixels(subTile.x * tileSize, subTile.y * tileSize, tileSize, tileSize, PixelFormat.getIntArgbInstance(), pixels, 0, tileSize);
+
+ for (int y = 0; y < tileSize; y++) {
+ for (int x = 0; x < tileSize; x++) {
+ final int argb = pixels[y * tileSize + x];
+ Arrays.fill(pixel, argb);
+ writer.setPixels(x * scaleFactor, y * scaleFactor, scaleFactor, scaleFactor, PixelFormat.getIntArgbInstance(), pixel, 0, scaleFactor);
+ }
+ }
+ return output;
+ }
+
+ /** Test how much time this takes to see if object pooling is needed. */
+ private static int[] genBuffer(int length) {
+ return new int[length];
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java b/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java
new file mode 100644
index 0000000..fa581fd
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java
@@ -0,0 +1,197 @@
+package togos.minecraft.maprend.gui;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import org.joml.AABBd;
+import org.joml.ImmutableVector2i;
+import org.joml.Vector2i;
+import org.joml.Vector2ic;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.image.WritableImage;
+import javafx.scene.paint.Color;
+import togos.minecraft.maprend.RegionMap.Region;
+
+public class RenderedRegion {
+
+ public enum RenderingState {
+ VALID, // Don't touch
+ INVALID, // Recalculate please
+ DRAWING, // Recalculating
+ REDRAW; // Aborted while recalculating, re-recalculate
+ // ABORT; // World changed, abort calculation
+
+ public boolean isInvalid() {
+ return this != VALID;
+ }
+ }
+
+ protected final RenderedMap map;
+ // public final AtomicBoolean invalid = new AtomicBoolean(true);
+ protected final RenderedImage image;
+ public final int level;
+ public final ImmutableVector2i position;
+ public final AtomicReference valid = new AtomicReference<>(RenderingState.INVALID);
+
+ public Region region;
+
+ public RenderedRegion(RenderedMap map, Region region) {
+ this(map, 0, new ImmutableVector2i(region.rx, region.rz));
+ this.region = region;
+ }
+
+ public RenderedRegion(RenderedMap map, int level, ImmutableVector2i position) {
+ this.map = map;
+ this.level = level;
+ this.position = position;
+ this.image = map.createImage(this);
+ // setImage(null);
+ }
+
+ public void setImage(WritableImage image) {
+ Objects.requireNonNull(image);
+ invalidateTree(true);
+ this.image.setImage(image);
+ valid.set(RenderingState.VALID);
+ }
+
+ public void invalidateTree(boolean keepImage) {
+ if (!valid.compareAndSet(RenderingState.DRAWING, RenderingState.REDRAW))
+ valid.set(RenderingState.INVALID);
+ if (level >= 0)
+ Arrays.stream(getBelow(false)).filter(__ -> __ != null).forEach(r -> r.invalidateTree(keepImage));
+ if (level <= 0) {
+ RenderedRegion above = getAbove(false);
+ if (above != null)
+ above.invalidateTree(keepImage);
+ }
+ if (!keepImage)
+ this.image.setImage(null);
+ }
+
+ public RenderedRegion[] getBelow(boolean create) {
+ return map.get(level + 1, RenderedMap.belowPos(position), create);
+ }
+
+ public RenderedRegion getAbove(boolean create) {
+ return map.get(level - 1, RenderedMap.abovePos(position), create);
+ }
+
+ public RenderedRegion getGround(boolean create) {
+ return map.get(0, RenderedMap.groundPos(position, level), create);
+ }
+
+ public boolean updateImage() {
+ boolean changed = false;
+
+ if (!this.image.isImageLoaded())
+ changed = true;
+ // This will load an image back from cache if needed
+ WritableImage image = this.image.getImage(true);
+
+ if (level != 0 && (image == null || valid.get().isInvalid())) {
+ if (level > 0) {
+ // check above
+ // get above image
+ RenderedRegion above = getGround(true);
+ if (above == null)
+ System.out.println(getGround(true));
+ WritableImage aboveImage = above.getImage(true);
+ // upscale image
+ if (aboveImage != null)
+ image = RenderedMap.doubleSize(image, aboveImage, level, new Vector2i(position.x() & ((1 << level) - 1), position.y() & ((1 << level) - 1)));
+ } else if (level < 0) {
+ // check below
+ RenderedRegion[] below = getBelow(true);
+ for (RenderedRegion r : below)
+ if (r != null)
+ changed |= r.updateImage();
+ // get below images
+ WritableImage topLeft = below[0] == null ? null : below[0].getImage(true);
+ WritableImage topRight = below[1] == null ? null : below[1].getImage(true);
+ WritableImage bottomLeft = below[2] == null ? null : below[2].getImage(true);
+ WritableImage bottomRight = below[3] == null ? null : below[3].getImage(true);
+ // downscale images
+ image = RenderedMap.halfSize(image, topLeft, topRight, bottomLeft, bottomRight);
+ }
+ this.image.setImage(image);
+ valid.set(RenderingState.VALID);
+ if (image != null)
+ changed = true;
+ }
+
+ return changed;
+ }
+
+ public WritableImage getImage(boolean force) {
+ return image.getImage(force);
+ }
+
+ public boolean isVisible(AABBd frustum) {
+ return isVisible(position, level, frustum);
+ }
+
+ public static boolean isVisible(Vector2ic position, int level, AABBd frustum) {
+ int size = WorldRendererCanvas.pow2(512, -level);
+ return frustum.testAABB(new AABBd(position.x() * size, position.y() * size, 0, (position.x() + 1) * size, (position.y() + 1) * size, 0));
+ }
+
+ public void draw(GraphicsContext gc, int drawingLevel, AABBd frustum, double scale) {
+ // bounds must have been checked here
+
+ int size = WorldRendererCanvas.pow2(512, -this.level);
+ final int overDraw = 3; // TODO make setting
+
+ WritableImage image = this.image.getImage(false);
+
+ if (image != null) {
+ if (drawingLevel <= this.level && this.level > 0)
+ // Fill background to prevent bleeding from lower levels of detail
+ gc.fillRect(position.x() * size + 1 / scale, position.y() * size + 1 / scale, size - 2 / scale, size - 2 / scale);
+ // Draw that image
+ gc.drawImage(image, position.x() * size, position.y() * size, size, size);
+ }
+
+ // Draw below if needed (check bounds)
+ if (drawingLevel > this.level || ((this.level < 0 || drawingLevel > (this.level - overDraw)) && image == null)) {
+ Arrays.stream(RenderedMap.belowPos(position))
+ .filter(v -> isVisible(v, this.level + 1, frustum))
+ .map(v -> map.get(this.level + 1, v, true))
+ .filter(r -> r != null)
+ .forEach(r -> r.draw(gc, drawingLevel, frustum, scale));
+ }
+ }
+
+ /** This method assumes the appropriate fill is already set */
+ public void drawBackground(GraphicsContext gc, double scale) {
+ int size = 512;// WorldRendererFX.pow2(512, -this.level);
+ // gc.translate(region.coordinates.x * 512, region.coordinates.y * 512);
+ gc.fillRect(position.x() * size - 1 / scale, position.y() * size - 1 / scale, size + 2 / scale, size + 2 / scale);
+ }
+
+ public void drawForeground(GraphicsContext gc, AABBd frustum, double scale) {
+ int size = 512;// WorldRendererFX.pow2(512, -this.level);
+ if (valid.get().isInvalid() && image.isImageSet()) {// reduce brightness
+ gc.setFill(new Color(0f, 0f, 0f, 0.5f));
+ gc.fillRect(position.x() * size, position.y() * size, size, size);
+ }
+ if (valid.get() == RenderingState.DRAWING || valid.get() == RenderingState.REDRAW) {
+ gc.setFill(new Color(0.9f, 0.9f, 0.15f, 1.0f));
+
+ double x = position.x() * 512, y = position.y() * 512, w = 512, h = 512, m = Math.min(6 / scale, 35);
+
+ double xw = Math.min(frustum.maxX, x + w);
+ double yh = Math.min(frustum.maxY, y + h);
+ x = Math.max(frustum.minX, x);
+ y = Math.max(frustum.minY, y);
+ w = xw - x;
+ h = yh - y;
+
+ // gc.translate(x, y);
+ gc.fillRect(x, y, w, m);
+ gc.fillRect(x, y + h - m, w, m);
+ gc.fillRect(x, y, m, h);
+ gc.fillRect(x + w - m, y, m, h);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java b/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java
new file mode 100644
index 0000000..3f4eda9
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java
@@ -0,0 +1,182 @@
+package togos.minecraft.maprend.gui;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.util.*;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.joml.Vector2dc;
+import org.joml.Vector3d;
+import javafx.application.Platform;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.image.WritableImage;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import togos.minecraft.maprend.BoundingRect;
+import togos.minecraft.maprend.RegionMap;
+import togos.minecraft.maprend.RegionRenderer;
+import togos.minecraft.maprend.gui.RenderedRegion.RenderingState;
+import togos.minecraft.maprend.io.RegionFile;
+
+public class WorldRendererCanvas extends Canvas implements Runnable {
+
+ public static final int THREAD_COUNT = 4;
+
+ protected RegionRenderer renderer;
+ protected RenderedMap map;
+
+ protected ScheduledThreadPoolExecutor executor;
+ protected final List> submitted = Collections.synchronizedList(new LinkedList<>());
+
+ protected GraphicsContext gc = getGraphicsContext2D();
+
+ public final DisplayViewport viewport = new DisplayViewport();
+
+ public WorldRendererCanvas(RegionRenderer renderer) {
+ this.renderer = Objects.requireNonNull(renderer);
+
+ {// Executor
+ executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(THREAD_COUNT);
+ map = new RenderedMap(executor);// Executors.newScheduledThreadPool(THREAD_COUNT / 2));
+ executor.scheduleAtFixedRate(() -> {
+ try {
+ // TODO execute more often if something changes and less often if not
+ // update upscaled/downscaled images of chunks
+ if (map.updateImage(viewport.getZoomLevel(), viewport.getFrustum()))
+ repaint();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ throw e;
+ }
+ }, 1000, 1000, TimeUnit.MILLISECONDS);
+ executor.scheduleWithFixedDelay(map::evictCache, 10, 10, TimeUnit.SECONDS);
+
+ executor.setKeepAliveTime(20, TimeUnit.SECONDS);
+ executor.allowCoreThreadTimeOut(true);
+ }
+
+ viewport.widthProperty.bind(widthProperty());
+ viewport.heightProperty.bind(heightProperty());
+ invalidateTextures();
+ viewport.frustumProperty.addListener(e -> repaint());
+ repaint();
+ }
+
+ public void loadWorld(File file) {
+ map.clearReload(RegionMap.load(file, BoundingRect.INFINITE).regions);
+ invalidateTextures();
+ }
+
+ public void invalidateTextures() {
+ map.invalidateAll();
+ for (int i = 0; i < THREAD_COUNT; i++)
+ executor.submit(this);
+ }
+
+ public void shutDown() {
+ executor.shutdownNow();
+ try {
+ executor.awaitTermination(1, TimeUnit.DAYS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ map.close();
+ }
+
+ /** Queues in a repaint event calling {@link renderWorld} from the JavaFX Application Thread */
+ public void repaint() {
+ if (Platform.isFxApplicationThread())
+ renderWorld();
+ else
+ Platform.runLater(this::renderWorld);
+ }
+
+ /** Requires to be called from the JavaFX Application Thread. */
+ public void renderWorld() {
+ gc = getGraphicsContext2D();
+ // gc.setStroke(Color.GREEN.deriveColor(0, 1, 1, .2));
+ gc.setLineWidth(10);
+ // gc.clearRect(0, 0, getWidth(), getHeight());
+ gc.setFill(new Color(0.2f, 0.2f, 0.6f, 1.0f));
+ gc.fillRect(0, 0, getWidth(), getHeight());
+
+ double scale = viewport.scaleProperty.get();
+ gc.save();
+ gc.scale(scale, scale);
+ Vector2dc translation = viewport.getTranslation();
+ gc.translate(translation.x(), translation.y());
+
+ map.draw(gc, viewport.getZoomLevel(), viewport.getFrustum(), scale);
+ gc.restore();
+
+ if (map.isNothingLoaded()) {
+ gc.setFill(Color.WHITE);
+ gc.setFont(Font.font(20));
+ gc.fillText("No regions loaded", 10, getHeight() - 10);
+ }
+
+ // gc.strokeRect(100, 100, getWidth() - 200, getHeight() - 200);
+ // gc.strokeRect(0, 0, getWidth() - 0, getHeight() - 0);
+ }
+
+ public RegionRenderer getRegionRenderer() {
+ return renderer;
+ }
+
+ @Override
+ public void run() {
+ RenderedRegion region = null;
+ region = nextRegion();
+ if (region == null)
+ return;
+ repaint();
+ try {
+ try (RegionFile rf = new RegionFile(region.region.regionFile)) {
+ BufferedImage texture2 = null;
+ do {
+ texture2 = renderer.render(rf);
+ } while (region.valid.compareAndSet(RenderingState.REDRAW, RenderingState.DRAWING) && !Thread.interrupted());
+
+ WritableImage texture = SwingFXUtils.toFXImage(texture2, null);
+ region.setImage(texture);
+ repaint();
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ } finally {
+ region.valid.set(RenderingState.VALID);
+ executor.submit(this);
+ }
+ }
+
+ /** Returns the next Region to render */
+ protected synchronized RenderedRegion nextRegion() {
+ // In region coordinates
+ Vector3d cursorPos = new Vector3d(viewport.getMouseInWorld(), 0).div(512).sub(.5, .5, 0);
+
+ Comparator comp = (a, b) -> Double.compare(new Vector3d(a.position.x(), a.position.y(), 0).sub(cursorPos).length(), new Vector3d(b.position.x(), b.position.y(), 0).sub(cursorPos).length());
+ RenderedRegion min = null;
+ for (RenderedRegion r : map.get(0).values())
+ if (r.valid.get() == RenderingState.INVALID && (min == null || comp.compare(min, r) > 0))
+ min = r;
+ if (min != null)
+ // min got handled by another Thread already (while we were still searching), so get a new one
+ if (!min.valid.compareAndSet(RenderingState.INVALID, RenderingState.DRAWING))
+ return nextRegion();
+ return min;
+ }
+
+ /** @return a*2^n using bit shifting */
+ public static int pow2(int a, int n) {
+ if (n > 0)
+ return a << n;
+ else if (n < 0)
+ return a >> -n;
+ else
+ return a;
+ }
+}
diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java b/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java
new file mode 100644
index 0000000..5d210ca
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java
@@ -0,0 +1,68 @@
+package togos.minecraft.maprend.gui.decoration;
+
+import org.joml.Vector2d;
+import org.joml.Vector2dc;
+import com.sun.glass.ui.Application;
+import com.sun.glass.ui.Robot;
+import javafx.scene.input.MouseButton;
+import javafx.scene.layout.Region;
+import togos.minecraft.maprend.gui.DisplayViewport;
+
+/**
+ * This decoration provides basic drag and zoom support, where you can set the button used for dragging as well as zoom speed and direction. Both
+ * functionalities are optional and can be disabled separately. If you disable both, this class will still forward mouse moved events to the view frustum. This
+ * might be useful if you use manual zooming logic externally and still want to zoom around the mouse center as it does normally.
+ */
+@SuppressWarnings("restriction")
+public class DragScrollDecoration extends Region {
+
+ protected Robot robot = Application.GetApplication().createRobot();
+ protected int cooldown;
+
+ /** Creates an instance of this class that will drag with the right mouse button and a scroll factor of 1/10. */
+ public DragScrollDecoration(DisplayViewport frustum) {
+ this(frustum, MouseButton.SECONDARY, 0.1d);
+ }
+
+ public DragScrollDecoration(DisplayViewport frustum, boolean allowDrag, boolean allowZoom) {
+ this(frustum, allowDrag ? MouseButton.SECONDARY : null, allowZoom ? 0.1d : 0);
+ }
+
+ /**
+ * @param dragButton The button that must be pressed to activate dragging. null will disable dragging.
+ * @param scrollFactor Higher values will increase the zoomed amount per scroll. Zero deactivates scrolling. Negative values invert the scroll direction.
+ */
+ public DragScrollDecoration(DisplayViewport frustum, MouseButton dragButton, double scrollFactor) {
+ setOnMouseMoved(e -> frustum.mousePosProperty.set(new Vector2d(e.getX(), e.getY())));
+ setOnMouseDragged(e -> {
+ Vector2dc old = frustum.mousePosProperty.get();
+ Vector2d current = new Vector2d(e.getX(), e.getY());
+ if (cooldown <= 0) {
+ if (dragButton != null && e.getButton() == dragButton) {
+ frustum.pan(current.x() - old.x(), current.y() - old.y());
+ double width = getWidth();
+ double height = getHeight();
+ double dx = 0, dy = 0;
+ if (current.x() < 1)
+ dx = width - 3;
+ if (current.x() > width - 2)
+ dx = -width + 4;
+ if (current.y() < 1)
+ dy = height - 3;
+ if (current.y() > height - 2)
+ dy = -height + 4;
+ if (dx != 0 || dy != 0) {
+ current.add(dx, dy);
+ robot.mouseMove((int) (e.getScreenX() + dx), (int) (e.getScreenY() + dy));
+ cooldown = 3;
+ }
+ }
+ } else if (cooldown > 0)
+ cooldown--;
+ frustum.mousePosProperty.set(current);
+ });
+
+ if (scrollFactor != 0)
+ setOnScroll(e -> frustum.mouseScroll(e.getTextDeltaY() * scrollFactor));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/SelectRectangleDecoration.java b/src/main/java/togos/minecraft/maprend/gui/decoration/SelectRectangleDecoration.java
new file mode 100644
index 0000000..86ad633
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/decoration/SelectRectangleDecoration.java
@@ -0,0 +1,70 @@
+package togos.minecraft.maprend.gui.decoration;
+
+import java.util.Objects;
+import org.joml.Rectangled;
+import org.joml.Vector2d;
+import org.joml.Vector2dc;
+import javafx.scene.input.MouseButton;
+import javafx.scene.paint.Color;
+import togos.minecraft.maprend.gui.CanvasHelper;
+import togos.minecraft.maprend.gui.DisplayViewport;
+
+public class SelectRectangleDecoration extends CanvasHelper {
+
+ protected final DisplayViewport frustum;
+
+ protected final Vector2d dragStart = new Vector2d(), dragEnd = new Vector2d();
+
+ public SelectRectangleDecoration(DisplayViewport frustum, Rectangled initialSelection) {
+ this(frustum, MouseButton.PRIMARY, initialSelection);
+ }
+
+ public SelectRectangleDecoration(DisplayViewport frustum, MouseButton selectButton, Rectangled initialSelection) {
+ this.frustum = Objects.requireNonNull(frustum);
+ Objects.requireNonNull(selectButton);
+ frustum.frustumProperty.addListener(e -> repaint());
+
+ if (initialSelection == null)
+ initialSelection = new Rectangled();
+ dragStart.set(initialSelection.minX, initialSelection.minY);
+ dragEnd.set(initialSelection.maxX, initialSelection.maxY);
+
+ setOnMousePressed(e -> {
+ if (e.getButton() == selectButton) {
+ dragStart.set(frustum.getMouseInWorld());
+ dragEnd.set(dragStart);
+ repaint();
+ }
+ });
+ setOnMouseDragged(e -> {
+ if (e.getButton() == selectButton)
+ dragEnd.set(frustum.getMouseInWorld());
+ repaint();
+ });
+ gc.setLineWidth(2);
+ gc.setFill(Color.BLUE.deriveColor(-35, .6, 1.3, 0.5));
+ gc.setStroke(Color.BLUE.deriveColor(0, 0.8, 1.3, 0.8));
+ }
+
+ @Override
+ public void render() {
+ gc.clearRect(0, 0, getWidth(), getHeight());
+
+ double scale = frustum.scaleProperty.get();
+ gc.save();
+ gc.scale(scale, scale);
+ Vector2dc translation = frustum.getTranslation();
+ gc.translate(translation.x(), translation.y());
+
+ gc.fillRect(Math.min(dragStart.x, dragEnd.x), Math.min(dragStart.y, dragEnd.y), Math.abs(dragStart.x - dragEnd.x), Math.abs(dragStart.y -
+ dragEnd.y));
+ gc.strokeRect(Math.min(dragStart.x, dragEnd.x), Math.min(dragStart.y, dragEnd.y), Math.abs(dragStart.x - dragEnd.x), Math.abs(dragStart.y -
+ dragEnd.y));
+
+ gc.restore();
+ }
+
+ public Rectangled getSelected() {
+ return new Rectangled(Math.min(dragStart.x, dragEnd.x), Math.min(dragStart.y, dragEnd.y), Math.max(dragStart.x, dragEnd.x), Math.max(dragStart.y, dragEnd.y));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/SettingsOverlay.java b/src/main/java/togos/minecraft/maprend/gui/decoration/SettingsOverlay.java
new file mode 100644
index 0000000..e4f9976
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/decoration/SettingsOverlay.java
@@ -0,0 +1,80 @@
+package togos.minecraft.maprend.gui.decoration;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Objects;
+import java.util.ResourceBundle;
+import org.controlsfx.control.RangeSlider;
+import javafx.animation.TranslateTransition;
+import javafx.beans.binding.Bindings;
+import javafx.beans.value.ChangeListener;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.fxml.Initializable;
+import javafx.scene.control.Label;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.VBox;
+import javafx.util.Duration;
+import togos.minecraft.maprend.gui.WorldRendererCanvas;
+
+public class SettingsOverlay extends AnchorPane implements Initializable {
+
+ @FXML
+ private Label minHeight, maxHeight;
+ @FXML
+ private RangeSlider heightSlider;
+ @FXML
+ private ToggleButton showButton;
+ @FXML
+ private VBox rightMenu;
+
+ protected WorldRendererCanvas panel;
+
+ public SettingsOverlay(WorldRendererCanvas panel) {
+ this.panel = Objects.requireNonNull(panel);
+ try {
+ FXMLLoader loader = new FXMLLoader(getClass().getResource("SettingsOverlay.fxml"));
+ loader.setRoot(this);
+ loader.setController(this);
+ loader.load();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void initialize(URL location, ResourceBundle resources) {
+ minHeight.textProperty().bind(Bindings.format("Min: %.0f", heightSlider.lowValueProperty()));
+ maxHeight.textProperty().bind(Bindings.format("Max: %.0f", heightSlider.highValueProperty()));
+
+ ChangeListener super Boolean> heightListener = (e, oldVal, newVal) -> {
+ if (oldVal && !newVal) {
+ if (e == heightSlider.lowValueChangingProperty())
+ panel.getRegionRenderer().settings.minHeight = (int) Math.round(heightSlider.lowValueProperty().getValue().doubleValue());
+ else if (e == heightSlider.highValueChangingProperty())
+ panel.getRegionRenderer().settings.maxHeight = (int) Math.round(heightSlider.highValueProperty().getValue().doubleValue());
+ panel.invalidateTextures();
+ panel.repaint();
+ }
+ };
+ heightSlider.lowValueChangingProperty().addListener(heightListener);
+ heightSlider.highValueChangingProperty().addListener(heightListener);
+
+ showButton.setOnAction(e -> {
+ if (showButton.isSelected()) {
+ // showButton.setText("Hide settings");
+ TranslateTransition transition = new TranslateTransition(Duration.millis(500), rightMenu);
+ transition.setFromX(rightMenu.getTranslateX());
+ transition.setToX(0);
+ transition.play();
+ } else {
+ // showButton.setText("Show settings");
+ TranslateTransition transition = new TranslateTransition(Duration.millis(500), rightMenu);
+ transition.setFromX(rightMenu.getTranslateX());
+ transition.setToX(rightMenu.getWidth() - 15);
+ transition.play();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/package-info.java b/src/main/java/togos/minecraft/maprend/gui/decoration/package-info.java
new file mode 100644
index 0000000..1ca6b91
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/decoration/package-info.java
@@ -0,0 +1,26 @@
+/**
+ *
+ * A Decoration is a {@link javafx.scene.Node} (typically a subclass of {@link javafx.scene.layout.Region}), that is part of the internal stack of the
+ * {@link togos.minecraft.maprend.gui.MapPane}. Multiple decorations are added on top of each other and may provide additional functionality or visual
+ * information to the world map. They all have the same size as the map. There are two types of Decorations:
+ *
+ *
A settings layer is a normal GUI layer on top that provides additional functionality. The pickOnBounds property should be set on false so
+ * that events only get caught where there actually is some GUI beneath. Otherwise user interaction events with the map might not reach their goal.
+ *
A decoration layer does not catch any events. All events that would reach it get caught by an internal component and distributed to all decoration
+ * layers. This allows to have multiple decorations that use the same events, e.g. one for dragging and one for clicking or using different mouse buttons.
+ * Decoration layers thus cannot easily contain conventional GUI.
+ *
+ *
+ *
+ * Available decorations:
+ *
+ *
{@link togos.minecraft.maprend.gui.decoration.SettingsOverlay}: Puts a hideable panel at the right of the map to change the min and max height setting.
+ *
+ *
{@link togos.minecraft.maprend.gui.decoration.DragScrollDecoration}: Provides basic drag and zoom functionality.
+ *
+ * What is yet to come: More settings, different decorations for selecting areas, showing points (players, monuments, etc.) on the map
+ *
+ *
+ * @author piegames
+ */
+package togos.minecraft.maprend.gui.decoration;
\ No newline at end of file
diff --git a/src/main/java/togos/minecraft/maprend/gui/package-info.java b/src/main/java/togos/minecraft/maprend/gui/package-info.java
new file mode 100644
index 0000000..0d9e513
--- /dev/null
+++ b/src/main/java/togos/minecraft/maprend/gui/package-info.java
@@ -0,0 +1,14 @@
+/**
+ *
+ * This package contains all classes to show an interactive map of a world in a JavaFX application that is highly customizable (or will be in the future). The
+ * most important class is {@link togos.minecraft.maprend.gui.WorldRendererCanvas}, a Canvas that loads and renders the world map. But it is recommended to use
+ * {@link togos.minecraft.maprend.gui.MapPane} instead, which will wrap the canvas in a GUI with proper layout and the possibility to add decorations and
+ * interactive functionality.
+ *
+ *
+ * This package is still under development. Everything might be subject to change. Use at own risk.
+ *