Give procedural objects their own tab

Variant picker, grouped by type.
This commit is contained in:
piexel committed 2026-10-20 12:37:39 +02:00
1 parent 8d12019246
commit dc7615219b
12 files changed
+419 -115

No files matched your search

@@ -40,6 +40,8 @@ enum MapView {
Group.BLOCKS, null, KeyEvent.VK_P), Group.BLOCKS, null, KeyEvent.VK_P),
OBJECTS("Objects", "Every object in the pack in 3D, with the blocks it is made of", OBJECTS("Objects", "Every object in the pack in 3D, with the blocks it is made of",
Group.BLOCKS, null, KeyEvent.VK_O), Group.BLOCKS, null, KeyEvent.VK_O),
PROCEDURAL("Procedural", "Trees, rock formations and more that Iris generates from settings, every variant in 3D",
Group.BLOCKS, null, KeyEvent.VK_T),
NOISE("Noise", "The pack's noise generators and Iris' built-in noise styles", NOISE("Noise", "The pack's noise generators and Iris' built-in noise styles",
Group.GENERATORS, null, KeyEvent.VK_N); Group.GENERATORS, null, KeyEvent.VK_N);
@@ -70,7 +72,7 @@ enum MapView {
} }
/** /**
* The tile renderer's mode for this view, or null for the palette, object and noise views, which draw themselves. * The tile renderer's mode for this view, or null for the palette, object, procedural and noise views, which draw themselves.
*/ */
RenderType renderType() { RenderType renderType() {
return renderType; return renderType;
@@ -46,7 +46,9 @@ import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter; import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.IntConsumer; import java.util.function.IntConsumer;
/** /**
@@ -56,7 +58,6 @@ import java.util.function.IntConsumer;
public final class MapWindow implements MapCanvas.Listener { public final class MapWindow implements MapCanvas.Listener {
private static final String MAP_CARD = "map"; private static final String MAP_CARD = "map";
private static final String PALETTES_CARD = "palettes"; private static final String PALETTES_CARD = "palettes";
private static final String OBJECTS_CARD = "objects";
private static final String NOISE_CARD = "noise"; private static final String NOISE_CARD = "noise";
private static final int SIDEBAR_WIDTH = 270; private static final int SIDEBAR_WIDTH = 270;
private static final int SIDEBAR_MINIMUM_WIDTH = 200; private static final int SIDEBAR_MINIMUM_WIDTH = 200;
@@ -91,7 +92,7 @@ public final class MapWindow implements MapCanvas.Listener {
private final MapLegendPanel legendPanel; private final MapLegendPanel legendPanel;
private NoiseExplorerGUI paletteExplorer; private NoiseExplorerGUI paletteExplorer;
private ObjectExplorer objectExplorer; private final Map<MapView, ObjectExplorer> objectExplorers = new EnumMap<>(MapView.class);
private NoiseExplorerGUI noiseExplorer; private NoiseExplorerGUI noiseExplorer;
private JComponent noiseSidebar; private JComponent noiseSidebar;
private MapView currentView; private MapView currentView;
@@ -204,8 +205,8 @@ public final class MapWindow implements MapCanvas.Listener {
showMap(previousView, view); showMap(previousView, view);
} else if (view == MapView.PALETTES) { } else if (view == MapView.PALETTES) {
showPalettes(previousView); showPalettes(previousView);
} else if (view == MapView.OBJECTS) { } else if (view == MapView.OBJECTS || view == MapView.PROCEDURAL) {
showObjects(); showObjectExplorer(view);
} else { } else {
showNoise(); showNoise();
} }
@@ -246,20 +247,18 @@ public final class MapWindow implements MapCanvas.Listener {
} }
} }
private void showObjects() { private void showObjectExplorer(MapView view) {
boolean firstOpen = objectExplorer == null; boolean firstOpen = !objectExplorers.containsKey(view);
if (firstOpen) { ObjectExplorer explorer = objectExplorers.computeIfAbsent(view, this::createObjectExplorer);
createObjectExplorer();
}
canvas.setActive(false); canvas.setActive(false);
showControls(explorerControls); showControls(explorerControls);
useSidebar(viewListAbove(objectExplorer.browser())); useSidebar(viewListAbove(explorer.browser()));
inspectorSlot.removeAll(); inspectorSlot.removeAll();
inspectorSlot.add(objectExplorer.overview(), BorderLayout.CENTER); inspectorSlot.add(explorer.overview(), BorderLayout.CENTER);
showCard(OBJECTS_CARD); showCard(view.name());
EventQueue.invokeLater(() -> viewList.select(currentView)); EventQueue.invokeLater(() -> viewList.select(currentView));
if (firstOpen) { if (firstOpen) {
objectExplorer.open(); explorer.open();
} }
} }
@@ -273,11 +272,13 @@ public final class MapWindow implements MapCanvas.Listener {
}); });
} }
private void createObjectExplorer() { private ObjectExplorer createObjectExplorer(MapView view) {
objectExplorer = new ObjectExplorer(start.engine(), start.worldId()); ObjectSource source = view == MapView.PROCEDURAL ? new ProceduralObjectSource() : new PackObjectSource();
keepClearOfIsland(objectExplorer.viewport()::setCoveredLeftWidth); ObjectExplorer explorer = new ObjectExplorer(start.engine(), start.worldId(), source);
content.add(objectExplorer.view(), OBJECTS_CARD); keepClearOfIsland(explorer::setCoveredLeftWidth);
statusBars.add(objectExplorer.statusBar(), OBJECTS_CARD); content.add(explorer.view(), view.name());
statusBars.add(explorer.statusBar(), view.name());
return explorer;
} }
private void showNoise() { private void showNoise() {
@@ -391,7 +392,8 @@ public final class MapWindow implements MapCanvas.Listener {
} }
private void resetExplorerView() { private void resetExplorerView() {
if (currentView == MapView.OBJECTS) { ObjectExplorer objectExplorer = objectExplorers.get(currentView);
if (objectExplorer != null) {
objectExplorer.resetCamera(); objectExplorer.resetCamera();
return; return;
} }
@@ -487,9 +489,7 @@ public final class MapWindow implements MapCanvas.Listener {
if (paletteExplorer != null) { if (paletteExplorer != null) {
paletteExplorer.close(); paletteExplorer.close();
} }
if (objectExplorer != null) { objectExplorers.values().forEach(ObjectExplorer::close);
objectExplorer.close();
}
if (noiseExplorer != null) { if (noiseExplorer != null) {
noiseExplorer.close(); noiseExplorer.close();
} }
@@ -51,15 +51,21 @@ final class ObjectBrowser extends JPanel {
private final JTextField searchField = new JTextField(); private final JTextField searchField = new JTextField();
private final DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); private final DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
private final JTree tree = new JTree(treeModel); private final JTree tree = new JTree(treeModel);
private final JLabel countLabel = new JLabel("Loading objects..."); private final JLabel countLabel = new JLabel();
private final String noun;
private List<String> objectKeys = List.of(); private List<String> objectKeys = List.of();
private String chosenKey; private String chosenKey;
private boolean updatingSelection; private boolean updatingSelection;
ObjectBrowser(Consumer<String> onObjectChosen) { /**
* @param noun what one entry is called in the browser's texts, such as "object"
*/
ObjectBrowser(String noun, Consumer<String> onObjectChosen) {
super(new BorderLayout()); super(new BorderLayout());
this.noun = noun;
this.onObjectChosen = onObjectChosen; this.onObjectChosen = onObjectChosen;
StudioToolbar.makeSearchField(searchField, "Search objects..."); countLabel.setText("Loading " + noun + "s...");
StudioToolbar.makeSearchField(searchField, "Search " + noun + "s...");
searchField.getDocument().addDocumentListener(new DocumentListener() { searchField.getDocument().addDocumentListener(new DocumentListener() {
@Override @Override
public void insertUpdate(DocumentEvent event) { public void insertUpdate(DocumentEvent event) {
@@ -97,7 +103,7 @@ final class ObjectBrowser extends JPanel {
void showObjects(List<String> keys) { void showObjects(List<String> keys) {
objectKeys = keys.stream().sorted(Comparator.comparing(key -> key.toLowerCase(Locale.ROOT))).toList(); objectKeys = keys.stream().sorted(Comparator.comparing(key -> key.toLowerCase(Locale.ROOT))).toList();
countLabel.setText(objectKeys.size() == 1 ? "1 object" : objectKeys.size() + " objects"); countLabel.setText(objectKeys.size() == 1 ? "1 " + noun : objectKeys.size() + " " + noun + "s");
rebuildTree(); rebuildTree();
} }
@@ -128,7 +134,7 @@ final class ObjectBrowser extends JPanel {
} }
} }
if (root.getChildCount() == 0) { if (root.getChildCount() == 0) {
root.add(new DefaultMutableTreeNode(Entry.note("No matching objects"))); root.add(new DefaultMutableTreeNode(Entry.note("No matching " + noun + "s")));
} }
return root; return root;
} }
@@ -19,10 +19,8 @@
package com.volmit.iris.core.gui; package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.BlockTextures.BlockTexture; import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisObject; import com.volmit.iris.engine.object.IrisObject;
import com.volmit.iris.engine.object.procedural.ProceduralObjectCatalog;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.util.BlockVector; import org.bukkit.util.BlockVector;
@@ -31,8 +29,6 @@ import javax.swing.JComponent;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -45,8 +41,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
/** /**
* The map window's Objects view: every object in the pack in a folder tree, the chosen one in 3D, and what it is made * The map window's Objects and Procedural views: everything its source offers in a folder tree, the chosen one in 3D,
* of. Inspired by the Procedural Objects workbench. * and what it is made of. Inspired by the Procedural Objects workbench.
*/ */
final class ObjectExplorer { final class ObjectExplorer {
private static final NamespacedKey TINT_BIOME = NamespacedKey.minecraft("plains"); private static final NamespacedKey TINT_BIOME = NamespacedKey.minecraft("plains");
@@ -55,10 +51,12 @@ final class ObjectExplorer {
private final Engine startEngine; private final Engine startEngine;
private final UUID worldId; private final UUID worldId;
private final ObjectBrowser browser = new ObjectBrowser(this::showObject); private final ObjectSource source;
private final ObjectBrowser browser;
private final ObjectViewport viewport = new ObjectViewport(); private final ObjectViewport viewport = new ObjectViewport();
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> showLayerCut()); private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> showLayerCut());
private final JComponent viewWithLayerCut = layerCut.floatingOver(viewport); private final ObjectVariantPicker variantPicker = new ObjectVariantPicker(this::showVariant);
private final JComponent viewWithOverlays = variantPicker.floatingOver(layerCut.floatingOver(viewport));
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough); private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
private final StudioStatusBar statusBar = new StudioStatusBar(); private final StudioStatusBar statusBar = new StudioStatusBar();
private final ExecutorService loader = Executors.newSingleThreadExecutor(runnable -> { private final ExecutorService loader = Executors.newSingleThreadExecutor(runnable -> {
@@ -74,33 +72,39 @@ final class ObjectExplorer {
private volatile BlockTextures textures; private volatile BlockTextures textures;
private volatile BiomeTintColors tintColors; private volatile BiomeTintColors tintColors;
private String shownKey; private String shownKey;
private List<IrisObject> shownVariants = List.of();
private LoadedObject shown; private LoadedObject shown;
private ObjectMesh wholeMesh; private ObjectMesh wholeMesh;
private Set<String> seeThroughTypes = Set.of(); private Set<String> seeThroughTypes = Set.of();
ObjectExplorer(Engine engine, UUID worldId) { ObjectExplorer(Engine engine, UUID worldId, ObjectSource source) {
this.startEngine = engine; this.startEngine = engine;
this.worldId = worldId; this.worldId = worldId;
this.source = source;
this.browser = new ObjectBrowser(source.noun(), this::showObject);
GuiHost.interruptOnPluginShutdown(loader); GuiHost.interruptOnPluginShutdown(loader);
GuiHost.registerHotloadHook(hotloadHook); GuiHost.registerHotloadHook(hotloadHook);
statusBar.showPosition(CAMERA_HINT); statusBar.showPosition(CAMERA_HINT);
viewport.bindKey(KeyEvent.VK_PAGE_UP, "show-layer-above", () -> layerCut.stepBy(1)); viewport.bindKey(KeyEvent.VK_PAGE_UP, "show-layer-above", () -> layerCut.stepBy(1));
viewport.bindKey(KeyEvent.VK_PAGE_DOWN, "hide-top-layer", () -> layerCut.stepBy(-1)); viewport.bindKey(KeyEvent.VK_PAGE_DOWN, "hide-top-layer", () -> layerCut.stepBy(-1));
viewport.bindKey(KeyEvent.VK_OPEN_BRACKET, "previous-variant", () -> variantPicker.stepBy(-1));
viewport.bindKey(KeyEvent.VK_CLOSE_BRACKET, "next-variant", () -> variantPicker.stepBy(1));
} }
JComponent browser() { JComponent browser() {
return browser; return browser;
} }
ObjectViewport viewport() {
return viewport;
}
/** /**
* The 3D view with the slider that cuts away its top layers. * The 3D view with the slider that cuts away its top layers and the bar that steps through variants.
*/ */
JComponent view() { JComponent view() {
return viewWithLayerCut; return viewWithOverlays;
}
void setCoveredLeftWidth(int width) {
viewport.setCoveredLeftWidth(width);
variantPicker.setCoveredLeftWidth(width);
} }
JComponent overview() { JComponent overview() {
@@ -143,10 +147,7 @@ final class ObjectExplorer {
} }
private List<String> objectKeys() { private List<String> objectKeys() {
IrisData data = engine().getData(); return source.keys(engine().getData());
List<String> keys = new ArrayList<>(Arrays.asList(data.getObjectLoader().getPossibleKeys()));
keys.addAll(data.getProceduralObjects().variantKeys());
return keys;
} }
private void showCatalog(List<String> keys) { private void showCatalog(List<String> keys) {
@@ -154,9 +155,10 @@ final class ObjectExplorer {
browser.showObjects(keys); browser.showObjects(keys);
String key = shownKey != null && keys.contains(shownKey) ? shownKey : browser.firstObject(); String key = shownKey != null && keys.contains(shownKey) ? shownKey : browser.firstObject();
if (key == null) { if (key == null) {
viewport.showMessage("This pack has no objects"); viewport.showMessage("This pack has no " + source.noun() + "s");
overview.clear(); overview.clear();
layerCut.showHeight(1); layerCut.showHeight(1);
variantPicker.showVariants(0);
return; return;
} }
browser.select(key); browser.select(key);
@@ -165,31 +167,54 @@ final class ObjectExplorer {
private void showObject(String key) { private void showObject(String key) {
shownKey = key; shownKey = key;
shown = null; shownVariants = List.of();
wholeMesh = null; variantPicker.showVariants(0);
meshRevision.incrementAndGet(); long revision = startLoading(key);
long revision = objectRevision.incrementAndGet();
viewport.showMessage("Loading " + key + "...");
statusBar.showActivity("Loading " + key + "...");
loader.execute(() -> { loader.execute(() -> {
if (revision != objectRevision.get()) { if (revision != objectRevision.get()) {
return; return;
} }
try { try {
LoadedObject loaded = load(key); List<IrisObject> variants = source.variants(engine().getData(), key);
EventQueue.invokeLater(() -> showLoaded(revision, loaded)); if (variants.isEmpty()) {
throw new IllegalStateException("Nothing could be loaded for " + key);
}
LoadedObject loaded = load(variants.get(0));
EventQueue.invokeLater(() -> {
if (revision == objectRevision.get()) {
shownVariants = variants;
variantPicker.showVariants(variants.size());
}
showLoaded(revision, loaded);
});
} catch (RuntimeException error) { } catch (RuntimeException error) {
EventQueue.invokeLater(() -> showFailure(revision, key, error)); EventQueue.invokeLater(() -> showFailure(revision, key, error));
} }
}); });
} }
private LoadedObject load(String key) { private void showVariant(int index) {
IrisObject object = objectFor(key); IrisObject variant = shownVariants.get(index);
if (object == null) { long revision = startLoading(variant.getLoadKey());
throw new IllegalStateException(ProceduralObjectCatalog.isVariantKey(key) loader.execute(() -> {
? "This variant has no blocks" : "The object file could not be read"); if (revision != objectRevision.get()) {
return;
} }
LoadedObject loaded = load(variant);
EventQueue.invokeLater(() -> showLoaded(revision, loaded));
});
}
private long startLoading(String key) {
shown = null;
wholeMesh = null;
meshRevision.incrementAndGet();
viewport.showMessage("Loading " + key + "...");
statusBar.showActivity("Loading " + key + "...");
return objectRevision.incrementAndGet();
}
private LoadedObject load(IrisObject object) {
Map<BlockVector, BlockData> blocks = new HashMap<>(); Map<BlockVector, BlockData> blocks = new HashMap<>();
object.getBlocks().forEach((position, block) -> { object.getBlocks().forEach((position, block) -> {
if (!block.getMaterial().isAir()) { if (!block.getMaterial().isAir()) {
@@ -197,17 +222,10 @@ final class ObjectExplorer {
} }
}); });
ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf); ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf);
ObjectSummary summary = ObjectSummary.of(key, mesh.bounds(), blocks.values(), this::iconOf); ObjectSummary summary = ObjectSummary.of(object.getLoadKey(), mesh.bounds(), blocks.values(), this::iconOf);
return new LoadedObject(blocks, mesh, summary); return new LoadedObject(blocks, mesh, summary);
} }
private IrisObject objectFor(String key) {
IrisData data = engine().getData();
if (ProceduralObjectCatalog.isVariantKey(key)) {
return data.getProceduralObjects().variant(key);
}
return data.getObjectLoader().load(key);
}
private void showLoaded(long revision, LoadedObject loaded) { private void showLoaded(long revision, LoadedObject loaded) {
if (revision != objectRevision.get()) { if (revision != objectRevision.get()) {
@@ -0,0 +1,38 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.object.IrisObject;
import java.util.List;
/**
* Where an object explorer gets what it shows: the pack's object files, or the objects Iris generates from settings.
* Both are called on the explorer's loader thread only.
*/
interface ObjectSource {
/** What one entry is called, such as "object", for the browser's texts. */
String noun();
/** The key of every entry, where a slash separates folders. */
List<String> keys(IrisData data);
/** The objects of the entry, one for an object file and one per variant for a procedural object; empty when none load. */
List<IrisObject> variants(IrisData data, String key);
}
@@ -0,0 +1,137 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.RoundRectangle2D;
import java.util.function.IntConsumer;
/**
* A small bar floating at the bottom of the 3D view that steps through the variants of a procedural object. It hides
* itself for anything with a single variant.
*/
final class ObjectVariantPicker extends JPanel {
private static final long serialVersionUID = -2716301839488217455L;
private static final int BOTTOM_MARGIN = 14;
private static final int LABEL_WIDTH = 110;
private static final String SHORTCUT_HINT = " · [ and ]";
private final JLabel shownVariant = new JLabel(" ", SwingConstants.CENTER);
private final IntConsumer onVariantChosen;
private int variantCount;
private int shownIndex;
private int coveredLeftWidth;
/**
* @param onVariantChosen told the index of the variant to show whenever the user steps to another one
*/
ObjectVariantPicker(IntConsumer onVariantChosen) {
super(new BorderLayout());
this.onVariantChosen = onVariantChosen;
setOpaque(false);
setBorder(StudioTheme.emptyBorder(3, 4, 3, 4));
shownVariant.setFont(StudioTheme.smallFont());
shownVariant.setForeground(StudioTheme.TEXT);
shownVariant.setPreferredSize(new Dimension(StudioTheme.scale(LABEL_WIDTH), shownVariant.getPreferredSize().height));
add(StudioToolbar.button("‹", "Previous variant" + SHORTCUT_HINT, () -> stepBy(-1)), BorderLayout.WEST);
add(shownVariant, BorderLayout.CENTER);
add(StudioToolbar.button("›", "Next variant" + SHORTCUT_HINT, () -> stepBy(1)), BorderLayout.EAST);
showVariants(0);
}
/**
* The view with this bar floating over its bottom edge, centred on the part the left island leaves uncovered.
*/
JComponent floatingOver(JComponent view) {
ObjectVariantPicker picker = this;
JPanel stack = new JPanel(null) {
private static final long serialVersionUID = 1609842937565014832L;
// The bar overlaps the view, so the view may not repaint without the bar on top of it.
@Override
public boolean isOptimizedDrawingEnabled() {
return false;
}
@Override
public void doLayout() {
view.setBounds(0, 0, getWidth(), getHeight());
Dimension size = picker.getPreferredSize();
int centerX = coveredLeftWidth + (getWidth() - coveredLeftWidth) / 2;
picker.setBounds(centerX - size.width / 2, getHeight() - size.height - StudioTheme.scale(BOTTOM_MARGIN), size.width, size.height);
}
};
stack.add(picker);
stack.add(view);
return stack;
}
void setCoveredLeftWidth(int width) {
coveredLeftWidth = width;
if (getParent() != null) {
getParent().revalidate();
}
}
/**
* Starts again at the first of {@code count} variants without telling anyone, since the first is what loads anyway.
*/
void showVariants(int count) {
variantCount = count;
shownIndex = 0;
showLabel();
setVisible(count > 1);
}
void stepBy(int step) {
if (variantCount < 2) {
return;
}
shownIndex = Math.floorMod(shownIndex + step, variantCount);
showLabel();
onVariantChosen.accept(shownIndex);
}
private void showLabel() {
shownVariant.setText("Variant " + (shownIndex + 1) + " of " + variantCount);
}
@Override
protected void paintComponent(Graphics graphics) {
Graphics2D canvas = (Graphics2D) graphics.create();
try {
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
double arc = StudioTheme.scale(StudioTheme.OVERLAY_ARC);
RoundRectangle2D shape = new RoundRectangle2D.Double(0.5, 0.5, getWidth() - 1D, getHeight() - 1D, arc, arc);
canvas.setColor(StudioTheme.OVERLAY_BACKGROUND);
canvas.fill(shape);
canvas.setColor(StudioTheme.OVERLAY_BORDER);
canvas.draw(shape);
} finally {
canvas.dispose();
}
}
}
@@ -0,0 +1,45 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.object.IrisObject;
import java.util.Arrays;
import java.util.List;
/**
* The pack's object files, by their load keys.
*/
final class PackObjectSource implements ObjectSource {
@Override
public String noun() {
return "object";
}
@Override
public List<String> keys(IrisData data) {
return Arrays.asList(data.getObjectLoader().getPossibleKeys());
}
@Override
public List<IrisObject> variants(IrisData data, String key) {
IrisObject object = data.getObjectLoader().load(key);
return object == null ? List.of() : List.of(object);
}
}
@@ -0,0 +1,62 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.object.IrisObject;
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* The objects the pack's procedural settings generate, one entry per definition in a folder per type.
*/
final class ProceduralObjectSource implements ObjectSource {
private static final Map<String, String> FOLDER_BY_KIND = Map.of(
"tree", "Trees",
"formation", "Formations",
"crystal", "Crystals",
"coral", "Coral",
"fungus", "Fungi",
"ruin", "Ruins");
private Map<String, ResolvedProceduralPlacement> definitionsByKey = Map.of();
@Override
public String noun() {
return "procedural object";
}
@Override
public List<String> keys(IrisData data) {
Map<String, ResolvedProceduralPlacement> byKey = new LinkedHashMap<>();
for (ResolvedProceduralPlacement definition : data.getProceduralObjects().definitions()) {
byKey.put(FOLDER_BY_KIND.getOrDefault(definition.kind(), definition.kind()) + "/" + definition.name(), definition);
}
definitionsByKey = byKey;
return List.copyOf(byKey.keySet());
}
@Override
public List<IrisObject> variants(IrisData data, String key) {
ResolvedProceduralPlacement definition = definitionsByKey.get(key);
return definition == null ? List.of() : definition.variants();
}
}
@@ -20,7 +20,6 @@ package com.volmit.iris.engine.object.procedural;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.volmit.iris.Iris; import com.volmit.iris.Iris;
import com.volmit.iris.core.loader.IrisData; import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.object.IrisObject;
import com.volmit.iris.engine.object.annotations.Snippet; import com.volmit.iris.engine.object.annotations.Snippet;
import com.volmit.iris.engine.object.procedural.coral.Coral; import com.volmit.iris.engine.object.procedural.coral.Coral;
import com.volmit.iris.engine.object.procedural.coral.ResolvedCoral; import com.volmit.iris.engine.object.procedural.coral.ResolvedCoral;
@@ -41,15 +40,14 @@ import java.io.Reader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
* The procedural objects of one pack: those its regions and biomes declare, and those saved on their own as snippets, * The procedural objects of one pack: those its regions and biomes declare, and those saved on their own as snippets,
* such as {@code snippet/procedural-tree/oak.json}. A definition several places share is baked once, and every variant * such as {@code snippet/procedural-tree/oak.json}. A definition several places share is baked once.
* can be found again by its key.
*/ */
public final class ProceduralObjectCatalog { public final class ProceduralObjectCatalog {
private static final Gson DEFINITION_JSON = new Gson(); private static final Gson DEFINITION_JSON = new Gson();
@@ -57,7 +55,6 @@ public final class ProceduralObjectCatalog {
private final IrisData data; private final IrisData data;
private final KMap<String, ResolvedProceduralPlacement> byDefinition = new KMap<>(); private final KMap<String, ResolvedProceduralPlacement> byDefinition = new KMap<>();
private final KMap<String, ResolvedProceduralPlacement> byVariantKey = new KMap<>();
public ProceduralObjectCatalog(IrisData data) { public ProceduralObjectCatalog(IrisData data) {
this.data = data; this.data = data;
@@ -74,7 +71,7 @@ public final class ProceduralObjectCatalog {
private ResolvedProceduralPlacement placementOf(Object definition) { private ResolvedProceduralPlacement placementOf(Object definition) {
String identity = definition.getClass().getName() + DEFINITION_JSON.toJson(definition); String identity = definition.getClass().getName() + DEFINITION_JSON.toJson(definition);
return byDefinition.computeIfAbsent(identity, ignored -> register(resolvedFor(definition))); return byDefinition.computeIfAbsent(identity, ignored -> resolvedFor(definition));
} }
private ResolvedProceduralPlacement resolvedFor(Object definition) { private ResolvedProceduralPlacement resolvedFor(Object definition) {
@@ -89,22 +86,29 @@ public final class ProceduralObjectCatalog {
}; };
} }
private ResolvedProceduralPlacement register(ResolvedProceduralPlacement placement) { /**
for (String key : placement.variantKeys()) { * Every procedural object the pack defines, in snippets or in regions and biomes, whether or not its dimensions
byVariantKey.putIfAbsent(key, placement); * reach them. Where two definitions share a type and name, the first one found stands for both.
*/
public List<ResolvedProceduralPlacement> definitions() {
Map<String, ResolvedProceduralPlacement> byTypeAndName = new LinkedHashMap<>();
for (Class<?> type : DEFINITION_TYPES) {
snippetDefinitions(type).forEach(definition -> addDefinition(byTypeAndName, placementOf(definition)));
} }
return placement; data.getRegionLoader().streamAll().forEach(region -> addDefinitions(byTypeAndName, region.getProceduralObjects()));
data.getBiomeLoader().streamAll().forEach(biome -> addDefinitions(byTypeAndName, biome.getProceduralObjects()));
return List.copyOf(byTypeAndName.values());
} }
/** The key of every variant the pack defines, in snippets or in regions and biomes, whether or not its dimensions reach them. */ private void addDefinitions(Map<String, ResolvedProceduralPlacement> byTypeAndName, ProceduralObjectSettings settings) {
public List<String> variantKeys() { if (settings == null) {
Set<String> keys = new LinkedHashSet<>(); return;
for (Class<?> type : DEFINITION_TYPES) {
snippetDefinitions(type).forEach(definition -> keys.addAll(placementOf(definition).variantKeys()));
} }
data.getRegionLoader().streamAll().forEach(region -> addVariantKeys(keys, region.getProceduralObjects())); settings.resolved(data).forEach(placement -> addDefinition(byTypeAndName, placement));
data.getBiomeLoader().streamAll().forEach(biome -> addVariantKeys(keys, biome.getProceduralObjects())); }
return List.copyOf(keys);
private static void addDefinition(Map<String, ResolvedProceduralPlacement> byTypeAndName, ResolvedProceduralPlacement placement) {
byTypeAndName.putIfAbsent(placement.kind() + "/" + placement.name(), placement);
} }
private List<Object> snippetDefinitions(Class<?> type) { private List<Object> snippetDefinitions(Class<?> type) {
@@ -136,22 +140,4 @@ public final class ProceduralObjectCatalog {
} }
} }
private void addVariantKeys(Set<String> keys, ProceduralObjectSettings settings) {
if (settings == null) {
return;
}
for (ResolvedProceduralPlacement placement : settings.resolved(data)) {
keys.addAll(placement.variantKeys());
}
}
/** The baked variant loaded under {@code key}; null when no definition resolved so far has it, or it baked no blocks. */
public IrisObject variant(String key) {
ResolvedProceduralPlacement placement = byVariantKey.get(key);
return placement == null ? null : placement.variant(key);
}
public static boolean isVariantKey(String key) {
return key.startsWith("procedural/");
}
} }
@@ -68,6 +68,11 @@ public abstract class ResolvedProceduralPlacement {
return shape.name(); return shape.name();
} }
/** The type of object, as the variant keys name it: tree, ruin, formation, coral, fungus or crystal. */
public String kind() {
return shape.kind();
}
/** /**
* Whether the world generator can place it. A place mode this version does not know, such as v4's CEILING_HANG, * Whether the world generator can place it. A place mode this version does not know, such as v4's CEILING_HANG,
* reads as null, and placing such an object on the surface would put it in the wrong place. * reads as null, and placing such an object on the surface would put it in the wrong place.
@@ -130,15 +135,6 @@ public abstract class ResolvedProceduralPlacement {
return baked.get(rng.i(baked.size())); return baked.get(rng.i(baked.size()));
} }
/** The baked variant loaded under {@code key}; null when it is not one of these, or baked no blocks. */
public IrisObject variant(String key) {
for (IrisObject variant : variants()) {
if (variant.getLoadKey().equals(key)) {
return variant;
}
}
return null;
}
/** Bakes every variant; the default draws variant {@code i} from {@code seed + i * stride}. */ /** Bakes every variant; the default draws variant {@code i} from {@code seed + i * stride}. */
protected List<IrisObject> bake(ObjectBuilder blocks) { protected List<IrisObject> bake(ObjectBuilder blocks) {
@@ -0,0 +1,7 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.79289 13.7929L2.20711 13.2071C1.81658 12.8166 1.81658 12.1834 2.20711 11.7929L8.79289 5.20711C9.18342 4.81658 9.81658 4.81658 10.2071 5.20711L10.7929 5.79289C11.1834 6.18342 11.1834 6.81658 10.7929 7.20711L4.20711 13.7929C3.81658 14.1834 3.18342 14.1834 2.79289 13.7929Z" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 8.5L7.5 6.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 2.5V1.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 5.5H14.5M11.5 9.5L12.5 10.5M6.5 4.5L5.5 3.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 889 B

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.79289 13.7929L2.20711 13.2071C1.81658 12.8166 1.81658 12.1834 2.20711 11.7929L8.79289 5.20711C9.18342 4.81658 9.81658 4.81658 10.2071 5.20711L10.7929 5.79289C11.1834 6.18342 11.1834 6.81658 10.7929 7.20711L4.20711 13.7929C3.81658 14.1834 3.18342 14.1834 2.79289 13.7929Z" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 8.5L7.5 6.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 2.5V1.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 5.5H14.5M11.5 9.5L12.5 10.5M6.5 4.5L5.5 3.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 889 B