Remove the before and after hotload comparison from the map

The single before picture wasn't pulling its weight. The toolbar
buttons, the C shortcut and the swipe and changes overlays are gone
until there's a better way to compare pack versions.

Model used: Claude Opus 5.5
This commit is contained in:
piexel committed 2026-10-19 13:52:04 +02:00
1 parent 5363cbbc02
commit 74756dd19e
3 files changed
+5 -368

No files matched your search

@@ -1,279 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.components.MapStyle;
import com.volmit.iris.core.gui.components.RenderType;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.Locale;
/**
* Keeps the picture of the map from just before a hotload, and shows it next to the new one: either split by a
* draggable divider, or with everything that stayed the same faded out.
*/
final class HotloadComparison {
private static final double DEFAULT_SWIPE_FRACTION = 0.5D;
private static final int DIVIDER_GRAB_DISTANCE = 8;
private static final int DIVIDER_HANDLE_RADIUS = 9;
private static final int UNCHANGED_FADE_TARGET = 0x1E1F22;
private static final double UNCHANGED_FADE = 0.78D;
private static final Color DIVIDER = new Color(255, 255, 255, 220);
private BufferedImage before;
private Scene beforeScene;
private Mode mode = Mode.OFF;
private double swipeFraction = DEFAULT_SWIPE_FRACTION;
private BufferedImage changes;
private ChangesKey changesKey;
private double changedShare;
enum Mode {
OFF,
SWIPE,
CHANGES;
Mode next() {
Mode[] modes = values();
return modes[(ordinal() + 1) % modes.length];
}
}
/**
* What a map picture shows, so the before picture is only laid over a now picture of the same kind and zoom.
*/
record Scene(RenderType type, MapStyle style, double blocksPerPixel, double centerX, double centerZ) {
boolean looksLike(Scene other) {
return type == other.type && style.equals(other.style);
}
}
boolean hasBefore() {
return before != null;
}
Mode mode() {
return mode;
}
void setMode(Mode nextMode) {
mode = nextMode;
}
void capture(BufferedImage layer, Scene scene) {
BufferedImage copy = new BufferedImage(layer.getWidth(), layer.getHeight(), BufferedImage.TYPE_INT_RGB);
copy.setAccelerationPriority(0F);
Graphics2D graphics = copy.createGraphics();
try {
graphics.drawImage(layer, 0, 0, null);
} finally {
graphics.dispose();
}
before = copy;
beforeScene = scene;
changes = null;
changesKey = null;
}
void forget() {
before = null;
beforeScene = null;
changes = null;
changesKey = null;
mode = Mode.OFF;
}
boolean isShowing() {
return mode != Mode.OFF && before != null;
}
boolean isNearDivider(int screenX, int width) {
return isShowing() && mode == Mode.SWIPE
&& Math.abs(screenX - dividerX(width)) <= StudioTheme.scale(DIVIDER_GRAB_DISTANCE);
}
void moveDivider(int screenX, int width) {
swipeFraction = Math.max(0D, Math.min(1D, screenX / (double) Math.max(1, width)));
}
/**
* Draws the comparison in place of the plain map picture.
*
* @param nowVersion changes whenever the now picture is redrawn, so the change mask is only rebuilt when needed
* @param nowComplete whether every tile of the now picture has been drawn; changes are only counted then
*/
void paint(Graphics2D canvas, BufferedImage now, long nowVersion, Scene nowScene, boolean nowComplete) {
canvas.drawImage(now, 0, 0, null);
if (!beforeScene.looksLike(nowScene)) {
drawCaption(canvas, now.getWidth(), "Before and after needs the view and layers the hotload had");
return;
}
if (mode == Mode.SWIPE) {
paintSwipe(canvas, now.getWidth(), now.getHeight(), nowScene);
return;
}
if (beforeScene.blocksPerPixel() != nowScene.blocksPerPixel()) {
drawCaption(canvas, now.getWidth(), "Changes needs the zoom the hotload had");
return;
}
if (!nowComplete) {
drawCaption(canvas, now.getWidth(), "Comparing once the map has finished drawing");
return;
}
int offsetX = (int) Math.round(beforeScreenX(now.getWidth(), nowScene));
int offsetZ = (int) Math.round(beforeScreenZ(now.getHeight(), nowScene));
canvas.drawImage(changesImage(now, nowVersion, offsetX, offsetZ), 0, 0, null);
drawCaption(canvas, now.getWidth(), changesCaption());
}
private void paintSwipe(Graphics2D canvas, int width, int height, Scene now) {
int dividerX = dividerX(width);
double scale = beforeScene.blocksPerPixel() / now.blocksPerPixel();
Graphics2D beforeSide = (Graphics2D) canvas.create();
try {
beforeSide.clipRect(0, 0, dividerX, height);
beforeSide.drawImage(before,
(int) Math.round(beforeScreenX(width, now)),
(int) Math.round(beforeScreenZ(height, now)),
(int) Math.round(before.getWidth() * scale),
(int) Math.round(before.getHeight() * scale),
null);
} finally {
beforeSide.dispose();
}
canvas.setColor(DIVIDER);
canvas.setStroke(new BasicStroke(StudioTheme.scale(2)));
canvas.drawLine(dividerX, 0, dividerX, height);
int radius = StudioTheme.scale(DIVIDER_HANDLE_RADIUS);
canvas.fillOval(dividerX - radius, height / 2 - radius, radius * 2, radius * 2);
canvas.setColor(StudioTheme.EDITOR_BACKGROUND);
int gripHalfHeight = radius / 2;
int gripGap = Math.max(1, radius / 4);
canvas.drawLine(dividerX - gripGap, height / 2 - gripHalfHeight, dividerX - gripGap, height / 2 + gripHalfHeight);
canvas.drawLine(dividerX + gripGap, height / 2 - gripHalfHeight, dividerX + gripGap, height / 2 + gripHalfHeight);
drawSideLabel(canvas, "Before", dividerX - StudioTheme.scale(12), 1D);
drawSideLabel(canvas, "Now", dividerX + StudioTheme.scale(12), 0D);
}
private int dividerX(int width) {
return (int) Math.round(width * swipeFraction);
}
// Where the before picture's top left corner lands on screen now, after any panning, zooming or resizing since.
private double beforeScreenX(int width, Scene now) {
double beforeLeftWorld = beforeScene.centerX() - before.getWidth() * 0.5D * beforeScene.blocksPerPixel();
return width * 0.5D + (beforeLeftWorld - now.centerX()) / now.blocksPerPixel();
}
private double beforeScreenZ(int height, Scene now) {
double beforeTopWorld = beforeScene.centerZ() - before.getHeight() * 0.5D * beforeScene.blocksPerPixel();
return height * 0.5D + (beforeTopWorld - now.centerZ()) / now.blocksPerPixel();
}
private BufferedImage changesImage(BufferedImage now, long nowVersion, int offsetX, int offsetZ) {
ChangesKey key = new ChangesKey(nowVersion, offsetX, offsetZ, now.getWidth(), now.getHeight());
if (changes != null && key.equals(changesKey)) {
return changes;
}
int width = now.getWidth();
int height = now.getHeight();
int[] nowPixels = pixels(now);
int[] beforePixels = pixels(before);
BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
mask.setAccelerationPriority(0F);
int[] maskPixels = pixels(mask);
long compared = 0L;
long changed = 0L;
for (int y = 0; y < height; y++) {
int beforeY = y - offsetZ;
boolean rowInBefore = beforeY >= 0 && beforeY < before.getHeight();
for (int x = 0; x < width; x++) {
int index = y * width + x;
int nowColor = nowPixels[index] & 0xFFFFFF;
int beforeX = x - offsetX;
boolean inBefore = rowInBefore && beforeX >= 0 && beforeX < before.getWidth();
boolean differs = inBefore && (beforePixels[beforeY * before.getWidth() + beforeX] & 0xFFFFFF) != nowColor;
if (inBefore) {
compared++;
}
if (differs) {
changed++;
maskPixels[index] = nowColor;
} else {
maskPixels[index] = fade(nowColor);
}
}
}
changedShare = compared == 0L ? 0D : changed / (double) compared;
changes = mask;
changesKey = key;
return mask;
}
private String changesCaption() {
if (changedShare == 0D) {
return "Nothing on screen changed in the hotload";
}
double percent = changedShare * 100D;
String amount = percent < 0.1D ? "<0.1" : String.format(Locale.ROOT, percent < 10D ? "%.1f" : "%.0f", percent);
return amount + "% of the map on screen changed in the hotload";
}
private static int[] pixels(BufferedImage image) {
return ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
}
private static int fade(int color) {
int red = (int) (((color >> 16) & 0xFF) * (1D - UNCHANGED_FADE) + ((UNCHANGED_FADE_TARGET >> 16) & 0xFF) * UNCHANGED_FADE);
int green = (int) (((color >> 8) & 0xFF) * (1D - UNCHANGED_FADE) + ((UNCHANGED_FADE_TARGET >> 8) & 0xFF) * UNCHANGED_FADE);
int blue = (int) ((color & 0xFF) * (1D - UNCHANGED_FADE) + (UNCHANGED_FADE_TARGET & 0xFF) * UNCHANGED_FADE);
return (red << 16) | (green << 8) | blue;
}
private static void drawSideLabel(Graphics2D canvas, String text, int anchorX, double pushX) {
canvas.setFont(StudioTheme.boldFont());
int padding = StudioTheme.scale(8);
int width = canvas.getFontMetrics().stringWidth(text) + padding * 2;
int height = canvas.getFontMetrics().getHeight() + padding;
int x = (int) Math.round(anchorX - width * pushX);
int y = StudioTheme.scale(12);
StudioOverlay.drawCard(canvas, x, y, width, height);
canvas.setColor(StudioTheme.TEXT);
canvas.drawString(text, x + padding, y + padding / 2 + canvas.getFontMetrics().getAscent());
}
private static void drawCaption(Graphics2D canvas, int width, String text) {
canvas.setFont(StudioTheme.font());
int padding = StudioTheme.scale(10);
int captionWidth = canvas.getFontMetrics().stringWidth(text) + padding * 2;
int height = canvas.getFontMetrics().getHeight() + padding;
int x = (width - captionWidth) / 2;
int y = StudioTheme.scale(12);
StudioOverlay.drawCard(canvas, x, y, captionWidth, height);
canvas.setColor(StudioTheme.TEXT);
canvas.drawString(text, x + padding, y + padding / 2 + canvas.getFontMetrics().getAscent());
}
private record ChangesKey(long nowVersion, int offsetX, int offsetZ, int width, int height) {
}
}
@@ -33,7 +33,6 @@ import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
@@ -87,7 +86,6 @@ final class MapCanvas extends JPanel {
private final Timer notificationTimer;
private final Timer legendTimer;
private final Map<String, Long> notifications = new LinkedHashMap<>();
private final HotloadComparison comparison = new HotloadComparison();
private Engine engine;
private IrisRenderer renderer;
@@ -95,7 +93,6 @@ final class MapCanvas extends JPanel {
private VisionRenderController.Frame renderFrame;
private BufferedImage tileLayer;
private boolean tileLayerStale = true;
private long tileLayerVersion;
private MapStyle style = MapStyle.PLAIN;
private RenderType currentType = RenderType.BIOME;
private String viewLabel = MapView.BIOMES.label();
@@ -105,7 +102,6 @@ final class MapCanvas extends JPanel {
private MapHoverProbe.Details hoverDetails;
private Point cursor;
private Point dragOrigin;
private boolean draggingDivider;
private double centerX;
private double centerZ;
private long contentRevision = 1L;
@@ -184,14 +180,6 @@ final class MapCanvas extends JPanel {
return style;
}
HotloadComparison.Mode comparisonMode() {
return comparison.hasBefore() ? comparison.mode() : HotloadComparison.Mode.OFF;
}
boolean hasBeforePicture() {
return comparison.hasBefore();
}
void start() {
refreshMarkers();
requestRender();
@@ -238,27 +226,14 @@ final class MapCanvas extends JPanel {
notifyUser("No active Iris engine");
return;
}
boolean captured = captureBeforePicture();
renderer = new IrisRenderer(engine);
overlay = new VisionOverlay(engine, openerId);
contentRevision++;
requestRender();
notifyUser(captured && comparison.mode() == HotloadComparison.Mode.OFF
? "Refreshing · press C to compare with before"
: "Refreshing");
notifyUser("Refreshing");
listener.onTogglesChanged();
}
// Only a finished picture is worth comparing against; a half drawn one would show its gaps as changes.
private boolean captureBeforePicture() {
if (tileLayer == null || controller.progress(renderFrame).completion() < 1D) {
comparison.forget();
return false;
}
comparison.capture(currentTileLayer(), currentScene());
return true;
}
void toggleShading() {
applyStyle(style.withShading(!style.shading()));
notifyUser(style.shading() ? "Terrain shading on" : "Terrain shading off");
@@ -297,13 +272,11 @@ final class MapCanvas extends JPanel {
}
/**
* Esc steps back one layer of emphasis: the highlight first, then the comparison, then the help.
* Esc steps back one layer of emphasis: the highlight first, then the help.
*/
void dismissEmphasis() {
if (style.highlight() != null) {
clearHighlight();
} else if (comparisonMode() != HotloadComparison.Mode.OFF) {
showComparison(HotloadComparison.Mode.OFF);
} else if (help) {
toggleHelp();
}
@@ -314,26 +287,6 @@ final class MapCanvas extends JPanel {
notifyUser("Highlight cleared");
}
void cycleComparison() {
showComparison(comparisonMode().next());
}
void showComparison(HotloadComparison.Mode mode) {
if (!comparison.hasBefore()) {
notifyUser("Refresh or hotload the pack first, then compare with before");
listener.onTogglesChanged();
return;
}
comparison.setMode(mode);
notifyUser(switch (mode) {
case OFF -> "Comparison off";
case SWIPE -> "Drag the divider to compare before and now";
case CHANGES -> "Showing what changed in the hotload";
});
repaint();
listener.onTogglesChanged();
}
private void applyStyle(MapStyle next) {
boolean heightsWereNeeded = needsHeights();
style = next;
@@ -440,9 +393,6 @@ final class MapCanvas extends JPanel {
public void mouseMoved(MouseEvent event) {
cursor = event.getPoint();
setDetailedHover(event.isShiftDown());
setCursor(comparison.isNearDivider(event.getX(), getWidth())
? Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)
: Cursor.getDefaultCursor());
showQuickHover();
hoverTimer.restart();
refreshStatus();
@@ -451,11 +401,6 @@ final class MapCanvas extends JPanel {
@Override
public void mouseDragged(MouseEvent event) {
cursor = event.getPoint();
if (draggingDivider) {
comparison.moveDivider(event.getX(), getWidth());
repaint();
return;
}
if (dragOrigin == null) {
return;
}
@@ -480,8 +425,6 @@ final class MapCanvas extends JPanel {
cursor = event.getPoint();
if (event.isPopupTrigger()) {
showContextMenu(event);
} else if (SwingUtilities.isLeftMouseButton(event) && comparison.isNearDivider(event.getX(), getWidth())) {
draggingDivider = true;
} else if (SwingUtilities.isLeftMouseButton(event)) {
dragOrigin = event.getPoint();
}
@@ -490,7 +433,6 @@ final class MapCanvas extends JPanel {
@Override
public void mouseReleased(MouseEvent event) {
dragOrigin = null;
draggingDivider = false;
if (event.isPopupTrigger()) {
showContextMenu(event);
}
@@ -628,17 +570,7 @@ final class MapCanvas extends JPanel {
// The display server gets one finished picture per paint: handing it every tile separately made the map lag.
private void renderTiles(Graphics2D canvas) {
BufferedImage layer = currentTileLayer();
if (!comparison.isShowing()) {
canvas.drawImage(layer, 0, 0, null);
return;
}
boolean complete = controller.progress(renderFrame).completion() >= 1D;
comparison.paint(canvas, layer, tileLayerVersion, currentScene(), complete);
}
private HotloadComparison.Scene currentScene() {
return new HotloadComparison.Scene(currentType, style, blocksPerPixel, centerX, centerZ);
canvas.drawImage(currentTileLayer(), 0, 0, null);
}
private BufferedImage currentTileLayer() {
@@ -657,7 +589,6 @@ final class MapCanvas extends JPanel {
layer.dispose();
}
tileLayerStale = false;
tileLayerVersion++;
}
return tileLayer;
}
@@ -833,7 +764,7 @@ final class MapCanvas extends JPanel {
private void renderHover(Graphics2D canvas) {
QuickHover quick = quickHover;
if (quick == null || draggingDivider) {
if (quick == null) {
return;
}
MapHoverProbe.Details details = hoverDetails;
@@ -846,7 +777,7 @@ final class MapCanvas extends JPanel {
private void renderHelp(Graphics2D canvas) {
List<String> keys = List.of("Drag", "Wheel / + -", "\\", "Double-click", "Right-click", "Ctrl+Click", "Alt+Click",
"Shift", "H", "Esc", "S", "B", "C", "R", "F", "G", "E", "M", "1 to 0, P", "/");
"Shift", "H", "Esc", "S", "B", "R", "F", "G", "E", "M", "1 to 0, P", "/");
List<String> descriptions = List.of(
"Move the map",
"Zoom in and out",
@@ -860,7 +791,6 @@ final class MapCanvas extends JPanel {
"Clear the highlight",
"Terrain shading",
"Region borders",
"Compare with before the last hotload",
"Redraw the map",
"Follow your player",
"Chunk and region grid",
@@ -73,10 +73,6 @@ public final class MapWindow implements MapCanvas.Listener {
private final JToggleButton followToggle = StudioToolbar.toggle("Follow", "Keep your player in the middle (F)");
private final JToggleButton shadingToggle = StudioToolbar.toggle("Shading", "Shade hills and valleys on every view (S)");
private final JToggleButton bordersToggle = StudioToolbar.toggle("Borders", "Outline the regions on every view (B)");
private final JToggleButton swipeToggle = StudioToolbar.toggle("Before / after",
"Split the map between before and after the last hotload or refresh (C)");
private final JToggleButton changesToggle = StudioToolbar.toggle("Changes",
"Fade out everything the last hotload or refresh left the same (C twice)");
private final MapLegendPanel legendPanel;
private NoiseExplorerGUI paletteExplorer;
@@ -150,18 +146,11 @@ public final class MapWindow implements MapCanvas.Listener {
mapControls.add(toolbarSeparator());
mapControls.add(shadingToggle);
mapControls.add(bordersToggle);
mapControls.add(toolbarSeparator());
mapControls.add(swipeToggle);
mapControls.add(changesToggle);
gridToggle.addActionListener(event -> canvas.toggleGrid());
entitiesToggle.addActionListener(event -> canvas.toggleEntities());
followToggle.addActionListener(event -> canvas.toggleFollow());
shadingToggle.addActionListener(event -> canvas.toggleShading());
bordersToggle.addActionListener(event -> canvas.toggleRegionBorders());
swipeToggle.addActionListener(event -> canvas.showComparison(
swipeToggle.isSelected() ? HotloadComparison.Mode.SWIPE : HotloadComparison.Mode.OFF));
changesToggle.addActionListener(event -> canvas.showComparison(
changesToggle.isSelected() ? HotloadComparison.Mode.CHANGES : HotloadComparison.Mode.OFF));
mapControls.forEach(toolbar::add);
paletteControls.add(StudioToolbar.button("Reset view", "Go back to the starting position and zoom (R)", this::resetPaletteView));
@@ -297,8 +286,6 @@ public final class MapWindow implements MapCanvas.Listener {
followToggle.setSelected(canvas.isFollowing());
shadingToggle.setSelected(canvas.style().shading());
bordersToggle.setSelected(canvas.style().regionBorders());
swipeToggle.setSelected(canvas.comparisonMode() == HotloadComparison.Mode.SWIPE);
changesToggle.setSelected(canvas.comparisonMode() == HotloadComparison.Mode.CHANGES);
}
@Override
@@ -340,7 +327,6 @@ public final class MapWindow implements MapCanvas.Listener {
bindMap(rootPane, KeyEvent.VK_ESCAPE, 0, "dismiss", canvas::dismissEmphasis);
bindMap(rootPane, KeyEvent.VK_S, 0, "shading", canvas::toggleShading);
bindMap(rootPane, KeyEvent.VK_B, 0, "borders", canvas::toggleRegionBorders);
bindMap(rootPane, KeyEvent.VK_C, 0, "compare", canvas::cycleComparison);
bindMap(rootPane, KeyEvent.VK_EQUALS, 0, "zoom-in", canvas::zoomIn);
bindMap(rootPane, KeyEvent.VK_EQUALS, KeyEvent.SHIFT_DOWN_MASK, "zoom-in-plus", canvas::zoomIn);
bindMap(rootPane, KeyEvent.VK_ADD, 0, "zoom-in-keypad", canvas::zoomIn);