diff --git a/bukkit0/src/main/java/com/boydti/fawe/bukkit/FaweBukkit.java b/bukkit0/src/main/java/com/boydti/fawe/bukkit/FaweBukkit.java index 110a7bc9..e31e4242 100644 --- a/bukkit0/src/main/java/com/boydti/fawe/bukkit/FaweBukkit.java +++ b/bukkit0/src/main/java/com/boydti/fawe/bukkit/FaweBukkit.java @@ -169,29 +169,31 @@ public class FaweBukkit implements IFawe, Listener { } try { return plugin.getQueue(world); - } catch (Throwable ignore) {} - // Disable incompatible settings - Settings.QUEUE.PARALLEL_THREADS = 1; // BukkitAPI placer is too slow to parallel thread at the chunk level - Settings.HISTORY.COMBINE_STAGES = false; // Performing a chunk copy (if possible) wouldn't be faster using the BukkitAPI - if (hasNMS) { - debug("====== NO NMS BLOCK PLACER FOUND ======"); - debug("FAWE couldn't find a fast block placer"); - debug("Bukkit version: " + Bukkit.getVersion()); - debug("NMS label: " + plugin.getClass().getSimpleName().split("_")[1]); - debug("Fallback placer: " + BukkitQueue_All.class); - debug("======================================="); - debug("Download the version of FAWE for your platform"); - debug(" - http://ci.athion.net/job/FastAsyncWorldEdit/lastSuccessfulBuild/artifact/target"); - debug("======================================="); - TaskManager.IMP.laterAsync(new Runnable() { - @Override - public void run() { - MainUtil.sendAdmin("&cNo NMS placer found, see console!"); - } - }, 1); - hasNMS = false; + } catch (Throwable ignore) { + // Disable incompatible settings + Settings.QUEUE.PARALLEL_THREADS = 1; // BukkitAPI placer is too slow to parallel thread at the chunk level + Settings.HISTORY.COMBINE_STAGES = false; // Performing a chunk copy (if possible) wouldn't be faster using the BukkitAPI + if (hasNMS) { + ignore.printStackTrace(); + debug("====== NO NMS BLOCK PLACER FOUND ======"); + debug("FAWE couldn't find a fast block placer"); + debug("Bukkit version: " + Bukkit.getVersion()); + debug("NMS label: " + plugin.getClass().getSimpleName().split("_")[1]); + debug("Fallback placer: " + BukkitQueue_All.class); + debug("======================================="); + debug("Download the version of FAWE for your platform"); + debug(" - http://ci.athion.net/job/FastAsyncWorldEdit/lastSuccessfulBuild/artifact/target"); + debug("======================================="); + TaskManager.IMP.laterAsync(new Runnable() { + @Override + public void run() { + MainUtil.sendAdmin("&cNo NMS placer found, see console!"); + } + }, 1); + hasNMS = false; + } + return new BukkitQueue_All(world); } - return new BukkitQueue_All(world); } /** diff --git a/core/src/main/java/com/boydti/fawe/Fawe.java b/core/src/main/java/com/boydti/fawe/Fawe.java index 5a19a642..593ec118 100644 --- a/core/src/main/java/com/boydti/fawe/Fawe.java +++ b/core/src/main/java/com/boydti/fawe/Fawe.java @@ -37,6 +37,7 @@ import com.sk89q.worldedit.command.ToolCommands; import com.sk89q.worldedit.command.ToolUtilCommands; import com.sk89q.worldedit.command.composition.SelectionCommand; import com.sk89q.worldedit.command.tool.AreaPickaxe; +import com.sk89q.worldedit.command.tool.BrushTool; import com.sk89q.worldedit.command.tool.LongRangeBuildTool; import com.sk89q.worldedit.command.tool.RecursivePickaxe; import com.sk89q.worldedit.command.tool.brush.GravityBrush; @@ -74,6 +75,7 @@ import com.sk89q.worldedit.function.visitor.RegionVisitor; import com.sk89q.worldedit.history.change.EntityCreate; import com.sk89q.worldedit.history.change.EntityRemove; import com.sk89q.worldedit.math.interpolation.KochanekBartelsInterpolation; +import com.sk89q.worldedit.math.transform.AffineTransform; import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.selector.CuboidRegionSelector; import com.sk89q.worldedit.session.SessionManager; @@ -356,11 +358,12 @@ public class Fawe { SchematicReader.inject(); SchematicWriter.inject(); ClipboardFormat.inject(); - // Brushes + // Brushes/Tools GravityBrush.inject(); // Fix for instant placement assumption LongRangeBuildTool.inject(); AreaPickaxe.inject(); // Fixes RecursivePickaxe.inject(); // Fixes + BrushTool.inject(); // Add transform // Selectors CuboidRegionSelector.inject(); // Translations // Visitors @@ -412,6 +415,7 @@ public class Fawe { NBTOutputStream.inject(); // New methods // Math KochanekBartelsInterpolation.inject(); // Optimizations + AffineTransform.inject(); // Optimizations try { CommandManager.inject(); // Async commands PlatformManager.inject(); // Async brushes / tools diff --git a/core/src/main/java/com/boydti/fawe/FaweCache.java b/core/src/main/java/com/boydti/fawe/FaweCache.java index 1d2c05d1..10511aa5 100644 --- a/core/src/main/java/com/boydti/fawe/FaweCache.java +++ b/core/src/main/java/com/boydti/fawe/FaweCache.java @@ -63,14 +63,14 @@ public class FaweCache { * Immutable BaseBlock cache * [ combined ] => block */ - public final static BaseBlock[] CACHE_BLOCK = new BaseBlock[Short.MAX_VALUE]; + public final static BaseBlock[] CACHE_BLOCK = new BaseBlock[Character.MAX_VALUE + 1]; /** * Faster than java random (since it just needs to look random) */ public final static PseudoRandom RANDOM = new PseudoRandom(); - public final static Color[] CACHE_COLOR = new Color[Short.MAX_VALUE]; + public final static Color[] CACHE_COLOR = new Color[Character.MAX_VALUE + 1]; /** * Get the cached BaseBlock object for an id/data
@@ -143,7 +143,7 @@ public class FaweCache { CACHE_DATA[i] = (byte) k; } - for (int i = 0; i < Short.MAX_VALUE; i++) { + for (int i = 0; i < Character.MAX_VALUE; i++) { int id = i >> 4; int data = i & 0xf; CACHE_BLOCK[i] = new BaseBlock(id, data) { 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 75e9d27d..86dcb960 100644 --- a/core/src/main/java/com/boydti/fawe/config/BBC.java +++ b/core/src/main/java/com/boydti/fawe/config/BBC.java @@ -47,7 +47,10 @@ public enum BBC { GENERATING_LINK_FAILED("&cFailed to generate download link!", "Web"), DOWNLOAD_LINK("%s", "Web"), - + MASK_DISABLED("Global mask disabled", "WorldEdit.General"), + MASK("Global mask set", "WorldEdit.General"), + TRANSFORM_DISABLED("Global transform disabled", "WorldEdit.General"), + TRANSFORM("Global transform set", "WorldEdit.General"), COMMAND_COPY("%s0 blocks were copied", "WorldEdit.Copy"), COMMAND_CUT("%s0 blocks were cut", "WorldEdit.Cut"), @@ -104,6 +107,8 @@ public enum BBC { BRUSH_RANGE("Brush size set", "WorldEdit.Brush"), BRUSH_MASK_DISABLED("Brush mask disabled", "WorldEdit.Brush"), BRUSH_MASK("Brush mask set", "WorldEdit.Brush"), + BRUSH_TRANSFORM_DISABLED("Brush transform disabled", "WorldEdit.Brush"), + BRUSH_TRANSFORM("Brush transform set", "WorldEdit.Brush"), BRUSH_MATERIAL("Brush material set", "WorldEdit.Brush"), ROLLBACK_ELEMENT("Undoing %s0", "WorldEdit.Rollback"), diff --git a/core/src/main/java/com/boydti/fawe/object/brush/CommandBrush.java b/core/src/main/java/com/boydti/fawe/object/brush/CommandBrush.java index a516e745..1c107c3a 100644 --- a/core/src/main/java/com/boydti/fawe/object/brush/CommandBrush.java +++ b/core/src/main/java/com/boydti/fawe/object/brush/CommandBrush.java @@ -8,6 +8,7 @@ import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.Vector; import com.sk89q.worldedit.WorldVectorFace; +import com.sk89q.worldedit.command.tool.BrushTool; import com.sk89q.worldedit.command.tool.brush.Brush; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.event.platform.CommandEvent; @@ -20,11 +21,13 @@ public class CommandBrush implements Brush { private final String command; private final Player player; private final int radius; + private final BrushTool tool; - public CommandBrush(Player player, String command, double radius) { + public CommandBrush(Player player, BrushTool tool, String command, double radius) { this.player = player; this.command = command; this.radius = (int) radius; + this.tool = tool; } @Override @@ -51,4 +54,4 @@ public class CommandBrush implements Brush { CommandManager.getInstance().handleCommand(event); } } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/object/brush/DoubleActionBrushTool.java b/core/src/main/java/com/boydti/fawe/object/brush/DoubleActionBrushTool.java index c2aa8b02..465ae125 100644 --- a/core/src/main/java/com/boydti/fawe/object/brush/DoubleActionBrushTool.java +++ b/core/src/main/java/com/boydti/fawe/object/brush/DoubleActionBrushTool.java @@ -1,5 +1,6 @@ package com.boydti.fawe.object.brush; +import com.boydti.fawe.object.extent.TransformExtent; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.LocalSession; @@ -29,6 +30,7 @@ public class DoubleActionBrushTool implements DoubleActionTraceTool { protected static int MAX_RANGE = 500; protected int range = -1; private Mask mask = null; + private TransformExtent transform = null; private DoubleActionBrush brush = null; @Nullable private Pattern material; @@ -50,6 +52,14 @@ public class DoubleActionBrushTool implements DoubleActionTraceTool { return player.hasPermission(permission); } + public TransformExtent getTransform() { + return transform; + } + + public void setTransform(TransformExtent transform) { + this.transform = transform; + } + /** * Get the filter. * @@ -168,7 +178,9 @@ public class DoubleActionBrushTool implements DoubleActionTraceTool { editSession.setMask(newMask); } } - + if (transform != null) { + editSession.addTransform(transform); + } try { brush.build(action, editSession, target, material, size); } catch (MaxChangedBlocksException e) { diff --git a/core/src/main/java/com/boydti/fawe/object/extent/AffineTransformExtent.java b/core/src/main/java/com/boydti/fawe/object/extent/AffineTransformExtent.java new file mode 100644 index 00000000..1c2d5caa --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/extent/AffineTransformExtent.java @@ -0,0 +1,138 @@ +package com.boydti.fawe.object.extent; + +import com.boydti.fawe.FaweCache; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.Vector2D; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.extent.transform.BlockTransformExtent; +import com.sk89q.worldedit.math.transform.AffineTransform; +import com.sk89q.worldedit.math.transform.Transform; +import com.sk89q.worldedit.world.biome.BaseBiome; +import com.sk89q.worldedit.world.registry.BlockRegistry; + +public class AffineTransformExtent extends TransformExtent { + private final Vector mutable = new Vector(); + private final BlockRegistry registry; + private int maxy; + private AffineTransform affine; + private BaseBlock[] BLOCK_TRANSFORM; + private BaseBlock[] BLOCK_TRANSFORM_INVERSE; + + private Vector min; + + public AffineTransformExtent(Extent parent, BlockRegistry registry) { + super(parent); + this.maxy = parent.getMaximumPoint().getBlockY(); + this.affine = new AffineTransform(); + this.registry = registry; + } + + private void cache() { + BLOCK_TRANSFORM = new BaseBlock[FaweCache.CACHE_BLOCK.length]; + BLOCK_TRANSFORM_INVERSE = new BaseBlock[FaweCache.CACHE_BLOCK.length]; + Transform inverse = affine.inverse(); + for (int i = 0; i < BLOCK_TRANSFORM.length; i++) { + BaseBlock block = FaweCache.CACHE_BLOCK[i]; + if (block != null) { + BLOCK_TRANSFORM[i] = BlockTransformExtent.transform(new BaseBlock(block), affine, registry); + BLOCK_TRANSFORM_INVERSE[i] = BlockTransformExtent.transform(new BaseBlock(block), inverse, registry); + } + } + } + + @Override + public TransformExtent setExtent(Extent extent) { + min = null; + maxy = extent.getMaximumPoint().getBlockY(); + return super.setExtent(extent); + } + + public AffineTransform getAffine() { + return affine; + } + + public void setAffine(AffineTransform affine) { + this.affine = affine; + cache(); + } + + private Vector getPos(Vector pos) { + if (min == null) { + min = new Vector(pos); + } + mutable.x = (pos.x - min.x); + mutable.y = (pos.y - min.y); + mutable.z = (pos.z - min.z); + Vector tmp = affine.apply(mutable); + tmp.x += min.x; + tmp.y += min.y; + tmp.z += min.z; + return tmp; + } + + private Vector getPos(int x, int y, int z) { + if (min == null) { + min = new Vector(x, y, z); + } + mutable.x = (x - min.x); + mutable.y = (y - min.y); + mutable.z = (z - min.z); + Vector tmp = affine.apply(mutable); + tmp.x += min.x; + tmp.y += min.y; + tmp.z += min.z; + return tmp; + } + + private final BaseBlock transformFast(BaseBlock block) { + return BLOCK_TRANSFORM[FaweCache.getCombined(block)]; + } + + private final BaseBlock transformFastInverse(BaseBlock block) { + return BLOCK_TRANSFORM_INVERSE[FaweCache.getCombined(block)]; + } + + @Override + public BaseBlock getLazyBlock(int x, int y, int z) { + return transformFast(super.getLazyBlock(getPos(x, y, z))); + } + + @Override + public BaseBlock getLazyBlock(Vector position) { + return transformFast(super.getLazyBlock(getPos(position))); + } + + @Override + public BaseBlock getBlock(Vector position) { + return transformFast(super.getBlock(getPos(position))); + } + + @Override + public BaseBiome getBiome(Vector2D position) { + mutable.x = position.getBlockX(); + mutable.z = position.getBlockZ(); + mutable.y = 0; + return super.getBiome(getPos(mutable).toVector2D()); + } + + @Override + public boolean setBlock(int x, int y, int z, BaseBlock block) throws WorldEditException { + return super.setBlock(getPos(x, y, z), transformFastInverse(block)); + } + + + @Override + public boolean setBlock(Vector location, BaseBlock block) throws WorldEditException { + return super.setBlock(getPos(location), transformFastInverse(block)); + } + + @Override + public boolean setBiome(Vector2D position, BaseBiome biome) { + mutable.x = position.getBlockX(); + mutable.z = position.getBlockZ(); + mutable.y = 0; + return super.setBiome(getPos(mutable).toVector2D(), biome); + } +} diff --git a/core/src/main/java/com/boydti/fawe/object/extent/DefaultTransformParser.java b/core/src/main/java/com/boydti/fawe/object/extent/DefaultTransformParser.java new file mode 100644 index 00000000..bc540899 --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/extent/DefaultTransformParser.java @@ -0,0 +1,125 @@ +package com.boydti.fawe.object.extent; + +import com.boydti.fawe.object.mask.CustomMask; +import com.boydti.fawe.util.ExtentTraverser; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.factory.DefaultMaskParser; +import com.sk89q.worldedit.extension.input.InputParseException; +import com.sk89q.worldedit.extension.input.NoMatchException; +import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.extent.NullExtent; +import com.sk89q.worldedit.function.pattern.Pattern; +import com.sk89q.worldedit.internal.expression.Expression; +import com.sk89q.worldedit.internal.expression.ExpressionException; +import com.sk89q.worldedit.internal.registry.InputParser; +import com.sk89q.worldedit.math.transform.AffineTransform; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Parses mask input strings. + */ +public class DefaultTransformParser extends InputParser { + + public DefaultTransformParser(WorldEdit worldEdit) { + super(worldEdit); + } + + private static CustomMask[] customMasks = new CustomMask[0]; + + public void addMask(CustomMask mask) { + checkNotNull(mask); + List list = new ArrayList<>(Arrays.asList(customMasks)); + list.add(mask); + customMasks = list.toArray(new CustomMask[list.size()]); + } + + public List getCustomMasks() { + return Arrays.asList(customMasks); + } + + @Override + public TransformExtent parseFromInput(String input, ParserContext context) throws InputParseException { + Extent extent = new NullExtent(); + for (String component : input.split(" ")) { + if (component.isEmpty()) { + continue; + } + extent = getTansformComponent(extent, component, context); + } + if (extent instanceof TransformExtent) { + return (TransformExtent) extent; + } + return null; + } + + private TransformExtent getTansformComponent(Extent parent, String component, ParserContext context) throws InputParseException { + final char firstChar = component.charAt(0); + switch (firstChar) { + case '#': + int colon = component.indexOf(':'); + if (colon != -1) { + String rest = component.substring(colon + 1); + switch (component.substring(0, colon).toLowerCase()) { + case "#pattern": { + Pattern pattern = worldEdit.getPatternFactory().parseFromInput(rest, context); + return new PatternTransform(parent, pattern); + } + case "#scale": { + try { + String[] split2 = component.split(":"); + double x = Math.abs(Expression.compile(split2[1]).evaluate()); + double y = Math.abs(Expression.compile(split2[2]).evaluate()); + double z = Math.abs(Expression.compile(split2[3]).evaluate()); + rest = rest.substring(Math.min(rest.length(), split2[1].length() + split2[2].length() + split2[3].length() + 3)); + if (!rest.isEmpty()) { + parent = parseFromInput(rest, context); + } + return new ScaleTransform(parent, x, y, z); + } catch (NumberFormatException | ExpressionException e) { + throw new InputParseException("The correct format is #scale:::"); + } + } + case "#rotate": { + try { + String[] split2 = component.split(":"); + double x = (Expression.compile(split2[1]).evaluate()); + double y = (Expression.compile(split2[2]).evaluate()); + double z = (Expression.compile(split2[3]).evaluate()); + rest = rest.substring(Math.min(rest.length(), split2[1].length() + split2[2].length() + split2[3].length() + 3)); + if (!rest.isEmpty()) { + parent = parseFromInput(rest, context); + } + ExtentTraverser traverser = new ExtentTraverser(parent).find(AffineTransformExtent.class); + AffineTransformExtent affine = (AffineTransformExtent) (traverser != null ? traverser.get() : null); + if (affine == null) { + parent = affine = new AffineTransformExtent(parent, context.requireWorld().getWorldData().getBlockRegistry()); + } + AffineTransform transform = affine.getAffine(); + transform = transform.rotateY(x); + transform = transform.rotateX(y); + transform = transform.rotateZ(z); + affine.setAffine(transform); + return (TransformExtent) parent; + } catch (NumberFormatException | ExpressionException e) { + throw new InputParseException("The correct format is #scale:::"); + } + } + default: + throw new NoMatchException("Unrecognized transform '" + component + "'"); + } + } + default: + throw new NoMatchException("Unrecognized transform '" + component + "'"); + } + } + + public static Class inject() { + return DefaultMaskParser.class; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/object/extent/PatternTransform.java b/core/src/main/java/com/boydti/fawe/object/extent/PatternTransform.java new file mode 100644 index 00000000..9e071244 --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/extent/PatternTransform.java @@ -0,0 +1,21 @@ +package com.boydti.fawe.object.extent; + +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.function.pattern.Pattern; + +public class PatternTransform extends TransformExtent { + private final Pattern pattern; + + public PatternTransform(Extent parent, Pattern pattern) { + super(parent); + this.pattern = pattern; + } + + @Override + public boolean setBlock(Vector location, BaseBlock block) throws WorldEditException { + return super.setBlock(location, pattern.apply(location)); + } +} diff --git a/core/src/main/java/com/boydti/fawe/object/extent/ScaleTransform.java b/core/src/main/java/com/boydti/fawe/object/extent/ScaleTransform.java new file mode 100644 index 00000000..3f8d5f7e --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/extent/ScaleTransform.java @@ -0,0 +1,121 @@ +package com.boydti.fawe.object.extent; + +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.Vector2D; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.world.biome.BaseBiome; +import javax.annotation.Nullable; + +public class ScaleTransform extends TransformExtent { + private final Vector mutable = new Vector(); + private final double dx,dy,dz; + private int maxy; + + private Vector min; + + public ScaleTransform(Extent parent, double dx, double dy, double dz) { + super(parent); + this.dx = dx; + this.dy = dy; + this.dz = dz; + this.maxy = parent.getMaximumPoint().getBlockY(); + } + + @Override + public TransformExtent setExtent(Extent extent) { + min = null; + maxy = extent.getMaximumPoint().getBlockY(); + return super.setExtent(extent); + } + + private Vector getPos(Vector pos) { + if (min == null) { + min = new Vector(pos); + } + mutable.x = min.x + (pos.x - min.x) * dx; + mutable.y = min.y + (pos.y - min.y) * dy; + mutable.z = min.z + (pos.z - min.z) * dz; + return mutable; + } + + private Vector getPos(int x, int y, int z) { + if (min == null) { + min = new Vector(x, y, z); + } + mutable.x = min.x + (x - min.x) * dx; + mutable.y = min.y + (y - min.y) * dy; + mutable.z = min.z + (z - min.z) * dz; + return mutable; + } + + + @Override + public boolean setBlock(Vector location, BaseBlock block) throws WorldEditException { + boolean result = false; + Vector pos = getPos(location); + double sx = pos.x; + double sy = pos.y; + double sz = pos.z; + double ex = sx + dx; + double ey = Math.min(maxy, sy + dy); + double ez = sz + dz; + for (pos.y = sy; pos.y < ey; pos.y++) { + for (pos.z = sz; pos.z < ez; pos.z++) { + for (pos.x = sx; pos.x < ex; pos.x++) { + result |= super.setBlock(pos, block); + } + } + } + return result; + } + + @Override + public boolean setBiome(Vector2D position, BaseBiome biome) { + boolean result = false; + Vector pos = getPos(position.getBlockX(), 0, position.getBlockZ()); + double sx = pos.x; + double sz = pos.z; + double ex = pos.x + dx; + double ez = pos.z + dz; + for (pos.z = sz; pos.z < ez; pos.z++) { + for (pos.x = sx; pos.x < ex; pos.x++) { + result |= super.setBiome(pos.toVector2D(), biome); + } + } + return result; + } + + @Override + public boolean setBlock(int x1, int y1, int z1, BaseBlock block) throws WorldEditException { + boolean result = false; + Vector pos = getPos(x1, y1, z1); + double sx = pos.x; + double sy = pos.y; + double sz = pos.z; + double ex = pos.x + dx; + double ey = Math.min(maxy, sy + dy); + double ez = pos.z + dz; + for (pos.y = sy; pos.y < ey; pos.y++) { + for (pos.z = sz; pos.z < ez; pos.z++) { + for (pos.x = sx; pos.x < ex; pos.x++) { + result |= super.setBlock(pos, block); + } + } + } + return result; + } + + + + @Nullable + @Override + public Entity createEntity(Location location, BaseEntity entity) { + Location newLoc = new Location(location.getExtent(), getPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()), location.getYaw(), location.getPitch()); + return super.createEntity(newLoc, entity); + } +} diff --git a/core/src/main/java/com/boydti/fawe/object/extent/TransformExtent.java b/core/src/main/java/com/boydti/fawe/object/extent/TransformExtent.java new file mode 100644 index 00000000..c3e01a97 --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/extent/TransformExtent.java @@ -0,0 +1,24 @@ +package com.boydti.fawe.object.extent; + +import com.boydti.fawe.util.ExtentTraverser; +import com.sk89q.worldedit.extent.AbstractDelegateExtent; +import com.sk89q.worldedit.extent.Extent; + + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TransformExtent extends AbstractDelegateExtent { + public TransformExtent(Extent parent) { + super(parent); + } + + public TransformExtent setExtent(Extent extent) { + checkNotNull(extent); + if (getExtent() instanceof TransformExtent) { + ((TransformExtent) getExtent()).setExtent(extent); + } else { + new ExtentTraverser(this).setNext(extent); + } + return this; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/object/mask/WallMask.java b/core/src/main/java/com/boydti/fawe/object/mask/WallMask.java new file mode 100644 index 00000000..892595f5 --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/mask/WallMask.java @@ -0,0 +1,36 @@ +package com.boydti.fawe.object.mask; + +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.function.mask.BlockMask; +import java.util.Collection; + +public class WallMask extends BlockMask { + private final int min, max; + + public WallMask(Extent extent, Collection blocks, int requiredMin, int requiredMax) { + super(extent, blocks); + this.min = requiredMin; + this.max = requiredMax; + } + + @Override + public boolean test(Vector v) { + int count = 0; + double x = v.x; + double y = v.y; + double z = v.z; + v.x = x + 1; + if (super.test(v) && ++count == min && max >= 8) { v.x = x; return true; } + v.x = x - 1; + if (super.test(v) && ++count == min && max >= 8) { v.x = x; return true; } + v.x = x; + v.z = z + 1; + if (super.test(v) && ++count == min && max >= 8) { v.z = z; return true; } + v.z = z - 1; + if (super.test(v) && ++count == min && max >= 8) { v.z = z; return true; } + v.z = z; + return count >= min && count <= max; + } +} diff --git a/core/src/main/java/com/boydti/fawe/object/pattern/ExpressionPattern.java b/core/src/main/java/com/boydti/fawe/object/pattern/ExpressionPattern.java new file mode 100644 index 00000000..72dddf02 --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/pattern/ExpressionPattern.java @@ -0,0 +1,59 @@ +package com.boydti.fawe.object.pattern; + +import com.boydti.fawe.FaweCache; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.function.pattern.AbstractPattern; +import com.sk89q.worldedit.internal.expression.Expression; +import com.sk89q.worldedit.internal.expression.ExpressionException; +import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; +import com.sk89q.worldedit.regions.shape.WorldEditExpressionEnvironment; + + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A mask that evaluates an expression. + * + *

Expressions are evaluated as {@code true} if they return a value + * greater than {@code 0}.

+ */ +public class ExpressionPattern extends AbstractPattern { + + private final Expression expression; + + /** + * Create a new instance. + * + * @param expression the expression + * @throws ExpressionException thrown if there is an error with the expression + */ + public ExpressionPattern(String expression) throws ExpressionException { + checkNotNull(expression); + this.expression = Expression.compile(expression, "x", "y", "z"); + } + + /** + * Create a new instance. + * + * @param expression the expression + */ + public ExpressionPattern(Expression expression) { + checkNotNull(expression); + this.expression = expression; + } + + @Override + public BaseBlock apply(Vector vector) { + try { + if (expression.getEnvironment() instanceof WorldEditExpressionEnvironment) { + ((WorldEditExpressionEnvironment) expression.getEnvironment()).setCurrentBlock(vector); + } + double combined = expression.evaluate(vector.getX(), vector.getY(), vector.getZ()); + return FaweCache.CACHE_BLOCK[(char) combined]; + } catch (EvaluationException e) { + return EditSession.nullBlock; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/object/pattern/SolidRandomOffsetPattern.java b/core/src/main/java/com/boydti/fawe/object/pattern/SolidRandomOffsetPattern.java new file mode 100644 index 00000000..71f03dbc --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/pattern/SolidRandomOffsetPattern.java @@ -0,0 +1,48 @@ +package com.boydti.fawe.object.pattern; + +import com.boydti.fawe.FaweCache; +import com.boydti.fawe.object.PseudoRandom; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.blocks.BlockType; +import com.sk89q.worldedit.function.pattern.AbstractPattern; +import com.sk89q.worldedit.function.pattern.Pattern; + +public class SolidRandomOffsetPattern extends AbstractPattern { + private final PseudoRandom r = new PseudoRandom(); + private final int dx, dy, dz, dx2, dy2, dz2; + private final Pattern pattern; + private final Vector mutable = new Vector(); + boolean[] solid; + + public SolidRandomOffsetPattern(Pattern pattern, int dx, int dy, int dz) { + this.pattern = pattern; + this.dx = dx; + this.dy = dy; + this.dz = dz; + this.dx2 = dx * 2 + 1; + this.dy2 = dy * 2 + 1; + this.dz2 = dz * 2 + 1; + solid = new boolean[Character.MAX_VALUE + 1]; + for (int id = 0; id < 4096; id++) { + for (int data = 0; data < 16; data++) { + if (!BlockType.canPassThrough(id, data)) { + solid[FaweCache.getCombined(id, data)] = true; + } + } + } + } + + @Override + public BaseBlock apply(Vector position) { + mutable.x = position.x + r.nextInt(dx2) - dx; + mutable.y = position.y + r.nextInt(dy2) - dy; + mutable.z = position.z + r.nextInt(dz2) - dz; + BaseBlock block = pattern.apply(mutable); + if (solid[FaweCache.getCombined(block)]) { + return block; + } else { + return pattern.apply(position); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/object/pattern/SurfaceRandomOffsetPattern.java b/core/src/main/java/com/boydti/fawe/object/pattern/SurfaceRandomOffsetPattern.java new file mode 100644 index 00000000..8769720b --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/pattern/SurfaceRandomOffsetPattern.java @@ -0,0 +1,50 @@ +package com.boydti.fawe.object.pattern; + +import com.boydti.fawe.FaweCache; +import com.boydti.fawe.object.PseudoRandom; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.blocks.BaseBlock; +import com.sk89q.worldedit.blocks.BlockType; +import com.sk89q.worldedit.function.pattern.AbstractPattern; +import com.sk89q.worldedit.function.pattern.Pattern; + +public class SurfaceRandomOffsetPattern extends AbstractPattern { + private final PseudoRandom r = new PseudoRandom(); + private final int dx, dy, dz, dx2, dy2, dz2; + private final Pattern pattern; + private final Vector mutable = new Vector(); + boolean[] solid; + + public SurfaceRandomOffsetPattern(Pattern pattern, int dx, int dy, int dz) { + this.pattern = pattern; + this.dx = dx; + this.dy = dy; + this.dz = dz; + this.dx2 = dx * 2 + 1; + this.dy2 = dy * 2 + 1; + this.dz2 = dz * 2 + 1; + solid = new boolean[Character.MAX_VALUE + 1]; + for (int id = 0; id < 4096; id++) { + for (int data = 0; data < 16; data++) { + if (!BlockType.canPassThrough(id, data)) { + solid[FaweCache.getCombined(id, data)] = true; + } + } + } + } + + @Override + public BaseBlock apply(Vector position) { + mutable.x = position.x + r.nextInt(dx2) - dx; + mutable.y = position.y + r.nextInt(dy2) - dy; + mutable.z = position.z + r.nextInt(dz2) - dz; + BaseBlock block = pattern.apply(mutable); + if (solid[FaweCache.getCombined(block)]) { + mutable.y++; + if (!solid[FaweCache.getCombined(pattern.apply(mutable))]) { + return block; + } + } + return pattern.apply(position); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/util/ShapeInterpolator.java b/core/src/main/java/com/boydti/fawe/util/ShapeInterpolator.java new file mode 100644 index 00000000..1e69c31b --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/util/ShapeInterpolator.java @@ -0,0 +1,735 @@ +package com.boydti.fawe.util; + +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.FlatteningPathIterator; +import java.awt.geom.IllegalPathStateException; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.Vector; + +/** + * Original source
+ * An interpolator for {@link Shape} objects. + * This class can be used to morph between the geometries + * of two relatively arbitrary shapes with the only restrictions being + * that the two different numbers of sub-paths or two shapes with + * disparate winding rules may not blend together in a pleasing + * manner. + * The ShapeEvaluator will do the best job it can if the shapes do + * not match in winding rule or number of sub-paths, but the geometry + * of the shapes may need to be adjusted by other means to make the + * shapes more like each other for best aesthetic effect. + *

+ * Note that the process of comparing two geometries and finding similar + * structures between them to blend for the morphing operation can be + * expensive. + * Instances of this class will properly perform the necessary + * geometric analysis of their arguments on every method call and attempt + * to cache the information so that they can operate more quickly if called + * multiple times in a row on the same pair of {@code Shape} objects. + * As a result attempting to mutate a {@code Shape} object that is stored + * in one of their keyframes may not have any effect if the associated + * interpolator has already cached the geometry. + * Also, it is advisable to use different instances of {@code ShapeEvaluator} + * for every pair of keyframes being morphed so that the cached information + * can be reused as much as possible. + */ +public class ShapeInterpolator { + + private Shape savedV0; + private Shape savedV1; + private Geometry geom0; + private Geometry geom1; + + public static Shape apply(Shape v0, Shape v1, float fraction) { + return apply(v0, v1, fraction, false); + } + + public static Shape apply(Shape v0, Shape v1, float fraction, boolean unionBounds) { + final ShapeInterpolator instance = new ShapeInterpolator(); + return instance.evaluate(v0, v1, fraction, unionBounds); + } + + /** Creates an interpolated shape from tight bounds. */ + public Shape evaluate(Shape v0, Shape v1, float fraction) { + return evaluate(v0, v1, fraction, false); + } + + /** Creates an interpolated shape. + * + * @param v0 the first shape + * @param v1 the second shape + * @param fraction the fraction from zero (just first shape) to one (just second shape) + * @param unionBounds if `true`, the shape reports bounds which are the union of + * the bounds of both shapes, if `false` it reports "tight" bounds + * using the actual interpolated path. + */ + public Shape evaluate(Shape v0, Shape v1, float fraction, boolean unionBounds) { + if (savedV0 != v0 || savedV1 != v1) { + if (savedV0 == v1 && savedV1 == v0) { + // Just swap the geometries + final Geometry tmp = geom0; + geom0 = geom1; + geom1 = tmp; + } else { + recalculate(v0, v1); + } + savedV0 = v0; + savedV1 = v1; + } + return getShape(fraction, unionBounds); + } + + private void recalculate(Shape v0, Shape v1) { + geom0 = new Geometry(v0); + geom1 = new Geometry(v1); + final float[] tVals0 = geom0.getTVals(); + final float[] tVals1 = geom1.getTVals(); + final float[] masterTVals = mergeTVals(tVals0, tVals1); + geom0.setTVals(masterTVals); + geom1.setTVals(masterTVals); + } + + private Shape getShape(float fraction, boolean unionBounds) { + return new MorphedShape(geom0, geom1, fraction, unionBounds); + } + + private static float[] mergeTVals(float[] tVals0, float[] tVals1) { + final int count = sortTVals(tVals0, tVals1, null); + final float[] newTVals = new float[count]; + sortTVals(tVals0, tVals1, newTVals); + return newTVals; + } + + private static int sortTVals(float[] tVals0, + float[] tVals1, + float[] newTVals) { + int i0 = 0; + int i1 = 0; + int numTVals = 0; + while (i0 < tVals0.length && i1 < tVals1.length) { + final float t0 = tVals0[i0]; + final float t1 = tVals1[i1]; + if (t0 <= t1) { + if (newTVals != null) { + newTVals[numTVals] = t0; + } + i0++; + } + if (t1 <= t0) { + if (newTVals != null) { + newTVals[numTVals] = t1; + } + i1++; + } + numTVals++; + } + return numTVals; + } + + private static float interp(float v0, float v1, float t) { + return (v0 + ((v1 - v0) * t)); + } + + private static class Geometry { + static final float THIRD = (1f / 3f); + static final float MIN_LEN = 0.001f; + + final int windingRule; + float[] bezierCoordinates; + int numCoordinates; + float[] myTVals; + + public Geometry(Shape s) { + // Multiple of 6 plus 2 more for initial move-to + bezierCoordinates = new float[20]; + final PathIterator pi = s.getPathIterator(null); + windingRule = pi.getWindingRule(); + if (pi.isDone()) { + // We will have 1 segment and it will be all zeros + // It will have 8 coordinates (2 for move-to, 6 for cubic) + numCoordinates = 8; + } + final float[] coordinates = new float[6]; + int type = pi.currentSegment(coordinates); + pi.next(); + if (type != PathIterator.SEG_MOVETO) { + throw new IllegalPathStateException("missing initial move-to"); + } + float curX, curY, movX, movY; + bezierCoordinates[0] = curX = movX = coordinates[0]; + bezierCoordinates[1] = curY = movY = coordinates[1]; + float newX, newY; + final Vector savedPathEndPoints = new Vector(); + numCoordinates = 2; + while (!pi.isDone()) { + switch (pi.currentSegment(coordinates)) { + case PathIterator.SEG_MOVETO: + if (curX != movX || curY != movY) { + appendLineTo(curX, curY, movX, movY); + curX = movX; + curY = movY; + } + newX = coordinates[0]; + newY = coordinates[1]; + if (curX != newX || curY != newY) { + savedPathEndPoints.add(new Point2D.Float(movX, movY)); + appendLineTo(curX, curY, newX, newY); + curX = movX = newX; + curY = movY = newY; + } + break; + case PathIterator.SEG_CLOSE: + if (curX != movX || curY != movY) { + appendLineTo(curX, curY, movX, movY); + curX = movX; + curY = movY; + } + break; + case PathIterator.SEG_LINETO: + newX = coordinates[0]; + newY = coordinates[1]; + appendLineTo(curX, curY, newX, newY); + curX = newX; + curY = newY; + break; + case PathIterator.SEG_QUADTO: + final float ctrlX = coordinates[0]; + final float ctrlY = coordinates[1]; + newX = coordinates[2]; + newY = coordinates[3]; + appendQuadTo(curX, curY, ctrlX, ctrlY, newX, newY); + curX = newX; + curY = newY; + break; + case PathIterator.SEG_CUBICTO: + newX = coordinates[4]; + newY = coordinates[5]; + appendCubicTo( + coordinates[0], coordinates[1], + coordinates[2], coordinates[3], + newX, newY); + curX = newX; + curY = newY; + break; + } + pi.next(); + } + // Add closing segment if either: + // - we only have initial move-to - expand it to an empty cubic + // - or we are not back to the starting point + if ((numCoordinates < 8) || curX != movX || curY != movY) { + appendLineTo(curX, curY, movX, movY); + curX = movX; + curY = movY; + } + // Now retrace our way back through all of the connecting + // inter-sub-path segments + for (int i = savedPathEndPoints.size()-1; i >= 0; i--) { + final Point2D.Float p = savedPathEndPoints.get(i); + newX = p.x; + newY = p.y; + if (curX != newX || curY != newY) { + appendLineTo(curX, curY, newX, newY); + curX = newX; + curY = newY; + } + } + // Now find the segment endpoint with the smallest Y coordinate + int minPt = 0; + float minX = bezierCoordinates[0]; + float minY = bezierCoordinates[1]; + for (int ci = 6; ci < numCoordinates; ci += 6) { + float x = bezierCoordinates[ci]; + float y = bezierCoordinates[ci + 1]; + if (y < minY || (y == minY && x < minX)) { + minPt = ci; + minX = x; + minY = y; + } + } + // If the smallest Y coordinate is not the first coordinate, + // rotate the points so that it is... + if (minPt > 0) { + // Keep in mind that first 2 coordinates == last 2 coordinates + final float[] newCoordinates = new float[numCoordinates]; + // Copy all coordinates from minPt to the end of the + // array to the beginning of the new array + System.arraycopy(bezierCoordinates, minPt, + newCoordinates, 0, + numCoordinates - minPt); + // Now we do not want to copy 0,1 as they are duplicates + // of the last 2 coordinates which we just copied. So + // we start the source copy at index 2, but we still + // copy a full minPt coordinates which copies the two + // coordinates that were at minPt to the last two elements + // of the array, thus ensuring that thew new array starts + // and ends with the same pair of coordinates... + System.arraycopy(bezierCoordinates, 2, + newCoordinates, numCoordinates - minPt, + minPt); + bezierCoordinates = newCoordinates; + } + /* Clockwise enforcement: + * - This technique is based on the formula for calculating + * the area of a Polygon. The standard formula is: + * Area(Poly) = 1/2 * sum(x[i]*y[i+1] - x[i+1]y[i]) + * - The returned area is negative if the polygon is + * "mostly clockwise" and positive if the polygon is + * "mostly counter-clockwise". + * - One failure mode of the Area calculation is if the + * Polygon is self-intersecting. This is due to the + * fact that the areas on each side of the self-intersection + * are bounded by segments which have opposite winding + * direction. Thus, those areas will have opposite signs + * on the accumulation of their area summations and end + * up canceling each other out partially. + * - This failure mode of the algorithm in determining the + * exact magnitude of the area is not actually a big problem + * for our needs here since we are only using the sign of + * the resulting area to figure out the overall winding + * direction of the path. If self-intersections cause + * different parts of the path to disagree as to the + * local winding direction, that is no matter as we just + * wait for the final answer to tell us which winding + * direction had greater representation. If the final + * result is zero then the path was equal parts clockwise + * and counter-clockwise and we do not care about which + * way we order it as either way will require half of the + * path to unwind and re-wind itself. + */ + float area = 0; + // Note that first and last points are the same so we + // do not need to process coordinates[0,1] against coordinates[n-2,n-1] + curX = bezierCoordinates[0]; + curY = bezierCoordinates[1]; + for (int i = 2; i < numCoordinates; i += 2) { + newX = bezierCoordinates[i]; + newY = bezierCoordinates[i + 1]; + area += curX * newY - newX * curY; + curX = newX; + curY = newY; + } + if (area < 0) { + /* The area is negative so the shape was clockwise + * in a Euclidean sense. But, our screen coordinate + * systems have the origin in the upper left so they + * are flipped. Thus, this path "looks" ccw on the + * screen so we are flipping it to "look" clockwise. + * Note that the first and last points are the same + * so we do not need to swap them. + * (Not that it matters whether the paths end up cw + * or ccw in the end as long as all of them are the + * same, but above we called this section "Clockwise + * Enforcement", so we do not want to be liars. ;-) + */ + // Note that [0,1] do not need to be swapped with [n-2,n-1] + // So first pair to swap is [2,3] and [n-4,n-3] + int i = 2; + int j = numCoordinates - 4; + while (i < j) { + curX = bezierCoordinates[i]; + curY = bezierCoordinates[i + 1]; + bezierCoordinates[i] = bezierCoordinates[j]; + bezierCoordinates[i + 1] = bezierCoordinates[j + 1]; + bezierCoordinates[j] = curX; + bezierCoordinates[j + 1] = curY; + i += 2; + j -= 2; + } + } + } + + private void appendLineTo(float x0, float y0, + float x1, float y1) { + appendCubicTo(// A third of the way from xy0 to xy1: + interp(x0, x1, THIRD), + interp(y0, y1, THIRD), + // A third of the way from xy1 back to xy0: + interp(x1, x0, THIRD), + interp(y1, y0, THIRD), + x1, y1); + } + + private void appendQuadTo(float x0, float y0, + float ctrlX, float ctrlY, + float x1, float y1) { + appendCubicTo(// A third of the way from ctrl X/Y back to xy0: + interp(ctrlX, x0, THIRD), + interp(ctrlY, y0, THIRD), + // A third of the way from ctrl X/Y to xy1: + interp(ctrlX, x1, THIRD), + interp(ctrlY, y1, THIRD), + x1, y1); + } + + private void appendCubicTo(float ctrlX1, float ctrlY1, + float ctrlX2, float ctrlY2, + float x1, float y1) { + if (numCoordinates + 6 > bezierCoordinates.length) { + // Keep array size to a multiple of 6 plus 2 + int newsize = (numCoordinates - 2) * 2 + 2; + final float[] newCoordinates = new float[newsize]; + System.arraycopy(bezierCoordinates, 0, newCoordinates, 0, numCoordinates); + bezierCoordinates = newCoordinates; + } + bezierCoordinates[numCoordinates++] = ctrlX1; + bezierCoordinates[numCoordinates++] = ctrlY1; + bezierCoordinates[numCoordinates++] = ctrlX2; + bezierCoordinates[numCoordinates++] = ctrlY2; + bezierCoordinates[numCoordinates++] = x1; + bezierCoordinates[numCoordinates++] = y1; + } + + public int getWindingRule() { + return windingRule; + } + + public int getNumCoordinates() { + return numCoordinates; + } + + public float getCoordinate(int i) { + return bezierCoordinates[i]; + } + + public float[] getTVals() { + if (myTVals != null) { + return myTVals; + } + + // assert(numCoordinates >= 8); + // assert(((numCoordinates - 2) % 6) == 0); + final float[] tVals = new float[(numCoordinates - 2) / 6 + 1]; + + // First calculate total "length" of path + // Length of each segment is averaged between + // the length between the endpoints (a lower bound for a cubic) + // and the length of the control polygon (an upper bound) + float segX = bezierCoordinates[0]; + float segY = bezierCoordinates[1]; + float tLen = 0; + int ci = 2; + int ti = 0; + while (ci < numCoordinates) { + float prevX, prevY, newX, newY; + prevX = segX; + prevY = segY; + newX = bezierCoordinates[ci++]; + newY = bezierCoordinates[ci++]; + prevX -= newX; + prevY -= newY; + float len = (float) Math.sqrt(prevX * prevX + prevY * prevY); + prevX = newX; + prevY = newY; + newX = bezierCoordinates[ci++]; + newY = bezierCoordinates[ci++]; + prevX -= newX; + prevY -= newY; + len += (float) Math.sqrt(prevX * prevX + prevY * prevY); + prevX = newX; + prevY = newY; + newX = bezierCoordinates[ci++]; + newY = bezierCoordinates[ci++]; + prevX -= newX; + prevY -= newY; + len += (float) Math.sqrt(prevX * prevX + prevY * prevY); + // len is now the total length of the control polygon + segX -= newX; + segY -= newY; + len += (float) Math.sqrt(segX * segX + segY * segY); + // len is now sum of linear length and control polygon length + len /= 2; + // len is now average of the two lengths + + /* If the result is zero length then we will have problems + * below trying to do the math and bookkeeping to split + * the segment or pair it against the segments in the + * other shape. Since these lengths are just estimates + * to map the segments of the two shapes onto corresponding + * segments of "approximately the same length", we will + * simply modify the length of this segment to be at least + * a minimum value and it will simply grow from zero or + * near zero length to a non-trivial size as it morphs. + */ + if (len < MIN_LEN) { + len = MIN_LEN; + } + tLen += len; + tVals[ti++] = tLen; + segX = newX; + segY = newY; + } + + // Now set tVals for each segment to its proportional + // part of the length + float prevT = tVals[0]; + tVals[0] = 0; + for (ti = 1; ti < tVals.length - 1; ti++) { + final float nextT = tVals[ti]; + tVals[ti] = prevT / tLen; + prevT = nextT; + } + tVals[ti] = 1; + return (myTVals = tVals); + } + + public void setTVals(float[] newTVals) { + final float[] oldCoordinates = bezierCoordinates; + final float[] newCoordinates = new float[2 + (newTVals.length - 1) * 6]; + final float[] oldTVals = getTVals(); + int oldCi = 0; + float x0, xc0, xc1, x1; + float y0, yc0, yc1, y1; + x0 = xc0 = xc1 = x1 = oldCoordinates[oldCi++]; + y0 = yc0 = yc1 = y1 = oldCoordinates[oldCi++]; + int newCi = 0; + newCoordinates[newCi++] = x0; + newCoordinates[newCi++] = y0; + float t0 = 0; + float t1 = 0; + int oldTi = 1; + int newTi = 1; + while (newTi < newTVals.length) { + if (t0 >= t1) { + x0 = x1; + y0 = y1; + xc0 = oldCoordinates[oldCi++]; + yc0 = oldCoordinates[oldCi++]; + xc1 = oldCoordinates[oldCi++]; + yc1 = oldCoordinates[oldCi++]; + x1 = oldCoordinates[oldCi++]; + y1 = oldCoordinates[oldCi++]; + t1 = oldTVals [oldTi++]; + } + float nt = newTVals[newTi++]; + // assert(nt > t0); + if (nt < t1) { + // Make nt proportional to [t0 => t1] range + float relT = (nt - t0) / (t1 - t0); + newCoordinates[newCi++] = x0 = interp(x0, xc0, relT); + newCoordinates[newCi++] = y0 = interp(y0, yc0, relT); + xc0 = interp(xc0, xc1, relT); + yc0 = interp(yc0, yc1, relT); + xc1 = interp(xc1, x1 , relT); + yc1 = interp(yc1, y1 , relT); + newCoordinates[newCi++] = x0 = interp(x0, xc0, relT); + newCoordinates[newCi++] = y0 = interp(y0, yc0, relT); + xc0 = interp(xc0, xc1, relT); + yc0 = interp(yc0, yc1, relT); + newCoordinates[newCi++] = x0 = interp(x0, xc0, relT); + newCoordinates[newCi++] = y0 = interp(y0, yc0, relT); + } else { + newCoordinates[newCi++] = xc0; + newCoordinates[newCi++] = yc0; + newCoordinates[newCi++] = xc1; + newCoordinates[newCi++] = yc1; + newCoordinates[newCi++] = x1; + newCoordinates[newCi++] = y1; + } + t0 = nt; + } + bezierCoordinates = newCoordinates; + numCoordinates = newCoordinates.length; + myTVals = newTVals; + } + } + + private static class MorphedShape implements Shape { + final Geometry geom0; + final Geometry geom1; + final float t; + final boolean unionBounds; + + MorphedShape(Geometry geom0, Geometry geom1, float t, boolean unionBounds) { + this.geom0 = geom0; + this.geom1 = geom1; + this.t = t; + this.unionBounds= unionBounds; + } + + public Rectangle getBounds() { + return getBounds2D().getBounds(); + } + + public Rectangle2D getBounds2D() { + final int n = geom0.getNumCoordinates(); + float xMin, yMin, xMax, yMax; + + if (unionBounds) { + xMin = xMax = geom0.getCoordinate(0); + yMin = yMax = geom0.getCoordinate(1); + for (int i = 2; i < n; i += 2) { + final float x = geom0.getCoordinate(i ); + final float y = geom0.getCoordinate(i+1); + if (xMin > x) { + xMin = x; + } + if (yMin > y) { + yMin = y; + } + if (xMax < x) { + xMax = x; + } + if (yMax < y) { + yMax = y; + } + } + final int m = geom1.getNumCoordinates(); + for (int i = 0; i < m; i += 2) { + final float x = geom1.getCoordinate(i ); + final float y = geom1.getCoordinate(i+1); + if (xMin > x) { + xMin = x; + } + if (yMin > y) { + yMin = y; + } + if (xMax < x) { + xMax = x; + } + if (yMax < y) { + yMax = y; + } + } + } else { + xMin = xMax = interp(geom0.getCoordinate(0), geom1.getCoordinate(0), t); + yMin = yMax = interp(geom0.getCoordinate(1), geom1.getCoordinate(1), t); + for (int i = 2; i < n; i += 2) { + final float x = interp(geom0.getCoordinate(i ), geom1.getCoordinate(i ), t); + final float y = interp(geom0.getCoordinate(i+1), geom1.getCoordinate(i+1), t); + if (xMin > x) { + xMin = x; + } + if (yMin > y) { + yMin = y; + } + if (xMax < x) { + xMax = x; + } + if (yMax < y) { + yMax = y; + } + } + } + return new Rectangle2D.Float(xMin, yMin, xMax - xMin, yMax - yMin); + } + + public boolean contains(double x, double y) { + return Path2D.contains(getPathIterator(null), x, y); + } + + public boolean contains(Point2D p) { + return Path2D.contains(getPathIterator(null), p); + } + + public boolean intersects(double x, double y, double w, double h) { + return Path2D.intersects(getPathIterator(null), x, y, w, h); + } + + public boolean intersects(Rectangle2D r) { + return Path2D.intersects(getPathIterator(null), r); + } + + public boolean contains(double x, double y, double width, double height) { + return Path2D.contains(getPathIterator(null), x, y, width, height); + } + + public boolean contains(Rectangle2D r) { + return Path2D.contains(getPathIterator(null), r); + } + + public PathIterator getPathIterator(AffineTransform at) { + return new Iterator(at, geom0, geom1, t); + } + + public PathIterator getPathIterator(AffineTransform at, double flatness) { + return new FlatteningPathIterator(getPathIterator(at), flatness); + } + } + + private static class Iterator implements PathIterator { + AffineTransform at; + Geometry g0; + Geometry g1; + float t; + int cIndex; + + public Iterator(AffineTransform at, + Geometry g0, Geometry g1, + float t) { + this.at = at; + this.g0 = g0; + this.g1 = g1; + this.t = t; + } + + /** + * @{inheritDoc} + */ + public int getWindingRule() { + return (t < 0.5 ? g0.getWindingRule() : g1.getWindingRule()); + } + + /** + * @{inheritDoc} + */ + public boolean isDone() { + return (cIndex > g0.getNumCoordinates()); + } + + /** + * @{inheritDoc} + */ + public void next() { + if (cIndex == 0) { + cIndex = 2; + } else { + cIndex += 6; + } + } + + /** + * @{inheritDoc} + */ + public int currentSegment(float[] coordinates) { + int type; + int n; + if (cIndex == 0) { + type = SEG_MOVETO; + n = 2; + } else if (cIndex >= g0.getNumCoordinates()) { + type = SEG_CLOSE; + n = 0; + } else { + type = SEG_CUBICTO; + n = 6; + } + if (n > 0) { + for (int i = 0; i < n; i++) { + coordinates[i] = interp( + g0.getCoordinate(cIndex + i), + g1.getCoordinate(cIndex + i), + t); + } + if (at != null) { + at.transform(coordinates, 0, coordinates, 0, n / 2); + } + } + return type; + } + + public int currentSegment(double[] coordinates) { + final float[] temp = new float[6]; + final int res = currentSegment(temp); + for (int i = 0; i < 6; i++) { + coordinates[i] = temp[i]; + } + return res; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sk89q/worldedit/EditSession.java b/core/src/main/java/com/sk89q/worldedit/EditSession.java index 46b53ef1..e3a3c3bc 100644 --- a/core/src/main/java/com/sk89q/worldedit/EditSession.java +++ b/core/src/main/java/com/sk89q/worldedit/EditSession.java @@ -45,6 +45,7 @@ import com.boydti.fawe.object.extent.FastWorldEditExtent; import com.boydti.fawe.object.extent.FaweRegionExtent; import com.boydti.fawe.object.extent.NullExtent; import com.boydti.fawe.object.extent.ProcessedWEExtent; +import com.boydti.fawe.object.extent.TransformExtent; import com.boydti.fawe.object.mask.ResettableMask; import com.boydti.fawe.object.progress.DefaultProgressTracker; import com.boydti.fawe.util.ExtentTraverser; @@ -572,6 +573,21 @@ public class EditSession extends AbstractWorld implements HasFaweQueue { return maskingExtent != null ? maskingExtent.get().getMask() : null; } + public void addTransform(TransformExtent transform) { + if (transform == null) { + ExtentTraverser traverser = new ExtentTraverser(this.extent).find(TransformExtent.class); + AbstractDelegateExtent next = extent; + while (traverser != null && traverser.get() instanceof TransformExtent) { + traverser = traverser.next(); + next = traverser.get(); + } + this.extent = next; + return; + } else { + this.extent = transform.setExtent(extent); + } + } + /** * Set a mask. * diff --git a/core/src/main/java/com/sk89q/worldedit/LocalSession.java b/core/src/main/java/com/sk89q/worldedit/LocalSession.java index d4ca251d..f675334d 100644 --- a/core/src/main/java/com/sk89q/worldedit/LocalSession.java +++ b/core/src/main/java/com/sk89q/worldedit/LocalSession.java @@ -29,6 +29,7 @@ import com.boydti.fawe.object.brush.DoubleActionBrushTool; import com.boydti.fawe.object.changeset.DiskStorageHistory; import com.boydti.fawe.object.changeset.FaweChangeSet; import com.boydti.fawe.object.clipboard.DiskOptimizedClipboard; +import com.boydti.fawe.object.extent.TransformExtent; import com.boydti.fawe.util.EditSessionBuilder; import com.boydti.fawe.util.MainUtil; import com.boydti.fawe.wrappers.WorldWrapper; @@ -138,6 +139,7 @@ public class LocalSession { private transient int cuiVersion = -1; private transient boolean fastMode = false; private transient Mask mask; + private TransformExtent transform = null; private transient TimeZone timezone = TimeZone.getDefault(); private transient World currentWorld; @@ -1203,7 +1205,12 @@ public class LocalSession { getBlockChangeLimit(), blockBag, player); editSession.setFastMode(fastMode); Request.request().setEditSession(editSession); - editSession.setMask(mask); + if (mask != null) { + editSession.setMask(mask); + } + if (transform != null) { + editSession.addTransform(transform); + } return editSession; } @@ -1254,6 +1261,14 @@ public class LocalSession { setMask(mask != null ? Masks.wrap(mask) : null); } + public TransformExtent getTransform() { + return transform; + } + + public void setTransform(TransformExtent transform) { + this.transform = transform; + } + public static Class inject() { return LocalSession.class; } 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 cdfb943c..26a7bebf 100644 --- a/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java +++ b/core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java @@ -390,7 +390,7 @@ public class BrushCommands { public void command(Player player, LocalSession session, EditSession editSession, @Optional("5") double radius, CommandContext args) throws WorldEditException { BrushTool tool = session.getBrushTool(player.getItemInHand()); String cmd = args.getJoinedStrings(1); - tool.setBrush(new CommandBrush(player, cmd, radius), "worldedit.brush.copy"); + tool.setBrush(new CommandBrush(player, tool, cmd, radius), "worldedit.brush.copy"); BBC.BRUSH_COMMAND.send(player, cmd); } diff --git a/core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java b/core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java index 476b589f..13892e51 100644 --- a/core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java +++ b/core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java @@ -300,8 +300,8 @@ public class ClipboardCommands { if (selectPasted) { Vector clipboardOffset = clipboard.getRegion().getMinimumPoint().subtract(clipboard.getOrigin()); - Vector realTo = to.add(holder.getTransform().apply(clipboardOffset)); - Vector max = realTo.add(holder.getTransform().apply(region.getMaximumPoint().subtract(region.getMinimumPoint()))); + Vector realTo = to.add(new Vector(holder.getTransform().apply(clipboardOffset))); + Vector max = realTo.add(new Vector(holder.getTransform().apply(region.getMaximumPoint().subtract(region.getMinimumPoint())))); RegionSelector selector = new CuboidRegionSelector(player.getWorld(), realTo, max); session.setRegionSelector(player.getWorld(), selector); selector.learnChanges(); diff --git a/core/src/main/java/com/sk89q/worldedit/command/FlattenedClipboardTransform.java b/core/src/main/java/com/sk89q/worldedit/command/FlattenedClipboardTransform.java index 1f52bda4..d557ed88 100644 --- a/core/src/main/java/com/sk89q/worldedit/command/FlattenedClipboardTransform.java +++ b/core/src/main/java/com/sk89q/worldedit/command/FlattenedClipboardTransform.java @@ -91,7 +91,7 @@ public class FlattenedClipboardTransform { maximum.setZ(minimum.getZ()) }; for (int i = 0; i < corners.length; i++) { - corners[i] = transformAround.apply(corners[i]); + corners[i] = transformAround.apply(new Vector(corners[i])); } Vector newMinimum = corners[0]; diff --git a/core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java b/core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java index 5cbe39c9..cb86bafb 100644 --- a/core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java +++ b/core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java @@ -1,6 +1,8 @@ package com.sk89q.worldedit.command; import com.boydti.fawe.config.BBC; +import com.boydti.fawe.object.extent.DefaultTransformParser; +import com.boydti.fawe.object.extent.TransformExtent; import com.sk89q.minecraft.util.commands.Command; import com.sk89q.minecraft.util.commands.CommandContext; import com.sk89q.minecraft.util.commands.CommandPermissions; @@ -20,6 +22,7 @@ import static com.google.common.base.Preconditions.checkNotNull; public class GeneralCommands { private final WorldEdit worldEdit; + private final DefaultTransformParser transformParser; /** * Create a new instance. @@ -29,6 +32,7 @@ public class GeneralCommands { public GeneralCommands(WorldEdit worldEdit) { checkNotNull(worldEdit); this.worldEdit = worldEdit; + transformParser = new DefaultTransformParser(worldEdit); } @Command( @@ -102,7 +106,7 @@ public class GeneralCommands { public void gmask(Player player, LocalSession session, EditSession editSession, @Optional CommandContext context) throws WorldEditException { if (context == null || context.argsLength() == 0) { session.setMask((Mask) null); - BBC.BRUSH_MASK_DISABLED.send(player); + BBC.MASK_DISABLED.send(player); } else { ParserContext parserContext = new ParserContext(); parserContext.setActor(player); @@ -111,7 +115,31 @@ public class GeneralCommands { parserContext.setExtent(editSession); Mask mask = worldEdit.getMaskFactory().parseFromInput(context.getJoinedStrings(0), parserContext); session.setMask(mask); - BBC.BRUSH_MASK.send(player); + BBC.MASK.send(player); + } + } + + @Command( + aliases = { "/gtransform", "gtransform" }, + usage = "[transform]", + desc = "Set the global transform", + min = 0, + max = -1 + ) + @CommandPermissions("worldedit.global-trasnform") + public void gtransform(Player player, LocalSession session, EditSession editSession, @Optional CommandContext context) throws WorldEditException { + if (context == null || context.argsLength() == 0) { + session.setTransform(null); + BBC.TRANSFORM_DISABLED.send(player); + } else { + ParserContext parserContext = new ParserContext(); + parserContext.setActor(player); + parserContext.setWorld(player.getWorld()); + parserContext.setSession(session); + parserContext.setExtent(editSession); + TransformExtent transform = transformParser.parseFromInput(context.getJoinedStrings(0), parserContext); + session.setTransform(transform); + BBC.TRANSFORM.send(player); } } diff --git a/core/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java b/core/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java index c6071386..e56507da 100644 --- a/core/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java +++ b/core/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java @@ -2,6 +2,8 @@ package com.sk89q.worldedit.command; import com.boydti.fawe.config.BBC; import com.boydti.fawe.object.brush.DoubleActionBrushTool; +import com.boydti.fawe.object.extent.DefaultTransformParser; +import com.boydti.fawe.object.extent.TransformExtent; import com.sk89q.minecraft.util.commands.Command; import com.sk89q.minecraft.util.commands.CommandContext; import com.sk89q.minecraft.util.commands.CommandPermissions; @@ -22,9 +24,11 @@ import com.sk89q.worldedit.util.command.parametric.Optional; */ public class ToolUtilCommands { private final WorldEdit we; + private final DefaultTransformParser transformParser; public ToolUtilCommands(WorldEdit we) { this.we = we; + this.transformParser = new DefaultTransformParser(we); } @Command( @@ -95,6 +99,42 @@ public class ToolUtilCommands { } } + @Command( + aliases = { "transform" }, + usage = "[transform]", + desc = "Set the brush transform", + min = 0, + max = -1 + ) + @CommandPermissions("worldedit.brush.options.transform") + public void transform(Player player, LocalSession session, EditSession editSession, @Optional CommandContext context) throws WorldEditException { + Tool tool = session.getTool(player.getItemInHand()); + if (tool == null) { + return; + } + if (context == null || context.argsLength() == 0) { + if (tool instanceof BrushTool) { + ((BrushTool) tool).setTransform(null); + } else if (tool instanceof DoubleActionBrushTool) { + ((DoubleActionBrushTool) tool).setTransform(null); + } + BBC.BRUSH_TRANSFORM_DISABLED.send(player); + } else { + ParserContext parserContext = new ParserContext(); + parserContext.setActor(player); + parserContext.setWorld(player.getWorld()); + parserContext.setSession(session); + parserContext.setExtent(editSession); + TransformExtent transform = transformParser.parseFromInput(context.getJoinedStrings(0), parserContext); + if (tool instanceof BrushTool) { + ((BrushTool) tool).setTransform(transform); + } else if (tool instanceof DoubleActionBrushTool) { + ((DoubleActionBrushTool) tool).setTransform(transform); + } + BBC.BRUSH_TRANSFORM.send(player); + } + } + @Command( aliases = { "mat", "material" }, usage = "[pattern]", @@ -104,7 +144,12 @@ public class ToolUtilCommands { ) @CommandPermissions("worldedit.brush.options.material") public void material(Player player, LocalSession session, EditSession editSession, Pattern pattern) throws WorldEditException { - session.getBrushTool(player.getItemInHand()).setFill(pattern); + Tool tool = session.getTool(player.getItemInHand()); + if (tool instanceof BrushTool) { + ((BrushTool) tool).setMask(null); + } else if (tool instanceof DoubleActionBrushTool) { + ((DoubleActionBrushTool) tool).setFill(pattern); + } BBC.BRUSH_MATERIAL.send(player); } @@ -118,7 +163,12 @@ public class ToolUtilCommands { @CommandPermissions("worldedit.brush.options.range") public void range(Player player, LocalSession session, EditSession editSession, CommandContext args) throws WorldEditException { int range = args.getInteger(0); - session.getBrushTool(player.getItemInHand()).setRange(range); + Tool tool = session.getTool(player.getItemInHand()); + if (tool instanceof BrushTool) { + ((BrushTool) tool).setMask(null); + } else if (tool instanceof DoubleActionBrushTool) { + ((DoubleActionBrushTool) tool).setRange(range); + } BBC.BRUSH_RANGE.send(player); } @@ -135,7 +185,12 @@ public class ToolUtilCommands { int radius = args.getInteger(0); we.checkMaxBrushRadius(radius); - session.getBrushTool(player.getItemInHand()).setSize(radius); + Tool tool = session.getTool(player.getItemInHand()); + if (tool instanceof BrushTool) { + ((BrushTool) tool).setMask(null); + } else if (tool instanceof DoubleActionBrushTool) { + ((DoubleActionBrushTool) tool).setSize(radius); + } BBC.BRUSH_SIZE.send(player); } diff --git a/core/src/main/java/com/sk89q/worldedit/command/tool/BrushTool.java b/core/src/main/java/com/sk89q/worldedit/command/tool/BrushTool.java new file mode 100644 index 00000000..6e61142f --- /dev/null +++ b/core/src/main/java/com/sk89q/worldedit/command/tool/BrushTool.java @@ -0,0 +1,201 @@ +package com.sk89q.worldedit.command.tool; + +import com.boydti.fawe.object.extent.TransformExtent; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.LocalConfiguration; +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.MaxChangedBlocksException; +import com.sk89q.worldedit.WorldVector; +import com.sk89q.worldedit.command.tool.brush.Brush; +import com.sk89q.worldedit.command.tool.brush.SphereBrush; +import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.extent.inventory.BlockBag; +import com.sk89q.worldedit.function.mask.Mask; +import com.sk89q.worldedit.function.mask.MaskIntersection; +import com.sk89q.worldedit.function.pattern.Pattern; +import com.sk89q.worldedit.session.request.Request; +import javax.annotation.Nullable; + + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Builds a shape at the place being looked at. + */ +public class BrushTool implements TraceTool { + + protected static int MAX_RANGE = 500; + protected int range = -1; + private Mask mask = null; + private TransformExtent transform = null; + private Brush brush = new SphereBrush(); + @Nullable + private Pattern material; + private double size = 1; + private String permission; + + /** + * Construct the tool. + * + * @param permission the permission to check before use is allowed + */ + public BrushTool(String permission) { + checkNotNull(permission); + this.permission = permission; + } + + @Override + public boolean canUse(Actor player) { + return player.hasPermission(permission); + } + + public TransformExtent getTransform() { + return transform; + } + + public void setTransform(TransformExtent transform) { + this.transform = transform; + } + + /** + * Get the filter. + * + * @return the filter + */ + public Mask getMask() { + return mask; + } + + /** + * Set the block filter used for identifying blocks to replace. + * + * @param filter the filter to set + */ + public void setMask(Mask filter) { + this.mask = filter; + } + + /** + * Set the brush. + * + * @param brush tbe brush + * @param permission the permission + */ + public void setBrush(Brush brush, String permission) { + this.brush = brush; + this.permission = permission; + } + + /** + * Get the current brush. + * + * @return the current brush + */ + public Brush getBrush() { + return brush; + } + + /** + * Set the material. + * + * @param material the material + */ + public void setFill(@Nullable Pattern material) { + this.material = material; + } + + /** + * Get the material. + * + * @return the material + */ + @Nullable public Pattern getMaterial() { + return material; + } + + /** + * Get the set brush size. + * + * @return a radius + */ + public double getSize() { + return size; + } + + /** + * Set the set brush size. + * + * @param radius a radius + */ + public void setSize(double radius) { + this.size = radius; + } + + /** + * Get the set brush range. + * + * @return the range of the brush in blocks + */ + public int getRange() { + return (range < 0) ? MAX_RANGE : Math.min(range, MAX_RANGE); + } + + /** + * Set the set brush range. + * + * @param range the range of the brush in blocks + */ + public void setRange(int range) { + this.range = range; + } + + @Override + public boolean actPrimary(Platform server, LocalConfiguration config, Player player, LocalSession session) { + WorldVector target = null; + target = player.getBlockTrace(getRange(), true); + + if (target == null) { + player.printError("No block in sight!"); + return true; + } + + BlockBag bag = session.getBlockBag(player); + + EditSession editSession = session.createEditSession(player); + Request.request().setEditSession(editSession); + if (mask != null) { + Mask existingMask = editSession.getMask(); + + if (existingMask == null) { + editSession.setMask(mask); + } else if (existingMask instanceof MaskIntersection) { + ((MaskIntersection) existingMask).add(mask); + } else { + MaskIntersection newMask = new MaskIntersection(existingMask); + newMask.add(mask); + editSession.setMask(newMask); + } + } + if (transform != null) { + editSession.addTransform(transform); + } + try { + brush.build(editSession, target, material, size); + } catch (MaxChangedBlocksException e) { + player.printError("Max blocks change limit reached."); + } finally { + if (bag != null) { + bag.flushChanges(); + } + session.remember(editSession); + } + + return true; + } + + public static Class inject() { + return BrushTool.class; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sk89q/worldedit/extension/factory/DefaultMaskParser.java b/core/src/main/java/com/sk89q/worldedit/extension/factory/DefaultMaskParser.java index 21b8c781..3d279327 100644 --- a/core/src/main/java/com/sk89q/worldedit/extension/factory/DefaultMaskParser.java +++ b/core/src/main/java/com/sk89q/worldedit/extension/factory/DefaultMaskParser.java @@ -7,12 +7,15 @@ import com.boydti.fawe.object.mask.DataMask; import com.boydti.fawe.object.mask.IdDataMask; import com.boydti.fawe.object.mask.IdMask; import com.boydti.fawe.object.mask.RadiusMask; +import com.boydti.fawe.object.mask.WallMask; import com.boydti.fawe.object.mask.XAxisMask; import com.boydti.fawe.object.mask.YAxisMask; import com.boydti.fawe.object.mask.ZAxisMask; +import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.IncompleteRegionException; import com.sk89q.worldedit.Vector; import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.blocks.BaseBlock; import com.sk89q.worldedit.extension.input.InputParseException; import com.sk89q.worldedit.extension.input.NoMatchException; import com.sk89q.worldedit.extension.input.ParserContext; @@ -102,6 +105,12 @@ public class DefaultMaskParser extends InputParser { final char firstChar = component.charAt(0); switch (firstChar) { case '#': + int colon = component.indexOf(':'); + if (colon != -1) { + String rest = component.substring(colon + 1); + component = component.substring(0, colon); + masks.add(getBlockMaskComponent(masks, rest, context)); + } switch (component.toLowerCase()) { case "#existing": return new ExistingBlockMask(extent); @@ -131,6 +140,13 @@ public class DefaultMaskParser extends InputParser { return new DataMask(extent); case "#iddata": return new IdDataMask(extent); + case "#wall": + masks.add(new ExistingBlockMask(extent)); + BlockMask matchAir = new BlockMask(extent, EditSession.nullBlock); + return new WallMask(extent, Arrays.asList(new BaseBlock(0)), 1, 8); + case "#surface": + masks.add(new ExistingBlockMask(extent)); + return new AdjacentMask(extent, Arrays.asList(new BaseBlock(0)), 1, 8); default: throw new NoMatchException("Unrecognized mask '" + component + "'"); } @@ -141,10 +157,10 @@ public class DefaultMaskParser extends InputParser { throw new InputParseException("Unknown angle '" + component + "' (not in form `/#,#`)"); } try { - int y1 = Integer.parseInt(split[0]); - int y2 = Integer.parseInt(split[1]); + int y1 = (int) Math.abs(Expression.compile(split[0]).evaluate()); + int y2 = (int) Math.abs(Expression.compile(split[1]).evaluate()); return new AngleMask(extent, y1, y2); - } catch (NumberFormatException e) { + } catch (NumberFormatException | ExpressionException e) { throw new InputParseException("Unknown angle '" + component + "' (not in form `/#,#`)"); } } @@ -154,13 +170,14 @@ public class DefaultMaskParser extends InputParser { throw new InputParseException("Unknown range '" + component + "' (not in form `{#,#`)"); } try { - int y1 = Integer.parseInt(split[0]); - int y2 = Integer.parseInt(split[1]); + int y1 = (int) Math.abs(Expression.compile(split[0]).evaluate()); + int y2 = (int) Math.abs(Expression.compile(split[1]).evaluate()); return new RadiusMask(y1, y2); - } catch (NumberFormatException e) { + } catch (NumberFormatException | ExpressionException e) { throw new InputParseException("Unknown range '" + component + "' (not in form `{#,#`)"); } } + case '|': case '~': { String[] split = component.substring(1).split("="); ParserContext tempContext = new ParserContext(context); @@ -171,13 +188,17 @@ public class DefaultMaskParser extends InputParser { int requiredMax = 8; if (split.length == 2) { String[] split2 = split[1].split(","); - requiredMin = Integer.parseInt(split2[0]); + requiredMin = (int) Math.abs(Expression.compile(split2[0]).evaluate()); if (split2.length == 2) { - requiredMax = Integer.parseInt(split2[1]); + requiredMax = (int) Math.abs(Expression.compile(split2[1]).evaluate()); } } - return new AdjacentMask(extent, worldEdit.getBlockFactory().parseFromListInput(component.substring(1), tempContext), requiredMin, requiredMax); - } catch (NumberFormatException e) { + if (firstChar == '~') { + return new AdjacentMask(extent, worldEdit.getBlockFactory().parseFromListInput(component.substring(1), tempContext), requiredMin, requiredMax); + } else { + return new WallMask(extent, worldEdit.getBlockFactory().parseFromListInput(component.substring(1), tempContext), requiredMin, requiredMax); + } + } catch (NumberFormatException | ExpressionException e) { throw new InputParseException("Unknown adjacent mask '" + component + "' (not in form `~[=count]`)"); } } @@ -208,9 +229,12 @@ public class DefaultMaskParser extends InputParser { return Masks.asMask(new BiomeMask2D(context.requireExtent(), biomes)); case '%': - int i = Integer.parseInt(component.substring(1)); - return new NoiseFilter(new RandomNoise(), ((double) i) / 100); - + try { + double i = Math.abs(Expression.compile(component.substring(1)).evaluate()); + return new NoiseFilter(new RandomNoise(), (i) / 100); + } catch (NumberFormatException | ExpressionException e) { + throw new InputParseException("Unknown percentage '" + component.substring(1) + "'"); + } case '=': try { Expression exp = Expression.compile(component.substring(1), "x", "y", "z"); diff --git a/core/src/main/java/com/sk89q/worldedit/extension/factory/HashTagPatternParser.java b/core/src/main/java/com/sk89q/worldedit/extension/factory/HashTagPatternParser.java index 668bcc9b..b4c6092e 100644 --- a/core/src/main/java/com/sk89q/worldedit/extension/factory/HashTagPatternParser.java +++ b/core/src/main/java/com/sk89q/worldedit/extension/factory/HashTagPatternParser.java @@ -1,6 +1,7 @@ package com.sk89q.worldedit.extension.factory; import com.boydti.fawe.object.pattern.ExistingPattern; +import com.boydti.fawe.object.pattern.ExpressionPattern; import com.boydti.fawe.object.pattern.Linear3DBlockPattern; import com.boydti.fawe.object.pattern.LinearBlockPattern; import com.boydti.fawe.object.pattern.MaskedPattern; @@ -11,9 +12,14 @@ import com.boydti.fawe.object.pattern.OffsetPattern; import com.boydti.fawe.object.pattern.PatternExtent; import com.boydti.fawe.object.pattern.RandomOffsetPattern; import com.boydti.fawe.object.pattern.RelativePattern; +import com.boydti.fawe.object.pattern.SolidRandomOffsetPattern; +import com.boydti.fawe.object.pattern.SurfaceRandomOffsetPattern; +import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.EmptyClipboardException; import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.Vector; import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.input.InputParseException; import com.sk89q.worldedit.extension.input.ParserContext; import com.sk89q.worldedit.extent.clipboard.Clipboard; @@ -25,6 +31,7 @@ import com.sk89q.worldedit.function.pattern.RandomPattern; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; import com.sk89q.worldedit.internal.registry.InputParser; +import com.sk89q.worldedit.regions.shape.WorldEditExpressionEnvironment; import com.sk89q.worldedit.session.ClipboardHolder; import com.sk89q.worldedit.session.request.Request; import java.util.ArrayList; @@ -137,6 +144,30 @@ public class HashTagPatternParser extends InputParser { } catch (NumberFormatException | ExpressionException e) { throw new InputParseException("The correct format is #offset::::"); } + case "#surfacespread": { + try { + int x = (int) Math.abs(Expression.compile(split2[1]).evaluate()); + int y = (int) Math.abs(Expression.compile(split2[2]).evaluate()); + int z = (int) Math.abs(Expression.compile(split2[3]).evaluate()); + rest = rest.substring(split2[1].length() + split2[2].length() + split2[3].length() + 3); + Pattern pattern = parseFromInput(rest, context); + return new SurfaceRandomOffsetPattern(pattern, x, y, z); + } catch (NumberFormatException | ExpressionException e) { + throw new InputParseException("The correct format is #spread::::"); + } + } + case "#solidspread": { + try { + int x = (int) Math.abs(Expression.compile(split2[1]).evaluate()); + int y = (int) Math.abs(Expression.compile(split2[2]).evaluate()); + int z = (int) Math.abs(Expression.compile(split2[3]).evaluate()); + rest = rest.substring(split2[1].length() + split2[2].length() + split2[3].length() + 3); + Pattern pattern = parseFromInput(rest, context); + return new SolidRandomOffsetPattern(pattern, x, y, z); + } catch (NumberFormatException | ExpressionException e) { + throw new InputParseException("The correct format is #spread::::"); + } + } case "#randomoffset": case "#spread": { try { @@ -176,6 +207,21 @@ public class HashTagPatternParser extends InputParser { } throw new InputParseException("Invalid, see: https://github.com/boy0001/FastAsyncWorldedit/wiki/WorldEdit-and-FAWE-patterns"); } + case '=': { + try { + Expression exp = Expression.compile(input.substring(1), "x", "y", "z"); + EditSession editSession = Request.request().getEditSession(); + if (editSession == null) { + editSession = context.requireSession().createEditSession((Player) context.getActor()); + } + WorldEditExpressionEnvironment env = new WorldEditExpressionEnvironment( + editSession, Vector.ONE, Vector.ZERO); + exp.setEnvironment(env); + return new ExpressionPattern(exp); + } catch (ExpressionException e) { + throw new InputParseException("Invalid expression: " + e.getMessage()); + } + } default: List items = split(input, ','); if (items.size() == 1) { @@ -183,23 +229,27 @@ public class HashTagPatternParser extends InputParser { } BlockFactory blockRegistry = worldEdit.getBlockFactory(); RandomPattern randomPattern = new RandomPattern(); - for (String token : items) { - Pattern pattern; - double chance; - // Parse special percentage syntax - if (token.matches("[0-9]+(\\.[0-9]*)?%.*")) { - String[] p = token.split("%"); - if (p.length < 2) { - throw new InputParseException("Missing the pattern after the % symbol for '" + input + "'"); + try { + for (String token : items) { + Pattern pattern; + double chance; + // Parse special percentage syntax + if (token.matches("[0-9]+(\\.[0-9]*)?%.*")) { + String[] p = token.split("%"); + if (p.length < 2) { + throw new InputParseException("Missing the pattern after the % symbol for '" + input + "'"); + } else { + chance = Expression.compile(p[0]).evaluate(); + pattern = parseFromInput(p[1], context); + } } else { - chance = Double.parseDouble(p[0]); - pattern = parseFromInput(p[1], context); + chance = 1; + pattern = parseFromInput(token, context); } - } else { - chance = 1; - pattern = parseFromInput(token, context); + randomPattern.add(pattern, chance); } - randomPattern.add(pattern, chance); + } catch (NumberFormatException | ExpressionException e) { + throw new InputParseException("Invalid, see: https://github.com/boy0001/FastAsyncWorldedit/wiki/WorldEdit-and-FAWE-patterns"); } return randomPattern; diff --git a/core/src/main/java/com/sk89q/worldedit/extent/AbstractDelegateExtent.java b/core/src/main/java/com/sk89q/worldedit/extent/AbstractDelegateExtent.java index ac3c8940..e046a834 100644 --- a/core/src/main/java/com/sk89q/worldedit/extent/AbstractDelegateExtent.java +++ b/core/src/main/java/com/sk89q/worldedit/extent/AbstractDelegateExtent.java @@ -27,7 +27,6 @@ import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.function.operation.Operation; import com.sk89q.worldedit.function.operation.OperationQueue; -import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.world.biome.BaseBiome; @@ -86,7 +85,7 @@ public abstract class AbstractDelegateExtent implements Extent { mutable.x = x; mutable.y = y; mutable.z = z; - return extent.setBlock(mutable, block); + return setBlock(mutable, block); } @Override diff --git a/core/src/main/java/com/sk89q/worldedit/extent/transform/BlockTransformExtent.java b/core/src/main/java/com/sk89q/worldedit/extent/transform/BlockTransformExtent.java index e0eceb85..6b57f6e0 100644 --- a/core/src/main/java/com/sk89q/worldedit/extent/transform/BlockTransformExtent.java +++ b/core/src/main/java/com/sk89q/worldedit/extent/transform/BlockTransformExtent.java @@ -1,24 +1,6 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - package com.sk89q.worldedit.extent.transform; +import com.boydti.fawe.FaweCache; import com.sk89q.worldedit.Vector; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseBlock; @@ -28,9 +10,9 @@ import com.sk89q.worldedit.math.transform.Transform; import com.sk89q.worldedit.world.registry.BlockRegistry; import com.sk89q.worldedit.world.registry.State; import com.sk89q.worldedit.world.registry.StateValue; - -import javax.annotation.Nullable; import java.util.Map; +import javax.annotation.Nullable; + import static com.google.common.base.Preconditions.checkNotNull; @@ -44,6 +26,8 @@ public class BlockTransformExtent extends AbstractDelegateExtent { private final Transform transform; private final BlockRegistry blockRegistry; + private final BaseBlock[] BLOCK_TRANSFORM; + private final BaseBlock[] BLOCK_TRANSFORM_INVERSE; /** * Create a new instance. @@ -57,6 +41,16 @@ public class BlockTransformExtent extends AbstractDelegateExtent { checkNotNull(blockRegistry); this.transform = transform; this.blockRegistry = blockRegistry; + BLOCK_TRANSFORM = new BaseBlock[FaweCache.CACHE_BLOCK.length]; + BLOCK_TRANSFORM_INVERSE = new BaseBlock[FaweCache.CACHE_BLOCK.length]; + Transform inverse = transform.inverse(); + for (int i = 0; i < BLOCK_TRANSFORM.length; i++) { + BaseBlock block = FaweCache.CACHE_BLOCK[i]; + if (block != null) { + BLOCK_TRANSFORM[i] = transform(new BaseBlock(block), transform, blockRegistry); + BLOCK_TRANSFORM_INVERSE[i] = transform(new BaseBlock(block), inverse, blockRegistry); + } + } } /** @@ -68,32 +62,28 @@ public class BlockTransformExtent extends AbstractDelegateExtent { return transform; } - /** - * Transform a block without making a copy. - * - * @param block the block - * @param reverse true to transform in the opposite direction - * @return the same block - */ - private BaseBlock transformBlock(BaseBlock block, boolean reverse) { - return transform(block, reverse ? transform.inverse() : transform, blockRegistry); - } - @Override public BaseBlock getBlock(Vector position) { - return transformBlock(super.getBlock(position), false); + return transformFast(super.getBlock(position)); } @Override public BaseBlock getLazyBlock(Vector position) { - return transformBlock(super.getLazyBlock(position), false); + return transformFast(super.getLazyBlock(position)); } @Override public boolean setBlock(Vector location, BaseBlock block) throws WorldEditException { - return super.setBlock(location, transformBlock(block, true)); + return super.setBlock(location, transformFastInverse(block)); } + private final BaseBlock transformFast(BaseBlock block) { + return BLOCK_TRANSFORM[FaweCache.getCombined(block)]; + } + + private final BaseBlock transformFastInverse(BaseBlock block) { + return BLOCK_TRANSFORM_INVERSE[FaweCache.getCombined(block)]; + } /** * Transform the given block using the given transform. @@ -157,7 +147,7 @@ public class BlockTransformExtent extends AbstractDelegateExtent { */ @Nullable private static StateValue getNewStateValue(State state, Transform transform, Vector oldDirection) { - Vector newDirection = transform.apply(oldDirection).subtract(transform.apply(Vector.ZERO)).normalize(); + Vector newDirection = new Vector(transform.apply(oldDirection)).subtract(transform.apply(Vector.ZERO)).normalize(); StateValue newValue = null; double closest = -2; boolean found = false; diff --git a/core/src/main/java/com/sk89q/worldedit/function/entity/ExtentEntityCopy.java b/core/src/main/java/com/sk89q/worldedit/function/entity/ExtentEntityCopy.java index cc8e1828..248775b9 100644 --- a/core/src/main/java/com/sk89q/worldedit/function/entity/ExtentEntityCopy.java +++ b/core/src/main/java/com/sk89q/worldedit/function/entity/ExtentEntityCopy.java @@ -27,7 +27,6 @@ import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.EntityFunction; -import com.sk89q.worldedit.function.operation.ForwardExtentCopy; import com.sk89q.worldedit.internal.helper.MCDirections; import com.sk89q.worldedit.math.transform.Transform; import com.sk89q.worldedit.util.Direction; @@ -99,7 +98,7 @@ public class ExtentEntityCopy implements EntityFunction { newDirection = transform.isIdentity() ? entity.getLocation().getDirection() - : transform.apply(location.getDirection()).subtract(transform.apply(Vector.ZERO)).normalize(); + : new Vector(transform.apply(location.getDirection())).subtract(transform.apply(Vector.ZERO)).normalize(); newLocation = new Location(destination, newPosition.add(to.round().add(0.5, 0.5, 0.5)), newDirection); // Some entities store their position data in NBT @@ -148,7 +147,7 @@ public class ExtentEntityCopy implements EntityFunction { Direction direction = MCDirections.fromHanging(d); if (direction != null) { - Vector vector = transform.apply(direction.toVector()).subtract(transform.apply(Vector.ZERO)).normalize(); + Vector vector = new Vector(transform.apply(direction.toVector())).subtract(transform.apply(Vector.ZERO)).normalize(); Direction newDirection = Direction.findClosest(vector, Flag.CARDINAL); builder.putByte("Direction", (byte) MCDirections.toHanging(newDirection)); diff --git a/core/src/main/java/com/sk89q/worldedit/function/mask/BlockMask.java b/core/src/main/java/com/sk89q/worldedit/function/mask/BlockMask.java index be9ef577..61b0a8ae 100644 --- a/core/src/main/java/com/sk89q/worldedit/function/mask/BlockMask.java +++ b/core/src/main/java/com/sk89q/worldedit/function/mask/BlockMask.java @@ -1,6 +1,7 @@ package com.sk89q.worldedit.function.mask; import com.boydti.fawe.FaweCache; +import com.boydti.fawe.util.StringMan; import com.sk89q.worldedit.Vector; import com.sk89q.worldedit.blocks.BaseBlock; import com.sk89q.worldedit.extent.Extent; @@ -110,6 +111,11 @@ public class BlockMask extends AbstractExtentMask { return computedLegacyList; } + @Override + public String toString() { + return StringMan.getString(getBlocks()); + } + public boolean test(int blockId) { return blockIds[blockId]; } diff --git a/core/src/main/java/com/sk89q/worldedit/math/transform/AffineTransform.java b/core/src/main/java/com/sk89q/worldedit/math/transform/AffineTransform.java new file mode 100644 index 00000000..26d6d508 --- /dev/null +++ b/core/src/main/java/com/sk89q/worldedit/math/transform/AffineTransform.java @@ -0,0 +1,307 @@ +package com.sk89q.worldedit.math.transform; + +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.math.MathUtils; + +/** + * An affine transform. + * + *

This class is from the + * JavaGeom project, + * which is licensed under LGPL v2.1.

+ */ +public class AffineTransform implements Transform { + + private Vector mutable = new Vector(); + + /** + * coefficients for x coordinate. + */ + private double m00, m01, m02, m03; + + /** + * coefficients for y coordinate. + */ + private double m10, m11, m12, m13; + + /** + * coefficients for z coordinate. + */ + private double m20, m21, m22, m23; + + // =================================================================== + // constructors + + /** + * Creates a new affine transform3D set to identity + */ + public AffineTransform() { + // init to identity matrix + m00 = m11 = m22 = 1; + m01 = m02 = m03 = 0; + m10 = m12 = m13 = 0; + m20 = m21 = m23 = 0; + } + + public AffineTransform(double[] coefs) { + if (coefs.length == 9) { + m00 = coefs[0]; + m01 = coefs[1]; + m02 = coefs[2]; + m10 = coefs[3]; + m11 = coefs[4]; + m12 = coefs[5]; + m20 = coefs[6]; + m21 = coefs[7]; + m22 = coefs[8]; + } else if (coefs.length == 12) { + m00 = coefs[0]; + m01 = coefs[1]; + m02 = coefs[2]; + m03 = coefs[3]; + m10 = coefs[4]; + m11 = coefs[5]; + m12 = coefs[6]; + m13 = coefs[7]; + m20 = coefs[8]; + m21 = coefs[9]; + m22 = coefs[10]; + m23 = coefs[11]; + } else { + throw new IllegalArgumentException( + "Input array must have 9 or 12 elements"); + } + } + + public AffineTransform(double xx, double yx, double zx, double tx, + double xy, double yy, double zy, double ty, double xz, double yz, + double zz, double tz) { + m00 = xx; + m01 = yx; + m02 = zx; + m03 = tx; + m10 = xy; + m11 = yy; + m12 = zy; + m13 = ty; + m20 = xz; + m21 = yz; + m22 = zz; + m23 = tz; + } + + // =================================================================== + // accessors + + @Override + public boolean isIdentity() { + if (m00 != 1) + return false; + if (m11 != 1) + return false; + if (m22 != 1) + return false; + if (m01 != 0) + return false; + if (m02 != 0) + return false; + if (m03 != 0) + return false; + if (m10 != 0) + return false; + if (m12 != 0) + return false; + if (m13 != 0) + return false; + if (m20 != 0) + return false; + if (m21 != 0) + return false; + if (m23 != 0) + return false; + return true; + } + + /** + * Returns the affine coefficients of the transform. Result is an array of + * 12 double. + */ + public double[] coefficients() { + return new double[]{m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23}; + } + + /** + * Computes the determinant of this transform. Can be zero. + * + * @return the determinant of the transform. + */ + private double determinant() { + return m00 * (m11 * m22 - m12 * m21) - m01 * (m10 * m22 - m20 * m12) + + m02 * (m10 * m21 - m20 * m11); + } + + /** + * Computes the inverse affine transform. + */ + @Override + public AffineTransform inverse() { + double det = this.determinant(); + return new AffineTransform( + (m11 * m22 - m21 * m12) / det, + (m21 * m01 - m01 * m22) / det, + (m01 * m12 - m11 * m02) / det, + (m01 * (m22 * m13 - m12 * m23) + m02 * (m11 * m23 - m21 * m13) + - m03 * (m11 * m22 - m21 * m12)) / det, + (m20 * m12 - m10 * m22) / det, + (m00 * m22 - m20 * m02) / det, + (m10 * m02 - m00 * m12) / det, + (m00 * (m12 * m23 - m22 * m13) - m02 * (m10 * m23 - m20 * m13) + + m03 * (m10 * m22 - m20 * m12)) / det, + (m10 * m21 - m20 * m11) / det, + (m20 * m01 - m00 * m21) / det, + (m00 * m11 - m10 * m01) / det, + (m00 * (m21 * m13 - m11 * m23) + m01 * (m10 * m23 - m20 * m13) + - m03 * (m10 * m21 - m20 * m11)) / det); + } + + // =================================================================== + // general methods + + /** + * Returns the affine transform created by applying first the affine + * transform given by {@code that}, then this affine transform. + * + * @param that the transform to apply first + * @return the composition this * that + */ + public AffineTransform concatenate(AffineTransform that) { + double n00 = m00 * that.m00 + m01 * that.m10 + m02 * that.m20; + double n01 = m00 * that.m01 + m01 * that.m11 + m02 * that.m21; + double n02 = m00 * that.m02 + m01 * that.m12 + m02 * that.m22; + double n03 = m00 * that.m03 + m01 * that.m13 + m02 * that.m23 + m03; + double n10 = m10 * that.m00 + m11 * that.m10 + m12 * that.m20; + double n11 = m10 * that.m01 + m11 * that.m11 + m12 * that.m21; + double n12 = m10 * that.m02 + m11 * that.m12 + m12 * that.m22; + double n13 = m10 * that.m03 + m11 * that.m13 + m12 * that.m23 + m13; + double n20 = m20 * that.m00 + m21 * that.m10 + m22 * that.m20; + double n21 = m20 * that.m01 + m21 * that.m11 + m22 * that.m21; + double n22 = m20 * that.m02 + m21 * that.m12 + m22 * that.m22; + double n23 = m20 * that.m03 + m21 * that.m13 + m22 * that.m23 + m23; + return new AffineTransform( + n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23); + } + + /** + * Return the affine transform created by applying first this affine + * transform, then the affine transform given by {@code that}. + * + * @param that the transform to apply in a second step + * @return the composition that * this + */ + public AffineTransform preConcatenate(AffineTransform that) { + double n00 = that.m00 * m00 + that.m01 * m10 + that.m02 * m20; + double n01 = that.m00 * m01 + that.m01 * m11 + that.m02 * m21; + double n02 = that.m00 * m02 + that.m01 * m12 + that.m02 * m22; + double n03 = that.m00 * m03 + that.m01 * m13 + that.m02 * m23 + that.m03; + double n10 = that.m10 * m00 + that.m11 * m10 + that.m12 * m20; + double n11 = that.m10 * m01 + that.m11 * m11 + that.m12 * m21; + double n12 = that.m10 * m02 + that.m11 * m12 + that.m12 * m22; + double n13 = that.m10 * m03 + that.m11 * m13 + that.m12 * m23 + that.m13; + double n20 = that.m20 * m00 + that.m21 * m10 + that.m22 * m20; + double n21 = that.m20 * m01 + that.m21 * m11 + that.m22 * m21; + double n22 = that.m20 * m02 + that.m21 * m12 + that.m22 * m22; + double n23 = that.m20 * m03 + that.m21 * m13 + that.m22 * m23 + that.m23; + return new AffineTransform( + n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23); + } + + public AffineTransform translate(Vector vec) { + return translate(vec.getX(), vec.getY(), vec.getZ()); + } + + public AffineTransform translate(double x, double y, double z) { + return concatenate(new AffineTransform(1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z)); + } + + public AffineTransform rotateX(double theta) { + double cot = MathUtils.dCos(theta); + double sit = MathUtils.dSin(theta); + return concatenate( + new AffineTransform( + 1, 0, 0, 0, + 0, cot, -sit, 0, + 0, sit, cot, 0)); + } + + public AffineTransform rotateY(double theta) { + double cot = MathUtils.dCos(theta); + double sit = MathUtils.dSin(theta); + return concatenate( + new AffineTransform( + cot, 0, sit, 0, + 0, 1, 0, 0, + -sit, 0, cot, 0)); + } + + public AffineTransform rotateZ(double theta) { + double cot = MathUtils.dCos(theta); + double sit = MathUtils.dSin(theta); + return concatenate( + new AffineTransform( + cot, -sit, 0, 0, + sit, cot, 0, 0, + 0, 0, 1, 0)); + } + + public AffineTransform scale(double s) { + return scale(s, s, s); + } + + public AffineTransform scale(double sx, double sy, double sz) { + return concatenate(new AffineTransform(sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0)); + } + + public AffineTransform scale(Vector vec) { + return scale(vec.getX(), vec.getY(), vec.getZ()); + } + + @Override + public Vector apply(Vector vector) { + // vector.getX() * m00 + vector.getY() * m01 + vector.getZ() * m02 + m03 + // vector.getX() * m10 + vector.getY() * m11 + vector.getZ() * m12 + m13 + // vector.getX() * m20 + vector.getY() * m21 + vector.getZ() * m22 + m23 + mutable.x = vector.getX() * m00 + vector.getY() * m01 + vector.getZ() * m02 + m03; + mutable.y = vector.getX() * m10 + vector.getY() * m11 + vector.getZ() * m12 + m13; + mutable.z = vector.getX() * m20 + vector.getY() * m21 + vector.getZ() * m22 + m23; + return new Vector( + vector.getX() * m00 + vector.getY() * m01 + vector.getZ() * m02 + m03, + vector.getX() * m10 + vector.getY() * m11 + vector.getZ() * m12 + m13, + vector.getX() * m20 + vector.getY() * m21 + vector.getZ() * m22 + m23); + } + + public AffineTransform combine(AffineTransform other) { + return concatenate(other); + } + + @Override + public Transform combine(Transform other) { + if (other instanceof AffineTransform) { + return concatenate((AffineTransform) other); + } else { + return new CombinedTransform(this, other); + } + } + + @Override + public String toString() { + return String.format("Affine[%g %g %g %g, %g %g %g %g, %g %g %g %g]}", m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23); + } + + public static Class inject() { + return AffineTransform.class; + } +} \ No newline at end of file