From 5b96a52e999ed19b23ed9ab8ef6c419ac379f26e Mon Sep 17 00:00:00 2001 From: Jesse Boyd Date: Wed, 28 Sep 2016 03:14:05 +1000 Subject: [PATCH] Not finished yet (spline brush) --- .../main/java/com/boydti/fawe/config/BBC.java | 2 + .../fawe/object/brush/RecurseBrush.java | 32 ++- .../boydti/fawe/object/brush/SplineBrush.java | 187 ++++++++++++++++ .../object/visitor/DFSRecursiveVisitor.java | 43 ++++ .../fawe/object/visitor/DFSVisitor.java | 211 ++++++++++++++++++ .../java/com/sk89q/worldedit/EditSession.java | 4 +- .../worldedit/command/BrushCommands.java | 34 ++- .../function/visitor/BreadthFirstSearch.java | 5 +- 8 files changed, 501 insertions(+), 17 deletions(-) create mode 100644 core/src/main/java/com/boydti/fawe/object/brush/SplineBrush.java create mode 100644 core/src/main/java/com/boydti/fawe/object/visitor/DFSRecursiveVisitor.java create mode 100644 core/src/main/java/com/boydti/fawe/object/visitor/DFSVisitor.java diff --git a/core/src/main/java/com/boydti/fawe/config/BBC.java b/core/src/main/java/com/boydti/fawe/config/BBC.java index 102f12a9..1ccd5408 100644 --- a/core/src/main/java/com/boydti/fawe/config/BBC.java +++ b/core/src/main/java/com/boydti/fawe/config/BBC.java @@ -92,6 +92,8 @@ public enum BBC { BRUSH_HEIGHT_INVALID("Invalid height map file (%s0)", "WorldEdit.Brush"), BRUSH_SMOOTH("Smooth brush equipped (%s0 x %s1 using %s2).", "WorldEdit.Brush"), BRUSH_SPHERE("Sphere brush shape equipped (%s0).", "WorldEdit.Brush"), + BRUSH_LINE("Line brush shape equipped (%s0).", "WorldEdit.Brush"), + BRUSH_SPLINE("Line brush shape equipped (%s0). Right click to select points, left click to execute.", "WorldEdit.Brush"), BRUSH_BLEND_BALL("Blend ball brush equipped (%s0).", "WorldEdit.Brush"), BRUSH_ERODE("Erode brush equipped (%s0).", "WorldEdit.Brush"), BRUSH_PASTE_NONE("Nothing to paste", "WorldEdit.Brush"), diff --git a/core/src/main/java/com/boydti/fawe/object/brush/RecurseBrush.java b/core/src/main/java/com/boydti/fawe/object/brush/RecurseBrush.java index 1443d7e2..f5208da6 100644 --- a/core/src/main/java/com/boydti/fawe/object/brush/RecurseBrush.java +++ b/core/src/main/java/com/boydti/fawe/object/brush/RecurseBrush.java @@ -1,5 +1,7 @@ package com.boydti.fawe.object.brush; +import com.boydti.fawe.object.mask.RadiusMask; +import com.boydti.fawe.object.visitor.DFSRecursiveVisitor; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.Vector; @@ -16,9 +18,11 @@ import com.sk89q.worldedit.function.visitor.RecursiveVisitor; public class RecurseBrush implements Brush { private final BrushTool tool; + private final boolean dfs; - public RecurseBrush(BrushTool tool) { + public RecurseBrush(BrushTool tool, boolean dfs) { this.tool = tool; + this.dfs = dfs; } @Override @@ -34,8 +38,28 @@ public class RecurseBrush implements Brush { } final BlockReplace replace = new BlockReplace(editSession, to); editSession.setMask((Mask) null); - RecursiveVisitor visitor = new RecursiveVisitor(mask, replace, radius); - visitor.visit(position); - Operations.completeBlindly(visitor); + final int maxY = editSession.getMaxY(); + if (dfs) { + final Mask radMask = new RadiusMask(0, (int) size); + DFSRecursiveVisitor visitor = new DFSRecursiveVisitor(mask, replace, Integer.MAX_VALUE, Integer.MAX_VALUE) { + @Override + public boolean isVisitable(Vector from, Vector to) { + int y = to.getBlockY(); + return y >= y && y < maxY && radMask.test(to) && super.isVisitable(from, to); + } + }; + visitor.visit(position); + Operations.completeBlindly(visitor); + } else { + RecursiveVisitor visitor = new RecursiveVisitor(mask, replace, radius) { + @Override + public boolean isVisitable(Vector from, Vector to) { + int y = to.getBlockY(); + return y >= y && y < maxY && super.isVisitable(from, to); + } + }; + visitor.visit(position); + Operations.completeBlindly(visitor); + } } } diff --git a/core/src/main/java/com/boydti/fawe/object/brush/SplineBrush.java b/core/src/main/java/com/boydti/fawe/object/brush/SplineBrush.java new file mode 100644 index 00000000..ea48d819 --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/brush/SplineBrush.java @@ -0,0 +1,187 @@ +package com.boydti.fawe.object.brush; + +import com.boydti.fawe.config.BBC; +import com.boydti.fawe.object.exception.FaweException; +import com.boydti.fawe.object.mask.IdMask; +import com.boydti.fawe.object.visitor.DFSRecursiveVisitor; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.MaxChangedBlocksException; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.function.RegionFunction; +import com.sk89q.worldedit.function.mask.Mask; +import com.sk89q.worldedit.function.mask.MaskIntersection; +import com.sk89q.worldedit.function.operation.Operations; +import com.sk89q.worldedit.function.pattern.Pattern; +import com.sk89q.worldedit.function.pattern.Patterns; +import com.sk89q.worldedit.math.interpolation.Interpolation; +import com.sk89q.worldedit.math.interpolation.KochanekBartelsInterpolation; +import com.sk89q.worldedit.math.interpolation.Node; +import com.sk89q.worldedit.math.transform.AffineTransform; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class SplineBrush implements DoubleActionBrush { + + public static int MAX_POINTS = 15; + private ArrayList> positionSets; + private int numSplines; + + private final DoubleActionBrushTool tool; + private final LocalSession session; + private final Player player; + + public SplineBrush(Player player, LocalSession session, DoubleActionBrushTool tool) { + this.tool = tool; + this.session = session; + this.player = player; + this.positionSets = new ArrayList<>(); + } + + @Override + public void build(DoubleActionBrushTool.BrushAction action, EditSession editSession, final Vector position, Pattern pattern, double size) throws MaxChangedBlocksException { + Mask mask = tool.getMask(); + if (mask == null) { + mask = new IdMask(editSession); + } else { + mask = new MaskIntersection(mask, new IdMask(editSession)); + } + switch (action) { + case PRIMARY: { // Right + if (positionSets.size() >= MAX_POINTS) { + throw new FaweException(BBC.WORLDEDIT_CANCEL_REASON_MAX_CHECKS); + } + final ArrayList points = new ArrayList<>(); + DFSRecursiveVisitor visitor = new DFSRecursiveVisitor(mask, new RegionFunction() { + @Override + public boolean apply(Vector p) throws WorldEditException { + points.add(new Vector(p)); + return true; + } + }, (int) size, 1); + visitor.visit(position); + Operations.completeBlindly(visitor); + if (points.size() > numSplines) { + numSplines = points.size(); + } + this.positionSets.add(points); + player.print("Added position, right click to spline them together!"); + break; + } + case SECONDARY: { + if (positionSets.size() < 2) { + player.print("Not enough positions set!"); + return; + } + List centroids = new ArrayList<>(); + for (List points : positionSets) { + centroids.add(getCentroid(points)); + } + + double tension = 0; + double bias = 0; + double continuity = 0; + double quality = 10; + + final List nodes = new ArrayList(centroids.size()); + + final Interpolation interpol = new KochanekBartelsInterpolation(); + for (final Vector nodevector : centroids) { + final Node n = new Node(nodevector); + n.setTension(tension); + n.setBias(bias); + n.setContinuity(continuity); + nodes.add(n); + } + + Vector up = new Vector(0, 1, 0); + AffineTransform transform = new AffineTransform(); + + // TODO index offset based on transform + + int samples = numSplines; + for (int i = 0; i < numSplines; i++) { + List currentSpline = new ArrayList<>(); + for (ArrayList points : positionSets) { + int listSize = points.size(); + int index = (int) (i * listSize / (double) (numSplines)); + currentSpline.add(points.get(index)); + } + editSession.drawSpline(Patterns.wrap(pattern), currentSpline, 0, 0, 0, 10, 0, true); + } + player.print("Created spline"); + positionSets.clear(); + numSplines = 0; + break; + } + } + } + + private Vector getCentroid(Collection points) { + Vector sum = new Vector(); + for (Vector p : points) { + sum.x += p.x; + sum.y += p.y; + sum.z += p.z; + } + return sum.multiply(1.0 / points.size()); + } + + private Vector normal(Collection points, Vector centroid) { + int n = points.size(); + switch (n) { + case 1: { + return null; + } + case 2: { + return null; + } + } + + // Calc full 3x3 covariance matrix, excluding symmetries: + double xx = 0.0; double xy = 0.0; double xz = 0.0; + double yy = 0.0; double yz = 0.0; double zz = 0.0; + + Vector r = new Vector(); + for (Vector p : points) { + r.x = p.x - centroid.x; + r.y = p.y - centroid.y; + r.z = p.z - centroid.z; + xx += r.x * r.x; + xy += r.x * r.y; + xz += r.x * r.z; + yy += r.y * r.y; + yz += r.y * r.z; + zz += r.z * r.z; + } + + double det_x = yy*zz - yz*yz; + double det_y = xx*zz - xz*xz; + double det_z = xx*yy - xy*xy; + + double det_max = Math.max(Math.max(det_x, det_y), det_z); + if (det_max <= 0.0) { + return null; + } + + // Pick path with best conditioning: + Vector dir; + if (det_max == det_x) { + double a = (xz*yz - xy*zz) / det_x; + double b = (xy*yz - xz*yy) / det_x; + dir = new Vector(1.0, a, b); + } else if (det_max == det_y) { + double a = (yz*xz - xy*zz) / det_y; + double b = (xy*xz - yz*xx) / det_y; + dir = new Vector(a, 1.0, b); + } else { + double a = (yz*xy - xz*yy) / det_z; + double b = (xz*xy - yz*xx) / det_z; + dir = new Vector(a, b, 1.0); + }; + return dir.normalize(); + } +} diff --git a/core/src/main/java/com/boydti/fawe/object/visitor/DFSRecursiveVisitor.java b/core/src/main/java/com/boydti/fawe/object/visitor/DFSRecursiveVisitor.java new file mode 100644 index 00000000..98b6bacb --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/visitor/DFSRecursiveVisitor.java @@ -0,0 +1,43 @@ +package com.boydti.fawe.object.visitor; + +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.function.RegionFunction; +import com.sk89q.worldedit.function.mask.Mask; +import com.sk89q.worldedit.function.visitor.RecursiveVisitor; + + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * An implementation of an {@link com.sk89q.worldedit.function.visitor.BreadthFirstSearch} that uses a mask to + * determine where a block should be visited. + */ +public class DFSRecursiveVisitor extends DFSVisitor { + + private final Mask mask; + + public DFSRecursiveVisitor(final Mask mask, final RegionFunction function) { + this(mask, function, Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + /** + * Create a new recursive visitor. + * + * @param mask the mask + * @param function the function + */ + public DFSRecursiveVisitor(final Mask mask, final RegionFunction function, int maxDepth, int maxBranching) { + super(function, maxDepth, maxBranching); + checkNotNull(mask); + this.mask = mask; + } + + @Override + public boolean isVisitable(final Vector from, final Vector to) { + return this.mask.test(to); + } + + public static Class inject() { + return RecursiveVisitor.class; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/object/visitor/DFSVisitor.java b/core/src/main/java/com/boydti/fawe/object/visitor/DFSVisitor.java new file mode 100644 index 00000000..abaebef5 --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/visitor/DFSVisitor.java @@ -0,0 +1,211 @@ +package com.boydti.fawe.object.visitor; + +import com.boydti.fawe.config.BBC; +import com.boydti.fawe.object.IntegerTrio; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.function.RegionFunction; +import com.sk89q.worldedit.function.operation.Operation; +import com.sk89q.worldedit.function.operation.RunContext; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class DFSVisitor implements Operation { + + private final RegionFunction function; + private final List directions = new ArrayList<>(); + private final Map visited; + private final ArrayDeque queue; + private final HashSet hashQueue; + private final int maxDepth; + private final int maxBranch; + private int affected = 0; + + public DFSVisitor(final RegionFunction function) { + this(function, Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + public DFSVisitor(final RegionFunction function, int maxDepth, int maxBranching) { + this.queue = new ArrayDeque<>(); + this.hashQueue = new LinkedHashSet<>(); + this.visited = new LinkedHashMap<>(); + this.function = function; + this.directions.add(new Vector(0, -1, 0)); + this.directions.add(new Vector(0, 1, 0)); + this.directions.add(new Vector(-1, 0, 0)); + this.directions.add(new Vector(1, 0, 0)); + this.directions.add(new Vector(0, 0, -1)); + this.directions.add(new Vector(0, 0, 1)); + this.maxDepth = maxDepth; + this.maxBranch = maxBranching; + } + + public abstract boolean isVisitable(Vector from, Vector to); + + public Collection getDirections() { + return this.directions; + } + + private IntegerTrio[] getIntDirections() { + IntegerTrio[] array = new IntegerTrio[directions.size()]; + for (int i = 0; i < array.length; i++) { + Vector dir = directions.get(i); + array[i] = new IntegerTrio(dir.getBlockX(), dir.getBlockY(), dir.getBlockZ()); + } + return array; + } + + public void visit(final Vector pos) { + Node node = new Node((int) pos.x, (int) pos.y, (int) pos.z); + if (!this.hashQueue.contains(node)) { + isVisitable(pos, pos); // Ignore this, just to initialize mask on this point + queue.addFirst(new NodePair(null, node, 0)); + hashQueue.add(node); + } + } + + @Override + public Operation resume(RunContext run) throws WorldEditException { + NodePair current; + Node from; + Node adjacent; + Vector mutable = new Vector(); + Vector mutable2 = new Vector(); + int countAdd,countAttempt; + IntegerTrio[] dirs = getIntDirections(); + + for (int layer = 0; !queue.isEmpty(); layer++) { + current = queue.poll(); + from = current.to; + hashQueue.remove(from); + if (visited.containsKey(from)) { + continue; + } + mutable.x = from.getX(); + mutable.y = from.getY(); + mutable.z = from.getZ(); + function.apply(mutable); + countAdd = 0; + countAttempt = 0; + for (IntegerTrio direction : dirs) { + mutable2.x = from.getX() + direction.x; + mutable2.y = from.getY() + direction.y; + mutable2.z = from.getZ() + direction.z; + if (isVisitable(mutable, mutable2)) { + adjacent = new Node(mutable2.getBlockX(), mutable2.getBlockY(), mutable2.getBlockZ()); + if ((current.from == null || !adjacent.equals(current.from))) { + AtomicInteger adjacentCount = visited.get(adjacent); + if (adjacentCount == null) { + if (countAdd++ < maxBranch) { + if (!hashQueue.contains(adjacent)) { + if (current.depth == maxDepth) { + countAttempt++; + } else { + hashQueue.add(adjacent); + queue.addFirst(new NodePair(from, adjacent, current.depth + 1)); + } + } else { + countAttempt++; + } + } else { + countAttempt++; + } + } else if (adjacentCount.decrementAndGet() == 0) { + visited.remove(adjacent); + } else if (hashQueue.contains(adjacent)) { + countAttempt++; + } + } + } + } + if (countAttempt > 0) { + visited.put(from, new AtomicInteger(countAttempt)); + } + affected++; + } + return null; + } + + @Override + public void cancel() { + + } + + @Override + public void addStatusMessages(List messages) { + messages.add(BBC.VISITOR_BLOCK.format(getAffected())); + } + + public int getAffected() { + return this.affected; + } + + public class NodePair { + public final Node to; + public final Node from; + private final int depth; + + public NodePair(Node from, Node to, int depth) { + this.from = from; + this.to = to; + this.depth = depth; + } + } + + public static final class Node { + private int x,y,z; + + public Node(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + private final void set(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + private final void set(Node node) { + this.x = node.x; + this.y = node.y; + this.z = node.z; + } + + @Override + public final int hashCode() { + return (x ^ (z << 12)) ^ (y << 24); + } + + private final int getX() { + return x; + } + + private final int getY() { + return y; + } + + private final int getZ() { + return z; + } + + @Override + public String toString() { + return x + "," + y + "," + z; + } + + @Override + public boolean equals(Object obj) { + Node other = (Node) obj; + return other.x == x && other.z == z && other.y == y; + } + } +} diff --git a/core/src/main/java/com/sk89q/worldedit/EditSession.java b/core/src/main/java/com/sk89q/worldedit/EditSession.java index 9f2ec78a..e2bca8dc 100644 --- a/core/src/main/java/com/sk89q/worldedit/EditSession.java +++ b/core/src/main/java/com/sk89q/worldedit/EditSession.java @@ -102,7 +102,6 @@ import com.sk89q.worldedit.history.changeset.ChangeSet; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; import com.sk89q.worldedit.internal.expression.runtime.RValue; -import com.sk89q.worldedit.math.interpolation.Interpolation; import com.sk89q.worldedit.math.interpolation.KochanekBartelsInterpolation; import com.sk89q.worldedit.math.interpolation.Node; import com.sk89q.worldedit.math.noise.RandomNoise; @@ -2684,7 +2683,7 @@ public class EditSession extends AbstractWorld implements HasFaweQueue { Set vset = new HashSet(); final List nodes = new ArrayList(nodevectors.size()); - final Interpolation interpol = new KochanekBartelsInterpolation(); + final KochanekBartelsInterpolation interpol = new KochanekBartelsInterpolation(); for (final Vector nodevector : nodevectors) { final Node n = new Node(nodevector); @@ -2701,7 +2700,6 @@ public class EditSession extends AbstractWorld implements HasFaweQueue { final int tipx = (int) Math.round(tipv.getX()); final int tipy = (int) Math.round(tipv.getY()); final int tipz = (int) Math.round(tipv.getZ()); - vset.add(new Vector(tipx, tipy, tipz)); } diff --git a/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java b/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java index 9782bd4f..cdfb943c 100644 --- a/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java +++ b/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java @@ -24,14 +24,15 @@ import com.boydti.fawe.config.BBC; import com.boydti.fawe.config.Settings; import com.boydti.fawe.object.FaweLimit; import com.boydti.fawe.object.FawePlayer; +import com.boydti.fawe.object.brush.BlendBall; import com.boydti.fawe.object.brush.CommandBrush; import com.boydti.fawe.object.brush.CopyPastaBrush; -import com.boydti.fawe.object.brush.HeightBrush; -import com.boydti.fawe.object.brush.BlendBall; import com.boydti.fawe.object.brush.DoubleActionBrushTool; import com.boydti.fawe.object.brush.ErodeBrush; +import com.boydti.fawe.object.brush.HeightBrush; import com.boydti.fawe.object.brush.LineBrush; import com.boydti.fawe.object.brush.RecurseBrush; +import com.boydti.fawe.object.brush.SplineBrush; import com.boydti.fawe.object.mask.IdMask; import com.sk89q.minecraft.util.commands.Command; import com.sk89q.minecraft.util.commands.CommandContext; @@ -123,16 +124,17 @@ public class BrushCommands { aliases = { "recursive", "recurse", "r" }, usage = " [radius]", desc = "Choose the recursive brush", - help = "Chooses the recursive brush", + help = "Chooses the recursive brush\n" + + "The -d flag Will apply in depth first order", min = 0, - max = 2 + max = 3 ) @CommandPermissions("worldedit.brush.recursive") - public void recursiveBrush(Player player, LocalSession session, EditSession editSession, Pattern fill, @Optional("2") double radius) throws WorldEditException { + public void recursiveBrush(Player player, LocalSession session, EditSession editSession, Pattern fill, @Optional("2") double radius, @Switch('d') boolean depthFirst) throws WorldEditException { worldEdit.checkMaxBrushRadius(radius); BrushTool tool = session.getBrushTool(player.getItemInHand()); tool.setSize(radius); - tool.setBrush(new RecurseBrush(tool), "worldedit.brush.recursive"); + tool.setBrush(new RecurseBrush(tool, depthFirst), "worldedit.brush.recursive"); tool.setMask(new IdMask(editSession)); tool.setFill(fill); BBC.BRUSH_SPHERE.send(player, radius); @@ -158,7 +160,25 @@ public class BrushCommands { tool.setFill(fill); tool.setSize(radius); tool.setBrush(new LineBrush(shell, select, flat), "worldedit.brush.line"); - BBC.BRUSH_SPHERE.send(player, radius); + BBC.BRUSH_LINE.send(player, radius); + } + + @Command( + aliases = { "spline", "spl" }, + usage = "", + desc = "Choose the spline brush", + help = "Chooses the spline brush", + min = 0, + max = 2 + ) + @CommandPermissions("worldedit.brush.spline") + public void splineBrush(Player player, LocalSession session, EditSession editSession, Pattern fill, @Optional("25") double radius) throws WorldEditException { + worldEdit.checkMaxBrushRadius(radius); + DoubleActionBrushTool tool = session.getDoubleActionBrushTool(player.getItemInHand()); + tool.setFill(fill); + tool.setSize(radius); + tool.setBrush(new SplineBrush(player, session, tool), "worldedit.brush.spline"); + BBC.BRUSH_SPLINE.send(player, radius); } @Command( diff --git a/core/src/main/java/com/sk89q/worldedit/function/visitor/BreadthFirstSearch.java b/core/src/main/java/com/sk89q/worldedit/function/visitor/BreadthFirstSearch.java index 2863fda5..82bd186d 100644 --- a/core/src/main/java/com/sk89q/worldedit/function/visitor/BreadthFirstSearch.java +++ b/core/src/main/java/com/sk89q/worldedit/function/visitor/BreadthFirstSearch.java @@ -67,7 +67,6 @@ public abstract class BreadthFirstSearch implements Operation { @Override public Operation resume(RunContext run) throws WorldEditException { - Runtime runtime = Runtime.getRuntime(); Node from; Node adjacent; Vector mutable = new Vector(); @@ -143,7 +142,7 @@ public abstract class BreadthFirstSearch implements Operation { public Node(int x, int y, int z) { this.x = x; - this.y = (short) y; + this.y = y; this.z = z; } @@ -157,7 +156,7 @@ public abstract class BreadthFirstSearch implements Operation { private final void set(int x, int y, int z) { this.x = x; - this.y = (short) y; + this.y = y; this.z = z; }