diff --git a/src/main/java/com/sk89q/worldedit/EditSession.java b/src/main/java/com/sk89q/worldedit/EditSession.java index bfd8f5df5..6f893dd71 100644 --- a/src/main/java/com/sk89q/worldedit/EditSession.java +++ b/src/main/java/com/sk89q/worldedit/EditSession.java @@ -41,6 +41,9 @@ import com.sk89q.worldedit.blocks.BlockType; import com.sk89q.worldedit.expression.Expression; import com.sk89q.worldedit.expression.ExpressionException; import com.sk89q.worldedit.expression.runtime.RValue; +import com.sk89q.worldedit.interpolation.Interpolation; +import com.sk89q.worldedit.interpolation.KochanekBartelsInterpolation; +import com.sk89q.worldedit.interpolation.Node; import com.sk89q.worldedit.masks.Mask; import com.sk89q.worldedit.patterns.Pattern; import com.sk89q.worldedit.regions.CuboidRegion; @@ -2999,38 +3002,28 @@ public class EditSession { /** * Draws a line (out of blocks) between two vectors. * - * @param pattern The block pattern used to draw the line + * @param pattern The block pattern used to draw the line. * @param pos1 One of the points that define the line. * @param pos2 The other point that defines the line. - * @param radius The radius of the line. + * @param radius The radius (thickness) of the line. * @param filled If false, only a shell will be generated. * * @return number of blocks affected * @throws MaxChangedBlocksException */ public int drawLine(Pattern pattern, Vector pos1, Vector pos2, double radius, boolean filled) - throws MaxChangedBlocksException { + throws MaxChangedBlocksException { Set vset = new HashSet(); - int affected = 0; boolean notdrawn = true; - int ceilrad = (int) Math.ceil(radius); int x1 = pos1.getBlockX(), y1 = pos1.getBlockY(), z1 = pos1.getBlockZ(); int x2 = pos2.getBlockX(), y2 = pos2.getBlockY(), z2 = pos2.getBlockZ(); int tipx = x1, tipy = y1, tipz = z1; int dx = Math.abs(x2 - x1), dy = Math.abs(y2 - y1), dz = Math.abs(z2 - z1); if (dx + dy + dz == 0) { - 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) { - vset.add(new Vector(loopx, loopy, loopz)); - } - } - } - } + vset.add(new Vector(tipx, tipy, tipz)); notdrawn = false; } @@ -3040,15 +3033,7 @@ public class EditSession { 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)); - 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) { - vset.add(new Vector(loopx, loopy, loopz)); - } - } - } - } + vset.add(new Vector(tipx, tipy, tipz)); } notdrawn = false; } @@ -3059,15 +3044,7 @@ public class EditSession { 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)); - 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) { - vset.add(new Vector(loopx, loopy, loopz)); - } - } - } - } + vset.add(new Vector(tipx, tipy, tipz)); } notdrawn = false; } @@ -3078,19 +3055,61 @@ public class EditSession { 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)); - 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) { - vset.add(new Vector(loopx, loopy, loopz)); - } - } - } - } + vset.add(new Vector(tipx, tipy, tipz)); } notdrawn = false; } + vset = getBallooned(vset, radius); + if (!filled) { + vset = getHollowed(vset); + } + return setBlocks(vset, pattern); + } + + /** + * Draws a spline (out of blocks) between specified vectors. + * + * @param pattern The block pattern used to draw the spline. + * @param nodevectors The list of vectors to draw through. + * @param tension The tension of every node. + * @param bias The bias of every node. + * @param continuity The continuity of every node. + * @param quality The quality of the spline. Must be greater than 0. + * @param radius The radius (thickness) of the spline. + * @param filled If false, only a shell will be generated. + * + * @return number of blocks affected + * @throws MaxChangedBlocksException + */ + public int drawSpline(Pattern pattern, List nodevectors, double tension, double bias, double continuity, double quality, double radius, boolean filled) + throws MaxChangedBlocksException { + + Set vset = new HashSet(); + List nodes = new ArrayList(nodevectors.size()); + + Interpolation interpol = new KochanekBartelsInterpolation(); + + for (int loop = 0; loop < nodevectors.size(); loop++) { + Node n = new Node(nodevectors.get(loop)); + 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); + int tipx = (int) Math.round(tipv.getX()); + int tipy = (int) Math.round(tipv.getY()); + int tipz = (int) Math.round(tipv.getZ()); + + vset.add(new Vector(tipx, tipy, tipz)); + } + + vset = getBallooned(vset, radius); if (!filled) { vset = getHollowed(vset); } @@ -3105,6 +3124,26 @@ public class EditSession { return Math.sqrt(sum); } + private static Set getBallooned(Set vset, double radius) { + Set returnset = new HashSet(); + int ceilrad = (int) Math.ceil(radius); + + for (Vector v : vset) { + int tipx = v.getBlockX(), tipy = v.getBlockY(), tipz = v.getBlockZ(); + + 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 Vector(loopx, loopy, loopz)); + } + } + } + } + } + return returnset; + } + private static Set getHollowed(Set vset) { Set returnset = new HashSet(); for (Vector v : vset) { diff --git a/src/main/java/com/sk89q/worldedit/commands/RegionCommands.java b/src/main/java/com/sk89q/worldedit/commands/RegionCommands.java index effcf897f..62e7b863b 100644 --- a/src/main/java/com/sk89q/worldedit/commands/RegionCommands.java +++ b/src/main/java/com/sk89q/worldedit/commands/RegionCommands.java @@ -23,6 +23,8 @@ import static com.sk89q.minecraft.util.commands.Logging.LogMode.ALL; import static com.sk89q.minecraft.util.commands.Logging.LogMode.ORIENTATION_REGION; import static com.sk89q.minecraft.util.commands.Logging.LogMode.REGION; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import com.sk89q.minecraft.util.commands.Command; @@ -41,12 +43,13 @@ import com.sk89q.worldedit.blocks.BlockID; import com.sk89q.worldedit.expression.ExpressionException; import com.sk89q.worldedit.filtering.GaussianKernel; import com.sk89q.worldedit.filtering.HeightMapFilter; -import com.sk89q.worldedit.masks.Mask; -import com.sk89q.worldedit.patterns.Pattern; -import com.sk89q.worldedit.patterns.SingleBlockPattern; -import com.sk89q.worldedit.regions.CuboidRegion; -import com.sk89q.worldedit.regions.Region; -import com.sk89q.worldedit.regions.RegionOperationException; +import com.sk89q.worldedit.masks.Mask; +import com.sk89q.worldedit.patterns.Pattern; +import com.sk89q.worldedit.patterns.SingleBlockPattern; +import com.sk89q.worldedit.regions.ConvexPolyhedralRegion; +import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.regions.RegionOperationException; /** * Region related commands. @@ -89,9 +92,10 @@ public class RegionCommands { @Command( aliases = { "/line" }, usage = " [thickness]", - desc = "Draws a line segment between selection corners", + desc = "Draws a line segment between cuboid selection corners", help = - "Draws a line segment between selection corners.\n" + + "Draws a line segment between cuboid selection corners.\n" + + "Can only be used with cuboid selections.\n" + "Flags:\n" + " -h generates only a shell", flags = "h", @@ -121,6 +125,43 @@ public class RegionCommands { player.print(blocksChanged + " block(s) have been changed."); } + + @Command( + aliases = { "/curve" }, + usage = " [thickness]", + desc = "Draws a spline through selected points", + help = + "Draws a spline through selected points.\n" + + "Can only be uesd with convex polyhedral selections.\n" + + "Flags:\n" + + " -h generates only a shell", + flags = "h", + min = 1, + max = 2 + ) + @CommandPermissions("worldedit.region.curve") + @Logging(REGION) + public void curve(CommandContext args, LocalSession session, LocalPlayer player, + EditSession editSession) throws WorldEditException { + + Region region = session.getSelection(session.getSelectionWorld()); + if (!(region instanceof ConvexPolyhedralRegion)) { + player.printError("Invalid region type"); + return; + } + if (args.argsLength() < 2 ? false : args.getDouble(1) < 0) { + player.printError("Invalid thickness. Must not be negative"); + return; + } + + Pattern pattern = we.getBlockPattern(player, args.getString(0)); + ConvexPolyhedralRegion cpregion = (ConvexPolyhedralRegion) region; + List vectors = new ArrayList(cpregion.getVertices()); + + int blocksChanged = editSession.drawSpline(pattern, vectors, 0, 0, 0, 10, args.argsLength() < 2 ? 0 : args.getDouble(1), !args.hasFlag('h')); + + player.print(blocksChanged + " block(s) have been changed."); + } @Command( aliases = { "/replace", "/re", "/rep" }, diff --git a/src/main/java/com/sk89q/worldedit/interpolation/Interpolation.java b/src/main/java/com/sk89q/worldedit/interpolation/Interpolation.java new file mode 100644 index 000000000..62d1412e2 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/interpolation/Interpolation.java @@ -0,0 +1,68 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q and contributors + * + * 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.sk89q.worldedit.interpolation; + +import java.util.List; + +import com.sk89q.worldedit.Vector; + +/** + * Represents an arbitrary function in ℝ → ℝ3 + * + * @author TomyLobo + * + */ +public interface Interpolation { + /** + * Sets nodes to be used by subsequent calls to + * {@link #getPosition(double)} and the other methods. + * + * @param nodes + */ + public void setNodes(List nodes); + + /** + * Gets the result of f(position) + * + * @param position + * @return + */ + public Vector getPosition(double position); + + /** + * Gets the result of f'(position). + * + * @param position + * @return + */ + public 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 + */ + double arcLength(double positionA, double positionB); + + int getSegment(double position); +} diff --git a/src/main/java/com/sk89q/worldedit/interpolation/KochanekBartelsInterpolation.java b/src/main/java/com/sk89q/worldedit/interpolation/KochanekBartelsInterpolation.java new file mode 100644 index 000000000..72dd305d3 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/interpolation/KochanekBartelsInterpolation.java @@ -0,0 +1,253 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q and contributors + * + * 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.sk89q.worldedit.interpolation; + +import java.util.Collections; +import java.util.List; + +import com.sk89q.worldedit.Vector; + +/** + * Kochanek-Bartels interpolation.
+ * Continuous in the 2nd derivative.
+ * Supports {@link Node#tension tension}, {@link Node#bias bias} and + * {@link Node#continuity continuity} parameters per {@link Node} + * + * @author TomyLobo + * + */ +public class KochanekBartelsInterpolation implements Interpolation { + 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; + + Node 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 + * + * @param indexLeft + * @param remainderLeft + * @param indexRight + * @param remainderRight + * @return + */ + 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; + + final int index = (int) Math.floor(position); + return index; + } +} diff --git a/src/main/java/com/sk89q/worldedit/interpolation/LinearInterpolation.java b/src/main/java/com/sk89q/worldedit/interpolation/LinearInterpolation.java new file mode 100644 index 000000000..68fb2aaad --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/interpolation/LinearInterpolation.java @@ -0,0 +1,158 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q and contributors + * + * 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.sk89q.worldedit.interpolation; + +import java.util.List; + +import com.sk89q.worldedit.Vector; + +/** + * Simple linear interpolation. Mainly used for testing. + * + * @author TomyLobo + * + */ +public class LinearInterpolation implements Interpolation { + private List nodes; + + @Override + public void setNodes(List nodes) { + this.nodes = nodes; + } + + @Override + public Vector getPosition(double position) { + if (nodes == null) + throw new IllegalStateException("Must call setNodes first."); + + if (position > 1) + return null; + + position *= nodes.size() - 1; + + final int index1 = (int) Math.floor(position); + final double remainder = position - index1; + + final Vector position1 = nodes.get(index1).getPosition(); + final Vector position2 = nodes.get(index1 + 1).getPosition(); + + return position1.multiply(1.0 - remainder).add(position2.multiply(remainder)); + } + + /* + Formula for position: + p1*(1-t) + p2*t + Formula for position in Horner/monomial form: + (p2-p1)*t + p1 + 1st Derivative: + p2-p1 + 2nd Derivative: + 0 + Integral: + (p2-p1)/2*t^2 + p1*t + constant + Integral in Horner form: + ((p2-p1)/2*t + p1)*t + constant + */ + + @Override + public Vector get1stDerivative(double position) { + if (nodes == null) + throw new IllegalStateException("Must call setNodes first."); + + if (position > 1) + return null; + + position *= nodes.size() - 1; + + final int index1 = (int) Math.floor(position); + + final Vector position1 = nodes.get(index1).getPosition(); + final Vector position2 = nodes.get(index1 + 1).getPosition(); + + return position2.subtract(position1); + } + + @Override + public double arcLength(double positionA, double positionB) { + if (nodes == null) + throw new IllegalStateException("Must call setNodes first."); + + if (positionA > positionB) + return arcLength(positionB, positionA); + + positionA *= nodes.size() - 1; + positionB *= nodes.size() - 1; + + 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 + * + * @param indexA + * @param remainderA + * @param indexB + * @param remainderB + * @return + */ + private double arcLengthRecursive(int indexA, double remainderA, int indexB, double remainderB) { + switch (indexB - indexA) { + case 0: + return arcLengthRecursive(indexA, remainderA, remainderB); + + case 1: + // This case is merely a speed-up for a very common case + return + arcLengthRecursive(indexA, remainderA, 1.0) + + arcLengthRecursive(indexB, 0.0, remainderB); + + default: + return + arcLengthRecursive(indexA, remainderA, indexB - 1, 1.0) + + arcLengthRecursive(indexB, 0.0, remainderB); + } + } + + private double arcLengthRecursive(int index, double remainderA, double remainderB) { + final Vector position1 = nodes.get(index).getPosition(); + final Vector position2 = nodes.get(index + 1).getPosition(); + + return position1.distance(position2) * (remainderB - remainderA); + } + + @Override + public int getSegment(double position) { + if (nodes == null) + throw new IllegalStateException("Must call setNodes first."); + + if (position > 1) + return Integer.MAX_VALUE; + + position *= nodes.size() - 1; + + final int index = (int) Math.floor(position); + return index; + } +} diff --git a/src/main/java/com/sk89q/worldedit/interpolation/Node.java b/src/main/java/com/sk89q/worldedit/interpolation/Node.java new file mode 100644 index 000000000..6e1a53e18 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/interpolation/Node.java @@ -0,0 +1,86 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q and contributors + * + * 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.sk89q.worldedit.interpolation; + +import com.sk89q.worldedit.Vector; + +/** + * Represents a node for interpolation.
+ * The {@link #tension}, {@link #bias} and {@link #continuity} fields + * are parameters for the Kochanek-Bartels interpolation algorithm. + * + * @author TomyLobo + * + */ +public class Node { + private Vector position; + + private double tension; + private double bias; + private double continuity; + + public Node() { + this(new Vector(0, 0, 0)); + } + + public Node(Node other) { + this.position = other.position; + + this.tension = other.tension; + this.bias = other.bias; + this.continuity = other.continuity; + } + + public Node(Vector position) { + this.position = position; + } + + + public Vector getPosition() { + return position; + } + + public void setPosition(Vector position) { + this.position = position; + } + + public double getTension() { + return tension; + } + + public void setTension(double tension) { + this.tension = tension; + } + + public double getBias() { + return bias; + } + + public void setBias(double bias) { + this.bias = bias; + } + + public double getContinuity() { + return continuity; + } + + public void setContinuity(double continuity) { + this.continuity = continuity; + } +} diff --git a/src/main/java/com/sk89q/worldedit/interpolation/ReparametrisingInterpolation.java b/src/main/java/com/sk89q/worldedit/interpolation/ReparametrisingInterpolation.java new file mode 100644 index 000000000..047a38df1 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/interpolation/ReparametrisingInterpolation.java @@ -0,0 +1,148 @@ +// $Id$ +/* + * WorldEditLibrary + * Copyright (C) 2010 sk89q and contributors + * + * 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.sk89q.worldedit.interpolation; + +import java.util.List; +import java.util.Map.Entry; +import java.util.TreeMap; + +import com.sk89q.worldedit.Vector; + +/** + * Reparametrises another interpolation function by arc length.
+ * This is done so entities travel at roughly the same speed across + * the whole route. + * + * @author TomyLobo + * + */ +public class ReparametrisingInterpolation implements Interpolation { + private final Interpolation baseInterpolation; + private double totalArcLength; + private final TreeMap cache = new TreeMap(); + + public ReparametrisingInterpolation(Interpolation baseInterpolation) { + this.baseInterpolation = baseInterpolation; + } + + @Override + public void setNodes(List nodes) { + baseInterpolation.setNodes(nodes); + cache.clear(); + cache.put(0.0, 0.0); + cache.put(totalArcLength = baseInterpolation.arcLength(0.0, 1.0), 1.0); + } + + public Interpolation getBaseInterpolation() { + return baseInterpolation; + } + + @Override + public Vector getPosition(double position) { + if (position > 1) + return null; + + return baseInterpolation.getPosition(arcToParameter(position)); + } + + @Override + public Vector get1stDerivative(double position) { + if (position > 1) + return null; + + return baseInterpolation.get1stDerivative(arcToParameter(position)).normalize().multiply(totalArcLength); + } + + @Override + public double arcLength(double positionA, double positionB) { + return baseInterpolation.arcLength(arcToParameter(positionA), arcToParameter(positionB)); + } + + private double arcToParameter(double arc) { + if (cache.isEmpty()) + throw new IllegalStateException("Must call setNodes first."); + + if (arc > 1) arc = 1; + arc *= totalArcLength; + + Entry floorEntry = cache.floorEntry(arc); + final double leftArc = floorEntry.getKey(); + final double leftParameter = floorEntry.getValue(); + + if (leftArc == arc) { + return leftParameter; + } + + Entry ceilingEntry = cache.ceilingEntry(arc); + if (ceilingEntry == null) { + System.out.println("Error in arcToParameter: no ceiling entry for "+arc+" found!"); + return 0; + } + final double rightArc = ceilingEntry.getKey(); + final double rightParameter = ceilingEntry.getValue(); + + if (rightArc == arc) { + return rightParameter; + } + + return evaluate(arc, leftArc, leftParameter, rightArc, rightParameter); + } + + private double evaluate(double arc, double leftArc, double leftParameter, double rightArc, double rightParameter) { + double midParameter = 0; + for (int i = 0; i < 10; ++i) { + midParameter = (leftParameter + rightParameter) * 0.5; + //final double midArc = leftArc + baseInterpolation.arcLength(leftParameter, midParameter); + final double midArc = baseInterpolation.arcLength(0, midParameter); + cache.put(midArc, midParameter); + + if (midArc < leftArc) { + return leftParameter; + } + + if (midArc > rightArc) { + return rightParameter; + } + + if (Math.abs(midArc - arc) < 0.01) { + return midParameter; + } + + if (arc < midArc) { + // search between left and mid + rightArc = midArc; + rightParameter = midParameter; + } + else { + // search between mid and right + leftArc = midArc; + leftParameter = midParameter; + } + } + return midParameter; + } + + @Override + public int getSegment(double position) { + if (position > 1) + return Integer.MAX_VALUE; + + return baseInterpolation.getSegment(arcToParameter(position)); + } +}