Show biome picking noise in the biome view

Noise switch, per-region breakdown.
This commit is contained in:
piexel committed 2026-10-21 21:06:00 +02:00
1 parent 4ac268047e
commit 3123ab3753
11 files changed
+687 -223

No files matched your search

@@ -0,0 +1,193 @@
/*
* 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.PickMeasuring.TypedSpread;
import com.volmit.iris.core.gui.components.PickDisplay;
import com.volmit.iris.engine.IrisComplex;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.InferredType;
import com.volmit.iris.engine.object.IrisRegion;
import com.volmit.iris.util.stream.ProceduralStream;
import javax.swing.JComponent;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* The biome view's explorer: every region picks the biomes on its ground from its own biome lists, so for each region
* in view it shows how its noise, or in hybrid terrain the height, picks them.
*/
final class BiomeExplorer {
private final Supplier<Engine> engineSource;
private final PickInspectorPanel inspector;
private final LatestMeasurement<Measurement> measurer = new LatestMeasurement<>("Iris Biome Explorer");
private MapCensus.ViewArea latestArea;
private MapCensus.ViewArea measuredArea;
/**
* @param onDisplayChosen receives how the map should draw the picks
*/
BiomeExplorer(Supplier<Engine> engineSource, Consumer<PickDisplay> onDisplayChosen) {
this.engineSource = engineSource;
this.inspector = new PickInspectorPanel("Biomes", onDisplayChosen);
}
PickInspectorPanel inspector() {
return inspector;
}
/**
* Measures the regions in this area and how they pick their biomes, unless that was already done.
*/
void measure(MapCensus.ViewArea area) {
latestArea = area;
if (area == null || area.equals(measuredArea)) {
return;
}
measuredArea = area;
measurer.measure(() -> measurePicks(area), this::show);
}
/**
* The pack changed, so the regions and their biomes are read again.
*/
void refresh() {
measuredArea = null;
measure(latestArea);
}
void close() {
measurer.close();
}
private void show(Measurement measurement) {
List<PickInspectorPanel.Section> sections = new ArrayList<>();
for (RegionPicks region : measurement.regions()) {
List<JComponent> charts = region.coverage() != null
? List.of(PickInspectorPanel.heightChart(region.coverage()))
: region.spreads().stream().map(spread -> (JComponent) new NoisePickChart(spread, "Biomes")).toList();
sections.add(new PickInspectorPanel.Section(region.name(), region.color(), region.share(), charts, region.notes()));
}
String detail = measurement.hybrid()
? "Hybrid terrain · each region picks biomes by height"
: "Each region splits its own biome noise by rarity";
inspector.showRegionSections(detail, sections, measurement.notes());
}
private Measurement measurePicks(MapCensus.ViewArea area) {
Engine engine = engineSource.get();
IrisComplex complex = engine == null ? null : engine.getComplex();
if (complex == null) {
return null;
}
Map<IrisRegion, RegionSamples> samplesByRegion = new IdentityHashMap<>();
int[] total = {0};
PickMeasuring.forEachSample(area, (x, z) -> {
IrisRegion region = complex.getRegionStream().get(x, z);
if (region == null) {
return;
}
total[0]++;
samplesByRegion.computeIfAbsent(region, subject -> new RegionSamples(engine, complex, subject)).add(x, z);
});
List<RegionPicks> regions = samplesByRegion.values().stream()
.sorted(Comparator.comparingInt(RegionSamples::count).reversed())
.map(samples -> samples.picks(engine, total[0]))
.toList();
List<String> notes = new ArrayList<>();
if (complex.isHybridTerrain()) {
notes.add(PickMeasuring.hybridNoiseNote());
} else {
PickMeasuring.addRarityNotes(notes, complex, regions.stream().flatMap(region -> region.spreads().stream()).toList());
}
notes.add(PickMeasuring.measuredNote());
return new Measurement(complex.isHybridTerrain(), regions, notes);
}
/**
* The spots in view one region owns. The biome view shows each spot's base biome, which the region picks from the
* biome list of the spot's continent type.
*/
private static final class RegionSamples {
private final IrisComplex complex;
private final IrisRegion region;
private final List<TypedSpread> spreads;
private final Map<InferredType, ProceduralStream<Double>> noiseByType = new EnumMap<>(InferredType.class);
private final List<Integer> pickingHeights = new ArrayList<>();
private int count;
private RegionSamples(Engine engine, IrisComplex complex, IrisRegion region) {
this.complex = complex;
this.region = region;
this.spreads = complex.isHybridTerrain() ? List.of() : PickMeasuring.biomeSpreads(engine, complex, region);
}
private void add(double x, double z) {
count++;
if (complex.isHybridTerrain()) {
pickingHeights.add((int) Math.round(complex.getPlacementHeightStream().get(x, z)));
return;
}
InferredType type = complex.getBridgeStream().get(x, z);
for (TypedSpread typed : spreads) {
if (typed.type() == type) {
typed.spread().add(noiseByType.computeIfAbsent(type, shown -> complex.biomePickNoiseStream(region, shown)).get(x, z));
}
}
}
private int count() {
return count;
}
private RegionPicks picks(Engine engine, int totalCount) {
double share = totalCount == 0 ? 0D : count / (double) totalCount;
String name = region.getName();
int color = PickMeasuring.regionColor(engine, region);
List<String> notes = new ArrayList<>();
if (complex.isHybridTerrain()) {
RegionHeightCoverage coverage = RegionHeightCoverage.measure(PickMeasuring.biomeRanges(engine, region),
pickingHeights.stream().mapToInt(Integer::intValue).toArray());
notes.addAll(coverage.notes());
return new RegionPicks(name, color, share, List.of(), coverage, notes);
}
List<TypedSpread> inView = spreads.stream().filter(typed -> typed.spread().sampleCount() > 0).toList();
PickMeasuring.addBiomeSpreadNotes(notes, inView);
PickMeasuring.addChildBiomeNote(notes, complex, region);
return new RegionPicks(name, color, share, inView.stream().map(TypedSpread::spread).toList(), null, notes);
}
}
/**
* @param coverage how the region's biomes cover the heights in view in hybrid terrain, or null in biome terrain
*/
private record RegionPicks(String name, int color, double share, List<NoisePickSpread> spreads,
RegionHeightCoverage coverage, List<String> notes) {
}
private record Measurement(boolean hybrid, List<RegionPicks> regions, List<String> notes) {
}
}
@@ -0,0 +1,81 @@
/*
* 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.Iris;
import java.awt.EventQueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Measures off the Swing thread, one measurement at a time, and only hands back the newest one asked for.
*/
final class LatestMeasurement<T> {
private final ThreadPoolExecutor worker;
private final AtomicLong generation = new AtomicLong();
LatestMeasurement(String threadName) {
this.worker = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), task -> {
Thread thread = new Thread(task, threadName);
thread.setDaemon(true);
return thread;
}, new ThreadPoolExecutor.DiscardOldestPolicy());
}
/**
* Runs the measurement and shows its result on the Swing thread, unless another was asked for meanwhile or it
* measured nothing.
*/
void measure(Supplier<T> measurement, Consumer<T> show) {
long requested = generation.incrementAndGet();
worker.execute(() -> {
T result = measureSafely(measurement);
EventQueue.invokeLater(() -> {
if (requested == generation.get() && result != null) {
show.accept(result);
}
});
});
}
/**
* Drops the measurement under way, whose result no longer fits.
*/
void discardPending() {
generation.incrementAndGet();
}
void close() {
worker.shutdownNow();
}
private static <T> T measureSafely(Supplier<T> measurement) {
try {
return measurement.get();
} catch (Throwable error) {
Iris.reportError(error);
return null;
}
}
}
@@ -227,15 +227,19 @@ final class MapCanvas extends JPanel {
}
}
PickDisplay pickDisplay() {
return pickDisplay;
}
/**
* Draws the region view's picks, the noise that picks them, or both.
* Draws the region or biome view's picks, the noise that picks them, or both.
*/
void showPicksAs(PickDisplay display) {
if (display == pickDisplay) {
return;
}
pickDisplay = display;
if (currentType.isRegionView()) {
if (currentType.drawsPickNoise()) {
requestRender();
}
}
@@ -247,6 +251,7 @@ final class MapCanvas extends JPanel {
private MapLayer currentLayer() {
return switch (currentType) {
case BIOME -> MapLayer.biomes(pickDisplay);
case REGION -> MapLayer.regions(pickDisplay);
case REGION_PREVIEW -> MapLayer.regionPreview(previewRegionKey, pickDisplay);
default -> MapLayer.of(currentType);
@@ -1098,7 +1103,7 @@ final class MapCanvas extends JPanel {
double worldZ = screenToWorldZ(point.y);
controller.submitProbe(
frame,
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), currentType.isRegionView()),
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), currentType.drawsPickNoise()),
details -> {
QuickHover quick = quickHover;
if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) {
@@ -68,17 +68,20 @@ final class MapHoverProbe {
dimension == null ? 0D : dimension.getFluidHeight(),
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
complex.explainHybridChoice(region, worldX, worldZ),
explainPicks ? pickNoise(complex, region, previewRegion == null, worldX, worldZ) : null
explainPicks ? pickNoise(complex, region, previewRegion, worldX, worldZ) : null
);
}
// HYBRID terrain picks biomes by height, which the placement line already tells, so only the region noise is left.
private static PickNoise pickNoise(IrisComplex complex, IrisRegion region, boolean regionPicked, double x, double z) {
Double regionNoise = regionPicked ? complex.getRegionStyleStream().get(x, z) : null;
// The preview shows surface biomes, which the height can move to another biome list; the real map shows base biomes.
private static PickNoise pickNoise(IrisComplex complex, IrisRegion region, IrisRegion previewRegion, double x, double z) {
Double regionNoise = previewRegion == null ? complex.getRegionStyleStream().get(x, z) : null;
if (complex.isHybridTerrain() || region == null) {
return new PickNoise(regionNoise, null, Double.NaN, null);
}
InferredType biomeType = complex.surfacePickAsIfIn(region, x, z).type();
InferredType biomeType = previewRegion != null
? complex.surfacePickAsIfIn(region, x, z).type()
: complex.getBridgeStream().get(x, z);
return new PickNoise(regionNoise, biomeType, complex.biomePickNoise(region, biomeType, x, z),
complex.biomePickedByNoise(region, biomeType, x, z));
}
@@ -78,7 +78,7 @@ public final class MapWindow implements MapCanvas.Listener {
private final JPanel inspectorSlot = new JPanel(new BorderLayout());
private final JComponent viewListScroll;
private final JSplitPane explorerSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
private final JSplitPane regionInspectorSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
private final JSplitPane pickInspectorSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
private final List<JComponent> mapControls = new ArrayList<>();
private final List<JComponent> explorerControls = new ArrayList<>();
private final List<JComponent> noiseControls = new ArrayList<>();
@@ -92,6 +92,7 @@ public final class MapWindow implements MapCanvas.Listener {
private final JToggleButton bordersToggle = StudioToolbar.toggle("Borders", "Outline the regions on every view (B)");
private final MapLegendPanel legendPanel;
private final RegionExplorer regionExplorer;
private final BiomeExplorer biomeExplorer;
private NoiseExplorerGUI paletteExplorer;
private final Map<MapView, ObjectExplorer> objectExplorers = new EnumMap<>(MapView.class);
@@ -109,6 +110,7 @@ public final class MapWindow implements MapCanvas.Listener {
this.viewListScroll = StudioToolbar.scroll(viewList);
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
this.regionExplorer = new RegionExplorer(canvas::engine, canvas::previewRegion, canvas::showPicksAs);
this.biomeExplorer = new BiomeExplorer(canvas::engine, canvas::showPicksAs);
canvas.centerOn(start.centerX(), start.centerZ());
buildFrame();
}
@@ -135,7 +137,7 @@ public final class MapWindow implements MapCanvas.Listener {
private void buildFrame() {
styleExplorerSplit();
styleRegionInspectorSplit();
stylePickInspectorSplit();
content.add(canvas, MAP_CARD);
statusBars.add(canvas.statusBar(), MAP_CARD);
sidebar.add(viewListScroll, BorderLayout.CENTER);
@@ -228,6 +230,8 @@ public final class MapWindow implements MapCanvas.Listener {
inspectorSlot.removeAll();
if (view == MapView.REGIONS) {
showRegionExplorer(previousView);
} else if (view == MapView.BIOMES) {
showBiomeExplorer();
} else {
useSidebar(viewListScroll);
inspectorSlot.add(legendPanel, BorderLayout.CENTER);
@@ -242,19 +246,31 @@ public final class MapWindow implements MapCanvas.Listener {
regionExplorer.loadRegions();
}
useSidebar(viewListAbove(regionExplorer.navigator()));
regionInspectorSplit.setTopComponent(regionExplorer.inspector());
regionInspectorSplit.setBottomComponent(legendPanel);
inspectorSlot.add(regionInspectorSplit, BorderLayout.CENTER);
showPickInspector(regionExplorer.inspector());
EventQueue.invokeLater(() -> viewList.select(currentView));
regionExplorer.measure(canvas.viewArea());
}
private void styleRegionInspectorSplit() {
regionInspectorSplit.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
regionInspectorSplit.setResizeWeight(0.7D);
regionInspectorSplit.setBackground(StudioTheme.PANEL_BACKGROUND);
regionInspectorSplit.setDividerSize(StudioTheme.scale(EXPLORER_DIVIDER_HEIGHT));
regionInspectorSplit.putClientProperty("FlatLaf.style", "style: grip");
private void showBiomeExplorer() {
useSidebar(viewListScroll);
showPickInspector(biomeExplorer.inspector());
biomeExplorer.measure(canvas.viewArea());
}
// The region and biome views share how the map draws its picks, so their switches show the same choice.
private void showPickInspector(PickInspectorPanel inspector) {
inspector.selectDisplay(canvas.pickDisplay());
pickInspectorSplit.setTopComponent(inspector);
pickInspectorSplit.setBottomComponent(legendPanel);
inspectorSlot.add(pickInspectorSplit, BorderLayout.CENTER);
}
private void stylePickInspectorSplit() {
pickInspectorSplit.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
pickInspectorSplit.setResizeWeight(0.7D);
pickInspectorSplit.setBackground(StudioTheme.PANEL_BACKGROUND);
pickInspectorSplit.setDividerSize(StudioTheme.scale(EXPLORER_DIVIDER_HEIGHT));
pickInspectorSplit.putClientProperty("FlatLaf.style", "style: grip");
}
private void showPalettes(MapView previousView) {
@@ -420,12 +436,15 @@ public final class MapWindow implements MapCanvas.Listener {
legendPanel.show(title, shares, highlight);
if (currentView == MapView.REGIONS) {
regionExplorer.measure(canvas.viewArea());
} else if (currentView == MapView.BIOMES) {
biomeExplorer.measure(canvas.viewArea());
}
}
@Override
public void onContentRefreshed() {
regionExplorer.refresh();
biomeExplorer.refresh();
}
private void resetExplorerView() {
@@ -524,6 +543,7 @@ public final class MapWindow implements MapCanvas.Listener {
private void close() {
canvas.close();
regionExplorer.close();
biomeExplorer.close();
if (paletteExplorer != null) {
paletteExplorer.close();
}
@@ -33,9 +33,15 @@ import javax.swing.SwingConstants;
import javax.swing.text.DefaultCaret;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
@@ -43,10 +49,10 @@ import java.util.Map;
import java.util.function.Consumer;
/**
* The region view's inspector: how the noise picks the regions, or for a chosen region how its biomes are picked,
* measured against what is in view and put in words.
* The inspector of the region and biome views: how noise picks the regions or biomes in view, measured against what
* is in view and put in words, with a switch for how the map draws the picks.
*/
final class RegionInspectorPanel extends JPanel {
final class PickInspectorPanel extends JPanel {
private static final long serialVersionUID = -2874105263188240317L;
private static final String MESSAGE_CARD = "message";
private static final String DETAILS_CARD = "details";
@@ -60,13 +66,15 @@ final class RegionInspectorPanel extends JPanel {
private final JPanel cardPanel = new JPanel(cards);
private final JTextArea message = wrappingText();
private final JPanel details = new ViewportWidthPanel();
private final RegionHeightChart heightChart = new RegionHeightChart();
private final String picksName;
/**
* @param picksName what the map shows when it draws the picks: "Regions" or "Biomes"
* @param onDisplayChosen receives how the map should draw the picks
*/
RegionInspectorPanel(Consumer<PickDisplay> onDisplayChosen) {
PickInspectorPanel(String picksName, Consumer<PickDisplay> onDisplayChosen) {
super(new BorderLayout());
this.picksName = picksName;
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
setBackground(StudioTheme.PANEL_BACKGROUND);
add(buildHeader(onDisplayChosen), BorderLayout.NORTH);
@@ -74,10 +82,13 @@ final class RegionInspectorPanel extends JPanel {
cardPanel.add(buildMessageCard(), MESSAGE_CARD);
details.setLayout(new BoxLayout(details, BoxLayout.Y_AXIS));
details.setBackground(StudioTheme.PANEL_BACKGROUND);
heightChart.setAlignmentX(LEFT_ALIGNMENT);
cardPanel.add(StudioToolbar.scroll(details), DETAILS_CARD);
add(cardPanel, BorderLayout.CENTER);
showMeasuring("Regions");
showMeasuring(picksName);
}
void selectDisplay(PickDisplay display) {
displayButtons.get(display).setSelected(true);
}
void showMeasuring(String name) {
@@ -100,8 +111,31 @@ final class RegionInspectorPanel extends JPanel {
void showCoverage(String regionName, RegionHeightCoverage coverage, List<String> notes) {
showHeader(regionName, "Hybrid terrain · biomes by height");
nameFirstDisplay("Biomes");
heightChart.show(coverage);
showDetails(List.of(heightChart), notes);
showDetails(List.of(heightChart(coverage)), notes);
}
/**
* One section per region in view, each with how it picks its biomes, then notes about all of them.
*/
void showRegionSections(String detailText, List<Section> sections, List<String> notes) {
showHeader(picksName, detailText);
nameFirstDisplay(picksName);
List<JComponent> components = new ArrayList<>();
for (Section section : sections) {
components.add(new SectionHeader(section));
components.addAll(section.charts());
if (!section.notes().isEmpty()) {
components.add(notesText(section.notes()));
}
}
showDetails(components, notes);
}
static RegionHeightChart heightChart(RegionHeightCoverage coverage) {
RegionHeightChart chart = new RegionHeightChart();
chart.setAlignmentX(LEFT_ALIGNMENT);
chart.show(coverage);
return chart;
}
private JPanel buildHeader(Consumer<PickDisplay> onDisplayChosen) {
@@ -136,9 +170,9 @@ final class RegionInspectorPanel extends JPanel {
return row;
}
private static String displayName(PickDisplay display) {
private String displayName(PickDisplay display) {
return switch (display) {
case PICKS -> "Regions";
case PICKS -> picksName;
case NOISE -> "Noise";
case BOTH -> "Both";
};
@@ -170,11 +204,7 @@ final class RegionInspectorPanel extends JPanel {
details.removeAll();
charts.forEach(details::add);
if (!notes.isEmpty()) {
JTextArea notesText = wrappingText();
notesText.setAlignmentX(LEFT_ALIGNMENT);
notesText.setBorder(StudioTheme.emptyBorder(4, 16, 12, 16));
notesText.setText(String.join(PARAGRAPH_BREAK, notes));
details.add(notesText);
details.add(notesText(notes));
}
details.revalidate();
details.repaint();
@@ -182,6 +212,14 @@ final class RegionInspectorPanel extends JPanel {
cards.show(cardPanel, DETAILS_CARD);
}
private static JTextArea notesText(List<String> notes) {
JTextArea text = wrappingText();
text.setAlignmentX(LEFT_ALIGNMENT);
text.setBorder(StudioTheme.emptyBorder(4, 16, 12, 16));
text.setText(String.join(PARAGRAPH_BREAK, notes));
return text;
}
private static JLabel sectionLabel(String text) {
JLabel label = new JLabel(text.toUpperCase(Locale.ROOT));
label.setForeground(StudioTheme.MUTED_TEXT);
@@ -202,6 +240,69 @@ final class RegionInspectorPanel extends JPanel {
return text;
}
/**
* One region of the biome view's inspector.
*
* @param share how much of the view the region covers
*/
record Section(String name, int color, double share, List<JComponent> charts, List<String> notes) {
}
/**
* A region's name in its colour above its charts, with how much of the view it covers.
*/
private static final class SectionHeader extends JComponent {
private static final long serialVersionUID = -4730226930154581123L;
private static final int HEIGHT = 34;
private static final int MARGIN = 16;
private static final int SWATCH_SIZE = 12;
private static final int SWATCH_GAP = 8;
private final Section section;
private SectionHeader(Section section) {
this.section = section;
setAlignmentX(LEFT_ALIGNMENT);
}
@Override
public Dimension getPreferredSize() {
return StudioTheme.scaledSize(PANEL_WIDTH, HEIGHT);
}
@Override
public Dimension getMaximumSize() {
return new Dimension(Integer.MAX_VALUE, getPreferredSize().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(StudioTheme.SEPARATOR);
canvas.fillRect(0, 0, getWidth(), 1);
int margin = StudioTheme.scale(MARGIN);
int swatch = StudioTheme.scale(SWATCH_SIZE);
canvas.setColor(new Color(section.color()));
int arc = StudioTheme.scale(4);
canvas.fillRoundRect(margin, (getHeight() - swatch) / 2, swatch, swatch, arc, arc);
canvas.setFont(StudioTheme.boldFont());
FontMetrics metrics = canvas.getFontMetrics();
int baseline = (getHeight() + metrics.getAscent() - metrics.getDescent()) / 2;
canvas.setColor(StudioTheme.TEXT);
canvas.drawString(section.name(), margin + swatch + StudioTheme.scale(SWATCH_GAP), baseline);
String share = NoisePickChart.percent(section.share()) + " of view";
canvas.setFont(StudioTheme.smallFont());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
canvas.drawString(share, getWidth() - margin - canvas.getFontMetrics().stringWidth(share), baseline);
} finally {
canvas.dispose();
}
}
}
/**
* Follows the scroll pane's width, so the charts stretch to it and the notes wrap inside it.
*/
@@ -0,0 +1,179 @@
/*
* 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.NoisePickSpread.Option;
import com.volmit.iris.core.gui.RegionHeightCoverage.BiomeRange;
import com.volmit.iris.core.gui.components.RenderType;
import com.volmit.iris.engine.IrisComplex;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.InferredType;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisRegion;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
/**
* Samples the map in view to measure how regions and biomes are picked, and puts what stands out in words; shared by
* the region and biome views.
*/
final class PickMeasuring {
static final List<InferredType> PLACED_TYPES = List.of(InferredType.LAND, InferredType.SHORE, InferredType.SEA);
private static final int SAMPLES_PER_SIDE = 96;
private PickMeasuring() {
}
static void forEachSample(MapCensus.ViewArea area, SampleVisitor visitor) {
double stepX = (area.maximumX() - area.minimumX()) / SAMPLES_PER_SIDE;
double stepZ = (area.maximumZ() - area.minimumZ()) / SAMPLES_PER_SIDE;
for (int row = 0; row < SAMPLES_PER_SIDE; row++) {
for (int column = 0; column < SAMPLES_PER_SIDE; column++) {
visitor.visit(area.minimumX() + (column + 0.5D) * stepX, area.minimumZ() + (row + 0.5D) * stepZ);
}
}
}
static Option regionOption(Engine engine, IrisRegion region) {
return new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity());
}
static int regionColor(Engine engine, IrisRegion region) {
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
}
/**
* One spread per biome list of the region that has biomes, in land, shore, sea order.
*/
static List<TypedSpread> biomeSpreads(Engine engine, IrisComplex complex, IrisRegion region) {
List<TypedSpread> spreads = new ArrayList<>();
for (InferredType type : PLACED_TYPES) {
List<Option> options = complex.noisePickedBiomes(region, type).stream()
.map(biome -> biomeOption(engine, biome))
.toList();
if (!options.isEmpty()) {
spreads.add(new TypedSpread(type,
NoisePickSpread.of(typeName(type) + " biome noise in view", options, complex.isLegacyRarity())));
}
}
return spreads;
}
static List<BiomeRange> biomeRanges(Engine engine, IrisRegion region) {
List<BiomeRange> ranges = new ArrayList<>();
for (InferredType type : PLACED_TYPES) {
for (IrisBiome biome : region.getBiomes(engine.getComplex(), type)) {
if (biome == null) {
continue;
}
ranges.add(new BiomeRange(biome.getLoadKey(), biome.getName(),
biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF, type,
biome.getPlacement().getMinHeight(), biome.getPlacement().getMaxHeight()));
}
}
return ranges;
}
static void addSpreadNotes(List<String> notes, NoisePickSpread spread, String noun) {
List<NoisePickSpread.Row> rows = spread.rows();
if (spread.sampleCount() == 0) {
notes.add("No " + noun + " is picked in view.");
return;
}
if (rows.size() == 1) {
notes.add(rows.getFirst().option().name() + " is the only " + noun + ", so the " + noun + " noise has nothing to pick.");
return;
}
if (spread.belowZeroShare() > 0D) {
notes.add(NoisePickChart.percent(spread.belowZeroShare()) + " of the " + noun + " noise in view is below 0, so "
+ rows.getFirst().option().name() + " gets it.");
}
if (spread.aboveOneShare() > 0D) {
notes.add(NoisePickChart.percent(spread.aboveOneShare()) + " of the " + noun + " noise in view is above 1, so "
+ rows.getLast().option().name() + " gets it.");
}
List<String> starved = rows.stream().filter(NoisePickSpread.Row::isFarBelowWeight).map(row -> row.option().name()).toList();
if (!starved.isEmpty()) {
boolean single = starved.size() == 1;
notes.add(String.join(", ", starved) + (single ? " gets" : " get") + " under half " + (single ? "its" : "their")
+ " weight in view (▼): the " + noun + " noise rarely reaches " + (single ? "its band." : "their bands."));
}
}
static void addBiomeSpreadNotes(List<String> notes, List<TypedSpread> spreads) {
for (TypedSpread typed : spreads) {
addSpreadNotes(notes, typed.spread(), typeName(typed.type()).toLowerCase(Locale.ROOT) + " biome");
}
}
static void addChildBiomeNote(List<String> notes, IrisComplex complex, IrisRegion region) {
String parents = PLACED_TYPES.stream()
.flatMap(type -> complex.noisePickedBiomes(region, type).stream())
.filter(biome -> !biome.getChildren().isEmpty())
.map(IrisBiome::getName)
.distinct()
.collect(Collectors.joining(", "));
if (!parents.isEmpty()) {
notes.add(parents + " then hand some spots to child biomes by a noise of their own.");
}
}
static void addRarityNotes(List<String> notes, IrisComplex complex, Collection<NoisePickSpread> spreads) {
if (spreads.stream().allMatch(spread -> spread.bands().size() <= 1)) {
return;
}
notes.add(complex.isLegacyRarity()
? "Legacy rarity: each weight is the total rarity minus the option's own."
: "Each weight is one over the rarity, so rarity 2 gets half the noise of rarity 1.");
}
static String measuredNote() {
return "Measured on a " + SAMPLES_PER_SIDE + " × " + SAMPLES_PER_SIDE + " grid over the map in view.";
}
static String hybridNoiseNote() {
return "Noise shows the height biomes are picked at: the overlay noise plus the dimension's biome placement "
+ "jitter. Where ranges share a height, each biome's competition noise, divided by its rarity, decides.";
}
static String typeName(InferredType type) {
String name = type.name().toLowerCase(Locale.ROOT);
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
private static Option biomeOption(Engine engine, IrisBiome biome) {
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
biome.getRarity());
}
@FunctionalInterface
interface SampleVisitor {
void visit(double x, double z);
}
/**
* The spread of the noise that picks from one of a region's biome lists.
*/
record TypedSpread(InferredType type, NoisePickSpread spread) {
}
}
@@ -18,50 +18,30 @@
package com.volmit.iris.core.gui;
import com.volmit.iris.Iris;
import com.volmit.iris.core.gui.NoisePickSpread.Option;
import com.volmit.iris.core.gui.RegionHeightCoverage.BiomeRange;
import com.volmit.iris.core.gui.PickMeasuring.TypedSpread;
import com.volmit.iris.core.gui.RegionNavigator.RegionRow;
import com.volmit.iris.core.gui.components.PickDisplay;
import com.volmit.iris.core.gui.components.RenderType;
import com.volmit.iris.engine.IrisComplex;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.InferredType;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisRegion;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* The region view's explorer: shows how the region noise picks the regions, and for a chosen region fills the map with
* its biomes and shows how they are picked. Measuring what is in view runs off the Swing thread and only the newest
* request counts.
* its biomes and shows how they are picked, measured against what is in view.
*/
final class RegionExplorer {
private static final int SAMPLES_PER_SIDE = 96;
private static final List<InferredType> PLACED_TYPES = List.of(InferredType.LAND, InferredType.SHORE, InferredType.SEA);
private final Supplier<Engine> engineSource;
private final Consumer<String> onPreviewChanged;
private final RegionNavigator navigator = new RegionNavigator(this::choose);
private final RegionInspectorPanel inspector;
private final ThreadPoolExecutor measurer = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1), RegionExplorer::daemonThread, new ThreadPoolExecutor.DiscardOldestPolicy());
private final AtomicLong measurementGeneration = new AtomicLong();
private final PickInspectorPanel inspector;
private final LatestMeasurement<Measurement> measurer = new LatestMeasurement<>("Iris Region Explorer");
private String chosenRegionKey;
private MapCensus.ViewArea latestArea;
private MapCensus.ViewArea measuredArea;
@@ -73,14 +53,14 @@ final class RegionExplorer {
RegionExplorer(Supplier<Engine> engineSource, Consumer<String> onPreviewChanged, Consumer<PickDisplay> onDisplayChosen) {
this.engineSource = engineSource;
this.onPreviewChanged = onPreviewChanged;
this.inspector = new RegionInspectorPanel(onDisplayChosen);
this.inspector = new PickInspectorPanel("Regions", onDisplayChosen);
}
RegionNavigator navigator() {
return navigator;
}
RegionInspectorPanel inspector() {
PickInspectorPanel inspector() {
return inspector;
}
@@ -90,8 +70,8 @@ final class RegionExplorer {
return;
}
List<RegionRow> rows = new ArrayList<>();
for (IrisRegion region : engine.getData().getRegionLoader().loadAll(engine.getDimension().getRegions())) {
rows.add(new RegionRow(region.getLoadKey(), region.getName(), regionColor(engine, region)));
for (IrisRegion region : engine.getComplex().noisePickedRegions()) {
rows.add(new RegionRow(region.getLoadKey(), region.getName(), PickMeasuring.regionColor(engine, region)));
}
navigator.showRegions(rows);
}
@@ -106,15 +86,7 @@ final class RegionExplorer {
}
measuredArea = area;
String regionKey = chosenRegionKey;
long generation = measurementGeneration.incrementAndGet();
measurer.execute(() -> {
Measurement measurement = measureSafely(regionKey, area);
EventQueue.invokeLater(() -> {
if (generation == measurementGeneration.get() && measurement != null) {
show(measurement);
}
});
});
measurer.measure(() -> measurePicks(regionKey, area), this::show);
}
/**
@@ -127,13 +99,13 @@ final class RegionExplorer {
}
void close() {
measurer.shutdownNow();
measurer.close();
}
private void choose(String regionKey) {
chosenRegionKey = regionKey;
measuredArea = null;
measurementGeneration.incrementAndGet();
measurer.discardPending();
onPreviewChanged.accept(regionKey);
inspector.showMeasuring(regionKey == null ? "Regions" : regionName(regionKey));
measure(latestArea);
@@ -156,15 +128,6 @@ final class RegionExplorer {
}
}
private Measurement measureSafely(String regionKey, MapCensus.ViewArea area) {
try {
return measurePicks(regionKey, area);
} catch (Throwable error) {
Iris.reportError(error);
return null;
}
}
private Measurement measurePicks(String regionKey, MapCensus.ViewArea area) {
Engine engine = engineSource.get();
IrisComplex complex = engine == null ? null : engine.getComplex();
@@ -184,155 +147,47 @@ final class RegionExplorer {
}
private static RegionPicks measureRegionPicks(Engine engine, IrisComplex complex, MapCensus.ViewArea area) {
List<Option> options = complex.noisePickedRegions().stream()
.map(region -> new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity()))
List<NoisePickSpread.Option> options = complex.noisePickedRegions().stream()
.map(region -> PickMeasuring.regionOption(engine, region))
.toList();
NoisePickSpread spread = NoisePickSpread.of("Region noise in view", options, complex.isLegacyRarity());
forEachSample(area, (x, z) -> spread.add(complex.getRegionStyleStream().get(x, z)));
PickMeasuring.forEachSample(area, (x, z) -> spread.add(complex.getRegionStyleStream().get(x, z)));
List<String> notes = new ArrayList<>();
addSpreadNotes(notes, spread, "region");
addRarityNotes(notes, complex, List.of(spread));
PickMeasuring.addSpreadNotes(notes, spread, "region");
PickMeasuring.addRarityNotes(notes, complex, List.of(spread));
notes.add(PickMeasuring.measuredNote());
return new RegionPicks(spread, notes);
}
// The preview treats every spot as part of the region, so each spot's biome list is the one its surface needs.
private static BiomePicks measureBiomePicks(Engine engine, IrisComplex complex, IrisRegion region, MapCensus.ViewArea area) {
Map<InferredType, NoisePickSpread> spreads = new LinkedHashMap<>();
for (InferredType type : PLACED_TYPES) {
List<Option> options = complex.noisePickedBiomes(region, type).stream()
.map(biome -> biomeOption(engine, biome))
.toList();
if (!options.isEmpty()) {
spreads.put(type, NoisePickSpread.of(typeName(type) + " biome noise in view", options, complex.isLegacyRarity()));
}
}
forEachSample(area, (x, z) -> {
IrisComplex.BiomePick pick = complex.surfacePickAsIfIn(region, x, z);
NoisePickSpread spread = spreads.get(pick.type());
if (spread != null) {
spread.add(complex.biomePickNoise(region, pick.type(), x, z));
}
List<TypedSpread> spreads = PickMeasuring.biomeSpreads(engine, complex, region);
PickMeasuring.forEachSample(area, (x, z) -> {
InferredType type = complex.surfacePickAsIfIn(region, x, z).type();
spreads.stream()
.filter(typed -> typed.type() == type)
.findFirst()
.ifPresent(typed -> typed.spread().add(complex.biomePickNoise(region, type, x, z)));
});
List<String> notes = new ArrayList<>();
spreads.forEach((type, spread) -> addSpreadNotes(notes, spread, typeName(type).toLowerCase(Locale.ROOT) + " biome"));
addChildBiomeNote(notes, complex, region);
addRarityNotes(notes, complex, spreads.values());
return new BiomePicks(region.getLoadKey(), region.getName(), List.copyOf(spreads.values()), notes);
PickMeasuring.addBiomeSpreadNotes(notes, spreads);
PickMeasuring.addChildBiomeNote(notes, complex, region);
List<NoisePickSpread> plainSpreads = spreads.stream().map(TypedSpread::spread).toList();
PickMeasuring.addRarityNotes(notes, complex, plainSpreads);
notes.add(PickMeasuring.measuredNote());
return new BiomePicks(region.getLoadKey(), region.getName(), plainSpreads, notes);
}
private static HeightPicks measureHeightPicks(Engine engine, IrisComplex complex, IrisRegion region, MapCensus.ViewArea area) {
RegionHeightCoverage coverage = RegionHeightCoverage.measure(biomeRanges(engine, region), pickingHeights(complex, area));
List<Integer> heights = new ArrayList<>();
PickMeasuring.forEachSample(area, (x, z) -> heights.add((int) Math.round(complex.getPlacementHeightStream().get(x, z))));
RegionHeightCoverage coverage = RegionHeightCoverage.measure(PickMeasuring.biomeRanges(engine, region),
heights.stream().mapToInt(Integer::intValue).toArray());
List<String> notes = new ArrayList<>(coverage.notes());
notes.add("Noise shows the height biomes are picked at: the overlay noise plus the dimension's biome placement "
+ "jitter. Where ranges share a height, each biome's competition noise, divided by its rarity, decides.");
notes.add(PickMeasuring.hybridNoiseNote());
return new HeightPicks(region.getLoadKey(), region.getName(), coverage, notes);
}
private static void addSpreadNotes(List<String> notes, NoisePickSpread spread, String noun) {
List<NoisePickSpread.Row> rows = spread.rows();
if (spread.sampleCount() == 0) {
notes.add("No " + noun + " is picked in view.");
return;
}
if (rows.size() == 1) {
notes.add(rows.getFirst().option().name() + " is the only " + noun + ", so the " + noun + " noise has nothing to pick.");
return;
}
if (spread.belowZeroShare() > 0D) {
notes.add(NoisePickChart.percent(spread.belowZeroShare()) + " of the " + noun + " noise in view is below 0, so "
+ rows.getFirst().option().name() + " gets it.");
}
if (spread.aboveOneShare() > 0D) {
notes.add(NoisePickChart.percent(spread.aboveOneShare()) + " of the " + noun + " noise in view is above 1, so "
+ rows.getLast().option().name() + " gets it.");
}
List<String> starved = rows.stream().filter(NoisePickSpread.Row::isFarBelowWeight).map(row -> row.option().name()).toList();
if (!starved.isEmpty()) {
notes.add(String.join(", ", starved) + (starved.size() == 1 ? " gets" : " get")
+ " under half " + (starved.size() == 1 ? "its" : "their") + " weight in view (▼): the "
+ noun + " noise rarely reaches " + (starved.size() == 1 ? "its band." : "their bands."));
}
}
private static void addChildBiomeNote(List<String> notes, IrisComplex complex, IrisRegion region) {
String parents = PLACED_TYPES.stream()
.flatMap(type -> complex.noisePickedBiomes(region, type).stream())
.filter(biome -> !biome.getChildren().isEmpty())
.map(IrisBiome::getName)
.distinct()
.collect(Collectors.joining(", "));
if (!parents.isEmpty()) {
notes.add(parents + " then hand some spots to child biomes by a noise of their own.");
}
}
private static void addRarityNotes(List<String> notes, IrisComplex complex, Collection<NoisePickSpread> spreads) {
if (spreads.stream().allMatch(spread -> spread.bands().size() <= 1)) {
return;
}
notes.add(complex.isLegacyRarity()
? "Legacy rarity: each weight is the total rarity minus the option's own."
: "Each weight is one over the rarity, so rarity 2 gets half the noise of rarity 1.");
notes.add("Measured on a " + SAMPLES_PER_SIDE + " × " + SAMPLES_PER_SIDE + " grid over the map in view.");
}
private static Option biomeOption(Engine engine, IrisBiome biome) {
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
biome.getRarity());
}
private static int regionColor(Engine engine, IrisRegion region) {
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
}
private static String typeName(InferredType type) {
String name = type.name().toLowerCase(Locale.ROOT);
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
private static List<BiomeRange> biomeRanges(Engine engine, IrisRegion region) {
List<BiomeRange> ranges = new ArrayList<>();
for (InferredType type : PLACED_TYPES) {
for (IrisBiome biome : region.getBiomes(engine.getComplex(), type)) {
if (biome == null) {
continue;
}
ranges.add(new BiomeRange(biome.getLoadKey(), biome.getName(),
biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF, type,
biome.getPlacement().getMinHeight(), biome.getPlacement().getMaxHeight()));
}
}
return ranges;
}
// HYBRID terrain picks biomes by the overlay noise plus the placement jitter, in blocks above the fluid.
private static int[] pickingHeights(IrisComplex complex, MapCensus.ViewArea area) {
int[] heights = new int[SAMPLES_PER_SIDE * SAMPLES_PER_SIDE];
int[] next = {0};
forEachSample(area, (x, z) -> heights[next[0]++] = (int) Math.round(complex.getPlacementHeightStream().get(x, z)));
return heights;
}
private static void forEachSample(MapCensus.ViewArea area, SampleVisitor visitor) {
double stepX = (area.maximumX() - area.minimumX()) / SAMPLES_PER_SIDE;
double stepZ = (area.maximumZ() - area.minimumZ()) / SAMPLES_PER_SIDE;
for (int row = 0; row < SAMPLES_PER_SIDE; row++) {
for (int column = 0; column < SAMPLES_PER_SIDE; column++) {
visitor.visit(area.minimumX() + (column + 0.5D) * stepX, area.minimumZ() + (row + 0.5D) * stepZ);
}
}
}
private static Thread daemonThread(Runnable task) {
Thread thread = new Thread(task, "Iris Region Explorer");
thread.setDaemon(true);
return thread;
}
@FunctionalInterface
private interface SampleVisitor {
void visit(double x, double z);
}
/**
* What was measured for the region view; the region key is null when every region was measured.
*/
@@ -214,17 +214,33 @@ public final class IrisRenderer {
ProceduralStream<Double> regionNoise = complex.getRegionStyleStream();
return new UnitNoise((gridIndex, x, z) -> regionNoise.get(x, z));
}
IrisRegion region = engine.getData().getRegionLoader().load(layer.previewRegionKey());
if (region == null) {
return new UnitNoise((gridIndex, x, z) -> 0D);
}
if (complex.isHybridTerrain()) {
ProceduralStream<Double> placementHeight = complex.getPlacementHeightStream();
return new PlacementHeightNoise((gridIndex, x, z) -> placementHeight.get(x, z), fluidHeight(), defaultHeightRange());
}
if (layer.type() == RenderType.BIOME) {
return new UnitNoise(baseBiomePickNoise(complex));
}
IrisRegion region = engine.getData().getRegionLoader().load(layer.previewRegionKey());
if (region == null) {
return new UnitNoise((gridIndex, x, z) -> 0D);
}
return new UnitNoise(biomePickNoise(complex, region, grid));
}
// The biome view shows each spot's base biome, which its own region picks from the biome list of the spot's type.
private static NoiseField baseBiomePickNoise(IrisComplex complex) {
ProceduralStream<IrisRegion> regions = complex.getRegionStream();
ProceduralStream<InferredType> types = complex.getBridgeStream();
Map<IrisRegion, Map<InferredType, ProceduralStream<Double>>> noiseByRegion = new IdentityHashMap<>();
return (gridIndex, x, z) -> {
IrisRegion region = regions.get(x, z);
return noiseByRegion.computeIfAbsent(region, subject -> new EnumMap<>(InferredType.class))
.computeIfAbsent(types.get(x, z), type -> complex.biomePickNoiseStream(region, type))
.get(x, z);
};
}
// Finding which biome list picked a spot is slow, so it is sampled in squares like the biomes; the noise is cheap.
private static NoiseField biomePickNoise(IrisComplex complex, IrisRegion region, TileGrid grid) {
IdentitySampler typeSampler = new IdentitySampler(grid, (x, z) -> complex.surfacePickAsIfIn(region, x, z).type().ordinal());
@@ -22,10 +22,10 @@ import java.util.Objects;
/**
* What a map tile shows: one of the render types, for a region preview which region every spot is treated as, and in
* the region view whether the noise that picks is drawn too.
* the region and biome views whether the noise that picks is drawn too.
*
* @param previewRegionKey the region a {@link RenderType#REGION_PREVIEW} tile shows everywhere, null for every other type
* @param display how the picking noise is drawn; only the region view draws anything but the picks
* @param display how the picking noise is drawn; only the region and biome views draw anything but the picks
*/
public record MapLayer(RenderType type, String previewRegionKey, PickDisplay display) {
public MapLayer {
@@ -34,8 +34,8 @@ public record MapLayer(RenderType type, String previewRegionKey, PickDisplay dis
if ((type == RenderType.REGION_PREVIEW) != (previewRegionKey != null)) {
throw new IllegalArgumentException("Only a region preview names a region");
}
if (display != PickDisplay.PICKS && !type.isRegionView()) {
throw new IllegalArgumentException("Only the region view draws its picking noise");
if (display != PickDisplay.PICKS && !type.drawsPickNoise()) {
throw new IllegalArgumentException(type + " has no picking noise to draw");
}
}
@@ -43,6 +43,10 @@ public record MapLayer(RenderType type, String previewRegionKey, PickDisplay dis
return new MapLayer(type, null, PickDisplay.PICKS);
}
public static MapLayer biomes(PickDisplay display) {
return new MapLayer(RenderType.BIOME, null, display);
}
public static MapLayer regions(PickDisplay display) {
return new MapLayer(RenderType.REGION, null, display);
}
@@ -24,4 +24,11 @@ public enum RenderType {
public boolean isRegionView() {
return this == REGION || this == REGION_PREVIEW;
}
/**
* Whether the view can draw the noise that picks what it shows: the regions, or the surface biomes.
*/
public boolean drawsPickNoise() {
return isRegionView() || this == BIOME;
}
}