diff --git a/src/main/java/com/volmit/iris/util/mantle/Mantle.java b/src/main/java/com/volmit/iris/util/mantle/Mantle.java index e17ae0b32..8113ef80b 100644 --- a/src/main/java/com/volmit/iris/util/mantle/Mantle.java +++ b/src/main/java/com/volmit/iris/util/mantle/Mantle.java @@ -18,8 +18,10 @@ package com.volmit.iris.util.mantle; +import com.google.common.collect.ImmutableList; import com.volmit.iris.Iris; import com.volmit.iris.engine.data.cache.Cache; +import com.volmit.iris.engine.object.basic.IrisPosition; import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.collection.KSet; import com.volmit.iris.util.documentation.BlockCoordinates; @@ -28,17 +30,20 @@ import com.volmit.iris.util.documentation.RegionCoordinates; import com.volmit.iris.util.format.C; import com.volmit.iris.util.format.Form; import com.volmit.iris.util.function.Consumer4; +import com.volmit.iris.util.math.INode; +import com.volmit.iris.util.math.KochanekBartelsInterpolation; import com.volmit.iris.util.math.M; +import com.volmit.iris.util.math.PathInterpolation; import com.volmit.iris.util.matter.Matter; import com.volmit.iris.util.parallel.BurstExecutor; import com.volmit.iris.util.parallel.HyperLock; import com.volmit.iris.util.parallel.MultiBurst; +import org.bukkit.util.Vector; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; @@ -79,6 +84,13 @@ public class Mantle { Iris.debug("Opened The Mantle " + C.DARK_AQUA + dataFolder.getAbsolutePath()); } + /** + * Raise a flag if it is lowered currently, If the flag was raised, execute the runnable + * @param x the chunk x + * @param z the chunk z + * @param flag the flag to raise + * @param r the runnable to fire if the flag is now raised (and was previously lowered) + */ @ChunkCoordinates public void raiseFlag(int x, int z, MantleFlag flag, Runnable r) { if (!hasFlag(x, z, flag)) { @@ -87,6 +99,13 @@ public class Mantle { } } + /** + * Lower a flag if it is raised. If the flag was lowered (meaning it was previously raised), execute the runnable + * @param x the chunk x + * @param z the chunk z + * @param flag the flag to lower + * @param r the runnable that is fired if the flag was raised but is now lowered + */ @ChunkCoordinates public void lowerFlag(int x, int z, MantleFlag flag, Runnable r) { if (hasFlag(x, z, flag)) { @@ -95,11 +114,27 @@ public class Mantle { } } + /** + * Flag or unflag a chunk + * @param x the chunk x + * @param z the chunk z + * @param flag the flag + * @param flagged should it be set to flagged or not + */ @ChunkCoordinates public void flag(int x, int z, MantleFlag flag, boolean flagged) { get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).flag(flag, flagged); } + /** + * Iterate data in a chunk + * @param x the chunk x + * @param z the chunk z + * @param type the type of data to iterate + * @param iterator the iterator (x,y,z,data) -> do stuff + * @param requiredFlags any required flags that must be met for this chunk to be iterated + * @param the type of data to iterate + */ @ChunkCoordinates public void iterateChunk(int x, int z, Class type, Consumer4 iterator, MantleFlag... requiredFlags) { for (MantleFlag i : requiredFlags) { @@ -111,17 +146,13 @@ public class Mantle { get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).iterate(type, iterator); } - @ChunkCoordinates - public void iterateChunk(int x, int z, Class type, Consumer4 iterator, BurstExecutor e, MantleFlag... requiredFlags) { - for (MantleFlag i : requiredFlags) { - if (!hasFlag(x, z, i)) { - return; - } - } - - get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).iterate(type, iterator, e); - } - + /** + * Does this chunk have a flag on it? + * @param x the x + * @param z the z + * @param flag the flag to test + * @return true if it's flagged + */ @ChunkCoordinates public boolean hasFlag(int x, int z, MantleFlag flag) { return get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).isFlagged(flag); @@ -190,6 +221,10 @@ public class Mantle { .get(x & 15, y & 15, z & 15); } + /** + * Is this mantle closed + * @return true if it is + */ public boolean isClosed() { return closed.get(); @@ -367,10 +402,23 @@ public class Mantle { })); } + /** + * Get the file for a region + * @param folder the folder + * @param x the x coord + * @param z the z coord + * @return the file + */ public static File fileForRegion(File folder, int x, int z) { return fileForRegion(folder, key(x, z)); } + /** + * Get the file for the given region + * @param folder the data folder + * @param key the region key + * @return the file + */ public static File fileForRegion(File folder, Long key) { String id = UUID.nameUUIDFromBytes(("TectonicPlate:" + key).getBytes(StandardCharsets.UTF_8)).toString(); File f = new File(folder, id.substring(0, 2) + "/" + id.split("\\Q-\\E")[3] + "/" + id + ".ttp"); @@ -378,6 +426,12 @@ public class Mantle { return f; } + /** + * Get the long value representing a chunk or region coordinate + * @param x the x + * @param z the z + * @return the value + */ public static Long key(int x, int z) { return Cache.key(x, z); } @@ -385,4 +439,437 @@ public class Mantle { public void saveAll() { } + + /** + * Set a sphere into the mantle + * @param cx the center x + * @param cy the center y + * @param cz the center z + * @param radius the radius of this sphere + * @param fill should it be filled? or just the outer shell? + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setSphere(int cx, int cy, int cz, double radius, boolean fill, T data) + { + setElipsoid(cx, cy, cz, radius, radius, radius, fill, data); + } + + /** + * Set an elipsoid into the mantle + * @param cx the center x + * @param cy the center y + * @param cz the center z + * @param rx the x radius + * @param ry the y radius + * @param rz the z radius + * @param fill should it be filled or just the outer shell? + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setElipsoid(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data) + { + rx += 0.5; + ry += 0.5; + rz += 0.5; + final double invRadiusX = 1 / rx; + final double invRadiusY = 1 / ry; + final double invRadiusZ = 1 / rz; + final int ceilRadiusX = (int) Math.ceil(rx); + final int ceilRadiusY = (int) Math.ceil(ry); + final int ceilRadiusZ = (int) Math.ceil(rz); + double nextXn = 0; + + forX: for (int x = 0; x <= ceilRadiusX; ++x) { + final double xn = nextXn; + nextXn = (x + 1) * invRadiusX; + double nextYn = 0; + forY: for (int y = 0; y <= ceilRadiusY; ++y) { + final double yn = nextYn; + nextYn = (y + 1) * invRadiusY; + double nextZn = 0; + for (int z = 0; z <= ceilRadiusZ; ++z) { + final double zn = nextZn; + nextZn = (z + 1) * invRadiusZ; + + double distanceSq = lengthSq(xn, yn, zn); + if (distanceSq > 1) { + if (z == 0) { + if (y == 0) { + break forX; + } + break forY; + } + break; + } + + if (!fill) { + if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) { + continue; + } + } + + set(x + cx,y + cy,z + cz, data); + set(-x + cx,y + cy,z + cz, data); + set(x + cx,-y + cy,z + cz, data); + set(x + cx,y + cy,-z + cz, data); + set(-x + cx,y + cy,-z + cz, data); + set(-x + cx,-y + cy,z + cz, data); + set(x + cx,-y + cy,-z + cz, data); + set(-x + cx,y + cy,-z + cz, data); + set(-x + cx,-y + cy,-z + cz, data); + } + } + } + } + + /** + * Set a cuboid of data in the mantle + * @param x1 the min x + * @param y1 the min y + * @param z1 the min z + * @param x2 the max x + * @param y2 the max y + * @param z2 the max z + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setCuboid(int x1, int y1, int z1, int x2, int y2, int z2, T data) + { + int j,k; + + for(int i = x1; i <= x2; i++) + { + for(j = x1; j <= x2; j++) + { + for(k = x1; k <= x2; k++) + { + set(i,j,k,data); + } + } + } + } + + /** + * Set a pyramid of data in the mantle + * @param cx the center x + * @param cy the base y + * @param cz the center z + * @param data the data to set + * @param size the size of the pyramid (width of base & height) + * @param filled should it be filled or hollow + * @param the type of data to apply to the mantle + */ + @SuppressWarnings("ConstantConditions") + public void setPyramid(int cx, int cy, int cz, T data, int size, boolean filled) { + int height = size; + + for (int y = 0; y <= height; ++y) { + size--; + for (int x = 0; x <= size; ++x) { + for (int z = 0; z <= size; ++z) { + if ((filled && z <= size && x <= size) || z == size || x == size) { + set(x + cx, y + cy, z + cz, data); + set(-x + cx, y + cy, z + cz, data); + set(x + cx, y + cy, -z + cz, data); + set(-x + cx, y + cy, -z + cz, data); + } + } + } + } + } + + /** + * Set a 3d tube spline interpolated with Kochanek Bartels + * @param nodevectors the vector points + * @param radius the radius + * @param filled if it should be filled or hollow + * @param data the data to set + */ + public void setSpline(List nodevectors, double radius, boolean filled, T data) { + setSpline(nodevectors, 0, 0, 0, 10, radius, filled, data); + } + + /** + * Set a 3d tube spline interpolated with Kochanek Bartels + * @param nodevectors the spline points + * @param tension the tension 0 + * @param bias the bias 0 + * @param continuity the continuity 0 + * @param quality the quality 10 + * @param radius the radius + * @param filled filled or hollow + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setSpline(List nodevectors, double tension, double bias, double continuity, double quality, double radius, boolean filled, T data) { + Set vset = new KSet<>(); + List nodes = new ArrayList<>(nodevectors.size()); + PathInterpolation interpol = new KochanekBartelsInterpolation(); + + for (Vector nodevector : nodevectors) { + INode n = new INode(nodevector); + n.setTension(tension); + n.setBias(bias); + n.setContinuity(continuity); + nodes.add(n); + } + + interpol.setNodes(nodes); + double splinelength = interpol.arcLength(0, 1); + for (double loop = 0; loop <= 1; loop += 1D / splinelength / quality) { + Vector tipv = interpol.getPosition(loop); + vset.add(new IrisPosition(tipv.toBlockVector())); + } + + vset = getBallooned(vset, radius); + if (!filled) { + vset = getHollowed(vset); + } + + set(vset, data); + } + + /** + * Set a 3d line + * @param a the first point + * @param b the second point + * @param radius the radius + * @param filled hollow or filled? + * @param data the data + * @param the type of data to apply to the mantle + */ + public void setLine(IrisPosition a, IrisPosition b, double radius, boolean filled, T data) + { + setLine(ImmutableList.of(a, b), radius, filled, data); + } + + /** + * Set lines for points + * @param vectors the points + * @param radius the radius + * @param filled hollow or filled? + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setLine(List vectors, double radius, boolean filled, T data) { + Set vset = new KSet<>(); + + for (int i = 0; vectors.size() != 0 && i < vectors.size() - 1; i++) { + IrisPosition pos1 = vectors.get(i); + IrisPosition pos2 = vectors.get(i + 1); + int x1 = pos1.getX(); + int y1 = pos1.getY(); + int z1 = pos1.getZ(); + int x2 = pos2.getX(); + int y2 = pos2.getY(); + int z2 = pos2.getZ(); + int tipx = x1; + int tipy = y1; + int tipz = z1; + int dx = Math.abs(x2 - x1); + int dy = Math.abs(y2 - y1); + int dz = Math.abs(z2 - z1); + + if (dx + dy + dz == 0) { + vset.add(new IrisPosition(tipx, tipy, tipz)); + continue; + } + + int dMax = Math.max(Math.max(dx, dy), dz); + if (dMax == dx) { + for (int domstep = 0; domstep <= dx; domstep++) { + tipx = x1 + domstep * (x2 - x1 > 0 ? 1 : -1); + tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dx) * (y2 - y1 > 0 ? 1 : -1)); + tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dx) * (z2 - z1 > 0 ? 1 : -1)); + + vset.add(new IrisPosition(tipx, tipy, tipz)); + } + } else if (dMax == dy) { + for (int domstep = 0; domstep <= dy; domstep++) { + tipy = y1 + domstep * (y2 - y1 > 0 ? 1 : -1); + tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dy) * (x2 - x1 > 0 ? 1 : -1)); + tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dy) * (z2 - z1 > 0 ? 1 : -1)); + + vset.add(new IrisPosition(tipx, tipy, tipz)); + } + } else /* if (dMax == dz) */ { + for (int domstep = 0; domstep <= dz; domstep++) { + tipz = z1 + domstep * (z2 - z1 > 0 ? 1 : -1); + tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dz) * (y2 - y1 > 0 ? 1 : -1)); + tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dz) * (x2 - x1 > 0 ? 1 : -1)); + + vset.add(new IrisPosition(tipx, tipy, tipz)); + } + } + } + + vset = getBallooned(vset, radius); + + if (!filled) { + vset = getHollowed(vset); + } + + set(vset, data); + } + + /** + * Set a cylinder in the mantle + * @param cx the center x + * @param cy the base y + * @param cz the center z + * @param data the data to set + * @param radius the radius + * @param height the height of the cyl + * @param filled filled or not + */ + public void setCylinder(int cx, int cy, int cz, T data, double radius, int height, boolean filled){ + setCylinder(cx, cy, cz, data, radius, radius, height, filled); + } + + /** + * Set a cylinder in the mantle + * @param cx the center x + * @param cy the base y + * @param cz the center z + * @param data the data to set + * @param radiusX the x radius + * @param radiusZ the z radius + * @param height the height of this cyl + * @param filled filled or hollow? + */ + public void setCylinder(int cx, int cy, int cz, T data, double radiusX, double radiusZ, int height, boolean filled) { + int affected = 0; + radiusX += 0.5; + radiusZ += 0.5; + + if (height == 0) { + return; + } else if (height < 0) { + height = -height; + cy = cy - height; + } + + if (cy < 0) { + cy = 0; + } else if (cy + height - 1 > worldHeight) { + height = worldHeight - cy + 1; + } + + final double invRadiusX = 1 / radiusX; + final double invRadiusZ = 1 / radiusZ; + final int ceilRadiusX = (int) Math.ceil(radiusX); + final int ceilRadiusZ = (int) Math.ceil(radiusZ); + double nextXn = 0; + + forX: for (int x = 0; x <= ceilRadiusX; ++x) { + final double xn = nextXn; + nextXn = (x + 1) * invRadiusX; + double nextZn = 0; + for (int z = 0; z <= ceilRadiusZ; ++z) { + final double zn = nextZn; + nextZn = (z + 1) * invRadiusZ; + double distanceSq = lengthSq(xn, zn); + + if (distanceSq > 1) { + if (z == 0) { + break forX; + } + + break; + } + + if (!filled) { + if (lengthSq(nextXn, zn) <= 1 && lengthSq(xn, nextZn) <= 1) { + continue; + } + } + + for (int y = 0; y < height; ++y) { + set(cx + x, cy + y, cz + z, data); + set(cx + -x, cy + y, cz + z, data); + set(cx + x, cy + y, cz + -z, data); + set(cx + -x, cy + y, cz + -z, data); + } + } + } + } + + + public void set(IrisPosition pos, T data) + { + set(pos.getX(), pos.getY(), pos.getZ(), data); + } + + public void set(List positions, T data) + { + for(IrisPosition i : positions) + { + set(i, data); + } + } + + public void set(Set positions, T data) + { + for(IrisPosition i : positions) + { + set(i, data); + } + } + + private static Set getBallooned(Set vset, double radius) { + Set returnset = new HashSet<>(); + int ceilrad = (int) Math.ceil(radius); + + for (IrisPosition v : vset) { + int tipx = v.getX(); + int tipy = v.getY(); + int tipz = v.getZ(); + + for (int loopx = tipx - ceilrad; loopx <= tipx + ceilrad; loopx++) { + for (int loopy = tipy - ceilrad; loopy <= tipy + ceilrad; loopy++) { + for (int loopz = tipz - ceilrad; loopz <= tipz + ceilrad; loopz++) { + if (hypot(loopx - tipx, loopy - tipy, loopz - tipz) <= radius) { + returnset.add(new IrisPosition(loopx, loopy, loopz)); + } + } + } + } + } + return returnset; + } + + private static Set getHollowed(Set vset) { + Set returnset = new KSet<>(); + for (IrisPosition v : vset) { + double x = v.getX(); + double y = v.getY(); + double z = v.getZ(); + if (!(vset.contains(new IrisPosition(x + 1, y, z)) + && vset.contains(new IrisPosition(x - 1, y, z)) + && vset.contains(new IrisPosition(x, y + 1, z)) + && vset.contains(new IrisPosition(x, y - 1, z)) + && vset.contains(new IrisPosition(x, y, z + 1)) + && vset.contains(new IrisPosition(x, y, z - 1)))) { + returnset.add(v); + } + } + return returnset; + } + + private static double hypot(double... pars) { + double sum = 0; + for (double d : pars) { + sum += Math.pow(d, 2); + } + return Math.sqrt(sum); + } + + private static double lengthSq(double x, double y, double z) { + return (x * x) + (y * y) + (z * z); + } + + private static double lengthSq(double x, double z) { + return (x * x) + (z * z); + } } diff --git a/src/main/java/com/volmit/iris/util/math/INode.java b/src/main/java/com/volmit/iris/util/math/INode.java new file mode 100644 index 000000000..cb831d5b0 --- /dev/null +++ b/src/main/java/com/volmit/iris/util/math/INode.java @@ -0,0 +1,47 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2021 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.volmit.iris.util.math; + +import lombok.Data; +import org.bukkit.util.Vector; + +@Data +public class INode { + + private Vector position; + private double tension; + private double bias; + private double continuity; + + public INode() { + this(new Vector(0,0,0)); + } + + public INode(INode other) { + this.position = other.position; + + this.tension = other.tension; + this.bias = other.bias; + this.continuity = other.continuity; + } + + public INode(Vector position) { + this.position = position; + } +} \ No newline at end of file diff --git a/src/main/java/com/volmit/iris/util/math/KochanekBartelsInterpolation.java b/src/main/java/com/volmit/iris/util/math/KochanekBartelsInterpolation.java new file mode 100644 index 000000000..75a01b205 --- /dev/null +++ b/src/main/java/com/volmit/iris/util/math/KochanekBartelsInterpolation.java @@ -0,0 +1,248 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2021 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.volmit.iris.util.math; + +import org.bukkit.util.Vector; + +import java.util.Collections; +import java.util.List; + +public class KochanekBartelsInterpolation implements PathInterpolation { + + private List nodes; + private Vector[] coeffA; + private Vector[] coeffB; + private Vector[] coeffC; + private Vector[] coeffD; + private double scaling; + + public KochanekBartelsInterpolation() { + setNodes(Collections.emptyList()); + } + + @Override + public void setNodes(List nodes) { + this.nodes = nodes; + recalc(); + } + + private void recalc() { + final int nNodes = nodes.size(); + coeffA = new Vector[nNodes]; + coeffB = new Vector[nNodes]; + coeffC = new Vector[nNodes]; + coeffD = new Vector[nNodes]; + + if (nNodes == 0) { + return; + } + + INode nodeB = nodes.get(0); + double tensionB = nodeB.getTension(); + double biasB = nodeB.getBias(); + double continuityB = nodeB.getContinuity(); + for (int i = 0; i < nNodes; ++i) { + final double tensionA = tensionB; + final double biasA = biasB; + final double continuityA = continuityB; + + if (i + 1 < nNodes) { + nodeB = nodes.get(i + 1); + tensionB = nodeB.getTension(); + biasB = nodeB.getBias(); + continuityB = nodeB.getContinuity(); + } + + // Kochanek-Bartels tangent coefficients + final double ta = (1 - tensionA) * (1 + biasA) * (1 + continuityA) / 2; // Factor for lhs of d[i] + final double tb = (1 - tensionA) * (1 - biasA) * (1 - continuityA) / 2; // Factor for rhs of d[i] + final double tc = (1 - tensionB) * (1 + biasB) * (1 - continuityB) / 2; // Factor for lhs of d[i+1] + final double td = (1 - tensionB) * (1 - biasB) * (1 + continuityB) / 2; // Factor for rhs of d[i+1] + + coeffA[i] = linearCombination(i, -ta, ta - tb - tc + 2, tb + tc - td - 2, td); + coeffB[i] = linearCombination(i, 2 * ta, -2 * ta + 2 * tb + tc - 3, -2 * tb - tc + td + 3, -td); + coeffC[i] = linearCombination(i, -ta, ta - tb, tb, 0); + //coeffD[i] = linearCombination(i, 0, 1, 0, 0); + coeffD[i] = retrieve(i); // this is an optimization + } + + scaling = nodes.size() - 1; + } + + /** + * Returns the linear combination of the given coefficients with the nodes adjacent to baseIndex. + * + * @param baseIndex node index + * @param f1 coefficient for baseIndex-1 + * @param f2 coefficient for baseIndex + * @param f3 coefficient for baseIndex+1 + * @param f4 coefficient for baseIndex+2 + * @return linear combination of nodes[n-1..n+2] with f1..4 + */ + private Vector linearCombination(int baseIndex, double f1, double f2, double f3, double f4) { + final Vector r1 = retrieve(baseIndex - 1).multiply(f1); + final Vector r2 = retrieve(baseIndex ).multiply(f2); + final Vector r3 = retrieve(baseIndex + 1).multiply(f3); + final Vector r4 = retrieve(baseIndex + 2).multiply(f4); + + return r1.add(r2).add(r3).add(r4); + } + + /** + * Retrieves a node. Indexes are clamped to the valid range. + * + * @param index node index to retrieve + * @return nodes[clamp(0, nodes.length-1)] + */ + private Vector retrieve(int index) { + if (index < 0) { + return fastRetrieve(0); + } + + if (index >= nodes.size()) { + return fastRetrieve(nodes.size() - 1); + } + + return fastRetrieve(index); + } + + private Vector fastRetrieve(int index) { + return nodes.get(index).getPosition(); + } + + @Override + public Vector getPosition(double position) { + if (coeffA == null) { + throw new IllegalStateException("Must call setNodes first."); + } + + if (position > 1) { + return null; + } + + position *= scaling; + + final int index = (int) Math.floor(position); + final double remainder = position - index; + + final Vector a = coeffA[index]; + final Vector b = coeffB[index]; + final Vector c = coeffC[index]; + final Vector d = coeffD[index]; + + return a.multiply(remainder).add(b).multiply(remainder).add(c).multiply(remainder).add(d); + } + + @Override + public Vector get1stDerivative(double position) { + if (coeffA == null) { + throw new IllegalStateException("Must call setNodes first."); + } + + if (position > 1) { + return null; + } + + position *= scaling; + + final int index = (int) Math.floor(position); + //final double remainder = position - index; + + final Vector a = coeffA[index]; + final Vector b = coeffB[index]; + final Vector c = coeffC[index]; + + return a.multiply(1.5 * position - 3.0 * index).add(b).multiply(2.0 * position).add(a.multiply(1.5 * index).subtract(b).multiply(2.0 * index)).add(c).multiply(scaling); + } + + @Override + public double arcLength(double positionA, double positionB) { + if (coeffA == null) { + throw new IllegalStateException("Must call setNodes first."); + } + + if (positionA > positionB) { + return arcLength(positionB, positionA); + } + + positionA *= scaling; + positionB *= scaling; + + final int indexA = (int) Math.floor(positionA); + final double remainderA = positionA - indexA; + + final int indexB = (int) Math.floor(positionB); + final double remainderB = positionB - indexB; + + return arcLengthRecursive(indexA, remainderA, indexB, remainderB); + } + + /** + * Assumes a < b. + */ + private double arcLengthRecursive(int indexLeft, double remainderLeft, int indexRight, double remainderRight) { + switch (indexRight - indexLeft) { + case 0: + return arcLengthRecursive(indexLeft, remainderLeft, remainderRight); + + case 1: + // This case is merely a speed-up for a very common case + return arcLengthRecursive(indexLeft, remainderLeft, 1.0) + + arcLengthRecursive(indexRight, 0.0, remainderRight); + + default: + return arcLengthRecursive(indexLeft, remainderLeft, indexRight - 1, 1.0) + + arcLengthRecursive(indexRight, 0.0, remainderRight); + } + } + + private double arcLengthRecursive(int index, double remainderLeft, double remainderRight) { + final Vector a = coeffA[index].multiply(3.0); + final Vector b = coeffB[index].multiply(2.0); + final Vector c = coeffC[index]; + + final int nPoints = 8; + + double accum = a.multiply(remainderLeft).add(b).multiply(remainderLeft).add(c).length() / 2.0; + for (int i = 1; i < nPoints - 1; ++i) { + double t = ((double) i) / nPoints; + t = (remainderRight - remainderLeft) * t + remainderLeft; + accum += a.multiply(t).add(b).multiply(t).add(c).length(); + } + + accum += a.multiply(remainderRight).add(b).multiply(remainderRight).add(c).length() / 2.0; + return accum * (remainderRight - remainderLeft) / nPoints; + } + + @Override + public int getSegment(double position) { + if (coeffA == null) { + throw new IllegalStateException("Must call setNodes first."); + } + + if (position > 1) { + return Integer.MAX_VALUE; + } + + position *= scaling; + + return (int) Math.floor(position); + } + +} \ No newline at end of file diff --git a/src/main/java/com/volmit/iris/util/math/PathInterpolation.java b/src/main/java/com/volmit/iris/util/math/PathInterpolation.java new file mode 100644 index 000000000..b352230bf --- /dev/null +++ b/src/main/java/com/volmit/iris/util/math/PathInterpolation.java @@ -0,0 +1,70 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2021 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.volmit.iris.util.math; + +import org.bukkit.util.Vector; + +import java.util.List; + +public interface PathInterpolation { + + /** + * Sets nodes to be used by subsequent calls to + * {@link #getPosition(double)} and the other methods. + * + * @param nodes the nodes + */ + void setNodes(List nodes); + + /** + * Gets the result of f(position). + * + * @param position the position to interpolate + * @return the result + */ + Vector getPosition(double position); + + /** + * Gets the result of f'(position). + * + * @param position the position to interpolate + * @return the result + */ + Vector get1stDerivative(double position); + + /** + * Gets the result of ∫ab|f'(t)| dt.
+ * That means it calculates the arc length (in meters) between positionA + * and positionB. + * + * @param positionA lower limit + * @param positionB upper limit + * @return the arc length + */ + double arcLength(double positionA, double positionB); + + /** + * Get the segment position. + * + * @param position the position + * @return the segment position + */ + int getSegment(double position); + +} \ No newline at end of file