From 93f5615f007c438c57aea0a702b6782eaa616536 Mon Sep 17 00:00:00 2001 From: piexel Date: Wed, 21 Oct 2026 14:00:36 +0200 Subject: [PATCH] Choose procedural seeds by typing or reroll Seed box lists tried seeds; bar keeps its size. --- .../volmit/iris/core/gui/ObjectExplorer.java | 16 +- .../iris/core/gui/ObjectSeedChooser.java | 160 ++++++++++++++++++ .../iris/core/gui/ObjectVariantPicker.java | 42 ++--- 3 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 core/src/main/java/com/volmit/iris/core/gui/ObjectSeedChooser.java diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java index 004ea9191..a650d48b4 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java @@ -214,6 +214,7 @@ final class ObjectExplorer { int variantIndex = reloadingShownObject ? shownVariantIndex : 0; shownKey = key; if (!reloadingShownObject) { + variantPicker.forgetTriedSeeds(); showVariantList(List.of(), OptionalLong.empty(), OptionalLong.empty(), 0); } long revision = startLoading(key); @@ -364,8 +365,7 @@ final class ObjectExplorer { } private String galleryHeading() { - String seed = shownSeed.isPresent() ? " · seed " + shownSeed.getAsLong() : ""; - return String.format(Locale.ROOT, "%s · %d variants%s", shownKey, shownVariants.size(), seed); + return String.format(Locale.ROOT, "%s · %d variants%s", shownKey, shownVariants.size(), seedNote()); } private static ObjectMesh.Bounds largestSize(List meshes) { @@ -426,9 +426,17 @@ final class ObjectExplorer { String cut = layerCut.isCutting() ? String.format(Locale.ROOT, " · showing layers 1-%d of %d", layerCut.shownLayerCount(), layerCut.layerCount()) : ""; - String seed = shownSeed.isPresent() ? " · seed " + shownSeed.getAsLong() : ""; statusBar.showSummary(String.format(Locale.ROOT, "%s%s · %,d blocks · %,d faces drawn%s", - summary.key(), seed, summary.blockCount(), mesh.faceCount(), cut)); + summary.key(), seedNote(), summary.blockCount(), mesh.faceCount(), cut)); + } + + // A seed being tried is not in the pack until its "seed" field says so. + private String seedNote() { + if (shownSeed.isEmpty()) { + return ""; + } + String note = " · seed " + shownSeed.getAsLong(); + return shownSeed.equals(packSeed) ? note : note + " (the pack has " + packSeed.getAsLong() + ")"; } private void showSeeThrough(Set blockTypes) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectSeedChooser.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectSeedChooser.java new file mode 100644 index 000000000..3f4d12804 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectSeedChooser.java @@ -0,0 +1,160 @@ +/* + * 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 javax.swing.DefaultComboBoxModel; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.UIManager; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalLong; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.LongConsumer; + +/** + * Where the seed of a procedural object's variants is chosen: typed in, picked again from the seeds tried on this + * object with the pack's own first, or rerolled at random. It keeps its size whatever it shows, so the bar it sits in + * never shifts under the mouse. + */ +final class ObjectSeedChooser extends JPanel { + private static final long serialVersionUID = 3127745038254406118L; + private static final int FIELD_WIDTH = 120; + private static final int TRIED_SEEDS_KEPT = 12; + // Small enough to read back and type, and far more seeds than anyone tries. + private static final int RANDOM_SEED_BOUND = 1_000_000; + + private final JComboBox seeds = new JComboBox<>(); + private final LongConsumer onSeedChosen; + private final List triedSeedsNewestFirst = new ArrayList<>(); + private OptionalLong packSeed = OptionalLong.empty(); + private long shownSeed; + private boolean showingSeeds; + + /** + * @param onSeedChosen told the seed to bake the variants from whenever the user chooses another one + */ + ObjectSeedChooser(LongConsumer onSeedChosen) { + super(new BorderLayout(StudioTheme.scale(4), 0)); + this.onSeedChosen = onSeedChosen; + setOpaque(false); + JLabel label = new JLabel("Seed"); + label.setFont(StudioTheme.smallFont()); + label.setForeground(StudioTheme.SECONDARY_TEXT); + seeds.setEditable(true); + seeds.setFont(StudioTheme.smallFont()); + seeds.setToolTipText("Type a seed and press Enter, or pick one tried before"); + seeds.setRenderer(new PackSeedMarker()); + seeds.setPreferredSize(new Dimension(StudioTheme.scale(FIELD_WIDTH), seeds.getPreferredSize().height)); + seeds.addActionListener(event -> { + if (!showingSeeds) { + EventQueue.invokeLater(this::chooseEnteredSeed); + } + }); + add(label, BorderLayout.WEST); + add(seeds, BorderLayout.CENTER); + add(StudioToolbar.button("Reroll", "Bake the variants from a random seed", this::reroll), BorderLayout.EAST); + } + + /** Starts a new object's list of tried seeds. */ + void forgetTriedSeeds() { + triedSeedsNewestFirst.clear(); + } + + void showSeed(long shown, OptionalLong pack) { + shownSeed = shown; + packSeed = pack; + if (!isPackSeed(shown)) { + rememberTried(shown); + } + DefaultComboBoxModel choices = new DefaultComboBoxModel<>(); + pack.ifPresent(choices::addElement); + triedSeedsNewestFirst.forEach(choices::addElement); + showingSeeds = true; + try { + seeds.setModel(choices); + seeds.setSelectedItem(shown); + } finally { + showingSeeds = false; + } + } + + private void rememberTried(long seed) { + triedSeedsNewestFirst.remove(seed); + triedSeedsNewestFirst.add(0, seed); + while (triedSeedsNewestFirst.size() > TRIED_SEEDS_KEPT) { + triedSeedsNewestFirst.remove(triedSeedsNewestFirst.size() - 1); + } + } + + // A pick from the list arrives as a seed, anything typed as text. + private void chooseEnteredSeed() { + OptionalLong entered = parse(seeds.getSelectedItem()); + if (entered.isEmpty()) { + UIManager.getLookAndFeel().provideErrorFeedback(seeds); + showSeed(shownSeed, packSeed); + return; + } + if (entered.getAsLong() != shownSeed) { + onSeedChosen.accept(entered.getAsLong()); + } + } + + private static OptionalLong parse(Object entered) { + if (entered instanceof Long seed) { + return OptionalLong.of(seed); + } + try { + return OptionalLong.of(Long.parseLong(String.valueOf(entered).trim())); + } catch (NumberFormatException notASeed) { + return OptionalLong.empty(); + } + } + + private void reroll() { + long seed; + do { + seed = ThreadLocalRandom.current().nextInt(RANDOM_SEED_BOUND); + } while (seed == shownSeed); + onSeedChosen.accept(seed); + } + + private boolean isPackSeed(Object seed) { + return packSeed.isPresent() && seed instanceof Long value && value == packSeed.getAsLong(); + } + + private final class PackSeedMarker extends DefaultListCellRenderer { + private static final long serialVersionUID = -6061513307766393251L; + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean selected, boolean focused) { + super.getListCellRendererComponent(list, value, index, selected, focused); + if (index >= 0 && isPackSeed(value)) { + setText(value + " (pack)"); + } + return this; + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectVariantPicker.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectVariantPicker.java index 99690982e..1e8880961 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectVariantPicker.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectVariantPicker.java @@ -43,48 +43,38 @@ final class ObjectVariantPicker extends JPanel { private static final long serialVersionUID = -2716301839488217455L; private static final int BOTTOM_MARGIN = 14; private static final int VARIANT_LABEL_WIDTH = 110; - private static final int SEED_LABEL_WIDTH = 100; private static final int SECTION_GAP = 10; private static final String VARIANT_SHORTCUT_HINT = " · [ and ]"; private static final String COMPARE_SHORTCUT_HINT = " · G"; private final JLabel shownVariant = label(VARIANT_LABEL_WIDTH); - private final JLabel shownSeed = label(SEED_LABEL_WIDTH); private final JComponent variantStepper; - private final JComponent seedStepper; - private final JButton packSeedButton; + private final ObjectSeedChooser seedChooser; private final JButton compareButton; private final IntConsumer onVariantChosen; - private final LongConsumer onSeedChosen; private int variantCount; private int shownIndex; - private OptionalLong seed = OptionalLong.empty(); - private OptionalLong packSeed = OptionalLong.empty(); + private boolean seeded; private int coveredLeftWidth; /** * @param onVariantChosen told the index of the variant to show whenever the user steps to another one - * @param onSeedChosen told the seed to bake the variants from whenever the user steps to another one + * @param onSeedChosen told the seed to bake the variants from whenever the user chooses another one * @param onCompareToggled told when the user switches between one variant and all of them */ ObjectVariantPicker(IntConsumer onVariantChosen, LongConsumer onSeedChosen, Runnable onCompareToggled) { this.onVariantChosen = onVariantChosen; - this.onSeedChosen = onSeedChosen; setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setOpaque(false); setBorder(StudioTheme.emptyBorder(3, 4, 3, 4)); variantStepper = stepper(shownVariant, StudioToolbar.button("‹", "Previous variant" + VARIANT_SHORTCUT_HINT, () -> stepBy(-1)), StudioToolbar.button("›", "Next variant" + VARIANT_SHORTCUT_HINT, () -> stepBy(1))); - seedStepper = stepper(shownSeed, - StudioToolbar.button("‹", "Bake the variants from the previous seed", () -> stepSeedBy(-1)), - StudioToolbar.button("›", "Bake the variants from the next seed", () -> stepSeedBy(1))); - packSeedButton = StudioToolbar.button("Pack seed", "", () -> packSeed.ifPresent(onSeedChosen::accept)); + seedChooser = new ObjectSeedChooser(onSeedChosen); compareButton = StudioToolbar.button("", "", onCompareToggled); add(variantStepper); add(Box.createHorizontalStrut(StudioTheme.scale(SECTION_GAP))); - add(seedStepper); - add(packSeedButton); + add(seedChooser); add(Box.createHorizontalStrut(StudioTheme.scale(SECTION_GAP))); add(compareButton); showComparing(false); @@ -153,19 +143,19 @@ final class ObjectVariantPicker extends JPanel { } /** - * Shows the seed the variants are baked from, with a way back to the pack's own seed once another is chosen. + * Shows the seed the variants are baked from; empty for objects not generated from a seed. */ void showSeed(OptionalLong shown, OptionalLong pack) { - seed = shown; - packSeed = pack; - shownSeed.setText(shown.isPresent() ? "Seed " + shown.getAsLong() : " "); - seedStepper.setVisible(shown.isPresent()); - boolean explored = shown.isPresent() && !shown.equals(pack); - packSeedButton.setVisible(explored); - pack.ifPresent(value -> packSeedButton.setToolTipText("Back to the seed the pack sets, " + value)); + seeded = shown.isPresent(); + seedChooser.setVisible(seeded); + shown.ifPresent(seed -> seedChooser.showSeed(seed, pack)); showWhenUseful(); } + void forgetTriedSeeds() { + seedChooser.forgetTriedSeeds(); + } + void showComparing(boolean comparing) { compareButton.setText(comparing ? "3D view" : "All variants"); compareButton.setToolTipText((comparing ? "Show the chosen variant in 3D" : "Show every variant side by side") + COMPARE_SHORTCUT_HINT); @@ -181,12 +171,8 @@ final class ObjectVariantPicker extends JPanel { onVariantChosen.accept(shownIndex); } - private void stepSeedBy(int step) { - seed.ifPresent(current -> onSeedChosen.accept(current + step)); - } - private void showWhenUseful() { - setVisible(variantCount > 1 || seed.isPresent()); + setVisible(variantCount > 1 || seeded); relayout(); }