mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 07:24:16 +00:00
Add a palette explorer for previewing biome palette layers
/iris studio palette (alias pal) opens the explorer on the biome you stand in, listing each biome's palette layers instead of noise styles. The map shows the blocks the layer places, the raw noise, or both, and a side panel shows which noise values the layer produces, which value range each block owns, and how its real share of the ground compares to its weight. World generation and the preview now sample layers through the same IrisBiome methods, so the preview matches what the world places. Model used: Claude Opus 5.5
This commit is contained in:
@@ -252,6 +252,16 @@ public class CommandStudio implements DecreeExecutor {
|
||||
NoiseExplorerGUI.launch();
|
||||
}
|
||||
|
||||
@Decree(description = "Preview biome palette layers (External GUI)", aliases = "pal")
|
||||
public void palette(
|
||||
@Param(description = "The biome to open on", contextual = true)
|
||||
IrisBiome biome
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
sender().sendMessage(C.GREEN + "Opening Palette Explorer!");
|
||||
NoiseExplorerGUI.launchPalettes(biome == null ? null : biome.getLoadKey());
|
||||
}
|
||||
|
||||
@Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER)
|
||||
public void charge() {
|
||||
if (!IrisToolbelt.isIrisWorld(world())) {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class BlockPreviewColors {
|
||||
private static final Method MAP_COLOR_METHOD = findMapColorMethod();
|
||||
private static final double[] MAP_SHADES = {1D, 0.86D, 0.71D, 0.53D};
|
||||
private static final int NO_MAP_COLOR = 0;
|
||||
|
||||
private BlockPreviewColors() {
|
||||
}
|
||||
|
||||
static Map<String, Integer> colorsByBlock(List<BlockData> blocks) {
|
||||
Map<String, Integer> colors = new HashMap<>();
|
||||
List<Integer> usedColors = new ArrayList<>();
|
||||
for (BlockData block : blocks) {
|
||||
String key = block.getAsString(true);
|
||||
if (colors.containsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
int baseColor = baseColor(block);
|
||||
int color = firstUnusedShade(baseColor, usedColors);
|
||||
usedColors.add(color);
|
||||
colors.put(key, color);
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
// Blocks that share a map colour, like sand and sandstone, get darker shades the way Minecraft maps shade them.
|
||||
private static int firstUnusedShade(int baseColor, List<Integer> usedColors) {
|
||||
for (double shade : MAP_SHADES) {
|
||||
int shaded = shade(baseColor, shade);
|
||||
if (!usedColors.contains(shaded)) {
|
||||
return shaded;
|
||||
}
|
||||
}
|
||||
return baseColor;
|
||||
}
|
||||
|
||||
private static int baseColor(BlockData block) {
|
||||
int mapColor = mapColor(block);
|
||||
return mapColor == NO_MAP_COLOR ? colorFromName(block.getMaterial().getKey().getKey()) : mapColor;
|
||||
}
|
||||
|
||||
private static int mapColor(BlockData block) {
|
||||
if (MAP_COLOR_METHOD == null) {
|
||||
return NO_MAP_COLOR;
|
||||
}
|
||||
try {
|
||||
return ((org.bukkit.Color) MAP_COLOR_METHOD.invoke(block)).asRGB();
|
||||
} catch (ReflectiveOperationException | RuntimeException error) {
|
||||
return NO_MAP_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
private static int colorFromName(String name) {
|
||||
float hue = (name.hashCode() & 0xFFFF) / (float) 0xFFFF;
|
||||
return Color.HSBtoRGB(hue, 0.45F, 0.75F) & 0xFFFFFF;
|
||||
}
|
||||
|
||||
static int shade(int color, double shade) {
|
||||
int red = (int) Math.min(255, ((color >>> 16) & 0xFF) * shade);
|
||||
int green = (int) Math.min(255, ((color >>> 8) & 0xFF) * shade);
|
||||
int blue = (int) Math.min(255, (color & 0xFF) * shade);
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
// Iris compiles against the 1.20.1 API, which predates BlockData#getMapColor.
|
||||
private static Method findMapColorMethod() {
|
||||
try {
|
||||
return BlockData.class.getMethod("getMapColor");
|
||||
} catch (NoSuchMethodException error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 java.awt.Color;
|
||||
|
||||
interface NoiseColorScale {
|
||||
int colorFinite(double value);
|
||||
|
||||
int colorInvalid();
|
||||
|
||||
Color displayColorNormalized(double normalized);
|
||||
|
||||
String label();
|
||||
|
||||
double minimum();
|
||||
|
||||
double maximum();
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import com.volmit.iris.engine.object.IrisGenerator;
|
||||
import com.volmit.iris.engine.object.NoiseStyle;
|
||||
import com.volmit.iris.util.function.NoiseProvider;
|
||||
@@ -82,26 +84,27 @@ import java.util.function.LongFunction;
|
||||
public final class NoiseExplorerGUI extends JPanel {
|
||||
private static final long serialVersionUID = 2094606939770332040L;
|
||||
private static final Color BACKGROUND = new Color(12, 15, 22);
|
||||
private static final Color SIDEBAR_BACKGROUND = new Color(17, 20, 29);
|
||||
private static final Color PANEL_BACKGROUND = new Color(23, 27, 38);
|
||||
private static final Color FIELD_BACKGROUND = new Color(29, 34, 47);
|
||||
static final Color SIDEBAR_BACKGROUND = new Color(17, 20, 29);
|
||||
static final Color PANEL_BACKGROUND = new Color(23, 27, 38);
|
||||
static final Color FIELD_BACKGROUND = new Color(29, 34, 47);
|
||||
private static final Color SELECTED_BACKGROUND = new Color(42, 55, 78);
|
||||
private static final Color PRIMARY_TEXT = new Color(230, 234, 242);
|
||||
private static final Color SECONDARY_TEXT = new Color(151, 160, 178);
|
||||
private static final Color ACCENT = new Color(99, 161, 255);
|
||||
private static final Color BORDER = new Color(48, 55, 72);
|
||||
private static final Color ERROR = new Color(255, 116, 116);
|
||||
static final Color PRIMARY_TEXT = new Color(230, 234, 242);
|
||||
static final Color SECONDARY_TEXT = new Color(151, 160, 178);
|
||||
static final Color ACCENT = new Color(99, 161, 255);
|
||||
static final Color BORDER = new Color(48, 55, 72);
|
||||
static final Color ERROR = new Color(255, 116, 116);
|
||||
private static final Color GRID = new Color(255, 255, 255, 15);
|
||||
private static final Color CROSSHAIR = new Color(255, 255, 255, 125);
|
||||
private static final Color LEGEND_BACKGROUND = new Color(10, 12, 18, 215);
|
||||
private static final Font MONOSPACED_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
private static final Font HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11);
|
||||
private static final Font ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||
private static final Font LEGEND_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
|
||||
static final Font HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11);
|
||||
static final Font ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||
static final Font LEGEND_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
|
||||
private static final int SIDEBAR_WIDTH = 270;
|
||||
private static final int STATUS_HEIGHT = 50;
|
||||
private static final int REFINEMENT_DELAY_MILLIS = 180;
|
||||
private static final long DEFAULT_SEED = 12345L;
|
||||
private static final int SUMMARIZED_BLOCK_COUNT = 3;
|
||||
private static final long COARSE_SAMPLE_BUDGET = 4_096L;
|
||||
private static final long MAX_REFINED_SAMPLE_BUDGET = 48_000L;
|
||||
private static final double REFINEMENT_TARGET_MILLIS = 250D;
|
||||
@@ -109,6 +112,7 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
private static final NoiseCategory[] CATEGORY_ORDER = {
|
||||
NoiseCategory.CUSTOM,
|
||||
NoiseCategory.PACK_GENERATORS,
|
||||
NoiseCategory.PACK_PALETTES,
|
||||
NoiseCategory.SIMPLEX,
|
||||
NoiseCategory.PERLIN,
|
||||
NoiseCategory.CELLULAR,
|
||||
@@ -126,6 +130,8 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
NoiseCategory.OTHER
|
||||
};
|
||||
|
||||
private final ExplorerMode mode;
|
||||
private final String initialPackSourceId;
|
||||
private volatile Engine activeEngine;
|
||||
private final LongFunction<NoiseProvider> customFactory;
|
||||
private final String customName;
|
||||
@@ -146,12 +152,15 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
private final JLabel progressLabel = new JLabel("Opening noise explorer...");
|
||||
private final JLabel catalogLabel = new JLabel("Pack catalog waiting");
|
||||
private final JComboBox<NoisePalette> palettePicker = new JComboBox<>(NoisePalette.values());
|
||||
private final PaletteInspectorPanel paletteInspector = new PaletteInspectorPanel(new PaletteInspectorListener());
|
||||
|
||||
private List<SourceItem> packSources = List.of();
|
||||
private NoiseViewport viewport = new NoiseViewport(0D, 0D, 1D);
|
||||
private NoisePalette palette;
|
||||
private SourceItem selectedSource;
|
||||
private NoiseProvider sampler;
|
||||
private PaletteLayerPreview palettePreview;
|
||||
private int paletteDepth;
|
||||
private NoiseRenderCoordinator.Result renderResult;
|
||||
private BufferedImage renderedImage;
|
||||
private Point pointer;
|
||||
@@ -162,10 +171,14 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
private boolean sourceLoading;
|
||||
private boolean rendering;
|
||||
private boolean disposed;
|
||||
private boolean awaitingInitialPackSource;
|
||||
private String errorMessage;
|
||||
|
||||
private NoiseExplorerGUI(Engine engine, String customGeneratorKey, IrisGenerator fallbackGenerator,
|
||||
String customName, long initialSeed) {
|
||||
private NoiseExplorerGUI(ExplorerMode mode, Engine engine, String customGeneratorKey, IrisGenerator fallbackGenerator,
|
||||
String customName, long initialSeed, String initialPackSourceId) {
|
||||
this.mode = mode;
|
||||
this.initialPackSourceId = initialPackSourceId;
|
||||
this.awaitingInitialPackSource = mode == ExplorerMode.PALETTES;
|
||||
this.activeEngine = engine;
|
||||
this.customFactory = fallbackGenerator == null
|
||||
? null
|
||||
@@ -193,21 +206,33 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
public static void launch() {
|
||||
Engine engine = GuiHost.findActiveEngine();
|
||||
EventQueue.invokeLater(() -> {
|
||||
NoiseExplorerGUI explorer = new NoiseExplorerGUI(engine, null, null, null, DEFAULT_SEED);
|
||||
NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.NOISE, engine, null, null, null, DEFAULT_SEED, null);
|
||||
buildFrame("Noise Explorer", explorer);
|
||||
});
|
||||
}
|
||||
|
||||
public static void launchPalettes(String biomeKey) {
|
||||
Engine engine = GuiHost.findActiveEngine();
|
||||
String initialSourceId = biomeKey == null ? null : paletteSourceId(biomeKey, 0);
|
||||
String title = biomeKey == null ? "Palette Explorer" : "Palette Explorer: " + biomeKey;
|
||||
EventQueue.invokeLater(() -> {
|
||||
NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.PALETTES, engine, null, null, null, DEFAULT_SEED, initialSourceId);
|
||||
buildFrame(title, explorer);
|
||||
});
|
||||
}
|
||||
|
||||
public static void launchGeneratorKey(String generatorKey, IrisGenerator fallbackGenerator, long initialSeed) {
|
||||
Engine engine = GuiHost.findActiveEngine();
|
||||
String displayName = generatorKey == null || generatorKey.isBlank() ? "Custom Generator" : generatorKey;
|
||||
EventQueue.invokeLater(() -> {
|
||||
NoiseExplorerGUI explorer = new NoiseExplorerGUI(
|
||||
ExplorerMode.NOISE,
|
||||
engine,
|
||||
generatorKey,
|
||||
fallbackGenerator,
|
||||
displayName,
|
||||
initialSeed
|
||||
initialSeed,
|
||||
null
|
||||
);
|
||||
buildFrame("Noise Explorer: " + displayName, explorer);
|
||||
});
|
||||
@@ -219,6 +244,7 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
frame.setLayout(new BorderLayout());
|
||||
frame.add(explorer.buildSidebar(), BorderLayout.WEST);
|
||||
frame.add(explorer, BorderLayout.CENTER);
|
||||
frame.add(explorer.paletteInspector, BorderLayout.EAST);
|
||||
frame.setSize(1440, 820);
|
||||
frame.setMinimumSize(new Dimension(760, 500));
|
||||
frame.setLocationRelativeTo(null);
|
||||
@@ -339,21 +365,11 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
catalogLabel.setFont(ITEM_FONT);
|
||||
controls.add(catalogLabel);
|
||||
|
||||
JLabel seedTitle = new JLabel("Seed");
|
||||
seedTitle.setForeground(SECONDARY_TEXT);
|
||||
seedTitle.setFont(HEADER_FONT);
|
||||
controls.add(seedTitle);
|
||||
JPanel seedRow = new JPanel(new BorderLayout(7, 0));
|
||||
seedRow.setOpaque(false);
|
||||
configureTextField(seedField);
|
||||
JButton applySeed = createButton("Apply");
|
||||
applySeed.addActionListener(event -> applySeed());
|
||||
seedField.addActionListener(event -> applySeed());
|
||||
seedRow.add(seedField, BorderLayout.CENTER);
|
||||
seedRow.add(applySeed, BorderLayout.EAST);
|
||||
controls.add(seedRow);
|
||||
if (mode == ExplorerMode.NOISE) {
|
||||
addSeedControls(controls);
|
||||
}
|
||||
|
||||
JLabel paletteTitle = new JLabel("Palette");
|
||||
JLabel paletteTitle = new JLabel(mode == ExplorerMode.PALETTES ? "Noise view colours" : "Palette");
|
||||
paletteTitle.setForeground(SECONDARY_TEXT);
|
||||
paletteTitle.setFont(HEADER_FONT);
|
||||
controls.add(paletteTitle);
|
||||
@@ -375,6 +391,22 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
return controls;
|
||||
}
|
||||
|
||||
private void addSeedControls(JPanel controls) {
|
||||
JLabel seedTitle = new JLabel("Seed");
|
||||
seedTitle.setForeground(SECONDARY_TEXT);
|
||||
seedTitle.setFont(HEADER_FONT);
|
||||
controls.add(seedTitle);
|
||||
JPanel seedRow = new JPanel(new BorderLayout(7, 0));
|
||||
seedRow.setOpaque(false);
|
||||
configureTextField(seedField);
|
||||
JButton applySeed = createButton("Apply");
|
||||
applySeed.addActionListener(event -> applySeed());
|
||||
seedField.addActionListener(event -> applySeed());
|
||||
seedRow.add(seedField, BorderLayout.CENTER);
|
||||
seedRow.add(applySeed, BorderLayout.EAST);
|
||||
controls.add(seedRow);
|
||||
}
|
||||
|
||||
private static JButton createButton(String text) {
|
||||
JButton button = new JButton(text);
|
||||
button.setBackground(FIELD_BACKGROUND);
|
||||
@@ -423,6 +455,9 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
}
|
||||
|
||||
private List<SourceItem> buildFixedSources() {
|
||||
if (mode == ExplorerMode.PALETTES) {
|
||||
return List.of();
|
||||
}
|
||||
List<SourceItem> sources = new ArrayList<>();
|
||||
if (customFactory != null && customName != null) {
|
||||
sources.add(new SourceItem(
|
||||
@@ -431,7 +466,7 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
customName,
|
||||
NoiseCategory.CUSTOM,
|
||||
true,
|
||||
customFactory::apply
|
||||
(sourceSeed, depth) -> LoadedSource.of(customFactory.apply(sourceSeed))
|
||||
));
|
||||
}
|
||||
for (NoiseStyle style : NoiseStyle.values()) {
|
||||
@@ -441,9 +476,9 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
style.name(),
|
||||
categorize(style),
|
||||
true,
|
||||
sourceSeed -> {
|
||||
(sourceSeed, depth) -> {
|
||||
CNG noise = style.create(new RNG(sourceSeed));
|
||||
return (x, z) -> noise.noise(x, z);
|
||||
return LoadedSource.of((x, z) -> noise.noise(x, z));
|
||||
}
|
||||
));
|
||||
}
|
||||
@@ -489,11 +524,17 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
return (x, z) -> selectedGenerator.getHeight(x, z, mixedSeed);
|
||||
}
|
||||
|
||||
private static List<SourceItem> loadPackSources(Engine engine) {
|
||||
private List<SourceItem> loadPackSources(Engine engine) {
|
||||
if (engine == null) {
|
||||
return List.of();
|
||||
}
|
||||
IrisData data = engine.getData();
|
||||
return switch (mode) {
|
||||
case NOISE -> loadPackGeneratorSources(engine.getData());
|
||||
case PALETTES -> loadPackPaletteSources(engine);
|
||||
};
|
||||
}
|
||||
|
||||
private static List<SourceItem> loadPackGeneratorSources(IrisData data) {
|
||||
String[] keys = data.getGeneratorLoader().getPossibleKeys();
|
||||
if (keys == null || keys.length == 0) {
|
||||
return List.of();
|
||||
@@ -507,19 +548,67 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
key,
|
||||
NoiseCategory.PACK_GENERATORS,
|
||||
true,
|
||||
sourceSeed -> {
|
||||
(sourceSeed, depth) -> {
|
||||
IrisGenerator generator = data.getGeneratorLoader().load(key);
|
||||
if (generator == null) {
|
||||
throw new IllegalStateException("Pack generator not found: " + key);
|
||||
}
|
||||
long mixedSeed = new RNG(sourceSeed).nextParallelRNG(3245).lmax();
|
||||
return (x, z) -> generator.getHeight(x, z, mixedSeed);
|
||||
return LoadedSource.of((x, z) -> generator.getHeight(x, z, mixedSeed));
|
||||
}
|
||||
));
|
||||
}
|
||||
return List.copyOf(sources);
|
||||
}
|
||||
|
||||
// Palette layers always use the world's terrain seed: they share their cached noise with the running world.
|
||||
private static List<SourceItem> loadPackPaletteSources(Engine engine) {
|
||||
IrisData data = engine.getData();
|
||||
String[] keys = data.getBiomeLoader().getPossibleKeys();
|
||||
if (keys == null || keys.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
Arrays.sort(keys);
|
||||
List<SourceItem> sources = new ArrayList<>();
|
||||
for (String key : keys) {
|
||||
IrisBiome biome = data.getBiomeLoader().load(key);
|
||||
if (biome == null) {
|
||||
continue;
|
||||
}
|
||||
for (int layer = 0; layer < biome.getLayers().size(); layer++) {
|
||||
int selectedLayer = layer;
|
||||
sources.add(new SourceItem(
|
||||
paletteSourceId(key, layer),
|
||||
"Layer " + layer + " · " + summarizeBlocks(biome.getLayers().get(layer)),
|
||||
key,
|
||||
NoiseCategory.PACK_PALETTES,
|
||||
false,
|
||||
(sourceSeed, depth) -> {
|
||||
PaletteLayerPreview preview = PaletteLayerPreview.load(engine, key, selectedLayer, depth);
|
||||
return new LoadedSource(preview::sample, preview);
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
return List.copyOf(sources);
|
||||
}
|
||||
|
||||
private static String paletteSourceId(String biomeKey, int layer) {
|
||||
return "palette:" + biomeKey + "#" + layer;
|
||||
}
|
||||
|
||||
private static String summarizeBlocks(IrisBiomePaletteLayer layer) {
|
||||
List<String> names = layer.getPalette().stream()
|
||||
.map(block -> block.getBlock().replace("minecraft:", ""))
|
||||
.distinct()
|
||||
.toList();
|
||||
if (names.size() <= SUMMARIZED_BLOCK_COUNT) {
|
||||
return String.join(", ", names);
|
||||
}
|
||||
return String.join(", ", names.subList(0, SUMMARIZED_BLOCK_COUNT))
|
||||
+ " +" + (names.size() - SUMMARIZED_BLOCK_COUNT) + " more";
|
||||
}
|
||||
|
||||
private void applyCatalog(long revision, List<SourceItem> sources, Throwable error) {
|
||||
if (disposed || revision != catalogLoadRevision.get()) {
|
||||
return;
|
||||
@@ -529,17 +618,14 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
if (error != null) {
|
||||
catalogLabel.setForeground(ERROR);
|
||||
catalogLabel.setText("Pack catalog unavailable");
|
||||
} else if (sources.isEmpty()) {
|
||||
catalogLabel.setForeground(SECONDARY_TEXT);
|
||||
catalogLabel.setText("No active pack generators");
|
||||
} else {
|
||||
catalogLabel.setForeground(SECONDARY_TEXT);
|
||||
catalogLabel.setText(sources.size() + " pack generators");
|
||||
catalogLabel.setText(catalogSummary(sources));
|
||||
}
|
||||
boolean reloadSelectedPack = false;
|
||||
if (error == null
|
||||
&& selectedSource != null
|
||||
&& selectedSource.category() == NoiseCategory.PACK_GENERATORS) {
|
||||
&& selectedSource.category().isFromPack()) {
|
||||
SourceItem replacement = findSourceById(selectedSource.id(), sources);
|
||||
if (replacement == null) {
|
||||
failRemovedPackSource(selectedSource);
|
||||
@@ -552,6 +638,29 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
if (reloadSelectedPack) {
|
||||
loadSelectedSource();
|
||||
}
|
||||
if (error == null && awaitingInitialPackSource && !sources.isEmpty()) {
|
||||
awaitingInitialPackSource = false;
|
||||
selectInitialPackSource(sources);
|
||||
}
|
||||
}
|
||||
|
||||
private void selectInitialPackSource(List<SourceItem> sources) {
|
||||
SourceItem requested = initialPackSourceId == null ? null : findSourceById(initialPackSourceId, sources);
|
||||
SourceItem initial = requested == null ? sources.get(0) : requested;
|
||||
if (!selectSourceInList(initial.id())) {
|
||||
selectSource(initial);
|
||||
}
|
||||
}
|
||||
|
||||
private String catalogSummary(List<SourceItem> sources) {
|
||||
if (mode == ExplorerMode.NOISE) {
|
||||
return sources.isEmpty() ? "No active pack generators" : sources.size() + " pack generators";
|
||||
}
|
||||
if (resolveEngine() == null) {
|
||||
return "Open a studio world to see its palettes";
|
||||
}
|
||||
long biomes = sources.stream().map(SourceItem::rawName).distinct().count();
|
||||
return sources.size() + " palette layers in " + biomes + " biomes";
|
||||
}
|
||||
|
||||
private void failRemovedPackSource(SourceItem removedSource) {
|
||||
@@ -560,15 +669,15 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
viewRevision++;
|
||||
renderCoordinator.cancel(viewRevision);
|
||||
selectedSource = null;
|
||||
sampler = null;
|
||||
clearSampler();
|
||||
renderedImage = null;
|
||||
renderResult = null;
|
||||
sourceLoading = false;
|
||||
rendering = false;
|
||||
errorMessage = "Pack generator removed during hotload: " + removedSource.rawName();
|
||||
errorMessage = "Pack source removed during hotload: " + removedSource.text();
|
||||
selectedLabel.setText("No source selected");
|
||||
progressLabel.setForeground(ERROR);
|
||||
progressLabel.setText("Selected pack generator was removed");
|
||||
progressLabel.setText("Selected pack source was removed");
|
||||
sourceList.clearSelection();
|
||||
repaint();
|
||||
}
|
||||
@@ -586,6 +695,10 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
if (matching.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (category == NoiseCategory.PACK_PALETTES) {
|
||||
addGroupedByBiome(matching);
|
||||
continue;
|
||||
}
|
||||
sourceModel.addElement(new ListItem(categoryLabel(category), null));
|
||||
for (SourceItem source : matching) {
|
||||
sourceModel.addElement(new ListItem(source.text(), source));
|
||||
@@ -596,6 +709,17 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
private void addGroupedByBiome(List<SourceItem> paletteSources) {
|
||||
String currentBiome = null;
|
||||
for (SourceItem source : paletteSources) {
|
||||
if (!source.rawName().equals(currentBiome)) {
|
||||
currentBiome = source.rawName();
|
||||
sourceModel.addElement(new ListItem(currentBiome, null));
|
||||
}
|
||||
sourceModel.addElement(new ListItem(source.text(), source));
|
||||
}
|
||||
}
|
||||
|
||||
private List<SourceItem> matchingSources(NoiseCategory category, String filter) {
|
||||
List<SourceItem> matching = new ArrayList<>();
|
||||
addMatchingSources(matching, fixedSources, category, filter);
|
||||
@@ -633,7 +757,8 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
return;
|
||||
}
|
||||
selectedSource = source;
|
||||
selectedLabel.setText(source.text());
|
||||
paletteDepth = 0;
|
||||
selectedLabel.setText(source.category() == NoiseCategory.PACK_PALETTES ? source.rawName() : source.text());
|
||||
loadSelectedSource();
|
||||
}
|
||||
|
||||
@@ -644,6 +769,7 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
}
|
||||
long revision = sourceLoadRevision.incrementAndGet();
|
||||
long requestedSeed = seed;
|
||||
int requestedDepth = paletteDepth;
|
||||
refinementTimer.stop();
|
||||
viewRevision++;
|
||||
renderCoordinator.cancel(viewRevision);
|
||||
@@ -658,11 +784,11 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
repaint();
|
||||
submitLatest(sourceExecutor, () -> {
|
||||
try {
|
||||
NoiseProvider loadedSampler = source.factory().create(requestedSeed);
|
||||
if (loadedSampler == null) {
|
||||
LoadedSource loaded = source.factory().create(requestedSeed, requestedDepth);
|
||||
if (loaded == null || loaded.sampler() == null) {
|
||||
throw new IllegalStateException("Noise source returned no sampler: " + source.rawName());
|
||||
}
|
||||
EventQueue.invokeLater(() -> applyLoadedSource(revision, source, loadedSampler));
|
||||
EventQueue.invokeLater(() -> applyLoadedSource(revision, source, loaded));
|
||||
} catch (Throwable error) {
|
||||
Iris.reportError(error);
|
||||
EventQueue.invokeLater(() -> applySourceError(revision, source, error));
|
||||
@@ -670,11 +796,13 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
});
|
||||
}
|
||||
|
||||
private void applyLoadedSource(long revision, SourceItem source, NoiseProvider loadedSampler) {
|
||||
private void applyLoadedSource(long revision, SourceItem source, LoadedSource loaded) {
|
||||
if (disposed || revision != sourceLoadRevision.get() || selectedSource != source) {
|
||||
return;
|
||||
}
|
||||
sampler = loadedSampler;
|
||||
sampler = loaded.sampler();
|
||||
palettePreview = loaded.palettePreview();
|
||||
showPaletteInspector();
|
||||
sourceLoading = false;
|
||||
errorMessage = null;
|
||||
renderedImage = null;
|
||||
@@ -688,7 +816,7 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
if (disposed || revision != sourceLoadRevision.get() || selectedSource != source) {
|
||||
return;
|
||||
}
|
||||
sampler = null;
|
||||
clearSampler();
|
||||
sourceLoading = false;
|
||||
rendering = false;
|
||||
errorMessage = readableError(error);
|
||||
@@ -697,6 +825,28 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void clearSampler() {
|
||||
sampler = null;
|
||||
palettePreview = null;
|
||||
showPaletteInspector();
|
||||
}
|
||||
|
||||
private void showPaletteInspector() {
|
||||
if (palettePreview == null) {
|
||||
paletteInspector.hideInspector();
|
||||
} else {
|
||||
paletteInspector.showPreview(palettePreview);
|
||||
}
|
||||
if (paletteInspector.getParent() != null) {
|
||||
paletteInspector.getParent().revalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private NoiseColorScale activeColorScale() {
|
||||
PaletteLayerPreview preview = palettePreview;
|
||||
return preview == null ? palette : new PaletteLayerScale(paletteInspector.selectedView(), preview, palette);
|
||||
}
|
||||
|
||||
private void applySeed() {
|
||||
String rawSeed = seedField.getText().trim();
|
||||
try {
|
||||
@@ -767,7 +917,7 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
viewRevision,
|
||||
sampler,
|
||||
viewport,
|
||||
palette,
|
||||
activeColorScale(),
|
||||
getWidth(),
|
||||
renderHeight(),
|
||||
sampleStep
|
||||
@@ -935,6 +1085,7 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
}
|
||||
|
||||
private void drawLegend(Graphics2D canvas, int width) {
|
||||
NoiseColorScale colorScale = activeColorScale();
|
||||
int legendWidth = 190;
|
||||
int legendHeight = 40;
|
||||
int legendX = Math.max(10, width - legendWidth - 12);
|
||||
@@ -946,16 +1097,16 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
int gradientWidth = legendWidth - 18;
|
||||
for (int x = 0; x < gradientWidth; x++) {
|
||||
double normalized = x / (double) Math.max(1, gradientWidth - 1);
|
||||
canvas.setColor(palette.displayColorNormalized(normalized));
|
||||
canvas.setColor(colorScale.displayColorNormalized(normalized));
|
||||
canvas.drawLine(gradientX + x, gradientY, gradientX + x, gradientY + 8);
|
||||
}
|
||||
canvas.setFont(LEGEND_FONT);
|
||||
canvas.setColor(PRIMARY_TEXT);
|
||||
canvas.drawString(formatNumber(palette.minimum()), gradientX, legendY + 31);
|
||||
String maximum = formatNumber(palette.maximum());
|
||||
canvas.drawString(formatNumber(colorScale.minimum()), gradientX, legendY + 31);
|
||||
String maximum = formatNumber(colorScale.maximum());
|
||||
int maximumWidth = canvas.getFontMetrics().stringWidth(maximum);
|
||||
canvas.drawString(maximum, gradientX + gradientWidth - maximumWidth, legendY + 31);
|
||||
String label = palette.label();
|
||||
String label = colorScale.label();
|
||||
int labelWidth = canvas.getFontMetrics().stringWidth(label);
|
||||
canvas.drawString(label, legendX + ((legendWidth - labelWidth) / 2), legendY + 31);
|
||||
}
|
||||
@@ -1010,12 +1161,24 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
if (renderResult == null) {
|
||||
return coordinates + (catalogLoading ? " · loading catalog" : "");
|
||||
}
|
||||
PaletteLayerPreview preview = palettePreview;
|
||||
if (preview != null) {
|
||||
return coordinates + " · " + describePaletteAt(preview, worldX, worldZ);
|
||||
}
|
||||
NoiseRenderCoordinator.Result result = renderResult;
|
||||
long clipped = result.underflow() + result.overflow();
|
||||
return coordinates + " · range " + formatNumber(result.minimum()) + ".." + formatNumber(result.maximum())
|
||||
+ " · clipped " + clipped + " invalid " + result.invalid();
|
||||
}
|
||||
|
||||
private static String describePaletteAt(PaletteLayerPreview preview, double worldX, double worldZ) {
|
||||
double noise = preview.sample(worldX, worldZ);
|
||||
if (Double.isNaN(noise)) {
|
||||
return "layer doesn't reach this depth here";
|
||||
}
|
||||
return preview.entryForNoise(noise).block() + " · noise " + formatNumber(noise);
|
||||
}
|
||||
|
||||
private int renderHeight() {
|
||||
return Math.max(1, getHeight() - STATUS_HEIGHT);
|
||||
}
|
||||
@@ -1103,6 +1266,7 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
return switch (category) {
|
||||
case CUSTOM -> "Custom";
|
||||
case PACK_GENERATORS -> "Pack Generators";
|
||||
case PACK_PALETTES -> "Pack Palettes";
|
||||
case SIMPLEX -> "Simplex";
|
||||
case PERLIN -> "Perlin";
|
||||
case CELLULAR -> "Cellular";
|
||||
@@ -1131,7 +1295,13 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SourceFactory {
|
||||
NoiseProvider create(long seed);
|
||||
LoadedSource create(long seed, int paletteDepth);
|
||||
}
|
||||
|
||||
private record LoadedSource(NoiseProvider sampler, PaletteLayerPreview palettePreview) {
|
||||
private static LoadedSource of(NoiseProvider sampler) {
|
||||
return new LoadedSource(sampler, null);
|
||||
}
|
||||
}
|
||||
|
||||
private record SourceItem(String id, String text, String rawName, NoiseCategory category, boolean usesSeed,
|
||||
@@ -1141,6 +1311,22 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
private record ListItem(String text, SourceItem source) {
|
||||
}
|
||||
|
||||
private final class PaletteInspectorListener implements PaletteInspectorPanel.Listener {
|
||||
@Override
|
||||
public void onViewChanged(PaletteLayerScale.View view) {
|
||||
requestPreview();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDepthChanged(int depth) {
|
||||
if (depth == paletteDepth) {
|
||||
return;
|
||||
}
|
||||
paletteDepth = depth;
|
||||
loadSelectedSource();
|
||||
}
|
||||
}
|
||||
|
||||
private final class RenderListener implements NoiseRenderCoordinator.Listener {
|
||||
@Override
|
||||
public void onRenderStarted(NoiseRenderCoordinator.Request request) {
|
||||
@@ -1217,9 +1403,15 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
private enum ExplorerMode {
|
||||
NOISE,
|
||||
PALETTES
|
||||
}
|
||||
|
||||
private enum NoiseCategory {
|
||||
CUSTOM,
|
||||
PACK_GENERATORS,
|
||||
PACK_PALETTES,
|
||||
SIMPLEX,
|
||||
PERLIN,
|
||||
CELLULAR,
|
||||
@@ -1234,6 +1426,10 @@ public final class NoiseExplorerGUI extends JPanel {
|
||||
NOWHERE,
|
||||
SIERPINSKI,
|
||||
UTILITY,
|
||||
OTHER
|
||||
OTHER;
|
||||
|
||||
private boolean isFromPack() {
|
||||
return this == PACK_GENERATORS || this == PACK_PALETTES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ package com.volmit.iris.core.gui;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
enum NoisePalette {
|
||||
enum NoisePalette implements NoiseColorScale {
|
||||
TERRAIN("Terrain", 0D, 1D, new int[]{0x071A2F, 0x155E75, 0x2A9D8F, 0xE9C46A, 0xF4F1DE}),
|
||||
SIGNED("Signed", -1D, 1D, new int[]{0x173B66, 0x4F86C6, 0xE7EDF3, 0xE89555, 0x9D3B35}),
|
||||
GRAYSCALE("Grayscale", 0D, 1D, new int[]{0x050505, 0xFFFFFF});
|
||||
@@ -48,7 +48,8 @@ enum NoisePalette {
|
||||
return colorFinite(value);
|
||||
}
|
||||
|
||||
int colorFinite(double value) {
|
||||
@Override
|
||||
public int colorFinite(double value) {
|
||||
double normalized = (value - minimum) / (maximum - minimum);
|
||||
return colorNormalized(normalized);
|
||||
}
|
||||
@@ -58,20 +59,29 @@ enum NoisePalette {
|
||||
return lookup[(int) Math.round(clipped * (lookup.length - 1))];
|
||||
}
|
||||
|
||||
Color displayColorNormalized(double normalized) {
|
||||
@Override
|
||||
public int colorInvalid() {
|
||||
return INVALID_COLOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Color displayColorNormalized(double normalized) {
|
||||
double clipped = Math.max(0D, Math.min(1D, normalized));
|
||||
return displayColors[(int) Math.round(clipped * (displayColors.length - 1))];
|
||||
}
|
||||
|
||||
String label() {
|
||||
@Override
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
double minimum() {
|
||||
@Override
|
||||
public double minimum() {
|
||||
return minimum;
|
||||
}
|
||||
|
||||
double maximum() {
|
||||
@Override
|
||||
public double maximum() {
|
||||
return maximum;
|
||||
}
|
||||
|
||||
|
||||
@@ -222,9 +222,9 @@ final class NoiseRenderCoordinator implements AutoCloseable {
|
||||
double sampleOffset = request.sampleStep() * 0.5D;
|
||||
double startWorldX = request.viewport().worldX(sampleOffset, request.width());
|
||||
double worldStep = request.viewport().blocksPerPixel() * request.sampleStep();
|
||||
NoisePalette palette = request.palette();
|
||||
double paletteMinimum = palette.minimum();
|
||||
double paletteMaximum = palette.maximum();
|
||||
NoiseColorScale colorScale = request.colorScale();
|
||||
double paletteMinimum = colorScale.minimum();
|
||||
double paletteMaximum = colorScale.maximum();
|
||||
for (int y = fromY; y < toY; y++) {
|
||||
double screenY = (y * (double) request.sampleStep()) + sampleOffset;
|
||||
double worldZ = request.viewport().worldZ(screenY, request.height());
|
||||
@@ -238,10 +238,10 @@ final class NoiseRenderCoordinator implements AutoCloseable {
|
||||
double value = request.sampler().noise(worldX, worldZ);
|
||||
boolean center = centerRow && x == outputWidth / 2;
|
||||
if (Double.isFinite(value)) {
|
||||
pixels[pixelIndex++] = palette.colorFinite(value);
|
||||
pixels[pixelIndex++] = colorScale.colorFinite(value);
|
||||
stats.acceptFinite(value, center, paletteMinimum, paletteMaximum);
|
||||
} else {
|
||||
pixels[pixelIndex++] = NoisePalette.INVALID_COLOR;
|
||||
pixels[pixelIndex++] = colorScale.colorInvalid();
|
||||
stats.acceptInvalid(value, center);
|
||||
}
|
||||
worldX += worldStep;
|
||||
@@ -315,12 +315,12 @@ final class NoiseRenderCoordinator implements AutoCloseable {
|
||||
void onRenderFailed(Request request, Throwable error);
|
||||
}
|
||||
|
||||
record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoisePalette palette,
|
||||
record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoiseColorScale colorScale,
|
||||
int width, int height, int sampleStep) {
|
||||
Request {
|
||||
Objects.requireNonNull(sampler, "sampler");
|
||||
Objects.requireNonNull(viewport, "viewport");
|
||||
Objects.requireNonNull(palette, "palette");
|
||||
Objects.requireNonNull(colorScale, "colorScale");
|
||||
if (revision < 0L || width < 1 || height < 1 || sampleStep < 1) {
|
||||
throw new IllegalArgumentException("Invalid noise render request");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
* 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 javax.swing.BorderFactory;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSlider;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.GridLayout;
|
||||
import java.awt.RenderingHints;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.ACCENT;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.BORDER;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.FIELD_BACKGROUND;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.HEADER_FONT;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.ITEM_FONT;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.LEGEND_FONT;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.PANEL_BACKGROUND;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.PRIMARY_TEXT;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.SECONDARY_TEXT;
|
||||
import static com.volmit.iris.core.gui.NoiseExplorerGUI.SIDEBAR_BACKGROUND;
|
||||
|
||||
final class PaletteInspectorPanel extends JPanel {
|
||||
private static final long serialVersionUID = 4473102938518712001L;
|
||||
private static final int PANEL_WIDTH = 310;
|
||||
private static final Color WARNING = new Color(255, 196, 92);
|
||||
private static final Color HISTOGRAM_BAR = new Color(151, 160, 178, 170);
|
||||
private static final Color BAND_DIVIDER = new Color(255, 255, 255, 70);
|
||||
|
||||
private final Listener listener;
|
||||
private final JLabel titleLabel = new JLabel();
|
||||
private final JLabel detailLabel = new JLabel();
|
||||
private final JComboBox<PaletteLayerScale.View> viewPicker = new JComboBox<>(PaletteLayerScale.View.values());
|
||||
private final JLabel depthLabel = new JLabel();
|
||||
private final JSlider depthSlider = new JSlider(0, 0, 0);
|
||||
private final StatisticsView statisticsView = new StatisticsView();
|
||||
private boolean updatingControls;
|
||||
|
||||
PaletteInspectorPanel(Listener listener) {
|
||||
super(new BorderLayout());
|
||||
this.listener = listener;
|
||||
setPreferredSize(new Dimension(PANEL_WIDTH, 0));
|
||||
setBackground(SIDEBAR_BACKGROUND);
|
||||
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, BORDER));
|
||||
add(buildControls(), BorderLayout.NORTH);
|
||||
JScrollPane statisticsScroll = new JScrollPane(statisticsView);
|
||||
statisticsScroll.setBorder(BorderFactory.createEmptyBorder());
|
||||
statisticsScroll.getVerticalScrollBar().setUnitIncrement(18);
|
||||
add(statisticsScroll, BorderLayout.CENTER);
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
void showPreview(PaletteLayerPreview preview) {
|
||||
updatingControls = true;
|
||||
try {
|
||||
titleLabel.setText(preview.biomeKey() + " · layer " + preview.layer());
|
||||
detailLabel.setText(thicknessText(preview) + " · zoom " + formatZoom(preview.zoom())
|
||||
+ " (×" + formatZoom(preview.zoom() * preview.zoom()) + " effective)");
|
||||
int deepestDepth = Math.max(0, preview.maxThickness() - 1);
|
||||
depthSlider.setMaximum(deepestDepth);
|
||||
depthSlider.setValue(preview.depth());
|
||||
depthSlider.setEnabled(deepestDepth > 0);
|
||||
depthLabel.setText(depthText(preview.depth(), deepestDepth));
|
||||
} finally {
|
||||
updatingControls = false;
|
||||
}
|
||||
statisticsView.show(preview);
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
void hideInspector() {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
PaletteLayerScale.View selectedView() {
|
||||
return (PaletteLayerScale.View) viewPicker.getSelectedItem();
|
||||
}
|
||||
|
||||
private JPanel buildControls() {
|
||||
JPanel controls = new JPanel(new GridLayout(0, 1, 0, 6));
|
||||
controls.setBackground(PANEL_BACKGROUND);
|
||||
controls.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 0, BORDER),
|
||||
BorderFactory.createEmptyBorder(12, 14, 12, 14)
|
||||
));
|
||||
titleLabel.setForeground(PRIMARY_TEXT);
|
||||
titleLabel.setFont(ITEM_FONT.deriveFont(Font.BOLD, 14F));
|
||||
detailLabel.setForeground(SECONDARY_TEXT);
|
||||
detailLabel.setFont(ITEM_FONT);
|
||||
controls.add(titleLabel);
|
||||
controls.add(detailLabel);
|
||||
|
||||
controls.add(sectionLabel("View"));
|
||||
viewPicker.setBackground(FIELD_BACKGROUND);
|
||||
viewPicker.setForeground(PRIMARY_TEXT);
|
||||
viewPicker.addActionListener(event -> listener.onViewChanged(selectedView()));
|
||||
controls.add(viewPicker);
|
||||
|
||||
depthLabel.setForeground(SECONDARY_TEXT);
|
||||
depthLabel.setFont(HEADER_FONT);
|
||||
controls.add(depthLabel);
|
||||
depthSlider.setOpaque(false);
|
||||
depthSlider.addChangeListener(event -> {
|
||||
if (!updatingControls && !depthSlider.getValueIsAdjusting()) {
|
||||
listener.onDepthChanged(depthSlider.getValue());
|
||||
}
|
||||
});
|
||||
depthSlider.addChangeListener(event -> depthLabel.setText(depthText(depthSlider.getValue(), depthSlider.getMaximum())));
|
||||
controls.add(depthSlider);
|
||||
return controls;
|
||||
}
|
||||
|
||||
private static JLabel sectionLabel(String text) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.setForeground(SECONDARY_TEXT);
|
||||
label.setFont(HEADER_FONT);
|
||||
return label;
|
||||
}
|
||||
|
||||
private static String thicknessText(PaletteLayerPreview preview) {
|
||||
if (preview.minThickness() == preview.maxThickness()) {
|
||||
return preview.maxThickness() + (preview.maxThickness() == 1 ? " block thick" : " blocks thick");
|
||||
}
|
||||
return preview.minThickness() + "–" + preview.maxThickness() + " blocks thick";
|
||||
}
|
||||
|
||||
private static String depthText(int depth, int deepestDepth) {
|
||||
if (deepestDepth == 0) {
|
||||
return "Depth: top block only";
|
||||
}
|
||||
return "Depth " + depth + (depth == 0 ? " (top)" : "") + " of " + deepestDepth;
|
||||
}
|
||||
|
||||
private static String formatZoom(double zoom) {
|
||||
return zoom == Math.rint(zoom) ? Long.toString((long) zoom) : String.format(Locale.ROOT, "%.2f", zoom);
|
||||
}
|
||||
|
||||
private static String percent(double share) {
|
||||
if (share > 0D && share < 0.001D) {
|
||||
return "<0.1%";
|
||||
}
|
||||
return String.format(Locale.ROOT, "%.1f%%", share * 100D);
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void onViewChanged(PaletteLayerScale.View view);
|
||||
|
||||
void onDepthChanged(int depth);
|
||||
}
|
||||
|
||||
private static final class StatisticsView extends JComponent {
|
||||
private static final long serialVersionUID = -2318840125766150871L;
|
||||
private static final int MARGIN = 14;
|
||||
private static final int HISTOGRAM_HEIGHT = 84;
|
||||
private static final int BAND_HEIGHT = 12;
|
||||
private static final int ROW_HEIGHT = 20;
|
||||
private static final int SWATCH_SIZE = 10;
|
||||
private static final int WEIGHT_COLUMN_WIDTH = 48;
|
||||
private static final int ACTUAL_COLUMN_WIDTH = 70;
|
||||
private static final int NOTE_LINE_HEIGHT = 15;
|
||||
|
||||
private PaletteLayerPreview preview;
|
||||
|
||||
private StatisticsView() {
|
||||
setOpaque(true);
|
||||
setBackground(SIDEBAR_BACKGROUND);
|
||||
}
|
||||
|
||||
private void show(PaletteLayerPreview nextPreview) {
|
||||
preview = nextPreview;
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
if (preview == null) {
|
||||
return new Dimension(PANEL_WIDTH - 20, 0);
|
||||
}
|
||||
int width = Math.max(getWidth(), PANEL_WIDTH - 20) - MARGIN * 2;
|
||||
FontMetrics noteMetrics = getFontMetrics(LEGEND_FONT);
|
||||
int noteLines = 0;
|
||||
for (String note : notes(preview)) {
|
||||
noteLines += wrap(note, noteMetrics, width).size() + 1;
|
||||
}
|
||||
int height = MARGIN + 18 + HISTOGRAM_HEIGHT + BAND_HEIGHT + 30
|
||||
+ 22 + ROW_HEIGHT * (preview.entries().size() + 1)
|
||||
+ 16 + noteLines * NOTE_LINE_HEIGHT + MARGIN;
|
||||
return new Dimension(PANEL_WIDTH - 20, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics graphics) {
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
canvas.setColor(SIDEBAR_BACKGROUND);
|
||||
canvas.fillRect(0, 0, getWidth(), getHeight());
|
||||
if (preview == null) {
|
||||
return;
|
||||
}
|
||||
int width = getWidth() - MARGIN * 2;
|
||||
int y = drawValueSpread(canvas, MARGIN, MARGIN, width);
|
||||
y = drawBlockTable(canvas, MARGIN, y + 22, width);
|
||||
drawNotes(canvas, MARGIN, y + 16, width);
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private int drawValueSpread(Graphics2D canvas, int x, int y, int width) {
|
||||
drawSectionTitle(canvas, "NOISE VALUES THIS LAYER PRODUCES", x, y + 10);
|
||||
int histogramTop = y + 18;
|
||||
long[] histogram = preview.histogram();
|
||||
long peak = 1L;
|
||||
for (long count : histogram) {
|
||||
peak = Math.max(peak, count);
|
||||
}
|
||||
canvas.setColor(HISTOGRAM_BAR);
|
||||
for (int bin = 0; bin < histogram.length; bin++) {
|
||||
int barLeft = x + (bin * width) / histogram.length;
|
||||
int barRight = x + ((bin + 1) * width) / histogram.length;
|
||||
int barHeight = (int) Math.round(HISTOGRAM_HEIGHT * (histogram[bin] / (double) peak));
|
||||
canvas.fillRect(barLeft, histogramTop + HISTOGRAM_HEIGHT - barHeight, Math.max(1, barRight - barLeft - 1), barHeight);
|
||||
}
|
||||
|
||||
int bandTop = histogramTop + HISTOGRAM_HEIGHT + 2;
|
||||
for (PaletteLayerPreview.Entry entry : preview.entries()) {
|
||||
if (Double.isNaN(entry.lowestNoise())) {
|
||||
continue;
|
||||
}
|
||||
int bandLeft = x + (int) Math.round(entry.lowestNoise() * width);
|
||||
int bandRight = x + (int) Math.round(entry.highestNoise() * width);
|
||||
canvas.setColor(new Color(entry.color()));
|
||||
canvas.fillRect(bandLeft, bandTop, Math.max(1, bandRight - bandLeft), BAND_HEIGHT);
|
||||
if (entry.highestNoise() < 1D) {
|
||||
canvas.setColor(BAND_DIVIDER);
|
||||
canvas.drawLine(bandRight, histogramTop, bandRight, bandTop + BAND_HEIGHT - 1);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.setFont(LEGEND_FONT);
|
||||
canvas.setColor(SECONDARY_TEXT);
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
int labelY = bandTop + BAND_HEIGHT + 13;
|
||||
canvas.drawString("0", x, labelY);
|
||||
canvas.drawString("0.5", x + (width - metrics.stringWidth("0.5")) / 2, labelY);
|
||||
canvas.drawString("1", x + width - metrics.stringWidth("1"), labelY);
|
||||
return labelY;
|
||||
}
|
||||
|
||||
private int drawBlockTable(Graphics2D canvas, int x, int y, int width) {
|
||||
int actualRight = x + width;
|
||||
int weightRight = actualRight - ACTUAL_COLUMN_WIDTH;
|
||||
drawSectionTitle(canvas, "BLOCKS", x, y);
|
||||
drawRightAligned(canvas, "weight", weightRight, y);
|
||||
drawRightAligned(canvas, "actual", actualRight, y);
|
||||
|
||||
int rowY = y + ROW_HEIGHT;
|
||||
for (PaletteLayerPreview.Entry entry : preview.entries()) {
|
||||
canvas.setColor(new Color(entry.color()));
|
||||
canvas.fillRoundRect(x, rowY - SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, 3, 3);
|
||||
canvas.setFont(ITEM_FONT);
|
||||
canvas.setColor(entry.found() ? PRIMARY_TEXT : NoiseExplorerGUI.ERROR);
|
||||
String name = entry.found() ? shortBlockName(entry.block()) : shortBlockName(entry.block()) + " (not found)";
|
||||
int nameLeft = x + SWATCH_SIZE + 7;
|
||||
canvas.drawString(fit(canvas, name, weightRight - WEIGHT_COLUMN_WIDTH - nameLeft), nameLeft, rowY);
|
||||
canvas.setColor(SECONDARY_TEXT);
|
||||
drawRightAligned(canvas, percent(entry.weightShare()), weightRight, rowY);
|
||||
canvas.setColor(actualColor(entry));
|
||||
drawRightAligned(canvas, actualText(entry), actualRight, rowY);
|
||||
rowY += ROW_HEIGHT;
|
||||
}
|
||||
return rowY - ROW_HEIGHT;
|
||||
}
|
||||
|
||||
private void drawNotes(Graphics2D canvas, int x, int y, int width) {
|
||||
canvas.setFont(LEGEND_FONT);
|
||||
canvas.setColor(SECONDARY_TEXT);
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
int lineY = y;
|
||||
for (String note : notes(preview)) {
|
||||
for (String line : wrap(note, metrics, width)) {
|
||||
canvas.drawString(line, x, lineY);
|
||||
lineY += NOTE_LINE_HEIGHT;
|
||||
}
|
||||
lineY += 4;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> notes(PaletteLayerPreview preview) {
|
||||
List<String> notes = new ArrayList<>();
|
||||
notes.add("▼ gets under half its weight, ▲ gets over double.");
|
||||
notes.add(String.format(Locale.ROOT, "Measured on a %d × %d grid spread over %,.0f blocks around 0, 0.",
|
||||
PaletteLayerPreview.STATISTICS_GRID_SIZE, PaletteLayerPreview.STATISTICS_GRID_SIZE,
|
||||
PaletteLayerPreview.statisticsAreaBlocks()));
|
||||
if (preview.absentShare() > 0D) {
|
||||
notes.add("The layer doesn't reach this depth on " + percent(preview.absentShare())
|
||||
+ " of the ground. That ground is dark on the map and left out of the shares.");
|
||||
}
|
||||
if (preview.outOfRangeShare() > 0D) {
|
||||
notes.add(percent(preview.outOfRangeShare()) + " of the noise values fall outside 0 to 1."
|
||||
+ " Those spots get the first block.");
|
||||
}
|
||||
if (preview.hasSlopeCondition()) {
|
||||
notes.add("This layer has a slope condition, which the preview ignores.");
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
private static String actualText(PaletteLayerPreview.Entry entry) {
|
||||
if (entry.isFarBelowWeight()) {
|
||||
return "▼ " + percent(entry.actualShare());
|
||||
}
|
||||
if (entry.isFarAboveWeight()) {
|
||||
return "▲ " + percent(entry.actualShare());
|
||||
}
|
||||
return percent(entry.actualShare());
|
||||
}
|
||||
|
||||
private static Color actualColor(PaletteLayerPreview.Entry entry) {
|
||||
return entry.isFarBelowWeight() || entry.isFarAboveWeight() ? WARNING : PRIMARY_TEXT;
|
||||
}
|
||||
|
||||
private static void drawSectionTitle(Graphics2D canvas, String title, int x, int y) {
|
||||
canvas.setFont(HEADER_FONT);
|
||||
canvas.setColor(ACCENT);
|
||||
canvas.drawString(title, x, y);
|
||||
}
|
||||
|
||||
private static void drawRightAligned(Graphics2D canvas, String text, int right, int y) {
|
||||
canvas.drawString(text, right - canvas.getFontMetrics().stringWidth(text), y);
|
||||
}
|
||||
|
||||
private static String shortBlockName(String block) {
|
||||
return block.startsWith("minecraft:") ? block.substring("minecraft:".length()) : block;
|
||||
}
|
||||
|
||||
private static String fit(Graphics2D canvas, String text, int maxWidth) {
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
if (metrics.stringWidth(text) <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
String shortened = text;
|
||||
while (shortened.length() > 1 && metrics.stringWidth(shortened + "…") > maxWidth) {
|
||||
shortened = shortened.substring(0, shortened.length() - 1);
|
||||
}
|
||||
return shortened + "…";
|
||||
}
|
||||
|
||||
private static List<String> wrap(String text, FontMetrics metrics, int maxWidth) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
StringBuilder line = new StringBuilder();
|
||||
for (String word : text.split(" ")) {
|
||||
String candidate = line.isEmpty() ? word : line + " " + word;
|
||||
if (metrics.stringWidth(candidate) > maxWidth && !line.isEmpty()) {
|
||||
lines.add(line.toString());
|
||||
line = new StringBuilder(word);
|
||||
} else {
|
||||
line = new StringBuilder(candidate);
|
||||
}
|
||||
}
|
||||
if (!line.isEmpty()) {
|
||||
lines.add(line.toString());
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* 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.loader.IrisData;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import com.volmit.iris.engine.object.IrisBlockData;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class PaletteLayerPreview {
|
||||
static final int HISTOGRAM_BINS = 50;
|
||||
static final int STATISTICS_GRID_SIZE = 64;
|
||||
static final double STATISTICS_SPACING_BLOCKS = 37D;
|
||||
private static final int MISSING_BLOCK_COLOR = 0x5A5F6B;
|
||||
private static final double FAR_FROM_WEIGHT_FACTOR = 2D;
|
||||
|
||||
private final String biomeKey;
|
||||
private final IrisBiome biome;
|
||||
private final IrisBiomePaletteLayer paletteLayer;
|
||||
private final int layer;
|
||||
private final int depth;
|
||||
private final RNG random;
|
||||
private final IrisData data;
|
||||
private final int[] entryBySlot;
|
||||
private final int[] colorByEntry;
|
||||
private final List<Entry> entries;
|
||||
private final long[] histogram;
|
||||
private final double absentShare;
|
||||
private final double outOfRangeShare;
|
||||
|
||||
private PaletteLayerPreview(String biomeKey, IrisBiome biome, int layer, int depth, RNG random, IrisData data) {
|
||||
this.biomeKey = biomeKey;
|
||||
this.biome = biome;
|
||||
this.paletteLayer = biome.getLayers().get(layer);
|
||||
this.layer = layer;
|
||||
this.depth = depth;
|
||||
this.random = random;
|
||||
this.data = data;
|
||||
this.entryBySlot = paletteLayer.getPaletteEntryPerSlot(data).stream().mapToInt(Integer::intValue).toArray();
|
||||
if (entryBySlot.length == 0) {
|
||||
throw new IllegalStateException("No palette block in " + biomeKey + " layer " + layer + " could be found");
|
||||
}
|
||||
this.colorByEntry = resolveEntryColors();
|
||||
|
||||
Statistics statistics = measure();
|
||||
this.histogram = statistics.histogram();
|
||||
this.absentShare = statistics.absentShare();
|
||||
this.outOfRangeShare = statistics.outOfRangeShare();
|
||||
this.entries = describeEntries(statistics.sharesByEntry());
|
||||
}
|
||||
|
||||
static PaletteLayerPreview load(Engine engine, String biomeKey, int layer, int depth) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
throw new IllegalStateException("Open a studio world to preview pack palettes");
|
||||
}
|
||||
IrisData data = engine.getData();
|
||||
IrisBiome biome = data.getBiomeLoader().load(biomeKey);
|
||||
if (biome == null || layer >= biome.getLayers().size()) {
|
||||
throw new IllegalStateException("Palette layer removed during hotload: " + biomeKey + " layer " + layer);
|
||||
}
|
||||
RNG terrainRandom = new RNG(engine.getSeedManager().getTerrain());
|
||||
return new PaletteLayerPreview(biomeKey, biome, layer, depth, terrainRandom, data);
|
||||
}
|
||||
|
||||
double sample(double x, double z) {
|
||||
if (biome.getLayerThickness(layer, x, z, random, data) <= depth) {
|
||||
return Double.NaN;
|
||||
}
|
||||
return biome.getLayerNoise(layer, x, z, depth, random, data);
|
||||
}
|
||||
|
||||
int colorForNoise(double noise) {
|
||||
return colorByEntry[entryIndexForNoise(noise)];
|
||||
}
|
||||
|
||||
Entry entryForNoise(double noise) {
|
||||
return entries.get(entryIndexForNoise(noise));
|
||||
}
|
||||
|
||||
String biomeKey() {
|
||||
return biomeKey;
|
||||
}
|
||||
|
||||
int layer() {
|
||||
return layer;
|
||||
}
|
||||
|
||||
int depth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
int minThickness() {
|
||||
return paletteLayer.getMinHeight();
|
||||
}
|
||||
|
||||
int maxThickness() {
|
||||
return paletteLayer.getMaxHeight();
|
||||
}
|
||||
|
||||
double zoom() {
|
||||
return paletteLayer.getZoom();
|
||||
}
|
||||
|
||||
boolean hasSlopeCondition() {
|
||||
return !paletteLayer.getSlopeCondition().isDefault();
|
||||
}
|
||||
|
||||
List<Entry> entries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
long[] histogram() {
|
||||
return histogram.clone();
|
||||
}
|
||||
|
||||
double absentShare() {
|
||||
return absentShare;
|
||||
}
|
||||
|
||||
double outOfRangeShare() {
|
||||
return outOfRangeShare;
|
||||
}
|
||||
|
||||
static double statisticsAreaBlocks() {
|
||||
return STATISTICS_GRID_SIZE * STATISTICS_SPACING_BLOCKS;
|
||||
}
|
||||
|
||||
private int entryIndexForNoise(double noise) {
|
||||
return entryBySlot[IrisBiomePaletteLayer.slotForNoise(noise, entryBySlot.length)];
|
||||
}
|
||||
|
||||
private int[] resolveEntryColors() {
|
||||
KList<IrisBlockData> palette = paletteLayer.getPalette();
|
||||
List<BlockData> blocks = new ArrayList<>();
|
||||
for (IrisBlockData entry : palette) {
|
||||
BlockData block = entry.getBlockData(data);
|
||||
if (block != null) {
|
||||
blocks.add(block);
|
||||
}
|
||||
}
|
||||
Map<String, Integer> colorsByBlock = BlockPreviewColors.colorsByBlock(blocks);
|
||||
int[] colors = new int[palette.size()];
|
||||
for (int entry = 0; entry < palette.size(); entry++) {
|
||||
BlockData block = palette.get(entry).getBlockData(data);
|
||||
colors[entry] = block == null ? MISSING_BLOCK_COLOR : colorsByBlock.get(block.getAsString(true));
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
private Statistics measure() {
|
||||
long[] values = new long[HISTOGRAM_BINS];
|
||||
long[] samplesByEntry = new long[paletteLayer.getPalette().size()];
|
||||
long present = 0;
|
||||
long absent = 0;
|
||||
long outOfRange = 0;
|
||||
double origin = -statisticsAreaBlocks() / 2D;
|
||||
for (int gridX = 0; gridX < STATISTICS_GRID_SIZE; gridX++) {
|
||||
for (int gridZ = 0; gridZ < STATISTICS_GRID_SIZE; gridZ++) {
|
||||
double noise = sample(origin + gridX * STATISTICS_SPACING_BLOCKS, origin + gridZ * STATISTICS_SPACING_BLOCKS);
|
||||
if (Double.isNaN(noise)) {
|
||||
absent++;
|
||||
continue;
|
||||
}
|
||||
present++;
|
||||
if (noise < 0D || noise > 1D) {
|
||||
outOfRange++;
|
||||
}
|
||||
values[Math.clamp((int) (noise * HISTOGRAM_BINS), 0, HISTOGRAM_BINS - 1)]++;
|
||||
samplesByEntry[entryIndexForNoise(noise)]++;
|
||||
}
|
||||
}
|
||||
double[] sharesByEntry = new double[samplesByEntry.length];
|
||||
for (int entry = 0; entry < samplesByEntry.length; entry++) {
|
||||
sharesByEntry[entry] = present == 0 ? 0D : samplesByEntry[entry] / (double) present;
|
||||
}
|
||||
long total = present + absent;
|
||||
return new Statistics(values, sharesByEntry, absent / (double) total, present == 0 ? 0D : outOfRange / (double) present);
|
||||
}
|
||||
|
||||
private List<Entry> describeEntries(double[] sharesByEntry) {
|
||||
KList<IrisBlockData> palette = paletteLayer.getPalette();
|
||||
int slotCount = entryBySlot.length;
|
||||
List<Entry> described = new ArrayList<>(palette.size());
|
||||
for (int entry = 0; entry < palette.size(); entry++) {
|
||||
IrisBlockData block = palette.get(entry);
|
||||
int firstSlot = firstSlotOf(entry);
|
||||
int lastSlot = lastSlotOf(entry);
|
||||
boolean placed = firstSlot >= 0;
|
||||
described.add(new Entry(
|
||||
block.getBlock(),
|
||||
block.getWeight(),
|
||||
block.getBlockData(data) != null,
|
||||
colorByEntry[entry],
|
||||
placed ? (lastSlot - firstSlot + 1) / (double) slotCount : 0D,
|
||||
sharesByEntry[entry],
|
||||
placed ? slotLowerBound(firstSlot, slotCount) : Double.NaN,
|
||||
placed ? slotUpperBound(lastSlot, slotCount) : Double.NaN
|
||||
));
|
||||
}
|
||||
return List.copyOf(described);
|
||||
}
|
||||
|
||||
private int firstSlotOf(int entry) {
|
||||
for (int slot = 0; slot < entryBySlot.length; slot++) {
|
||||
if (entryBySlot[slot] == entry) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int lastSlotOf(int entry) {
|
||||
for (int slot = entryBySlot.length - 1; slot >= 0; slot--) {
|
||||
if (entryBySlot[slot] == entry) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Slots are picked by rounding, so the first and last slot only own half a step of the value range.
|
||||
private static double slotLowerBound(int slot, int slotCount) {
|
||||
return slot == 0 ? 0D : (slot - 0.5D) / (slotCount - 1);
|
||||
}
|
||||
|
||||
private static double slotUpperBound(int slot, int slotCount) {
|
||||
return slot == slotCount - 1 ? 1D : (slot + 0.5D) / (slotCount - 1);
|
||||
}
|
||||
|
||||
record Entry(String block, int weight, boolean found, int color, double weightShare, double actualShare,
|
||||
double lowestNoise, double highestNoise) {
|
||||
boolean isFarBelowWeight() {
|
||||
return found && actualShare < weightShare / FAR_FROM_WEIGHT_FACTOR;
|
||||
}
|
||||
|
||||
boolean isFarAboveWeight() {
|
||||
return found && actualShare > weightShare * FAR_FROM_WEIGHT_FACTOR;
|
||||
}
|
||||
}
|
||||
|
||||
private record Statistics(long[] histogram, double[] sharesByEntry, double absentShare, double outOfRangeShare) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 java.awt.Color;
|
||||
|
||||
final class PaletteLayerScale implements NoiseColorScale {
|
||||
static final int ABSENT_COLOR = 0x161A24;
|
||||
private static final double OVERLAY_DARKEST_SHADE = 0.3D;
|
||||
|
||||
private final View view;
|
||||
private final PaletteLayerPreview preview;
|
||||
private final NoisePalette noisePalette;
|
||||
|
||||
PaletteLayerScale(View view, PaletteLayerPreview preview, NoisePalette noisePalette) {
|
||||
this.view = view;
|
||||
this.preview = preview;
|
||||
this.noisePalette = noisePalette;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int colorFinite(double value) {
|
||||
return switch (view) {
|
||||
case BLOCKS -> preview.colorForNoise(value);
|
||||
case NOISE -> noisePalette.colorFinite(value);
|
||||
case BLOCKS_SHADED_BY_NOISE -> BlockPreviewColors.shade(preview.colorForNoise(value), overlayShade(value));
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int colorInvalid() {
|
||||
return ABSENT_COLOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Color displayColorNormalized(double normalized) {
|
||||
return new Color(colorFinite(minimum() + normalized * (maximum() - minimum())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return view == View.NOISE ? noisePalette.label() : view.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double minimum() {
|
||||
return view == View.NOISE ? noisePalette.minimum() : 0D;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double maximum() {
|
||||
return view == View.NOISE ? noisePalette.maximum() : 1D;
|
||||
}
|
||||
|
||||
private static double overlayShade(double value) {
|
||||
return OVERLAY_DARKEST_SHADE + (1D - OVERLAY_DARKEST_SHADE) * Math.clamp(value, 0D, 1D);
|
||||
}
|
||||
|
||||
enum View {
|
||||
BLOCKS("Blocks"),
|
||||
NOISE("Noise"),
|
||||
BLOCKS_SHADED_BY_NOISE("Blocks + noise");
|
||||
|
||||
private final String label;
|
||||
|
||||
View(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,8 +314,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
}
|
||||
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
CNG hgen = getLayerHeightGenerators(random, rdata).get(i);
|
||||
double d = hgen.fit(layers.get(i).getMinHeight(), layers.get(i).getMaxHeight(), wx / layers.get(i).getZoom(), wz / layers.get(i).getZoom());
|
||||
double d = getLayerThickness(i, wx, wz, random, rdata);
|
||||
|
||||
IrisSlopeClip sc = getLayers().get(i).getSlopeCondition();
|
||||
|
||||
@@ -335,7 +334,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
}
|
||||
|
||||
try {
|
||||
data.add(getLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / layers.get(i).getZoom(), j, (wz - j) / layers.get(i).getZoom(), rdata));
|
||||
data.add(getLayerBlock(i, wx, wz, j, random, rdata));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
@@ -360,6 +359,25 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
return data;
|
||||
}
|
||||
|
||||
public int getLayerThickness(int layer, double wx, double wz, RNG random, IrisData rdata) {
|
||||
IrisBiomePaletteLayer paletteLayer = layers.get(layer);
|
||||
CNG thicknessGenerator = getLayerHeightGenerators(random, rdata).get(layer);
|
||||
return thicknessGenerator.fit(paletteLayer.getMinHeight(), paletteLayer.getMaxHeight(), wx / paletteLayer.getZoom(), wz / paletteLayer.getZoom());
|
||||
}
|
||||
|
||||
public BlockData getLayerBlock(int layer, double wx, double wz, int depth, RNG random, IrisData rdata) {
|
||||
return layers.get(layer).getAtDepth(layerRandom(layer, random), wx, wz, depth, rdata);
|
||||
}
|
||||
|
||||
public double getLayerNoise(int layer, double wx, double wz, int depth, RNG random, IrisData rdata) {
|
||||
return layers.get(layer).getNoiseAtDepth(layerRandom(layer, random), wx, wz, depth, rdata);
|
||||
}
|
||||
|
||||
// Only the first call's random seeds the layer's cached generator, and generation always makes that call at depth 0.
|
||||
private static RNG layerRandom(int layer, RNG random) {
|
||||
return random.nextParallelRNG(layer);
|
||||
}
|
||||
|
||||
public KList<BlockData> generateCeilingLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
||||
KList<BlockData> data = new KList<>();
|
||||
|
||||
@@ -415,8 +433,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
}
|
||||
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
CNG hgen = getLayerHeightGenerators(random, rdata).get(i);
|
||||
double d = hgen.fit(layers.get(i).getMinHeight(), layers.get(i).getMaxHeight(), wx / layers.get(i).getZoom(), wz / layers.get(i).getZoom());
|
||||
double d = getLayerThickness(i, wx, wz, random, rdata);
|
||||
|
||||
IrisSlopeClip sc = getLayers().get(i).getSlopeCondition();
|
||||
|
||||
@@ -432,7 +449,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
|
||||
for (int j = 0; j < d; j++) {
|
||||
try {
|
||||
data.add(getLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / layers.get(i).getZoom(), j, (wz - j) / layers.get(i).getZoom(), rdata));
|
||||
data.add(getLayerBlock(i, wx, wz, j, random, rdata));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.data.cache.AtomicCache;
|
||||
import com.volmit.iris.engine.object.annotations.*;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.interpolation.IrisInterpolation;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.noise.CNG;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -69,15 +70,37 @@ public class IrisBiomePaletteLayer {
|
||||
}
|
||||
|
||||
public BlockData get(RNG rng, double x, double y, double z, IrisData data) {
|
||||
if (getBlockData(data).isEmpty()) {
|
||||
KList<BlockData> blocks = getBlockData(data);
|
||||
if (blocks.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (getBlockData(data).size() == 1) {
|
||||
return getBlockData(data).get(0);
|
||||
if (blocks.size() == 1) {
|
||||
return blocks.get(0);
|
||||
}
|
||||
|
||||
return getLayerGenerator(rng, data).fit(getBlockData(data), x / zoom, y / zoom, z / zoom);
|
||||
return blocks.get(slotForNoise(getLayerGenerator(rng, data).noise(x / zoom, y / zoom, z / zoom), blocks.size()));
|
||||
}
|
||||
|
||||
public BlockData getAtDepth(RNG rng, double wx, double wz, int depth, IrisData data) {
|
||||
return get(rng, depthShiftedX(wx, depth), depth, depthShiftedZ(wz, depth), data);
|
||||
}
|
||||
|
||||
public double getNoiseAtDepth(RNG rng, double wx, double wz, int depth, IrisData data) {
|
||||
return getLayerGenerator(rng, data).noise(depthShiftedX(wx, depth) / zoom, depth / zoom, depthShiftedZ(wz, depth) / zoom);
|
||||
}
|
||||
|
||||
public static int slotForNoise(double noise, int slotCount) {
|
||||
int slot = (int) Math.round(IrisInterpolation.lerp(0, slotCount - 1, noise));
|
||||
return slot >= 0 && slot < slotCount ? slot : 0;
|
||||
}
|
||||
|
||||
private double depthShiftedX(double wx, int depth) {
|
||||
return (wx + depth) / zoom;
|
||||
}
|
||||
|
||||
private double depthShiftedZ(double wz, int depth) {
|
||||
return (wz - depth) / zoom;
|
||||
}
|
||||
|
||||
public CNG getLayerGenerator(RNG rng, IrisData data) {
|
||||
@@ -98,19 +121,30 @@ public class IrisBiomePaletteLayer {
|
||||
return blockData.aquire(() ->
|
||||
{
|
||||
KList<BlockData> blockData = new KList<>();
|
||||
for (IrisBlockData ix : palette) {
|
||||
BlockData bx = ix.getBlockData(data);
|
||||
if (bx != null) {
|
||||
for (int i = 0; i < ix.getWeight(); i++) {
|
||||
blockData.add(bx);
|
||||
}
|
||||
}
|
||||
for (int entry : getPaletteEntryPerSlot(data)) {
|
||||
blockData.add(palette.get(entry).getBlockData(data));
|
||||
}
|
||||
|
||||
return blockData;
|
||||
});
|
||||
}
|
||||
|
||||
public KList<Integer> getPaletteEntryPerSlot(IrisData data) {
|
||||
KList<Integer> entries = new KList<>();
|
||||
for (int entry = 0; entry < palette.size(); entry++) {
|
||||
IrisBlockData block = palette.get(entry);
|
||||
if (block.getBlockData(data) == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < block.getWeight(); i++) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public IrisBiomePaletteLayer zero() {
|
||||
palette.clear();
|
||||
return this;
|
||||
|
||||
Reference in New Issue
Block a user