mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 17:00:26 +00:00
Gap-free object rotation
- Rotations off the right angles fill every target cell from the nearest source block - Source blocks that no target cell samples still place at their rotated position - Right-angle rotations keep the direct transform - Markers, canopy and tile data follow their resampled source blocks - Datapack export bakes off-angle variants with the same resampler - The unused rotateCopy paths are removed - Placement goldens match the resampled output
This commit is contained in:
1 parent
2462330787
commit
b0860f94fc
14 files changed
+874
-189
No files matched your search
+45
-2
@@ -6,6 +6,7 @@ import art.arcane.iris.pack.datapack.export.TemplateVolume;
|
||||
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
|
||||
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
|
||||
import art.arcane.iris.structure.object.RotationResampler;
|
||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -16,6 +17,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
public final class ObjectVariantBaker {
|
||||
private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air");
|
||||
@@ -35,6 +37,10 @@ public final class ObjectVariantBaker {
|
||||
.comparingInt(Voxel::y)
|
||||
.thenComparingInt(Voxel::x)
|
||||
.thenComparingInt(Voxel::z);
|
||||
private static final Comparator<Voxel> PLACEMENT_ORDER = Comparator
|
||||
.comparingInt(Voxel::x)
|
||||
.thenComparingInt(Voxel::y)
|
||||
.thenComparingInt(Voxel::z);
|
||||
|
||||
public static String templateId(String namespace, String objectPath, VariantSpec spec, DataVersion target) {
|
||||
VanillaResourceIdentifier.requireNamespace(namespace, "template namespace");
|
||||
@@ -59,7 +65,7 @@ public final class ObjectVariantBaker {
|
||||
sizeZ = scaledSize(sizeZ, spec.scale());
|
||||
voxels = scale(voxels, spec.scale(), sizeX, sizeY, sizeZ);
|
||||
}
|
||||
voxels = anchor(voxels, spec.rotation(), sizeX / 2, sizeY / 2, sizeZ / 2);
|
||||
voxels = anchor(templateId, voxels, spec.rotation(), sizeX / 2, sizeY / 2, sizeZ / 2);
|
||||
if (spec.warp() != null) {
|
||||
voxels = warp(voxels, spec.warp());
|
||||
}
|
||||
@@ -243,11 +249,19 @@ public final class ObjectVariantBaker {
|
||||
return new ArrayList<>(scaled.values());
|
||||
}
|
||||
|
||||
private static List<Voxel> anchor(List<Voxel> voxels, VariantSpec.Rotation rotation, int centerX, int centerY, int centerZ) {
|
||||
private static List<Voxel> anchor(String templateId, List<Voxel> voxels, VariantSpec.Rotation rotation, int centerX,
|
||||
int centerY, int centerZ) {
|
||||
BlockStateRotation states = BlockStateRotation.of(rotation.x(), rotation.y(), rotation.z());
|
||||
double[] lift = rotate(0, centerY, 0, rotation);
|
||||
int rotatedCenterY = (int) Math.floor(lift[1]);
|
||||
List<Voxel> sorted = new ArrayList<>(voxels);
|
||||
if (!rotation.rightAngles()) {
|
||||
sorted.sort(PLACEMENT_ORDER);
|
||||
RotationResampler.Targets targets = RotationResampler.of(templateId, offsets(sorted, Voxel::x, centerX),
|
||||
offsets(sorted, Voxel::y, centerY), offsets(sorted, Voxel::z, centerZ))
|
||||
.resample(rotation.x(), rotation.y(), rotation.z());
|
||||
return resampled(sorted, targets, states, rotatedCenterY);
|
||||
}
|
||||
sorted.sort(VOXEL_ORDER);
|
||||
Map<Long, Voxel> anchored = new LinkedHashMap<>();
|
||||
for (Voxel voxel : sorted) {
|
||||
@@ -260,6 +274,35 @@ public final class ObjectVariantBaker {
|
||||
return new ArrayList<>(anchored.values());
|
||||
}
|
||||
|
||||
private static int[] offsets(List<Voxel> voxels, ToIntFunction<Voxel> axis, int center) {
|
||||
int[] offsets = new int[voxels.size()];
|
||||
for (int index = 0; index < offsets.length; index++) {
|
||||
offsets[index] = axis.applyAsInt(voxels.get(index)) - center;
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
private static List<Voxel> resampled(List<Voxel> sources, RotationResampler.Targets targets, BlockStateRotation states,
|
||||
int liftY) {
|
||||
int[] x = targets.x();
|
||||
int[] y = targets.y();
|
||||
int[] z = targets.z();
|
||||
int[] origins = targets.sources();
|
||||
VanillaBlockState[] turned = new VanillaBlockState[sources.size()];
|
||||
List<Voxel> anchored = new ArrayList<>(origins.length);
|
||||
for (int index = 0; index < origins.length; index++) {
|
||||
int origin = origins[index];
|
||||
Voxel source = sources.get(origin);
|
||||
VanillaBlockState state = turned[origin];
|
||||
if (state == null) {
|
||||
state = states.apply(source.state());
|
||||
turned[origin] = state;
|
||||
}
|
||||
anchored.add(new Voxel(x[index], y[index] + liftY, z[index], state, source.nbt(), source.root()));
|
||||
}
|
||||
return anchored;
|
||||
}
|
||||
|
||||
private static double[] rotate(double x, double y, double z, VariantSpec.Rotation rotation) {
|
||||
double rx = x;
|
||||
double ry = y;
|
||||
|
||||
+4
@@ -44,6 +44,10 @@ public record VariantSpec(double scale, Rotation rotation, VariantWarp warp, dou
|
||||
public boolean none() {
|
||||
return x == 0 && y == 0 && z == 0;
|
||||
}
|
||||
|
||||
public boolean rightAngles() {
|
||||
return x % 90 == 0 && y % 90 == 0 && z % 90 == 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
@@ -386,10 +386,6 @@ public class IrisObject extends IrisRegistrant {
|
||||
});
|
||||
}
|
||||
|
||||
public IrisObject rotateCopy(IrisObjectRotation rt) {
|
||||
return IrisObjectTransforms.rotateCopy(this, rt);
|
||||
}
|
||||
|
||||
public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||
return IrisObjectTransforms.scaled(this, scale, interpolation);
|
||||
}
|
||||
|
||||
@@ -168,14 +168,6 @@ public class IrisObjectRotation {
|
||||
return getRotation(spin, zAxis);
|
||||
}
|
||||
|
||||
public IrisObject rotateCopy(IrisObject e) {
|
||||
if (e == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return e.rotateCopy(this);
|
||||
}
|
||||
|
||||
public IrisBlockVector rotate(IrisBlockVector direction) {
|
||||
return rotate(direction, 0, 0, 0);
|
||||
}
|
||||
|
||||
@@ -43,72 +43,12 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
|
||||
* Geometric transforms for {@link IrisObject}: scaling and the interpolated upscalers.
|
||||
*/
|
||||
final class IrisObjectTransforms {
|
||||
private IrisObjectTransforms() {
|
||||
}
|
||||
|
||||
static IrisObject rotateCopy(IrisObject self, IrisObjectRotation rt) {
|
||||
IrisObject copy = self.copy();
|
||||
rotate(copy, rt, 0, 0, 0);
|
||||
return copy;
|
||||
}
|
||||
|
||||
static void rotate(IrisObject self, IrisObjectRotation r, int spinx, int spiny, int spinz) {
|
||||
self.writeLock.lock();
|
||||
try {
|
||||
VectorMap<NativeBlockState> d = new VectorMap<>();
|
||||
VectorMap<TileData> dx = new VectorMap<>();
|
||||
TreeGrounding grounding = self.getTreeGrounding();
|
||||
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
|
||||
|
||||
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
|
||||
NativeBlockState rotated = r.rotate(entry.getValue(), spinx, spiny, spinz);
|
||||
if (rotated == null) {
|
||||
continue;
|
||||
}
|
||||
IrisBlockVector destination = r.rotate(entry.getKey(), spinx, spiny, spinz);
|
||||
d.put(destination, rotated);
|
||||
if (canopy != null) {
|
||||
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(destination.getBlockX(),
|
||||
destination.getBlockY(), destination.getBlockZ());
|
||||
IrisBlockVector source = entry.getKey();
|
||||
if (grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
|
||||
canopy.add(cell);
|
||||
} else {
|
||||
canopy.remove(cell);
|
||||
}
|
||||
}
|
||||
TileData tile = self.states.get(entry.getKey());
|
||||
if (tile == null) {
|
||||
dx.remove(destination);
|
||||
} else {
|
||||
dx.put(destination, tile);
|
||||
}
|
||||
}
|
||||
|
||||
self.blocks = d;
|
||||
self.states = dx;
|
||||
if (grounding != null) {
|
||||
List<TreeGrounding.Root> roots = new ArrayList<>();
|
||||
for (TreeGrounding.Root root : grounding.roots()) {
|
||||
IrisBlockVector position = r.rotate(new IrisBlockVector(root.x(), root.y(), root.z()), spinx, spiny, spinz);
|
||||
NativeBlockState state = d.get(position);
|
||||
if (state != null) {
|
||||
roots.add(new TreeGrounding.Root(position.getBlockX(), position.getBlockY(), position.getBlockZ(), state));
|
||||
}
|
||||
}
|
||||
self.setTreeGrounding(TreeGrounding.create(roots, grounding.maxExtension(), canopy));
|
||||
}
|
||||
IrisObjectShaping.shrinkwrap(self);
|
||||
self.surfaceSupportOffsets.reset();
|
||||
self.floatingFootprint.reset();
|
||||
} finally {
|
||||
self.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||
return scaled(self, scale, interpolation, ScaleOrigin.DEFAULT);
|
||||
}
|
||||
|
||||
@@ -67,12 +67,12 @@ final class ObjectMarkerPlacement {
|
||||
}
|
||||
}
|
||||
|
||||
String[] resolved = new String[bake.count()];
|
||||
String[] bySource = new String[bake.sourceCount()];
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
int index = 0;
|
||||
while (index < resolved.length && cursor.next()) {
|
||||
resolved[index++] = markers.get(cursor.key());
|
||||
while (index < bySource.length && cursor.next()) {
|
||||
bySource[index++] = markers.get(cursor.key());
|
||||
}
|
||||
return resolved;
|
||||
return bake.markers(bySource);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -21,15 +22,25 @@ final class ObjectPlacementBake {
|
||||
private final int[] offsetY;
|
||||
private final int[] offsetZ;
|
||||
private final int[] layerY;
|
||||
private final short[] paletteIndexes;
|
||||
private final long[] canopy;
|
||||
private final int[] tileBlocks;
|
||||
private final TileData[] tileValues;
|
||||
private final int[] sources;
|
||||
private volatile double[] exact;
|
||||
|
||||
private ObjectPlacementBake(Layout layout, Key key, int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY) {
|
||||
private ObjectPlacementBake(Layout layout, Key key, Cells cells) {
|
||||
this.layout = layout;
|
||||
this.key = key;
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
this.offsetZ = offsetZ;
|
||||
this.layerY = layerY;
|
||||
this.offsetX = cells.offsetX();
|
||||
this.offsetY = cells.offsetY();
|
||||
this.offsetZ = cells.offsetZ();
|
||||
this.layerY = cells.layerY();
|
||||
this.paletteIndexes = cells.paletteIndexes();
|
||||
this.canopy = cells.canopy();
|
||||
this.tileBlocks = cells.tileBlocks();
|
||||
this.tileValues = cells.tileValues();
|
||||
this.sources = cells.sources();
|
||||
}
|
||||
|
||||
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||
@@ -42,7 +53,10 @@ final class ObjectPlacementBake {
|
||||
if (cached != null && cached.layout == layout) {
|
||||
return cached;
|
||||
}
|
||||
ObjectPlacementBake built = bake(object, layout, key, spin, ceilingHang, translateOffset);
|
||||
Cells cells = spin.axisAligned
|
||||
? transformed(object, layout, spin, ceilingHang, translateOffset)
|
||||
: resampled(object, layout, spin, ceilingHang, translateOffset);
|
||||
ObjectPlacementBake built = new ObjectPlacementBake(layout, key, cells);
|
||||
if (cached != null) {
|
||||
variants.replace(key, cached, built);
|
||||
} else if (variants.size() < MAXIMUM_VARIANTS_PER_OBJECT) {
|
||||
@@ -54,8 +68,8 @@ final class ObjectPlacementBake {
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectPlacementBake bake(IrisObject object, Layout layout, Key key, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
private static Cells transformed(IrisObject object, Layout layout, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
int count = layout.count;
|
||||
int[] x = new int[count];
|
||||
int[] y = new int[count];
|
||||
@@ -66,7 +80,7 @@ final class ObjectPlacementBake {
|
||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next() && index < count) {
|
||||
transform(cursor.key(), rotated, spin, ceilingHang, translateOffset);
|
||||
RotationResampler.transform(cursor.key(), rotated, spin, ceilingHang, translateOffset);
|
||||
int roundedY = (int) Math.round(rotated.getY());
|
||||
int floorY = rotated.getBlockY();
|
||||
x[index] = (int) Math.round(rotated.getX());
|
||||
@@ -79,24 +93,46 @@ final class ObjectPlacementBake {
|
||||
if (index != count) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was built");
|
||||
}
|
||||
return new ObjectPlacementBake(layout, key, x, y, z, layerDiffers ? layer : null);
|
||||
return new Cells(x, y, z, layerDiffers ? layer : null, layout.paletteIndexes, layout.canopy, layout.tileBlocks,
|
||||
layout.tileValues, null);
|
||||
}
|
||||
|
||||
private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
target.setX(source.getX());
|
||||
target.setY(source.getY());
|
||||
target.setZ(source.getZ());
|
||||
spin.rotate(target);
|
||||
if (ceilingHang) {
|
||||
target.setY(-target.getBlockY());
|
||||
}
|
||||
if (translateOffset != null) {
|
||||
target.add(translateOffset);
|
||||
private static Cells resampled(IrisObject object, Layout layout, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
RotationResampler.Targets targets = layout.resampler(object).resample(spin, ceilingHang, translateOffset);
|
||||
return gather(layout, targets.x(), targets.y(), targets.z(), targets.sources());
|
||||
}
|
||||
|
||||
private static Cells gather(Layout layout, int[] x, int[] y, int[] z, int[] sources) {
|
||||
int count = sources.length;
|
||||
short[] paletteIndexes = new short[count];
|
||||
long[] canopy = layout.canopy == null ? null : new long[(count + 63) >>> 6];
|
||||
IntArrayList tileBlocks = layout.tileBlocks == null ? null : new IntArrayList();
|
||||
List<TileData> tileValues = layout.tileBlocks == null ? null : new ArrayList<>();
|
||||
for (int index = 0; index < count; index++) {
|
||||
int source = sources[index];
|
||||
paletteIndexes[index] = layout.paletteIndexes[source];
|
||||
if (canopy != null && (layout.canopy[source >>> 6] & (1L << (source & 63))) != 0L) {
|
||||
canopy[index >>> 6] |= 1L << (index & 63);
|
||||
}
|
||||
if (tileBlocks != null) {
|
||||
int slot = Arrays.binarySearch(layout.tileBlocks, source);
|
||||
if (slot >= 0) {
|
||||
tileBlocks.add(index);
|
||||
tileValues.add(layout.tileValues[slot]);
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean tiled = tileBlocks != null && !tileBlocks.isEmpty();
|
||||
return new Cells(x, y, z, null, paletteIndexes, canopy, tiled ? tileBlocks.toIntArray() : null,
|
||||
tiled ? tileValues.toArray(new TileData[0]) : null, sources);
|
||||
}
|
||||
|
||||
int count() {
|
||||
return offsetX.length;
|
||||
}
|
||||
|
||||
int sourceCount() {
|
||||
return layout.count;
|
||||
}
|
||||
|
||||
@@ -117,7 +153,7 @@ final class ObjectPlacementBake {
|
||||
}
|
||||
|
||||
int paletteIndex(int index) {
|
||||
return layout.paletteIndexes[index];
|
||||
return paletteIndexes[index];
|
||||
}
|
||||
|
||||
NativeBlockState[] palette() {
|
||||
@@ -125,32 +161,51 @@ final class ObjectPlacementBake {
|
||||
}
|
||||
|
||||
int[] tileBlocks() {
|
||||
return layout.tileBlocks;
|
||||
return tileBlocks;
|
||||
}
|
||||
|
||||
TileData[] tileValues() {
|
||||
return layout.tileValues;
|
||||
return tileValues;
|
||||
}
|
||||
|
||||
boolean canopy(int index) {
|
||||
long[] canopy = layout.canopy;
|
||||
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
|
||||
}
|
||||
|
||||
String[] markers(String[] sourceMarkers) {
|
||||
if (sources == null) {
|
||||
return sourceMarkers;
|
||||
}
|
||||
String[] unclaimed = sourceMarkers.clone();
|
||||
String[] resolved = new String[sources.length];
|
||||
for (int index = 0; index < sources.length; index++) {
|
||||
int source = sources[index];
|
||||
resolved[index] = unclaimed[source];
|
||||
unclaimed[source] = null;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
double[] exact(IrisObject object, SpinKernel spin) {
|
||||
double[] resolved = exact;
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
resolved = sources == null ? transformedExact(object, spin) : targetExact();
|
||||
exact = resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private double[] transformedExact(IrisObject object, SpinKernel spin) {
|
||||
int count = layout.count;
|
||||
object.readLock.lock();
|
||||
try {
|
||||
resolved = new double[count * 3];
|
||||
double[] resolved = new double[count * 3];
|
||||
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next() && index < count) {
|
||||
transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset());
|
||||
RotationResampler.transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset());
|
||||
resolved[index * 3] = rotated.getX();
|
||||
resolved[index * 3 + 1] = rotated.getY();
|
||||
resolved[index * 3 + 2] = rotated.getZ();
|
||||
@@ -159,10 +214,19 @@ final class ObjectPlacementBake {
|
||||
if (index != count) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read");
|
||||
}
|
||||
return resolved;
|
||||
} finally {
|
||||
object.readLock.unlock();
|
||||
}
|
||||
exact = resolved;
|
||||
}
|
||||
|
||||
private double[] targetExact() {
|
||||
double[] resolved = new double[offsetX.length * 3];
|
||||
for (int index = 0; index < offsetX.length; index++) {
|
||||
resolved[index * 3] = offsetX[index];
|
||||
resolved[index * 3 + 1] = offsetY[index];
|
||||
resolved[index * 3 + 2] = offsetZ[index];
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -176,6 +240,7 @@ final class ObjectPlacementBake {
|
||||
private final int[] tileBlocks;
|
||||
private final TileData[] tileValues;
|
||||
private final long[] canopy;
|
||||
private volatile RotationResampler resampler;
|
||||
|
||||
private Layout(long blocksRevision, long statesRevision, TreeGrounding grounding, int count, short[] paletteIndexes,
|
||||
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
|
||||
@@ -270,6 +335,15 @@ final class ObjectPlacementBake {
|
||||
&& object.states.modificationRevision() == statesRevision
|
||||
&& object.getTreeGrounding() == grounding;
|
||||
}
|
||||
|
||||
private RotationResampler resampler(IrisObject object) {
|
||||
RotationResampler resolved = resampler;
|
||||
if (resolved == null) {
|
||||
resolved = RotationResampler.of(object, count);
|
||||
resampler = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin,
|
||||
@@ -291,4 +365,8 @@ final class ObjectPlacementBake {
|
||||
Double.longBitsToDouble(translateY), Double.longBitsToDouble(translateZ)) : null;
|
||||
}
|
||||
}
|
||||
|
||||
private record Cells(int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes, long[] canopy,
|
||||
int[] tileBlocks, TileData[] tileValues, int[] sources) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.VectorMap;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
public final class RotationResampler {
|
||||
private final int[] sourceX;
|
||||
private final int[] sourceY;
|
||||
private final int[] sourceZ;
|
||||
private final Box sourceBox;
|
||||
private final int[] sourceCells;
|
||||
|
||||
private RotationResampler(int[] sourceX, int[] sourceY, int[] sourceZ, Box sourceBox, int[] sourceCells) {
|
||||
this.sourceX = sourceX;
|
||||
this.sourceY = sourceY;
|
||||
this.sourceZ = sourceZ;
|
||||
this.sourceBox = sourceBox;
|
||||
this.sourceCells = sourceCells;
|
||||
}
|
||||
|
||||
public static RotationResampler of(String owner, int[] x, int[] y, int[] z) {
|
||||
int count = x.length;
|
||||
if (y.length != count || z.length != count) {
|
||||
throw new IllegalArgumentException(owner + " has " + x.length + "/" + y.length + "/" + z.length
|
||||
+ " source coordinates on its x/y/z axes");
|
||||
}
|
||||
if (count == 0) {
|
||||
return new RotationResampler(x, y, z, new Box(0, 0, 0, 0, 0, 0), new int[1]);
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (int index = 0; index < count; index++) {
|
||||
minX = Math.min(minX, x[index]);
|
||||
minY = Math.min(minY, y[index]);
|
||||
minZ = Math.min(minZ, z[index]);
|
||||
maxX = Math.max(maxX, x[index]);
|
||||
maxY = Math.max(maxY, y[index]);
|
||||
maxZ = Math.max(maxZ, z[index]);
|
||||
}
|
||||
Box box = new Box(minX, maxX, minY, maxY, minZ, maxZ);
|
||||
if (box.volume() > Integer.MAX_VALUE - 8) {
|
||||
throw new IllegalStateException(owner + " spans " + (maxX - minX + 1) + "x" + (maxY - minY + 1) + "x"
|
||||
+ (maxZ - minZ + 1) + " blocks, too large to resample for a non-right-angle rotation");
|
||||
}
|
||||
int[] cells = new int[(int) box.volume()];
|
||||
for (int index = 0; index < count; index++) {
|
||||
cells[(int) box.cell(x[index], y[index], z[index])] = index + 1;
|
||||
}
|
||||
return new RotationResampler(x, y, z, box, cells);
|
||||
}
|
||||
|
||||
static RotationResampler of(IrisObject object, int count) {
|
||||
int[] x = new int[count];
|
||||
int[] y = new int[count];
|
||||
int[] z = new int[count];
|
||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||
int index = 0;
|
||||
while (index < count && cursor.next()) {
|
||||
IrisBlockVector position = cursor.key();
|
||||
x[index] = position.getBlockX();
|
||||
y[index] = position.getBlockY();
|
||||
z[index] = position.getBlockZ();
|
||||
index++;
|
||||
}
|
||||
if (index != count) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement grid was built");
|
||||
}
|
||||
return of("Object " + object.getLoadKey(), x, y, z);
|
||||
}
|
||||
|
||||
public Targets resample(double xDegrees, double yDegrees, double zDegrees) {
|
||||
return resample(new SpinKernel(IrisObjectRotation.of(xDegrees, yDegrees, zDegrees), 0, 0, 0), false, null);
|
||||
}
|
||||
|
||||
Targets resample(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||
int count = sourceX.length;
|
||||
Box bounds = targetBounds(spin, ceilingHang, translateOffset);
|
||||
long[] occupied = new long[(int) ((bounds.volume() + 63L) >>> 6)];
|
||||
long[] sampled = new long[(count + 63) >>> 6];
|
||||
IntArrayList x = new IntArrayList(count);
|
||||
IntArrayList y = new IntArrayList(count);
|
||||
IntArrayList z = new IntArrayList(count);
|
||||
IntArrayList sources = new IntArrayList(count);
|
||||
IrisBlockVector probe = new IrisBlockVector(0, 0, 0);
|
||||
for (int targetX = bounds.minX(); targetX <= bounds.maxX(); targetX++) {
|
||||
for (int targetY = bounds.minY(); targetY <= bounds.maxY(); targetY++) {
|
||||
for (int targetZ = bounds.minZ(); targetZ <= bounds.maxZ(); targetZ++) {
|
||||
probe.setX(targetX);
|
||||
probe.setY(targetY);
|
||||
probe.setZ(targetZ);
|
||||
if (translateOffset != null) {
|
||||
probe.subtract(translateOffset);
|
||||
}
|
||||
if (ceilingHang) {
|
||||
probe.setY(-probe.getY());
|
||||
}
|
||||
spin.inverseRotate(probe);
|
||||
int source = source((int) Math.round(probe.getX()), (int) Math.round(probe.getY()),
|
||||
(int) Math.round(probe.getZ()));
|
||||
if (source < 0) {
|
||||
continue;
|
||||
}
|
||||
long cell = bounds.cell(targetX, targetY, targetZ);
|
||||
occupied[(int) (cell >>> 6)] |= 1L << (cell & 63L);
|
||||
sampled[source >>> 6] |= 1L << (source & 63);
|
||||
x.add(targetX);
|
||||
y.add(targetY);
|
||||
z.add(targetZ);
|
||||
sources.add(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int source = 0; source < count; source++) {
|
||||
if ((sampled[source >>> 6] & (1L << (source & 63))) != 0L) {
|
||||
continue;
|
||||
}
|
||||
probe.setX(sourceX[source]);
|
||||
probe.setY(sourceY[source]);
|
||||
probe.setZ(sourceZ[source]);
|
||||
transform(probe, probe, spin, ceilingHang, translateOffset);
|
||||
int targetX = Math.clamp(Math.round(probe.getX()), bounds.minX(), bounds.maxX());
|
||||
int targetY = Math.clamp(Math.round(probe.getY()), bounds.minY(), bounds.maxY());
|
||||
int targetZ = Math.clamp(Math.round(probe.getZ()), bounds.minZ(), bounds.maxZ());
|
||||
long cell = bounds.cell(targetX, targetY, targetZ);
|
||||
if ((occupied[(int) (cell >>> 6)] & (1L << (cell & 63L))) == 0L) {
|
||||
occupied[(int) (cell >>> 6)] |= 1L << (cell & 63L);
|
||||
x.add(targetX);
|
||||
y.add(targetY);
|
||||
z.add(targetZ);
|
||||
sources.add(source);
|
||||
}
|
||||
}
|
||||
return new Targets(x.toIntArray(), y.toIntArray(), z.toIntArray(), sources.toIntArray());
|
||||
}
|
||||
|
||||
static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
target.setX(source.getX());
|
||||
target.setY(source.getY());
|
||||
target.setZ(source.getZ());
|
||||
spin.rotate(target);
|
||||
if (ceilingHang) {
|
||||
target.setY(-target.getBlockY());
|
||||
}
|
||||
if (translateOffset != null) {
|
||||
target.add(translateOffset);
|
||||
}
|
||||
}
|
||||
|
||||
private int source(int x, int y, int z) {
|
||||
return sourceBox.contains(x, y, z) ? sourceCells[(int) sourceBox.cell(x, y, z)] - 1 : -1;
|
||||
}
|
||||
|
||||
private Box targetBounds(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
IrisBlockVector corner = new IrisBlockVector(0, 0, 0);
|
||||
for (int index = 0; index < 8; index++) {
|
||||
corner.setX((index & 1) == 0 ? sourceBox.minX() : sourceBox.maxX());
|
||||
corner.setY((index & 2) == 0 ? sourceBox.minY() : sourceBox.maxY());
|
||||
corner.setZ((index & 4) == 0 ? sourceBox.minZ() : sourceBox.maxZ());
|
||||
transform(corner, corner, spin, ceilingHang, translateOffset);
|
||||
int x = (int) Math.round(corner.getX());
|
||||
int y = (int) Math.round(corner.getY());
|
||||
int z = (int) Math.round(corner.getZ());
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
minZ = Math.min(minZ, z);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
maxZ = Math.max(maxZ, z);
|
||||
}
|
||||
return new Box(minX, maxX, minY, maxY, minZ, maxZ);
|
||||
}
|
||||
|
||||
public record Targets(int[] x, int[] y, int[] z, int[] sources) {
|
||||
public int count() {
|
||||
return sources.length;
|
||||
}
|
||||
}
|
||||
|
||||
private record Box(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {
|
||||
private long volume() {
|
||||
return (long) (maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1);
|
||||
}
|
||||
|
||||
private boolean contains(int x, int y, int z) {
|
||||
return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ;
|
||||
}
|
||||
|
||||
private long cell(int x, int y, int z) {
|
||||
return ((long) (x - minX) * (maxY - minY + 1) + (y - minY)) * (maxZ - minZ + 1) + (z - minZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,10 @@ final class SpinKernel {
|
||||
static final int MODE_QUARTER = 2;
|
||||
static final int MODE_THREE_QUARTER = 3;
|
||||
static final int MODE_ANGLE = 4;
|
||||
private static final double AXIS_ALIGNED_EPSILON = 1.0E-9D;
|
||||
|
||||
final boolean rotates;
|
||||
final boolean axisAligned;
|
||||
final int xMode;
|
||||
final int yMode;
|
||||
final int zMode;
|
||||
@@ -44,6 +46,9 @@ final class SpinKernel {
|
||||
zSin = Math.sin(zAngle);
|
||||
yCos = Math.cos(yAngle);
|
||||
ySin = Math.sin(yAngle);
|
||||
axisAligned = Math.abs(xCos * xSin) < AXIS_ALIGNED_EPSILON
|
||||
&& Math.abs(zCos * zSin) < AXIS_ALIGNED_EPSILON
|
||||
&& Math.abs(yCos * ySin) < AXIS_ALIGNED_EPSILON;
|
||||
}
|
||||
|
||||
private static int modeOf(IrisAxisRotationClamp clamp) {
|
||||
@@ -149,4 +154,85 @@ final class SpinKernel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void inverseRotate(IrisBlockVector v) {
|
||||
if (!rotates) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (yMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setX(-v.getX());
|
||||
v.setZ(-v.getZ());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(-v.getZ());
|
||||
v.setZ(x);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(v.getZ());
|
||||
v.setZ(-x);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = yCos * v.getX() - ySin * v.getZ();
|
||||
double z = ySin * v.getX() + yCos * v.getZ();
|
||||
v.setX(x);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
switch (zMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setY(-v.getY());
|
||||
v.setX(-v.getX());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(-v.getX());
|
||||
v.setX(y);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(v.getX());
|
||||
v.setX(-y);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = zCos * v.getX() + zSin * v.getY();
|
||||
double y = -zSin * v.getX() + zCos * v.getY();
|
||||
v.setX(x);
|
||||
v.setY(y);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
switch (xMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setZ(-v.getZ());
|
||||
v.setY(-v.getY());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(-v.getY());
|
||||
v.setY(z);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(v.getY());
|
||||
v.setY(-z);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double y = xCos * v.getY() + xSin * v.getZ();
|
||||
double z = -xSin * v.getY() + xCos * v.getZ();
|
||||
v.setY(y);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+174
@@ -6,12 +6,15 @@ import art.arcane.iris.pack.datapack.export.TemplateBlock;
|
||||
import art.arcane.iris.pack.datapack.export.TemplateVolume;
|
||||
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
|
||||
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
|
||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
@@ -22,6 +25,8 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ObjectVariantBakerTest {
|
||||
private static final String NAMESPACE = "iris_export";
|
||||
private static final String LEAVES = "minecraft:oak_leaves[distance=1,persistent=true,waterlogged=false]";
|
||||
private static final long RIGHT_ANGLE_GOLDEN = 0x5db739a5b940bf1dL;
|
||||
|
||||
@Test
|
||||
public void variantBakingIsDeterministic() throws IOException {
|
||||
@@ -245,6 +250,171 @@ public class ObjectVariantBakerTest {
|
||||
assertEquals(2, lifted.roots().reach());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diagonalTurnsFillSolidCanopiesWithoutHoles() {
|
||||
for (double degrees : new double[]{45, 135}) {
|
||||
assertSolid(leafDisc(6, 3), degrees, (x, z) -> x * x + z * z <= 25D);
|
||||
assertSolid(leafBox(4, 3), degrees, (x, z) -> Math.abs(x) <= 3.75D && Math.abs(z) <= 3.75D);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diagonalTurnsKeepEveryThinTrunkColumn() {
|
||||
int[][] columns = {{2, 0}, {0, 2}, {-2, 0}, {3, 1}};
|
||||
List<TemplateBlock> blocks = new ArrayList<>();
|
||||
for (int[] column : columns) {
|
||||
for (int y = 0; y < 6; y++) {
|
||||
blocks.add(block(column[0] + 3, y, column[1] + 3, "minecraft:oak_log[axis=y]"));
|
||||
}
|
||||
}
|
||||
TemplateVolume trunks = new TemplateVolume(7, 6, 7, blocks);
|
||||
for (double degrees : new double[]{45, 135, 225, 315}) {
|
||||
BakedVariant variant = turned(trunks, new VariantSpec.Rotation(0, degrees, 0));
|
||||
Map<Long, int[]> heights = new HashMap<>();
|
||||
for (TemplateBlock block : variant.volume().blocks()) {
|
||||
int x = anchoredX(variant, block);
|
||||
int z = anchoredZ(variant, block);
|
||||
heights.computeIfAbsent(cell(x, 0, z), ignored -> new int[]{x, z, 0})[2]++;
|
||||
}
|
||||
double cos = Math.cos(Math.toRadians(degrees));
|
||||
double sin = Math.sin(Math.toRadians(degrees));
|
||||
for (int[] column : columns) {
|
||||
long forward = cell((int) Math.round(cos * column[0] + sin * column[1]), 0,
|
||||
(int) Math.round(-sin * column[0] + cos * column[1]));
|
||||
boolean kept = false;
|
||||
for (Map.Entry<Long, int[]> output : heights.entrySet()) {
|
||||
int[] value = output.getValue();
|
||||
assertEquals(degrees + " degrees cut a trunk column short", 6, value[2]);
|
||||
boolean samples = (int) Math.round(cos * value[0] - sin * value[1]) == column[0]
|
||||
&& (int) Math.round(sin * value[0] + cos * value[1]) == column[1];
|
||||
kept |= samples || output.getKey() == forward;
|
||||
}
|
||||
assertTrue(degrees + " degrees lost the trunk column at " + column[0] + "," + column[1], kept);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rightAngleTurnsKeepTheirExactLayout() {
|
||||
CompoundTag chest = new CompoundTag();
|
||||
chest.putString("id", "minecraft:chest");
|
||||
TemplateVolume volume = new TemplateVolume(4, 4, 4, List.of(
|
||||
block(0, 0, 0, "minecraft:stone"),
|
||||
block(3, 0, 1, "minecraft:stone"),
|
||||
block(1, 1, 2, "minecraft:oak_log[axis=x]"),
|
||||
block(2, 2, 0, "minecraft:oak_stairs[facing=north,half=bottom,shape=outer_left,waterlogged=false]"),
|
||||
new TemplateBlock(3, 1, 3, VanillaBlockState.parse("minecraft:chest[facing=east,type=single,waterlogged=false]"), chest),
|
||||
block(1, 3, 1, "minecraft:oak_leaves[distance=1,persistent=false,waterlogged=false]"),
|
||||
block(0, 2, 3, "minecraft:oak_slab[type=bottom,waterlogged=false]")));
|
||||
TreeRoots roots = new TreeRoots(List.of(block(0, 0, 0, "minecraft:stone")), 2);
|
||||
StringBuilder layout = new StringBuilder();
|
||||
for (double scale : new double[]{1, 1.5}) {
|
||||
for (int x = 0; x < 360; x += 90) {
|
||||
for (int y = 0; y < 360; y += 90) {
|
||||
for (int z = 0; z < 360; z += 90) {
|
||||
VariantSpec spec = VariantSpec.builder().scale(scale).rotation(new VariantSpec.Rotation(x, y, z)).build();
|
||||
describe(layout, new ObjectVariantBaker().bake("iris_export:object/turn/a", new ObjectSource(volume, -1, roots), spec));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(RIGHT_ANGLE_GOLDEN, fnv(layout));
|
||||
}
|
||||
|
||||
private static void assertSolid(TemplateVolume source, double degrees, Interior interior) {
|
||||
BakedVariant variant = turned(source, new VariantSpec.Rotation(0, degrees, 0));
|
||||
Set<Long> covered = new HashSet<>();
|
||||
for (TemplateBlock block : variant.volume().blocks()) {
|
||||
assertTrue(degrees + " degrees wrote a cell twice",
|
||||
covered.add(cell(anchoredX(variant, block), anchoredY(variant, block), anchoredZ(variant, block))));
|
||||
}
|
||||
double cos = Math.cos(Math.toRadians(degrees));
|
||||
double sin = Math.sin(Math.toRadians(degrees));
|
||||
int reach = Math.max(source.sizeX(), source.sizeZ());
|
||||
for (int y = 0; y < source.sizeY(); y++) {
|
||||
for (int x = -reach; x <= reach; x++) {
|
||||
for (int z = -reach; z <= reach; z++) {
|
||||
if (interior.contains(cos * x - sin * z, sin * x + cos * z)) {
|
||||
assertTrue(degrees + " degrees left a hole at " + x + "," + y + "," + z, covered.contains(cell(x, y, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BakedVariant turned(TemplateVolume source, VariantSpec.Rotation rotation) {
|
||||
return new ObjectVariantBaker().bake("iris_export:object/turn/b", new ObjectSource(source, 0, TreeRoots.NONE),
|
||||
VariantSpec.builder().rotation(rotation).build());
|
||||
}
|
||||
|
||||
private static int anchoredX(BakedVariant variant, TemplateBlock block) {
|
||||
return block.x() - (variant.volume().sizeX() - 1) / 2;
|
||||
}
|
||||
|
||||
private static int anchoredY(BakedVariant variant, TemplateBlock block) {
|
||||
return block.y() + variant.baseY();
|
||||
}
|
||||
|
||||
private static int anchoredZ(BakedVariant variant, TemplateBlock block) {
|
||||
return block.z() - (variant.volume().sizeZ() - 1) / 2;
|
||||
}
|
||||
|
||||
private static TemplateVolume leafDisc(int radius, int height) {
|
||||
List<TemplateBlock> blocks = new ArrayList<>();
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (x * x + z * z <= radius * radius) {
|
||||
blocks.add(block(x + radius, y, z + radius, LEAVES));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new TemplateVolume(radius * 2 + 1, height, radius * 2 + 1, blocks);
|
||||
}
|
||||
|
||||
private static TemplateVolume leafBox(int half, int height) {
|
||||
List<TemplateBlock> blocks = new ArrayList<>();
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = -half; x <= half; x++) {
|
||||
for (int z = -half; z <= half; z++) {
|
||||
blocks.add(block(x + half, y, z + half, LEAVES));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new TemplateVolume(half * 2 + 1, height, half * 2 + 1, blocks);
|
||||
}
|
||||
|
||||
private static void describe(StringBuilder layout, BakedVariant variant) {
|
||||
TemplateVolume volume = variant.volume();
|
||||
layout.append(variant.id()).append(' ').append(volume.sizeX()).append('x').append(volume.sizeY()).append('x')
|
||||
.append(volume.sizeZ()).append(" base=").append(variant.baseY()).append('\n');
|
||||
for (TemplateBlock block : volume.blocks()) {
|
||||
layout.append(block.x()).append(',').append(block.y()).append(',').append(block.z()).append(' ')
|
||||
.append(block.state().canonical()).append(' ')
|
||||
.append(block.nbt() == null ? "-" : block.nbt().getString("id")).append('\n');
|
||||
}
|
||||
layout.append("roots reach=").append(variant.roots().reach()).append('\n');
|
||||
for (TemplateBlock cell : variant.roots().cells()) {
|
||||
layout.append(cell.x()).append(',').append(cell.y()).append(',').append(cell.z()).append(' ')
|
||||
.append(cell.state().canonical()).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
private static long fnv(CharSequence text) {
|
||||
long hash = 0xcbf29ce484222325L;
|
||||
for (int index = 0; index < text.length(); index++) {
|
||||
hash ^= text.charAt(index);
|
||||
hash *= 0x100000001b3L;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static long cell(int x, int y, int z) {
|
||||
return (((long) x & 0x1FFFFFL) << 42) | (((long) y & 0x1FFFFFL) << 21) | ((long) z & 0x1FFFFFL);
|
||||
}
|
||||
|
||||
private static TemplateVolume tree() {
|
||||
List<TemplateBlock> blocks = new ArrayList<>();
|
||||
for (int y = 0; y < 5; y++) {
|
||||
@@ -271,4 +441,8 @@ public class ObjectVariantBakerTest {
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
private interface Interior {
|
||||
boolean contains(double x, double z);
|
||||
}
|
||||
}
|
||||
@@ -56,66 +56,6 @@ public class IrisObjectTransformsTileTest {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rotationOmitsUnsupportedBlocksAndTheirTiles() {
|
||||
IrisObject source = new IrisObject(3, 1, 1);
|
||||
NativeBlockState unsupported = state("minecraft:wall_torch");
|
||||
source.setUnsigned(0, 0, 0, unsupported);
|
||||
source.setUnsignedTile(0, 0, 0, tile("minecraft:wall_torch", "omitted"));
|
||||
source.setUnsigned(1, 0, 0, state("minecraft:chest"));
|
||||
source.setUnsignedTile(1, 0, 0, tile("minecraft:chest", "kept"));
|
||||
source.setUnsigned(2, 0, 0, state("minecraft:stone"));
|
||||
IrisObjectRotation.StateRotator previous = IrisObjectRotation.bindPlatformRotator(
|
||||
(rotation, block, x, y, z) -> block == unsupported ? null : block);
|
||||
IrisObject rotated;
|
||||
try {
|
||||
rotated = source.rotateCopy(IrisObjectRotation.of(0, 90, 0));
|
||||
} finally {
|
||||
IrisObjectRotation.restorePlatformRotator(previous);
|
||||
}
|
||||
|
||||
assertEquals(2, rotated.getBlocks().size());
|
||||
assertEquals(1, rotated.getStates().size());
|
||||
for (Map.Entry<IrisBlockVector, TileData> entry : rotated.getStates()) {
|
||||
assertEquals("minecraft:chest", rotated.getBlocks().get(entry.getKey()).materialKey());
|
||||
assertEquals("kept", entry.getValue().getProperties().get("name"));
|
||||
}
|
||||
assertEquals(3, source.getBlocks().size());
|
||||
assertEquals(2, source.getStates().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalRotationKeepsTileDataOnlyForTheWinningBlock() {
|
||||
IrisObject source = new IrisObject(9, 1, 9);
|
||||
for (int x = 0; x < 9; x++) {
|
||||
for (int z = 0; z < 9; z++) {
|
||||
boolean chest = (x + z) % 2 == 0;
|
||||
source.setUnsigned(x, 0, z, state(chest ? "minecraft:chest" : "minecraft:stone"));
|
||||
if (chest) {
|
||||
source.setUnsignedTile(x, 0, z, tile("minecraft:chest", x + ":" + z));
|
||||
}
|
||||
}
|
||||
}
|
||||
IrisObjectRotation.StateRotator previous = IrisObjectRotation.bindPlatformRotator(
|
||||
(rotation, block, x, y, z) -> block);
|
||||
IrisObject rotated;
|
||||
try {
|
||||
rotated = source.rotateCopy(IrisObjectRotation.of(0, 45, 0));
|
||||
} finally {
|
||||
IrisObjectRotation.restorePlatformRotator(previous);
|
||||
}
|
||||
|
||||
assertTrue(rotated.getBlocks().size() < source.getBlocks().size());
|
||||
for (Map.Entry<IrisBlockVector, TileData> entry : rotated.getStates()) {
|
||||
assertEquals("minecraft:chest", rotated.getBlocks().get(entry.getKey()).materialKey());
|
||||
}
|
||||
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : rotated.getBlocks()) {
|
||||
if (entry.getValue().materialKey().equals("minecraft:chest")) {
|
||||
assertNotNull(rotated.getStates().get(entry.getKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scalingWaitsForMatchingGeometryAndVolume() throws Exception {
|
||||
assertConsistentScaling(false);
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.pack.datapack.export.TemplateBlock;
|
||||
import art.arcane.iris.pack.datapack.export.TemplateVolume;
|
||||
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
|
||||
import art.arcane.iris.pack.datapack.export.compile.object.BakedVariant;
|
||||
import art.arcane.iris.pack.datapack.export.compile.object.ObjectSource;
|
||||
import art.arcane.iris.pack.datapack.export.compile.object.ObjectVariantBaker;
|
||||
import art.arcane.iris.pack.datapack.export.compile.object.TreeRoots;
|
||||
import art.arcane.iris.pack.datapack.export.compile.object.VariantSpec;
|
||||
import art.arcane.iris.testsupport.KeyedBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
@@ -91,7 +103,11 @@ public class ObjectPlacementBakeTest {
|
||||
distinct.add(ObjectPlacementBake.Key.of(spin, false, translate));
|
||||
assertSame(first.palette(), bake.palette());
|
||||
assertTrue(object.placementBakes.size() <= 32);
|
||||
assertTransformed(object, spin, translate, bake);
|
||||
if (spin.axisAligned) {
|
||||
assertTransformed(object, spin, false, translate, bake);
|
||||
} else {
|
||||
assertResampled(object, spin, translate, bake);
|
||||
}
|
||||
if (placement == 2000) {
|
||||
settled = new HashMap<>(object.placementBakes);
|
||||
}
|
||||
@@ -104,13 +120,240 @@ public class ObjectPlacementBakeTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertTransformed(IrisObject object, SpinKernel spin, IrisBlockVector translate, ObjectPlacementBake bake) {
|
||||
@Test
|
||||
public void diagonalYawTurnsFillEverySolidInteriorCellExactlyOnce() {
|
||||
IrisObject disc = leafDisc(6, 3);
|
||||
IrisObject box = leafBox(4, 3);
|
||||
IrisObjectRotation diagonal = yawRotation(45D);
|
||||
for (int degrees : new int[]{45, 135}) {
|
||||
SpinKernel spin = new SpinKernel(diagonal, 0, degrees, 0);
|
||||
assertSolidCoverage(ObjectPlacementBake.of(disc, spin, false, null), degrees, 6, 3,
|
||||
(x, z) -> x * x + z * z <= 25D);
|
||||
assertSolidCoverage(ObjectPlacementBake.of(box, spin, false, null), degrees, 6, 3,
|
||||
(x, z) -> Math.abs(x) <= 3.75D && Math.abs(z) <= 3.75D);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diagonalYawTurnsKeepEveryThinTrunkColumn() {
|
||||
IrisObject object = new IrisObject(7, 6, 7);
|
||||
KeyedBlockState log = new KeyedBlockState("minecraft:oak_log[axis=y]");
|
||||
int[][] columns = {{2, 0}, {0, 2}, {-2, 0}, {3, 1}};
|
||||
for (int[] column : columns) {
|
||||
for (int y = 0; y < 6; y++) {
|
||||
object.getBlocks().put(new IrisBlockVector(column[0], y, column[1]), log);
|
||||
}
|
||||
}
|
||||
IrisObjectRotation diagonal = yawRotation(45D);
|
||||
for (int degrees : new int[]{45, 135, 225, 315}) {
|
||||
SpinKernel spin = new SpinKernel(diagonal, 0, degrees, 0);
|
||||
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, false, null);
|
||||
Set<Long> covered = new HashSet<>();
|
||||
Set<Long> sampled = new HashSet<>();
|
||||
for (int index = 0; index < bake.count(); index++) {
|
||||
assertTrue(covered.add(cell(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index))));
|
||||
IrisBlockVector inverse = new IrisBlockVector(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index));
|
||||
spin.inverseRotate(inverse);
|
||||
sampled.add(cell((int) Math.round(inverse.getX()), (int) Math.round(inverse.getY()), (int) Math.round(inverse.getZ())));
|
||||
}
|
||||
for (IrisBlockVector source : object.getBlocks().keys()) {
|
||||
IrisBlockVector forward = source.clone();
|
||||
spin.rotate(forward);
|
||||
boolean represented = sampled.contains(cell(source.getBlockX(), source.getBlockY(), source.getBlockZ()))
|
||||
|| covered.contains(cell((int) Math.round(forward.getX()), (int) Math.round(forward.getY()),
|
||||
(int) Math.round(forward.getZ())));
|
||||
assertTrue(degrees + " degrees dropped trunk block " + source, represented);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void quarterTurnsKeepTheForwardTransform() {
|
||||
IrisObject object = leafDisc(6, 3);
|
||||
object.getBlocks().put(new IrisBlockVector(7, 1, -2), new KeyedBlockState("minecraft:oak_log[axis=x]"));
|
||||
IrisBlockVector translate = new IrisBlockVector(1.5D, -2D, 0.25D);
|
||||
IrisObjectRotation[] rotations = {new IrisObjectRotation(), yawRotation(45D)};
|
||||
for (IrisObjectRotation rotation : rotations) {
|
||||
for (int degrees = 0; degrees < 360; degrees += 90) {
|
||||
SpinKernel spin = new SpinKernel(rotation, 0, degrees, 0);
|
||||
assertTransformed(object, spin, false, null, ObjectPlacementBake.of(object, spin, false, null));
|
||||
assertTransformed(object, spin, true, translate, ObjectPlacementBake.of(object, spin, true, translate));
|
||||
}
|
||||
}
|
||||
for (int degrees = 90; degrees < 360; degrees += 90) {
|
||||
SpinKernel spin = new SpinKernel(IrisObjectRotation.of(0D, degrees, 0D), 0, 0, 0);
|
||||
assertTransformed(object, spin, false, null, ObjectPlacementBake.of(object, spin, false, null));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackVariantsMatchPlacementAtNonRightAngles() {
|
||||
IrisObject object = new IrisObject(13, 5, 13);
|
||||
KeyedBlockState leaves = new KeyedBlockState("minecraft:oak_leaves[distance=1,persistent=true]");
|
||||
for (int y = 0; y <= 2; y++) {
|
||||
for (int x = -6; x <= 6; x++) {
|
||||
for (int z = -6; z <= 6; z++) {
|
||||
if (x * x + z * z <= 36) {
|
||||
object.getBlocks().put(new IrisBlockVector(x, y, z), leaves);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = -2; y <= 1; y++) {
|
||||
object.getBlocks().put(new IrisBlockVector(0, y, 0), new KeyedBlockState("minecraft:oak_log[axis=y]"));
|
||||
object.getBlocks().put(new IrisBlockVector(4, y, -3), new KeyedBlockState("minecraft:birch_log[axis=y]"));
|
||||
}
|
||||
object.getBlocks().put(new IrisBlockVector(-5, 2, 2), new KeyedBlockState("minecraft:gold_block"));
|
||||
object.getBlocks().put(new IrisBlockVector(2, -2, 5), new KeyedBlockState("minecraft:stone"));
|
||||
List<TemplateBlock> blocks = new ArrayList<>();
|
||||
for (IrisBlockVector key : object.getBlocks().keys()) {
|
||||
blocks.add(new TemplateBlock(key.getBlockX() + object.getCenter().getX(), key.getBlockY() + object.getCenter().getY(),
|
||||
key.getBlockZ() + object.getCenter().getZ(), VanillaBlockState.parse(object.getBlocks().get(key).key()), null));
|
||||
}
|
||||
ObjectSource source = new ObjectSource(new TemplateVolume(object.getW(), object.getH(), object.getD(), blocks), 0,
|
||||
TreeRoots.NONE);
|
||||
double[][] angles = {{0D, 45D, 0D}, {0D, 135D, 0D}, {0D, 30D, 0D}, {20D, 45D, 0D}, {0D, 60D, 15D}};
|
||||
for (double[] angle : angles) {
|
||||
IrisObjectRotation rotation = IrisObjectRotation.of(angle[0], angle[1], angle[2]);
|
||||
SpinKernel spin = new SpinKernel(rotation, 0, 0, 0);
|
||||
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, false, null);
|
||||
int lift = rotation.rotate(new IrisBlockVector(0, object.getCenter().getY(), 0), 0, 0, 0).getBlockY();
|
||||
Set<String> placed = new HashSet<>();
|
||||
for (int index = 0; index < bake.count(); index++) {
|
||||
String key = bake.palette()[bake.paletteIndex(index)].key();
|
||||
placed.add(bake.offsetX(index) + "," + (bake.offsetY(index) + lift) + "," + bake.offsetZ(index) + " "
|
||||
+ key.substring(0, key.indexOf('[') < 0 ? key.length() : key.indexOf('[')));
|
||||
}
|
||||
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/parity/a", source,
|
||||
VariantSpec.builder().rotation(new VariantSpec.Rotation(angle[0], angle[1], angle[2])).build());
|
||||
Set<String> exported = new HashSet<>();
|
||||
TemplateVolume volume = variant.volume();
|
||||
for (TemplateBlock block : volume.blocks()) {
|
||||
exported.add((block.x() - (volume.sizeX() - 1) / 2) + "," + (block.y() + variant.baseY()) + ","
|
||||
+ (block.z() - (volume.sizeZ() - 1) / 2) + " " + block.state().name());
|
||||
}
|
||||
assertEquals(Arrays.toString(angle), placed, exported);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertResampled(IrisObject object, SpinKernel spin, IrisBlockVector translate, ObjectPlacementBake bake) {
|
||||
double[] exact = bake.exact(object, spin);
|
||||
Set<Long> forward = new HashSet<>();
|
||||
for (IrisBlockVector source : object.getBlocks().keys()) {
|
||||
IrisBlockVector moved = source.clone();
|
||||
spin.rotate(moved);
|
||||
moved.add(translate);
|
||||
forward.add(cell((int) Math.round(moved.getX()), (int) Math.round(moved.getY()), (int) Math.round(moved.getZ())));
|
||||
}
|
||||
Set<Long> covered = new HashSet<>();
|
||||
Set<Long> sampled = new HashSet<>();
|
||||
for (int index = 0; index < bake.count(); index++) {
|
||||
long target = cell(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index));
|
||||
assertTrue(covered.add(target));
|
||||
IrisBlockVector inverse = new IrisBlockVector(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index));
|
||||
inverse.subtract(translate);
|
||||
spin.inverseRotate(inverse);
|
||||
IrisBlockVector nearest = new IrisBlockVector((int) Math.round(inverse.getX()), (int) Math.round(inverse.getY()),
|
||||
(int) Math.round(inverse.getZ()));
|
||||
NativeBlockState state = bake.palette()[bake.paletteIndex(index)];
|
||||
NativeBlockState nearestState = object.getBlocks().get(nearest);
|
||||
if (nearestState != null) {
|
||||
assertSame(nearestState, state);
|
||||
sampled.add(cell(nearest.getBlockX(), nearest.getBlockY(), nearest.getBlockZ()));
|
||||
} else {
|
||||
assertTrue(forward.contains(target));
|
||||
}
|
||||
assertEquals(bake.offsetY(index), bake.layerY(index));
|
||||
assertEquals(bake.offsetX(index), exact[index * 3], 0D);
|
||||
assertEquals(bake.offsetY(index), exact[index * 3 + 1], 0D);
|
||||
assertEquals(bake.offsetZ(index), exact[index * 3 + 2], 0D);
|
||||
}
|
||||
for (IrisBlockVector source : object.getBlocks().keys()) {
|
||||
IrisBlockVector moved = source.clone();
|
||||
spin.rotate(moved);
|
||||
moved.add(translate);
|
||||
assertTrue(sampled.contains(cell(source.getBlockX(), source.getBlockY(), source.getBlockZ()))
|
||||
|| covered.contains(cell((int) Math.round(moved.getX()), (int) Math.round(moved.getY()),
|
||||
(int) Math.round(moved.getZ()))));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertSolidCoverage(ObjectPlacementBake bake, int degrees, int radius, int height, Interior interior) {
|
||||
Map<Long, Integer> covered = new HashMap<>();
|
||||
for (int index = 0; index < bake.count(); index++) {
|
||||
covered.merge(cell(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index)), 1, Integer::sum);
|
||||
}
|
||||
double cos = Math.cos(Math.toRadians(degrees));
|
||||
double sin = Math.sin(Math.toRadians(degrees));
|
||||
int reach = radius * 2;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = -reach; x <= reach; x++) {
|
||||
for (int z = -reach; z <= reach; z++) {
|
||||
int writes = covered.getOrDefault(cell(x, y, z), 0);
|
||||
assertTrue(degrees + " degrees wrote cell " + x + "," + y + "," + z + " " + writes + " times", writes <= 1);
|
||||
if (interior.contains(cos * x - sin * z, sin * x + cos * z)) {
|
||||
assertEquals(degrees + " degrees left a hole at " + x + "," + y + "," + z, 1, writes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long cell(int x, int y, int z) {
|
||||
return (((long) x & 0x1FFFFFL) << 42) | (((long) y & 0x1FFFFFL) << 21) | ((long) z & 0x1FFFFFL);
|
||||
}
|
||||
|
||||
private static IrisObjectRotation yawRotation(double interval) {
|
||||
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
rotation.setYAxis(new IrisAxisRotationClamp(true, false, 0D, 0D, interval));
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private static IrisObject leafDisc(int radius, int height) {
|
||||
IrisObject object = new IrisObject(radius * 2 + 1, height, radius * 2 + 1);
|
||||
KeyedBlockState leaves = new KeyedBlockState("minecraft:oak_leaves[distance=1,persistent=true]");
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (x * x + z * z <= radius * radius) {
|
||||
object.getBlocks().put(new IrisBlockVector(x, y, z), leaves);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
private static IrisObject leafBox(int half, int height) {
|
||||
IrisObject object = new IrisObject(half * 2 + 1, height, half * 2 + 1);
|
||||
KeyedBlockState leaves = new KeyedBlockState("minecraft:oak_leaves[distance=1,persistent=true]");
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = -half; x <= half; x++) {
|
||||
for (int z = -half; z <= half; z++) {
|
||||
object.getBlocks().put(new IrisBlockVector(x, y, z), leaves);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
private interface Interior {
|
||||
boolean contains(double x, double z);
|
||||
}
|
||||
|
||||
private static void assertTransformed(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translate,
|
||||
ObjectPlacementBake bake) {
|
||||
double[] exact = bake.exact(object, spin);
|
||||
int index = 0;
|
||||
for (IrisBlockVector source : object.getBlocks().keys()) {
|
||||
IrisBlockVector expected = source.clone();
|
||||
spin.rotate(expected);
|
||||
expected.add(translate);
|
||||
if (ceilingHang) {
|
||||
expected.setY(-expected.getBlockY());
|
||||
}
|
||||
if (translate != null) {
|
||||
expected.add(translate);
|
||||
}
|
||||
assertEquals((int) Math.round(expected.getX()), bake.offsetX(index));
|
||||
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
|
||||
assertEquals((int) Math.round(expected.getZ()), bake.offsetZ(index));
|
||||
|
||||
@@ -63,9 +63,9 @@ public class ObjectPlacementGoldenTest {
|
||||
|
||||
private static final int OBJECTS = 200;
|
||||
private static final int SEEDS = 5;
|
||||
private static final long GOLDEN = 0x0b870e7696a055b0L;
|
||||
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
|
||||
private static final long HIGH_VARIANT_GOLDEN = 0x998044b16f7f1f6fL;
|
||||
private static final long GOLDEN = 0x930e130dc51b6f67L;
|
||||
private static final long MARKER_GOLDEN = 0x67ae55d574c3894eL;
|
||||
private static final long HIGH_VARIANT_GOLDEN = 0xdd61791b0f7e4d37L;
|
||||
|
||||
private static final List<String> PALETTE = List.of(
|
||||
"minecraft:stone",
|
||||
|
||||
@@ -297,21 +297,6 @@ public class TreeGroundingPlacementTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rotatedCopiesKeepMetadataOnTheirActualRootBlocks() {
|
||||
IrisObject source = tree(true);
|
||||
Map<Position, NativeBlockState> original = snapshot(source);
|
||||
TreeGrounding grounding = source.getTreeGrounding();
|
||||
for (int angle : new int[]{45, 90, 180, 270}) {
|
||||
IrisObject rotated = source.rotateCopy(IrisObjectRotation.of(0, angle, 0));
|
||||
|
||||
assertGroundingMatchesBlocks(rotated);
|
||||
assertEquals(3, rotated.getTreeGrounding().maxExtension());
|
||||
assertEquals(original, snapshot(source));
|
||||
assertSame(grounding, source.getTreeGrounding());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shrinkwrapMovesRootMetadataAndInvalidatesItsFootprint() {
|
||||
IrisObject source = tree(true);
|
||||
@@ -397,7 +382,6 @@ public class TreeGroundingPlacementTest {
|
||||
IrisObject shrunk = source.copy();
|
||||
shrunk.shrinkwrap();
|
||||
for (IrisObject transformed : List.of(source.copy(), shrunk,
|
||||
source.rotateCopy(IrisObjectRotation.of(0, 90, 0)),
|
||||
source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE),
|
||||
source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE))) {
|
||||
assertGroundingMatchesBlocks(transformed);
|
||||
|
||||
Reference in new issue
Block a user