/* Copyright (c) 2010-2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.world; import se.llbit.chunky.ui.MapViewMode; import se.llbit.math.QuickMath; /** * Abstract representation of a view over a map of chunks. * * @author Jesper Öqvist <jesper@llbit.se> */ public class ChunkView { /** * Minimum block scale for the map view. */ public static final int BLOCK_SCALE_MIN = 1; /** * Maximum block scale for the map view. */ public static final int BLOCK_SCALE_MAX = 32 * 16; /** * Default block scale for the map view. */ public static final int DEFAULT_BLOCK_SCALE = 4 * 16; /** * A zero-size chunk view useful as a default chunk view for an uninitialized map. */ public static final ChunkView EMPTY = new ChunkView(0, 0, 0, 0, DEFAULT_BLOCK_SCALE, MapViewMode.AUTO, World.SEA_LEVEL) { @Override public boolean isChunkVisible(int x, int z) { return false; } @Override public boolean isRegionVisible(int x, int z) { return false; } }; public final MapViewMode renderer; public final int layer; // Center position. public final double x; public final double z; // Visible chunks. public final double x0; public final double z0; public final double x1; public final double z1; // Rendered chunks. public final int cx0; public final int cz0; public final int cx1; public final int cz1; // Preloaded chunks. public final int px0; public final int pz0; public final int px1; public final int pz1; // Rendered regions. public final int prx0; public final int prz0; public final int prx1; public final int prz1; public final int width; public final int height; public final int scale; public final int chunkScale; public ChunkView(double x, double z, int width, int height, MapViewMode renderer, int layer) { this(x, z, width, height, 16, renderer, layer); } public ChunkView(ChunkView other) { this(other.x, other.z, other.width, other.height, other.scale, other.renderer, other.layer); } public ChunkView(double x, double z, int width, int height, int scale, MapViewMode renderer, int layer) { this.renderer = renderer; this.layer = Math.max(0, Math.min(Chunk.Y_MAX - 1, layer)); scale = clampScale(scale); this.scale = scale; if (this.scale <= 12) { chunkScale = 1; } else if (this.scale <= 12 * 16) { chunkScale = 16; } else { this.chunkScale = 16 * 16; } this.x = x; this.z = z; double cw = width / (2. * this.scale); double ch = height / (2. * this.scale); this.x0 = x - cw; this.x1 = x + cw; this.z0 = z - ch; this.z1 = z + ch; this.width = width; this.height = height; // Visible chunks [integer coordinates]: cx0 = (int) QuickMath.floor(x0); cx1 = (int) QuickMath.floor(x1); cz0 = (int) QuickMath.floor(z0); cz1 = (int) QuickMath.floor(z1); if (this.scale >= 16) { px0 = cx0 - 1; px1 = cx1 + 1; pz0 = cz0 - 1; pz1 = cz1 + 1; prx0 = px0 >> 5; prx1 = px1 >> 5; prz0 = pz0 >> 5; prz1 = pz1 >> 5; } else { // Visible regions. int irx0 = cx0 >> 5; int irx1 = cx1 >> 5; int irz0 = cz0 >> 5; int irz1 = cz1 >> 5; prx0 = irx0; prx1 = irx1; prz0 = irz0; prz1 = irz1; px0 = prx0 << 5; px1 = (prx1 << 5) + 31; pz0 = prz0 << 5; pz1 = (prz1 << 5) + 31; } } public boolean shouldPreload(Chunk chunk) { if (chunk.isEmpty()) { return false; } ChunkPosition pos = chunk.getPosition(); return isChunkVisible(pos.x, pos.z); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof ChunkView) { ChunkView other = (ChunkView) obj; return scale == other.scale && px0 == other.px0 && px1 == other.px1 && pz0 == other.pz0 && pz1 == other.pz1 && layer == other.layer && renderer == other.renderer; } return false; } public boolean isVisible(ChunkPosition pos) { return shouldPreload(pos); } /** * Determines if a chunk or region is visible based on the view scale. * If the scale is greater than or equal to 16 then the test is for chunk visibility, * otherwise region visibility is checked. */ public boolean shouldPreload(ChunkPosition pos) { return chunkScale >= 16 ? isChunkVisible(pos.x, pos.z) : isRegionVisible(pos.x, pos.z); } public boolean isChunkVisible(ChunkPosition chunk) { return isChunkVisible(chunk.x, chunk.z); } public boolean isChunkVisible(int x, int z) { return px0 <= x && px1 >= x && pz0 <= z && pz1 >= z; } public boolean isRegionVisible(ChunkPosition pos) { return isRegionVisible(pos.x, pos.z); } public boolean isRegionVisible(int x, int z) { return prx0 <= x && prx1 >= x && prz0 <= z && prz1 >= z; } @Override public String toString() { return String.format("[(%d, %d), (%d, %d)]", px0, pz0, px1, pz1); } /** * Clamp the block scale to the minimum / maximum values if it is outside * the valid value range. * @return clamped scale value */ public static int clampScale(int scale) { return Math.max(BLOCK_SCALE_MIN, Math.min(BLOCK_SCALE_MAX, scale)); } /** * @param other the previous view state * @return {@code true} if changing to this view from the given old * view should trigger a map repaint. */ public boolean shouldRepaint(ChunkView other) { if (px0 != other.px0 || px1 != other.px1 || pz0 != other.pz0 || pz1 != other.pz1 || renderer != other.renderer) { return true; } if (renderer == MapViewMode.LAYER && layer != other.layer) { return true; } return scale != other.scale; } }