From 8e47bede86cb316a401faf7657865c58a925460a Mon Sep 17 00:00:00 2001 From: piegamesde Date: Tue, 29 Aug 2017 10:24:42 +0200 Subject: [PATCH 1/7] Redone minimal changes But this time with setting the formatter to only affect the lines that got edited. --- .../togos/minecraft/maprend/RegionMap.java | 3 +- .../minecraft/maprend/RegionRenderer.java | 136 +++++++----------- .../minecraft/maprend/RenderSettings.java | 48 +++++++ .../minecraft/maprend/io/RegionFile.java | 12 +- 4 files changed, 110 insertions(+), 89 deletions(-) create mode 100644 src/main/java/togos/minecraft/maprend/RenderSettings.java diff --git a/src/main/java/togos/minecraft/maprend/RegionMap.java b/src/main/java/togos/minecraft/maprend/RegionMap.java index 196b53e..e34ed85 100644 --- a/src/main/java/togos/minecraft/maprend/RegionMap.java +++ b/src/main/java/togos/minecraft/maprend/RegionMap.java @@ -8,7 +8,8 @@ public class RegionMap { - static class Region { + + public static class Region { public int rx, rz; public File regionFile; public File imageFile; diff --git a/src/main/java/togos/minecraft/maprend/RegionRenderer.java b/src/main/java/togos/minecraft/maprend/RegionRenderer.java index 34425a1..1595957 100644 --- a/src/main/java/togos/minecraft/maprend/RegionRenderer.java +++ b/src/main/java/togos/minecraft/maprend/RegionRenderer.java @@ -3,28 +3,10 @@ import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; -import java.io.DataInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - +import java.io.*; +import java.util.*; import javax.imageio.ImageIO; - -import org.jnbt.ByteArrayTag; -import org.jnbt.ByteTag; -import org.jnbt.CompoundTag; -import org.jnbt.ListTag; -import org.jnbt.NBTInputStream; -import org.jnbt.Tag; - +import org.jnbt.*; import togos.minecraft.maprend.BiomeMap.Biome; import togos.minecraft.maprend.BlockMap.Block; import togos.minecraft.maprend.RegionMap.Region; @@ -61,6 +43,7 @@ class RenderThread extends Thread { this.force = force; } + @Override public void run() { try { for( Region reg : regions ) renderRegion(reg, outputDir, force); @@ -74,47 +57,25 @@ public void run() { public final Set defaultedBlockIds = new HashSet(); public final Set defaultedBlockIdDataValues = new HashSet(); public final Set defaultedBiomeIds = new HashSet(); - public final boolean debug; public final BlockMap blockMap; public final BiomeMap biomeMap; public final int air16Color; // Color of 16 air blocks stacked - public final int minHeight; - public final int maxHeight; - public final int shadingReferenceAltitude; - public final int altitudeShadingFactor; - public final int minAltitudeShading; - public final int maxAltitudeShading; - public final String mapTitle; - public final int[] mapScales; /** * Alpha below which blocks are considered transparent for purposes of shading * (i.e. blocks with alpha < this will not be shaded, but blocks below them will be) */ private int shadeOpacityCutoff = 0x20; - - public RegionRenderer( - BlockMap blockMap, BiomeMap biomeMap, boolean debug, int minHeight, int maxHeight, - int shadingRefAlt, int minAltShading, int maxAltShading, int altShadingFactor, - String mapTitle, int[] mapScales - ) { - assert blockMap != null; - assert biomeMap != null; - - this.blockMap = blockMap; - this.biomeMap = biomeMap; - this.air16Color = Color.overlay( 0, getColor(0, 0, 0), 16 ); - this.debug = debug; - - this.minHeight = minHeight; - this.maxHeight = maxHeight; - this.shadingReferenceAltitude = shadingRefAlt; - this.minAltitudeShading = minAltShading; - this.maxAltitudeShading = maxAltShading; - this.altitudeShadingFactor = altShadingFactor; - - this.mapTitle = mapTitle; - this.mapScales = mapScales; + + public final RenderSettings settings; + + public RegionRenderer(RenderSettings settings) throws IOException { + this.settings = settings; + + blockMap = settings.colorMapFile == null ? BlockMap.loadDefault() : BlockMap.load(settings.colorMapFile); + biomeMap = settings.biomeMapFile == null ? BiomeMap.loadDefault() : BiomeMap.load(settings.biomeMapFile); + + this.air16Color = Color.overlay(0, getColor(0, 0, 0), 16); } /** @@ -249,9 +210,11 @@ protected void shade( short[] height, int[] color ) { if( shade > 10 ) shade = 10; if( shade < -10 ) shade = -10; - int altShade = altitudeShadingFactor * (height[idx] - shadingReferenceAltitude) / 255; - if( altShade < minAltitudeShading ) altShade = minAltitudeShading; - if( altShade > maxAltitudeShading ) altShade = maxAltitudeShading; + int altShade = settings.altitudeShadingFactor * (height[idx] - settings.shadingReferenceAltitude) / 255; + if (altShade < settings.minAltitudeShading) + altShade = settings.minAltitudeShading; + if (altShade > settings.maxAltitudeShading) + altShade = settings.maxAltitudeShading; shade += altShade; @@ -309,15 +272,18 @@ protected void preRender( RegionFile rf, int[] colors, short[] heights ) { for( int s=0; s= maxHeight ) continue; - if( absY+16 <= minHeight ) continue; + if (absY >= settings.maxHeight) + continue; + if (absY + 16 <= settings.minHeight) + continue; if( usedSections[s] ) { short[] blockIds = sectionBlockIds[s]; byte[] blockData = sectionBlockData[s]; for( int idx=z*16+x, y=0; y<16; ++y, idx+=256, ++absY ) { - if( absY < minHeight || absY >= maxHeight ) continue; + if (absY < settings.minHeight || absY >= settings.maxHeight) + continue; final short blockId = blockIds[idx]; final byte blockDatum = blockData[idx]; @@ -328,7 +294,7 @@ protected void preRender( RegionFile rf, int[] colors, short[] heights ) { } } } else { - if( minHeight <= absY && maxHeight >= absY+16 ) { + if (settings.minHeight <= absY && settings.maxHeight >= absY + 16) { // Optimize the 16-blocks-of-air case: pixelColor = Color.overlay( pixelColor, air16Color ); } else { @@ -412,7 +378,8 @@ public void renderAll( RegionMap rm, File outputDir, boolean force, int threadCo renderThreads.add(renderThread); } - if( debug ) System.err.println("Using "+renderThreads.size()+" render threads"); + if (settings.debug) + System.err.println("Using " + renderThreads.size() + " render threads"); for( RenderThread renderThread : renderThreads ) renderThread.start(); @@ -424,7 +391,8 @@ public void renderAll( RegionMap rm, File outputDir, boolean force, int threadCo public void renderRegion( Region r, File outputDir, boolean force ) throws IOException { if( r == null ) return; - if( debug ) System.err.print("Region "+pad(r.rx, 4)+", "+pad(r.rz, 4)+"..."); + if (settings.debug) + System.err.print("Region " + pad(r.rx, 4) + ", " + pad(r.rz, 4) + "..."); String imageFilename = "tile."+r.rx+"."+r.rz+".png"; File fullSizeImageFile = r.imageFile = new File( outputDir, imageFilename ); @@ -433,11 +401,12 @@ public void renderRegion( Region r, File outputDir, boolean force ) throws IOExc if( force || !fullSizeImageFile.exists() || fullSizeImageFile.lastModified() < r.regionFile.lastModified() ) { fullSizeNeedsReRender = true; } else { - if( debug ) System.err.println("image already up-to-date"); + if (settings.debug) + System.err.println("image already up-to-date"); } boolean anyScalesNeedReRender = false; - for( int scale : mapScales ) { + for (int scale : settings.mapScales) { if( scale == 1 ) continue; File f = new File( outputDir, "tile."+r.rx+"."+r.rz+".1-"+scale+".png" ); if( force || !f.exists() || f.lastModified() < r.regionFile.lastModified() ) { @@ -448,7 +417,8 @@ public void renderRegion( Region r, File outputDir, boolean force ) throws IOExc BufferedImage fullSize; if( fullSizeNeedsReRender ) { fullSizeImageFile.delete(); - if( debug ) System.err.println("generating "+imageFilename+"..."); + if (settings.debug) + System.err.println("generating " + imageFilename + "..."); RegionFile rf = new RegionFile( r.regionFile ); try { @@ -472,10 +442,11 @@ public void renderRegion( Region r, File outputDir, boolean force ) throws IOExc return; } - for( int scale : mapScales ) { + for (int scale : settings.mapScales) { if( scale == 1 ) continue; // Already wrote! File f = new File( outputDir, "tile."+r.rx+"."+r.rz+".1-"+scale+".png" ); - if( debug ) System.err.println("generating "+f+"..."); + if (settings.debug) + System.err.println("generating " + f + "..."); int size = 512 / scale; BufferedImage rescaled = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); Graphics2D g = rescaled.createGraphics(); @@ -492,8 +463,9 @@ public void renderRegion( Region r, File outputDir, boolean force ) throws IOExc * within the given bounds (inclusive) */ public void createTileHtml( int minX, int minZ, int maxX, int maxZ, File outputDir ) { - if( debug ) System.err.println("Writing HTML tiles..."); - for( int scale : mapScales ) { + if (settings.debug) + System.err.println("Writing HTML tiles..."); + for (int scale : settings.mapScales) { int regionSize = 512 / scale; try { @@ -522,7 +494,7 @@ public void createTileHtml( int minX, int minZ, int maxX, int maxZ, File outputD ))); try { w.write("\n"); - w.write(""+mapTitle+" - 1:"+scale+"\n"); + w.write("" + settings.mapTitle + " - 1:" + scale + "\n"); w.write("\n"); w.write("\n"); w.write("
"); @@ -554,11 +526,11 @@ public void createTileHtml( int minX, int minZ, int maxX, int maxZ, File outputD } w.write("
\n"); - if( mapScales.length > 1 ) { + if (settings.mapScales.length > 1) { w.write("
"); w.write("

Scales:

"); w.write("
    "); - for( int otherScale : mapScales ) { + for (int otherScale : settings.mapScales) { if( otherScale == scale ) { w.write("
  • 1:"+scale+"
  • "); } else { @@ -583,15 +555,17 @@ public void createTileHtml( int minX, int minZ, int maxX, int maxZ, File outputD } public void createImageTree( RegionMap rm ) { - if( debug ) System.err.println("Composing image tree..."); + if (settings.debug) + System.err.println("Composing image tree..."); ImageTreeComposer itc = new ImageTreeComposer(new ContentStore()); System.out.println( itc.compose( rm ) ); } public void createBigImage( RegionMap rm, File outputDir) { - if( debug ) System.err.println( "Creating big image..." ); + if (settings.debug) + System.err.println("Creating big image..."); BigImageMerger bic = new BigImageMerger(); - bic.createBigImage( rm, outputDir, debug ); + bic.createBigImage(rm, outputDir, settings.debug); } public static final String USAGE = @@ -762,18 +736,12 @@ public int run() throws IOException, InterruptedException { return 0; } - final BlockMap colorMap = colorMapFile == null ? BlockMap.loadDefault() : - BlockMap.load(colorMapFile); - - final BiomeMap biomeMap = biomeMapFile == null ? BiomeMap.loadDefault() : - BiomeMap.load( biomeMapFile ); - RegionMap rm = RegionMap.load(regionFiles, regionLimitRect); - RegionRenderer rr = new RegionRenderer( - colorMap, biomeMap, debug, minHeight, maxHeight, + RegionRenderer rr = new RegionRenderer(new RenderSettings( + colorMapFile, biomeMapFile, debug, minHeight, maxHeight, shadingReferenceAltitude, minAltitudeShading, maxAltitudeShading, altitudeShadingFactor, mapTitle, mapScales - ); + )); rr.renderAll(rm, outputDir, forceReRender, threadCount); diff --git a/src/main/java/togos/minecraft/maprend/RenderSettings.java b/src/main/java/togos/minecraft/maprend/RenderSettings.java new file mode 100644 index 0000000..d791481 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/RenderSettings.java @@ -0,0 +1,48 @@ +package togos.minecraft.maprend; + +import java.io.File; + +public class RenderSettings { + + public boolean debug = false; + public File colorMapFile = null; + public File biomeMapFile = null; + public int minHeight = Integer.MIN_VALUE; + public int maxHeight = Integer.MAX_VALUE; + + /** + * Above = brighter, below = darker TODO affect grass color in a more native way + */ + public int shadingReferenceAltitude = 64; + /** Maximum brightness difference through shading */ + public int altitudeShadingFactor = 50; + + public int minAltitudeShading = -20; + public int maxAltitudeShading = +20; + + public String mapTitle = "Regions"; + public int[] mapScales = { 1 }; + + public RenderSettings() { + } + + public RenderSettings( + File colorMapFile, File biomeMapFile, boolean debug, int minHeight, int maxHeight, + int shadingRefAlt, int minAltShading, int maxAltShading, int altShadingFactor, + String mapTitle, int[] mapScales) { + + this.colorMapFile = colorMapFile; + this.biomeMapFile = biomeMapFile; + this.debug = debug; + + this.minHeight = minHeight; + this.maxHeight = maxHeight; + this.shadingReferenceAltitude = shadingRefAlt; + this.minAltitudeShading = minAltShading; + this.maxAltitudeShading = maxAltShading; + this.altitudeShadingFactor = altShadingFactor; + + this.mapTitle = mapTitle; + this.mapScales = mapScales; + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/io/RegionFile.java b/src/main/java/togos/minecraft/maprend/io/RegionFile.java index 88a16d2..fd6441d 100644 --- a/src/main/java/togos/minecraft/maprend/io/RegionFile.java +++ b/src/main/java/togos/minecraft/maprend/io/RegionFile.java @@ -65,9 +65,11 @@ counts. The chunk offset for a chunk (x, z) begins at byte 4*(x+z*32) in the import java.io.*; import java.util.ArrayList; -import java.util.zip.*; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; -public class RegionFile +public class RegionFile implements Closeable { public static final int VERSION_GZIP = 1; public static final int VERSION_DEFLATE = 2; @@ -278,7 +280,8 @@ public ChunkBuffer(int x, int z, int format) { this.format = format; } - public void close() { + @Override + public void close() { RegionFile.this.write(x, z, buf, count, format); } } @@ -393,7 +396,8 @@ private void setTimestamp(int x, int z, int value) throws IOException { file.writeInt(value); } - public void close() throws IOException { + @Override + public void close() throws IOException { file.close(); } } \ No newline at end of file From d4e51234c705803fb2b37fa9f40a634584e9fa99 Mon Sep 17 00:00:00 2001 From: piegamesde Date: Mon, 28 Aug 2017 18:53:00 +0200 Subject: [PATCH 2/7] The GUI Here it is. Main class is GuiMain. It has been tested quit well in the old pull request and a little after cleaning up the code, so it should work. --- .../togos/minecraft/maprend/DotMinecraft.java | 27 ++ .../minecraft/maprend/gui/RenderedImage.java | 49 +++ .../minecraft/maprend/gui/RenderedMap.java | 383 ++++++++++++++++++ .../minecraft/maprend/gui/RenderedRegion.java | 195 +++++++++ .../maprend/gui/WorldRendererFX.java | 314 ++++++++++++++ .../minecraft/maprend/gui/package-info.java | 5 + .../maprend/guistandalone/GuiController.java | 198 +++++++++ .../maprend/guistandalone/GuiMain.java | 41 ++ .../maprend/guistandalone/scene.fxml | 141 +++++++ 9 files changed, 1353 insertions(+) create mode 100644 src/main/java/togos/minecraft/maprend/DotMinecraft.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/RenderedImage.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/RenderedMap.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/WorldRendererFX.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/package-info.java create mode 100644 src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java create mode 100644 src/main/java/togos/minecraft/maprend/guistandalone/GuiMain.java create mode 100644 src/main/resources/togos/minecraft/maprend/guistandalone/scene.fxml diff --git a/src/main/java/togos/minecraft/maprend/DotMinecraft.java b/src/main/java/togos/minecraft/maprend/DotMinecraft.java new file mode 100644 index 0000000..a698e99 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/DotMinecraft.java @@ -0,0 +1,27 @@ +package togos.minecraft.maprend; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class DotMinecraft { + + private DotMinecraft() { + } + + public static final Path DOTMINECRAFT; + + static { + Path mc = null; + String OS = System.getProperty("os.name").toUpperCase(); + if (OS.contains("WIN")) { + mc = Paths.get(System.getenv("APPDATA")).resolve(".minecraft"); + } else if (OS.contains("MAC")) { + mc = Paths.get(System.getProperty("user.home") + "/Library/Application Support").resolve("minecraft"); + } else if (OS.contains("NUX")) { + mc = Paths.get("~/.minecraft"); + } else { + mc = Paths.get(System.getProperty("user.dir")); + } + DOTMINECRAFT = mc; + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedImage.java b/src/main/java/togos/minecraft/maprend/gui/RenderedImage.java new file mode 100644 index 0000000..6521239 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/RenderedImage.java @@ -0,0 +1,49 @@ +package togos.minecraft.maprend.gui; + +import java.util.Objects; +import org.joml.Vector3ic; +import org.mapdb.HTreeMap; +import javafx.scene.image.WritableImage; + +public class RenderedImage { + + // private WritableImage image; + private final RenderedMap map; + private final Vector3ic key; + private final HTreeMap cache; + private volatile boolean hasValue = false; + + public RenderedImage(RenderedMap map, HTreeMap cache, Vector3ic key) { + this.map = Objects.requireNonNull(map); + this.cache = Objects.requireNonNull(cache); + this.key = Objects.requireNonNull(key); + // System.out.println(cache + " " + key); + } + + public void setImage(WritableImage image) { + // this.image = image; + // System.out.println(cache + " " + key + " " + image); + if (image == null) + cache.remove(key); + else + cache.put(key, image); + hasValue = image != null; + } + + /** Returns if the last {@link #setImage(WritableImage)} call was with a non-null value. */ + public boolean isImageSet() { + return hasValue; + } + + /** + * Returns true if the image is loaded in RAM, causing {@link #getImage(boolean)} to return immediately. Returns false if the image got written to disk to + * save memory. + */ + public boolean isImageLoaded() { + return map.isImageLoaded(key);// cache.containsKey(key); + } + + public WritableImage getImage(boolean force) { + return (force || isImageLoaded()) ? cache.get(key) : null; + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java b/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java new file mode 100644 index 0000000..739563a --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java @@ -0,0 +1,383 @@ +package togos.minecraft.maprend.gui; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.joml.*; +import org.mapdb.*; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.PixelReader; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.paint.Color; +import togos.minecraft.maprend.RegionMap.Region; + +public class RenderedMap { + + /** https://github.com/jankotek/mapdb/issues/839 */ + protected Set unloaded = ConcurrentHashMap.newKeySet(); + public final Serializer VECTOR_SERIALIZER = new Serializer() { + + @SuppressWarnings("unchecked") + @Override + public void serialize(DataOutput2 out, Vector3ic value) throws IOException { + unloaded.add(value); + Serializer.JAVA.serialize(out, value); + } + + @Override + public Vector3ic deserialize(DataInput2 input, int available) throws IOException { + Vector3ic value = (Vector3ic) Serializer.JAVA.deserialize(input, available); + unloaded.remove(value); + return value; + } + + }; + + /** + * Thank the JavaFX guys who a) Made WriteableImage not Serializable b) Didn't include any serialization except by converting to BufferedImage for this ugly + * mess. + */ + public final Serializer IMAGE_SERIALIZER = new Serializer() { + + @Override + public boolean isTrusted() { + return true; + } + + @Override + public void serialize(DataOutput2 out, WritableImage value) throws IOException { + byte[] data = new byte[512 * 512 * 4]; + value.getPixelReader().getPixels(0, 0, 512, 512, + PixelFormat.getByteBgraInstance(), data, 0, 512 * 4); + out.write(data); + } + + @Override + public WritableImage deserialize(DataInput2 input, int available) throws IOException { + byte[] data = new byte[available]; + input.readFully(data); + WritableImage ret = new WritableImage(512, 512); + ret.getPixelWriter().setPixels(0, 0, 512, 512, + PixelFormat.getByteBgraInstance(), data, 0, 512 * 4); + return ret; + } + }; + + // Disk for overflow + private static final DB cacheDBDisk = DBMaker.tempFileDB().fileDeleteAfterClose().closeOnJvmShutdown().make(); + // Fast memory cache + private static final DB cacheDBMem = DBMaker.heapDB().closeOnJvmShutdown().make(); + + private final HTreeMap cacheMapDisk, cacheMapDiskMem, cacheMapMem; + + private Map plainRegions = new HashMap<>(); + private Map> regions = new HashMap<>(); + + @SuppressWarnings("unchecked") + public RenderedMap(ScheduledExecutorService executor) { + cacheMapDisk = cacheDBDisk.hashMap("OnDisk" + System.identityHashCode(this), VECTOR_SERIALIZER, IMAGE_SERIALIZER).create(); + cacheMapDisk.clear(); + cacheMapDiskMem = cacheDBMem + .hashMap("RenderedRegionCache" + System.identityHashCode(this), Serializer.JAVA, Serializer.JAVA) + // .expireStoreSize(1) + .expireMaxSize(1024) + .expireAfterCreate() + .expireAfterUpdate() + // .expireAfterGet() + .expireAfterCreate(30, TimeUnit.SECONDS) + .expireAfterUpdate(30, TimeUnit.SECONDS) + .expireAfterGet(60, TimeUnit.SECONDS) + .expireOverflow(cacheMapDisk) + .expireExecutor(executor) + .expireExecutorPeriod(10000) + .create(); + // cacheMapMem = cacheMapDiskMem; + cacheMapMem = cacheDBMem.hashMap("ScaledRegionCache" + System.identityHashCode(this), Serializer.JAVA, Serializer.JAVA) + // .expireStoreSize(1) + .expireMaxSize(512) + .expireAfterCreate() + .expireAfterUpdate() + // .expireAfterGet() + .expireAfterCreate(30, TimeUnit.SECONDS) + .expireAfterUpdate(30, TimeUnit.SECONDS) + .expireAfterGet(60, TimeUnit.SECONDS) + // .expireOverflow(cacheMapDisk) + .expireExecutor(executor) + .expireExecutorPeriod(10000) + .create(); + cacheMapDisk.checkThreadSafe(); + cacheMapDiskMem.checkThreadSafe(); + cacheMapMem.checkThreadSafe(); + clearReload(Collections.emptyList()); + } + + public RenderedMap(ScheduledExecutorService executor, Collection positions) { + this(executor); + clearReload(positions); + } + + public void close() { + clearReload(Collections.emptyList()); + cacheMapDiskMem.close(); + cacheMapMem.close(); + cacheMapDisk.close(); + cacheDBMem.close(); + cacheDBDisk.close(); + } + + public void evictCache() { + cacheMapDiskMem.expireEvict(); + cacheMapMem.expireEvict(); + cacheMapDisk.expireEvict(); + } + + public void clearReload(Collection positions) { + cacheMapDisk.clear(); + cacheMapDiskMem.clear(); + cacheMapMem.clear(); + regions.clear(); + plainRegions.clear(); + regions.put(0, plainRegions); + positions.stream().map(r -> new RenderedRegion(this, r)).forEach(r -> plainRegions.put(r.position, r)); + } + + public void invalidateAll() { + // System.out.println("Invalidate all"); + plainRegions.values().forEach(r -> r.invalidateTree(true)); + } + + public boolean isNothingLoaded() { + return get(0).isEmpty(); + } + + public void draw(GraphicsContext gc, int level, AABBd frustum, double scale) { + Map map = get(level > 0 ? 0 : level); + gc.setFill(new Color(0.3f, 0.3f, 0.9f, 1.0f)); // Background color + plainRegions.values() + .stream() + .filter(r -> r.isVisible(frustum)) + .forEach(r -> r.drawBackground(gc, scale)); + try { + map + .entrySet() + .stream() + .filter(e -> RenderedRegion.isVisible(e.getKey(), level > 0 ? 0 : level, frustum)) + .map(e -> { + RenderedRegion r = e.getValue(); + if (e.getValue() == null) + r = get(level, e.getKey(), true); + return r; + }) + .forEach(r -> r.draw(gc, level, frustum, scale)); + } catch (NullPointerException e) { + // This seems to be a pretty rare exception that might be linked to some race hazard when daw() is called during the reloading of the map. + // Nonetheless this should not happen because every time it happens on level 0 and at no point in time a level 0 region has a key associated with a + // non-null value. + System.out.println(level); + System.out.println(plainRegions.entrySet()); + System.out.println(regions.entrySet()); + throw e; + } + plainRegions.values() + .stream() + .filter(r -> r.isVisible(frustum)) + .forEach(r -> r.drawForeground(gc, frustum, scale)); + } + + public boolean updateImage(int level, AABBd frustum) { + try { + Thread current = Thread.currentThread(); + if (regions.isEmpty()) + // Race hazard: updateImage() is called while clearReload() is reloading all the chunks + return false; + return get(level) + .entrySet() + .stream() + .map(e -> e.getValue()) + .filter(r -> r != null) + // .filter(e -> e.getValue().needsUpdate()) + .filter(r -> r.isVisible(frustum)) + // .sorted(comp) + .filter(r -> current.isInterrupted() ? false : r.updateImage()) + .limit(10) + .collect(Collectors.toList()) + .size() > 0; + } catch (ConcurrentModificationException e) { + // System.out.println(e); + return true; + } + } + + public Map get(int level) { + // if (new Error().getStackTrace().length > 180) + // Sometimes, this throws a StackOverflowError, but Eclipse doesn't show when this method got called before the recursion in the stack trace. This + // is to catch the error prematurely in the hope of getting a full stack trace + // TODO this is really bad for performance, remove ASAP the bug is found and fixed + // throw new InternalError("Stack overflow."); + Map ret = regions.get(level); + try { + // the bug might be when requesting values on level 0 that are null (not calculated) + if (ret == null && level != 0) { + Map ret2 = new HashMap(); + ret = ret2; + // the bug might be to using abovePos() regardless of the level being zoomed in or out + get(level < 0 ? level + 1 : level - 1).keySet().stream().map(RenderedMap::abovePos).distinct().forEach(v -> ret2.put(v, null)); + + regions.put(level, ret); + } + } catch (StackOverflowError e) { + System.out.println(level + " " + ret); + throw e; + } + return ret; + } + + public void putImage(Vector2ic pos, WritableImage image) { + if (!plainRegions.containsKey(pos)) + throw new IllegalArgumentException("Position out of bounds"); + plainRegions.get(pos).setImage(image); + } + + public RenderedImage createImage(RenderedRegion r) { + return new RenderedImage(this, r.level <= 0 ? cacheMapDiskMem : cacheMapMem, new Vector3i(r.position.x(), r.position.y(), r.level).toImmutable()); + } + + protected boolean isImageLoaded(Vector3ic key) { + return !unloaded.contains(key); + } + + public RenderedRegion get(int level, Vector2ic position, boolean create) { + Map map = get(level); + RenderedRegion r = map.get(position); + if (create && r == null && level != 0 && ((level > 0 /* && plainRegions.containsKey(new Vector2i(position.x() >> level, position.y() >> + * level).toImmutable()) */) || map.containsKey(position))) { + r = new RenderedRegion(this, level, position); + if (level < 0) + Arrays.stream(belowPos(position)).forEach(pos -> get(level + 1, position, true)); + if (level > 0) + get(level - 1, abovePos(position), true); + map.put(position, r); + } + return r; + } + + public RenderedRegion[] get(int level, Vector2ic[] belowPos, boolean create) { + RenderedRegion[] ret = new RenderedRegion[belowPos.length]; + for (int i = 0; i < belowPos.length; i++) + ret[i] = get(level, belowPos[i], create); + return ret; + } + + public static Vector2i abovePos(Vector2ic pos) { + return groundPos(pos, 1); + } + + public static Vector2i groundPos(Vector2ic pos, int levelDiff) { + return new Vector2i(pos.x() >> levelDiff, pos.y() >> levelDiff); + } + + public static Vector2i[] belowPos(Vector2ic pos) { + Vector2i belowPos = new Vector2i(pos.x() << 1, pos.y() << 1); + return new Vector2i[] { + new Vector2i(0, 0).add(belowPos), + new Vector2i(1, 0).add(belowPos), + new Vector2i(0, 1).add(belowPos), + new Vector2i(1, 1).add(belowPos) + }; + } + + public static WritableImage halfSize(WritableImage old, WritableImage... corners) { + return halfSize(corners[0], corners[1], corners[2], corners[3]); + } + + public static WritableImage halfSize(WritableImage old, WritableImage topLeft, WritableImage topRight, WritableImage bottomLeft, WritableImage bottomRight) { + WritableImage output = old != null ? old : new WritableImage(512, 512); + + PixelReader topLeftReader = topLeft != null ? topLeft.getPixelReader() : null; + PixelReader topRightReader = topRight != null ? topRight.getPixelReader() : null; + PixelReader bottomLeftReader = bottomLeft != null ? bottomLeft.getPixelReader() : null; + PixelReader bottomRightReader = bottomRight != null ? bottomRight.getPixelReader() : null; + + int[] topLeftPixels = genBuffer(512 * 512); + int[] topRightPixels = genBuffer(512 * 512); + int[] bottomLeftPixels = genBuffer(512 * 512); + int[] bottomRightPixels = genBuffer(512 * 512); + + if (topLeftReader != null) + topLeftReader.getPixels(0, 0, 512, 512, PixelFormat.getIntArgbInstance(), topLeftPixels, 0, 512); + if (topRightReader != null) + topRightReader.getPixels(0, 0, 512, 512, PixelFormat.getIntArgbInstance(), topRightPixels, 0, 512); + if (bottomLeftReader != null) + bottomLeftReader.getPixels(0, 0, 512, 512, PixelFormat.getIntArgbInstance(), bottomLeftPixels, 0, 512); + if (bottomRightReader != null) + bottomRightReader.getPixels(0, 0, 512, 512, PixelFormat.getIntArgbInstance(), bottomRightPixels, 0, 512); + + PixelWriter writer = output.getPixelWriter(); + + // TODO optimize with buffers + for (int y = 0; y < 256; y++) { + for (int x = 0; x < 256; x++) { + int rx = x * 2; + int ry = y * 2; + writer.setArgb(x, y, topLeftReader != null ? sampleColor(rx, ry, topLeftPixels) : 0); + writer.setArgb(x + 256, y, topRightReader != null ? sampleColor(rx, ry, topRightPixels) : 0); + writer.setArgb(x, y + 256, bottomLeftReader != null ? sampleColor(rx, ry, bottomLeftPixels) : 0); + writer.setArgb(x + 256, y + 256, bottomRightReader != null ? sampleColor(rx, ry, bottomRightPixels) : 0); + } + } + return output; + } + + private static int sampleColor(int x, int y, int[] colors) { + // Image image = new Image(is, requestedWidth, requestedHeight, preserveRatio, smooth) + // int[] colors = genBuffer(4); + // reader.getPixels(x, y, 2, 2, WritablePixelFormat.getIntArgbInstance(), colors, 0, 2); + + int c1 = colors[y * 512 + x], c2 = colors[y * 512 + x + 1], c3 = colors[y * 512 + x + 512], c4 = colors[y * 512 + x + 513]; + // TODO premultiply alpha to avoid dark edges + long ret = 0;// use long against overflow + long a1 = c1 >>> 24, a2 = c2 >>> 24, a3 = c3 >>> 24, a4 = c4 >>> 24; + // alpha + ret |= ((a1 + a2 + a3 + a4) << 22) & 0xFF000000; + // red + ret |= ((((c1 & 0x00FF0000) * a1 + (c2 & 0x00FF0000) * a2 + (c3 & 0x00FF0000) * a3 + (c4 & 0x00FF0000) * a4) / 255) >> 2) & 0x00FF0000; + // green + ret |= ((((c1 & 0x0000FF00) * a1 + (c2 & 0x0000FF00) * a2 + (c3 & 0x0000FF00) * a3 + (c4 & 0x0000FF00) * a4) / 255) >> 2) & 0x0000FF00; + // blue + ret |= ((((c1 & 0x000000FF) * a1 + (c2 & 0x000000FF) * a2 + (c3 & 0x000000FF) * a3 + (c4 & 0x000000FF) * a4) / 255) >> 2) & 0x000000FF; + return (int) ret; + } + + public static WritableImage doubleSize(WritableImage old, WritableImage input, int levelDiff, Vector2i subTile) { + WritableImage output = old != null ? old : new WritableImage(512, 512); + + PixelReader reader = input.getPixelReader(); + PixelWriter writer = output.getPixelWriter(); + + int tileSize = 512 >> levelDiff; + int scaleFactor = 1 << levelDiff; + + int[] pixels = genBuffer(tileSize * tileSize); + int[] pixel = genBuffer(scaleFactor * scaleFactor); + reader.getPixels(subTile.x * tileSize, subTile.y * tileSize, tileSize, tileSize, PixelFormat.getIntArgbInstance(), pixels, 0, tileSize); + + for (int y = 0; y < tileSize; y++) { + for (int x = 0; x < tileSize; x++) { + final int argb = pixels[y * tileSize + x]; + Arrays.fill(pixel, argb); + writer.setPixels(x * scaleFactor, y * scaleFactor, scaleFactor, scaleFactor, PixelFormat.getIntArgbInstance(), pixel, 0, scaleFactor); + } + } + return output; + } + + /** Test how much time this takes to see if object pooling is needed. */ + private static int[] genBuffer(int length) { + return new int[length]; + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java b/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java new file mode 100644 index 0000000..1bb1998 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java @@ -0,0 +1,195 @@ +package togos.minecraft.maprend.gui; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import org.joml.AABBd; +import org.joml.Vector2i; +import org.joml.Vector2ic; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.WritableImage; +import javafx.scene.paint.Color; +import togos.minecraft.maprend.RegionMap.Region; + +public class RenderedRegion { + + public enum RenderingState { + VALID, // Don't touch + INVALID, // Recalculate please + DRAWING, // Recalculating + REDRAW; // Aborted while recalculating, re-recalculate + // ABORT; // World changed, abort calculation + + public boolean isInvalid() { + return this != VALID; + } + } + + protected final RenderedMap map; + // public final AtomicBoolean invalid = new AtomicBoolean(true); + protected final RenderedImage image; + public final int level; + public final Vector2ic position; + public final AtomicReference valid = new AtomicReference(RenderingState.INVALID); + + public Region region; + + public RenderedRegion(RenderedMap map, Region region) { + this(map, 0, new Vector2i(region.rx, region.rz).toImmutable()); + this.region = region; + } + + public RenderedRegion(RenderedMap map, int level, Vector2ic position) { + this.map = map; + this.level = level; + this.position = position; + this.image = map.createImage(this); + // setImage(null); + } + + public void setImage(WritableImage image) { + Objects.requireNonNull(image); + invalidateTree(true); + this.image.setImage(image); + valid.set(RenderingState.VALID); + } + + public void invalidateTree(boolean keepImage) { + if (!valid.compareAndSet(RenderingState.DRAWING, RenderingState.REDRAW)) + valid.set(RenderingState.INVALID); + if (level >= 0) + Arrays.stream(getBelow(false)).filter(__ -> __ != null).forEach(r -> r.invalidateTree(keepImage)); + if (level <= 0) { + RenderedRegion above = getAbove(false); + if (above != null) + above.invalidateTree(keepImage); + } + if (!keepImage) + this.image.setImage(null); + } + + public RenderedRegion[] getBelow(boolean create) { + return map.get(level + 1, RenderedMap.belowPos(position), create); + } + + public RenderedRegion getAbove(boolean create) { + return map.get(level - 1, RenderedMap.abovePos(position), create); + } + + public RenderedRegion getGround(boolean create) { + return map.get(0, RenderedMap.groundPos(position, level), create); + } + + public boolean updateImage() { + boolean changed = false; + + if (!this.image.isImageLoaded()) + changed = true; + // This will load an image back from cache if needed + WritableImage image = this.image.getImage(true); + + if (level != 0 && (image == null || valid.get().isInvalid())) { + if (level > 0) { + + // check above + // get above image + RenderedRegion above = getGround(true); + WritableImage aboveImage = above.getImage(true); + // upscale image + if (aboveImage != null) + image = RenderedMap.doubleSize(image, aboveImage, level, new Vector2i(position.x() & ((1 << level) - 1), position.y() & ((1 << level) - 1))); + } else if (level < 0) { + // check below + RenderedRegion[] below = getBelow(true); + for (RenderedRegion r : below) + if (r != null) + changed |= r.updateImage(); + // get below images + WritableImage topLeft = below[0] == null ? null : below[0].getImage(true); + WritableImage topRight = below[1] == null ? null : below[1].getImage(true); + WritableImage bottomLeft = below[2] == null ? null : below[2].getImage(true); + WritableImage bottomRight = below[3] == null ? null : below[3].getImage(true); + // downscale images + image = RenderedMap.halfSize(image, topLeft, topRight, bottomLeft, bottomRight); + } + this.image.setImage(image); + valid.set(RenderingState.VALID); + if (image != null) + changed = true; + } + + return changed; + } + + public WritableImage getImage(boolean force) { + return image.getImage(force); + } + + public boolean isVisible(AABBd frustum) { + return isVisible(position, level, frustum); + } + + public static boolean isVisible(Vector2ic position, int level, AABBd frustum) { + int size = WorldRendererFX.pow2(512, -level); + return frustum.testAABB(new AABBd(position.x() * size, position.y() * size, 0, (position.x() + 1) * size, (position.y() + 1) * size, 0)); + } + + public void draw(GraphicsContext gc, int drawingLevel, AABBd frustum, double scale) { + // bounds must have been checked here + + int size = WorldRendererFX.pow2(512, -this.level); + final int overDraw = 3; // TODO make setting + + WritableImage image = this.image.getImage(false); + + if (image != null) { + if (drawingLevel <= this.level && this.level > 0) + // Fill background to prevent bleeding from lower levels of detail + gc.fillRect(position.x() * size + 1 / scale, position.y() * size + 1 / scale, size - 2 / scale, size - 2 / scale); + // Draw that image + gc.drawImage(image, position.x() * size, position.y() * size, size, size); + } + + // Draw below if needed (check bounds) + if (drawingLevel > this.level || ((this.level < 0 || drawingLevel > (this.level - overDraw)) && image == null)) { + Arrays.stream(RenderedMap.belowPos(position)) + .filter(v -> isVisible(v, this.level + 1, frustum)) + .map(v -> map.get(this.level + 1, v, true)) + .filter(r -> r != null) + .forEach(r -> r.draw(gc, drawingLevel, frustum, scale)); + } + } + + /** This method assumes the appropriate fill is already set */ + public void drawBackground(GraphicsContext gc, double scale) { + int size = 512;// WorldRendererFX.pow2(512, -this.level); + // gc.translate(region.coordinates.x * 512, region.coordinates.y * 512); + gc.fillRect(position.x() * size - 1 / scale, position.y() * size - 1 / scale, size + 2 / scale, size + 2 / scale); + } + + public void drawForeground(GraphicsContext gc, AABBd frustum, double scale) { + int size = 512;// WorldRendererFX.pow2(512, -this.level); + if (valid.get().isInvalid() && image.isImageSet()) {// reduce brightness + gc.setFill(new Color(0f, 0f, 0f, 0.5f)); + gc.fillRect(position.x() * size, position.y() * size, size, size); + } + if (valid.get() == RenderingState.DRAWING || valid.get() == RenderingState.REDRAW) { + gc.setFill(new Color(0.9f, 0.9f, 0.15f, 1.0f)); + + double x = position.x() * 512, y = position.y() * 512, w = 512, h = 512, m = Math.min(6 / scale, 35); + + double xw = Math.min(frustum.maxX, x + w); + double yh = Math.min(frustum.maxY, y + h); + x = Math.max(frustum.minX, x); + y = Math.max(frustum.minY, y); + w = xw - x; + h = yh - y; + + // gc.translate(x, y); + gc.fillRect(x, y, w, m); + gc.fillRect(x, y + h - m, w, m); + gc.fillRect(x, y, m, h); + gc.fillRect(x + w - m, y, m, h); + } + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/WorldRendererFX.java b/src/main/java/togos/minecraft/maprend/gui/WorldRendererFX.java new file mode 100644 index 0000000..f746905 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/WorldRendererFX.java @@ -0,0 +1,314 @@ +package togos.minecraft.maprend.gui; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.joml.AABBd; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3d; +import com.sun.glass.ui.Application; +import com.sun.glass.ui.Robot; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.WritableImage; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.util.Duration; +import togos.minecraft.maprend.BoundingRect; +import togos.minecraft.maprend.RegionMap; +import togos.minecraft.maprend.RegionRenderer; +import togos.minecraft.maprend.gui.RenderedRegion.RenderingState; +import togos.minecraft.maprend.io.RegionFile; + +@SuppressWarnings("restriction") +public class WorldRendererFX extends Canvas implements Runnable { + + public static final int THREAD_COUNT = 4; + public static final int MAX_ZOOM_LEVEL = 7; // up to 9 without rounding errors + + protected RegionRenderer renderer; + protected RenderedMap map; + + protected boolean dragging = false; + protected Vector3d mousePos = new Vector3d(); + protected Vector3d translation = new Vector3d(0, 0, 0); + protected double realScale = 1; + protected DoubleProperty scaleProperty = new SimpleDoubleProperty(); + protected Timeline scaleTimeline; + protected AABBd frustum; + + protected ScheduledThreadPoolExecutor executor; + protected final List> submitted = Collections.synchronizedList(new LinkedList<>()); + + protected GraphicsContext gc = getGraphicsContext2D(); + protected Robot robot = Application.GetApplication().createRobot(); + + public WorldRendererFX(RegionRenderer renderer) { + executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(THREAD_COUNT); + map = new RenderedMap(executor);// Executors.newScheduledThreadPool(THREAD_COUNT / 2)); + executor.scheduleAtFixedRate(() -> { + try { + // TODO execute more often if something changes and less often if not + // update upscaled/downscaled images of chunks + int level = (int) Math.ceil(scaleProperty.get()); + if (map.updateImage(level, frustum)) + repaint(); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + }, 1000, 1000, TimeUnit.MILLISECONDS); + executor.scheduleWithFixedDelay(map::evictCache, 10, 10, TimeUnit.SECONDS); + + executor.setKeepAliveTime(20, TimeUnit.SECONDS); + executor.allowCoreThreadTimeOut(true); + + this.renderer = Objects.requireNonNull(renderer); + + {// Listeners + setOnMouseMoved(e -> mouseMove(e.getX(), e.getY())); + setOnMouseDragged(e -> mouseMove(e.getX(), e.getY())); + setOnMousePressed(e -> { + if (e.getButton().ordinal() == 3) + buttonPress(true); + }); + setOnMouseReleased(e -> { + if (e.getButton().ordinal() == 3) + buttonPress(false); + }); + setOnScroll(e -> mouseScroll(e.getTextDeltaY())); + InvalidationListener l = e -> { + frustumChanged(); + repaint(); + }; + widthProperty().addListener(l); + heightProperty().addListener(l); + scaleProperty.addListener(e -> onScrollChanged()); + } + + // loadWorld(file); + invalidateTextures(); + frustumChanged(); + repaint(); + } + + public void loadWorld(File file) { + // regions.clear(); + // regions = RegionMap.load(file, BoundingRect.INFINITE).regions.stream().collect(Collectors.toMap(r -> new Vector2i(r.rx, r.rz), r -> new + // RenderedRegion2(r))); + // map.clear(); + map.clearReload(RegionMap.load(file, BoundingRect.INFINITE).regions); + invalidateTextures(); + } + + public void invalidateTextures() { + map.invalidateAll(); + for (int i = 0; i < THREAD_COUNT; i++) + executor.submit(this); + } + + public void shutDown() { + executor.shutdownNow(); + try { + executor.awaitTermination(1, TimeUnit.DAYS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + map.close(); + } + + /** Queues in a repaint event calling {@link renderWorld} from the JavaFX Application Thread */ + public void repaint() { + Platform.runLater(this::renderWorld); + } + + /** Requires the projection to be set up to {@code GL11.glOrtho(0, width, height, 0, -1, 1);} and to be called from the JavaFX Application Thread. */ + public void renderWorld() { + gc = getGraphicsContext2D(); + // gc.setStroke(Color.GREEN.deriveColor(0, 1, 1, .2)); + gc.setLineWidth(10); + // gc.clearRect(0, 0, getWidth(), getHeight()); + gc.setFill(new Color(0.2f, 0.2f, 0.6f, 1.0f)); + gc.fillRect(0, 0, getWidth(), getHeight()); + + gc.save(); + gc.scale(realScale, realScale); + gc.translate(translation.x, translation.y); + + int level = (int) Math.ceil(scaleProperty.get()); + map.draw(gc, level, frustum, realScale); + gc.restore(); + + if (map.isNothingLoaded()) { + gc.setFill(Color.WHITE); + gc.setFont(Font.font(20)); + gc.fillText("No regions loaded", 10, getHeight() - 10); + } + + // gc.strokeRect(100, 100, getWidth() - 200, getHeight() - 200); + // gc.strokeRect(0, 0, getWidth() - 0, getHeight() - 0); + } + + public void mouseMove(double x, double y) { + Vector3d lastCursor = new Vector3d(mousePos); + mousePos.set(x, y, 0); + + if (dragging) { + // Difference in world space + Vector3d dragDist = new Vector3d(lastCursor).sub(mousePos).negate(); + translation.add(new Vector3d(dragDist.x / realScale, dragDist.y / realScale, 0)); + frustumChanged(); + repaint(); + + // TODO limit dragging to prevent the map of going out of bounds + + // Wrap mouse when dragging beyond bounds. TODO fix and add it back + // Bounds screen = localToScreen(getBoundsInLocal()); + // int mx = robot.getMouseX(); + // int my = robot.getMouseY(); + // + // int dx = 0, dy = 0; + // if (mx <= screen.getMinX()) { + // dx = 1; + // robot.mouseMove((int) (screen.getMaxX() - 2), my); + // } + // if (mx >= screen.getMaxX() - 1) { + // dx = -1; + // robot.mouseMove((int) (screen.getMinX() + 1), my); + // } + // if (my <= screen.getMinY()) { + // dy = 1; + // robot.mouseMove(mx, (int) (screen.getMaxY() - 2)); + // } + // if (my >= screen.getMaxY() - 1) { + // dy = -1; + // robot.mouseMove(mx, (int) (screen.getMinY() + 1)); + // } + // + // dragDist = new Vector3d(screen.getMaxX() - screen.getMinX(), screen.getMaxY() - screen.getMinY(), 0).mul(dx, dy, 0).negate(); + // translation.add(new Vector3d(dragDist.x / realScale, dragDist.y / realScale, 0)); + } + } + + public void mouseScroll(double dy) { + double currentValue = scaleProperty.get(); + double missingTime = 0; + if (scaleTimeline != null) { + missingTime = (scaleTimeline.getTotalDuration().toMillis() - scaleTimeline.getCurrentTime().toMillis()) / 2; + scaleTimeline.jumpTo("end"); + } + double scale = scaleProperty.get(); + scaleProperty.set(currentValue); + scale += dy / 10; + if (scale > MAX_ZOOM_LEVEL) + scale = MAX_ZOOM_LEVEL; + if (scale < -MAX_ZOOM_LEVEL) + scale = -MAX_ZOOM_LEVEL; + scaleTimeline = new Timeline(new KeyFrame(Duration.millis(100 + missingTime), new KeyValue(scaleProperty, scale, Interpolator.EASE_BOTH))); + scaleTimeline.play(); + } + + private void onScrollChanged() { + double oldScale = realScale; + realScale = Math.pow(2, scaleProperty.get()); + // In world coordinates + Vector3d cursorPos = new Vector3d(mousePos).div(oldScale).sub(translation); + translation.add(cursorPos); + translation.div(realScale / oldScale); + translation.sub(cursorPos); + frustumChanged(); + repaint(); + } + + public void buttonPress(boolean pressed) { + dragging = pressed; + } + + /* Rebuild view frustum */ + protected void frustumChanged() { + frustum = new AABBd( + new Vector3d(000, 000, 0).div(realScale).sub(translation.x, translation.y, 0), + new Vector3d(getWidth() - 000, getHeight() - 000, 0).div(realScale).sub(translation.x, translation.y, 0)); + // frustum = new AABBd(-Double.MAX_VALUE, -Double.MAX_VALUE, 0, Double.MAX_VALUE, Double.MAX_VALUE, 0); + // System.out.println(frustum); + } + + public boolean isVisible(Vector2f point) { + return true; + } + + public boolean isVisible(Vector2i region) { + return true; + } + + public RegionRenderer getRegionRenderer() { + return renderer; + } + + @Override + public void run() { + RenderedRegion region = null; + region = nextRegion(); + if (region == null) + return; + repaint(); + try { + try (RegionFile rf = new RegionFile(region.region.regionFile)) { + BufferedImage texture2 = null; + do { + texture2 = renderer.render(rf); + } while (region.valid.compareAndSet(RenderingState.REDRAW, RenderingState.DRAWING) && !Thread.interrupted()); + + WritableImage texture = SwingFXUtils.toFXImage(texture2, null); + region.setImage(texture); + repaint(); + } + } catch (Throwable e) { + e.printStackTrace(); + } finally { + region.valid.set(RenderingState.VALID); + executor.submit(this); + } + } + + /** Returns the next Region to render */ + protected synchronized RenderedRegion nextRegion() { + // In region coordinates + Vector3d cursorPos = new Vector3d(mousePos).div(realScale).sub(translation).div(512).sub(.5, .5, 0); + + Comparator comp = (a, b) -> Double.compare(new Vector3d(a.position.x(), a.position.y(), 0).sub(cursorPos).length(), new Vector3d(b.position.x(), b.position.y(), 0).sub(cursorPos).length()); + RenderedRegion min = null; + for (RenderedRegion r : map.get(0).values()) + if (r.valid.get() == RenderingState.INVALID && (min == null || comp.compare(min, r) > 0)) + min = r; + if (min != null) + // min got handled by another Thread already (while we were still searching), so get a new one + if (!min.valid.compareAndSet(RenderingState.INVALID, RenderingState.DRAWING)) + return nextRegion(); + return min; + } + + /** return a*2^n */ + public static int pow2(int a, int n) { + if (n > 0) + return a << n; + else if (n < 0) + return a >> -n; + else + return a; + } +} diff --git a/src/main/java/togos/minecraft/maprend/gui/package-info.java b/src/main/java/togos/minecraft/maprend/gui/package-info.java new file mode 100644 index 0000000..67cd0bc --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/package-info.java @@ -0,0 +1,5 @@ +/** + * @author piegames + * + */ +package togos.minecraft.maprend.gui; \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java b/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java new file mode 100644 index 0000000..b60e5d8 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java @@ -0,0 +1,198 @@ +package togos.minecraft.maprend.guistandalone; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.controlsfx.control.RangeSlider; +import org.controlsfx.dialog.CommandLinksDialog; +import org.controlsfx.dialog.CommandLinksDialog.CommandLinksButtonType; +import javafx.animation.TranslateTransition; +import javafx.beans.binding.Bindings; +import javafx.beans.value.ChangeListener; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.DirectoryChooser; +import javafx.stage.Modality; +import javafx.util.Duration; +import togos.minecraft.maprend.DotMinecraft; +import togos.minecraft.maprend.RegionRenderer; +import togos.minecraft.maprend.RenderSettings; +import togos.minecraft.maprend.gui.WorldRendererFX; + +public class GuiController implements Initializable { + + public WorldRendererFX panel; + + @FXML + private Label minHeight, maxHeight; + + @FXML + private RangeSlider heightSlider; + + @FXML + private StackPane stackPane; + + @FXML + private ToggleButton showButton; + + @FXML + private VBox rightMenu; + + @FXML + private TextField pathField; + + @FXML + private Button browseButton; + + @FXML + BorderPane rendererContainer; + + public GuiController() { + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + minHeight.textProperty().bind(Bindings.format("Min: %.0f", heightSlider.lowValueProperty())); + maxHeight.textProperty().bind(Bindings.format("Max: %.0f", heightSlider.highValueProperty())); + + try { + panel = new WorldRendererFX(new RegionRenderer(new RenderSettings())); + // anchorPane.getChildren().add(panel); + rendererContainer.setCenter(panel); + // anchorPane.getChildren().clear(); + // AnchorPane.setTopAnchor(panel, 0.0); + // AnchorPane.setBottomAnchor(panel, 0.0); + // AnchorPane.setLeftAnchor(panel, 0.0); + // AnchorPane.setRightAnchor(panel, 0.0); + panel.widthProperty().bind(rendererContainer.widthProperty()); + panel.heightProperty().bind(rendererContainer.heightProperty()); + rendererContainer.setPickOnBounds(true); + } catch (IOException e1) { + e1.printStackTrace(); + } + + ChangeListener heightListener = (e, oldVal, newVal) -> { + if (oldVal && !newVal) { + if (e == heightSlider.lowValueChangingProperty()) + panel.getRegionRenderer().settings.minHeight = (int) Math.round(heightSlider.lowValueProperty().getValue().doubleValue()); + else if (e == heightSlider.highValueChangingProperty()) + panel.getRegionRenderer().settings.maxHeight = (int) Math.round(heightSlider.highValueProperty().getValue().doubleValue()); + panel.invalidateTextures(); + panel.repaint(); + } + }; + heightSlider.lowValueChangingProperty().addListener(heightListener); + heightSlider.highValueChangingProperty().addListener(heightListener); + + showButton.setOnAction(e -> { + if (showButton.isSelected()) { + // showButton.setText("Hide settings"); + TranslateTransition transition = new TranslateTransition(Duration.millis(500), rightMenu); + transition.setFromX(rightMenu.getTranslateX()); + transition.setToX(0); + transition.play(); + } else { + // showButton.setText("Show settings"); + TranslateTransition transition = new TranslateTransition(Duration.millis(500), rightMenu); + transition.setFromX(rightMenu.getTranslateX()); + transition.setToX(rightMenu.getWidth() - 15); + transition.play(); + } + }); + } + + public void reloadWorld() { + String world = pathField.getText(); + if (world.isEmpty()) + return; + try { + Path path = Paths.get(world); + if (Files.exists(path) && Files.isDirectory(path)) { + if (Files.exists(path.resolve("level.dat"))) { // Selected world folder + String[] dimensions = new String[] { "region", "DIM-1\\region", "DIM1\\region" }; + String[] dimensionNames = new String[] { "Overworld", "Nether", "End" }; + final Path path2 = path; + List availableDimensions = IntStream.range(0, 3) + .filter(i -> Files.exists(path2.resolve(dimensions[i]))) + .mapToObj(i -> new CommandLinksButtonType(dimensionNames[i], false)) + .collect(Collectors.toList()); + if (!availableDimensions.isEmpty()) { + // TODO change to conventional button dialog, this one is too big for our use case and doesn't look good + CommandLinksDialog dialog = new CommandLinksDialog(availableDimensions); + dialog.setTitle("Select dimension"); + dialog.initModality(Modality.APPLICATION_MODAL); + Optional result = availableDimensions.size() == 1 ? Optional.of(availableDimensions.get(0).getButtonType()) : dialog.showAndWait(); + if (result.isPresent()) { + switch (result.get().getText()) { + case "Overworld": + path = path.resolve(dimensions[0]); + break; + case "Nether": + path = path.resolve(dimensions[1]); + break; + case "End": + path = path.resolve(dimensions[2]); + break; + } + } else { + throw new Error("TODO"); + } + } + pathField.setText(path.toAbsolutePath().toString()); + } + + // Region folder selected from here + if (!hasFilesWithEnding(path, "mca")) + new Alert(AlertType.WARNING, "Your selected folder seems to not contain any useful files." + (hasFilesWithEnding(path, "mcr") + ? " It does contain some region files in the old format though, please open this world in a newer version of Minecraft to automatically convert them." : "")).showAndWait(); + panel.loadWorld(path.toFile()); + } else + new Alert(AlertType.ERROR, "Folder does not exist", ButtonType.OK).showAndWait(); + } catch (InvalidPathException e) { + new Alert(AlertType.ERROR, "Invalid path", ButtonType.OK).showAndWait(); + } + } + + public void browse() { + DirectoryChooser dialog = new DirectoryChooser(); + File f = pathField.getText().isEmpty() ? DotMinecraft.DOTMINECRAFT.resolve("saves").toFile() : new File(pathField.getText()); + // dialog.getExtensionFilters().add(new ExtensionFilter(description, extensions)) + dialog.setInitialDirectory(f); + try { + f = dialog.showDialog(null); + } catch (IllegalArgumentException e) { + // Invalid initial folder + dialog.setInitialDirectory(DotMinecraft.DOTMINECRAFT.resolve("saves").toFile()); + f = dialog.showDialog(null); + } + pathField.setText(f == null ? "" : f.getAbsolutePath()); + pathField.fireEvent(new ActionEvent()); + } + + private boolean hasFilesWithEnding(Path path, String ending) { + try { + return Files.list(path).anyMatch(p -> p.getFileName().toString().endsWith("." + ending)); + } catch (IOException e) { + System.err.println("Could not read content of the folder: " + path); + e.printStackTrace(); + // No warning will be shown to the user. If there is a severe error, it will pop up again when trying to load it. If the folder does not + // contain any usable files, the world map will be empty without warning, + return true; + } + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/guistandalone/GuiMain.java b/src/main/java/togos/minecraft/maprend/guistandalone/GuiMain.java new file mode 100644 index 0000000..037d9e0 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/guistandalone/GuiMain.java @@ -0,0 +1,41 @@ +package togos.minecraft.maprend.guistandalone; + +import java.io.IOException; +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +public class GuiMain extends Application { + + private GuiController controller; + + public GuiMain() { + + } + + @Override + public void start(Stage stage) throws IOException { + stage.setTitle("TMCMR world renderer"); + + FXMLLoader loader = new FXMLLoader(getClass().getResource("scene.fxml")); + Parent root = (Parent) loader.load(); + controller = (GuiController) loader.getController(); + + stage.setScene(new Scene(root, 500, 350)); + stage.show(); + + AnchorPane.setTopAnchor(root.lookup("#rightMenu"), root.lookup("#showButton").getBoundsInLocal().getHeight() + 5); + } + + @Override + public void stop() { + controller.panel.shutDown(); + } + + public static void main(String[] args) { + Application.launch(args); + } +} \ No newline at end of file diff --git a/src/main/resources/togos/minecraft/maprend/guistandalone/scene.fxml b/src/main/resources/togos/minecraft/maprend/guistandalone/scene.fxml new file mode 100644 index 0000000..cb5db8a --- /dev/null +++ b/src/main/resources/togos/minecraft/maprend/guistandalone/scene.fxml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + +
    + + + + + +
    + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +
    From 9ab051d10c3da2c6cdf3608bd2ee9495c237d8ea Mon Sep 17 00:00:00 2001 From: piegamesde Date: Mon, 4 Sep 2017 11:26:22 +0200 Subject: [PATCH 3/7] Minimal changes The absolute minimal changes needed to make this feature work. --- src/main/java/togos/minecraft/maprend/io/RegionFile.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/togos/minecraft/maprend/io/RegionFile.java b/src/main/java/togos/minecraft/maprend/io/RegionFile.java index fd6441d..f2ffa37 100644 --- a/src/main/java/togos/minecraft/maprend/io/RegionFile.java +++ b/src/main/java/togos/minecraft/maprend/io/RegionFile.java @@ -396,8 +396,8 @@ private void setTimestamp(int x, int z, int value) throws IOException { file.writeInt(value); } - @Override - public void close() throws IOException { + @Override + public void close() throws IOException { file.close(); } } \ No newline at end of file From 36e9d55b66dc66795f384384060ec26a377b554e Mon Sep 17 00:00:00 2001 From: piegamesde Date: Mon, 4 Sep 2017 11:27:06 +0200 Subject: [PATCH 4/7] The GUI --- .../maprend/gui/SettingsOverlay.fxml | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/main/resources/togos/minecraft/maprend/gui/SettingsOverlay.fxml diff --git a/src/main/resources/togos/minecraft/maprend/gui/SettingsOverlay.fxml b/src/main/resources/togos/minecraft/maprend/gui/SettingsOverlay.fxml new file mode 100644 index 0000000..1492dbc --- /dev/null +++ b/src/main/resources/togos/minecraft/maprend/gui/SettingsOverlay.fxml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 32d194d44560cd6c9048d05ac4c406345aba8326 Mon Sep 17 00:00:00 2001 From: piegamesde Date: Wed, 20 Sep 2017 23:00:27 +0200 Subject: [PATCH 5/7] More library-friendly - Made the gui package easier to use in libraries - Added rudimental documentation, mostly to the new features --- .../minecraft/maprend/gui/DisplayFrustum.java | 168 ++++++++++ .../maprend/gui/DisplaySettings.java | 14 + .../togos/minecraft/maprend/gui/MapPane.java | 44 +++ .../minecraft/maprend/gui/RenderedRegion.java | 4 +- .../maprend/gui/WorldRendererCanvas.java | 191 +++++++++++ .../maprend/gui/WorldRendererContainer.java | 33 ++ .../maprend/gui/WorldRendererFX.java | 314 ------------------ .../gui/decoration/DragScrollDecoration.java | 40 +++ .../gui/decoration/SettingsOverlay.java | 80 +++++ .../maprend/gui/decoration/package-info.java | 26 ++ .../minecraft/maprend/gui/package-info.java | 11 +- .../maprend/guistandalone/GuiController.java | 91 +---- .../maprend/guistandalone/GuiMain.java | 6 +- .../maprend/gui/SettingsOverlay.fxml | 67 ---- .../gui/decoration/SettingsOverlay.fxml | 77 +++++ .../maprend/guistandalone/scene.fxml | 81 +---- 16 files changed, 708 insertions(+), 539 deletions(-) create mode 100644 src/main/java/togos/minecraft/maprend/gui/DisplayFrustum.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/MapPane.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/WorldRendererContainer.java delete mode 100644 src/main/java/togos/minecraft/maprend/gui/WorldRendererFX.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/decoration/SettingsOverlay.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/decoration/package-info.java delete mode 100644 src/main/resources/togos/minecraft/maprend/gui/SettingsOverlay.fxml create mode 100644 src/main/resources/togos/minecraft/maprend/gui/decoration/SettingsOverlay.fxml diff --git a/src/main/java/togos/minecraft/maprend/gui/DisplayFrustum.java b/src/main/java/togos/minecraft/maprend/gui/DisplayFrustum.java new file mode 100644 index 0000000..bf414ac --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/DisplayFrustum.java @@ -0,0 +1,168 @@ +package togos.minecraft.maprend.gui; + +import java.util.Observable; +import org.joml.AABBd; +import org.joml.Vector3d; +import org.joml.Vector3dc; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.binding.Bindings; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.Node; +import javafx.util.Duration; + +/** + * An object of this class represents the viewport frustum of a {@link WorldRendererCanvas}. All units are in world coordinates by default, meaning one unit is + * equal to one block. + */ +public class DisplayFrustum { + + public static final int MAX_ZOOM_LEVEL = 7; // up to 9 without rounding errors + + /** The size of the viewport is needed to calculate the frustum's size in world coordinates. */ + public final DoubleProperty widthProperty = new SimpleDoubleProperty(800); + /** The size of the viewport is needed to calculate the frustum's size in world coordinates. */ + public final DoubleProperty heightProperty = new SimpleDoubleProperty(500); + + /** + * A linear scaling factor to zoom in or out on the map. Larger values mean zoom in, smaller values mean zoom out. Avoid changing this property directly + * because it leads to inconsistency between scale and zoom. Use {@link #zoomProperty} instead.
    + * A change in this value will scale the map to the new scaling value accordingly. The center of the scaling operation will be the mouse's current position. + * Then, {@link #frustumChanged()} is called. + */ + public final DoubleProperty scaleProperty = new SimpleDoubleProperty(1); + /** + * The zoom is unlike the scale not linear, but on an exponential basis. It is defined as scale=Math.pow(2, zoom). A zoom of zero means a scale + * of 1:1, each unit increase means zooming out by a factor of 2, a each unit decrease results in zooming in by a factor of 2.
    + * Each change of this value will result in {@link #scaleProperty} to be adjusted and the frustum to be recaltulated. + */ + public final DoubleProperty zoomProperty = new SimpleDoubleProperty(0); + /** + * Add a listener to get notified when the frustum has changed, which usually results in the need of redrawing the map to reflect the changes in the + * viewport. + */ + public Observable repaint = new Observable() { + + @Override + public synchronized void notifyObservers(Object arg) { + setChanged(); // Why the hell is this method protected? + super.notifyObservers(arg); + } + }; + + protected boolean dragging = false; + protected Vector3d mousePos = new Vector3d(); + protected Vector3d translation = new Vector3d(0, 0, 0); + protected Timeline zoomTimeline; + protected AABBd frustum; + + public DisplayFrustum() { + scaleProperty.bind(Bindings.createDoubleBinding(() -> Math.pow(2, zoomProperty.get()), zoomProperty)); + scaleProperty.addListener((e, oldScale, newScale) -> { + // In world coordinates + Vector3d cursorPos = new Vector3d(mousePos).div(oldScale.doubleValue()).sub(translation); + translation.add(cursorPos); + translation.div(newScale.doubleValue() / oldScale.doubleValue()); + translation.sub(cursorPos); + + frustumChanged(); + }); + + widthProperty.addListener(e -> frustumChanged()); + heightProperty.addListener(e -> frustumChanged()); + } + + /** + * Sets the new location of the mouse. + * + * @param x the new x-coordinate of the mouse, in local coordinates of the according canvas. + * @param y the new y-coordinate of the mouse, in local coordinates of the according canvas. + * @see Node#boundsInLocalProperty() + */ + public void mouseMove(double x, double y) { + Vector3d lastCursor = new Vector3d(mousePos); + mousePos.set(x, y, 0); + + if (dragging) { + // Difference in world space + Vector3d dragDist = new Vector3d(lastCursor).sub(mousePos).negate(); + double scale = scaleProperty.get(); + translation.add(new Vector3d(dragDist.x / scale, dragDist.y / scale, 0)); + frustumChanged(); + } + } + + /** + * Zooms in or out around the position currently shown by the mouse cursor. The zooming will be animated with an eased interpolation for 100 milliseconds. + * + * @param deltaZoom The amount of change to the zoom property. 1 means zoom out by a factor of 2, -1 means zoom in by a factor of 2. + * @see #zoomProperty + */ + public void mouseScroll(double deltaZoom) { + double currentValue = zoomProperty.get(); + double missingTime = 0; + if (zoomTimeline != null) { + missingTime = (zoomTimeline.getTotalDuration().toMillis() - zoomTimeline.getCurrentTime().toMillis()) / 2; + zoomTimeline.jumpTo("end"); + } + double scale = zoomProperty.get(); + zoomProperty.set(currentValue); + scale += deltaZoom; + if (scale > MAX_ZOOM_LEVEL) + scale = MAX_ZOOM_LEVEL; + if (scale < -MAX_ZOOM_LEVEL) + scale = -MAX_ZOOM_LEVEL; + zoomTimeline = new Timeline(new KeyFrame(Duration.millis(100 + missingTime), new KeyValue(zoomProperty, scale, Interpolator.EASE_BOTH))); + zoomTimeline.play(); + } + + /** Call this to tell if the mouse is currently dragging or not. */ + public void buttonPress(boolean pressed) { + dragging = pressed; + } + + /** + * Rebuilds the view frustum and calls for a repaint. This method is called automatically after changing any of the relevant values, but there might be the + * need of requesting a recalculation manually. + * + * @see #getFrustum() + * @see #repaint + */ + public void frustumChanged() { + frustum = new AABBd(// TODO optimize + new Vector3d(000, 000, 0).div(scaleProperty.get()).sub(translation.x, translation.y, 0), + new Vector3d(widthProperty.get() - 000, heightProperty.get() - 000, 0).div(scaleProperty.get()).sub(translation.x, translation.y, 0)); + // frustum = new AABBd(-Double.MAX_VALUE, -Double.MAX_VALUE, 0, Double.MAX_VALUE, Double.MAX_VALUE, 0); + // System.out.println(frustum); + repaint.notifyObservers(); + } + + /** + * Returns the currently visible part of the world. The coordinates are world coordinates, relative to the world's origin with one unit corresponding to one + * // * block. The vertical range of this AABBd (the z axis) is from zero to zero. This object is not immutable and should not be changed. + * {@link #frustumChanged()} replaces it with a new instance on every call. + */ + public AABBd getFrustum() { + return frustum; + } + + /** + * The zoom level is just a rounded version of {@link #zoomProperty}. The result can be interpreted as level of detail as per common definition. Note that + * when zoomed in this returns negative values. A "negative" LOD (increase of detail) is needed because JavaFX does not support nearest image interpolation + * to display the image correctly. + */ + public int getZoomLevel() { + return (int) Math.ceil(zoomProperty.get()); + } + + public Vector3dc getMousePos() { + return mousePos; + } + + public Vector3dc getTranslation() { + return translation; + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java b/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java new file mode 100644 index 0000000..4ce0a37 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java @@ -0,0 +1,14 @@ +package togos.minecraft.maprend.gui; + +public class DisplaySettings { + // public int threadCount; + // public int overDraw; + // public int diskCacheSize; + // public int memoryCacheSize; + // public int maxZoomInLevel; + // public int maxZoomOutLevel; + // public float loadingFrustum; + + public DisplaySettings() { + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/MapPane.java b/src/main/java/togos/minecraft/maprend/gui/MapPane.java new file mode 100644 index 0000000..7bcd0af --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/MapPane.java @@ -0,0 +1,44 @@ +package togos.minecraft.maprend.gui; + +import java.util.LinkedList; +import java.util.Objects; +import com.sun.javafx.collections.ObservableListWrapper; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.event.EventType; +import javafx.scene.Node; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; + +/***/ +@SuppressWarnings("restriction") +public class MapPane extends StackPane { + + /***/ + public final ObservableList settingsLayers = new ObservableListWrapper(new LinkedList<>()); + /***/ + public final ObservableList decorationLayers = new ObservableListWrapper(new LinkedList<>()); + + protected Region catchEvents; + + public final WorldRendererCanvas renderer; + + public MapPane(WorldRendererCanvas renderer) { + this.renderer = Objects.requireNonNull(renderer); + catchEvents = new Region(); + ListChangeListener l = c -> { + getChildren().clear(); + getChildren().addAll(decorationLayers); + getChildren().add(catchEvents); + getChildren().addAll(settingsLayers); + // System.out.println(Arrays.toString(getChildren().toArray())); + }; + settingsLayers.addListener(l); + decorationLayers.addListener(l); + + catchEvents.setPickOnBounds(true); + catchEvents.addEventFilter(EventType.ROOT, event -> decorationLayers.forEach(l1 -> l1.fireEvent(event))); + + decorationLayers.add(new WorldRendererContainer(renderer)); + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java b/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java index 1bb1998..55e0cbf 100644 --- a/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java +++ b/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java @@ -130,14 +130,14 @@ public boolean isVisible(AABBd frustum) { } public static boolean isVisible(Vector2ic position, int level, AABBd frustum) { - int size = WorldRendererFX.pow2(512, -level); + int size = WorldRendererCanvas.pow2(512, -level); return frustum.testAABB(new AABBd(position.x() * size, position.y() * size, 0, (position.x() + 1) * size, (position.y() + 1) * size, 0)); } public void draw(GraphicsContext gc, int drawingLevel, AABBd frustum, double scale) { // bounds must have been checked here - int size = WorldRendererFX.pow2(512, -this.level); + int size = WorldRendererCanvas.pow2(512, -this.level); final int overDraw = 3; // TODO make setting WritableImage image = this.image.getImage(false); diff --git a/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java b/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java new file mode 100644 index 0000000..f68180c --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java @@ -0,0 +1,191 @@ +package togos.minecraft.maprend.gui; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.joml.Vector3d; +import org.joml.Vector3dc; +import com.sun.glass.ui.Application; +import com.sun.glass.ui.Robot; +import javafx.application.Platform; +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.WritableImage; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import togos.minecraft.maprend.BoundingRect; +import togos.minecraft.maprend.RegionMap; +import togos.minecraft.maprend.RegionRenderer; +import togos.minecraft.maprend.gui.RenderedRegion.RenderingState; +import togos.minecraft.maprend.io.RegionFile; + +@SuppressWarnings("restriction") +public class WorldRendererCanvas extends Canvas implements Runnable { + + public static final int THREAD_COUNT = 4; + + protected RegionRenderer renderer; + protected RenderedMap map; + + protected ScheduledThreadPoolExecutor executor; + protected final List> submitted = Collections.synchronizedList(new LinkedList<>()); + + protected GraphicsContext gc = getGraphicsContext2D(); + protected Robot robot = Application.GetApplication().createRobot(); + + public final DisplayFrustum frustum = new DisplayFrustum(); + + public WorldRendererCanvas(RegionRenderer renderer) { + this.renderer = Objects.requireNonNull(renderer); + + {// Executor + executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(THREAD_COUNT); + map = new RenderedMap(executor);// Executors.newScheduledThreadPool(THREAD_COUNT / 2)); + executor.scheduleAtFixedRate(() -> { + try { + // TODO execute more often if something changes and less often if not + // update upscaled/downscaled images of chunks + if (map.updateImage(frustum.getZoomLevel(), frustum.getFrustum())) + repaint(); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + }, 1000, 1000, TimeUnit.MILLISECONDS); + executor.scheduleWithFixedDelay(map::evictCache, 10, 10, TimeUnit.SECONDS); + + executor.setKeepAliveTime(20, TimeUnit.SECONDS); + executor.allowCoreThreadTimeOut(true); + } + + frustum.widthProperty.bind(widthProperty()); + frustum.heightProperty.bind(heightProperty()); + invalidateTextures(); + frustum.frustumChanged(); + frustum.repaint.addObserver((o, a) -> repaint()); + repaint(); + } + + public void loadWorld(File file) { + // regions.clear(); + // regions = RegionMap.load(file, BoundingRect.INFINITE).regions.stream().collect(Collectors.toMap(r -> new Vector2i(r.rx, r.rz), r -> new + // RenderedRegion2(r))); + // map.clear(); + map.clearReload(RegionMap.load(file, BoundingRect.INFINITE).regions); + invalidateTextures(); + } + + public void invalidateTextures() { + map.invalidateAll(); + for (int i = 0; i < THREAD_COUNT; i++) + executor.submit(this); + } + + public void shutDown() { + executor.shutdownNow(); + try { + executor.awaitTermination(1, TimeUnit.DAYS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + map.close(); + } + + /** Queues in a repaint event calling {@link renderWorld} from the JavaFX Application Thread */ + public void repaint() { + if (Platform.isFxApplicationThread()) + renderWorld(); + else + Platform.runLater(this::renderWorld); + } + + /** Requires to be called from the JavaFX Application Thread. */ + public void renderWorld() { + gc = getGraphicsContext2D(); + // gc.setStroke(Color.GREEN.deriveColor(0, 1, 1, .2)); + gc.setLineWidth(10); + // gc.clearRect(0, 0, getWidth(), getHeight()); + gc.setFill(new Color(0.2f, 0.2f, 0.6f, 1.0f)); + gc.fillRect(0, 0, getWidth(), getHeight()); + + double scale = frustum.scaleProperty.get(); + gc.save(); + gc.scale(scale, scale); + Vector3dc translation = frustum.getTranslation(); + gc.translate(translation.x(), translation.y()); + + map.draw(gc, frustum.getZoomLevel(), frustum.getFrustum(), scale); + gc.restore(); + + if (map.isNothingLoaded()) { + gc.setFill(Color.WHITE); + gc.setFont(Font.font(20)); + gc.fillText("No regions loaded", 10, getHeight() - 10); + } + + // gc.strokeRect(100, 100, getWidth() - 200, getHeight() - 200); + // gc.strokeRect(0, 0, getWidth() - 0, getHeight() - 0); + } + + public RegionRenderer getRegionRenderer() { + return renderer; + } + + @Override + public void run() { + RenderedRegion region = null; + region = nextRegion(); + if (region == null) + return; + repaint(); + try { + try (RegionFile rf = new RegionFile(region.region.regionFile)) { + BufferedImage texture2 = null; + do { + texture2 = renderer.render(rf); + } while (region.valid.compareAndSet(RenderingState.REDRAW, RenderingState.DRAWING) && !Thread.interrupted()); + + WritableImage texture = SwingFXUtils.toFXImage(texture2, null); + region.setImage(texture); + repaint(); + } + } catch (Throwable e) { + e.printStackTrace(); + } finally { + region.valid.set(RenderingState.VALID); + executor.submit(this); + } + } + + /** Returns the next Region to render */ + protected synchronized RenderedRegion nextRegion() { + // In region coordinates + Vector3d cursorPos = new Vector3d(frustum.getMousePos()).div(frustum.scaleProperty.get()).sub(frustum.getTranslation()).div(512).sub(.5, .5, 0); + + Comparator comp = (a, b) -> Double.compare(new Vector3d(a.position.x(), a.position.y(), 0).sub(cursorPos).length(), new Vector3d(b.position.x(), b.position.y(), 0).sub(cursorPos).length()); + RenderedRegion min = null; + for (RenderedRegion r : map.get(0).values()) + if (r.valid.get() == RenderingState.INVALID && (min == null || comp.compare(min, r) > 0)) + min = r; + if (min != null) + // min got handled by another Thread already (while we were still searching), so get a new one + if (!min.valid.compareAndSet(RenderingState.INVALID, RenderingState.DRAWING)) + return nextRegion(); + return min; + } + + /** @return a*2^n using bit shifting */ + public static int pow2(int a, int n) { + if (n > 0) + return a << n; + else if (n < 0) + return a >> -n; + else + return a; + } +} diff --git a/src/main/java/togos/minecraft/maprend/gui/WorldRendererContainer.java b/src/main/java/togos/minecraft/maprend/gui/WorldRendererContainer.java new file mode 100644 index 0000000..6072568 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/WorldRendererContainer.java @@ -0,0 +1,33 @@ +package togos.minecraft.maprend.gui; + +import java.util.Objects; +import javafx.scene.canvas.Canvas; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.BorderPane; + +/** + *

    + * Wraps a {@link WorldRendererCanvas} into an AnchorPane and a BorderPane. This is because a {@link Canvas} such as the renderer needs to have a preferred size + * set to properly work in many layouts. This wrapper will make sure that the canvas will always use exactly as much of the space available - no less and no + * more. It is recommended to use this class in most layout situations. + *

    + */ +public class WorldRendererContainer extends AnchorPane { + + public final WorldRendererCanvas renderer; + + public WorldRendererContainer(WorldRendererCanvas renderer) { + this.renderer = Objects.requireNonNull(renderer); + + BorderPane wrapper = new BorderPane(renderer); + wrapper.setStyle("-fx-background-color: red"); + wrapper.setPickOnBounds(true); + renderer.widthProperty().bind(wrapper.widthProperty()); + renderer.heightProperty().bind(wrapper.heightProperty()); + AnchorPane.setTopAnchor(wrapper, 0D); + AnchorPane.setBottomAnchor(wrapper, 0D); + AnchorPane.setLeftAnchor(wrapper, 0D); + AnchorPane.setRightAnchor(wrapper, 0D); + getChildren().add(wrapper); + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/WorldRendererFX.java b/src/main/java/togos/minecraft/maprend/gui/WorldRendererFX.java deleted file mode 100644 index f746905..0000000 --- a/src/main/java/togos/minecraft/maprend/gui/WorldRendererFX.java +++ /dev/null @@ -1,314 +0,0 @@ -package togos.minecraft.maprend.gui; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import org.joml.AABBd; -import org.joml.Vector2f; -import org.joml.Vector2i; -import org.joml.Vector3d; -import com.sun.glass.ui.Application; -import com.sun.glass.ui.Robot; -import javafx.animation.Interpolator; -import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; -import javafx.animation.Timeline; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.embed.swing.SwingFXUtils; -import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.image.WritableImage; -import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import javafx.util.Duration; -import togos.minecraft.maprend.BoundingRect; -import togos.minecraft.maprend.RegionMap; -import togos.minecraft.maprend.RegionRenderer; -import togos.minecraft.maprend.gui.RenderedRegion.RenderingState; -import togos.minecraft.maprend.io.RegionFile; - -@SuppressWarnings("restriction") -public class WorldRendererFX extends Canvas implements Runnable { - - public static final int THREAD_COUNT = 4; - public static final int MAX_ZOOM_LEVEL = 7; // up to 9 without rounding errors - - protected RegionRenderer renderer; - protected RenderedMap map; - - protected boolean dragging = false; - protected Vector3d mousePos = new Vector3d(); - protected Vector3d translation = new Vector3d(0, 0, 0); - protected double realScale = 1; - protected DoubleProperty scaleProperty = new SimpleDoubleProperty(); - protected Timeline scaleTimeline; - protected AABBd frustum; - - protected ScheduledThreadPoolExecutor executor; - protected final List> submitted = Collections.synchronizedList(new LinkedList<>()); - - protected GraphicsContext gc = getGraphicsContext2D(); - protected Robot robot = Application.GetApplication().createRobot(); - - public WorldRendererFX(RegionRenderer renderer) { - executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(THREAD_COUNT); - map = new RenderedMap(executor);// Executors.newScheduledThreadPool(THREAD_COUNT / 2)); - executor.scheduleAtFixedRate(() -> { - try { - // TODO execute more often if something changes and less often if not - // update upscaled/downscaled images of chunks - int level = (int) Math.ceil(scaleProperty.get()); - if (map.updateImage(level, frustum)) - repaint(); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } - }, 1000, 1000, TimeUnit.MILLISECONDS); - executor.scheduleWithFixedDelay(map::evictCache, 10, 10, TimeUnit.SECONDS); - - executor.setKeepAliveTime(20, TimeUnit.SECONDS); - executor.allowCoreThreadTimeOut(true); - - this.renderer = Objects.requireNonNull(renderer); - - {// Listeners - setOnMouseMoved(e -> mouseMove(e.getX(), e.getY())); - setOnMouseDragged(e -> mouseMove(e.getX(), e.getY())); - setOnMousePressed(e -> { - if (e.getButton().ordinal() == 3) - buttonPress(true); - }); - setOnMouseReleased(e -> { - if (e.getButton().ordinal() == 3) - buttonPress(false); - }); - setOnScroll(e -> mouseScroll(e.getTextDeltaY())); - InvalidationListener l = e -> { - frustumChanged(); - repaint(); - }; - widthProperty().addListener(l); - heightProperty().addListener(l); - scaleProperty.addListener(e -> onScrollChanged()); - } - - // loadWorld(file); - invalidateTextures(); - frustumChanged(); - repaint(); - } - - public void loadWorld(File file) { - // regions.clear(); - // regions = RegionMap.load(file, BoundingRect.INFINITE).regions.stream().collect(Collectors.toMap(r -> new Vector2i(r.rx, r.rz), r -> new - // RenderedRegion2(r))); - // map.clear(); - map.clearReload(RegionMap.load(file, BoundingRect.INFINITE).regions); - invalidateTextures(); - } - - public void invalidateTextures() { - map.invalidateAll(); - for (int i = 0; i < THREAD_COUNT; i++) - executor.submit(this); - } - - public void shutDown() { - executor.shutdownNow(); - try { - executor.awaitTermination(1, TimeUnit.DAYS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - map.close(); - } - - /** Queues in a repaint event calling {@link renderWorld} from the JavaFX Application Thread */ - public void repaint() { - Platform.runLater(this::renderWorld); - } - - /** Requires the projection to be set up to {@code GL11.glOrtho(0, width, height, 0, -1, 1);} and to be called from the JavaFX Application Thread. */ - public void renderWorld() { - gc = getGraphicsContext2D(); - // gc.setStroke(Color.GREEN.deriveColor(0, 1, 1, .2)); - gc.setLineWidth(10); - // gc.clearRect(0, 0, getWidth(), getHeight()); - gc.setFill(new Color(0.2f, 0.2f, 0.6f, 1.0f)); - gc.fillRect(0, 0, getWidth(), getHeight()); - - gc.save(); - gc.scale(realScale, realScale); - gc.translate(translation.x, translation.y); - - int level = (int) Math.ceil(scaleProperty.get()); - map.draw(gc, level, frustum, realScale); - gc.restore(); - - if (map.isNothingLoaded()) { - gc.setFill(Color.WHITE); - gc.setFont(Font.font(20)); - gc.fillText("No regions loaded", 10, getHeight() - 10); - } - - // gc.strokeRect(100, 100, getWidth() - 200, getHeight() - 200); - // gc.strokeRect(0, 0, getWidth() - 0, getHeight() - 0); - } - - public void mouseMove(double x, double y) { - Vector3d lastCursor = new Vector3d(mousePos); - mousePos.set(x, y, 0); - - if (dragging) { - // Difference in world space - Vector3d dragDist = new Vector3d(lastCursor).sub(mousePos).negate(); - translation.add(new Vector3d(dragDist.x / realScale, dragDist.y / realScale, 0)); - frustumChanged(); - repaint(); - - // TODO limit dragging to prevent the map of going out of bounds - - // Wrap mouse when dragging beyond bounds. TODO fix and add it back - // Bounds screen = localToScreen(getBoundsInLocal()); - // int mx = robot.getMouseX(); - // int my = robot.getMouseY(); - // - // int dx = 0, dy = 0; - // if (mx <= screen.getMinX()) { - // dx = 1; - // robot.mouseMove((int) (screen.getMaxX() - 2), my); - // } - // if (mx >= screen.getMaxX() - 1) { - // dx = -1; - // robot.mouseMove((int) (screen.getMinX() + 1), my); - // } - // if (my <= screen.getMinY()) { - // dy = 1; - // robot.mouseMove(mx, (int) (screen.getMaxY() - 2)); - // } - // if (my >= screen.getMaxY() - 1) { - // dy = -1; - // robot.mouseMove(mx, (int) (screen.getMinY() + 1)); - // } - // - // dragDist = new Vector3d(screen.getMaxX() - screen.getMinX(), screen.getMaxY() - screen.getMinY(), 0).mul(dx, dy, 0).negate(); - // translation.add(new Vector3d(dragDist.x / realScale, dragDist.y / realScale, 0)); - } - } - - public void mouseScroll(double dy) { - double currentValue = scaleProperty.get(); - double missingTime = 0; - if (scaleTimeline != null) { - missingTime = (scaleTimeline.getTotalDuration().toMillis() - scaleTimeline.getCurrentTime().toMillis()) / 2; - scaleTimeline.jumpTo("end"); - } - double scale = scaleProperty.get(); - scaleProperty.set(currentValue); - scale += dy / 10; - if (scale > MAX_ZOOM_LEVEL) - scale = MAX_ZOOM_LEVEL; - if (scale < -MAX_ZOOM_LEVEL) - scale = -MAX_ZOOM_LEVEL; - scaleTimeline = new Timeline(new KeyFrame(Duration.millis(100 + missingTime), new KeyValue(scaleProperty, scale, Interpolator.EASE_BOTH))); - scaleTimeline.play(); - } - - private void onScrollChanged() { - double oldScale = realScale; - realScale = Math.pow(2, scaleProperty.get()); - // In world coordinates - Vector3d cursorPos = new Vector3d(mousePos).div(oldScale).sub(translation); - translation.add(cursorPos); - translation.div(realScale / oldScale); - translation.sub(cursorPos); - frustumChanged(); - repaint(); - } - - public void buttonPress(boolean pressed) { - dragging = pressed; - } - - /* Rebuild view frustum */ - protected void frustumChanged() { - frustum = new AABBd( - new Vector3d(000, 000, 0).div(realScale).sub(translation.x, translation.y, 0), - new Vector3d(getWidth() - 000, getHeight() - 000, 0).div(realScale).sub(translation.x, translation.y, 0)); - // frustum = new AABBd(-Double.MAX_VALUE, -Double.MAX_VALUE, 0, Double.MAX_VALUE, Double.MAX_VALUE, 0); - // System.out.println(frustum); - } - - public boolean isVisible(Vector2f point) { - return true; - } - - public boolean isVisible(Vector2i region) { - return true; - } - - public RegionRenderer getRegionRenderer() { - return renderer; - } - - @Override - public void run() { - RenderedRegion region = null; - region = nextRegion(); - if (region == null) - return; - repaint(); - try { - try (RegionFile rf = new RegionFile(region.region.regionFile)) { - BufferedImage texture2 = null; - do { - texture2 = renderer.render(rf); - } while (region.valid.compareAndSet(RenderingState.REDRAW, RenderingState.DRAWING) && !Thread.interrupted()); - - WritableImage texture = SwingFXUtils.toFXImage(texture2, null); - region.setImage(texture); - repaint(); - } - } catch (Throwable e) { - e.printStackTrace(); - } finally { - region.valid.set(RenderingState.VALID); - executor.submit(this); - } - } - - /** Returns the next Region to render */ - protected synchronized RenderedRegion nextRegion() { - // In region coordinates - Vector3d cursorPos = new Vector3d(mousePos).div(realScale).sub(translation).div(512).sub(.5, .5, 0); - - Comparator comp = (a, b) -> Double.compare(new Vector3d(a.position.x(), a.position.y(), 0).sub(cursorPos).length(), new Vector3d(b.position.x(), b.position.y(), 0).sub(cursorPos).length()); - RenderedRegion min = null; - for (RenderedRegion r : map.get(0).values()) - if (r.valid.get() == RenderingState.INVALID && (min == null || comp.compare(min, r) > 0)) - min = r; - if (min != null) - // min got handled by another Thread already (while we were still searching), so get a new one - if (!min.valid.compareAndSet(RenderingState.INVALID, RenderingState.DRAWING)) - return nextRegion(); - return min; - } - - /** return a*2^n */ - public static int pow2(int a, int n) { - if (n > 0) - return a << n; - else if (n < 0) - return a >> -n; - else - return a; - } -} diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java b/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java new file mode 100644 index 0000000..6a709b9 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java @@ -0,0 +1,40 @@ +package togos.minecraft.maprend.gui.decoration; + +import javafx.scene.input.MouseButton; +import javafx.scene.layout.Region; +import togos.minecraft.maprend.gui.DisplayFrustum; + +/** + * This decoration provides basic drag and zoom support, where you can set the button used for dragging as well as zoom speed and direction. Both + * functionalities are optional and can be disabled separatedly. If you disable both, this class will still forward mouse moved events to the view frustum. This + * might be useful if you use manual zooming logic externally and still want to zoom around the mouse center as it does normally. + */ +public class DragScrollDecoration extends Region { + + /** Creates an instance of this class that will drag with the right mouse button and a scroll factor of 1/10. */ + public DragScrollDecoration(DisplayFrustum frustum) { + this(frustum, MouseButton.SECONDARY, 0.1d); + } + + /** + * @param dragButton The button that must be pressed to activate dragging. null will disable dragging. + * @param scrollFactor Higher values will increase the zoomed amount per scroll. Zero deactivates scrolling. Negative values invert the scroll direction. + */ + public DragScrollDecoration(DisplayFrustum frustum, MouseButton dragButton, double scrollFactor) { + setOnMouseMoved(e -> frustum.mouseMove(e.getX(), e.getY())); + setOnMouseDragged(e -> frustum.mouseMove(e.getX(), e.getY())); + + if (dragButton != null) { + setOnMousePressed(e -> { + if (e.getButton().ordinal() == dragButton.ordinal()) + frustum.buttonPress(true); + }); + setOnMouseReleased(e -> { + if (e.getButton().ordinal() == dragButton.ordinal()) + frustum.buttonPress(false); + }); + } + if (scrollFactor != 0) + setOnScroll(e -> frustum.mouseScroll(e.getTextDeltaY() * scrollFactor)); + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/SettingsOverlay.java b/src/main/java/togos/minecraft/maprend/gui/decoration/SettingsOverlay.java new file mode 100644 index 0000000..e4f9976 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/decoration/SettingsOverlay.java @@ -0,0 +1,80 @@ +package togos.minecraft.maprend.gui.decoration; + +import java.io.IOException; +import java.net.URL; +import java.util.Objects; +import java.util.ResourceBundle; +import org.controlsfx.control.RangeSlider; +import javafx.animation.TranslateTransition; +import javafx.beans.binding.Bindings; +import javafx.beans.value.ChangeListener; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.control.ToggleButton; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import javafx.util.Duration; +import togos.minecraft.maprend.gui.WorldRendererCanvas; + +public class SettingsOverlay extends AnchorPane implements Initializable { + + @FXML + private Label minHeight, maxHeight; + @FXML + private RangeSlider heightSlider; + @FXML + private ToggleButton showButton; + @FXML + private VBox rightMenu; + + protected WorldRendererCanvas panel; + + public SettingsOverlay(WorldRendererCanvas panel) { + this.panel = Objects.requireNonNull(panel); + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("SettingsOverlay.fxml")); + loader.setRoot(this); + loader.setController(this); + loader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + minHeight.textProperty().bind(Bindings.format("Min: %.0f", heightSlider.lowValueProperty())); + maxHeight.textProperty().bind(Bindings.format("Max: %.0f", heightSlider.highValueProperty())); + + ChangeListener heightListener = (e, oldVal, newVal) -> { + if (oldVal && !newVal) { + if (e == heightSlider.lowValueChangingProperty()) + panel.getRegionRenderer().settings.minHeight = (int) Math.round(heightSlider.lowValueProperty().getValue().doubleValue()); + else if (e == heightSlider.highValueChangingProperty()) + panel.getRegionRenderer().settings.maxHeight = (int) Math.round(heightSlider.highValueProperty().getValue().doubleValue()); + panel.invalidateTextures(); + panel.repaint(); + } + }; + heightSlider.lowValueChangingProperty().addListener(heightListener); + heightSlider.highValueChangingProperty().addListener(heightListener); + + showButton.setOnAction(e -> { + if (showButton.isSelected()) { + // showButton.setText("Hide settings"); + TranslateTransition transition = new TranslateTransition(Duration.millis(500), rightMenu); + transition.setFromX(rightMenu.getTranslateX()); + transition.setToX(0); + transition.play(); + } else { + // showButton.setText("Show settings"); + TranslateTransition transition = new TranslateTransition(Duration.millis(500), rightMenu); + transition.setFromX(rightMenu.getTranslateX()); + transition.setToX(rightMenu.getWidth() - 15); + transition.play(); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/package-info.java b/src/main/java/togos/minecraft/maprend/gui/decoration/package-info.java new file mode 100644 index 0000000..1ca6b91 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/decoration/package-info.java @@ -0,0 +1,26 @@ +/** + *

    + * A Decoration is a {@link javafx.scene.Node} (typically a subclass of {@link javafx.scene.layout.Region}), that is part of the internal stack of the + * {@link togos.minecraft.maprend.gui.MapPane}. Multiple decorations are added on top of each other and may provide additional functionality or visual + * information to the world map. They all have the same size as the map. There are two types of Decorations: + *

      + *
    • A settings layer is a normal GUI layer on top that provides additional functionality. The pickOnBounds property should be set on false so + * that events only get caught where there actually is some GUI beneath. Otherwise user interaction events with the map might not reach their goal.
    • + *
    • A decoration layer does not catch any events. All events that would reach it get caught by an internal component and distributed to all decoration + * layers. This allows to have multiple decorations that use the same events, e.g. one for dragging and one for clicking or using different mouse buttons. + * Decoration layers thus cannot easily contain conventional GUI.
    • + *
    + *

    + *

    + * Available decorations: + *

      + *
    • {@link togos.minecraft.maprend.gui.decoration.SettingsOverlay}: Puts a hideable panel at the right of the map to change the min and max height setting. + *
    • + *
    • {@link togos.minecraft.maprend.gui.decoration.DragScrollDecoration}: Provides basic drag and zoom functionality.
    • + *
    + * What is yet to come: More settings, different decorations for selecting areas, showing points (players, monuments, etc.) on the map + *

    + * + * @author piegames + */ +package togos.minecraft.maprend.gui.decoration; \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/package-info.java b/src/main/java/togos/minecraft/maprend/gui/package-info.java index 67cd0bc..0d9e513 100644 --- a/src/main/java/togos/minecraft/maprend/gui/package-info.java +++ b/src/main/java/togos/minecraft/maprend/gui/package-info.java @@ -1,5 +1,14 @@ /** + *

    + * This package contains all classes to show an interactive map of a world in a JavaFX application that is highly customizable (or will be in the future). The + * most important class is {@link togos.minecraft.maprend.gui.WorldRendererCanvas}, a Canvas that loads and renders the world map. But it is recommended to use + * {@link togos.minecraft.maprend.gui.MapPane} instead, which will wrap the canvas in a GUI with proper layout and the possibility to add decorations and + * interactive functionality. + *

    + *

    + * This package is still under development. Everything might be subject to change. Use at own risk. + *

    + * * @author piegames - * */ package togos.minecraft.maprend.gui; \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java b/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java index b60e5d8..c290500 100644 --- a/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java +++ b/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java @@ -12,108 +12,53 @@ import java.util.ResourceBundle; import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.controlsfx.control.RangeSlider; import org.controlsfx.dialog.CommandLinksDialog; import org.controlsfx.dialog.CommandLinksDialog.CommandLinksButtonType; -import javafx.animation.TranslateTransition; -import javafx.beans.binding.Bindings; -import javafx.beans.value.ChangeListener; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; -import javafx.scene.control.*; +import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; import javafx.stage.DirectoryChooser; import javafx.stage.Modality; -import javafx.util.Duration; import togos.minecraft.maprend.DotMinecraft; import togos.minecraft.maprend.RegionRenderer; import togos.minecraft.maprend.RenderSettings; -import togos.minecraft.maprend.gui.WorldRendererFX; +import togos.minecraft.maprend.gui.MapPane; +import togos.minecraft.maprend.gui.WorldRendererCanvas; +import togos.minecraft.maprend.gui.decoration.DragScrollDecoration; +import togos.minecraft.maprend.gui.decoration.SettingsOverlay; public class GuiController implements Initializable { - public WorldRendererFX panel; + public WorldRendererCanvas renderer; @FXML - private Label minHeight, maxHeight; - - @FXML - private RangeSlider heightSlider; - - @FXML - private StackPane stackPane; - - @FXML - private ToggleButton showButton; - + private BorderPane root; @FXML - private VBox rightMenu; - - @FXML - private TextField pathField; - + private TextField pathField; @FXML - private Button browseButton; + private Button browseButton; - @FXML - BorderPane rendererContainer; + protected MapPane pane; public GuiController() { } @Override public void initialize(URL location, ResourceBundle resources) { - minHeight.textProperty().bind(Bindings.format("Min: %.0f", heightSlider.lowValueProperty())); - maxHeight.textProperty().bind(Bindings.format("Max: %.0f", heightSlider.highValueProperty())); - try { - panel = new WorldRendererFX(new RegionRenderer(new RenderSettings())); - // anchorPane.getChildren().add(panel); - rendererContainer.setCenter(panel); - // anchorPane.getChildren().clear(); - // AnchorPane.setTopAnchor(panel, 0.0); - // AnchorPane.setBottomAnchor(panel, 0.0); - // AnchorPane.setLeftAnchor(panel, 0.0); - // AnchorPane.setRightAnchor(panel, 0.0); - panel.widthProperty().bind(rendererContainer.widthProperty()); - panel.heightProperty().bind(rendererContainer.heightProperty()); - rendererContainer.setPickOnBounds(true); + renderer = new WorldRendererCanvas(new RegionRenderer(new RenderSettings())); + root.setCenter(pane = new MapPane(renderer)); } catch (IOException e1) { e1.printStackTrace(); } - - ChangeListener heightListener = (e, oldVal, newVal) -> { - if (oldVal && !newVal) { - if (e == heightSlider.lowValueChangingProperty()) - panel.getRegionRenderer().settings.minHeight = (int) Math.round(heightSlider.lowValueProperty().getValue().doubleValue()); - else if (e == heightSlider.highValueChangingProperty()) - panel.getRegionRenderer().settings.maxHeight = (int) Math.round(heightSlider.highValueProperty().getValue().doubleValue()); - panel.invalidateTextures(); - panel.repaint(); - } - }; - heightSlider.lowValueChangingProperty().addListener(heightListener); - heightSlider.highValueChangingProperty().addListener(heightListener); - - showButton.setOnAction(e -> { - if (showButton.isSelected()) { - // showButton.setText("Hide settings"); - TranslateTransition transition = new TranslateTransition(Duration.millis(500), rightMenu); - transition.setFromX(rightMenu.getTranslateX()); - transition.setToX(0); - transition.play(); - } else { - // showButton.setText("Show settings"); - TranslateTransition transition = new TranslateTransition(Duration.millis(500), rightMenu); - transition.setFromX(rightMenu.getTranslateX()); - transition.setToX(rightMenu.getWidth() - 15); - transition.play(); - } - }); + pane.decorationLayers.add(new DragScrollDecoration(renderer.frustum)); + pane.settingsLayers.add(new SettingsOverlay(renderer)); } public void reloadWorld() { @@ -160,7 +105,7 @@ public void reloadWorld() { if (!hasFilesWithEnding(path, "mca")) new Alert(AlertType.WARNING, "Your selected folder seems to not contain any useful files." + (hasFilesWithEnding(path, "mcr") ? " It does contain some region files in the old format though, please open this world in a newer version of Minecraft to automatically convert them." : "")).showAndWait(); - panel.loadWorld(path.toFile()); + renderer.loadWorld(path.toFile()); } else new Alert(AlertType.ERROR, "Folder does not exist", ButtonType.OK).showAndWait(); } catch (InvalidPathException e) { diff --git a/src/main/java/togos/minecraft/maprend/guistandalone/GuiMain.java b/src/main/java/togos/minecraft/maprend/guistandalone/GuiMain.java index 037d9e0..ebdd436 100644 --- a/src/main/java/togos/minecraft/maprend/guistandalone/GuiMain.java +++ b/src/main/java/togos/minecraft/maprend/guistandalone/GuiMain.java @@ -5,7 +5,6 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; -import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; public class GuiMain extends Application { @@ -13,7 +12,6 @@ public class GuiMain extends Application { private GuiController controller; public GuiMain() { - } @Override @@ -26,13 +24,11 @@ public void start(Stage stage) throws IOException { stage.setScene(new Scene(root, 500, 350)); stage.show(); - - AnchorPane.setTopAnchor(root.lookup("#rightMenu"), root.lookup("#showButton").getBoundsInLocal().getHeight() + 5); } @Override public void stop() { - controller.panel.shutDown(); + controller.renderer.shutDown(); } public static void main(String[] args) { diff --git a/src/main/resources/togos/minecraft/maprend/gui/SettingsOverlay.fxml b/src/main/resources/togos/minecraft/maprend/gui/SettingsOverlay.fxml deleted file mode 100644 index 1492dbc..0000000 --- a/src/main/resources/togos/minecraft/maprend/gui/SettingsOverlay.fxml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/togos/minecraft/maprend/gui/decoration/SettingsOverlay.fxml b/src/main/resources/togos/minecraft/maprend/gui/decoration/SettingsOverlay.fxml new file mode 100644 index 0000000..1321e2a --- /dev/null +++ b/src/main/resources/togos/minecraft/maprend/gui/decoration/SettingsOverlay.fxml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/togos/minecraft/maprend/guistandalone/scene.fxml b/src/main/resources/togos/minecraft/maprend/guistandalone/scene.fxml index cb5db8a..ecb7c8e 100644 --- a/src/main/resources/togos/minecraft/maprend/guistandalone/scene.fxml +++ b/src/main/resources/togos/minecraft/maprend/guistandalone/scene.fxml @@ -1,14 +1,11 @@ + - - - - - - + fx:controller="togos.minecraft.maprend.guistandalone.GuiController" + fx:id="root"> @@ -68,74 +64,5 @@
    - - - - - - - - - - - - - - - - - - - - - - -
    -
    + \ No newline at end of file From 7854d92405e5cea26b26e6190d2aad8fd719aef4 Mon Sep 17 00:00:00 2001 From: Unknown <14054505+piegamesde@users.noreply.github.com> Date: Mon, 30 Jul 2018 11:41:16 +0200 Subject: [PATCH 6/7] Refactoring, library stuff The library API is still work in progress. --- .classpath | 56 ++- pom.xml | 5 - src/main/java/org/joml/ImmutableVector2i.java | 427 ++++++++++++++++ src/main/java/org/joml/ImmutableVector3i.java | 475 ++++++++++++++++++ .../maprend/gui/CanvasContainer.java | 27 + .../minecraft/maprend/gui/CanvasHelper.java | 31 ++ .../minecraft/maprend/gui/DisplayFrustum.java | 168 ------- .../maprend/gui/DisplaySettings.java | 4 + .../maprend/gui/DisplayViewport.java | 148 ++++++ .../togos/minecraft/maprend/gui/MapPane.java | 2 +- .../minecraft/maprend/gui/RenderedMap.java | 42 +- .../minecraft/maprend/gui/RenderedRegion.java | 12 +- .../maprend/gui/WorldRendererCanvas.java | 31 +- .../maprend/gui/WorldRendererContainer.java | 33 -- .../gui/decoration/DragScrollDecoration.java | 60 ++- .../decoration/SelectRectangleDecoration.java | 70 +++ .../maprend/guistandalone/GuiController.java | 2 +- 17 files changed, 1300 insertions(+), 293 deletions(-) create mode 100644 src/main/java/org/joml/ImmutableVector2i.java create mode 100644 src/main/java/org/joml/ImmutableVector3i.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/CanvasContainer.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/CanvasHelper.java delete mode 100644 src/main/java/togos/minecraft/maprend/gui/DisplayFrustum.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/DisplayViewport.java delete mode 100644 src/main/java/togos/minecraft/maprend/gui/WorldRendererContainer.java create mode 100644 src/main/java/togos/minecraft/maprend/gui/decoration/SelectRectangleDecoration.java diff --git a/.classpath b/.classpath index dfc7bed..790d0fb 100644 --- a/.classpath +++ b/.classpath @@ -1,23 +1,33 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 618cdf0..42ac2a3 100644 --- a/pom.xml +++ b/pom.xml @@ -28,11 +28,6 @@ - org.mapdb mapdb diff --git a/src/main/java/org/joml/ImmutableVector2i.java b/src/main/java/org/joml/ImmutableVector2i.java new file mode 100644 index 0000000..653a4b7 --- /dev/null +++ b/src/main/java/org/joml/ImmutableVector2i.java @@ -0,0 +1,427 @@ +/* (C) Copyright 2015-2017 Richard Greenlees + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +package org.joml; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.text.DecimalFormat; +import java.text.NumberFormat; + +/** + * Represents a 2D vector with single-precision. The values of this class are set in the constructor and cannot be changed. Operations that modify this vector + * either take in a destination vector that will be changed or return a changed immutable copy of this vector with the computation's result. + * + * @author RGreenlees + * @author Kai Burjack + * @author Hans Uhlig + * @author Julian Fischer + */ +public class ImmutableVector2i implements Vector2ic { + + /** + * The x component of the vector. + */ + public final int x; + /** + * The y component of the vector. + */ + public final int y; + + /** + * Create a new {@link ImmutableVector2i} and initialize its components to zero. + * + * @Deprecated Use {@link #ZERO} instead. + */ + @Deprecated + public ImmutableVector2i() { + this(0); + } + + /** + * Create a new {@link ImmutableVector2i} and initialize both of its components with the given value. + * + * @param s the value of both components + */ + public ImmutableVector2i(int s) { + this(s, s); + } + + /** + * Create a new {@link ImmutableVector2i} and initialize its components to the given values. + * + * @param x the x component + * @param y the y component + */ + public ImmutableVector2i(int x, int y) { + this.x = x; + this.y = y; + } + + /** + * Create a new {@link ImmutableVector2i} and initialize its components to the one of the given vector. + * + * @param v the {@link Vector2ic} to copy the values from + */ + public ImmutableVector2i(Vector2ic v) { + x = v.x(); + y = v.y(); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#x() */ + @Override + public int x() { + return this.x; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#y() */ + @Override + public int y() { + return this.y; + } + + /** + * Set the value of the specified component of this vector and returns it in a new vector. + * + * @param component the component whose value to set, within [0..1] + * @param value the value to set + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + * @throws IllegalArgumentException if component is not within [0..1] + */ + public ImmutableVector2i setComponent(int component, int value) throws IllegalArgumentException { + int x = this.x; + int y = this.y; + switch (component) { + case 0: + x = value; + break; + case 1: + y = value; + break; + default: + throw new IllegalArgumentException(); + } + return new ImmutableVector2i(x, y); + } + + // #ifdef __HAS_NIO__ + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#get(java.nio.ByteBuffer) */ + @Override + public ByteBuffer get(ByteBuffer buffer) { + return get(buffer.position(), buffer); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#get(int, java.nio.ByteBuffer) */ + @Override + public ByteBuffer get(int index, ByteBuffer buffer) { + MemUtil.INSTANCE.put(this.toMutableVector(), index, buffer); + return buffer; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#get(java.nio.IntBuffer) */ + @Override + public IntBuffer get(IntBuffer buffer) { + return get(buffer.position(), buffer); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#get(int, java.nio.IntBuffer) */ + @Override + public IntBuffer get(int index, IntBuffer buffer) { + MemUtil.INSTANCE.put(this.toMutableVector(), index, buffer); + return buffer; + } + // #endif + + public Vector2i toMutableVector() { + return new Vector2i(this); + } + + /** + * Subtract the supplied vector from this one and store the result in a new vector. + * + * @param v the vector to subtract + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + */ + public ImmutableVector2i sub(Vector2ic v) { + return new ImmutableVector2i(x - v.x(), y - v.y()); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#sub(org.joml.Vector2ic, org.joml.Vector2i) */ + @Override + public Vector2i sub(Vector2ic v, Vector2i dest) { + dest.x = x - v.x(); + dest.y = y - v.y(); + return dest; + } + + /** + * Decrement the components of this vector by the given values. + * + * @param x the x component to subtract + * @param y the y component to subtract + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + */ + public ImmutableVector2i sub(int x, int y) { + return new ImmutableVector2i(this.x - x, this.y - y); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#sub(int, int, org.joml.Vector2i) */ + @Override + public Vector2i sub(int x, int y, Vector2i dest) { + dest.x = this.x - x; + dest.y = this.y - y; + return dest; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#lengthSquared() */ + @Override + public long lengthSquared() { + return x * x + y * y; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#length() */ + @Override + public double length() { + return Math.sqrt(lengthSquared()); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#distance(org.joml.Vector2ic) */ + @Override + public double distance(Vector2ic v) { + return Math.sqrt(distanceSquared(v)); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#distance(int, int) */ + @Override + public double distance(int x, int y) { + return Math.sqrt(distanceSquared(x, y)); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#distanceSquared(org.joml.Vector2ic) */ + @Override + public long distanceSquared(Vector2ic v) { + int dx = this.x - v.x(); + int dy = this.y - v.y(); + return dx * dx + dy * dy; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#distanceSquared(int, int) */ + @Override + public long distanceSquared(int x, int y) { + int dx = this.x - x; + int dy = this.y - y; + return dx * dx + dy * dy; + } + + /** + * Add the supplied vector to this one. + * + * @param v the vector to add + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + */ + public ImmutableVector2i add(Vector2ic v) { + return add(v.x(), v.y()); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#add(org.joml.Vector2ic, org.joml.Vector2i) */ + @Override + public Vector2i add(Vector2ic v, Vector2i dest) { + dest.x = x + v.x(); + dest.y = y + v.y(); + return dest; + } + + /** + * Increment the components of this vector by the given values. + * + * @param x the x component to add + * @param y the y component to add + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + */ + public ImmutableVector2i add(int x, int y) { + return new ImmutableVector2i(this.x + x, this.y + y); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#add(int, int, int, org.joml.Vector2i) */ + @Override + public Vector2i add(int x, int y, Vector2i dest) { + dest.x = this.x + x; + dest.y = this.y + y; + return dest; + } + + /** + * Multiply all components of this vector by the given scalar value. + * + * @param scalar the scalar to multiply this vector by + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + */ + public ImmutableVector2i mul(int scalar) { + return new ImmutableVector2i(x * scalar, y * scalar); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#mul(int, org.joml.Vector2i) */ + @Override + public Vector2i mul(int scalar, Vector2i dest) { + dest.x = x * scalar; + dest.y = y * scalar; + return dest; + } + + /** + * Multiply the componentes of this vector by the values given in v. + * + * @param v the vector to multiply + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + */ + public ImmutableVector2i mul(Vector2ic v) { + return new ImmutableVector2i(this.x * v.x(), this.y * v.y()); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#mul(org.joml.Vector2ic, org.joml.Vector2i) */ + @Override + public Vector2i mul(Vector2ic v, Vector2i dest) { + dest.x = x * v.x(); + dest.y = y * v.y(); + return dest; + } + + /** + * Multiply the components of this vector by the given values. + * + * @param x the x component to multiply + * @param y the y component to multiply + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + */ + public ImmutableVector2i mul(int x, int y) { + return new ImmutableVector2i(this.x * x, this.y * y); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#mul(int, int, int, org.joml.Vector2i) */ + @Override + public Vector2i mul(int x, int y, Vector2i dest) { + dest.x = this.x * x; + dest.y = this.y * y; + return dest; + } + + public static final ImmutableVector2i ZERO = new ImmutableVector2i(); + + /** + * Negate this vector. + * + * @return a new {@code ImmutableVector2i} containing a modified version of this according to the operation + */ + public ImmutableVector2i negate() { + return new ImmutableVector2i(-x, -y); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector2ic#negate(org.joml.Vector2i) */ + @Override + public Vector2i negate(Vector2i dest) { + dest.x = -x; + dest.y = -y; + return dest; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + x; + result = prime * result + y; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ImmutableVector2i other = (ImmutableVector2i) obj; + if (x != other.x) { + return false; + } + if (y != other.y) { + return false; + } + return true; + } + + /** + * Return a string representation of this vector. + *

    + * This method creates a new {@link DecimalFormat} on every invocation with the format string "0.000E0;-". + * + * @return the string representation + */ + @Override + public String toString() { + return Runtime.formatNumbers(toString(Options.NUMBER_FORMAT)); + } + + /** + * Return a string representation of this vector by formatting the vector components with the given {@link NumberFormat}. + * + * @param formatter the {@link NumberFormat} used to format the vector components with + * @return the string representation + */ + public String toString(NumberFormat formatter) { + return "(" + formatter.format(x) + " " + formatter.format(y) + ")"; + } + +} diff --git a/src/main/java/org/joml/ImmutableVector3i.java b/src/main/java/org/joml/ImmutableVector3i.java new file mode 100644 index 0000000..74f249e --- /dev/null +++ b/src/main/java/org/joml/ImmutableVector3i.java @@ -0,0 +1,475 @@ +/* (C) Copyright 2015-2017 Richard Greenlees + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +package org.joml; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.text.DecimalFormat; +import java.text.NumberFormat; + +/** + * Contains the definition of a Vector comprising 3 ints and associated transformations. The values of this class are set in the constructor and cannot be + * changed. Operations that modify this vector either take in a destination vector that will be changed or return a changed immutable copy of this vector with + * the computation's result. + * + * @author Richard Greenlees + * @author Kai Burjack + * @author Hans Uhlig + * @author Julian Fischer + */ +public class ImmutableVector3i implements Vector3ic { + + /** + * The x component of the vector. + */ + public final int x; + /** + * The y component of the vector. + */ + public final int y; + /** + * The z component of the vector. + */ + public final int z; + + /** + * Create a new {@link Vector3i} of (0, 0, 0). + * + * @Deprecated Use {@link #ZERO} instead. + */ + @Deprecated + public ImmutableVector3i() { + this(0); + } + + /** + * Create a new {@link Vector3i} and initialize all three components with the given value. + * + * @param d the value of all three components + */ + public ImmutableVector3i(int d) { + this(d, d, d); + } + + /** + * Create a new {@link Vector3i} with the given component values. + * + * @param x the value of x + * @param y the value of y + * @param z the value of z + */ + public ImmutableVector3i(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Create a new {@link Vector3i} with the same values as v. + * + * @param v the {@link Vector3ic} to copy the values from + */ + public ImmutableVector3i(Vector3ic v) { + this.x = v.x(); + this.y = v.y(); + this.z = v.z(); + } + + /** + * Create a new {@link Vector3i} with the first two components from the given v and the given z + * + * @param v the {@link Vector2ic} to copy the values from + * @param z the z component + */ + public ImmutableVector3i(Vector2ic v, int z) { + this.x = v.x(); + this.y = v.y(); + this.z = z; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#x() */ + @Override + public int x() { + return this.x; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#y() */ + @Override + public int y() { + return this.y; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#z() */ + @Override + public int z() { + return this.z; + } + + /** + * Set the value of the specified component of this vector and returns it in a new vector. + * + * @param component the component whose value to set, within [0..2] + * @param value the value to set + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + * @throws IllegalArgumentException if component is not within [0..2] + */ + public ImmutableVector3i setComponent(int component, int value) throws IllegalArgumentException { + int x = this.x; + int y = this.y; + int z = this.z; + switch (component) { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + default: + throw new IllegalArgumentException(); + } + return new ImmutableVector3i(x, y, z); + } + + // #ifdef __HAS_NIO__ + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#get(java.nio.IntBuffer) */ + @Override + public IntBuffer get(IntBuffer buffer) { + return get(buffer.position(), buffer); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#get(int, java.nio.IntBuffer) */ + @Override + public IntBuffer get(int index, IntBuffer buffer) { + MemUtil.INSTANCE.put(this.toMutableVector(), index, buffer); + return buffer; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#get(java.nio.ByteBuffer) */ + @Override + public ByteBuffer get(ByteBuffer buffer) { + return get(buffer.position(), buffer); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#get(int, java.nio.ByteBuffer) */ + @Override + public ByteBuffer get(int index, ByteBuffer buffer) { + MemUtil.INSTANCE.put(this.toMutableVector(), index, buffer); + return buffer; + } + // #endif + + public Vector3i toMutableVector() { + return new Vector3i(this); + } + + /** + * Subtract the supplied vector from this one and store the result in a new vector. + * + * @param v the vector to subtract + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + */ + public ImmutableVector3i sub(Vector3ic v) { + return new ImmutableVector3i(x - v.x(), y - v.y(), z - v.z()); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#sub(org.joml.Vector3ic, org.joml.Vector3i) */ + @Override + public Vector3i sub(Vector3ic v, Vector3i dest) { + dest.x = x - v.x(); + dest.y = y - v.y(); + dest.z = z - v.z(); + return dest; + } + + /** + * Decrement the components of this vector by the given values. + * + * @param x the x component to subtract + * @param y the y component to subtract + * @param z the z component to subtract + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + */ + public ImmutableVector3i sub(int x, int y, int z) { + return new ImmutableVector3i(this.x - x, this.y - y, this.z - z); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#sub(int, int, int, org.joml.Vector3i) */ + @Override + public Vector3i sub(int x, int y, int z, Vector3i dest) { + dest.x = this.x - x; + dest.y = this.y - y; + dest.z = this.z - z; + return dest; + } + + /** + * Add the supplied vector to this one. + * + * @param v the vector to add + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + */ + public ImmutableVector3i add(Vector3ic v) { + return add(v.x(), v.y(), v.z()); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#add(org.joml.Vector3ic, org.joml.Vector3i) */ + @Override + public Vector3i add(Vector3ic v, Vector3i dest) { + dest.x = x + v.x(); + dest.y = y + v.y(); + dest.z = z + v.z(); + return dest; + } + + /** + * Increment the components of this vector by the given values. + * + * @param x the x component to add + * @param y the y component to add + * @param z the z component to add + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + */ + public ImmutableVector3i add(int x, int y, int z) { + return new ImmutableVector3i(this.x + x, this.y + y, this.z + z); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#add(int, int, int, org.joml.Vector3i) */ + @Override + public Vector3i add(int x, int y, int z, Vector3i dest) { + dest.x = this.x + x; + dest.y = this.y + y; + dest.z = this.z + z; + return dest; + } + + /** + * Multiply all components of this vector by the given scalar value. + * + * @param scalar the scalar to multiply this vector by + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + */ + public ImmutableVector3i mul(int scalar) { + return new ImmutableVector3i(x * scalar, y * scalar, z * scalar); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#mul(int, org.joml.Vector3i) */ + @Override + public Vector3i mul(int scalar, Vector3i dest) { + dest.x = x * scalar; + dest.y = y * scalar; + dest.y = z * scalar; + return dest; + } + + /** + * Multiply the componentes of this vector by the values given in v. + * + * @param v the vector to multiply + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + */ + public ImmutableVector3i mul(Vector3ic v) { + return new ImmutableVector3i(this.x * v.x(), this.y * v.y(), this.z * v.z()); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#mul(org.joml.Vector3ic, org.joml.Vector3i) */ + @Override + public Vector3i mul(Vector3ic v, Vector3i dest) { + dest.x = x * v.x(); + dest.y = y * v.y(); + dest.z = z * v.z(); + return dest; + } + + /** + * Multiply the components of this vector by the given values. + * + * @param x the x component to multiply + * @param y the y component to multiply + * @param z the z component to multiply + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + */ + public ImmutableVector3i mul(int x, int y, int z) { + return new ImmutableVector3i(this.x * x, this.y * y, this.z * z); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#mul(int, int, int, org.joml.Vector3i) */ + @Override + public Vector3i mul(int x, int y, int z, Vector3i dest) { + dest.x = this.x * x; + dest.y = this.y * y; + dest.z = this.z * z; + return dest; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#lengthSquared() */ + @Override + public long lengthSquared() { + return x * x + y * y + z * z; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#length() */ + @Override + public double length() { + return Math.sqrt(lengthSquared()); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#distance(org.joml.Vector3ic) */ + @Override + public double distance(Vector3ic v) { + return Math.sqrt(distanceSquared(v)); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#distance(int, int, int) */ + @Override + public double distance(int x, int y, int z) { + return Math.sqrt(distanceSquared(x, y, z)); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#distanceSquared(org.joml.Vector3ic) */ + @Override + public long distanceSquared(Vector3ic v) { + int dx = this.x - v.x(); + int dy = this.y - v.y(); + int dz = this.z - v.z(); + return dx * dx + dy * dy + dz * dz; + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#distanceSquared(int, int, int) */ + @Override + public long distanceSquared(int x, int y, int z) { + int dx = this.x - x; + int dy = this.y - y; + int dz = this.z - z; + return dx * dx + dy * dy + dz * dz; + } + + public static final ImmutableVector3i ZERO = new ImmutableVector3i(); + + /** + * Return a string representation of this vector. + *

    + * This method creates a new {@link DecimalFormat} on every invocation with the format string "0.000E0;-". + * + * @return the string representation + */ + @Override + public String toString() { + return Runtime.formatNumbers(toString(Options.NUMBER_FORMAT)); + } + + /** + * Return a string representation of this vector by formatting the vector components with the given {@link NumberFormat}. + * + * @param formatter the {@link NumberFormat} used to format the vector components with + * @return the string representation + */ + public String toString(NumberFormat formatter) { + return "(" + formatter.format(x) + " " + formatter.format(y) + " " + formatter.format(z) + ")"; + } + + /** + * Negate this vector. + * + * @return a new {@code ImmutableVector3i} containing a modified version of this according to the operation + */ + public ImmutableVector3i negate() { + return new ImmutableVector3i(-x, -y, -z); + } + + /* (non-Javadoc) + * + * @see org.joml.Vector3ic#negate(org.joml.Vector3i) */ + @Override + public Vector3i negate(Vector3i dest) { + dest.x = -x; + dest.y = -y; + dest.z = -z; + return dest; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + x; + result = prime * result + y; + result = prime * result + z; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Vector3i other = (Vector3i) obj; + if (x != other.x) { + return false; + } + if (y != other.y) { + return false; + } + if (z != other.z) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/CanvasContainer.java b/src/main/java/togos/minecraft/maprend/gui/CanvasContainer.java new file mode 100644 index 0000000..5b9b856 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/CanvasContainer.java @@ -0,0 +1,27 @@ +package togos.minecraft.maprend.gui; + +import javafx.scene.canvas.Canvas; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.BorderPane; + +/** + *

    + * Wraps a {@link Canvas} into an AnchorPane and a BorderPane. This is because a Canvas needs to have a preferred size set to properly work in many layouts. + * This wrapper will make sure that the canvas will always use exactly as much of the space available - no less and no more. It is recommended to use this class + * in most layout situations when working with canvases. + *

    + */ +public class CanvasContainer extends AnchorPane { + + public CanvasContainer(Canvas canvas) { + BorderPane wrapper = new BorderPane(canvas); + wrapper.setPickOnBounds(true); + canvas.widthProperty().bind(wrapper.widthProperty()); + canvas.heightProperty().bind(wrapper.heightProperty()); + AnchorPane.setTopAnchor(wrapper, 0D); + AnchorPane.setBottomAnchor(wrapper, 0D); + AnchorPane.setLeftAnchor(wrapper, 0D); + AnchorPane.setRightAnchor(wrapper, 0D); + getChildren().add(wrapper); + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/CanvasHelper.java b/src/main/java/togos/minecraft/maprend/gui/CanvasHelper.java new file mode 100644 index 0000000..1ca10d7 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/CanvasHelper.java @@ -0,0 +1,31 @@ +package togos.minecraft.maprend.gui; + +import javafx.application.Platform; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; + +public abstract class CanvasHelper extends CanvasContainer { + + protected Canvas canvas; + protected GraphicsContext gc; + + public CanvasHelper() { + this(null); + } + + /* A little hack to pass arguments in super() and also store them as field. Java is weird sometimes */ + private CanvasHelper(Canvas canvas) { + super(canvas = new Canvas()); + this.canvas = canvas; + gc = canvas.getGraphicsContext2D(); + } + + public final void repaint() { + if (Platform.isFxApplicationThread()) + render(); + else + Platform.runLater(this::render); + } + + protected abstract void render(); +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/DisplayFrustum.java b/src/main/java/togos/minecraft/maprend/gui/DisplayFrustum.java deleted file mode 100644 index bf414ac..0000000 --- a/src/main/java/togos/minecraft/maprend/gui/DisplayFrustum.java +++ /dev/null @@ -1,168 +0,0 @@ -package togos.minecraft.maprend.gui; - -import java.util.Observable; -import org.joml.AABBd; -import org.joml.Vector3d; -import org.joml.Vector3dc; -import javafx.animation.Interpolator; -import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; -import javafx.animation.Timeline; -import javafx.beans.binding.Bindings; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.scene.Node; -import javafx.util.Duration; - -/** - * An object of this class represents the viewport frustum of a {@link WorldRendererCanvas}. All units are in world coordinates by default, meaning one unit is - * equal to one block. - */ -public class DisplayFrustum { - - public static final int MAX_ZOOM_LEVEL = 7; // up to 9 without rounding errors - - /** The size of the viewport is needed to calculate the frustum's size in world coordinates. */ - public final DoubleProperty widthProperty = new SimpleDoubleProperty(800); - /** The size of the viewport is needed to calculate the frustum's size in world coordinates. */ - public final DoubleProperty heightProperty = new SimpleDoubleProperty(500); - - /** - * A linear scaling factor to zoom in or out on the map. Larger values mean zoom in, smaller values mean zoom out. Avoid changing this property directly - * because it leads to inconsistency between scale and zoom. Use {@link #zoomProperty} instead.
    - * A change in this value will scale the map to the new scaling value accordingly. The center of the scaling operation will be the mouse's current position. - * Then, {@link #frustumChanged()} is called. - */ - public final DoubleProperty scaleProperty = new SimpleDoubleProperty(1); - /** - * The zoom is unlike the scale not linear, but on an exponential basis. It is defined as scale=Math.pow(2, zoom). A zoom of zero means a scale - * of 1:1, each unit increase means zooming out by a factor of 2, a each unit decrease results in zooming in by a factor of 2.
    - * Each change of this value will result in {@link #scaleProperty} to be adjusted and the frustum to be recaltulated. - */ - public final DoubleProperty zoomProperty = new SimpleDoubleProperty(0); - /** - * Add a listener to get notified when the frustum has changed, which usually results in the need of redrawing the map to reflect the changes in the - * viewport. - */ - public Observable repaint = new Observable() { - - @Override - public synchronized void notifyObservers(Object arg) { - setChanged(); // Why the hell is this method protected? - super.notifyObservers(arg); - } - }; - - protected boolean dragging = false; - protected Vector3d mousePos = new Vector3d(); - protected Vector3d translation = new Vector3d(0, 0, 0); - protected Timeline zoomTimeline; - protected AABBd frustum; - - public DisplayFrustum() { - scaleProperty.bind(Bindings.createDoubleBinding(() -> Math.pow(2, zoomProperty.get()), zoomProperty)); - scaleProperty.addListener((e, oldScale, newScale) -> { - // In world coordinates - Vector3d cursorPos = new Vector3d(mousePos).div(oldScale.doubleValue()).sub(translation); - translation.add(cursorPos); - translation.div(newScale.doubleValue() / oldScale.doubleValue()); - translation.sub(cursorPos); - - frustumChanged(); - }); - - widthProperty.addListener(e -> frustumChanged()); - heightProperty.addListener(e -> frustumChanged()); - } - - /** - * Sets the new location of the mouse. - * - * @param x the new x-coordinate of the mouse, in local coordinates of the according canvas. - * @param y the new y-coordinate of the mouse, in local coordinates of the according canvas. - * @see Node#boundsInLocalProperty() - */ - public void mouseMove(double x, double y) { - Vector3d lastCursor = new Vector3d(mousePos); - mousePos.set(x, y, 0); - - if (dragging) { - // Difference in world space - Vector3d dragDist = new Vector3d(lastCursor).sub(mousePos).negate(); - double scale = scaleProperty.get(); - translation.add(new Vector3d(dragDist.x / scale, dragDist.y / scale, 0)); - frustumChanged(); - } - } - - /** - * Zooms in or out around the position currently shown by the mouse cursor. The zooming will be animated with an eased interpolation for 100 milliseconds. - * - * @param deltaZoom The amount of change to the zoom property. 1 means zoom out by a factor of 2, -1 means zoom in by a factor of 2. - * @see #zoomProperty - */ - public void mouseScroll(double deltaZoom) { - double currentValue = zoomProperty.get(); - double missingTime = 0; - if (zoomTimeline != null) { - missingTime = (zoomTimeline.getTotalDuration().toMillis() - zoomTimeline.getCurrentTime().toMillis()) / 2; - zoomTimeline.jumpTo("end"); - } - double scale = zoomProperty.get(); - zoomProperty.set(currentValue); - scale += deltaZoom; - if (scale > MAX_ZOOM_LEVEL) - scale = MAX_ZOOM_LEVEL; - if (scale < -MAX_ZOOM_LEVEL) - scale = -MAX_ZOOM_LEVEL; - zoomTimeline = new Timeline(new KeyFrame(Duration.millis(100 + missingTime), new KeyValue(zoomProperty, scale, Interpolator.EASE_BOTH))); - zoomTimeline.play(); - } - - /** Call this to tell if the mouse is currently dragging or not. */ - public void buttonPress(boolean pressed) { - dragging = pressed; - } - - /** - * Rebuilds the view frustum and calls for a repaint. This method is called automatically after changing any of the relevant values, but there might be the - * need of requesting a recalculation manually. - * - * @see #getFrustum() - * @see #repaint - */ - public void frustumChanged() { - frustum = new AABBd(// TODO optimize - new Vector3d(000, 000, 0).div(scaleProperty.get()).sub(translation.x, translation.y, 0), - new Vector3d(widthProperty.get() - 000, heightProperty.get() - 000, 0).div(scaleProperty.get()).sub(translation.x, translation.y, 0)); - // frustum = new AABBd(-Double.MAX_VALUE, -Double.MAX_VALUE, 0, Double.MAX_VALUE, Double.MAX_VALUE, 0); - // System.out.println(frustum); - repaint.notifyObservers(); - } - - /** - * Returns the currently visible part of the world. The coordinates are world coordinates, relative to the world's origin with one unit corresponding to one - * // * block. The vertical range of this AABBd (the z axis) is from zero to zero. This object is not immutable and should not be changed. - * {@link #frustumChanged()} replaces it with a new instance on every call. - */ - public AABBd getFrustum() { - return frustum; - } - - /** - * The zoom level is just a rounded version of {@link #zoomProperty}. The result can be interpreted as level of detail as per common definition. Note that - * when zoomed in this returns negative values. A "negative" LOD (increase of detail) is needed because JavaFX does not support nearest image interpolation - * to display the image correctly. - */ - public int getZoomLevel() { - return (int) Math.ceil(zoomProperty.get()); - } - - public Vector3dc getMousePos() { - return mousePos; - } - - public Vector3dc getTranslation() { - return translation; - } -} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java b/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java index 4ce0a37..645ae65 100644 --- a/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java +++ b/src/main/java/togos/minecraft/maprend/gui/DisplaySettings.java @@ -1,5 +1,7 @@ package togos.minecraft.maprend.gui; +import org.joml.AABBd; + public class DisplaySettings { // public int threadCount; // public int overDraw; @@ -8,6 +10,8 @@ public class DisplaySettings { // public int maxZoomInLevel; // public int maxZoomOutLevel; // public float loadingFrustum; + public AABBd viewRestrictionInner; + public AABBd viewRestrictionOuter; public DisplaySettings() { } diff --git a/src/main/java/togos/minecraft/maprend/gui/DisplayViewport.java b/src/main/java/togos/minecraft/maprend/gui/DisplayViewport.java new file mode 100644 index 0000000..8603cf5 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/DisplayViewport.java @@ -0,0 +1,148 @@ +package togos.minecraft.maprend.gui; + +import org.joml.AABBd; +import org.joml.Vector2d; +import org.joml.Vector2dc; +import org.joml.Vector3d; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.binding.Bindings; +import javafx.beans.property.*; +import javafx.util.Duration; + + +/** + * An object of this class represents the camera viewport of a {@link WorldRendererCanvas}. All units are in world coordinates by default, meaning one unit is + * equal to one block.
    + * Warning: The properties used here may use mutable objects. It is strongly discouraged to modify their inner state; treat them as immutable. The system + * does not check whether the values are mutable or not. Note that checking is important when reading as well as when writing the respecting property. Modifying + * objects will result in wrong calculations of the viewport and may lead to undefined behaviour on third-party components using this. + */ +public class DisplayViewport { + + // up to 9 without rounding errors + public static final int MAX_ZOOM_LEVEL = 7; + + /** The size of the viewport is needed to calculate the frustum's size in world coordinates. */ + public final DoubleProperty widthProperty = new SimpleDoubleProperty(800); + /** The size of the viewport is needed to calculate the frustum's size in world coordinates. */ + public final DoubleProperty heightProperty = new SimpleDoubleProperty(500); + + /** + * A linear scaling factor to zoom in or out on the map. Larger values mean zoom in, smaller values mean zoom out. Avoid changing this property directly + * because it leads to inconsistency between scale and zoom. Use {@link #zoomProperty} instead.
    + * A change in this value will scale the map to the new scaling value accordingly. The center of the scaling operation will be the mouse's current position. + */ + public final DoubleProperty scaleProperty = new SimpleDoubleProperty(1); + /** + * The zoom is unlike the scale not linear, but on an exponential basis. It is defined as scale=Math.pow(2, zoom). A zoom of zero means a scale + * of 1:1, each unit increase means zooming out by a factor of 2, a each unit decrease results in zooming in by a factor of 2. + */ + public final DoubleProperty zoomProperty = new SimpleDoubleProperty(0); + + /** The current mouse position in local coordinates. Used for calculations when zooming around that point. */ + public final ObjectProperty mousePosProperty = new SimpleObjectProperty<>(new Vector2d()); + private final ReadOnlyObjectWrapper mouseWorld = new ReadOnlyObjectWrapper<>(new Vector2d()); + /** The current mouse position in world coordinates (1 unit == 1 block). */ + public final ReadOnlyObjectProperty mouseWorldProperty = mouseWorld.getReadOnlyProperty(); + /** The translation of the map relative to its origin, in blocks/pixels. */ + public final ObjectProperty translationProperty = new SimpleObjectProperty<>(new Vector2d()); + private final ReadOnlyObjectWrapper frustum = new ReadOnlyObjectWrapper<>(new AABBd()); + /** + * The camera frustum of this viewport. It represents the area of the world visible to the current camera. Translating, zooming or resizing the map result + * in an update of this object. + */ + public final ReadOnlyObjectProperty frustumProperty = frustum.getReadOnlyProperty(); + protected Timeline zoomTimeline; + + public DisplayViewport() { + scaleProperty.bind(Bindings.createDoubleBinding(() -> Math.pow(2, zoomProperty.get()), zoomProperty)); + scaleProperty.addListener((e, oldScale, newScale) -> { + // In world coordinates + Vector2d cursorPos = new Vector2d(mousePosProperty.get()).mul(1d / oldScale.doubleValue()).sub(translationProperty.get()); + Vector2d translation = new Vector2d(this.translationProperty.get()); + translation.add(cursorPos); + translation.mul(oldScale.doubleValue() / newScale.doubleValue()); + translation.sub(cursorPos); + this.translationProperty.set(translation); + }); + + // Use bindings because they are executed lazily + frustum.bind(Bindings.createObjectBinding(() -> { + Vector2dc translation = this.translationProperty.get(); + return new AABBd(// TODO optimise + new Vector3d(-translation.x(), -translation.y(), 0), + new Vector3d(widthProperty.get(), heightProperty.get(), 0).div(scaleProperty.get()).sub(translation.x(), translation.y(), 0)); + }, translationProperty, widthProperty, heightProperty)); + mouseWorld.bind(Bindings.createObjectBinding(() -> mousePosProperty.get().mul(1d / scaleProperty.get(), new Vector2d()).sub(translationProperty.get()), mousePosProperty, scaleProperty)); + } + + /** + * Offsets the view of the map by the given amount of pixels in the respective direction. + * + * @param dx how many pixels the map should move horizontally + * @param dy how many pixels the map should move vertically + */ + public void pan(double dx, double dy) { + double scale = scaleProperty.get(); + translationProperty.set(new Vector2d(dx / scale, dy / scale).add(translationProperty.get())); + } + + /** + * Zooms in or out around the position currently shown by the mouse cursor. The zooming will be animated with an eased interpolation for 100 milliseconds. + * The resulting scale is clamped to [-MAX_ZOOM_LEVEL, MAX_ZOOM_LEVEL]. If called while the animation from a previous call has not finished, + * that animation is stopped in its current state and both are combined to a new animation that is slightly faster than executing both animations + * sequentially. + * + * @param deltaZoom The amount of change to the zoom property. -1 means zoom out by a factor of 2, 1 means zoom in by a factor of 2. + * @see #zoomProperty + */ + public void mouseScroll(double deltaZoom) { + double currentValue = zoomProperty.get(); + double missingTime = 0; + if (zoomTimeline != null) { + // Divide by 2 to fasten things up + missingTime = (zoomTimeline.getTotalDuration().toMillis() - zoomTimeline.getCurrentTime().toMillis()) / 2; + zoomTimeline.jumpTo("end"); + } + double scale = zoomProperty.get(); + zoomProperty.set(currentValue); + scale += deltaZoom; + // Clamp + if (scale > MAX_ZOOM_LEVEL) + scale = MAX_ZOOM_LEVEL; + if (scale < -MAX_ZOOM_LEVEL) + scale = -MAX_ZOOM_LEVEL; + zoomTimeline = new Timeline(new KeyFrame(Duration.millis(100 + missingTime), new KeyValue(zoomProperty, scale, Interpolator.EASE_BOTH))); + zoomTimeline.play(); + } + + /** + * Returns the currently visible part of the world. The coordinates are world coordinates, relative to the world's origin with one unit corresponding to one + * block. The vertical range of this AABBd (the z axis) is from zero to zero. This object is not immutable and should not be changed. + */ + public AABBd getFrustum() { + return frustum.get(); + } + + /** + * The zoom level is a rounded version of {@link #zoomProperty}. Use this to control the level of detail used for MIP-mapping. + */ + public int getZoomLevel() { + return (int) Math.ceil(zoomProperty.get()); + } + + public Vector2dc getMousePos() { + return mousePosProperty.get(); + } + + public Vector2dc getMouseInWorld() { + return mouseWorldProperty.get(); + } + + public Vector2dc getTranslation() { + return translationProperty.get(); + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/MapPane.java b/src/main/java/togos/minecraft/maprend/gui/MapPane.java index 7bcd0af..5377ad0 100644 --- a/src/main/java/togos/minecraft/maprend/gui/MapPane.java +++ b/src/main/java/togos/minecraft/maprend/gui/MapPane.java @@ -39,6 +39,6 @@ public MapPane(WorldRendererCanvas renderer) { catchEvents.setPickOnBounds(true); catchEvents.addEventFilter(EventType.ROOT, event -> decorationLayers.forEach(l1 -> l1.fireEvent(event))); - decorationLayers.add(new WorldRendererContainer(renderer)); + decorationLayers.add(new CanvasContainer(renderer)); } } \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java b/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java index 739563a..a8fb128 100644 --- a/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java +++ b/src/main/java/togos/minecraft/maprend/gui/RenderedMap.java @@ -75,8 +75,8 @@ public WritableImage deserialize(DataInput2 input, int available) throws IOExcep private final HTreeMap cacheMapDisk, cacheMapDiskMem, cacheMapMem; - private Map plainRegions = new HashMap<>(); - private Map> regions = new HashMap<>(); + private Map plainRegions = new HashMap<>(); + private Map> regions = new HashMap<>(); @SuppressWarnings("unchecked") public RenderedMap(ScheduledExecutorService executor) { @@ -156,7 +156,7 @@ public boolean isNothingLoaded() { } public void draw(GraphicsContext gc, int level, AABBd frustum, double scale) { - Map map = get(level > 0 ? 0 : level); + Map map = get(level > 0 ? 0 : level); gc.setFill(new Color(0.3f, 0.3f, 0.9f, 1.0f)); // Background color plainRegions.values() .stream() @@ -213,20 +213,20 @@ public boolean updateImage(int level, AABBd frustum) { } } - public Map get(int level) { + public Map get(int level) { // if (new Error().getStackTrace().length > 180) // Sometimes, this throws a StackOverflowError, but Eclipse doesn't show when this method got called before the recursion in the stack trace. This // is to catch the error prematurely in the hope of getting a full stack trace // TODO this is really bad for performance, remove ASAP the bug is found and fixed // throw new InternalError("Stack overflow."); - Map ret = regions.get(level); + Map ret = regions.get(level); try { // the bug might be when requesting values on level 0 that are null (not calculated) if (ret == null && level != 0) { - Map ret2 = new HashMap(); + Map ret2 = new HashMap<>(); ret = ret2; // the bug might be to using abovePos() regardless of the level being zoomed in or out - get(level < 0 ? level + 1 : level - 1).keySet().stream().map(RenderedMap::abovePos).distinct().forEach(v -> ret2.put(v, null)); + get(level < 0 ? level + 1 : level - 1).keySet().stream().map(RenderedMap::abovePos).map(v -> new ImmutableVector2i(v)).distinct().forEach(v -> ret2.put(v, null)); regions.put(level, ret); } @@ -251,9 +251,9 @@ protected boolean isImageLoaded(Vector3ic key) { return !unloaded.contains(key); } - public RenderedRegion get(int level, Vector2ic position, boolean create) { - Map map = get(level); - RenderedRegion r = map.get(position); + public RenderedRegion get(int level, ImmutableVector2i position, boolean create) { + Map map = get(level); + RenderedRegion r = map.get(new ImmutableVector2i(position)); if (create && r == null && level != 0 && ((level > 0 /* && plainRegions.containsKey(new Vector2i(position.x() >> level, position.y() >> * level).toImmutable()) */) || map.containsKey(position))) { r = new RenderedRegion(this, level, position); @@ -266,28 +266,28 @@ public RenderedRegion get(int level, Vector2ic position, boolean create) { return r; } - public RenderedRegion[] get(int level, Vector2ic[] belowPos, boolean create) { + public RenderedRegion[] get(int level, ImmutableVector2i[] belowPos, boolean create) { RenderedRegion[] ret = new RenderedRegion[belowPos.length]; for (int i = 0; i < belowPos.length; i++) ret[i] = get(level, belowPos[i], create); return ret; } - public static Vector2i abovePos(Vector2ic pos) { + public static ImmutableVector2i abovePos(Vector2ic pos) { return groundPos(pos, 1); } - public static Vector2i groundPos(Vector2ic pos, int levelDiff) { - return new Vector2i(pos.x() >> levelDiff, pos.y() >> levelDiff); + public static ImmutableVector2i groundPos(Vector2ic pos, int levelDiff) { + return new ImmutableVector2i(pos.x() >> levelDiff, pos.y() >> levelDiff); } - public static Vector2i[] belowPos(Vector2ic pos) { - Vector2i belowPos = new Vector2i(pos.x() << 1, pos.y() << 1); - return new Vector2i[] { - new Vector2i(0, 0).add(belowPos), - new Vector2i(1, 0).add(belowPos), - new Vector2i(0, 1).add(belowPos), - new Vector2i(1, 1).add(belowPos) + public static ImmutableVector2i[] belowPos(Vector2ic pos) { + ImmutableVector2i belowPos = new ImmutableVector2i(pos.x() << 1, pos.y() << 1); + return new ImmutableVector2i[] { + new ImmutableVector2i(0, 0).add(belowPos), + new ImmutableVector2i(1, 0).add(belowPos), + new ImmutableVector2i(0, 1).add(belowPos), + new ImmutableVector2i(1, 1).add(belowPos) }; } diff --git a/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java b/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java index 55e0cbf..fa581fd 100644 --- a/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java +++ b/src/main/java/togos/minecraft/maprend/gui/RenderedRegion.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import org.joml.AABBd; +import org.joml.ImmutableVector2i; import org.joml.Vector2i; import org.joml.Vector2ic; import javafx.scene.canvas.GraphicsContext; @@ -29,17 +30,17 @@ public boolean isInvalid() { // public final AtomicBoolean invalid = new AtomicBoolean(true); protected final RenderedImage image; public final int level; - public final Vector2ic position; - public final AtomicReference valid = new AtomicReference(RenderingState.INVALID); + public final ImmutableVector2i position; + public final AtomicReference valid = new AtomicReference<>(RenderingState.INVALID); public Region region; public RenderedRegion(RenderedMap map, Region region) { - this(map, 0, new Vector2i(region.rx, region.rz).toImmutable()); + this(map, 0, new ImmutableVector2i(region.rx, region.rz)); this.region = region; } - public RenderedRegion(RenderedMap map, int level, Vector2ic position) { + public RenderedRegion(RenderedMap map, int level, ImmutableVector2i position) { this.map = map; this.level = level; this.position = position; @@ -90,10 +91,11 @@ public boolean updateImage() { if (level != 0 && (image == null || valid.get().isInvalid())) { if (level > 0) { - // check above // get above image RenderedRegion above = getGround(true); + if (above == null) + System.out.println(getGround(true)); WritableImage aboveImage = above.getImage(true); // upscale image if (aboveImage != null) diff --git a/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java b/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java index f68180c..3f4eda9 100644 --- a/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java +++ b/src/main/java/togos/minecraft/maprend/gui/WorldRendererCanvas.java @@ -7,10 +7,8 @@ import java.util.concurrent.Future; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.joml.Vector2dc; import org.joml.Vector3d; -import org.joml.Vector3dc; -import com.sun.glass.ui.Application; -import com.sun.glass.ui.Robot; import javafx.application.Platform; import javafx.embed.swing.SwingFXUtils; import javafx.scene.canvas.Canvas; @@ -24,7 +22,6 @@ import togos.minecraft.maprend.gui.RenderedRegion.RenderingState; import togos.minecraft.maprend.io.RegionFile; -@SuppressWarnings("restriction") public class WorldRendererCanvas extends Canvas implements Runnable { public static final int THREAD_COUNT = 4; @@ -36,9 +33,8 @@ public class WorldRendererCanvas extends Canvas implements Runnable { protected final List> submitted = Collections.synchronizedList(new LinkedList<>()); protected GraphicsContext gc = getGraphicsContext2D(); - protected Robot robot = Application.GetApplication().createRobot(); - public final DisplayFrustum frustum = new DisplayFrustum(); + public final DisplayViewport viewport = new DisplayViewport(); public WorldRendererCanvas(RegionRenderer renderer) { this.renderer = Objects.requireNonNull(renderer); @@ -50,7 +46,7 @@ public WorldRendererCanvas(RegionRenderer renderer) { try { // TODO execute more often if something changes and less often if not // update upscaled/downscaled images of chunks - if (map.updateImage(frustum.getZoomLevel(), frustum.getFrustum())) + if (map.updateImage(viewport.getZoomLevel(), viewport.getFrustum())) repaint(); } catch (Throwable e) { e.printStackTrace(); @@ -63,19 +59,14 @@ public WorldRendererCanvas(RegionRenderer renderer) { executor.allowCoreThreadTimeOut(true); } - frustum.widthProperty.bind(widthProperty()); - frustum.heightProperty.bind(heightProperty()); + viewport.widthProperty.bind(widthProperty()); + viewport.heightProperty.bind(heightProperty()); invalidateTextures(); - frustum.frustumChanged(); - frustum.repaint.addObserver((o, a) -> repaint()); + viewport.frustumProperty.addListener(e -> repaint()); repaint(); } public void loadWorld(File file) { - // regions.clear(); - // regions = RegionMap.load(file, BoundingRect.INFINITE).regions.stream().collect(Collectors.toMap(r -> new Vector2i(r.rx, r.rz), r -> new - // RenderedRegion2(r))); - // map.clear(); map.clearReload(RegionMap.load(file, BoundingRect.INFINITE).regions); invalidateTextures(); } @@ -113,13 +104,13 @@ public void renderWorld() { gc.setFill(new Color(0.2f, 0.2f, 0.6f, 1.0f)); gc.fillRect(0, 0, getWidth(), getHeight()); - double scale = frustum.scaleProperty.get(); + double scale = viewport.scaleProperty.get(); gc.save(); gc.scale(scale, scale); - Vector3dc translation = frustum.getTranslation(); + Vector2dc translation = viewport.getTranslation(); gc.translate(translation.x(), translation.y()); - map.draw(gc, frustum.getZoomLevel(), frustum.getFrustum(), scale); + map.draw(gc, viewport.getZoomLevel(), viewport.getFrustum(), scale); gc.restore(); if (map.isNothingLoaded()) { @@ -165,7 +156,7 @@ public void run() { /** Returns the next Region to render */ protected synchronized RenderedRegion nextRegion() { // In region coordinates - Vector3d cursorPos = new Vector3d(frustum.getMousePos()).div(frustum.scaleProperty.get()).sub(frustum.getTranslation()).div(512).sub(.5, .5, 0); + Vector3d cursorPos = new Vector3d(viewport.getMouseInWorld(), 0).div(512).sub(.5, .5, 0); Comparator comp = (a, b) -> Double.compare(new Vector3d(a.position.x(), a.position.y(), 0).sub(cursorPos).length(), new Vector3d(b.position.x(), b.position.y(), 0).sub(cursorPos).length()); RenderedRegion min = null; @@ -175,7 +166,7 @@ protected synchronized RenderedRegion nextRegion() { if (min != null) // min got handled by another Thread already (while we were still searching), so get a new one if (!min.valid.compareAndSet(RenderingState.INVALID, RenderingState.DRAWING)) - return nextRegion(); + return nextRegion(); return min; } diff --git a/src/main/java/togos/minecraft/maprend/gui/WorldRendererContainer.java b/src/main/java/togos/minecraft/maprend/gui/WorldRendererContainer.java deleted file mode 100644 index 6072568..0000000 --- a/src/main/java/togos/minecraft/maprend/gui/WorldRendererContainer.java +++ /dev/null @@ -1,33 +0,0 @@ -package togos.minecraft.maprend.gui; - -import java.util.Objects; -import javafx.scene.canvas.Canvas; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.BorderPane; - -/** - *

    - * Wraps a {@link WorldRendererCanvas} into an AnchorPane and a BorderPane. This is because a {@link Canvas} such as the renderer needs to have a preferred size - * set to properly work in many layouts. This wrapper will make sure that the canvas will always use exactly as much of the space available - no less and no - * more. It is recommended to use this class in most layout situations. - *

    - */ -public class WorldRendererContainer extends AnchorPane { - - public final WorldRendererCanvas renderer; - - public WorldRendererContainer(WorldRendererCanvas renderer) { - this.renderer = Objects.requireNonNull(renderer); - - BorderPane wrapper = new BorderPane(renderer); - wrapper.setStyle("-fx-background-color: red"); - wrapper.setPickOnBounds(true); - renderer.widthProperty().bind(wrapper.widthProperty()); - renderer.heightProperty().bind(wrapper.heightProperty()); - AnchorPane.setTopAnchor(wrapper, 0D); - AnchorPane.setBottomAnchor(wrapper, 0D); - AnchorPane.setLeftAnchor(wrapper, 0D); - AnchorPane.setRightAnchor(wrapper, 0D); - getChildren().add(wrapper); - } -} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java b/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java index 6a709b9..5d210ca 100644 --- a/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java +++ b/src/main/java/togos/minecraft/maprend/gui/decoration/DragScrollDecoration.java @@ -1,39 +1,67 @@ package togos.minecraft.maprend.gui.decoration; +import org.joml.Vector2d; +import org.joml.Vector2dc; +import com.sun.glass.ui.Application; +import com.sun.glass.ui.Robot; import javafx.scene.input.MouseButton; import javafx.scene.layout.Region; -import togos.minecraft.maprend.gui.DisplayFrustum; +import togos.minecraft.maprend.gui.DisplayViewport; /** * This decoration provides basic drag and zoom support, where you can set the button used for dragging as well as zoom speed and direction. Both - * functionalities are optional and can be disabled separatedly. If you disable both, this class will still forward mouse moved events to the view frustum. This + * functionalities are optional and can be disabled separately. If you disable both, this class will still forward mouse moved events to the view frustum. This * might be useful if you use manual zooming logic externally and still want to zoom around the mouse center as it does normally. */ +@SuppressWarnings("restriction") public class DragScrollDecoration extends Region { + protected Robot robot = Application.GetApplication().createRobot(); + protected int cooldown; + /** Creates an instance of this class that will drag with the right mouse button and a scroll factor of 1/10. */ - public DragScrollDecoration(DisplayFrustum frustum) { + public DragScrollDecoration(DisplayViewport frustum) { this(frustum, MouseButton.SECONDARY, 0.1d); } + public DragScrollDecoration(DisplayViewport frustum, boolean allowDrag, boolean allowZoom) { + this(frustum, allowDrag ? MouseButton.SECONDARY : null, allowZoom ? 0.1d : 0); + } + /** * @param dragButton The button that must be pressed to activate dragging. null will disable dragging. * @param scrollFactor Higher values will increase the zoomed amount per scroll. Zero deactivates scrolling. Negative values invert the scroll direction. */ - public DragScrollDecoration(DisplayFrustum frustum, MouseButton dragButton, double scrollFactor) { - setOnMouseMoved(e -> frustum.mouseMove(e.getX(), e.getY())); - setOnMouseDragged(e -> frustum.mouseMove(e.getX(), e.getY())); + public DragScrollDecoration(DisplayViewport frustum, MouseButton dragButton, double scrollFactor) { + setOnMouseMoved(e -> frustum.mousePosProperty.set(new Vector2d(e.getX(), e.getY()))); + setOnMouseDragged(e -> { + Vector2dc old = frustum.mousePosProperty.get(); + Vector2d current = new Vector2d(e.getX(), e.getY()); + if (cooldown <= 0) { + if (dragButton != null && e.getButton() == dragButton) { + frustum.pan(current.x() - old.x(), current.y() - old.y()); + double width = getWidth(); + double height = getHeight(); + double dx = 0, dy = 0; + if (current.x() < 1) + dx = width - 3; + if (current.x() > width - 2) + dx = -width + 4; + if (current.y() < 1) + dy = height - 3; + if (current.y() > height - 2) + dy = -height + 4; + if (dx != 0 || dy != 0) { + current.add(dx, dy); + robot.mouseMove((int) (e.getScreenX() + dx), (int) (e.getScreenY() + dy)); + cooldown = 3; + } + } + } else if (cooldown > 0) + cooldown--; + frustum.mousePosProperty.set(current); + }); - if (dragButton != null) { - setOnMousePressed(e -> { - if (e.getButton().ordinal() == dragButton.ordinal()) - frustum.buttonPress(true); - }); - setOnMouseReleased(e -> { - if (e.getButton().ordinal() == dragButton.ordinal()) - frustum.buttonPress(false); - }); - } if (scrollFactor != 0) setOnScroll(e -> frustum.mouseScroll(e.getTextDeltaY() * scrollFactor)); } diff --git a/src/main/java/togos/minecraft/maprend/gui/decoration/SelectRectangleDecoration.java b/src/main/java/togos/minecraft/maprend/gui/decoration/SelectRectangleDecoration.java new file mode 100644 index 0000000..86ad633 --- /dev/null +++ b/src/main/java/togos/minecraft/maprend/gui/decoration/SelectRectangleDecoration.java @@ -0,0 +1,70 @@ +package togos.minecraft.maprend.gui.decoration; + +import java.util.Objects; +import org.joml.Rectangled; +import org.joml.Vector2d; +import org.joml.Vector2dc; +import javafx.scene.input.MouseButton; +import javafx.scene.paint.Color; +import togos.minecraft.maprend.gui.CanvasHelper; +import togos.minecraft.maprend.gui.DisplayViewport; + +public class SelectRectangleDecoration extends CanvasHelper { + + protected final DisplayViewport frustum; + + protected final Vector2d dragStart = new Vector2d(), dragEnd = new Vector2d(); + + public SelectRectangleDecoration(DisplayViewport frustum, Rectangled initialSelection) { + this(frustum, MouseButton.PRIMARY, initialSelection); + } + + public SelectRectangleDecoration(DisplayViewport frustum, MouseButton selectButton, Rectangled initialSelection) { + this.frustum = Objects.requireNonNull(frustum); + Objects.requireNonNull(selectButton); + frustum.frustumProperty.addListener(e -> repaint()); + + if (initialSelection == null) + initialSelection = new Rectangled(); + dragStart.set(initialSelection.minX, initialSelection.minY); + dragEnd.set(initialSelection.maxX, initialSelection.maxY); + + setOnMousePressed(e -> { + if (e.getButton() == selectButton) { + dragStart.set(frustum.getMouseInWorld()); + dragEnd.set(dragStart); + repaint(); + } + }); + setOnMouseDragged(e -> { + if (e.getButton() == selectButton) + dragEnd.set(frustum.getMouseInWorld()); + repaint(); + }); + gc.setLineWidth(2); + gc.setFill(Color.BLUE.deriveColor(-35, .6, 1.3, 0.5)); + gc.setStroke(Color.BLUE.deriveColor(0, 0.8, 1.3, 0.8)); + } + + @Override + public void render() { + gc.clearRect(0, 0, getWidth(), getHeight()); + + double scale = frustum.scaleProperty.get(); + gc.save(); + gc.scale(scale, scale); + Vector2dc translation = frustum.getTranslation(); + gc.translate(translation.x(), translation.y()); + + gc.fillRect(Math.min(dragStart.x, dragEnd.x), Math.min(dragStart.y, dragEnd.y), Math.abs(dragStart.x - dragEnd.x), Math.abs(dragStart.y - + dragEnd.y)); + gc.strokeRect(Math.min(dragStart.x, dragEnd.x), Math.min(dragStart.y, dragEnd.y), Math.abs(dragStart.x - dragEnd.x), Math.abs(dragStart.y - + dragEnd.y)); + + gc.restore(); + } + + public Rectangled getSelected() { + return new Rectangled(Math.min(dragStart.x, dragEnd.x), Math.min(dragStart.y, dragEnd.y), Math.max(dragStart.x, dragEnd.x), Math.max(dragStart.y, dragEnd.y)); + } +} \ No newline at end of file diff --git a/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java b/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java index c290500..fe66207 100644 --- a/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java +++ b/src/main/java/togos/minecraft/maprend/guistandalone/GuiController.java @@ -57,7 +57,7 @@ public void initialize(URL location, ResourceBundle resources) { } catch (IOException e1) { e1.printStackTrace(); } - pane.decorationLayers.add(new DragScrollDecoration(renderer.frustum)); + pane.decorationLayers.add(new DragScrollDecoration(renderer.viewport)); pane.settingsLayers.add(new SettingsOverlay(renderer)); } From 1409e726a69a46470de0e1e76ea41919c6ce6e35 Mon Sep 17 00:00:00 2001 From: Unknown <14054505+piegamesde@users.noreply.github.com> Date: Fri, 17 Aug 2018 12:08:30 +0200 Subject: [PATCH 7/7] Update block colors to 1.13 --- .../resources/block-color-instructions.json | 828 ++++++++++++++++++ 1 file changed, 828 insertions(+) create mode 100644 src/main/resources/block-color-instructions.json diff --git a/src/main/resources/block-color-instructions.json b/src/main/resources/block-color-instructions.json new file mode 100644 index 0000000..935f7b7 --- /dev/null +++ b/src/main/resources/block-color-instructions.json @@ -0,0 +1,828 @@ +{ + "__comment": "Map a block id + state to a color. The each key represents a block state, with the different properties appended to the block's name separated via comma. A star is used as wildcard for all possible values of a property. The value is an array, where the first element denotes the mode (transparent, texture or fixed), the second one (if needed) tells the texture or the color and the optional third entry may be 'foliage', 'grass' or 'water' for tinting. If the value is just a string, this is a shortcut for a simple texture without tinting.", + + "air": ["fixed", "0x00000000"], + + "acacia_button,face=*,facing=*,powered=*": ["transparent"], + "acacia_door,facing=*,half=*,hinge=*,open=*": "acacia_door_top.png", + "acacia_fence,north=*,south=*,east=*,west=*": ["fixed", "TODO"], + "acacia_fence_gate,facing=*,in_wall=*,open=*": ["fixed", "TODO"], + "acacia_leaves": ["texture", "acacia_leaves.png", "foliage"], + "acacia_log,axis=y": "acacia_log_top.png", + "acacia_log,axis=z": "acacia_log.png", + "acacia_log,axis=x": "acacia_log.png", + "acacia_planks": "acacia_planks.png", + "acacia_pressure_plate,powered=*": "acacia_planks.png", + "acacia_sapling": "acacia_sapling.png", + "acacia_slab,type=*,": "acacia_planks.png", + "acacia_stairs,facing=*,half=*,shape=*": "acacia_planks.png", + "acacia_trapdoor,open=false,half=*,facing=*": "acacia_trapdoor.png", + "acacia_trapdoor,open=true,half=*,facing=*": ["transparent"], + "acacia_wood,axis=*": "acacia_log.png", + + "activator_rail,powered=false,shape=*,": "activator_rail.png", + "activator_rail,powered=true,shape=*,": "activator_rail_on.png", + "allium": "allium.png", + "andesite": "andesite.png", + "anvil,facing=*": "anvil_top.png", + "attached_melon_stem,facing=*": ["transparent"], + "attached_pumpkin_stem,facing=*": ["transparent"], + "azure_bluet": "azure_bluet.png", + "barrier": ["fixed", "0x00FF0000"], + "beacon": "beacon.png", + "bedrock": "bedrock.png", + "beetroots,age=0": "beetroots_stage0.png", + "beetroots,age=1": "beetroots_stage1.png", + "beetroots,age=2": "beetroots_stage2.png", + "beetroots,age=3": "beetroots_stage3.png", + + "birch_button,face=*,facing=*,powered=*": ["transparent"], + "birch_door,facing=*,half=*,hinge=*,open=*": "birch_door_top.png", + "birch_fence,north=*,south=*,east=*,west=*": ["fixed", "TODO"], + "birch_fence_gate,facing=*,in_wall=*,open=*": ["fixed", "TODO"], + "birch_leaves": ["texture", "birch_leaves.png", "foliage"], + "birch_log,axis=y": "birch_log_top.png", + "birch_log,axis=z": "birch_log.png", + "birch_log,axis=x": "birch_log.png", + "birch_planks": "birch_planks.png", + "birch_pressure_plate,powered=*": "birch_planks.png", + "birch_sapling": "birch_sapling.png", + "birch_slab,type=*,": "birch_planks.png", + "birch_stairs,facing=*,half=*,shape=*": "birch_planks.png", + "birch_trapdoor,open=false,half=*,facing=*": "birch_trapdoor.png", + "birch_trapdoor,open=true,half=*,facing=*": ["transparent"], + "birch_wood,axis=*": "birch_log.png", + + "black_banner": ["transparent"], + "black_bed": ["transparent"], + "black_carpet": "black_wool.png", + "black_concrete": "black_concrete.png", + "black_concrete_powder": "black_concrete_powder.png", + "black_glazed_terracotta,facing=*": "black_glazed_terracotta.png", + "black_shulker_box": "black_shulker_box.png", + "black_stained_glass": "black_stained_glass.png", + "black_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "black_terracotta": "black_terracotta.png", + "black_wall_banner": ["transparent"], + "black_wool": "black_wool.png", + + "blue_banner": ["transparent"], + "blue_bed": ["transparent"], + "blue_carpet": "blue_wool.png", + "blue_concrete": "blue_concrete.png", + "blue_concrete_powder": "blue_concrete_powder.png", + "blue_glazed_terracotta,facing=*": "blue_glazed_terracotta.png", + "blue_ice": "blue_ice.png", + "blue_orchid": "blue_orchid.png", + "blue_shulker_box": "blue_shulker_box.png", + "blue_stained_glass": "blue_stained_glass.png", + "blue_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "blue_terracotta": "blue_terracotta.png", + "blue_wall_banner": ["transparent"], + "blue_wool": "blue_wool.png", + + "bone_block,axis=y": "bone_block_top.png", + "bone_block,axis=x": "bone_block_side.png", + "bone_block,axis=z": "bone_block_side.png", + "bookshelf": "oak_planks.png", + + "brain_coral": "brain_coral.png", + "brain_coral_block": "brain_coral_block.png", + "brain_coral_fan": "brain_coral_fan.png", + "brain_coral_wall_fan,facing=*": "brain_coral_fan.png", + + "brewing_stand,has_bottle_0=*,has_bottle_1=*,has_bottle_2=*": "stone.png", + "bricks": "bricks.png", + "brick_slab,type=*": "bricks.png", + "brick_stairs,facing=*,half=*,shape=*": "bricks.png", + + "brown_banner": ["transparent"], + "brown_bed": ["transparent"], + "brown_carpet": "brown_wool.png", + "brown_concrete": "brown_concrete.png", + "brown_concrete_powder": "brown_concrete_powder.png", + "brown_glazed_terracotta,facing=*": "brown_glazed_terracotta.png", + "brown_mushroom": "brown_mushroom.png", + "brown_mushroom_block": "brown_mushroom_block.png", + "brown_shulker_box": "brown_shulker_box.png", + "brown_stained_glass": "brown_stained_glass.png", + "brown_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "brown_terracotta": "brown_terracotta.png", + "brown_wall_banner": ["transparent"], + "brown_wool": "brown_wool.png", + + "bubble_column": ["texture", "water_flow.png", "water"], + "bubble_coral": "bubble_coral.png", + "bubble_coral_block": "bubble_coral_block.png", + "bubble_coral_fan": "bubble_coral_fan.png", + "bubble_coral_wall_fan,facing=*": "bubble_coral_fan.png", + + "cactus": "cactus_top.png", + "cake,bites=*": "cake_top.png", + "carrots,age=0": "carrots_stage0.png", + "carrots,age=1": "carrots_stage0.png", + "carrots,age=2": "carrots_stage1.png", + "carrots,age=3": "carrots_stage1.png", + "carrots,age=4": "carrots_stage2.png", + "carrots,age=5": "carrots_stage2.png", + "carrots,age=6": "carrots_stage3.png", + "carrots,age=7": "carrots_stage3.png", + "carved_pumpkin": "pumpkin_top.png", + "cauldron,level=*": "cauldron_inner.png", + + "cave_air": ["fixed", "0x00000000"], + + "chain_command_block,conditional=false,facing=down": "chain_command_block_back.png", + "chain_command_block,conditional=false,facing=up": "chain_command_block_front.png", + "chain_command_block,conditional=false,facing=north": "chain_command_block_side.png", + "chain_command_block,conditional=false,facing=south": "chain_command_block_side.png", + "chain_command_block,conditional=false,facing=east": "chain_command_block_side.png", + "chain_command_block,conditional=false,facing=west": "chain_command_block_side.png", + "chain_command_block,conditional=true,facing=down": "chain_command_block_back.png", + "chain_command_block,conditional=true,facing=up": "chain_command_block_front.png", + "chain_command_block,conditional=true,facing=north": "chain_command_block_conditional.png", + "chain_command_block,conditional=true,facing=south": "chain_command_block_conditional.png", + "chain_command_block,conditional=true,facing=east": "chain_command_block_conditional.png", + "chain_command_block,conditional=true,facing=west": "chain_command_block_conditional.png", + "chest": ["fixed", "TODO"], + "chipped_anvil,facing=*": "chipped_anvil_top.png", + "chiseled_quartz_block": "chiseled_quartz_block_top.png", + "chiseled_red_sandstone": "red_sandstone_top.png", + "chiseled_sandstone": "sandstone_top.png", + "chiseled_stone_bricks": "chiseled_stone_bricks.png", + "chorus_flower,age=0": "chorus_flower.png", + "chorus_flower,age=1": "chorus_flower.png", + "chorus_flower,age=2": "chorus_flower.png", + "chorus_flower,age=3": "chorus_flower.png", + "chorus_flower,age=4": "chorus_flower.png", + "chorus_flower,age=5": "chorus_flower_dead.png", + "clay": "clay.png", + "coal_block": "coal_block.png", + "coal_ore": "coal_ore.png", + "coarse_dirt": "coarse_dirt.png", + "cobblestone": "cobblestone.png", + "cobblestone_slab,type=*": "cobblestone.png", + "cobblestone_stairs,facing=*,half=*,shape=*": "cobblestone.png", + "cobblestone_wall,up=*,north=*,south=*,west=*,east=*": "cobblestone.png", + "cobweb": "cobweb.png", + "cocoa,age=0,facing=*": "cocoa_stage0.png", + "cocoa,age=1,facing=*": "cocoa_stage1.png", + "cocoa,age=2,facing=*": "cocoa_stage2.png", + + "command_block,conditional=false,facing=down": "command_block_back.png", + "command_block,conditional=false,facing=up": "command_block_front.png", + "command_block,conditional=false,facing=north": "command_block_side.png", + "command_block,conditional=false,facing=south": "command_block_side.png", + "command_block,conditional=false,facing=east": "command_block_side.png", + "command_block,conditional=false,facing=west": "command_block_side.png", + "command_block,conditional=true,facing=down": "command_block_back.png", + "command_block,conditional=true,facing=up": "command_block_front.png", + "command_block,conditional=true,facing=north": "command_block_conditional.png", + "command_block,conditional=true,facing=south": "command_block_conditional.png", + "command_block,conditional=true,facing=east": "command_block_conditional.png", + "command_block,conditional=true,facing=west": "command_block_conditional.png", + + "comparator,powered=false,facing=*,mode=*": "comparator.png", + "comparator,powered=true,facing=*,mode=*": "comparator_on.png", + "conduit": "conduit.png", + "cracked_stone_bricks": "cracked_stone_bricks.png", + "crafting_table": "crafting_table_top.png", + "creeper_head": ["transparent"], + "creeper_wall_head": ["transparent"], + "cut_red_sandstone": "red_sandstone_top.png", + "cut_sandstone": "sandstone_top.png", + + "cyan_banner": ["transparent"], + "cyan_bed": ["transparent"], + "cyan_carpet": "cyan_wool.png", + "cyan_concrete": "cyan_concrete.png", + "cyan_concrete_powder": "cyan_concrete_powder.png", + "cyan_glazed_terracotta,facing=*": "cyan_glazed_terracotta.png", + "cyan_shulker_box": "cyan_shulker_box.png", + "cyan_stained_glass": "cyan_stained_glass.png", + "cyan_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "cyan_terracotta": "cyan_terracotta.png", + "cyan_wall_banner": ["transparent"], + "cyan_wool": "cyan_wool.png", + + "damaged_anvil,facing=*": "damaged_anvil_top.png", + "dandelion": "dandelion.png", + + "dark_oak_button,face=*,facing=*,powered=*": ["transparent"], + "dark_oak_door,facing=*,half=*,hinge=*,open=*": "dark_oak_door_top.png", + "dark_oak_fence,north=*,south=*,east=*,west=*": ["fixed", "TODO"], + "dark_oak_fence_gate,facing=*,in_wall=*,open=*": ["fixed", "TODO"], + "dark_oak_leaves": ["texture", "dark_oak_leaves.png", "foliage"], + "dark_oak_log,axis=y": "dark_oak_log_top.png", + "dark_oak_log,axis=z": "dark_oak_log.png", + "dark_oak_log,axis=x": "dark_oak_log.png", + "dark_oak_planks": "dark_oak_planks.png", + "dark_oak_pressure_plate,powered=*": "dark_oak_planks.png", + "dark_oak_sapling": "dark_oak_sapling.png", + "dark_oak_slab,type=*,": "dark_oak_planks.png", + "dark_oak_stairs,facing=*,half=*,shape=*": "dark_oak_planks.png", + "dark_oak_trapdoor,open=false,half=*,facing=*": "dark_oak_trapdoor.png", + "dark_oak_trapdoor,open=true,half=*,facing=*": ["transparent"], + "dark_oak_wood,axis=*": "dark_oak_log.png", + + "dark_prismarine": "dark_prismarine.png", + "dark_prismarine_slab,type=*": "dark_prismarine.png", + "dark_prismarine_stairs,facing=*,half=*,shape=*": "dark_prismarine.png", + "dailight_detector,inverted=false": "daylight_detector_top.png", + "dailight_detector,inverted=true": "daylight_detector_inverted_top.png", + + "dead_brain_coral_block": "dead_brain_coral_block.png", + "dead_brain_coral_fan": "dead_brain_coral_fan.png", + "dead_brain_coral_wall_fan,facing=*": "dead_brain_coral_fan.png", + "dead_bubble_coral_block": "dead_bubble_coral_block.png", + "dead_bubble_coral_fan": "dead_bubble_coral_fan.png", + "dead_bubble_coral_wall_fan,facing=*": "dead_bubble_coral_fan.png", + + "dead_bush": "dead_bush.png", + + "dead_fire_coral_block": "dead_fire_coral_block.png", + "dead_fire_coral_fan": "dead_fire_coral_fan.png", + "dead_fire_coral_wall_fan,facing=*": "dead_fire_coral_fan.png", + "dead_horn_coral_block": "dead_horn_coral_block.png", + "dead_horn_coral_fan": "dead_horn_coral_fan.png", + "dead_horn_coral_wall_fan,facing=*": "dead_horn_coral_fan.png", + "dead_tube_coral_block": "dead_tube_coral_block.png", + "dead_tube_coral_fan": "dead_tube_coral_fan.png", + "dead_tube_coral_wall_fan,facing=*": "dead_tube_coral_fan.png", + + "detector_rail,powered=false,shape=*": "detector_rail.png", + "detector_rail,powered=true,shape=*": "detector_rail_on.png", + "diamond_block": "diamond_block.png", + "diamond_ore": "diamond_ore.png", + "diorite": "diorite.png", + "dirt": "dirt.png", + "dispenser,facing=north": "furnace_top.png", + "dispenser,facing=south": "furnace_top.png", + "dispenser,facing=east": "furnace_top.png", + "dispenser,facing=west": "furnace_top.png", + "dispenser,facing=up": "dispenser_front_vertical.png", + "dispenser,facing=down": "furnace_top.png", + "dragon_egg": "dragon_egg.png", + "dragon_head": "dragon_egg.png", + "dragon_wall_head": "dragon_egg.png", + "dried_kelp_block": "dried_kelp_top.png", + "dropper,facing=north": "furnace_top.png", + "dropper,facing=south": "furnace_top.png", + "dropper,facing=east": "furnace_top.png", + "dropper,facing=west": "furnace_top.png", + "dropper,facing=up": "dropper_front_vertical.png", + "dropper,facing=down": "furnace_top.png", + "emerald_block": "emerald_block.png", + "emerald_ore": "emerald_ore.png", + "enchanting_table": "enchanting_table_top.png", + + "ender_chest": ["fixed", "TODO"], + "end_gateway": "end_portal_frame_top.png", + "end_portal": ["fixed", "TODO"], + "end_portal_frame,eye=*,facing=*": "end_portal_frame_top.png", + "end_rod": ["transparent"], + "end_stone": "end_stone.png", + "end_stone_bricks": "end_stone_bricks.png", + + "farmland,moisture=0": "farmland.png", + "farmland,moisture=1": "farmland.png", + "farmland,moisture=2": "farmland.png", + "farmland,moisture=3": "farmland.png", + "farmland,moisture=4": "farmland.png", + "farmland,moisture=5": "farmland.png", + "farmland,moisture=6": "farmland.png", + "farmland,moisture=7": "farmland_moist.png", + + "fern": "fern.png", + "fire,north=*,south=*,east=*,west=*": "fire_0.png", + + "fire_coral": "fire_coral.png", + "fire_coral_block": "fire_coral_block.png", + "fire_coral_fan": "fire_coral_fan.png", + "fire_coral_wall_fan,facing=*": "fire_coral_fan.png", + + "flower_pot": ["transparent"], + "frosted_ice,age=0": "frosted_ice_0.png", + "frosted_ice,age=1": "frosted_ice_1.png", + "frosted_ice,age=2": "frosted_ice_2.png", + "frosted_ice,age=3": "frosted_ice_3.png", + "furnace,facing=*,lit=*": "furnace_top.png", + "glass": "glass.png", + "glass_pane,north=*,south=*,west=*,east=*": ["transparent"], + "glowstone": "glowstone.png", + "gold_block": "gold_block.png", + "gold_ore": "gold_ore.png", + "granite": "granite.png", + "grass": ["texture", "grass.png", "grass"], + "grass_block,snowy=false": ["texture", "grass_block_top.png", "grass"], + "grass_block,snowy=true": "snow.png", + "grass_path": "grass_path_top.png", + "gravel": "gravel.png", + + "gray_banner": ["transparent"], + "gray_bed": ["transparent"], + "gray_carpet": "gray_wool.png", + "gray_concrete": "gray_concrete.png", + "gray_concrete_powder": "gray_concrete_powder.png", + "gray_glazed_terracotta,facing=*": "gray_glazed_terracotta.png", + "gray_shulker_box": "gray_shulker_box.png", + "gray_stained_glass": "gray_stained_glass.png", + "gray_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "gray_terracotta": "gray_terracotta.png", + "gray_wall_banner": ["transparent"], + "gray_wool": "gray_wool.png", + + "green_banner": ["transparent"], + "green_bed": ["transparent"], + "green_carpet": "green_wool.png", + "green_concrete": "green_concrete.png", + "green_concrete_powder": "green_concrete_powder.png", + "green_glazed_terracotta,facing=*": "green_glazed_terracotta.png", + "green_shulker_box": "green_shulker_box.png", + "green_stained_glass": "green_stained_glass.png", + "green_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "green_terracotta": "green_terracotta.png", + "green_wall_banner": ["transparent"], + "green_wool": "green_wool.png", + + "hay_block,axis=y": "hay_block_top.png", + "hay_block,axis=x": "hay_block_side.png", + "hay_block,axis=z": "hay_block_side.png", + "heavy_weighted_pressure_plate,power=*": "iron_block.png", + "hopper,facing=*": "hopper_top.png", + + "horn_coral": "horn_coral.png", + "horn_coral_block": "horn_coral_block.png", + "horn_coral_fan": "horn_coral_fan.png", + "horn_coral_wall_fan,facing=*": "horn_coral_fan.png", + + "ice": "ice.png", + + "infested_chiseled_stone_bricks": "chiseled_stone_bricks.png", + "infested_cobblestone": "infested_cobblestone.png", + "infested_cracked_stone_bricks": "cracked_stone_bricks.png", + "infested_mossy_stone_bricks": "mossy_stone_bricks.png", + "infested_stone": "stone.png", + "infested_stone_bricks": "stone_bricks.png", + + "iron_bars,north=*,south=*,west=*,east=*": ["transparent"], + "iron_block": "iron_block.png", + "iron_door,facing=*,half=*,hinge=*,open=*": "iron_door_top.png", + "iron_ore": "iron_ore.png", + "iron_trapdoor,open=false,half=*,facing=*": "iron_trapdoor.png", + "iron_trapdoor,open=true,half=*,facing=*": ["transparent"], + + "item_frame,map=*": ["transparent"], + "jack_o_landern,facing=*": "pumpkin_top.png", + "jukebox": "jukebox_top", + + "jungle_button,face=*,facing=*,powered=*": ["transparent"], + "jungle_door,facing=*,half=*,hinge=*,open=*": "jungle_door_top.png", + "jungle_fence,north=*,south=*,east=*,west=*": ["fixed", "TODO"], + "jungle_fence_gate,facing=*,in_wall=*,open=*": ["fixed", "TODO"], + "jungle_leaves": ["texture", "jungle_leaves.png", "foliage"], + "jungle_log,axis=y": "jungle_log_top.png", + "jungle_log,axis=z": "jungle_log.png", + "jungle_log,axis=x": "jungle_log.png", + "jungle_planks": "acacia_planks.png", + "jungle_pressure_plate,powered=*": "jungle_planks.png", + "jungle_sapling": "jungle_sapling.png", + "jungle_slab,type=*,": "jungle_planks.png", + "jungle_stairs,facing=*,half=*,shape=*": "jungle_planks.png", + "jungle_trapdoor,open=false,half=*,facing=*": "jungle_trapdoor.png", + "jungle_trapdoor,open=true,half=*,facing=*": ["transparent"], + "jungle_wood,axis=*": "jungle_log.png", + + "kelp": "kelp.png", + "kelp_plant": "kelp_plant.png", + "ladder,facing=*": ["transparent"], + "lapis_block": "lapis_block.png", + "lapis_ore": "lapis_ore.png", + "large_fern,half=upper": "large_fern_top.png", + "large_fern,half=lower": "large_fern_bottom.png", + "lava": "lava_still.png", + "lever,face=*,facing=*,powered=*": ["transparent"], + + "light_blue_banner": ["transparent"], + "light_blue_bed": ["transparent"], + "light_blue_carpet": "light_blue_wool.png", + "light_blue_concrete": "light_blue_concrete.png", + "light_blue_concrete_powder": "light_blue_concrete_powder.png", + "light_blue_glazed_terracotta,facing=*": "light_blue_glazed_terracotta.png", + "light_blue_shulker_box": "light_blue_shulker_box.png", + "light_blue_stained_glass": "light_blue_stained_glass.png", + "light_blue_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "light_blue_terracotta": "light_blue_terracotta.png", + "light_blue_wall_banner": ["transparent"], + "light_blue_wool": "light_blue_wool.png", + + "light_gray_banner": ["transparent"], + "light_gray_bed": ["transparent"], + "light_gray_carpet": "light_gray_wool.png", + "light_gray_concrete": "light_gray_concrete.png", + "light_gray_concrete_powder": "light_gray_concrete_powder.png", + "light_gray_glazed_terracotta,facing=*": "light_gray_glazed_terracotta.png", + "light_gray_shulker_box": "light_gray_shulker_box.png", + "light_gray_stained_glass": "light_gray_stained_glass.png", + "light_gray_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "light_gray_terracotta": "light_gray_terracotta.png", + "light_gray_wall_banner": ["transparent"], + "light_gray_wool": "light_gray_wool.png", + + "lilac,half=lower": "lilac_bottom.png", + "lilac,half=upper": "lilac_top.png", + "lily_pad": "lily_pad.png", + + "lime_banner": ["transparent"], + "lime_bed": ["transparent"], + "lime_carpet": "lime_wool.png", + "lime_concrete": "lime_concrete.png", + "lime_concrete_powder": "lime_concrete_powder.png", + "lime_glazed_terracotta,facing=*": "lime_glazed_terracotta.png", + "lime_shulker_box": "lime_shulker_box.png", + "lime_stained_glass": "lime_stained_glass.png", + "lime_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "lime_terracotta": "lime_terracotta.png", + "lime_wall_banner": ["transparent"], + "lime_wool": "lime_wool.png", + + "magenta_banner": ["transparent"], + "magenta_bed": ["transparent"], + "magenta_carpet": "magenta_wool.png", + "magenta_concrete": "magenta_concrete.png", + "magenta_concrete_powder": "magenta_concrete_powder.png", + "magenta_glazed_terracotta,facing=*": "magenta_glazed_terracotta.png", + "magenta_shulker_box": "magenta_shulker_box.png", + "magenta_stained_glass": "magenta_stained_glass.png", + "magenta_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "magenta_terracotta": "magenta_terracotta.png", + "magenta_wall_banner": ["transparent"], + "magenta_wool": "magenta_wool.png", + + "magma_block": "magma_block.png", + "melon": "melon_top.png", + "mossy_cobblestone": "mossy_cobblestone.png", + "mossy_cobblestone_wall,up=*,north=*,south=*,west=*,east=*": "mossy_ cobblestone.png", + "mossy_cobblestone_bricks": "mossy_cobblestone_bricks.png", + "moving_piston": ["transparent"], + "mushroom_stem,north=*,south=*,west=*,east=*": "mushroom_block_inside.png", + "mycelium,snowy=false": "mycelium_top.png", + "mycelium,snowy=true": "snow.png", + + "nether_brick_fence,north=*,south=*,east=*,west=*": ["fixed", "TODO"], + "nether_bricks": "nether_bricks.png", + "nether_brick_slab,type=*,": "nether_bricks.png", + "nether_brick_stairs,facing=*,half=*,shape=*": "nether_bricks.png", + "nether_portal,axis=*": "nether_portal.png", + "nether_quartz_ore": "nether_quartz_ore.png", + "netherrack": "netherrack.png", + "nether_wart,age=0": "nether_wart_stage0.png", + "nether_wart,age=1": "nether_wart_stage1.png", + "nether_wart,age=2": "nether_wart_stage2.png", + "nether_wart_block": "nether_wart_block.png", + + "note_block": "note_block.png", + + "oak_button,face=*,facing=*,powered=*": ["transparent"], + "oak_door,facing=*,half=*,hinge=*,open=*": "oak_door_top.png", + "oak_fence,north=*,south=*,east=*,west=*": ["fixed", "TODO"], + "oak_fence_gate,facing=*,in_wall=*,open=*": ["fixed", "TODO"], + "oak_leaves": ["texture", "oak_leaves.png", "foliage"], + "oak_log,axis=y": "oak_log_top.png", + "oak_log,axis=z": "oak_log.png", + "oak_log,axis=x": "oak_log.png", + "oak_planks": "oak_planks.png", + "oak_pressure_plate,powered=*": "oak_planks.png", + "oak_sapling": "oak_sapling.png", + "oak_slab,type=*,": "oak_planks.png", + "oak_stairs,facing=*,half=*,shape=*": "oak_planks.png", + "oak_trapdoor,open=false,half=*,facing=*": "oak_trapdoor.png", + "oak_trapdoor,open=true,half=*,facing=*": ["transparent"], + "oak_wood,axis=*": "oak_log.png", + + "observer,facing=north,powered=*": "observer_top.png", + "observer,facing=south,powered=*": "observer_top.png", + "observer,facing=west,powered=*": "observer_top.png", + "observer,facing=east,powered=*": "observer_top.png", + "observer,facing=up,powered=*": "observer_front.png", + "observer,facing=down,powered=false": "observer_back.png", + "observer,facing=down,powered=true": "observer_back_on.png", + "obsidian": "obsidian.png", + + "orange_banner": ["transparent"], + "orange_bed": ["transparent"], + "orange_carpet": "orange_wool.png", + "orange_concrete": "orange_concrete.png", + "orange_concrete_powder": "orange_concrete_powder.png", + "orange_glazed_terracotta,facing=*": "orange_glazed_terracotta.png", + "orange_shulker_box": "orange_shulker_box.png", + "orange_stained_glass": "orange_stained_glass.png", + "orange_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "orange_terracotta": "orange_terracotta.png", + "orange_tulip": "orange_tulip.png", + "orange_wall_banner": ["transparent"], + "orange_wool": "orange_wool.png", + + "packed_ice": "packed_ice.png", + "peony,half=lower": "peony_bottom.png", + "peony,half=upper": "peony_top.png", + "petrified_oak_slab,type=*": "oak_planks.png", + + "pink_banner": ["transparent"], + "pink_bed": ["transparent"], + "pink_carpet": "pink_wool.png", + "pink_concrete": "pink_concrete.png", + "pink_concrete_powder": "pink_concrete_powder.png", + "pink_glazed_terracotta,facing=*": "pink_glazed_terracotta.png", + "pink_shulker_box": "pink_shulker_box.png", + "pink_stained_glass": "pink_stained_glass.png", + "pink_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "pink_terracotta": "pink_terracotta.png", + "pink_tulip": "pink_tulip.png", + "pink_wall_banner": ["transparent"], + "pink_wool": "pink_wool.png", + + "piston,facing=north,extended=*": "piston_side.png", + "piston,facing=south,extended=*": "piston_side.png", + "piston,facing=west,extended=*": "piston_side.png", + "piston,facing=east,extended=*": "piston_side.png", + "piston,facing=down,extended=*": "piston_bottom.png", + "piston,facing=up,extended=false": "piston_top.png", + "piston,facing=up,extended=true": "piston_inner.png", + "piston_head,facing=up,short=*,type=normal": "piston_top.png", + "piston_head,facing=up,short=*,type=sticky": "piston_top_sticky.png", + "piston_head,facing=north,short=*,type=sticky": ["transparent"], + "piston_head,facing=south,short=*,type=sticky": ["transparent"], + "piston_head,facing=west,short=*,type=sticky": ["transparent"], + "piston_head,facing=east,short=*,type=sticky": ["transparent"], + "piston_head,facing=down,short=*,type=sticky": ["transparent"], + + "player_head": ["transparent"], + "player_wall_head": ["transparent"], + "podzol,snowy=false": "podzol_top.png", + "podzol,snowy=true": "snow.png", + "polished_andesite": "polished_andesite.png", + "polished_diorite": "polished_diorite.png", + "polished_granite": "polished_granite.png", + "poppy": "poppy.png", + "potatoes,age=0": "potatoes_stage0", + "potatoes,age=1": "potatoes_stage0", + "potatoes,age=2": "potatoes_stage1", + "potatoes,age=3": "potatoes_stage1", + "potatoes,age=4": "potatoes_stage2", + "potatoes,age=5": "potatoes_stage2", + "potatoes,age=6": "potatoes_stage3", + "potatoes,age=7": "potatoes_stage3", + + "potted_acacia_sapling": ["transparent"], + "potted_allium": ["transparent"], + "potted_azure_bluet": ["transparent"], + "potted_birch_sapling": ["transparent"], + "potted_blue_orchid": ["transparent"], + "potted_brown_mushroom": ["transparent"], + "potted_cactus": ["transparent"], + "potted_dandelion": ["transparent"], + "potted_dark_oak_sapling": ["transparent"], + "potted_dead_bush": ["transparent"], + "potted_fern": ["transparent"], + "potted_jungle_sapling": ["transparent"], + "potted_oak_sapling": ["transparent"], + "potted_orange_tulip": ["transparent"], + "potted_oxeye_daisy": ["transparent"], + "potted_pink_tulip": ["transparent"], + "potted_poppy": ["transparent"], + "potted_red_mushroom": ["transparent"], + "potted_red_tulip": ["transparent"], + "potted_spruce_sapling": ["transparent"], + "potted_white_tulip": ["transparent"], + + "powered_rail,powered=false,shape=*,": "powered_rail.png", + "powered_rail,powered=true,shape=*,": "powered_rail_on.png", + "prismarine": "prismarine.png", + "prismarine_bricks": "prismarine_bricks.png", + "prismarine_brick_slab,type=*": "prismarine_bricks.png", + "prismarine_brick_stairs,facing=*,half=*,shape=*": "prismarine_bricks.png", + "prismarine_slab,type=*": "prismarine.png", + "prismarine_stairs,facing=*,half=*,shape=*": "prismarine.png", + + "pumpkin": "pumpkin_top.png", + "pumpkin_stem,age=*": ["transparent"], + + "purple_banner": ["transparent"], + "purple_bed": ["transparent"], + "purple_carpet": "purple_wool.png", + "purple_concrete": "purple_concrete.png", + "purple_concrete_powder": "purple_concrete_powder.png", + "purple_glazed_terracotta,facing=*": "purple_glazed_terracotta.png", + "purple_shulker_box": "purple_shulker_box.png", + "purple_stained_glass": "purple_stained_glass.png", + "purple_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "purple_terracotta": "purple_terracotta.png", + "purple_wall_banner": ["transparent"], + "purple_wool": "purple_wool.png", + + "purpur_block": "purpur_block.png", + "purpur_pillar,axis=y": "purpur_pillar_top.png", + "purpur_pillar,axis=x": "purpur_pillar.png", + "purpur_pillar,axis=z": "purpur_pillar.png", + "purpur_slab,type=*": "purpur_block.png", + "purpur_stairs,facing=*,half=*,shape=*": "purpur_block.png", + + "quartz_block": "quartz_block_top.png", + "quartz_pillar,axis=y": "quartz_pillar_top.png", + "quartz_pillar,axis=x": "quartz_pillar.png", + "quartz_pillar,axis=z": "quartz_pillar.png", + "quartz_slab,type=*": "quartz_block_top.png", + "quartz_stairs,facing=*,half=*,shape=*": "quartz_block_top.png", + + "rail,shape=*": "rail.png", + + "red_banner": ["transparent"], + "red_bed": ["transparent"], + "red_carpet": "red_wool.png", + "red_concrete": "red_concrete.png", + "red_concrete_powder": "red_concrete_powder.png", + "red_glazed_terracotta,facing=*": "red_glazed_terracotta.png", + "red_mushroom": "red_mushroom.png", + "red_mushroom_block,up=true,north=*,south=*,west=*,east=*,down=*": "red_mushroom_block.png", + "red_mushroom_block,up=false,north=*,south=*,west=*,east=*,down=*": "mushroom_block_inside.png", + "red_nether_bricks": "red_nether_bricks.png", + "red_sand": "red_sand.png", + "red_sandstone": "red_sandstone_top.png", + "red_sandstone_slab,type=*": "red_sandstone_top.png", + "red_sandstone_stairs,facing=*,half=*,shape=*": "red_sandstone_top.png", + "red_shulker_box": "red_shulker_box.png", + "red_stained_glass": "red_stained_glass.png", + "red_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "redstone_block": "redstone_block.png", + "redstone_lamp,lit=false": "redstone_lamp.png", + "redstone_lamp,lit=true": "redstone_lamp_on.png", + "redstone_ore,lit=*": "redstone_ore.png", + "redstone_torch": ["transparent"], + "redstone_wall_torch": ["transparent"], + "redstone_wire,north=*,south=*,west=*,east=*": ["fixed", "TODO"], + "red_terracotta": "red_terracotta.png", + "red_tulip": "red_tulip.png", + "red_wall_banner": ["transparent"], + "red_wool": "red_wool.png", + + "repeater,powered=false,delay=*,facing=*,locked=*": "repeater.png", + "repeater,powered=true,delay=*,facing=*,locked=*": "repeater_on.png", + + "repeating_command_block,conditional=false,facing=down": "repeating_command_block_back.png", + "repeating_command_block,conditional=false,facing=up": "repeating_command_block_front.png", + "repeating_command_block,conditional=false,facing=north": "repeating_command_block_side.png", + "repeating_command_block,conditional=false,facing=south": "repeating_command_block_side.png", + "repeating_command_block,conditional=false,facing=east": "repeating_command_block_side.png", + "repeating_command_block,conditional=false,facing=west": "repeating_command_block_side.png", + "repeating_command_block,conditional=true,facing=down": "repeating_command_block_back.png", + "repeating_command_block,conditional=true,facing=up": "repeating_command_block_front.png", + "repeating_command_block,conditional=true,facing=north": "repeating_command_block_conditional.png", + "repeating_command_block,conditional=true,facing=south": "repeating_command_block_conditional.png", + "repeating_command_block,conditional=true,facing=east": "repeating_command_block_conditional.png", + "repeating_command_block,conditional=true,facing=west": "repeating_command_block_conditional.png", + + "rose_bush,half=lower": "rose_bush_bottom.png", + "rose_bush,half=upper": "rose_bush_top.png", + "sand": "sand.png", + "sandstone": "sandstone_top.png", + "sandstone_slab,type=*": "sandstone_top.png", + "sandstone_stairs,facing=*,half=*,shape=*": "sandstone_top.png", + "seagrass": "seagrass.png", + "sea_lantern": "sea_lantern.png", + "sea_pickle,pickles=*,waterlogged=*": "sea_pickle.png", + "shulker_box": "shulker_box.png", + "sign": ["transparent"], + "skeleton_skull": ["transparent"], + "skeleton_wall_skull": ["transparent"], + "slime_block": "slime_block.png", + "smooth_quartz": "quartz_block_top.png", + "smooth_red_sandstone": "red_sandstone_top.png", + "smooth_stone": "stone_slab_top.png", + "snow,layers=*": "snow.png", + "snow_block": "snow.png", + "soul_sand": "soul_sand.png", + "spawner": "spawner.png", + "sponge": "sponge.png", + + "spruce_button,face=*,facing=*,powered=*": ["transparent"], + "spruce_door,facing=*,half=*,hinge=*,open=*": "spruce_door_top.png", + "spruce_fence,north=*,south=*,east=*,west=*": ["fixed", "TODO"], + "spruce_fence_gate,facing=*,in_wall=*,open=*": ["fixed", "TODO"], + "spruce_leaves": ["texture", "spruce_leaves.png", "foliage"], + "spruce_log,axis=y": "spruce_log_top.png", + "spruce_log,axis=z": "spruce_log.png", + "spruce_log,axis=x": "spruce_log.png", + "spruce_planks": "spruce_planks.png", + "spruce_pressure_plate,powered=*": "spruce_planks.png", + "spruce_sapling": "spruce_sapling.png", + "spruce_slab,type=*,": "spruce_planks.png", + "spruce_stairs,facing=*,half=*,shape=*": "spruce_planks.png", + "spruce_trapdoor,open=false,half=*,facing=*": "spruce_trapdoor.png", + "spruce_trapdoor,open=true,half=*,facing=*": ["transparent"], + "spruce_wood,axis=*": "spruce_log.png", + + "stone": "stone.png", + "stone_bricks": "stone_bricks.png", + "stone_brick_slab,type=*": "stone_bricks.png", + "stone_brick_stairs,facing=*,half=*,shape=*": "stone_bricks.png", + "stone_slab,type=*": "stone_slab_top.png", + + "stripped_acacia_log,axis=y": "stripped_acacia_log_top.png", + "stripped_acacia_log,axis=x": "stripped_acacia_log.png", + "stripped_acacia_log,axis=z": "stripped_acacia_log.png", + "stripped_acacia_wood,axis=*": "stripped_acacia_log.png", + "stripped_birch_log,axis=y": "stripped_birch_log_top.png", + "stripped_birch_log,axis=x": "stripped_birch_log.png", + "stripped_birch_log,axis=z": "stripped_birch_log.png", + "stripped_birch_wood,axis=*": "stripped_birch_log.png", + "stripped_dark_oak_log,axis=y": "stripped_dark_oak_log_top.png", + "stripped_dark_oak_log,axis=x": "stripped_dark_oak_log.png", + "stripped_dark_oak_log,axis=z": "stripped_dark_oak_log.png", + "stripped_dark_oak_wood,axis=*": "stripped_dark_oak_log.png", + "stripped_jungle_log,axis=y": "stripped_jungle_log_top.png", + "stripped_jungle_log,axis=x": "stripped_jungle_log.png", + "stripped_jungle_log,axis=z": "stripped_jungle_log.png", + "stripped_jungle_wood,axis=*": "stripped_jungle_log.png", + "stripped_oak_log,axis=y": "stripped_oak_log_top.png", + "stripped_oak_log,axis=x": "stripped_oak_log.png", + "stripped_oak_log,axis=z": "stripped_oak_log.png", + "stripped_oak_wood,axis=*": "stripped_oak_log.png", + "stripped_spruce_log,axis=y": "stripped_spruce_log_top.png", + "stripped_spruce_log,axis=x": "stripped_spruce_log.png", + "stripped_spruce_log,axis=z": "stripped_spruce_log.png", + "stripped_spruce_wood,axis=*": "stripped_spruce_log.png", + + "structure_block,mode=save": "structure_block_save.png", + "structure_block,mode=load": "structure_block_load.png", + "structure_block,mode=corner": "structure_block_corner.png", + "structure_block,mode=data": "structure_block_data.png", + "structure_void": ["transparent"], + + "sugar_cane": "sugar_cane.png", + "sunflower,half=lower": "sunflower_bottom.png", + "sunflower,half=upper": "sunflower_top.png", + "tall_grass,half=lower": "tall_grass_bottom.png", + "tall_grass,half=upper": "tall_grass_top.png", + "tall_seagrass,half=lower": "tall_seagrass_bottom.png", + "tall_seagrass,half=upper": "tall_seagrass_top.png", + "terracotta": "terracotta.png", + "tnt": "tnt_top.png", + "torch": ["transparent"], + "trapped_chest": ["fixed", "TODO"], + "tripwire,attached=*,east=*,north=*,south=*,west=*": ["transparent"], + "tripwire_hook,attached=*,facing=*,powered=*": ["transparent"], + + "tube_coral": "tube_coral.png", + "tube_coral_block": "tube_coral_block.png", + "tube_coral_fan": "tube_coral_fan.png", + "tube_coral_wall_fan,facing=*": "tube_coral_fan.png", + + "turtle_egg,eggs=*,hatch=*": ["transparent"], + "vine,east=*,north=*,south=*,up=*,west=*": ["transparent"], + "void_air": ["fixed", "0x00000000"], + "wall_sign,facing=*": ["transparent"], + "water": ["texture", "water_still.png", "water"], + "wet_sponge": "wet_sponge.png", + "wheat,stage=0": "wheat_stage0.png", + "wheat,stage=1": "wheat_stage1.png", + "wheat,stage=2": "wheat_stage2.png", + "wheat,stage=3": "wheat_stage3.png", + "wheat,stage=4": "wheat_stage4.png", + "wheat,stage=5": "wheat_stage5.png", + "wheat,stage=6": "wheat_stage6.png", + "wheat,stage=7": "wheat_stage7.png", + + "white_banner": ["transparent"], + "white_bed": ["transparent"], + "white_carpet": "white_wool.png", + "white_concrete": "white_concrete.png", + "white_concrete_powder": "white_concrete_powder.png", + "white_glazed_terracotta,facing=*": "white_glazed_terracotta.png", + "white_shulker_box": "white_shulker_box.png", + "white_stained_glass": "white_stained_glass.png", + "white_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "white_terracotta": "white_terracotta.png", + "white_tulip": "white_tulip.png", + "white_wall_banner": ["transparent"], + "white_wool": "white_wool.png", + + "wither_skeleton_skull": ["transparent"], + "wither_skeleton_wall_skull": ["transparent"], + + "yellow_banner": ["transparent"], + "yellow_bed": ["transparent"], + "yellow_carpet": "yellow_wool.png", + "yellow_concrete": "yellow_concrete.png", + "yellow_concrete_powder": "yellow_concrete_powder.png", + "yellow_glazed_terracotta,facing=*": "yellow_glazed_terracotta.png", + "yellow_shulker_box": "yellow_shulker_box.png", + "yellow_stained_glass": "yellow_stained_glass.png", + "yellow_stained_glass_pane,north=*,south=*,east=*,west=*": ["transparent"], + "yellow_terracotta": "yellow_terracotta.png", + "yellow_wall_banner": ["transparent"], + "yellow_wool": "yellow_wool.png", + + "zombie_head": ["transparent"], + "zombie_wall_head": ["transparent"] +}