org.pepsoft.worldpainter.threedeeview.ThreeDeeView Maven / Gradle / Ivy
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter.threedeeview;
import org.pepsoft.util.ProgressReceiver;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.*;
import org.pepsoft.worldpainter.biomeschemes.CustomBiomeManager;
import org.pepsoft.worldpainter.layers.Layer;
import org.pepsoft.worldpainter.layers.Void;
import org.pepsoft.worldpainter.threedeeview.Tile3DRenderer.LayerVisibilityMode;
import javax.swing.Timer;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.*;
import static java.awt.image.BufferedImage.TYPE_BYTE_BINARY;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
import static org.pepsoft.minecraft.Constants.DEFAULT_WATER_LEVEL;
import static org.pepsoft.util.AwtUtils.doOnEventThread;
import static org.pepsoft.worldpainter.Constants.TILE_SIZE;
import static org.pepsoft.worldpainter.threedeeview.Tile3DRenderer.LayerVisibilityMode.SURFACE;
import static org.pepsoft.worldpainter.threedeeview.Tile3DRenderer.LayerVisibilityMode.SYNC;
/**
*
* @author pepijn
*/
public class ThreeDeeView extends JComponent implements Dimension.Listener, Tile.Listener, HierarchyListener, ActionListener, Scrollable {
public ThreeDeeView(Dimension dimension, ColourScheme colourScheme, CustomBiomeManager customBiomeManager, int rotation, int zoom) {
this.dimension = dimension;
this.colourScheme = colourScheme;
this.customBiomeManager = customBiomeManager;
this.rotation = rotation;
this.zoom = zoom;
scale = (int) Math.pow(2.0, Math.abs(zoom - 1));
// System.out.println("Zoom " + zoom + " -> scale " + scale);
minHeight = dimension.getMinHeight();
maxHeight = dimension.getMaxHeight();
if (dimension.getTileFactory() instanceof HeightMapTileFactory) {
waterLevel = ((HeightMapTileFactory) dimension.getTileFactory()).getWaterHeight();
} else {
waterLevel = DEFAULT_WATER_LEVEL;
}
upsideDown = dimension.getAnchor().invert; // Ceiling dimension
zSortedTiles = new TreeMap<>();
for (Tile tile: dimension.getTiles()) {
final Rectangle tileBaseBounds = getTileBounds(tile.getX(), tile.getY(), 0, 0, 0);
zSortedTiles.computeIfAbsent(tileBaseBounds.y, y -> new TreeMap<>()).put(tileBaseBounds.x, tile);
}
threeDeeRenderManager = new ThreeDeeRenderManager(dimension, colourScheme, customBiomeManager, rotation);
dimension.addDimensionListener(this);
for (Tile tile: dimension.getTiles()) {
tile.addListener(this);
}
int width = dimension.getWidth() * TILE_SIZE + dimension.getHeight() * TILE_SIZE;
int height = width / 2 + maxHeight - minHeight - 1;
// maxX = dimension.getHighestX();
// maxY = dimension.getHighestY();
maxX = maxY = 0;
// xOffset = 512;
// yOffset = 256;
// xOffset = yOffset = 0;
switch (rotation) {
case 0:
xOffset = -getTileBounds(dimension.getLowestX(), dimension.getHighestY(), maxHeight).x;
yOffset = -getTileBounds(dimension.getLowestX(), dimension.getLowestY(), maxHeight).y;
break;
case 1:
xOffset = -getTileBounds(dimension.getHighestX(), dimension.getHighestY(), maxHeight).x;
yOffset = -getTileBounds(dimension.getLowestX(), dimension.getHighestY(), maxHeight).y;
break;
case 2:
xOffset = -getTileBounds(dimension.getHighestX(), dimension.getLowestY(), maxHeight).x;
yOffset = -getTileBounds(dimension.getHighestX(), dimension.getHighestY(), maxHeight).y;
break;
case 3:
xOffset = -getTileBounds(dimension.getLowestX(), dimension.getLowestY(), maxHeight).x;
yOffset = -getTileBounds(dimension.getHighestX(), dimension.getLowestY(), maxHeight).y;
break;
default:
throw new IllegalArgumentException();
}
// System.out.println("xOffset: " + xOffset + ", yOffset: " + yOffset);
java.awt.Dimension preferredSize = zoom(new java.awt.Dimension(width, height));
setPreferredSize(preferredSize);
setMinimumSize(preferredSize);
setMaximumSize(preferredSize);
setSize(preferredSize);
addHierarchyListener(this);
}
public RefreshMode getRefreshMode() {
return refreshMode;
}
public void setRefreshMode(RefreshMode refreshMode) {
this.refreshMode = refreshMode;
}
public Rectangle getImageBounds() {
Rectangle imageBounds = null;
for (Map row: zSortedTiles.values()) {
for (Tile tile: row.values()) {
if (imageBounds == null) {
imageBounds = getTileBounds(tile);
} else {
imageBounds = imageBounds.union(getTileBounds(tile));
}
}
}
return imageBounds;
}
public BufferedImage getImage(Rectangle imageBounds, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled {
final Tile3DRenderer renderer = new Tile3DRenderer(dimension, colourScheme, customBiomeManager, rotation, layerVisibility, hiddenLayers);
// Paint the complete image
final int tileCount = zSortedTiles.values().stream().mapToInt(Map::size).sum();
final BufferedImage image = new BufferedImage(imageBounds.width, imageBounds.height, TYPE_INT_ARGB);
final Graphics2D g2 = image.createGraphics();
try {
int tileNo = 0;
for (Map row: zSortedTiles.values()) {
for (Tile tile: row.values()) {
final Rectangle tileBounds = getTileBounds(tile);
g2.drawImage(renderer.render(tile), tileBounds.x - imageBounds.x, tileBounds.y - imageBounds.y, null);
if (progressReceiver != null) {
tileNo++;
progressReceiver.setProgress((float) tileNo / tileCount);
}
}
}
} finally {
g2.dispose();
}
return image;
}
public Point worldToView(int x, int y) {
// highlightTile = new Point(x >> 7, y >> 7);
switch (rotation) {
case 0:
return zoom(new Point(xOffset + TILE_SIZE + x - y, yOffset + maxHeight - minHeight - 1 - TILE_SIZE / 2 + (y + x) / 2));
case 1:
return zoom(new Point(xOffset + TILE_SIZE * 2 - x - y, yOffset + maxHeight - minHeight - 1 - (y - x) / 2));
case 2:
return zoom(new Point(xOffset + TILE_SIZE - x + y, yOffset + maxHeight - minHeight - 1 + TILE_SIZE / 2 - (y + x) / 2));
case 3:
return zoom(new Point(xOffset + x + y, yOffset + maxHeight - minHeight - 1 + (y - x) / 2));
default:
throw new IllegalArgumentException();
}
}
public Tile getCentreMostTile() {
return centreTile;
}
public Point getHighlightTile() {
return highlightTile;
}
public void setHighlightTile(Point highlightTile) {
this.highlightTile = highlightTile;
repaint();
}
public int getZoom() {
return zoom;
}
public void setZoom(int zoom) {
this.zoom = zoom;
scale = (int) Math.pow(2.0, Math.abs(zoom - 1));
// System.out.println("Zoom " + zoom + " -> scale " + scale);
int width = dimension.getWidth() * TILE_SIZE + dimension.getHeight() * TILE_SIZE;
int height = width / 2 + maxHeight - minHeight - 1;
java.awt.Dimension preferredSize = zoom(new java.awt.Dimension(width, height));
setPreferredSize(preferredSize);
setMinimumSize(preferredSize);
setMaximumSize(preferredSize);
setSize(preferredSize);
repaint();
}
public void setLayerVisibility(LayerVisibilityMode layerVisibility) {
if (layerVisibility != this.layerVisibility) {
this.layerVisibility = layerVisibility;
threeDeeRenderManager.setLayerVisibility(layerVisibility);
refresh(false);
}
}
public void setHiddenLayers(Set hiddenLayers) {
if (! Objects.equals(hiddenLayers, this.hiddenLayers)) {
this.hiddenLayers = hiddenLayers;
threeDeeRenderManager.setHiddenLayers(hiddenLayers);
if (layerVisibility == SYNC) {
refresh(false);
}
}
}
/**
* Centre the view on a particular tile. Specifically, centre the view on a
* point waterLevel above the centre of the floor of the tile.
*
* @param tile The tile to centre the view on.
*/
public void moveToTile(Tile tile) {
Rectangle tileBounds = zoom(getTileBounds(tile));
moveTo(new Point(tileBounds.x + tileBounds.width / 2, tileBounds.y + tileBounds.height - TILE_SIZE / 2));
// highlightTile = new Point(tileX, tileY);
}
/**
* Centre the view on a particular point in the world. Specifically, centre
* the view on a point waterLevel above the floor of the world at
* the specified coordinates.
*
* @param x The X coordinate in blocks on which to centre the view.
* @param y The Y coordinate in blocks on which to centre the view.
*/
public void moveTo(int x, int y) {
Point coords = worldToView(x, y);
// highlightPoint = coords;
moveTo(coords);
}
public void refresh(boolean clear) {
threeDeeRenderManager.stop();
if (clear) {
renderedTiles.clear();
dirtyTiles.clear();
} else {
dirtyTiles.putAll(renderedTiles);
renderedTiles.clear();
}
repaint();
}
// Dimension.Listener
@Override
public void tilesAdded(Dimension dimension, Set tiles) {
// threeDeeRenderManager.renderTile(tile);
tiles.forEach(tile -> {
final Rectangle tileBaseBounds = getTileBounds(tile.getX(), tile.getY(), 0, 0, 0);
zSortedTiles.computeIfAbsent(tileBaseBounds.y, y -> new TreeMap<>()).put(tileBaseBounds.x, tile);
tile.addListener(this);
});
}
@Override
public void tilesRemoved(Dimension dimension, Set tiles) {
for (Tile tile: tiles) {
tile.removeListener(this);
final Rectangle tileBaseBounds = getTileBounds(tile.getX(), tile.getY(), 0, 0, 0);
if (zSortedTiles.containsKey(tileBaseBounds.y)) {
zSortedTiles.get(tileBaseBounds.y).remove(tileBaseBounds.x);
}
}
// renderedTiles.remove(new Point(tile.getX(), tile.getY()));
// TODO: the tile will be re-added if it was on the render queue, but
// since this can currently never happen anyway we will deal with that
// when it becomes necessary
}
@Override public void overlayAdded(Dimension dimension, int index, Overlay overlay) {}
@Override public void overlayRemoved(Dimension dimension, int index, Overlay overlay) {}
// Tile.Listener
@Override
public void heightMapChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void terrainChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void waterLevelChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void seedsChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void layerDataChanged(Tile tile, Set changedLayers) {
for (Layer layer: changedLayers) {
if (((layerVisibility == SURFACE) && (! Tile3DRenderer.DEFAULT_HIDDEN_LAYERS.contains(layer)))
|| (layerVisibility == SYNC)
|| (layer instanceof Void)) {
scheduleTileForRendering(tile);
return;
}
}
}
@Override
public void allBitLayerDataChanged(Tile tile) {
scheduleTileForRendering(tile);
}
@Override
public void allNonBitlayerDataChanged(Tile tile) {
scheduleTileForRendering(tile);
}
// HierarchyListener
@Override
public void hierarchyChanged(HierarchyEvent event) {
if ((event.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0) {
if (isDisplayable()) {
// for (Tile tile: dimension.getTiles()) {
// threeDeeRenderManager.renderTile(tile);
// }
timer = new Timer(250, this);
timer.start();
} else {
timer.stop();
timer = null;
threeDeeRenderManager.stop();
for (Tile tile : dimension.getTiles()) {
tile.removeListener(this);
}
dimension.removeDimensionListener(this);
}
}
}
// ActionListener
@Override
public void actionPerformed(ActionEvent e) {
// Send tiles to be rendered
if ((! tilesWaitingToBeRendered.isEmpty()) && ((System.currentTimeMillis() - lastTileChange) > 250)) {
tilesWaitingToBeRendered.forEach(threeDeeRenderManager::renderTile);
tilesWaitingToBeRendered.clear();
}
// Collect rendered tiles
final Set renderResults = threeDeeRenderManager.getRenderedTiles();
Rectangle repaintArea = null;
for (RenderResult renderResult : renderResults) {
final Tile tile = renderResult.getTile();
renderedTiles.put(tile, renderResult.getImage());
dirtyTiles.remove(tile);
final Rectangle tileBounds = zoom(getTileBounds(tile));
if (repaintArea == null) {
repaintArea = tileBounds;
} else {
repaintArea = repaintArea.union(tileBounds);
}
}
if (repaintArea != null) {
// System.out.println("Repainting " + repaintArea);
repaint(repaintArea);
}
}
// Scrollable
@Override
public java.awt.Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return 10;
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return 100;
}
@Override
public boolean getScrollableTracksViewportWidth() {
return false;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
@Override
protected void paintComponent(Graphics g) {
// System.out.println("Drawing");
final Graphics2D g2 = (Graphics2D) g;
if (zoom != 1) {
final double scaleFactor = Math.pow(2.0, zoom - 1);
// System.out.println("Scaling with factor " + scaleFactor);
g2.scale(scaleFactor, scaleFactor);
if (zoom > 1) {
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
} else {
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
}
}
if (upsideDown) {
g2.scale(1.0, -1.0);
g2.translate(0, -getHeight());
}
final Rectangle visibleRect = unzoom(getVisibleRect());
// System.out.println("Unzoomed visible rectangle: " + visibleRect);
final int centerX = visibleRect.x + visibleRect.width / 2;
final int centerY = visibleRect.y + visibleRect.height / 2 + waterLevel;
Tile mostCentredTile = null;
int smallestDistance = Integer.MAX_VALUE;
final Rectangle clipBounds = g2.getClipBounds();
for (SortedMap row: zSortedTiles.subMap(clipBounds.y - yOffset - maxHeight, clipBounds.y + clipBounds.height - yOffset + maxHeight).values()) {
for (Tile tile: row.subMap(clipBounds.x - xOffset - TILE_SIZE * 2, clipBounds.x + clipBounds.width - xOffset).values()) {
final Rectangle tileBounds = getTileBounds(tile);
// System.out.print("Tile bounds: " + tileBounds);
if (tileBounds.intersects(clipBounds)) {
// System.out.println(" intersects");
final int dx = tileBounds.x + tileBounds.width / 2 - centerX;
final int dy = tileBounds.y + tileBounds.height - TILE_SIZE / 2 - centerY;
final int dist = (int) Math.sqrt((dx * dx) + (dy * dy));
if (dist < smallestDistance) {
smallestDistance = dist;
mostCentredTile = tile;
}
BufferedImage tileImg = renderedTiles.get(tile);
if (tileImg == null) {
tilesWaitingToBeRendered.add(0, tile);
tileImg = dirtyTiles.get(tile);
}
if (tileImg != null) {
if (tileImg != TILE_NOT_RENDERABLE) {
g2.drawImage(tileImg, tileBounds.x, tileBounds.y, null);
} else {
g2.setColor(Color.RED);
g2.drawRect(tileBounds.x, tileBounds.y, tileBounds.width, tileBounds.height);
g2.drawLine(tileBounds.x, tileBounds.y, tileBounds.x + tileBounds.width, tileBounds.y + tileBounds.height);
g2.drawLine(tileBounds.x + tileBounds.width, tileBounds.y, tileBounds.x, tileBounds.y + tileBounds.height);
}
}
// } else {
// System.out.println(" does NOT intersect");
}
}
}
if (mostCentredTile != null) {
centreTile = mostCentredTile;
}
if (highlightTile != null) {
g2.setColor(Color.RED);
Rectangle rect = getTileBounds(highlightTile.x, highlightTile.y, maxHeight);
g2.drawRect(rect.x, rect.y, rect.width, rect.height);
}
if (highlightPoint != null) {
g2.setColor(Color.RED);
g2.drawLine(highlightPoint.x - 2, highlightPoint.y, highlightPoint.x + 2, highlightPoint.y);
g2.drawLine(highlightPoint.x, highlightPoint.y - 2, highlightPoint.x, highlightPoint.y + 2);
}
// for (Map.Entry entry: renderedTiles.entrySet()) {
// Point tileCoords = entry.getKey();
// BufferedImage tileImg = entry.getValue();
// Rectangle tileBounds = getTileBounds(tileCoords.x, tileCoords.y);
// if (tileBounds.intersects(clipBounds)) {
// g2.drawImage(tileImg, tileBounds.x, tileBounds.y, null);
//// g2.setColor(Color.RED);
//// g2.drawRect(tileBounds.x, tileBounds.y, tileBounds.width, tileBounds.height);
// }
// }
}
private void scheduleTileForRendering(final Tile tile) {
// System.out.println("Scheduling tile for rendering: " + tile.getX() + ", " + tile.getY());
final JViewport parent = (JViewport) getParent();
if (parent == null) {
// This has been observed in the wild; possible after the 3D view has been removed from the 3D frame?
return;
}
doOnEventThread(() -> {
final Rectangle visibleArea = parent.getViewRect();
final Rectangle tileBounds = zoom(getTileBounds(tile));
if (tileBounds.intersects(visibleArea)) {
// The tile is (partially) visible, so it should be repainted immediately
switch (refreshMode) {
case IMMEDIATE:
threeDeeRenderManager.renderTile(tile);
break;
case DELAYED:
tilesWaitingToBeRendered.add(tile);
lastTileChange = System.currentTimeMillis();
break;
case MANUAL:
// Do nothing
break;
default:
throw new InternalError();
}
} else {
// The tile is not visible, so repaint it when it becomes visible
tilesWaitingToBeRendered.remove(tile);
renderedTiles.remove(tile);
}
});
}
private Rectangle getTileBounds(final Tile tile) {
return getTileBounds(tile.getX(), tile.getY(), Math.max(tile.getHighestIntHeight(), tile.getHighestWaterLevel()) + 1);
}
private Rectangle getTileBounds(final int x, final int y, final int maxHeight) {
return getTileBounds(x, y, maxHeight, xOffset, yOffset);
}
private Rectangle getTileBounds(final int x, final int y, final int maxHeight, final int xOffset, final int yOffset) {
switch (rotation) {
case 0:
return new Rectangle(xOffset + (x - y) * TILE_SIZE,
yOffset + (x + y) * TILE_SIZE / 2 + (this.maxHeight - maxHeight),
2 * TILE_SIZE,
TILE_SIZE + maxHeight - minHeight - 1);
case 1:
return new Rectangle(xOffset + ((maxY - y) - x) * TILE_SIZE,
yOffset + ((maxY - y) + x) * TILE_SIZE / 2 + (this.maxHeight - maxHeight),
2 * TILE_SIZE,
TILE_SIZE + maxHeight - minHeight - 1);
case 2:
return new Rectangle(xOffset + ((maxX - x) - (maxY - y)) * TILE_SIZE,
yOffset + ((maxX - x) + (maxY - y)) * TILE_SIZE / 2 + (this.maxHeight - maxHeight),
2 * TILE_SIZE,
TILE_SIZE + maxHeight - minHeight - 1);
case 3:
return new Rectangle(xOffset + (y - (maxX - x)) * TILE_SIZE,
yOffset + (y + (maxX - x)) * TILE_SIZE / 2 + (this.maxHeight - maxHeight),
2 * TILE_SIZE,
TILE_SIZE + maxHeight - minHeight - 1);
default:
throw new IllegalArgumentException();
}
}
/**
* Centre the view on a particular point in view coordinates.
*
* @param coords The point to centre on in view coordinates.
*/
private void moveTo(Point coords) {
Rectangle visibleRect = getVisibleRect();
scrollRectToVisible(new Rectangle(coords.x - visibleRect.width / 2, coords.y - visibleRect.height / 2, visibleRect.width, visibleRect.height));
}
private java.awt.Dimension zoom(java.awt.Dimension dimension) {
if (zoom < 1) {
dimension.width /= scale;
dimension.height /= scale;
} else if (zoom > 1) {
dimension.width *= scale;
dimension.height *= scale;
}
return dimension;
}
private Point zoom(Point point) {
if (zoom < 1) {
point.x /= scale;
point.y /= scale;
} else if (zoom > 1) {
point.x *= scale;
point.y *= scale;
}
return point;
}
private Rectangle zoom(Rectangle rectangle) {
if (zoom < 1) {
rectangle.x /= scale;
rectangle.y /= scale;
rectangle.width /= scale;
rectangle.height /= scale;
} else if (zoom > 1) {
rectangle.x *= scale;
rectangle.y *= scale;
rectangle.width *= scale;
rectangle.height *= scale;
}
return rectangle;
}
private Rectangle unzoom(Rectangle rectangle) {
if (zoom < 1) {
rectangle.x *= scale;
rectangle.y *= scale;
rectangle.width *= scale;
rectangle.height *= scale;
} else if (zoom > 1) {
rectangle.x /= scale;
rectangle.y /= scale;
rectangle.width /= scale;
rectangle.height /= scale;
}
return rectangle;
}
private java.awt.Dimension unzoom(java.awt.Dimension dimension) {
if (zoom < 1) {
dimension.width *= scale;
dimension.height *= scale;
} else if (zoom > 1) {
dimension.width /= scale;
dimension.height /= scale;
}
return dimension;
}
private final Dimension dimension;
private final Map renderedTiles = new HashMap<>(), dirtyTiles = new HashMap<>();
private final ThreeDeeRenderManager threeDeeRenderManager;
private final ColourScheme colourScheme;
private final List tilesWaitingToBeRendered = new LinkedList<>();
private final int minHeight, maxHeight;
private final int xOffset, yOffset, maxX, maxY;
private final int rotation;
private final SortedMap> zSortedTiles;
private final CustomBiomeManager customBiomeManager;
private final boolean upsideDown;
private Timer timer;
private long lastTileChange;
private RefreshMode refreshMode = RefreshMode.DELAYED;
private Tile centreTile;
private int waterLevel, zoom = 1, scale = 1;
private Point highlightTile, highlightPoint;
private LayerVisibilityMode layerVisibility;
private Set hiddenLayers;
public static final BufferedImage TILE_NOT_RENDERABLE = new BufferedImage(1, 1, TYPE_BYTE_BINARY);
// private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(ThreeDeeView.class);
private static final long serialVersionUID = 2011101701L;
public enum RefreshMode { IMMEDIATE, DELAYED, MANUAL }
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy