From 3123ab375335d32745ce07f0a9a7df6d0f97c055 Mon Sep 17 00:00:00 2001 From: piexel Date: Wed, 21 Oct 2026 21:06:00 +0200 Subject: [PATCH] Show biome picking noise in the biome view Noise switch, per-region breakdown. --- .../volmit/iris/core/gui/BiomeExplorer.java | 193 ++++++++++++++++ .../iris/core/gui/LatestMeasurement.java | 81 +++++++ .../com/volmit/iris/core/gui/MapCanvas.java | 11 +- .../volmit/iris/core/gui/MapHoverProbe.java | 11 +- .../com/volmit/iris/core/gui/MapWindow.java | 42 +++- .../iris/core/gui/PickInspectorPanel.java | 133 +++++++++-- .../volmit/iris/core/gui/PickMeasuring.java | 179 +++++++++++++++ .../volmit/iris/core/gui/RegionExplorer.java | 217 +++--------------- .../core/gui/components/IrisRenderer.java | 24 +- .../iris/core/gui/components/MapLayer.java | 12 +- .../iris/core/gui/components/RenderType.java | 7 + 11 files changed, 687 insertions(+), 223 deletions(-) create mode 100644 core/src/main/java/com/volmit/iris/core/gui/BiomeExplorer.java create mode 100644 core/src/main/java/com/volmit/iris/core/gui/LatestMeasurement.java create mode 100644 core/src/main/java/com/volmit/iris/core/gui/PickMeasuring.java 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