diff --git a/src/main/java/com/cleanroommc/modularui/screen/GuiScreenWrapper.java b/src/main/java/com/cleanroommc/modularui/screen/GuiScreenWrapper.java index 5a3070532..23bde0e92 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/GuiScreenWrapper.java +++ b/src/main/java/com/cleanroommc/modularui/screen/GuiScreenWrapper.java @@ -37,6 +37,7 @@ import net.minecraftforge.fml.relauncher.SideOnly; import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; import java.io.IOException; import java.util.Set; diff --git a/src/main/java/com/cleanroommc/modularui/test/ResizerTest.java b/src/main/java/com/cleanroommc/modularui/test/ResizerTest.java index a80afd7bd..73702fd38 100644 --- a/src/main/java/com/cleanroommc/modularui/test/ResizerTest.java +++ b/src/main/java/com/cleanroommc/modularui/test/ResizerTest.java @@ -1,14 +1,11 @@ package com.cleanroommc.modularui.test; -import com.cleanroommc.modularui.api.drawable.IKey; -import com.cleanroommc.modularui.api.layout.IViewportStack; -import com.cleanroommc.modularui.drawable.GuiTextures; import com.cleanroommc.modularui.screen.CustomModularScreen; import com.cleanroommc.modularui.screen.ModularPanel; import com.cleanroommc.modularui.screen.viewport.GuiContext; -import com.cleanroommc.modularui.widget.Widget; - -import net.minecraft.client.Minecraft; +import com.cleanroommc.modularui.utils.fakeworld.ArraySchema; +import com.cleanroommc.modularui.utils.fakeworld.ISchema; +import com.cleanroommc.modularui.widgets.SchemaWidget; import org.jetbrains.annotations.NotNull; @@ -24,34 +21,50 @@ public class ResizerTest extends CustomModularScreen { .background(new SpriteDrawable(sprite)) .size(20) .align(Alignment.Center));*/ - return ModularPanel.defaultPanel("main") + /*TrackedDummyWorld world = new TrackedDummyWorld(); + world.addBlock(new BlockPos(0, 0, 0), new BlockInfo(Blocks.DIAMOND_BLOCK.getDefaultState())); + world.addBlock(new BlockPos(0, 1, 0), new BlockInfo(Blocks.BEDROCK.getDefaultState())); + world.addBlock(new BlockPos(1, 0, 1), new BlockInfo(Blocks.GOLD_BLOCK.getDefaultState()));*/ +/* return ModularPanel.defaultPanel("main") .size(150) - .child(new SpinningWidget() - .size(80, 20) - .center() - .background(GuiTextures.MC_BUTTON) - .overlay(IKey.str("Text")) - .addTooltipLine("Long Tooltip Line")); - /*.child(new Column() - .alignX(0.5f) - .heightRel(1f) - .margin(0, 7) - .coverChildrenWidth() - .mainAxisAlignment(Alignment.MainAxis.SPACE_BETWEEN) - .child(new ButtonWidget<>().width(40)) - .child(new Row().height(30).widthRel(1f).background(GuiTextures.CHECKBOARD).debugName("row")) - .child(new ButtonWidget<>()));*/ - } + .overlay(new SchemaRenderer(BoxSchema.of(Minecraft.getMinecraft().world, new BlockPos(Minecraft.getMinecraft().player), 5)) + .cameraFunc((camera, schema) -> { + double pitch = Math.PI / 4; + double T = 4000D; + double yaw = Minecraft.getSystemTime() % T / T * Math.PI * 2; + camera.setLookAt(new BlockPos(Minecraft.getMinecraft().player), 20, yaw, pitch); + }) + .isometric(true) + .asIcon().size(140));*/ + + + /*MapSchema world = new MapSchema.Builder() + .add(new BlockPos(0, 0, 0), Blocks.DIAMOND_BLOCK.getDefaultState()) + .add(new BlockPos(0, 1, 0), Blocks.BEDROCK.getDefaultState()) + .add(new BlockPos(0, 2, 0), Blocks.WOOL.getDefaultState()) + .add(new BlockPos(1, 0, 1), Blocks.GOLD_BLOCK.getDefaultState()) + .add(new BlockPos(0, 3, 0), Blocks.BEACON.getDefaultState()) + .build();*/ - private static class SpinningWidget extends Widget { + ISchema schema = ArraySchema.builder() + .layer("D D", " ", " ", " ") + .layer(" DDD ", " E E ", " ", " ") + .layer(" DDD ", " E ", " G ", " B ") + .layer(" DDD ", " E E ", " ", " ") + .layer("D D", " ", " ", " ") + .where('D', "minecraft:gold_block") + .where('E', "minecraft:emerald_block") + .where('G', "minecraft:diamond_block") + .where('B', "minecraft:beacon") + .build(); - @Override - public void transform(IViewportStack stack) { - super.transform(stack); - stack.translate(getArea().width / 2f, getArea().height / 2f); - float p = Minecraft.getSystemTime() % 4000 / 4000f; - stack.rotateZ((float) (p * Math.PI * 2)); - stack.translate(-getArea().width / 2f, -getArea().height / 2f); - } + var panel = ModularPanel.defaultPanel("main").size(170); + panel.child(new SchemaWidget(schema) + .full()) + .child(new SchemaWidget.LayerButton(schema, 0, 3) + .bottom(1) + .left(1) + .size(16)); + return panel; } } diff --git a/src/main/java/com/cleanroommc/modularui/utils/GuiUtils.java b/src/main/java/com/cleanroommc/modularui/utils/GuiUtils.java index 611b38dac..f600bf91e 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/GuiUtils.java +++ b/src/main/java/com/cleanroommc/modularui/utils/GuiUtils.java @@ -25,23 +25,23 @@ public static Matrix4f getTransformationMatrix() { } public static Matrix4f getTransformationMatrix(Matrix4f matrix4f) { - floatBuffer.position(0); + floatBuffer.rewind(); getTransformationBuffer(floatBuffer); matrix4f.load(floatBuffer); return matrix4f; } public static void setTransformationMatrix(Matrix4f matrix) { - floatBuffer.position(0); + floatBuffer.rewind(); matrix.store(floatBuffer); - floatBuffer.position(0); + floatBuffer.rewind(); GL11.glLoadMatrix(floatBuffer); } public static void applyTransformationMatrix(Matrix4f matrix) { - floatBuffer.position(0); + floatBuffer.rewind(); matrix.store(floatBuffer); - floatBuffer.position(0); + floatBuffer.rewind(); GL11.glMultMatrix(floatBuffer); } } diff --git a/src/main/java/com/cleanroommc/modularui/utils/VectorUtil.java b/src/main/java/com/cleanroommc/modularui/utils/VectorUtil.java new file mode 100644 index 000000000..efe7d1094 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/VectorUtil.java @@ -0,0 +1,52 @@ +package com.cleanroommc.modularui.utils; + +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.Vec3i; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.util.vector.Vector3f; + +public class VectorUtil { + + public static Vector3f set(Vector3f target, float x, float y, float z) { + if (target == null) target = new Vector3f(); + target.set(x, y, z); + return target; + } + + @NotNull + public static Vector3f set(@Nullable Vector3f target, Vec3d vec) { + return set(target, (float) vec.x, (float) vec.y, (float) vec.z); + } + + @NotNull + public static Vector3f set(@Nullable Vector3f target, Vec3i vec) { + return set(target, vec.getX(), vec.getY(), vec.getZ()); + } + + public static Vector3f vec3f(Vec3d vec3d) { + return set(null, vec3d); + } + + public static Vector3f vec3f(Vec3i vec3i) { + return set(null, vec3i); + } + + public static Vector3f vec3fAdd(Vector3f source, Vector3f target, float x, float y, float z) { + if (target == null) target = new Vector3f(); + if (source == null) return set(target, x, y, z); + if (target != source) target.set(source); + return target.translate(x, y, z); + } + + @NotNull + public static Vector3f vec3fAdd(Vector3f source, @Nullable Vector3f target, Vec3i vec) { + return vec3fAdd(source, target, vec.getX(), vec.getY(), vec.getZ()); + } + + @NotNull + public static Vector3f vec3fAdd(Vector3f source, @Nullable Vector3f target, Vec3d vec) { + return vec3fAdd(source, target, (float) vec.x, (float) vec.y, (float) vec.z); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/ArraySchema.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/ArraySchema.java new file mode 100644 index 000000000..95f263104 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/ArraySchema.java @@ -0,0 +1,235 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import com.cleanroommc.modularui.ModularUI; + +import it.unimi.dsi.fastutil.chars.Char2ObjectMap; + +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; + +import it.unimi.dsi.fastutil.chars.CharArraySet; +import it.unimi.dsi.fastutil.chars.CharSet; + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import com.google.common.collect.AbstractIterator; + +import net.minecraftforge.fml.common.registry.ForgeRegistries; + +import org.apache.commons.lang3.tuple.MutablePair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; + +public class ArraySchema implements ISchema { + + public static Builder builder() { + return new Builder(); + } + + private final World world; + private final BlockInfo[][][] blocks; + private BiPredicate renderFilter; + private final Vec3d center; + + public ArraySchema(BlockInfo[][][] blocks) { + this.blocks = blocks; + this.world = new DummyWorld(); + BlockPos.MutableBlockPos current = new BlockPos.MutableBlockPos(); + BlockPos.MutableBlockPos max = new BlockPos.MutableBlockPos(BlockPosUtil.MIN); + for (int x = 0; x < blocks.length; x++) { + for (int y = 0; y < blocks[x].length; y++) { + for (int z = 0; z < blocks[x][y].length; z++) { + BlockInfo block = blocks[x][y][z]; + if (block == null) continue; + current.setPos(x, y, z); + BlockPosUtil.setMax(max, current); + block.apply(this.world, current); + } + } + } + this.center = BlockPosUtil.getCenterD(BlockPos.ORIGIN, BlockPosUtil.add(max, 1, 1, 1)); + } + + @Override + public World getWorld() { + return world; + } + + @Override + public Vec3d getFocus() { + return center; + } + + @Override + public BlockPos getOrigin() { + return BlockPos.ORIGIN; + } + + @Override + public void setRenderFilter(@Nullable BiPredicate renderFilter) { + this.renderFilter = renderFilter; + } + + @Override + public @Nullable BiPredicate getRenderFilter() { + return renderFilter; + } + + @NotNull + @Override + public Iterator> iterator() { + return new AbstractIterator<>() { + + private final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + private final MutablePair pair = new MutablePair<>(pos, null); + private int x = 0, y = 0, z = -1; + + @Override + protected Map.Entry computeNext() { + BlockInfo info; + while (true) { + if (++z >= blocks[x][y].length) { + z = 0; + if (++y >= blocks[x].length) { + y = 0; + if (++x >= blocks.length) { + return endOfData(); + } + } + } + pos.setPos(x, y, z); + info = blocks[x][y][z]; + if (info != null && renderFilter.test(pos, info)) { + pair.setRight(info); + return pair; + } + } + } + }; + } + + public static class Builder { + + private final List tensor = new ArrayList<>(); + private final Char2ObjectMap blockMap = new Char2ObjectOpenHashMap<>(); + + public Builder() { + blockMap.put(' ', BlockInfo.EMPTY); + blockMap.put('#', BlockInfo.EMPTY); + } + + public Builder layer(String... layer) { + this.tensor.add(layer); + return this; + } + + public Builder where(char c, BlockInfo info) { + this.blockMap.put(c, info); + return this; + } + + public Builder whereAir(char c) { + return where(c, BlockInfo.EMPTY); + } + + public Builder where(char c, IBlockState blockState) { + return where(c, new BlockInfo(blockState)); + } + + public Builder where(char c, IBlockState blockState, TileEntity tile) { + return where(c, new BlockInfo(blockState, tile)); + } + + public Builder where(char c, Block block) { + return where(c, new BlockInfo(block)); + } + + public Builder where(char c, ResourceLocation registryName, int stateMeta) { + Block block = ForgeRegistries.BLOCKS.getValue(registryName); + if (block == null) throw new IllegalArgumentException("Block with name " + registryName + " doesn't exist!"); + IBlockState state = block.getStateFromMeta(stateMeta); + return where(c, new BlockInfo(state)); + } + + public Builder where(char c, ResourceLocation registryName) { + return where(c, registryName, 0); + } + + public Builder where(char c, String registryName, int stateMeta) { + return where(c, new ResourceLocation(registryName), stateMeta); + } + + public Builder where(char c, String registryName) { + return where(c, new ResourceLocation(registryName), 0); + } + + private void validate() { + if (this.tensor.isEmpty()) { + throw new IllegalArgumentException("no block matrix defined"); + } + List errors = new ArrayList<>(); + CharSet checkedChars = new CharArraySet(); + int layerSize = this.tensor.get(0).length; + for (int x = 0; x < this.tensor.size(); x++) { + String[] xLayer = this.tensor.get(x); + if (xLayer.length == 0) { + errors.add(String.format("Layer %s is empty. This is not right", x + 1)); + } else if (xLayer.length != layerSize) { + errors.add(String.format("Invalid x-layer size. Expected %s, but got %s at layer %s", layerSize, xLayer.length, x + 1)); + } + int rowSize = xLayer[0].length(); + for (int y = 0; y < xLayer.length; y++) { + String yRow = xLayer[y]; + if (yRow.isEmpty()) { + errors.add(String.format("Row %s in layer %s is empty. This is not right", y + 1, x + 1)); + } else if (yRow.length() != rowSize) { + errors.add(String.format("Invalid x-layer size. Expected %s, but got %s at row %s in layer %s", layerSize, xLayer.length, y + 1, x + 1)); + } + for (int z = 0; z < yRow.length(); z++) { + char zChar = yRow.charAt(z); + if (!checkedChars.contains(zChar)) { + if (!this.blockMap.containsKey(zChar)) { + errors.add(String.format("Found char '%s' at char %s in row %s in layer %s, but character was not found in map!", zChar, z + 1, y + 1, x + 1)); + } + checkedChars.add(zChar); + } + } + } + } + if (!errors.isEmpty()) { + ModularUI.LOGGER.error("Error validating ArrayScheme BlockArray:"); + for (String e : errors) ModularUI.LOGGER.error(" - {}", e); + throw new IllegalArgumentException("The ArraySchema builder was misconfigured. See message above."); + } + } + + public ArraySchema build() { + validate(); + BlockInfo[][][] blocks = new BlockInfo[this.tensor.size()][this.tensor.get(0).length][this.tensor.get(0)[0].length()]; + for (int x = 0; x < this.tensor.size(); x++) { + String[] xLayer = this.tensor.get(x); + for (int y = 0; y < xLayer.length; y++) { + String yRow = xLayer[y]; + for (int z = 0; z < yRow.length(); z++) { + char zChar = yRow.charAt(z); + BlockInfo info = this.blockMap.get(zChar); + if (info == null || info == BlockInfo.EMPTY) continue; // null -> any allowed -> don't need to check + blocks[x][y][z] = info; + } + } + } + return new ArraySchema(blocks); + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BlockInfo.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BlockInfo.java new file mode 100644 index 000000000..44e4effa7 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BlockInfo.java @@ -0,0 +1,148 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockAccess; +import net.minecraft.world.World; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * BlockInfo represents immutable information for block in world + * This includes block state and tile entity, and needed for complete representation + * of some complex blocks like machines, when rendering or manipulating them without world instance + */ +public class BlockInfo { + + public static final BlockInfo EMPTY = new BlockInfo(Blocks.AIR); + public static final BlockInfo INVALID = new BlockInfo(Blocks.AIR); + + public static BlockInfo of(IBlockAccess world, BlockPos pos) { + IBlockState blockState = world.getBlockState(pos); + if (blockState.getBlock().isAir(blockState, world, pos)) { + return EMPTY; + } + TileEntity tile = null; + if (blockState.getBlock().hasTileEntity(blockState)) { + tile = world.getTileEntity(pos); + } + return new BlockInfo(blockState, tile); + } + + private IBlockState blockState; + private TileEntity tileEntity; + + public BlockInfo(@NotNull Block block) { + this(block.getDefaultState()); + } + + public BlockInfo(@NotNull IBlockState blockState) { + this(blockState, null); + } + + public BlockInfo(@NotNull IBlockState blockState, @Nullable TileEntity tileEntity) { + set(blockState, tileEntity); + } + + public IBlockState getBlockState() { + return blockState; + } + + public TileEntity getTileEntity() { + return tileEntity; + } + + public void apply(World world, BlockPos pos) { + world.setBlockState(pos, blockState); + if (tileEntity != null) { + world.setTileEntity(pos, tileEntity); + } else { + tileEntity = world.getTileEntity(pos); + } + } + + BlockInfo set(IBlockState state, TileEntity tile) { + Preconditions.checkNotNull(state, "Block state must not be null!"); + Preconditions.checkArgument(tile == null || state.getBlock().hasTileEntity(state), + "Cannot create block info with tile entity for block not having it!"); + this.blockState = state; + this.tileEntity = tile; + return this; + } + + public boolean isMutable() { + return false; + } + + public Mut toMutable() { + return new Mut(this.blockState, this.tileEntity); + } + + public BlockInfo toImmutable() { + return this; + } + + public BlockInfo copy() { + return new BlockInfo(this.blockState, this.tileEntity); + } + + public static class Mut extends BlockInfo { + + public static final Mut SHARED = new Mut(); + + public Mut() { + this(Blocks.AIR); + } + + public Mut(@NotNull Block block) { + super(block); + } + + public Mut(@NotNull IBlockState blockState) { + super(blockState); + } + + public Mut(@NotNull IBlockState blockState, @Nullable TileEntity tileEntity) { + super(blockState, tileEntity); + } + + @Override + public Mut set(IBlockState state, TileEntity tile) { + return (Mut) super.set(state, tile); + } + + public Mut set(IBlockAccess world, BlockPos pos) { + IBlockState blockState = world.getBlockState(pos); + TileEntity tile = null; + if (blockState.getBlock().hasTileEntity(blockState)) { + tile = world.getTileEntity(pos); + } + return set(blockState, tile); + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Mut toMutable() { + return this; + } + + @Override + public BlockInfo toImmutable() { + return new BlockInfo(getBlockState(), getTileEntity()); + } + + @Override + public Mut copy() { + return new Mut(getBlockState(), getTileEntity()); + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BlockPosUtil.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BlockPosUtil.java new file mode 100644 index 000000000..5a48cbf5a --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BlockPosUtil.java @@ -0,0 +1,84 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; + +public class BlockPosUtil { + + public static final BlockPos MAX = new BlockPos(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + public static final BlockPos MIN = new BlockPos(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + + public static int getManhattanDistance(BlockPos p1, BlockPos p2) { + return getXDist(p1, p2) + getYDist(p1, p2) + getZDist(p1, p2); + } + + public static int getBlockCountInside(BlockPos p1, BlockPos p2) { + return getXDist(p1, p2) * getYDist(p1, p2) * getZDist(p1, p2); + } + + public static int getXDist(BlockPos p1, BlockPos p2) { + return Math.abs(p1.getX() - p2.getX()); + } + + public static int getYDist(BlockPos p1, BlockPos p2) { + return Math.abs(p1.getY() - p2.getY()); + } + + public static int getZDist(BlockPos p1, BlockPos p2) { + return Math.abs(p1.getZ() - p2.getZ()); + } + + public static BlockPos getMin(BlockPos p1, BlockPos p2) { + return new BlockPos(Math.min(p1.getX(), p2.getX()), Math.min(p1.getY(), p2.getY()), Math.min(p1.getZ(), p2.getZ())); + } + + public static BlockPos getMax(BlockPos p1, BlockPos p2) { + return new BlockPos(Math.max(p1.getX(), p2.getX()), Math.max(p1.getY(), p2.getY()), Math.max(p1.getZ(), p2.getZ())); + } + + public static void setMin(BlockPos.MutableBlockPos p1, BlockPos p2) { + p1.setPos(Math.min(p1.getX(), p2.getX()), Math.min(p1.getY(), p2.getY()), Math.min(p1.getZ(), p2.getZ())); + } + + public static void setMax(BlockPos.MutableBlockPos p1, BlockPos p2) { + p1.setPos(Math.max(p1.getX(), p2.getX()), Math.max(p1.getY(), p2.getY()), Math.max(p1.getZ(), p2.getZ())); + } + + public static BlockPos getCenter(BlockPos p1, BlockPos p2) { + BlockPos min = getMin(p1, p2); + return new BlockPos(getXDist(p1, p2) / 2 + min.getX(), getYDist(p1, p2) / 2 + min.getY(), getYDist(p1, p2) / 2 + min.getY()); + } + + public static Vec3d getCenterD(BlockPos p1, BlockPos p2) { + return getCenterD(getMin(p1, p2), getXDist(p1, p2), getYDist(p1, p2), getZDist(p1, p2)); + } + + public static Vec3d getCenterD(BlockPos origin, int xs, int ys, int zs) { + return new Vec3d(xs / 2.0 + origin.getX(), ys / 2.0 + origin.getY(), zs / 2.0 + origin.getY()); + } + + public static Iterable getAllInside(BlockPos p1, BlockPos p2, boolean includeBorder) { + int x0 = Math.min(p1.getX(), p2.getX()), y0 = Math.min(p1.getY(), p2.getY()), z0 = Math.min(p1.getZ(), p2.getZ()); + int x1 = Math.max(p1.getX(), p2.getX()), y1 = Math.max(p1.getY(), p2.getY()), z1 = Math.max(p1.getZ(), p2.getZ()); + if (includeBorder) { + x0--; + y0--; + z0--; + } else { + x1--; + y1--; + z1--; + } + return BlockPos.getAllInBoxMutable(x0, y0, z0, x1, y1, z1); + } + + public static boolean isOnBorder(BlockPos boxMin, BlockPos boxMax, BlockPos p) { + return p.getX() == boxMin.getX() || p.getX() == boxMax.getX() || + p.getY() == boxMin.getY() || p.getY() == boxMax.getY() || + p.getZ() == boxMin.getZ() || p.getZ() == boxMax.getZ(); + } + + public static BlockPos.MutableBlockPos add(BlockPos.MutableBlockPos pos, int x, int y, int z) { + return pos.setPos(pos.getX() + x, pos.getY() + y, pos.getZ() + z); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BoxSchema.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BoxSchema.java new file mode 100644 index 000000000..5daa2a9bb --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/BoxSchema.java @@ -0,0 +1,53 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import java.util.function.BiPredicate; + +public class BoxSchema extends PosListSchema { + + public static BoxSchema of(World world, BlockPos center, int r) { + return new BoxSchema(world, center.add(-r, -r, -r), center.add(r, r, r), (blockPos, blockInfo) -> true); + } + + public static BoxSchema of(World world, BlockPos center, int r, BiPredicate renderFilter) { + return new BoxSchema(world, center.add(-r, -r, -r), center.add(r, r, r), renderFilter); + } + + private final World world; + private final BlockPos min, max; + private final Vec3d center; + + public BoxSchema(World world, BlockPos min, BlockPos max, BiPredicate renderFilter) { + super(world, BlockPosUtil.getAllInside(min, max, false), renderFilter); + this.world = world; + this.min = BlockPosUtil.getMin(min, max); + this.max = BlockPosUtil.getMax(min, max); + this.center = BlockPosUtil.getCenterD(min, max); + } + + @Override + public World getWorld() { + return world; + } + + @Override + public Vec3d getFocus() { + return center; + } + + @Override + public BlockPos getOrigin() { + return min; + } + + public BlockPos getMin() { + return min; + } + + public BlockPos getMax() { + return max; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Camera.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Camera.java new file mode 100644 index 000000000..e11b7fcf3 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Camera.java @@ -0,0 +1,57 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.util.math.Vec3i; + +import org.lwjgl.util.vector.Vector3f; + +public class Camera { + + private final Vector3f pos; + private final Vector3f lookAt; + + public Camera(Vector3f pos, Vector3f lookAt) { + this.pos = pos; + this.lookAt = lookAt; + } + + public Camera setLookAt(Vector3f pos, Vector3f lookAt) { + this.pos.set(pos); + this.lookAt.set(lookAt); + return this; + } + + public Camera setLookAt(float x, float y, float z) { + this.lookAt.set(x, y, z); + return this; + } + + public Camera setPos(float x, float y, float z) { + this.lookAt.set(x, y, z); + return this; + } + + public Camera setLookAt(Vector3f lookAt, double radius, double yaw, double pitch) { + return setLookAt(lookAt.x, lookAt.y, lookAt.z, radius, yaw, pitch); + } + + public Camera setLookAt(Vec3i lookAt, double radius, double yaw, double pitch) { + return setLookAt(lookAt.getX(), lookAt.getY(), lookAt.getZ(), radius, yaw, pitch); + } + + public Camera setLookAt(float lookAtX, float lookAtY, float lookAtZ, double radius, double yaw, double pitch) { + setLookAt(lookAtX, lookAtY, lookAtZ); + Vector3f pos = new Vector3f((float) Math.cos(yaw), (float) 0, (float) Math.sin(yaw)); + pos.y += (float) (Math.tan(pitch) * pos.length()); + pos.normalise().scale((float) radius); + this.pos.set(pos.translate(lookAtX, lookAtY, lookAtZ)); + return this; + } + + public Vector3f getPos() { + return pos; + } + + public Vector3f getLookAt() { + return lookAt; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummyChunkProvider.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummyChunkProvider.java new file mode 100644 index 000000000..79d301418 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummyChunkProvider.java @@ -0,0 +1,57 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.IChunkProvider; + +import io.netty.util.collection.LongObjectHashMap; +import io.netty.util.collection.LongObjectMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class DummyChunkProvider implements IChunkProvider { + + private final World world; + private final LongObjectMap loadedChunks = new LongObjectHashMap<>(); + + public DummyChunkProvider(World world) { + this.world = world; + } + + @Nullable + @Override + public Chunk getLoadedChunk(int x, int z) { + return loadedChunks.get(ChunkPos.asLong(x, z)); + } + + @NotNull + @Override + public Chunk provideChunk(int x, int z) { + long chunkKey = ChunkPos.asLong(x, z); + if (loadedChunks.containsKey(chunkKey)) + return loadedChunks.get(chunkKey); + Chunk chunk = new Chunk(world, x, z); + loadedChunks.put(chunkKey, chunk); + return chunk; + } + + @Override + public boolean tick() { + for (Chunk chunk : loadedChunks.values()) { + chunk.onTick(false); + } + return !loadedChunks.isEmpty(); + } + + @NotNull + @Override + public String makeString() { + return "Dummy"; + } + + @Override + public boolean isChunkGeneratedAt(int x, int z) { + return true; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummySaveHandler.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummySaveHandler.java new file mode 100644 index 000000000..66040aeed --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummySaveHandler.java @@ -0,0 +1,105 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.datafix.DataFixer; +import net.minecraft.world.World; +import net.minecraft.world.WorldProvider; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.IChunkLoader; +import net.minecraft.world.gen.structure.template.TemplateManager; +import net.minecraft.world.storage.IPlayerFileData; +import net.minecraft.world.storage.ISaveHandler; +import net.minecraft.world.storage.WorldInfo; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; + +@SuppressWarnings("all") +public class DummySaveHandler implements ISaveHandler, IPlayerFileData, IChunkLoader { + + @Nullable + @Override + public WorldInfo loadWorldInfo() { + return null; + } + + @Override + public void checkSessionLock() {} + + @NotNull + @Override + public IChunkLoader getChunkLoader(@NotNull WorldProvider provider) { + return this; + } + + @NotNull + @Override + public IPlayerFileData getPlayerNBTManager() { + return this; + } + + @NotNull + @Override + public TemplateManager getStructureTemplateManager() { + return new TemplateManager("", new DataFixer(0)); + } + + @Override + public void saveWorldInfoWithPlayer(@NotNull WorldInfo worldInformation, @NotNull NBTTagCompound tagCompound) {} + + @Override + public void saveWorldInfo(@NotNull WorldInfo worldInformation) {} + + @NotNull + @Override + public File getWorldDirectory() { + return null; + } + + @NotNull + @Override + public File getMapFileFromName(@NotNull String mapName) { + return null; + } + + @Nullable + @Override + public Chunk loadChunk(@NotNull World worldIn, int x, int z) { + return null; + } + + @Override + public void saveChunk(@NotNull World worldIn, @NotNull Chunk chunkIn) {} + + @Override + public void saveExtraChunkData(@NotNull World worldIn, @NotNull Chunk chunkIn) {} + + @Override + public void chunkTick() {} + + @Override + public void flush() {} + + @Override + public boolean isChunkGeneratedAt(int x, int z) { + return false; + } + + @Override + public void writePlayerData(@NotNull EntityPlayer player) {} + + @Nullable + @Override + public NBTTagCompound readPlayerData(@NotNull EntityPlayer player) { + return null; + } + + @NotNull + @Override + public String[] getAvailablePlayerDat() { + return new String[0]; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummyWorld.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummyWorld.java new file mode 100644 index 000000000..9ea204d90 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/DummyWorld.java @@ -0,0 +1,93 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.profiler.Profiler; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.*; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.storage.WorldInfo; +import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraftforge.fml.relauncher.FMLLaunchHandler; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class DummyWorld extends World { + + private static final WorldSettings DEFAULT_SETTINGS = new WorldSettings( + 1L, GameType.SURVIVAL, true, false, WorldType.DEFAULT); + + public static final DummyWorld INSTANCE = new DummyWorld(); + + public DummyWorld() { + super(new DummySaveHandler(), new WorldInfo(DEFAULT_SETTINGS, "DummyServer"), new WorldProviderSurface(), + new Profiler(), false); + // Guarantee the dimension ID was not reset by the provider + this.provider.setDimension(Integer.MAX_VALUE); + int providerDim = this.provider.getDimension(); + this.provider.setWorld(this); + this.provider.setDimension(providerDim); + this.chunkProvider = this.createChunkProvider(); + this.calculateInitialSkylight(); + this.calculateInitialWeather(); + this.getWorldBorder().setSize(30000000); + // De-allocate lightUpdateBlockList, checkLightFor uses this + ObfuscationReflectionHelper.setPrivateValue(World.class, this, null, + FMLLaunchHandler.isDeobfuscatedEnvironment() ? "lightUpdateBlockList" : "field_72994_J"); + } + + @Override + public void notifyNeighborsRespectDebug(@NotNull BlockPos pos, @NotNull Block blockType, boolean p_175722_3_) { + // NOOP - do not trigger forge events + } + + @Override + public void notifyNeighborsOfStateChange(@NotNull BlockPos pos, @NotNull Block blockType, boolean updateObservers) { + // NOOP - do not trigger forge events + } + + @Override + public void notifyNeighborsOfStateExcept(@NotNull BlockPos pos, @NotNull Block blockType, + @NotNull EnumFacing skipSide) { + // NOOP - do not trigger forge events + } + + @Override + public void markAndNotifyBlock(@NotNull BlockPos pos, @Nullable Chunk chunk, @NotNull IBlockState iblockstate, + @NotNull IBlockState newState, int flags) { + // NOOP - do not trigger forge events + } + + @Override + public void notifyBlockUpdate(@NotNull BlockPos pos, @NotNull IBlockState oldState, @NotNull IBlockState newState, + int flags) {} + + @Override + public void markBlockRangeForRenderUpdate(@NotNull BlockPos rangeMin, @NotNull BlockPos rangeMax) {} + + @Override + public void markBlockRangeForRenderUpdate(int x1, int y1, int z1, int x2, int y2, int z2) {} + + @Override + public void updateObservingBlocksAt(@NotNull BlockPos pos, @NotNull Block blockType) {} + + @NotNull + @Override + protected IChunkProvider createChunkProvider() { + return new DummyChunkProvider(this); + } + + @Override + protected boolean isChunkLoaded(int x, int z, boolean allowEmpty) { + return chunkProvider.isChunkGeneratedAt(x, z); + } + + @Override + // De-allocated lightUpdateBlockList, default return + public boolean checkLightFor(@NotNull EnumSkyBlock lightType, @NotNull BlockPos pos) { + return true; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/ISchema.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/ISchema.java new file mode 100644 index 000000000..7f2847d97 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/ISchema.java @@ -0,0 +1,23 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.function.BiPredicate; + +public interface ISchema extends Iterable> { + + World getWorld(); + + Vec3d getFocus(); + + BlockPos getOrigin(); + + void setRenderFilter(@Nullable BiPredicate renderFilter); + + @Nullable BiPredicate getRenderFilter(); +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/MapSchema.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/MapSchema.java new file mode 100644 index 000000000..e4781aa3e --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/MapSchema.java @@ -0,0 +1,146 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import com.google.common.collect.AbstractIterator; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.Function; + +public class MapSchema implements ISchema { + + private final World world; + private final Object2ObjectOpenHashMap blocks = new Object2ObjectOpenHashMap<>(); + private BiPredicate renderFilter; + private final BlockPos origin; + private final Vec3d center; + + public MapSchema(Map blocks) { + this(blocks, null); + } + + public MapSchema(Map blocks, BiPredicate renderFilter) { + this.world = new DummyWorld(); + this.renderFilter = renderFilter; + BlockPos.MutableBlockPos min = new BlockPos.MutableBlockPos(BlockPosUtil.MAX); + BlockPos.MutableBlockPos max = new BlockPos.MutableBlockPos(BlockPosUtil.MIN); + if (!blocks.isEmpty()) { + for (var entry : blocks.entrySet()) { + if (entry.getValue().getBlockState().getBlock() != Blocks.AIR) { + this.blocks.put(entry.getKey(), entry.getValue()); + entry.getValue().apply(this.world, entry.getKey()); + BlockPosUtil.setMin(min, entry.getKey()); + BlockPosUtil.setMax(max, entry.getKey()); + } + } + } else { + min.setPos(0, 0, 0); + max.setPos(0, 0, 0); + } + this.origin = min.toImmutable(); + this.center = BlockPosUtil.getCenterD(min, max); + } + + @Override + public void setRenderFilter(@Nullable BiPredicate renderFilter) { + this.renderFilter = renderFilter; + } + + @Override + public @Nullable BiPredicate getRenderFilter() { + return renderFilter; + } + + @Override + public World getWorld() { + return this.world; + } + + @Override + public Vec3d getFocus() { + return center; + } + + @Override + public BlockPos getOrigin() { + return origin; + } + + @NotNull + @Override + public Iterator> iterator() { + return new AbstractIterator<>() { + + private final ObjectIterator> it = blocks.object2ObjectEntrySet().fastIterator(); + + @Override + protected Map.Entry computeNext() { + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (renderFilter == null || renderFilter.test(entry.getKey(), entry.getValue())) { + return entry; + } + } + return endOfData(); + } + }; + } + + public static class Builder { + + private final Object2ObjectOpenHashMap blocks = new Object2ObjectOpenHashMap<>(); + private BiPredicate renderFilter; + + public Builder add(BlockPos pos, IBlockState state) { + return add(pos, state, null); + } + + public Builder add(BlockPos pos, IBlockState state, TileEntity customTile) { + if (state.getBlock() == Blocks.AIR) return this; + this.blocks.put(pos, new BlockInfo(state, customTile)); + return this; + } + + public Builder add(BlockPos pos, BlockInfo blockInfo) { + this.blocks.put(pos, blockInfo.toImmutable()); + return this; + } + + public Builder add(Iterable posList, Function function) { + for (BlockPos pos : posList) { + BlockInfo info = function.apply(pos).toImmutable(); + add(pos, info); + } + return this; + } + + public Builder add(Map blocks) { + this.blocks.putAll(blocks); + return this; + } + + public Builder setRenderFilter(BiPredicate renderFilter) { + this.renderFilter = renderFilter; + return this; + } + + public MapSchema build() { + if (renderFilter == null) { + return new MapSchema(this.blocks); + } + return new MapSchema(this.blocks, renderFilter); + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/PosListSchema.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/PosListSchema.java new file mode 100644 index 000000000..6faaf31e6 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/PosListSchema.java @@ -0,0 +1,69 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; +import java.util.Map; +import java.util.function.BiPredicate; + +public abstract class PosListSchema implements ISchema { + + private final World world; + private final Iterable posList; + private BiPredicate renderFilter; + + public PosListSchema(World world, Iterable posList, BiPredicate renderFilter) { + this.world = world; + this.posList = posList; + this.renderFilter = renderFilter; + } + + @Override + public void setRenderFilter(@Nullable BiPredicate renderFilter) { + this.renderFilter = renderFilter; + } + + @Override + public @Nullable BiPredicate getRenderFilter() { + return renderFilter; + } + + @Override + public World getWorld() { + return world; + } + + @NotNull + @Override + public Iterator> iterator() { + return new Iterator<>() { + + private final Iterator posIt = PosListSchema.this.posList.iterator(); + private final MutablePair pair = new MutablePair<>(); + + @Override + public boolean hasNext() { + return posIt.hasNext(); + } + + @Override + public Pair next() { + BlockPos pos = posIt.next(); + pair.setLeft(pos); + BlockInfo.Mut.SHARED.set(PosListSchema.this.world, pos); + if (renderFilter == null || renderFilter.test(pos, BlockInfo.Mut.SHARED)) { + pair.setRight(BlockInfo.Mut.SHARED); + } else { + pair.setRight(BlockInfo.EMPTY); + } + return pair; + } + }; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Projection.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Projection.java new file mode 100644 index 000000000..8a877c633 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Projection.java @@ -0,0 +1,87 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.util.math.BlockPos; + +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.util.glu.GLU; +import org.lwjgl.util.vector.Vector3f; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public class Projection { + + public static final Projection INSTANCE = new Projection(); + + protected static final FloatBuffer MODELVIEW_MATRIX_BUFFER = BufferUtils.createFloatBuffer(16); + protected static final FloatBuffer PROJECTION_MATRIX_BUFFER = BufferUtils.createFloatBuffer(16); + protected static final IntBuffer VIEWPORT_BUFFER = BufferUtils.createIntBuffer(16); + protected static final FloatBuffer PIXEL_DEPTH_BUFFER = BufferUtils.createFloatBuffer(1); + protected static final FloatBuffer OBJECT_POS_BUFFER = BufferUtils.createFloatBuffer(3); + + private Projection() {} + + public Vector3f project(BlockPos pos) { + // read current rendering parameters + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, MODELVIEW_MATRIX_BUFFER); + GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, PROJECTION_MATRIX_BUFFER); + GL11.glGetInteger(GL11.GL_VIEWPORT, VIEWPORT_BUFFER); + + // rewind buffers after write by OpenGL glGet calls + MODELVIEW_MATRIX_BUFFER.rewind(); + PROJECTION_MATRIX_BUFFER.rewind(); + VIEWPORT_BUFFER.rewind(); + OBJECT_POS_BUFFER.rewind(); + + // call gluProject with retrieved parameters + GLU.gluProject(pos.getX() + 0.5f, pos.getY() + 0.5f, pos.getZ() + 0.5f, MODELVIEW_MATRIX_BUFFER, + PROJECTION_MATRIX_BUFFER, VIEWPORT_BUFFER, OBJECT_POS_BUFFER); + + // rewind buffers after read + VIEWPORT_BUFFER.rewind(); + PROJECTION_MATRIX_BUFFER.rewind(); + MODELVIEW_MATRIX_BUFFER.rewind(); + OBJECT_POS_BUFFER.rewind(); + + // obtain position in Screen + float winX = OBJECT_POS_BUFFER.get(); + float winY = OBJECT_POS_BUFFER.get(); + float winZ = OBJECT_POS_BUFFER.get(); + + return new Vector3f(winX, winY, winZ); + } + + public Vector3f unProject(int screenX, int screenY) { + // read current rendering parameters + GL11.glReadPixels(screenX, screenY, 1, 1, GL11.GL_DEPTH_COMPONENT, GL11.GL_FLOAT, PIXEL_DEPTH_BUFFER); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, MODELVIEW_MATRIX_BUFFER); + GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, PROJECTION_MATRIX_BUFFER); + GL11.glGetInteger(GL11.GL_VIEWPORT, VIEWPORT_BUFFER); + + // rewind buffers after write by OpenGL glGet calls + PIXEL_DEPTH_BUFFER.rewind(); + MODELVIEW_MATRIX_BUFFER.rewind(); + PROJECTION_MATRIX_BUFFER.rewind(); + VIEWPORT_BUFFER.rewind(); + OBJECT_POS_BUFFER.rewind(); + + // call gluUnProject with retrieved parameters + GLU.gluUnProject(screenX, screenY, PIXEL_DEPTH_BUFFER.get(), MODELVIEW_MATRIX_BUFFER, PROJECTION_MATRIX_BUFFER, VIEWPORT_BUFFER, + OBJECT_POS_BUFFER); + + // rewind buffers after read + PIXEL_DEPTH_BUFFER.rewind(); + VIEWPORT_BUFFER.rewind(); + PROJECTION_MATRIX_BUFFER.rewind(); + MODELVIEW_MATRIX_BUFFER.rewind(); + OBJECT_POS_BUFFER.rewind(); + + // obtain absolute position in world + float posX = OBJECT_POS_BUFFER.get(); + float posY = OBJECT_POS_BUFFER.get(); + float posZ = OBJECT_POS_BUFFER.get(); + + return new Vector3f(posX, posY, posZ); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/RenderWorld.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/RenderWorld.java new file mode 100644 index 000000000..74bc0b889 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/RenderWorld.java @@ -0,0 +1,77 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockAccess; +import net.minecraft.world.World; +import net.minecraft.world.WorldType; +import net.minecraft.world.biome.Biome; + +import net.minecraft.world.chunk.Chunk; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class RenderWorld implements IBlockAccess { + + private final ISchema schema; + private final World world; + + public RenderWorld(ISchema schema) { + this.schema = schema; + this.world = schema.getWorld(); + } + + @Nullable + @Override + public TileEntity getTileEntity(@NotNull BlockPos pos) { + if (this.schema == null) return this.world.getTileEntity(pos); + BlockInfo.Mut.SHARED.set(this.world, pos); + return this.schema.getRenderFilter().test(pos, BlockInfo.Mut.SHARED) ? BlockInfo.Mut.SHARED.getTileEntity() : null; + } + + @Override + public int getCombinedLight(@NotNull BlockPos pos, int lightValue) { + return this.world.getCombinedLight(pos, lightValue); + } + + @Override + public @NotNull IBlockState getBlockState(@NotNull BlockPos pos) { + if (this.schema == null) return this.world.getBlockState(pos); + BlockInfo.Mut.SHARED.set(this.world, pos); + return this.schema.getRenderFilter().test(pos, BlockInfo.Mut.SHARED) ? BlockInfo.Mut.SHARED.getBlockState() : Blocks.AIR.getDefaultState(); + } + + @Override + public boolean isAirBlock(@NotNull BlockPos pos) { + IBlockState state = getBlockState(pos); + return state.getBlock().isAir(state, this, pos); + } + + @Override + public @NotNull Biome getBiome(@NotNull BlockPos pos) { + return this.world.getBiome(pos); + } + + @Override + public int getStrongPower(@NotNull BlockPos pos, @NotNull EnumFacing direction) { + return this.world.getStrongPower(pos, direction); + } + + @Override + public @NotNull WorldType getWorldType() { + return this.world.getWorldType(); + } + + @Override + public boolean isSideSolid(@NotNull BlockPos pos, @NotNull EnumFacing side, boolean _default) { + if (!world.isValid(pos)) return _default; + + Chunk chunk = world.getChunk(pos); + if (chunk == null || chunk.isEmpty()) return _default; + return getBlockState(pos).isSideSolid(this, pos, side); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/SchemaRenderer.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/SchemaRenderer.java new file mode 100644 index 000000000..1c29d700d --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/SchemaRenderer.java @@ -0,0 +1,336 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import com.cleanroommc.modularui.api.drawable.IDrawable; +import com.cleanroommc.modularui.screen.viewport.GuiContext; +import com.cleanroommc.modularui.theme.WidgetTheme; +import com.cleanroommc.modularui.utils.Color; +import com.cleanroommc.modularui.widget.sizer.Area; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.*; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.BlockRenderLayer; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.RayTraceResult; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.IBlockAccess; +import net.minecraftforge.client.ForgeHooksClient; +import net.minecraftforge.client.MinecraftForgeClient; + +import org.lwjgl.opengl.EXTFramebufferObject; +import org.lwjgl.opengl.GL11; +import org.lwjgl.util.glu.GLU; +import org.lwjgl.util.vector.Vector3f; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; + +public class SchemaRenderer implements IDrawable { + + private static final Framebuffer FBO = new Framebuffer(1080, 1080, true); + + private final ISchema schema; + private final IBlockAccess renderWorld; + private final Framebuffer framebuffer; + private final Camera camera = new Camera(new Vector3f(), new Vector3f()); + private boolean cameraSetup = false; + private DoubleSupplier scale; + private BooleanSupplier disableTESR; + private Consumer onRayTrace; + private Consumer afterRender; + private BiConsumer cameraFunc; + private int clearColor = 0; + private boolean isometric = false; + + public SchemaRenderer(ISchema schema, Framebuffer framebuffer) { + this.schema = schema; + this.framebuffer = framebuffer; + this.renderWorld = new RenderWorld(schema); + } + + public SchemaRenderer(ISchema schema) { + this(schema, FBO); + } + + public SchemaRenderer cameraFunc(BiConsumer camera) { + this.cameraFunc = camera; + return this; + } + + public SchemaRenderer onRayTrace(Consumer consumer) { + this.onRayTrace = consumer; + return this; + } + + public SchemaRenderer afterRender(Consumer consumer) { + this.afterRender = consumer; + return this; + } + + public SchemaRenderer isometric(boolean isometric) { + this.isometric = isometric; + return this; + } + + public SchemaRenderer scale(double scale) { + return scale(() -> scale); + } + + public SchemaRenderer scale(DoubleSupplier scale) { + this.scale = scale; + return this; + } + + public SchemaRenderer disableTESR(boolean disable) { + return disableTESR(() -> disable); + } + + public SchemaRenderer disableTESR(BooleanSupplier disable) { + this.disableTESR = disable; + return this; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + render(x, y, width, height, context.getMouseX(), context.getMouseY()); + } + + public void render(int x, int y, int width, int height, int mouseX, int mouseY) { + if (this.cameraFunc != null) { + this.cameraFunc.accept(this.camera, this.schema); + } + if (Objects.nonNull(scale)) { + Vector3f cameraPos = camera.getPos(); + Vector3f looking = camera.getLookAt(); + Vector3f.sub(cameraPos, looking, cameraPos); + if (cameraPos.length() != 0.0f) cameraPos.normalise(); + cameraPos.scale((float) scale.getAsDouble()); + Vector3f.add(looking, cameraPos, cameraPos); + } + int lastFbo = bindFBO(); + setupCamera(this.framebuffer.framebufferWidth, this.framebuffer.framebufferHeight); + renderWorld(); + if (this.onRayTrace != null && Area.isInside(x, y, width, height, mouseX, mouseY)) { + this.onRayTrace.accept(new IRayTracer() { + @Override + public RayTraceResult rayTrace(int screenX, int screenY) { + return SchemaRenderer.this.rayTrace(Projection.INSTANCE.unProject(screenX, screenY)); + } + + @Override + public RayTraceResult rayTraceMousePos() { + return rayTrace(mouseX, mouseY); + } + }); + } + resetCamera(); + unbindFBO(lastFbo); + + // bind FBO as texture + GlStateManager.enableTexture2D(); + GlStateManager.disableLighting(); + lastFbo = GL11.glGetInteger(GL11.GL_TEXTURE_2D); + GlStateManager.bindTexture(this.framebuffer.framebufferTexture); + GlStateManager.color(1, 1, 1, 1); + + // render rect with FBO texture + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder bufferbuilder = tessellator.getBuffer(); + bufferbuilder.begin(7, DefaultVertexFormats.POSITION_TEX); + + bufferbuilder.pos(x + width, y + height, 0).tex(1, 0).endVertex(); + bufferbuilder.pos(x + width, y, 0).tex(1, 1).endVertex(); + bufferbuilder.pos(x, y, 0).tex(0, 1).endVertex(); + bufferbuilder.pos(x, y + height, 0).tex(0, 0).endVertex(); + tessellator.draw(); + + GlStateManager.bindTexture(lastFbo); + } + + private void renderWorld() { + Minecraft mc = Minecraft.getMinecraft(); + GlStateManager.enableCull(); + GlStateManager.enableRescaleNormal(); + RenderHelper.disableStandardItemLighting(); + mc.entityRenderer.disableLightmap(); + mc.renderEngine.bindTexture(TextureMap.LOCATION_BLOCKS_TEXTURE); + BlockRenderLayer oldRenderLayer = MinecraftForgeClient.getRenderLayer(); + GlStateManager.disableLighting(); + GlStateManager.enableTexture2D(); + GlStateManager.enableAlpha(); + + try { // render block in each layer + for (BlockRenderLayer layer : BlockRenderLayer.values()) { + ForgeHooksClient.setRenderLayer(layer); + int pass = layer == BlockRenderLayer.TRANSLUCENT ? 1 : 0; + setDefaultPassRenderState(pass); + BufferBuilder buffer = Tessellator.getInstance().getBuffer(); + buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.BLOCK); + BlockRendererDispatcher blockrendererdispatcher = mc.getBlockRendererDispatcher(); + this.schema.forEach(pair -> { + BlockPos pos = pair.getKey(); + IBlockState state = pair.getValue().getBlockState(); + if (!state.getBlock().isAir(state, this.renderWorld, pos) && state.getBlock().canRenderInLayer(state, layer)) { + blockrendererdispatcher.renderBlock(state, pos, this.renderWorld, buffer); + } + }); + Tessellator.getInstance().draw(); + Tessellator.getInstance().getBuffer().setTranslation(0, 0, 0); + } + } finally { + ForgeHooksClient.setRenderLayer(oldRenderLayer); + } + + RenderHelper.enableStandardItemLighting(); + GlStateManager.enableLighting(); + + // render TESR + if (disableTESR == null || !disableTESR.getAsBoolean()) { + for (int pass = 0; pass < 2; pass++) { + ForgeHooksClient.setRenderPass(pass); + int finalPass = pass; + GlStateManager.color(1, 1, 1, 1); + setDefaultPassRenderState(pass); + this.schema.forEach(pair -> { + BlockPos pos = pair.getKey(); + TileEntity tile = pair.getValue().getTileEntity(); + if (tile != null && tile.shouldRenderInPass(finalPass)) { + TileEntityRendererDispatcher.instance.render(tile, pos.getX(), pos.getY(), pos.getZ(), 0); + } + }); + } + } + ForgeHooksClient.setRenderPass(-1); + GlStateManager.enableDepth(); + GlStateManager.disableBlend(); + GlStateManager.depthMask(true); + if (this.afterRender != null) { + this.afterRender.accept(Projection.INSTANCE); + } + } + + private static void setDefaultPassRenderState(int pass) { + GlStateManager.color(1, 1, 1, 1); + if (pass == 0) { // SOLID + GlStateManager.enableDepth(); + GlStateManager.disableBlend(); + GlStateManager.depthMask(true); + } else { // TRANSLUCENT + GlStateManager.enableBlend(); + GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GlStateManager.depthMask(false); + } + } + + protected void setupCamera(int width, int height) { + //GlStateManager.pushAttrib(); + + Minecraft.getMinecraft().entityRenderer.disableLightmap(); + GlStateManager.disableLighting(); + GlStateManager.enableDepth(); + GlStateManager.enableBlend(); + + // setup viewport and clear GL buffers + GlStateManager.viewport(0, 0, width, height); + Color.setGlColor(clearColor); + GlStateManager.clear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + + // setup projection matrix to perspective + GlStateManager.matrixMode(GL11.GL_PROJECTION); + GlStateManager.pushMatrix(); + GlStateManager.loadIdentity(); + + float near = this.isometric ? 1f : 0.1f; + float far = 10000.0f; + float fovY = 60.0f; // Field of view in the Y direction + float aspect = (float) width / height; // width and height are the dimensions of your window + float top = near * (float) Math.tan(Math.toRadians(fovY) / 2.0); + float bottom = -top; + float left = aspect * bottom; + float right = aspect * top; + if (this.isometric) { + GL11.glOrtho(left, right, bottom, top, near, far); + } else { + GL11.glFrustum(left, right, bottom, top, near, far); + } + + // setup modelview matrix + GlStateManager.matrixMode(GL11.GL_MODELVIEW); + GlStateManager.pushMatrix(); + GlStateManager.loadIdentity(); + if (this.isometric) { + GlStateManager.scale(0.1, 0.1, 0.1); + } + var c = this.camera.getPos(); + var lookAt = this.camera.getLookAt(); + GLU.gluLookAt(c.x, c.y, c.z, lookAt.x, lookAt.y, lookAt.z, 0, 1, 0); + this.cameraSetup = true; + } + + protected void resetCamera() { + this.cameraSetup = false; + // reset viewport + Minecraft minecraft = Minecraft.getMinecraft(); + GlStateManager.viewport(0, 0, minecraft.displayWidth, minecraft.displayHeight); + + // reset projection matrix + GlStateManager.matrixMode(GL11.GL_PROJECTION); + GlStateManager.popMatrix(); + + // reset modelview matrix + GlStateManager.matrixMode(GL11.GL_MODELVIEW); + GlStateManager.popMatrix(); + + GlStateManager.disableBlend(); + GlStateManager.disableDepth(); + + // reset attributes + // GlStateManager.popAttrib(); + } + + private int bindFBO() { + int lastID = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); + this.framebuffer.setFramebufferColor(0.0F, 0.0F, 0.0F, 0.0F); + this.framebuffer.framebufferClear(); + this.framebuffer.bindFramebuffer(true); + GlStateManager.pushMatrix(); + return lastID; + } + + private void unbindFBO(int lastID) { + GlStateManager.popMatrix(); + this.framebuffer.unbindFramebufferTexture(); + OpenGlHelper.glBindFramebuffer(OpenGlHelper.GL_FRAMEBUFFER, lastID); + } + + private RayTraceResult rayTrace(Vector3f hitPos) { + Vec3d startPos = new Vec3d(this.camera.getPos().x, this.camera.getPos().y, this.camera.getPos().z); + hitPos.scale(2); // Double view range to ensure pos can be seen. + Vec3d endPos = new Vec3d((hitPos.x - startPos.x), (hitPos.y - startPos.y), (hitPos.z - startPos.z)); + return this.schema.getWorld().rayTraceBlocks(startPos, endPos); + } + + public boolean isCameraSetup() { + return cameraSetup; + } + + public interface IRayTracer { + + RayTraceResult rayTrace(int screenX, int screenY); + + RayTraceResult rayTraceMousePos(); + } + + public interface ICamera { + + void setupCamera(Vector3f cameraPos, Vector3f lookAt); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/SchemaWorld.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/SchemaWorld.java new file mode 100644 index 000000000..969cffd22 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/SchemaWorld.java @@ -0,0 +1,118 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import com.google.common.collect.AbstractIterator; +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectListIterator; +import org.apache.commons.lang3.tuple.MutablePair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; +import java.util.Map; +import java.util.function.BiPredicate; + +public class SchemaWorld extends DummyWorld implements ISchema { + + private final ObjectLinkedOpenHashSet blocks = new ObjectLinkedOpenHashSet<>(); + private BiPredicate renderFilter; + private final BlockPos.MutableBlockPos min = new BlockPos.MutableBlockPos(); + private final BlockPos.MutableBlockPos max = new BlockPos.MutableBlockPos(); + + public SchemaWorld() { + this((blockPos, blockInfo) -> true); + } + + public SchemaWorld(BiPredicate renderFilter) { + this.renderFilter = renderFilter; + } + + @Override + public void setRenderFilter(@Nullable BiPredicate renderFilter) { + this.renderFilter = renderFilter; + } + + @Override + public @Nullable BiPredicate getRenderFilter() { + return renderFilter; + } + + @Override + public boolean setBlockState(@NotNull BlockPos pos, @NotNull IBlockState newState, int flags) { + boolean renderTest; + boolean state; + if (renderFilter == null || renderFilter.test(pos, BlockInfo.of(this, pos))) { + renderTest = true; + state = super.setBlockState(pos, newState, flags); + } else { + renderTest = state = false; + } + + if (newState.getBlock().isAir(newState, this, pos)) { + if (this.blocks.remove(pos) && BlockPosUtil.isOnBorder(min, max, pos)) { + if (this.blocks.isEmpty()) { + this.min.setPos(0, 0, 0); + this.max.setPos(0, 0, 0); + } else { + min.setPos(BlockPosUtil.MAX); + max.setPos(BlockPosUtil.MIN); + for (BlockPos pos1 : blocks) { + BlockPosUtil.setMin(min, pos1); + BlockPosUtil.setMax(max, pos1); + } + } + } + } else if (this.blocks.isEmpty()) { + if (!renderTest) return false; + this.blocks.add(pos); + this.min.setPos(pos); + this.max.setPos(pos); + } else if (renderTest && this.blocks.add(pos)) { + BlockPosUtil.setMin(this.min, pos); + BlockPosUtil.setMax(this.max, pos); + } + return renderTest && state; + } + + @Override + public World getWorld() { + return this; + } + + @Override + public Vec3d getFocus() { + return BlockPosUtil.getCenterD(this.min, this.max); + } + + @Override + public BlockPos getOrigin() { + return this.min; + } + + @NotNull + @Override + public Iterator> iterator() { + return new AbstractIterator<>() { + private final ObjectListIterator it = blocks.iterator(); + private final BlockInfo.Mut info = new BlockInfo.Mut(); + private final MutablePair pair = new MutablePair<>(null, this.info); + + @Override + protected Map.Entry computeNext() { + while (it.hasNext()) { + var pos = it.next(); + this.info.set(SchemaWorld.this, pos); + this.pair.setLeft(pos); + if (renderFilter == null || renderFilter.test(pos, info)) { + return this.pair; + } + } + return endOfData(); + } + }; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Structure.java b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Structure.java new file mode 100644 index 000000000..516229d69 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/fakeworld/Structure.java @@ -0,0 +1,85 @@ +package com.cleanroommc.modularui.utils.fakeworld; + +import com.google.common.base.Predicate; +import com.google.common.collect.Lists; +import it.unimi.dsi.fastutil.chars.Char2ObjectMap; +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; + +import it.unimi.dsi.fastutil.chars.CharArrayList; +import it.unimi.dsi.fastutil.chars.CharList; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import net.minecraft.block.state.BlockWorldState; +import net.minecraft.util.math.BlockPos; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class Structure { + private Structure(){} + + public static StaticBuilder staticBuilder(){ + return new StaticBuilder(); + } + + public static class StaticBuilder { + private final List matrix = new ObjectArrayList<>(); + private final Char2ObjectMap map = new Char2ObjectOpenHashMap<>(); + + + private StaticBuilder() { + this.map.put(' ', air()); + } + + public StaticBuilder aisle(String... aisle) { + this.matrix.add(aisle); + return this; + } + + public StaticBuilder where(char c, BlockInfo blockInfo) { + this.map.put(c, blockInfo); + return this; + } + + public static BlockInfo air() { + return BlockInfo.EMPTY; + } + + public Map buildPosMap() { + checkMissingPredicates(); + Map posMap = new Object2ObjectOpenHashMap<>(); + + for (int y = 0; y < matrix.size(); y++) { + var aisle = matrix.get(y); + for (int x = 0; x < aisle.length; x++) { + var aisleX = aisle[x].toCharArray(); + for (int z = 0; z < aisleX.length; z++) { + var aisleZ = aisleX[z]; + posMap.put(new BlockPos(x, y, z), map.get(aisleZ)); + } + } + } + + return posMap; + } + + private void checkMissingPredicates() { + CharList list = new CharArrayList(); + + for (Char2ObjectMap.Entry entry : map.char2ObjectEntrySet()) { + if (Objects.isNull(entry.getValue())) list.add(entry.getCharKey()); + } + + if (!list.isEmpty()) throw new IllegalStateException( + list.stream().map(Object::toString).collect(Collectors.joining(",", "Predicates for character(s) ", " are missing"))); + } + + } + +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/Area.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/Area.java index 4b88b18e9..42f5ca012 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/sizer/Area.java +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/Area.java @@ -14,6 +14,11 @@ */ public class Area extends Rectangle implements IUnResizeable { + public static boolean isInside(int x, int y, int w, int h, int px, int py) { + SHARED.set(x, y, w, h); + return SHARED.isInside(px, py); + } + public static final Area SHARED = new Area(); /** diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SchemaWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SchemaWidget.java new file mode 100644 index 000000000..6808ea564 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/SchemaWidget.java @@ -0,0 +1,183 @@ +package com.cleanroommc.modularui.widgets; + +import com.cleanroommc.modularui.api.drawable.IDrawable; +import com.cleanroommc.modularui.api.drawable.IKey; +import com.cleanroommc.modularui.api.widget.Interactable; +import com.cleanroommc.modularui.drawable.GuiTextures; +import com.cleanroommc.modularui.screen.ModularScreen; +import com.cleanroommc.modularui.utils.VectorUtil; +import com.cleanroommc.modularui.utils.fakeworld.ISchema; +import com.cleanroommc.modularui.utils.fakeworld.SchemaRenderer; +import com.cleanroommc.modularui.widget.Widget; + +import net.minecraft.util.math.MathHelper; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.util.vector.Vector3f; + +public class SchemaWidget extends Widget implements Interactable { + + public static final float PI = (float) Math.PI; + public static final float PI2 = 2 * PI; + + private final SchemaRenderer schema; + private boolean enableRotation = true; + private boolean enableTranslation = true; + private boolean enableScaling = true; + private int lastMouseX; + private int lastMouseY; + private double scale = 10; + private float pitch = (float) (Math.PI / 4f); + private float yaw = (float) (Math.PI / 4f); + private final Vector3f offset = new Vector3f(); + + public SchemaWidget(ISchema schema) { + this(new SchemaRenderer(schema)); + } + + public SchemaWidget(SchemaRenderer schema) { + this.schema = schema; + schema.cameraFunc((camera, $schema) -> { + Vector3f focus = VectorUtil.vec3fAdd(this.offset, null, $schema.getFocus()); + camera.setLookAt(focus, scale, yaw, pitch); + }); + } + + @Override + public boolean onMouseScroll(ModularScreen.UpOrDown scrollDirection, int amount) { + if (this.enableScaling) { + scale(-scrollDirection.modifier * amount / 120.0); + return true; + } + return false; + } + + @Override + public @NotNull Result onMousePressed(int mouseButton) { + this.lastMouseX = getContext().getAbsMouseX(); + this.lastMouseY = getContext().getAbsMouseY(); + return Result.SUCCESS; + } + + @Override + public void onMouseDrag(int mouseButton, long timeSinceClick) { + int mouseX = getContext().getAbsMouseX(); + int mouseY = getContext().getAbsMouseY(); + int dx = mouseX - lastMouseX; + int dy = mouseY - lastMouseY; + if (mouseButton == 0 && this.enableRotation) { + float moveScale = 0.025f; + yaw = (yaw + dx * moveScale + PI2) % PI2; + pitch = MathHelper.clamp(pitch + dy * moveScale, -PI2 / 4 + 0.001f, PI2 / 4 - 0.001f); + } else if (mouseButton == 2 && this.enableTranslation) { + // the idea is to construct a vector which points upwards from the camerae pov (y-axis on screen) + // this vector determines the amount of z offset from mouse movement in y + float y = (float) Math.cos(pitch); + float moveScale = 0.06f; + // with this the offset can be moved by dy + offset.translate(0, dy * y * moveScale, 0); + // to respect dx we need a new vector which is perpendicular on the previous vector (x-axis on screen) + // y = 0 => mouse movement in x does not move y + float phi = (yaw + PI / 2) % PI2; + float x = (float) Math.cos(phi); + float z = (float) Math.sin(phi); + offset.translate(dx * x * moveScale, 0, dx * z * moveScale); + } + this.lastMouseX = mouseX; + this.lastMouseY = mouseY; + } + + public SchemaWidget scale(double scale) { + this.scale += scale; + return this; + } + + public SchemaWidget pitch(float pitch) { + this.pitch += pitch; + return this; + } + + public SchemaWidget yaw(float yaw) { + this.yaw += yaw; + return this; + } + + public SchemaWidget offset(float x, float y, float z) { + this.offset.set(x, y, z); + return this; + } + + public SchemaWidget enableDragRotation(boolean enable) { + this.enableRotation = enable; + return this; + } + + public SchemaWidget enableDragTranslation(boolean enable) { + this.enableTranslation = enable; + return this; + } + + public SchemaWidget enableScrollScaling(boolean enable) { + this.enableScaling = enable; + return this; + } + + public SchemaWidget enableInteraction(boolean rotation, boolean translation, boolean scaling) { + return enableDragRotation(rotation) + .enableDragTranslation(translation) + .enableScrollScaling(scaling); + } + + public SchemaWidget enableAllInteraction(boolean enable) { + return enableInteraction(enable, enable, enable); + } + + @Override + public @Nullable IDrawable getOverlay() { + return schema; + } + + public static class LayerButton extends ButtonWidget { + + private final int minLayer; + private final int maxLayer; + private int currentLayer = Integer.MIN_VALUE; + + public LayerButton(ISchema schema, int minLayer, int maxLayer) { + this.minLayer = minLayer; + this.maxLayer = maxLayer; + background(GuiTextures.MC_BACKGROUND); + overlay(IKey.dynamic(() -> currentLayer > Integer.MIN_VALUE ? Integer.toString(currentLayer) : "ALL").scale(0.5f)); + + onMousePressed(mouseButton -> { + if (mouseButton == 0 || mouseButton == 1) { + if (mouseButton == 0) { + if (currentLayer == Integer.MIN_VALUE) { + currentLayer = minLayer; + } else { + currentLayer++; + } + } else { + if (currentLayer == Integer.MIN_VALUE) { + currentLayer = maxLayer; + } else { + currentLayer--; + } + } + if (currentLayer > maxLayer || currentLayer < minLayer) { + currentLayer = Integer.MIN_VALUE; + } + return true; + } + return false; + }); + schema.setRenderFilter((blockPos, blockInfo) -> currentLayer == Integer.MIN_VALUE || currentLayer >= blockPos.getY()); + } + + public LayerButton startLayer(int start) { + this.currentLayer = start; + return this; + } + } +}