/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.jcwhatever.nucleus.utils.coords; import com.jcwhatever.nucleus.providers.math.FastMath; import com.jcwhatever.nucleus.providers.math.IRotationMatrix; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.Rand; import com.jcwhatever.nucleus.utils.ThreadSingletons; import com.jcwhatever.nucleus.utils.ThreadSingletons.ISingletonFactory; import com.jcwhatever.nucleus.utils.materials.Materials; import com.jcwhatever.nucleus.utils.text.TextUtils; import com.jcwhatever.nucleus.utils.validate.IValidator; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.util.Vector; import javax.annotation.Nullable; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Collection; /** * Location utilities. */ public final class LocationUtils { private LocationUtils () {} // array to help convert yaw to block face private static final BlockFace[] YAW_FACES = new BlockFace[] { BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST, BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST, BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST, BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST, BlockFace.SOUTH }; private static final ISingletonFactory<Location> SINGLETON_FACTORY = new ISingletonFactory<Location>() { @Override public Location create(Thread thread) { return new Location(null, 0, 0, 0); } }; private static final Vector3D VECTOR = new Vector3D(); /** * Create a new {@link ThreadSingletons}. */ public static ThreadSingletons<Location> createThreadSingleton() { return new ThreadSingletons<>(SINGLETON_FACTORY); } /** * Copy the values from a source {@link org.bukkit.Location} to a new * {@link org.bukkit.Location}. * * @param source The source location. * * @return A new {@link org.bukkit.Location}. */ public static Location copy(Location source) { PreCon.notNull(source); Location destination = new Location(null, 0, 0, 0); return copy(source, destination); } /** * Copy the values from a source {@link org.bukkit.Location} to a destination * {@link org.bukkit.Location}. * * @param source The source location. * @param destination The destination location. * * @return The destination {@link org.bukkit.Location}. */ public static Location copy(Location source, Location destination) { PreCon.notNull(source); PreCon.notNull(destination); destination.setWorld(source.getWorld()); destination.setX(source.getX()); destination.setY(source.getY()); destination.setZ(source.getZ()); destination.setYaw(source.getYaw()); destination.setPitch(source.getPitch()); return destination; } /** * Copy the values from a source {@link org.bukkit.Location} to a destination * {@link org.bukkit.util.Vector}. * * @param source The source location. * @param destination The destination vector. * * @return The destination {@link org.bukkit.util.Vector}. */ public static Vector copy(Location source, Vector destination) { PreCon.notNull(source); PreCon.notNull(destination); destination.setX(source.getX()); destination.setY(source.getY()); destination.setZ(source.getZ()); return destination; } /** * Copy the values from a source {@link org.bukkit.util.Vector} to a new * {@link org.bukkit.util.Vector}. * * @param source The source location. * * @return A new {@link org.bukkit.util.Vector}. */ public static Vector copy(Vector source) { PreCon.notNull(source); Vector vector = new Vector(0, 0, 0); return copy(source, vector); } /** * Copy the values from a source {@link org.bukkit.util.Vector} to a destination * {@link org.bukkit.util.Vector}. * * @param source The source location. * @param destination The destination vector. * * @return The destination {@link org.bukkit.util.Vector}. */ public static Vector copy(Vector source, Vector destination) { PreCon.notNull(source); PreCon.notNull(destination); destination.setX(source.getX()); destination.setY(source.getY()); destination.setZ(source.getZ()); return destination; } /** * Copy a source {@link org.bukkit.Location} and center the X and Z coordinates of the * copy to the source locations block. * * @param source The source location. * * @return A new {@link org.bukkit.Location} containing the result. */ public static Location getCenteredLocation(Location source) { PreCon.notNull(source); return getCenteredLocation(source, new Location(null, 0, 0, 0)); } /** * Copy a source {@link org.bukkit.Location} to an output {@link org.bukkit.Location} and * center the X and Z coordinates of the output to the source locations block. * * @param source The source location. * @param output The location to put the results into. * * @return The output {@link org.bukkit.Location}. */ public static Location getCenteredLocation(Location source, Location output) { PreCon.notNull(source); PreCon.notNull(output); output.setWorld(source.getWorld()); output.setX(source.getBlockX() + 0.5); output.setY(source.getY()); output.setZ(source.getBlockZ() + 0.5); output.setYaw(source.getYaw()); output.setPitch(source.getPitch()); return output; } /** * Copy a source {@link org.bukkit.Location} and change coordinate values to block * coordinates in the copy. * * <p>Removes yaw and pitch values, converts coordinates to whole numbers.</p> * * @param source The source location. * * @return A new {@link org.bukkit.Location} containing the result. */ public static Location getBlockLocation(Location source) { PreCon.notNull(source); return getBlockLocation(source, new Location(null, 0, 0, 0)); } /** * Copy a source {@link org.bukkit.Location} to an output location and change coordinate * values to block coordinates in the output. * * <p>Removes yaw and pitch values, converts coordinates to whole numbers.</p> * * @param source The source location. * @param output The location to put the results into. * * @return The output {@link org.bukkit.Location}. */ public static Location getBlockLocation(Location source, Location output) { PreCon.notNull(source); PreCon.notNull(output); output.setWorld(source.getWorld()); output.setX(source.getBlockX()); output.setY(source.getBlockY()); output.setZ(source.getBlockZ()); output.setYaw(0); output.setPitch(0); return output; } /** * Copy a source {@link org.bukkit.Location} and add values to the copy without changing * the original {@link org.bukkit.Location}. * * @param source The source location. * @param x The value to add to the X coordinates. * @param y The value to add to the Y coordinates. * @param z The value to add to the Z coordinates. * * @return A new {@link org.bukkit.Location}. */ public static Location add(Location source, double x, double y, double z) { return add(source, x, y, z, new Location(null, 0, 0, 0)); } /** * Copy a source {@link org.bukkit.Location} to an output location and add values to the * output without changing the original {@link org.bukkit.Location}. * * @param source The source location. * @param x The value to add to the X coordinates. * @param y The value to add to the Y coordinates. * @param z The value to add to the Z coordinates. * @param output The location to put the results into. * * @return The output {@link org.bukkit.Location}. */ public static Location add(Location source, double x, double y, double z, Location output) { return copy(source, output).add(x, y, z); } /** * Copy a source {@link org.bukkit.Location} and add noise to the copy. * * <p>Translates the location to another random location within the specified * radius of the source location randomly.</p> * * @param source The location. * @param radiusX The max radius on the X axis. * @param radiusY The max radius on the Y axis. * @param radiusZ The max radius on the Z axis. * * @return A new {@link org.bukkit.Location}. */ public static Location addNoise(Location source, double radiusX, double radiusY, double radiusZ) { PreCon.notNull(source); return addNoise(source, radiusX, radiusY, radiusZ, new Location(null, 0, 0, 0)); } /** * Copy a source {@link org.bukkit.Location} to an output location and add noise * to the output. * * <p>Translates the location to another random location within the specified * radius of the source location randomly.</p> * * @param source The location. * @param radiusX The max radius on the X axis. * @param radiusY The max radius on the Y axis. * @param radiusZ The max radius on the Z axis. * @param output The location to put the results into. * * @return The output {@link org.bukkit.Location}. */ public static Location addNoise(Location source, double radiusX, double radiusY, double radiusZ, Location output) { PreCon.notNull(source); PreCon.positiveNumber(radiusX); PreCon.positiveNumber(radiusY); PreCon.positiveNumber(radiusZ); PreCon.notNull(output); double noiseX = 0; double noiseY = 0; double noiseZ = 0; if (radiusX > 0) { noiseX = Rand.getDouble(radiusX * 2) - radiusX; } if (radiusY > 0) { noiseY = Rand.getDouble(radiusY * 2) - radiusY; } if (radiusZ > 0) { noiseZ = Rand.getDouble(radiusZ * 2) - radiusZ; } return copy(source, output).add(noiseX, noiseY, noiseZ); } /** * Determine if 2 locations can be considered the same using the specified * precision. * * <p>The precision is used as: location1 is about the same as location2 +/- precision.</p> * * @param location1 The first location to compare. * @param location2 The second location to compare. * @param precision The precision. */ public static boolean isLocationMatch(Location location1, Location location2, double precision) { PreCon.notNull(location1); PreCon.notNull(location2); PreCon.positiveNumber(precision); double xDelta = Math.abs(location1.getX() - location2.getX()); double zDelta = Math.abs(location1.getZ() - location2.getZ()); double yDelta = Math.abs(location1.getY() - location2.getY()); return xDelta <= precision && zDelta <= precision && yDelta <= precision; } /** * Parse a {@link org.bukkit.Location} from a formatted string. * * <p>Format of string : x,y,z</p> * * @param world The world the location is for. * @param coordinates The string coordinates. * * @return A new {@link org.bukkit.Location} or null if a location could not be parsed. */ @Nullable public static Location parseSimpleLocation(World world, String coordinates) { return parseSimpleLocation(world, coordinates, new Location(null, 0, 0, 0)); } /** * Parse a {@link org.bukkit.Location} from a formatted string. * * <p>Format of string : x,y,z</p> * * @param world The world the location is for. * @param coordinates The string coordinates. * @param output The location place the results in. * * @return The output {@link org.bukkit.Location} or null if a location could not be parsed. */ @Nullable public static Location parseSimpleLocation(World world, String coordinates, Location output) { PreCon.notNull(output); PreCon.notNull(world); PreCon.notNull(coordinates); String[] parts = TextUtils.PATTERN_COMMA.split(coordinates); if (parts.length != 3) return null; double x = TextUtils.parseDouble(parts[0], Double.MAX_VALUE); double y = TextUtils.parseDouble(parts[1], Double.MAX_VALUE); double z = TextUtils.parseDouble(parts[2], Double.MAX_VALUE); if (x != Double.MAX_VALUE && y != Double.MAX_VALUE && z != Double.MAX_VALUE) { output.setWorld(world); output.setX(x); output.setY(y); output.setZ(z); return output; } return null; } /** * Parse a {@link org.bukkit.Location} from a formatted string. * * <p>Format of string: x,y,z,yawF,pitchF,worldName</p> * * @param coordinates The string coordinates. * * @return A new {@link SyncLocation} or null if the string could not be parsed. */ @Nullable public static SyncLocation parseLocation(String coordinates) { PreCon.notNull(coordinates); SyncLocation location = new SyncLocation((World)null, 0, 0, 0); return parseLocation(coordinates, location); } /** * Parse a location from a formatted string. * * <p>Format of string: x,y,z,yawF,pitchF,worldName</p> * * @param coordinates The string coordinates. * * @return A new {@link SyncLocation} or null if the string could not be parsed. */ @Nullable public static SyncLocation parseLocation(String coordinates, SyncLocation output) { PreCon.notNull(coordinates); String[] parts = TextUtils.PATTERN_COMMA.split(coordinates); if (parts.length != 6) return null; double x = TextUtils.parseDouble(parts[0], Double.MAX_VALUE); if (x == Double.MAX_VALUE) return null; double y = TextUtils.parseDouble(parts[1], Double.MAX_VALUE); if (y == Double.MAX_VALUE) return null; double z = TextUtils.parseDouble(parts[2], Double.MAX_VALUE); if (z == Double.MAX_VALUE) return null; float yaw = TextUtils.parseFloat(parts[3], Float.MAX_VALUE); if (yaw == Float.MAX_VALUE) return null; float pitch = TextUtils.parseFloat(parts[4], Float.MAX_VALUE); if (pitch == Float.MAX_VALUE) return null; output.setWorld(parts[5]); output.setX(x); output.setY(y); output.setZ(z); output.setYaw(yaw); output.setPitch(pitch); return output; } /** * Parse the world name from a from a location formatted string. * * <p>Format of string: x,y,z,yawF,pitchF,worldName</p> * * <p>Useful when the world the location is for is not loaded and * the name is needed.</p> * * @param coordinates The string coordinates. * * @return Null if the string could not be parsed. */ @Nullable public static String parseLocationWorldName(String coordinates) { PreCon.notNull(coordinates); String[] parts = TextUtils.PATTERN_COMMA.split(coordinates); if (parts.length != 6) return null; return parts[5]; } /** * Convert a {@link org.bukkit.Location} to a parsable string. * * @param location The location to convert. */ public static String serialize(Location location) { PreCon.notNull(location); return String.valueOf(location.getX()) + ',' + location.getY() + ',' + location.getZ() + ',' + location.getYaw() + ',' + location.getPitch() + ',' + location.getWorld().getName(); } /** * Convert a {@link org.bukkit.Location} to a parsable string. * * @param location The location to convert. * @param floatingPointPlaces The number of places in the floating point values. */ public static String serialize(Location location, int floatingPointPlaces) { PreCon.notNull(location); PreCon.positiveNumber(floatingPointPlaces); BigDecimal x = new BigDecimal(floatingPointPlaces == 0 ? location.getBlockX() : location.getX()) .setScale(floatingPointPlaces, RoundingMode.HALF_UP); BigDecimal y = new BigDecimal(floatingPointPlaces == 0 ? location.getBlockY() : location.getY()) .setScale(floatingPointPlaces, RoundingMode.HALF_UP); BigDecimal z = new BigDecimal(floatingPointPlaces == 0 ? location.getBlockZ() : location.getZ()) .setScale(floatingPointPlaces, RoundingMode.HALF_UP); BigDecimal yaw = new BigDecimal(location.getYaw()) .setScale(floatingPointPlaces, RoundingMode.HALF_UP); BigDecimal pitch = new BigDecimal(location.getPitch()) .setScale(floatingPointPlaces, RoundingMode.HALF_UP); return String.valueOf(x) + ',' + y + ',' + z + ',' + yaw + ',' + pitch + ',' + location.getWorld().getName(); } /** * Convert a locations yaw angle to a {@link org.bukkit.block.BlockFace}. * * @param location The location to convert. */ public static BlockFace getYawBlockFace(Location location) { PreCon.notNull(location); return getYawBlockFace(location.getYaw()); } /** * Convert a yaw angle to a {@link org.bukkit.block.BlockFace}. * * @param yaw The yaw angle to convert. */ public static BlockFace getYawBlockFace(float yaw) { yaw = yaw + 11.25f; yaw = yaw < 0 ? 360 - (Math.abs(yaw) % 360) : yaw % 360; int i = (int)(yaw / 22.5); return YAW_FACES[i]; } /** * Find a surface block (solid block that can be walked on) {@link org.bukkit.Location} * below the provided search location. * * @param source The source location. * * @return A new {@link org.bukkit.Location} or null if the search reaches below 0 on * the Y axis. */ @Nullable public static Location findSurfaceBelow(Location source) { return findSurfaceBelow(source, new Location(null, 0, 0, 0)); } /** * Find a surface block (solid block that can be walked on) {@link org.bukkit.Location} * below the specified source location. * * @param source The source location. * * @return The output {@link org.bukkit.Location} or null if the search reaches below * 0 on the Y axis. */ @Nullable public static Location findSurfaceBelow(Location source, Location output) { PreCon.notNull(source); PreCon.notNull(output); output.setWorld(source.getWorld()); output.setX(source.getX()); output.setY(source.getBlockY()); output.setZ(source.getZ()); output.setYaw(source.getYaw()); output.setPitch(source.getPitch()); if (!Materials.isTransparent(output.getBlock().getType())) return output; output.add(0, -1, 0); Block current = output.getBlock(); while (!Materials.isSurface(current.getType())) { output.add(0, -1, 0); current = output.getBlock(); if (output.getY() < 0) { return null; } } return output; } /** * Get the {@link org.bukkit.Location} closest to the specified source location. * * @param source The source location. * @param locations The location candidates. */ @Nullable public static Location getClosestLocation(Location source, Collection<Location> locations) { return getClosestLocation(source, locations, null); } /** * Get the {@link org.bukkit.Location} closest to the specified source location. * * @param source The source location. * @param locations The location candidates. * @param validator The validator used to determine if a location is a candidate. */ @Nullable public static Location getClosestLocation(Location source, Collection<Location> locations, @Nullable IValidator<Location> validator) { PreCon.notNull(source); PreCon.notNull(locations); Location closest = null; double closestDist = Double.MAX_VALUE; for (Location loc : locations) { if (validator != null && !validator.isValid(loc)) continue; double dist; if ((dist = source.distanceSquared(loc)) < closestDist) { closest = loc; closestDist = dist; } } return closest; } /** * Determine if a target {@link org.bukkit.Location} is within the specified radius of * a source location. * * <p>If all radius values are equal, the radius is spherical. Otherwise the radius is cuboid.</p> * * @param source The source {@link org.bukkit.Location}. * @param target The target {@link org.bukkit.Location}. * @param radiusX The x-axis radius. * @param radiusY The y-axis radius. * @param radiusZ The z-axis radius. */ public static boolean isInRange(Location source, Location target, double radiusX, double radiusY, double radiusZ) { PreCon.notNull(source); PreCon.notNull(target); PreCon.positiveNumber(radiusX); PreCon.positiveNumber(radiusY); PreCon.positiveNumber(radiusZ); if (Double.compare(radiusX, radiusZ) == 0 && Double.compare(radiusY, radiusZ) == 0) { return source.distanceSquared(target) <= radiusX * radiusX; } else { double deltaX = Math.abs(source.getX() - target.getX()); double deltaY = Math.abs(source.getY() - target.getY()); double deltaZ = Math.abs(source.getZ() - target.getZ()); return deltaX <= radiusX && deltaY <= radiusY && deltaZ <= radiusZ; } } /** * Get a {@link org.bukkit.Location} that is a specified distance from a source location * using the source locations yaw angle to determine the direction of the new location * from the source location. * * <p>The new points Y coordinates are the same as the source location.</p> * * @param source The source location. * @param distance The distance from the source location. * * @return A new {@link org.bukkit.Location}. */ public static Location getYawLocation(Location source, double distance) { return getYawLocation(source, distance, source.getYaw(), new Location(null, 0, 0, 0)); } /** * Get a {@link org.bukkit.Location} that is a specified distance from a source location * using the source locations yaw angle to determine the direction of the new location * from the source location. * * <p>The new points Y coordinates are the same as the source location.</p> * * @param source The source location. * @param distance The distance from the source location. * @param output The {@link org.bukkit.Location} to output the results into. * * @return The output {@link org.bukkit.Location}. */ public static Location getYawLocation(Location source, double distance, Location output) { return getYawLocation(source, distance, source.getYaw(), output); } /** * Get a {@link org.bukkit.Location} that is a specified distance from a source location * using the specified yaw angle to determine the direction of the new location * from the source location. * * <p>The new points Y coordinates are the same as the source location.</p> * * @param source The source location. * @param distance The distance from the source location. * @param yaw The minecraft yaw angle (-180 to 180). * * @return A new {@link org.bukkit.Location}. */ public static Location getYawLocation(Location source, double distance, float yaw) { return getYawLocation(source, distance, yaw, new Location(null, 0, 0, 0)); } /** * Get a {@link org.bukkit.Location} that is a specified distance from a source location * using the specified yaw angle to determine the direction of the new location * from the source location. * * <p>The new points Y coordinates are the same as the source location.</p> * * @param source The source location. * @param distance The distance from the source location. * @param yaw The minecraft yaw angle (-180 to 180). * @param output The {@link org.bukkit.Location} to output the result into. * * @return The output {@link org.bukkit.Location}. */ public static Location getYawLocation(Location source, double distance, float yaw, Location output) { PreCon.notNull(source); PreCon.notNull(output); yaw = clampYaw(yaw); double x = FastMath.sin(-yaw) * distance; double z = FastMath.cos(-yaw) * distance; output.setWorld(source.getWorld()); output.setX(source.getX() + x); output.setY(source.getY()); output.setZ(source.getZ() + z); output.setYaw(source.getYaw()); output.setPitch(source.getPitch()); return output; } /** * Get the Minecraft yaw angle from the source location towards the target location. * * @param source The source {@link org.bukkit.Location}. * @param target The target {@link org.bukkit.Location}. */ public static float getYawAngle(Location source, Location target) { PreCon.notNull(source); PreCon.notNull(target); double deltaX = target.getX() - source.getX(); double deltaZ = target.getZ() - source.getZ(); double angle = FastMath.atan2(deltaX, deltaZ); float result = (float)Math.toDegrees(angle); return Float.compare(result, 0.0f) == 0 ? result : -result; } /** * Ensure a yaw angle is between -180 and 180 degrees. * * @param yaw The yaw angle to clamp. * * @return Clamped yaw angle. */ public static float clampYaw(float yaw) { while (yaw > 180f) yaw -= 360f; while (yaw < -180f) yaw += 360f; return yaw; } /** * Ensure a pitch angle is no more than 90 degrees and no less than -90 degrees. * * @param pitch The pitch angle to limit. * * @return Limited pitch angle. */ public static float limitPitch(float pitch) { return pitch >= 0 ? Math.min(90, pitch) : Math.max(-90, pitch); } /** * Rotate a {@link org.bukkit.Location} around an axis {@link org.bukkit.Location}. * * @param axis The axis location. * @param location The location to move. * @param rotationX The rotation around the X axis in degrees. * @param rotationY The rotation around the Y axis in degrees. * @param rotationZ The rotation around the Z axis in degrees. */ public static Location rotate(Location axis, Location location, double rotationX, double rotationY, double rotationZ) { PreCon.notNull(axis); PreCon.notNull(location); return rotate(axis, location, rotationX, rotationY, rotationZ, new Location(null, 0, 0, 0)); } /** * Rotate a {@link org.bukkit.Location} around an axis {@link org.bukkit.Location}. * * <p>Rotations are performed in the order: X, Y, then Z</p> * * @param axis The axis location. * @param location The location to move. * @param rotationX The rotation around the X axis in degrees. * @param rotationY The rotation around the Y axis in degrees. * @param rotationZ The rotation around the Z axis in degrees. * @param output The location to put results into. * * @return The output location. */ public static Location rotate(Location axis, Location location, double rotationX, double rotationY, double rotationZ, Location output) { PreCon.notNull(axis); PreCon.notNull(location); PreCon.notNull(output); Vector3D vector = VECTOR.copyFrom3D(location).subtract3D(axis); if (rotationX != 0.0D) { IRotationMatrix xMatrix = FastMath.getRotationMatrix((float) rotationX); xMatrix.rotateX(vector, vector); } if (rotationY != 0.0D) { IRotationMatrix yMatrix = FastMath.getRotationMatrix((float) rotationY); yMatrix.rotateY(vector, vector); } if (rotationZ != 0.0D) { IRotationMatrix zMatrix = FastMath.getRotationMatrix((float) rotationZ); zMatrix.rotateZ(vector, vector); } output.setWorld(location.getWorld()); output.setYaw(location.getYaw() + (float)rotationY); output.setPitch(location.getPitch()); vector.add3D(axis).copyTo3D(output); return output; } }