diff --git a/core/src/main/java/com/volmit/iris/core/gui/BiomeExplorer.java b/core/src/main/java/com/volmit/iris/core/gui/BiomeExplorer.java
new file mode 100644
index 000000000..5137ffbcb
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/BiomeExplorer.java
@@ -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 .
+ */
+
+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 engineSource;
+ private final PickInspectorPanel inspector;
+ private final LatestMeasurement 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 engineSource, Consumer 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 sections = new ArrayList<>();
+ for (RegionPicks region : measurement.regions()) {
+ List 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 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 regions = samplesByRegion.values().stream()
+ .sorted(Comparator.comparingInt(RegionSamples::count).reversed())
+ .map(samples -> samples.picks(engine, total[0]))
+ .toList();
+ List 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 spreads;
+ private final Map> noiseByType = new EnumMap<>(InferredType.class);
+ private final List 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 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 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 spreads,
+ RegionHeightCoverage coverage, List notes) {
+ }
+
+ private record Measurement(boolean hybrid, List regions, List notes) {
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/LatestMeasurement.java b/core/src/main/java/com/volmit/iris/core/gui/LatestMeasurement.java
new file mode 100644
index 000000000..4bf923a38
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/LatestMeasurement.java
@@ -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 .
+ */
+
+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 {
+ 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 measurement, Consumer 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 measureSafely(Supplier measurement) {
+ try {
+ return measurement.get();
+ } catch (Throwable error) {
+ Iris.reportError(error);
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapCanvas.java b/core/src/main/java/com/volmit/iris/core/gui/MapCanvas.java
index 4b87dff38..7dff9e6ff 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/MapCanvas.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/MapCanvas.java
@@ -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()) {
diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapHoverProbe.java b/core/src/main/java/com/volmit/iris/core/gui/MapHoverProbe.java
index a958b5dc2..0313e3ba3 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/MapHoverProbe.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/MapHoverProbe.java
@@ -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));
}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java b/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java
index 28b0d039d..237e63533 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java
@@ -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 mapControls = new ArrayList<>();
private final List explorerControls = new ArrayList<>();
private final List 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 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();
}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java b/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java
index 7a646ba77..2a21a0c48 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java
@@ -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 onDisplayChosen) {
+ PickInspectorPanel(String picksName, Consumer 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 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 sections, List notes) {
+ showHeader(picksName, detailText);
+ nameFirstDisplay(picksName);
+ List 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 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 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 charts, List 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.
*/
diff --git a/core/src/main/java/com/volmit/iris/core/gui/PickMeasuring.java b/core/src/main/java/com/volmit/iris/core/gui/PickMeasuring.java
new file mode 100644
index 000000000..b30015b60
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/PickMeasuring.java
@@ -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 .
+ */
+
+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 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 biomeSpreads(Engine engine, IrisComplex complex, IrisRegion region) {
+ List spreads = new ArrayList<>();
+ for (InferredType type : PLACED_TYPES) {
+ List