diff --git a/src/main/java/com/gtnewhorizon/structurelib/alignment/IntegerAxisSwap.java b/src/main/java/com/gtnewhorizon/structurelib/alignment/IntegerAxisSwap.java index 526ba1f1..b9d69a5f 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/alignment/IntegerAxisSwap.java +++ b/src/main/java/com/gtnewhorizon/structurelib/alignment/IntegerAxisSwap.java @@ -5,7 +5,11 @@ import net.minecraft.util.Vec3; import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Vector3i; + import com.gtnewhorizon.structurelib.alignment.enumerable.Direction; +import com.gtnewhorizon.structurelib.coords.CoordinateSystem; +import com.gtnewhorizon.structurelib.coords.Position; import com.gtnewhorizon.structurelib.util.Vec3Impl; public class IntegerAxisSwap { @@ -91,4 +95,32 @@ public void inverseTranslate(double[] point, double[] out) { out[1] = forFirstAxis.get1() * point[0] + forSecondAxis.get1() * point[1] + forThirdAxis.get1() * point[2]; out[2] = forFirstAxis.get2() * point[0] + forSecondAxis.get2() * point[1] + forThirdAxis.get2() * point[2]; } + + public Vector3i translate(Vector3i point) { + return new Vector3i( + forFirstAxis.get0() * point.x() + forFirstAxis.get1() * point.y() + forFirstAxis.get2() * point.z(), + forSecondAxis.get0() * point.x() + forSecondAxis.get1() * point.y() + forSecondAxis.get2() * point.z(), + forThirdAxis.get0() * point.x() + forThirdAxis.get1() * point.y() + forThirdAxis.get2() * point.z()); + } + + public Vector3i inverseTranslate(Vector3i point) { + return new Vector3i( + forFirstAxis.get0() * point.x() + forSecondAxis.get0() * point.y() + forThirdAxis.get0() * point.z(), + forFirstAxis.get1() * point.x() + forSecondAxis.get1() * point.y() + forThirdAxis.get1() * point.z(), + forFirstAxis.get2() * point.x() + forSecondAxis.get2() * point.y() + forThirdAxis.get2() * point.z()); + } + + public > Position translate(Position point) { + return new Position<>( + forFirstAxis.get0() * point.x() + forFirstAxis.get1() * point.y() + forFirstAxis.get2() * point.z(), + forSecondAxis.get0() * point.x() + forSecondAxis.get1() * point.y() + forSecondAxis.get2() * point.z(), + forThirdAxis.get0() * point.x() + forThirdAxis.get1() * point.y() + forThirdAxis.get2() * point.z()); + } + + public > Position inverseTranslate(Position point) { + return new Position<>( + forFirstAxis.get0() * point.x() + forSecondAxis.get0() * point.y() + forThirdAxis.get0() * point.z(), + forFirstAxis.get1() * point.x() + forSecondAxis.get1() * point.y() + forThirdAxis.get1() * point.z(), + forFirstAxis.get2() * point.x() + forSecondAxis.get2() * point.y() + forThirdAxis.get2() * point.z()); + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/alignment/enumerable/ExtendedFacing.java b/src/main/java/com/gtnewhorizon/structurelib/alignment/enumerable/ExtendedFacing.java index 6358b1a6..a92d840e 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/alignment/enumerable/ExtendedFacing.java +++ b/src/main/java/com/gtnewhorizon/structurelib/alignment/enumerable/ExtendedFacing.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableSet; import com.gtnewhorizon.structurelib.alignment.IAlignment; import com.gtnewhorizon.structurelib.alignment.IntegerAxisSwap; +import com.gtnewhorizon.structurelib.coords.StructureRelativeCoords; import com.gtnewhorizon.structurelib.util.Vec3Impl; public enum ExtendedFacing { @@ -166,6 +167,7 @@ public enum ExtendedFacing { private final String name; private final IntegerAxisSwap integerAxisSwap; + private final StructureRelativeCoords coordinateSystem; ExtendedFacing(String name) { this.name = name; @@ -247,6 +249,7 @@ public enum ExtendedFacing { this.b = b; this.c = c; integerAxisSwap = new IntegerAxisSwap(a, b, c); + coordinateSystem = new StructureRelativeCoords(integerAxisSwap); } public static ExtendedFacing of(ForgeDirection direction, Rotation rotation, Flip flip) { @@ -437,4 +440,8 @@ public ForgeDirection getRelativeBackInWorld() { public ForgeDirection getRelativeForwardInWorld() { return c.getOpposite(); } + + public StructureRelativeCoords asCoordinateSystem() { + return coordinateSystem; + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/ControllerRelativeCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/ControllerRelativeCoords.java new file mode 100644 index 00000000..96fdb664 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/ControllerRelativeCoords.java @@ -0,0 +1,40 @@ +package com.gtnewhorizon.structurelib.coords; + +/** + * Controller-relative coordinates. The controller is at 0,0,0. No axis swapping takes place, each axis points in the + * same directions as the matching world axis. + */ +public class ControllerRelativeCoords implements CoordinateSystem { + + public final int controllerX, controllerY, controllerZ; + + public ControllerRelativeCoords(int controllerX, int controllerY, int controllerZ) { + this.controllerX = controllerX; + this.controllerY = controllerY; + this.controllerZ = controllerZ; + } + + @Override + public Position translate(Position position) { + return translate(position, controllerX, controllerY, controllerZ); + } + + @Override + public Position translateInverse(Position position) { + return translateInverse(position, controllerX, controllerY, controllerZ); + } + + public static Position translate(Position position, int controllerX, + int controllerY, int controllerZ) { + position.sub(controllerX, controllerY, controllerZ); + + return CoordinateSystem.transmute(position); + } + + public static Position translateInverse(Position position, int controllerX, + int controllerY, int controllerZ) { + position.add(controllerX, controllerY, controllerZ); + + return CoordinateSystem.transmute(position); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/CoordinateSystem.java b/src/main/java/com/gtnewhorizon/structurelib/coords/CoordinateSystem.java new file mode 100644 index 00000000..69e21457 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/CoordinateSystem.java @@ -0,0 +1,34 @@ +package com.gtnewhorizon.structurelib.coords; + +import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; + +/** + * A coordinate system is a statically-checked helper for determining which coordinates a {@link Position} has. + * Translation methods never copy a {@link Position} - if a position must remain unchanged, it must be copied + * separately. Most coordinate systems will have static methods that allow you to avoid allocations, though some (such + * as the ones provided by {@link ExtendedFacing#asCoordinateSystem()}) have no reason to be static. + * + * @param + * @param + */ +public interface CoordinateSystem, Parent extends CoordinateSystem> { + + /** + * Translates a position from the parent coordinate system into this one. + */ + Position translate(Position position); + + /** + * Translates a position from this coordinate system into the parent. + */ + Position translateInverse(Position position); + + /** + * Helper method that converts a position's coordinate system. Should not be used unless you know it's correct. + */ + static , P2 extends CoordinateSystem> Position transmute( + Position pos) { + // noinspection unchecked + return (Position) pos; + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java b/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java new file mode 100644 index 00000000..6b84b2a8 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java @@ -0,0 +1,73 @@ +package com.gtnewhorizon.structurelib.coords; + +import net.minecraft.util.MathHelper; +import net.minecraft.util.Vec3; +import net.minecraft.world.ChunkPosition; + +import org.joml.Vector3f; +import org.joml.Vector3i; + +import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; +import com.gtnewhorizon.structurelib.util.Vec3Impl; + +/** + * A {@link BlockPos} with a statically-checked coordinate system. + */ +public class Position> extends BlockPos { + + public Position() { + + } + + public Position(int x, int y, int z) { + super(x, y, z); + } + + public Position(float x, float y, float z) { + super(MathHelper.floor_float(x), MathHelper.floor_float(y), MathHelper.floor_float(z)); + } + + public Position(double x, double y, double z) { + super(MathHelper.floor_double(x), MathHelper.floor_double(y), MathHelper.floor_double(z)); + } + + public Position(ChunkPosition chunkPosition) { + super(chunkPosition); + } + + public Position(Vector3i v) { + super(v.x, v.y, v.z); + } + + public Position(Vector3f v) { + this(v.x, v.y, v.z); + } + + public Position(Vec3 v) { + this(v.xCoord, v.yCoord, v.zCoord); + } + + public Position(Vec3Impl v) { + this(v.get0(), v.get1(), v.get2()); + } + + public Position copy() { + return new Position<>(x, y, z); + } + + public Vector3i toVector3i() { + return new Vector3i(x, y, z); + } + + public Vector3f toVector3f() { + return new Vector3f(x, y, z); + } + + public Vec3 toVec3() { + return Vec3.createVectorHelper(x, y, z); + } + + public Vec3Impl toVec3Impl() { + return new Vec3Impl(x, y, z); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/StructureDefinitionCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureDefinitionCoords.java new file mode 100644 index 00000000..b764b1e6 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureDefinitionCoords.java @@ -0,0 +1,38 @@ +package com.gtnewhorizon.structurelib.coords; + +/// The structure definition-local coords. 0,0,0 is the front-most left-most top-most element. Coordinates increase as +/// you go down, right, or back. +public class StructureDefinitionCoords implements CoordinateSystem { + + public final int offsetX, offsetY, offsetZ; + + public StructureDefinitionCoords(int offsetX, int offsetY, int offsetZ) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } + + @Override + public Position translate(Position position) { + return translate(position, offsetX, offsetY, offsetZ); + } + + @Override + public Position translateInverse(Position position) { + return translateInverse(position, offsetX, offsetY, offsetZ); + } + + public static Position translate(Position position, int offsetX, + int offsetY, int offsetZ) { + position.add(offsetX, offsetY, offsetZ); + + return CoordinateSystem.transmute(position); + } + + public static Position translateInverse(Position position, + int offsetX, int offsetY, int offsetZ) { + position.sub(offsetX, offsetY, offsetZ); + + return CoordinateSystem.transmute(position); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java new file mode 100644 index 00000000..ea52ffff --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java @@ -0,0 +1,31 @@ +package com.gtnewhorizon.structurelib.coords; + +import com.gtnewhorizon.structurelib.alignment.IntegerAxisSwap; + +/** + * Coordinates relative to the structure. 0,0,0 is the controller. 0,0,1 is the block immediately behind the controller + * (looking at it from its face). 1,0,0 is the block to the right of the controller. 0,1,0 is the block below the + * controller. + */ +public class StructureRelativeCoords implements CoordinateSystem { + + public final IntegerAxisSwap axisSwap; + + public StructureRelativeCoords(IntegerAxisSwap axisSwap) { + this.axisSwap = axisSwap; + } + + @Override + public Position translate(Position position) { + position.set(axisSwap.translate(position)); + + return CoordinateSystem.transmute(position); + } + + @Override + public Position translateInverse(Position position) { + position.set(axisSwap.inverseTranslate(position)); + + return CoordinateSystem.transmute(position); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/WorldCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/WorldCoords.java new file mode 100644 index 00000000..73b7e36b --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/WorldCoords.java @@ -0,0 +1,21 @@ +package com.gtnewhorizon.structurelib.coords; + +/** + * World block coordinates + */ +public class WorldCoords implements CoordinateSystem { + + public static final WorldCoords INSTANCE = new WorldCoords(); + + private WorldCoords() {} + + @Override + public Position translate(Position position) { + return position; + } + + @Override + public Position translateInverse(Position position) { + return position; + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java index c95f8172..6c69fa52 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java @@ -3,8 +3,11 @@ import static com.gtnewhorizon.structurelib.structure.IStructureWalker.ignoreBlockUnloaded; import static com.gtnewhorizon.structurelib.structure.IStructureWalker.skipBlockUnloaded; +import java.util.List; import java.util.function.Function; +import javax.annotation.Nonnull; + import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; @@ -15,6 +18,8 @@ import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable; import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; +import com.gtnewhorizon.structurelib.coords.Position; +import com.gtnewhorizon.structurelib.coords.StructureDefinitionCoords; /** * This is the structure definition of your multi. You will have one of these for each multi. @@ -335,6 +340,49 @@ default void iterate(String piece, World world, ExtendedFacing extendedFacing, i "customIterate"); } + /** + * Gets the controller position for a piece. Controller positions are denoted by a tilde. + * + * @param piece The piece name + * @return The controller position, or null if it doesn't exist for the given piece + */ + Position getControllerPosition(String piece); + + /** + * Gets the coordinate system for a piece. This is used to translate structure definition coordinates into structure + * relative coordinates. + * + * @param piece The piece + * @return The coordinate system + * @throws IllegalArgumentException When the piece does not exist + */ + StructureDefinitionCoords getCoordinateSystem(String piece); + + /** + * Gets a socket from a piece. + * + * @param piece The piece name + * @param socket The socket character + * @return The socket's position + * @throws IllegalArgumentException When the piece does not exist + * @throws IllegalArgumentException When the socket does not exist + * @throws IllegalStateException When more than one socket exists with this character + * @see StructureDefinition.Builder#addSocket(char, char) + */ + Position getSocket(String piece, char socket); + + /** + * Gets all sockets with a given character for a piece. + * + * @param piece The piece name + * @param socket The socket character + * @return The list of socket positions, or an empty list if the socket is not present + * @throws IllegalArgumentException When the piece does not exist + * @see StructureDefinition.Builder#addSocket(char, char) + */ + @Nonnull + List> getAllSockets(String piece, char socket); + /** * Low level utility. * diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java index 2c335b9d..02448d04 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java @@ -4,33 +4,49 @@ import static com.gtnewhorizon.structurelib.structure.StructureUtility.notAir; import static com.gtnewhorizon.structurelib.structure.StructureUtility.step; -import java.util.Arrays; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; -import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.gtnewhorizon.structurelib.coords.Position; +import com.gtnewhorizon.structurelib.coords.StructureDefinitionCoords; import com.gtnewhorizon.structurelib.util.Vec3Impl; +import it.unimi.dsi.fastutil.chars.Char2CharOpenHashMap; +import it.unimi.dsi.fastutil.chars.CharOpenHashSet; + public class StructureDefinition implements IStructureDefinition { private final Map> elements; private final Map shapes; private final Map[]> structures; private final Map> occupiedSpaces; + private final Map> controllers; + private final Map>> socketLocations; public static Builder builder() { return new Builder<>(); } private StructureDefinition(Map> elements, Map shapes, - Map[]> structures, Map> occupiedSpaces) { + Map[]> structures, Map> occupiedSpaces, + Map> controllers, + Map>> socketLocations) { this.elements = elements; this.shapes = shapes; this.structures = structures; this.occupiedSpaces = occupiedSpaces; + this.controllers = controllers; + this.socketLocations = socketLocations; } public static class Builder { @@ -41,12 +57,18 @@ public static class Builder { private char d = '\uD000'; private final Map navigates; private final Map> elements; + private final Map uncompiledShapes; private final Map shapes; private final Map> occupiedSpaces; + private final Map> controllers = new HashMap<>(); + private final Char2CharOpenHashMap socketTranslations = new Char2CharOpenHashMap(); + private final Map>> socketLocations = new HashMap<>(); + private Builder() { navigates = new HashMap<>(); elements = new HashMap<>(); + uncompiledShapes = new HashMap<>(); shapes = new HashMap<>(); occupiedSpaces = new HashMap<>(); } @@ -82,7 +104,69 @@ public Map getShapes() { * @return this builder */ public Builder addShape(String name, String[][] structurePiece) { + uncompiledShapes.put(name, structurePiece); + + return this; + } + + /** + * @deprecated use the unboxed version + */ + @Deprecated + public Builder addElement(Character name, IStructureElement structurePiece) { + elements.putIfAbsent(name, structurePiece); + return this; + } + + public Builder addElement(char name, IStructureElement structurePiece) { + elements.putIfAbsent(name, structurePiece); + return this; + } + + /** + * Denotes {@code name} as a socket. Sockets are virtual structure elements that record their position in the + * structure for future retrieval. They are transformed into real structure elements and do not need their own + * element (see {@code replacement}). + * + * @param name The socket name character + * @param replacement The replacement character + * @see IStructureDefinition#getSocket(String, char) + * @see IStructureDefinition#getAllSockets(String, char) + */ + public Builder addSocket(char name, char replacement) { + socketTranslations.put(name, replacement); + return this; + } + + /** + * Like {@link #addSocket(char, char)} but without the replacement. The structure must have a structure element + * with the given character. + * + * @param name The socket name character + */ + public Builder addSocket(char name) { + // Adding a name -> name entry like this isn't ideal but this is the cleanest solution + socketTranslations.put(name, name); + return this; + } + + public IStructureDefinition build() { + uncompiledShapes.forEach(this::compileShape); + + Map[]> structures = compileStructureMap(); + + return new StructureDefinition<>( + new HashMap<>(elements), + new HashMap<>(shapes), + structures, + occupiedSpaces, + controllers, + socketLocations); + } + + private void compileShape(String name, String[][] structurePiece) { StringBuilder builder = new StringBuilder(); + if (structurePiece.length > 0) { for (String[] strings : structurePiece) { if (strings.length > 0) { @@ -95,6 +179,11 @@ public Builder addShape(String name, String[][] structurePiece) { } builder.setLength(builder.length() - 1); } + + // Always create a map for this piece, even if it doesn't have any sockets + // noinspection UnstableApiUsage + socketLocations.put(name, MultimapBuilder.hashKeys().arrayListValues().build()); + Set occupiedSpace = new HashSet<>(); // these track the global current location int aa = 0, bb = 0, cc = 0; @@ -103,6 +192,15 @@ public Builder addShape(String name, String[][] structurePiece) { // I do know these are pretty bad variable naming, but I have no better idea for (int i = 0; i < builder.length(); i++) { char ch = builder.charAt(i); + + // Translate the element char if it's a socket, and record the socket position + if (socketTranslations.containsKey(ch)) { + socketLocations.get(name).put(ch, new Position<>(aa, bb, cc)); + + ch = socketTranslations.get(ch); + builder.setCharAt(i, ch); + } + if (ch == ' ') { builder.setCharAt(i, A); ch = A; @@ -110,6 +208,7 @@ public Builder addShape(String name, String[][] structurePiece) { builder.setCharAt(i, A); ch = A; occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc)); + controllers.put(name, new Position<>(aa, bb, cc)); } if (ch == A) { aa++; @@ -156,68 +255,62 @@ public Builder addShape(String name, String[][] structurePiece) { if (built.contains("-")) { addElement('-', isAir()); } + shapes.put(name, built); - return this; } - /** - * @deprecated use the unboxed version - */ - @Deprecated - public Builder addElement(Character name, IStructureElement structurePiece) { - elements.putIfAbsent(name, structurePiece); - return this; - } + @SuppressWarnings("unchecked") + private Map[]> compileStructureMap() { + CharOpenHashSet missing = findMissing(); - public Builder addElement(char name, IStructureElement structurePiece) { - elements.putIfAbsent(name, structurePiece); - return this; - } + if (!missing.isEmpty()) { + throw new RuntimeException("Missing Structure Element bindings for: " + missing); + } - public IStructureDefinition build() { - Map[]> structures = compileStructureMap(); - return new StructureDefinition<>( - new HashMap<>(elements), - new HashMap<>(shapes), - structures, - occupiedSpaces); - } + Map[]> map = new HashMap<>(); - @SuppressWarnings("unchecked") - private Map[]> compileElementSetMap() { - Set missing = findMissing(); - if (missing.isEmpty()) { - return shapes.entrySet().stream().collect( - Collectors.toMap( - Map.Entry::getKey, - e -> e.getValue().chars().mapToObj(c -> (char) c).distinct().map(elements::get) - .toArray(IStructureElement[]::new))); - } else { - throw new RuntimeException( - "Missing Structure Element bindings for (chars as integers): " - + Arrays.toString(missing.toArray())); + for (Entry e : shapes.entrySet()) { + char[] chars = e.getValue().toCharArray(); + IStructureElement[] compiled = new IStructureElement[chars.length]; + + int a = 0, b = 0, c = 0; + + for (int i = 0; i < chars.length; i++) { + char ch = chars[i]; + + IStructureElement element = this.elements.get(ch); + + compiled[i] = element; + + if (element.isNavigating()) { + a = (element.resetA() ? 0 : a) + element.getStepA(); + b = (element.resetB() ? 0 : b) + element.getStepB(); + c = (element.resetC() ? 0 : c) + element.getStepC(); + } else { + a++; + } + } + + if (map.put(e.getKey(), compiled) != null) { + throw new IllegalStateException("Duplicate shape: " + e.getKey()); + } } + + return map; } - @SuppressWarnings("unchecked") - private Map[]> compileStructureMap() { - Set missing = findMissing(); - if (missing.isEmpty()) { - return shapes.entrySet().stream().collect( - Collectors.toMap( - Map.Entry::getKey, - e -> e.getValue().chars().mapToObj(c -> elements.get((char) c)) - .toArray(IStructureElement[]::new))); - } else { - throw new RuntimeException( - "Missing Structure Element bindings for (chars as integers): " - + Arrays.toString(missing.toArray())); + private CharOpenHashSet findMissing() { + CharOpenHashSet missing = new CharOpenHashSet(); + + for (String shape : shapes.values()) { + for (char c : shape.toCharArray()) { + if (!elements.containsKey(c)) { + missing.add(c); + } + } } - } - private Set findMissing() { - return shapes.values().stream().flatMapToInt(CharSequence::chars) - .filter(i -> !elements.containsKey((char) i)).boxed().collect(Collectors.toSet()); + return missing; } } @@ -246,4 +339,61 @@ public boolean isContainedInStructure(String name, int offsetA, int offsetB, int if (occupiedSpace == null) throw new NoSuchElementException(name); return occupiedSpace.contains(new Vec3Impl(offsetA, offsetB, offsetC)); } + + @Override + public Position getControllerPosition(String piece) { + return controllers.get(piece); + } + + @Override + public StructureDefinitionCoords getCoordinateSystem(String piece) { + var controller = controllers.get(piece); + + if (controller == null) { + throw new IllegalArgumentException("Invalid piece: " + piece); + } + + return new StructureDefinitionCoords(controller.x, controller.y, controller.z); + } + + @Override + public Position getSocket(String piece, char socket) { + var piecePositions = socketLocations.get(piece); + + if (piecePositions == null) { + throw new IllegalArgumentException("Invalid piece: " + piece); + } + + var positions = piecePositions.get(socket); + + if (positions.isEmpty()) { + throw new IllegalArgumentException("Socket " + socket + " for piece " + piece + " does not exist"); + } + + if (positions.size() > 1) { + throw new IllegalStateException( + "Socket " + socket + + " for piece " + + piece + + " has several positions, but expected one and only one"); + } + + return positions.get(0).copy(); + } + + @Override + @Nonnull + public List> getAllSockets(String piece, char socket) { + var piecePositions = socketLocations.get(piece); + + if (piecePositions == null) { + throw new IllegalArgumentException("Invalid piece: " + piece); + } + + var list = new ArrayList<>(piecePositions.get(socket)); + + list.replaceAll(Position::copy); + + return list; + } } diff --git a/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java b/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java new file mode 100644 index 00000000..963611ac --- /dev/null +++ b/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java @@ -0,0 +1,102 @@ +package com.gtnewhorizon.structurelib.coords; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; +import com.gtnewhorizon.structurelib.structure.IStructureDefinition; +import com.gtnewhorizon.structurelib.structure.StructureUtility; + +// spotless:off +class CoordinateTests { + + private IStructureDefinition getDefinition() { + String[][] main = { + { " ", " ", " ", " ", " " }, + { " ", " ", " ", " ", " " }, + { " ", " ", " X ", " ", " " }, + { " ", " ", " YY ", " ", " " }, + { " ~ ", " ", " ", " ", " " } + }; + + return IStructureDefinition.builder() + .addShape("main", StructureUtility.transpose(main)) + .addSocket('X', ' ') + .addSocket('Y', ' ') + .build(); + } + + @Test + void testSocketAndCoordSystemTranslationToWorld() { + IStructureDefinition def = getDefinition(); + + // Note: the position is the same object between all of these transforms, it is never copied, its generic just + // changes + + var p = def.getSocket("main", 'X'); + Assertions.assertEquals(new Position<>(2, 2, 2), p); + + var p2 = def.getCoordinateSystem("main").translateInverse(p); + Assertions.assertEquals(new Position<>(0, -2, 2), p2); + + var p3 = ExtendedFacing.NORTH_NORMAL_NONE.asCoordinateSystem().translateInverse(p2); + Assertions.assertEquals(new Position<>(0, 2, 2), p3); + + var p4 = ControllerRelativeCoords.translateInverse(p3, 5, 5, 5); + Assertions.assertEquals(new Position<>(5, 7, 7), p4); + } + + @Test + void testSocketAndCoordSystemTranslationToStructureDef() { + IStructureDefinition def = getDefinition(); + + // Note: the position is the same object between all of these transforms, it is never copied, its generic just + // changes + + var p = new Position(5, 7, 7); + + var p2 = ControllerRelativeCoords.translate(p, 5, 5, 5); + Assertions.assertEquals(new Position<>(0, 2, 2), p2); + + var p3 = ExtendedFacing.NORTH_NORMAL_NONE.asCoordinateSystem().translate(p2); + Assertions.assertEquals(new Position<>(0, -2, 2), p3); + + var p4 = def.getCoordinateSystem("main").translate(p3); + Assertions.assertEquals(new Position<>(2, 2, 2), p4); + + Assertions.assertEquals(def.getSocket("main", 'X'), p4); + } + + @Test + void testSocketGetters() { + IStructureDefinition def = getDefinition(); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> def.getSocket("invalid", 'X')); + Assertions.assertThrows( + IllegalStateException.class, + () -> def.getSocket("main", 'Y')); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> def.getSocket("main", 'Z')); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> def.getAllSockets("invalid", 'X')); + + Assertions.assertEquals( + Collections.emptyList(), + def.getAllSockets("main", 'Z')); + Assertions.assertEquals( + Arrays.asList(new Position<>(2, 2, 2)), + def.getAllSockets("main", 'X')); + Assertions.assertEquals( + Arrays.asList(new Position<>(2, 3, 2), new Position<>(3, 3, 2)), + def.getAllSockets("main", 'Y')); + } +} +// spotless:on