mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +00:00
Compare procedural variants side by side
Grid at one scale; step seeds, back to pack seed.
This commit is contained in:
1 parent
03fd5e4bf3
commit
f30f761ae6
8 files changed
+703
-71
No files matched your search
@@ -19,6 +19,7 @@
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
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.object.IrisObject;
|
||||
import org.bukkit.NamespacedKey;
|
||||
@@ -26,13 +27,17 @@ import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.util.BlockVector;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -42,12 +47,15 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* The map window's Objects and Procedural views: everything its source offers in a folder tree, the chosen one in 3D,
|
||||
* and what it is made of. Inspired by the Procedural Objects workbench.
|
||||
* and what it is made of. A procedural object's variants can also be compared side by side, baked from any seed.
|
||||
* Inspired by the Procedural Objects workbench.
|
||||
*/
|
||||
final class ObjectExplorer {
|
||||
private static final NamespacedKey TINT_BIOME = NamespacedKey.minecraft("plains");
|
||||
private static final int UNKNOWN_BLOCK_COLOR = 0x8A8A8A;
|
||||
private static final String CAMERA_HINT = "Drag to orbit · Shift-drag to pan · Scroll to zoom · Page Up/Down cuts layers · R resets";
|
||||
private static final String ONE_VARIANT_CARD = "one variant";
|
||||
private static final String ALL_VARIANTS_CARD = "all variants";
|
||||
|
||||
private final Engine startEngine;
|
||||
private final UUID worldId;
|
||||
@@ -55,8 +63,11 @@ final class ObjectExplorer {
|
||||
private final ObjectBrowser browser;
|
||||
private final ObjectViewport viewport = new ObjectViewport();
|
||||
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> showLayerCut());
|
||||
private final ObjectVariantPicker variantPicker = new ObjectVariantPicker(this::showVariant);
|
||||
private final JComponent viewWithOverlays = variantPicker.floatingOver(layerCut.floatingOver(viewport));
|
||||
private final ObjectVariantPicker variantPicker = new ObjectVariantPicker(this::showVariant, this::showSeed, this::toggleComparing);
|
||||
private final ObjectVariantGallery gallery = new ObjectVariantGallery(this::openVariant, this::redrawThumbnails);
|
||||
private final CardLayout viewCards = new CardLayout();
|
||||
private final JPanel views = new JPanel(viewCards);
|
||||
private final JComponent viewWithOverlays;
|
||||
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
|
||||
private final StudioStatusBar statusBar = new StudioStatusBar();
|
||||
private final ExecutorService loader = Executors.newSingleThreadExecutor(runnable -> {
|
||||
@@ -65,7 +76,14 @@ final class ObjectExplorer {
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
});
|
||||
private final ExecutorService thumbnailPainter = Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris Variant Thumbnails");
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
});
|
||||
private final AtomicLong objectRevision = new AtomicLong();
|
||||
private final AtomicLong variantListRevision = new AtomicLong();
|
||||
private final AtomicLong meshRevision = new AtomicLong();
|
||||
private final Map<String, BlockModel> models = new ConcurrentHashMap<>();
|
||||
private final Runnable hotloadHook = this::reloadPack;
|
||||
@@ -73,6 +91,11 @@ final class ObjectExplorer {
|
||||
private volatile BiomeTintColors tintColors;
|
||||
private String shownKey;
|
||||
private List<IrisObject> shownVariants = List.of();
|
||||
private int shownVariantIndex;
|
||||
private OptionalLong packSeed = OptionalLong.empty();
|
||||
private OptionalLong shownSeed = OptionalLong.empty();
|
||||
private boolean comparingVariants;
|
||||
private long thumbnailsDrawnForRevision = -1;
|
||||
private LoadedObject shown;
|
||||
private ObjectMesh wholeMesh;
|
||||
private Set<String> seeThroughTypes = Set.of();
|
||||
@@ -82,13 +105,22 @@ final class ObjectExplorer {
|
||||
this.worldId = worldId;
|
||||
this.source = source;
|
||||
this.browser = new ObjectBrowser(source.noun(), this::showObject);
|
||||
views.add(layerCut.floatingOver(viewport), ONE_VARIANT_CARD);
|
||||
views.add(gallery, ALL_VARIANTS_CARD);
|
||||
this.viewWithOverlays = variantPicker.floatingOver(views);
|
||||
GuiHost.interruptOnPluginShutdown(loader);
|
||||
GuiHost.interruptOnPluginShutdown(thumbnailPainter);
|
||||
GuiHost.registerHotloadHook(hotloadHook);
|
||||
statusBar.showPosition(CAMERA_HINT);
|
||||
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_OPEN_BRACKET, "previous-variant", () -> variantPicker.stepBy(-1));
|
||||
viewport.bindKey(KeyEvent.VK_CLOSE_BRACKET, "next-variant", () -> variantPicker.stepBy(1));
|
||||
viewport.bindKey(KeyEvent.VK_G, "compare-variants", this::toggleComparing);
|
||||
gallery.bindKey(KeyEvent.VK_OPEN_BRACKET, "previous-variant", () -> variantPicker.stepBy(-1));
|
||||
gallery.bindKey(KeyEvent.VK_CLOSE_BRACKET, "next-variant", () -> variantPicker.stepBy(1));
|
||||
gallery.bindKey(KeyEvent.VK_G, "show-one-variant", this::toggleComparing);
|
||||
gallery.bindKey(KeyEvent.VK_ESCAPE, "close-comparison", this::toggleComparing);
|
||||
}
|
||||
|
||||
JComponent browser() {
|
||||
@@ -96,7 +128,8 @@ final class ObjectExplorer {
|
||||
}
|
||||
|
||||
/**
|
||||
* The 3D view with the slider that cuts away its top layers and the bar that steps through variants.
|
||||
* The 3D view with the slider that cuts away its top layers, or every variant side by side, under the bar that
|
||||
* steps through variants and seeds.
|
||||
*/
|
||||
JComponent view() {
|
||||
return viewWithOverlays;
|
||||
@@ -104,6 +137,7 @@ final class ObjectExplorer {
|
||||
|
||||
void setCoveredLeftWidth(int width) {
|
||||
viewport.setCoveredLeftWidth(width);
|
||||
gallery.setCoveredLeftWidth(width);
|
||||
variantPicker.setCoveredLeftWidth(width);
|
||||
}
|
||||
|
||||
@@ -133,6 +167,7 @@ final class ObjectExplorer {
|
||||
GuiHost.unregisterHotloadHook(hotloadHook);
|
||||
objectRevision.incrementAndGet();
|
||||
loader.shutdownNow();
|
||||
thumbnailPainter.shutdownNow();
|
||||
viewport.close();
|
||||
}
|
||||
|
||||
@@ -158,42 +193,103 @@ final class ObjectExplorer {
|
||||
viewport.showMessage("This pack has no " + source.noun() + "s");
|
||||
overview.clear();
|
||||
layerCut.showHeight(1);
|
||||
variantPicker.showVariants(0);
|
||||
if (comparingVariants) {
|
||||
toggleComparing();
|
||||
}
|
||||
showVariantList(List.of(), OptionalLong.empty(), OptionalLong.empty(), 0);
|
||||
return;
|
||||
}
|
||||
browser.select(key);
|
||||
showObject(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the first variant of the entry, baked from the pack's seed. The pack reloading the shown entry keeps the
|
||||
* variant and the seed being looked at, unless the pack itself now sets another seed.
|
||||
*/
|
||||
private void showObject(String key) {
|
||||
boolean reloadingShownObject = key.equals(shownKey);
|
||||
OptionalLong previousPackSeed = packSeed;
|
||||
OptionalLong exploredSeed = shownSeed;
|
||||
int variantIndex = reloadingShownObject ? shownVariantIndex : 0;
|
||||
shownKey = key;
|
||||
shownVariants = List.of();
|
||||
variantPicker.showVariants(0);
|
||||
if (!reloadingShownObject) {
|
||||
showVariantList(List.of(), OptionalLong.empty(), OptionalLong.empty(), 0);
|
||||
}
|
||||
long revision = startLoading(key);
|
||||
loader.execute(() -> {
|
||||
if (revision != objectRevision.get()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<IrisObject> variants = source.variants(engine().getData(), key);
|
||||
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) {
|
||||
EventQueue.invokeLater(() -> showFailure(revision, key, error));
|
||||
OptionalLong pack = source.seed(key);
|
||||
OptionalLong seed = reloadingShownObject && pack.equals(previousPackSeed) ? exploredSeed : pack;
|
||||
loadVariants(revision, key, pack, seed, variantIndex);
|
||||
});
|
||||
}
|
||||
|
||||
// The seed shows at once, so stepping again continues from it even while this one bakes or fails.
|
||||
private void showSeed(long seed) {
|
||||
String key = shownKey;
|
||||
OptionalLong pack = packSeed;
|
||||
int variantIndex = shownVariantIndex;
|
||||
shownSeed = OptionalLong.of(seed);
|
||||
variantPicker.showSeed(shownSeed, pack);
|
||||
long revision = startLoading(key + " with seed " + seed);
|
||||
loader.execute(() -> {
|
||||
if (revision == objectRevision.get()) {
|
||||
loadVariants(revision, key, pack, OptionalLong.of(seed), variantIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// On the loader thread.
|
||||
private void loadVariants(long revision, String key, OptionalLong pack, OptionalLong seed, int preferredIndex) {
|
||||
try {
|
||||
IrisData data = engine().getData();
|
||||
List<IrisObject> variants = seed.isPresent() ? source.variants(data, key, seed.getAsLong()) : source.variants(data, key);
|
||||
if (variants.isEmpty()) {
|
||||
throw new IllegalStateException("Nothing could be loaded for " + key);
|
||||
}
|
||||
int index = preferredIndex < variants.size() ? preferredIndex : 0;
|
||||
LoadedObject loaded = load(variants.get(index));
|
||||
EventQueue.invokeLater(() -> {
|
||||
if (revision != objectRevision.get()) {
|
||||
return;
|
||||
}
|
||||
showVariantList(variants, pack, seed, index);
|
||||
showLoaded(revision, loaded);
|
||||
});
|
||||
} catch (RuntimeException error) {
|
||||
EventQueue.invokeLater(() -> showFailure(revision, key, error));
|
||||
}
|
||||
}
|
||||
|
||||
private void showVariantList(List<IrisObject> variants, OptionalLong pack, OptionalLong seed, int index) {
|
||||
shownVariants = variants;
|
||||
shownVariantIndex = index;
|
||||
packSeed = pack;
|
||||
shownSeed = seed;
|
||||
variantListRevision.incrementAndGet();
|
||||
variantPicker.showVariants(variants.size(), index);
|
||||
variantPicker.showSeed(seed, pack);
|
||||
if (!comparingVariants) {
|
||||
return;
|
||||
}
|
||||
if (variants.isEmpty()) {
|
||||
gallery.showMessage("Loading " + shownKey + "...");
|
||||
} else if (variants.size() == 1) {
|
||||
toggleComparing();
|
||||
} else {
|
||||
drawThumbnails();
|
||||
}
|
||||
}
|
||||
|
||||
private void showVariant(int index) {
|
||||
if (index >= shownVariants.size()) {
|
||||
return;
|
||||
}
|
||||
shownVariantIndex = index;
|
||||
gallery.select(index);
|
||||
IrisObject variant = shownVariants.get(index);
|
||||
long revision = startLoading(variant.getLoadKey());
|
||||
loader.execute(() -> {
|
||||
@@ -205,6 +301,85 @@ final class ObjectExplorer {
|
||||
});
|
||||
}
|
||||
|
||||
private void openVariant(int index) {
|
||||
variantPicker.showVariants(shownVariants.size(), index);
|
||||
showVariant(index);
|
||||
toggleComparing();
|
||||
}
|
||||
|
||||
private void toggleComparing() {
|
||||
if (!comparingVariants && shownVariants.size() < 2) {
|
||||
return;
|
||||
}
|
||||
comparingVariants = !comparingVariants;
|
||||
variantPicker.showComparing(comparingVariants);
|
||||
viewCards.show(views, comparingVariants ? ALL_VARIANTS_CARD : ONE_VARIANT_CARD);
|
||||
if (comparingVariants) {
|
||||
drawThumbnails();
|
||||
gallery.requestFocusInWindow();
|
||||
} else {
|
||||
viewport.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
|
||||
private void redrawThumbnails() {
|
||||
thumbnailsDrawnForRevision = -1;
|
||||
if (comparingVariants) {
|
||||
drawThumbnails();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the side-by-side view with every shown variant, all drawn at the scale that fits the largest, so their
|
||||
* sizes compare. Thumbnails arrive one at a time on their own thread, leaving the loader free for the 3D view.
|
||||
*/
|
||||
private void drawThumbnails() {
|
||||
long revision = variantListRevision.get();
|
||||
if (revision == thumbnailsDrawnForRevision) {
|
||||
return;
|
||||
}
|
||||
thumbnailsDrawnForRevision = revision;
|
||||
List<IrisObject> variants = shownVariants;
|
||||
gallery.showVariants(galleryHeading(), variants.size(), shownVariantIndex);
|
||||
int size = gallery.thumbnailSize();
|
||||
thumbnailPainter.execute(() -> {
|
||||
List<ObjectMesh> meshes = new ArrayList<>();
|
||||
for (IrisObject variant : variants) {
|
||||
if (revision != variantListRevision.get()) {
|
||||
return;
|
||||
}
|
||||
meshes.add(ObjectMesh.build(visibleBlocks(variant), this::modelOf));
|
||||
}
|
||||
ObjectMesh.Bounds sizeToFit = largestSize(meshes);
|
||||
for (int index = 0; index < meshes.size() && revision == variantListRevision.get(); index++) {
|
||||
BufferedImage thumbnail = ObjectRenderer.renderThumbnail(meshes.get(index), sizeToFit, size, size);
|
||||
int shownIndex = index;
|
||||
EventQueue.invokeLater(() -> {
|
||||
if (revision == variantListRevision.get()) {
|
||||
gallery.showThumbnail(shownIndex, thumbnail);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String galleryHeading() {
|
||||
String seed = shownSeed.isPresent() ? " · seed " + shownSeed.getAsLong() : "";
|
||||
return String.format(Locale.ROOT, "%s · %d variants%s", shownKey, shownVariants.size(), seed);
|
||||
}
|
||||
|
||||
private static ObjectMesh.Bounds largestSize(List<ObjectMesh> meshes) {
|
||||
int sizeX = 1;
|
||||
int sizeY = 1;
|
||||
int sizeZ = 1;
|
||||
for (ObjectMesh mesh : meshes) {
|
||||
sizeX = Math.max(sizeX, mesh.bounds().sizeX());
|
||||
sizeY = Math.max(sizeY, mesh.bounds().sizeY());
|
||||
sizeZ = Math.max(sizeZ, mesh.bounds().sizeZ());
|
||||
}
|
||||
return new ObjectMesh.Bounds(0, 0, 0, sizeX, sizeY, sizeZ);
|
||||
}
|
||||
|
||||
private long startLoading(String key) {
|
||||
shown = null;
|
||||
wholeMesh = null;
|
||||
@@ -215,15 +390,20 @@ final class ObjectExplorer {
|
||||
}
|
||||
|
||||
private LoadedObject load(IrisObject object) {
|
||||
Map<BlockVector, BlockData> blocks = visibleBlocks(object);
|
||||
ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf);
|
||||
ObjectSummary summary = ObjectSummary.of(object.getLoadKey(), mesh.bounds(), blocks.values(), this::iconOf);
|
||||
return new LoadedObject(blocks, mesh, summary);
|
||||
}
|
||||
|
||||
private static Map<BlockVector, BlockData> visibleBlocks(IrisObject object) {
|
||||
Map<BlockVector, BlockData> blocks = new HashMap<>();
|
||||
object.getBlocks().forEach((position, block) -> {
|
||||
if (!block.getMaterial().isAir()) {
|
||||
blocks.put(position, block);
|
||||
}
|
||||
});
|
||||
ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf);
|
||||
ObjectSummary summary = ObjectSummary.of(object.getLoadKey(), mesh.bounds(), blocks.values(), this::iconOf);
|
||||
return new LoadedObject(blocks, mesh, summary);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
|
||||
@@ -246,8 +426,9 @@ final class ObjectExplorer {
|
||||
String cut = layerCut.isCutting()
|
||||
? String.format(Locale.ROOT, " · showing layers 1-%d of %d", layerCut.shownLayerCount(), layerCut.layerCount())
|
||||
: "";
|
||||
statusBar.showSummary(String.format(Locale.ROOT, "%s · %,d blocks · %,d faces drawn%s",
|
||||
summary.key(), summary.blockCount(), mesh.faceCount(), cut));
|
||||
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));
|
||||
}
|
||||
|
||||
private void showSeeThrough(Set<String> blockTypes) {
|
||||
@@ -296,6 +477,8 @@ final class ObjectExplorer {
|
||||
}
|
||||
statusBar.showActivity("");
|
||||
viewport.showMessage("Could not load " + key);
|
||||
gallery.showMessage("Could not load " + key);
|
||||
thumbnailsDrawnForRevision = -1;
|
||||
overview.clear();
|
||||
layerCut.showHeight(1);
|
||||
statusBar.showError(error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage());
|
||||
|
||||
@@ -47,6 +47,8 @@ final class ObjectRenderer {
|
||||
// A face's own texels end a hair inside its UV rectangle, so rounding never picks the texel next to it.
|
||||
private static final double TEXEL_EDGE_MARGIN = 1e-6D;
|
||||
private static final double FULL_ALPHA = 255D;
|
||||
// Zoom 2 fills the picture with the object's bounding box; thumbnails keep a hair of margin around it.
|
||||
private static final ObjectCamera THUMBNAIL_CAMERA = new ObjectCamera(ObjectCamera.START.yaw(), ObjectCamera.START.pitch(), 1.9D, 0D, 0D);
|
||||
|
||||
private final int width;
|
||||
private final int height;
|
||||
@@ -56,13 +58,15 @@ final class ObjectRenderer {
|
||||
private final Projection projection;
|
||||
private final int gridColor = GRID.getRGB() & 0xFFFFFF;
|
||||
|
||||
private ObjectRenderer(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
|
||||
private ObjectRenderer(BufferedImage image, Projection projection) {
|
||||
this.width = image.getWidth();
|
||||
this.height = image.getHeight();
|
||||
this.pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
this.depths = new double[pixels.length];
|
||||
this.showsObject = new boolean[pixels.length];
|
||||
this.projection = new Projection(mesh.bounds(), camera, width, height, coveredLeftWidth);
|
||||
this.projection = projection;
|
||||
Arrays.fill(pixels, StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF);
|
||||
Arrays.fill(depths, Double.NEGATIVE_INFINITY);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,21 +74,38 @@ final class ObjectRenderer {
|
||||
* fitted in the rest
|
||||
*/
|
||||
static BufferedImage render(ObjectMesh mesh, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
|
||||
BufferedImage image = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
|
||||
ObjectRenderer renderer = new ObjectRenderer(mesh, camera, image, Math.max(0, Math.min(width - 1, coveredLeftWidth)));
|
||||
Arrays.fill(renderer.pixels, StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF);
|
||||
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
|
||||
BufferedImage image = blankImage(width, height);
|
||||
int covered = Math.max(0, Math.min(image.getWidth() - 1, coveredLeftWidth));
|
||||
ObjectRenderer renderer = new ObjectRenderer(image, new Projection(mesh.bounds(), mesh.bounds(), camera, image.getWidth(), image.getHeight(), covered));
|
||||
renderer.drawGrid(mesh.bounds());
|
||||
renderer.drawMesh(mesh);
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws {@code mesh} from the starting angle, without the floor grid, at the scale that fits an object of
|
||||
* {@code sizeToFit}. Objects drawn with the same size compare at their true sizes, standing on the same ground.
|
||||
*/
|
||||
static BufferedImage renderThumbnail(ObjectMesh mesh, ObjectMesh.Bounds sizeToFit, int width, int height) {
|
||||
BufferedImage image = blankImage(width, height);
|
||||
new ObjectRenderer(image, new Projection(mesh.bounds(), sizeToFit, THUMBNAIL_CAMERA, image.getWidth(), image.getHeight(), 0)).drawMesh(mesh);
|
||||
return image;
|
||||
}
|
||||
|
||||
private static BufferedImage blankImage(int width, int height) {
|
||||
return new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
|
||||
}
|
||||
|
||||
private void drawMesh(ObjectMesh mesh) {
|
||||
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
|
||||
renderer.drawQuad(quad, Pass.SOLID);
|
||||
drawQuad(quad, Pass.SOLID);
|
||||
}
|
||||
for (ObjectMesh.Quad quad : mesh.cutAwayQuads()) {
|
||||
renderer.drawQuad(quad, Pass.CUT_AWAY_GHOST);
|
||||
drawQuad(quad, Pass.CUT_AWAY_GHOST);
|
||||
}
|
||||
for (ObjectMesh.Quad quad : renderer.farthestFirst(mesh.translucentQuads())) {
|
||||
renderer.drawQuad(quad, Pass.BLENDED);
|
||||
for (ObjectMesh.Quad quad : farthestFirst(mesh.translucentQuads())) {
|
||||
drawQuad(quad, Pass.BLENDED);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private List<ObjectMesh.Quad> farthestFirst(List<ObjectMesh.Quad> quads) {
|
||||
@@ -263,17 +284,21 @@ final class ObjectRenderer {
|
||||
private final double screenCenterX;
|
||||
private final double screenCenterY;
|
||||
|
||||
private Projection(ObjectMesh.Bounds bounds, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
|
||||
this.centerX = (bounds.minX() + bounds.maxX()) / 2D;
|
||||
this.centerY = (bounds.minY() + bounds.maxY()) / 2D;
|
||||
this.centerZ = (bounds.minZ() + bounds.maxZ()) / 2D;
|
||||
/**
|
||||
* @param shown the object drawn, centred across and standing where an object of {@code scaledAs} would
|
||||
* @param scaledAs the size that is fitted to the view
|
||||
*/
|
||||
private Projection(ObjectMesh.Bounds shown, ObjectMesh.Bounds scaledAs, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
|
||||
this.centerX = (shown.minX() + shown.maxX()) / 2D;
|
||||
this.centerY = shown.minY() + scaledAs.sizeY() / 2D;
|
||||
this.centerZ = (shown.minZ() + shown.maxZ()) / 2D;
|
||||
this.cosYaw = Math.cos(camera.yaw());
|
||||
this.sinYaw = Math.sin(camera.yaw());
|
||||
this.cosPitch = Math.cos(camera.pitch());
|
||||
this.sinPitch = Math.sin(camera.pitch());
|
||||
int visibleWidth = width - coveredLeftWidth;
|
||||
double fit = fitAtStartingAngle(bounds, visibleWidth / (double) height);
|
||||
this.pixelsPerBlock = camera.zoom() * fit * height / 2D / bounds.largestSize();
|
||||
double fit = fitAtStartingAngle(scaledAs, visibleWidth / (double) height);
|
||||
this.pixelsPerBlock = camera.zoom() * fit * height / 2D / scaledAs.largestSize();
|
||||
this.screenCenterX = coveredLeftWidth + visibleWidth / 2D + camera.panX() * visibleWidth / 2D;
|
||||
this.screenCenterY = height / 2D - camera.panY() * height / 2D;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
/**
|
||||
* Where an object explorer gets what it shows: the pack's object files, or the objects Iris generates from settings.
|
||||
@@ -35,4 +36,14 @@ interface ObjectSource {
|
||||
|
||||
/** 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);
|
||||
|
||||
/** The seed the pack bakes the entry's variants from; empty for anything not generated from a seed. */
|
||||
default OptionalLong seed(String key) {
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
|
||||
/** The entry's variants as {@code seed} would bake them; only asked of entries that have a seed. */
|
||||
default List<IrisObject> variants(IrisData data, String key, long seed) {
|
||||
throw new UnsupportedOperationException(key + " is not generated from a seed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* 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.AbstractAction;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.Scrollable;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
/**
|
||||
* Every variant of a procedural object side by side, drawn at one scale so their sizes compare. Clicking one opens it
|
||||
* in the 3D view.
|
||||
*/
|
||||
final class ObjectVariantGallery extends JPanel {
|
||||
private static final long serialVersionUID = 6153024487310558262L;
|
||||
private static final int THUMBNAIL_SIZE = 220;
|
||||
private static final int CARD_PADDING = 6;
|
||||
private static final int CARD_GAP = 12;
|
||||
private static final int EDGE_MARGIN = 16;
|
||||
// The variant bar floats over the bottom edge, so the last row scrolls clear of it.
|
||||
private static final int VARIANT_BAR_CLEARANCE = 64;
|
||||
|
||||
private final JLabel title = new JLabel(" ");
|
||||
private final CardGrid grid = new CardGrid();
|
||||
private final IntConsumer onVariantOpened;
|
||||
private final Runnable onThemeChanged;
|
||||
private final List<VariantCard> cards = new ArrayList<>();
|
||||
private int coveredLeftWidth;
|
||||
private int selectedIndex;
|
||||
|
||||
/**
|
||||
* @param onVariantOpened told the index of a variant the user clicks
|
||||
* @param onThemeChanged told when the theme changes, since thumbnails are drawn on the theme's background
|
||||
*/
|
||||
ObjectVariantGallery(IntConsumer onVariantOpened, Runnable onThemeChanged) {
|
||||
super(new BorderLayout(0, StudioTheme.scale(EDGE_MARGIN)));
|
||||
this.onVariantOpened = onVariantOpened;
|
||||
this.onThemeChanged = onThemeChanged;
|
||||
setOpaque(true);
|
||||
setBackground(StudioTheme.EDITOR_BACKGROUND);
|
||||
setFocusable(true);
|
||||
title.setFont(StudioTheme.sectionFont());
|
||||
title.setForeground(StudioTheme.TEXT);
|
||||
JScrollPane scroll = StudioToolbar.scroll(grid);
|
||||
scroll.setOpaque(false);
|
||||
scroll.getViewport().setOpaque(false);
|
||||
add(title, BorderLayout.NORTH);
|
||||
add(scroll, BorderLayout.CENTER);
|
||||
keepClearOfOverlays();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUI() {
|
||||
super.updateUI();
|
||||
if (onThemeChanged != null) {
|
||||
onThemeChanged.run();
|
||||
}
|
||||
}
|
||||
|
||||
void bindKey(int keyCode, String name, Runnable action) {
|
||||
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(keyCode, 0), name);
|
||||
getActionMap().put(name, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
action.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setCoveredLeftWidth(int width) {
|
||||
coveredLeftWidth = width;
|
||||
keepClearOfOverlays();
|
||||
revalidate();
|
||||
}
|
||||
|
||||
int thumbnailSize() {
|
||||
return StudioTheme.scale(THUMBNAIL_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty cards for {@code count} variants, waiting for their thumbnails.
|
||||
*/
|
||||
void showVariants(String heading, int count, int selected) {
|
||||
title.setText(heading);
|
||||
cards.clear();
|
||||
grid.removeAll();
|
||||
for (int index = 0; index < count; index++) {
|
||||
VariantCard card = new VariantCard(index);
|
||||
cards.add(card);
|
||||
grid.add(card);
|
||||
}
|
||||
select(selected);
|
||||
grid.revalidate();
|
||||
grid.repaint();
|
||||
}
|
||||
|
||||
void showThumbnail(int index, BufferedImage thumbnail) {
|
||||
if (index < cards.size()) {
|
||||
cards.get(index).showThumbnail(thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
void select(int index) {
|
||||
selectedIndex = index;
|
||||
cards.forEach(VariantCard::repaint);
|
||||
if (index < cards.size()) {
|
||||
VariantCard card = cards.get(index);
|
||||
grid.scrollRectToVisible(card.getBounds());
|
||||
}
|
||||
}
|
||||
|
||||
void showMessage(String text) {
|
||||
title.setText(text);
|
||||
cards.clear();
|
||||
grid.removeAll();
|
||||
grid.revalidate();
|
||||
grid.repaint();
|
||||
}
|
||||
|
||||
private void keepClearOfOverlays() {
|
||||
int margin = StudioTheme.scale(EDGE_MARGIN);
|
||||
setBorder(BorderFactory.createEmptyBorder(margin, coveredLeftWidth + margin, StudioTheme.scale(VARIANT_BAR_CLEARANCE), margin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cards in as many columns as fit the width, wrapping onto more rows.
|
||||
*/
|
||||
private final class CardGrid extends JPanel implements Scrollable {
|
||||
private static final long serialVersionUID = -3618705187215931408L;
|
||||
|
||||
CardGrid() {
|
||||
super(null);
|
||||
setOpaque(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doLayout() {
|
||||
Dimension card = cardSize();
|
||||
int gap = StudioTheme.scale(CARD_GAP);
|
||||
int columns = columns();
|
||||
for (int index = 0; index < getComponentCount(); index++) {
|
||||
int column = index % columns;
|
||||
int row = index / columns;
|
||||
getComponent(index).setBounds(column * (card.width + gap), row * (card.height + gap), card.width, card.height);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
Dimension card = cardSize();
|
||||
int gap = StudioTheme.scale(CARD_GAP);
|
||||
int rows = (getComponentCount() + columns() - 1) / columns();
|
||||
return new Dimension(card.width, Math.max(0, rows * (card.height + gap) - gap));
|
||||
}
|
||||
|
||||
private int columns() {
|
||||
int gap = StudioTheme.scale(CARD_GAP);
|
||||
return Math.max(1, (getWidth() + gap) / (cardSize().width + gap));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredScrollableViewportSize() {
|
||||
return getPreferredSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||
return cardSize().height / 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||
return visibleRect.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportWidth() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportHeight() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Dimension cardSize() {
|
||||
int padding = StudioTheme.scale(CARD_PADDING);
|
||||
int labelHeight = getFontMetrics(StudioTheme.smallFont()).getHeight();
|
||||
return new Dimension(thumbnailSize() + 2 * padding, thumbnailSize() + labelHeight + 3 * padding);
|
||||
}
|
||||
|
||||
private final class VariantCard extends JComponent {
|
||||
private static final long serialVersionUID = 2296135546207834180L;
|
||||
|
||||
private final int index;
|
||||
private BufferedImage thumbnail;
|
||||
private boolean hovered;
|
||||
|
||||
VariantCard(int index) {
|
||||
this.index = index;
|
||||
setToolTipText("Open variant " + (index + 1) + " in 3D");
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent event) {
|
||||
onVariantOpened.accept(VariantCard.this.index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent event) {
|
||||
setHovered(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent event) {
|
||||
setHovered(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showThumbnail(BufferedImage image) {
|
||||
thumbnail = image;
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void setHovered(boolean nowHovered) {
|
||||
hovered = nowHovered;
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics graphics) {
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
double arc = StudioTheme.scale(StudioTheme.OVERLAY_ARC);
|
||||
canvas.setColor(index == selectedIndex ? StudioTheme.SELECTION : hovered ? StudioTheme.HOVER_BACKGROUND : StudioTheme.PANEL_BACKGROUND);
|
||||
canvas.fill(new RoundRectangle2D.Double(0, 0, getWidth(), getHeight(), arc, arc));
|
||||
int padding = StudioTheme.scale(CARD_PADDING);
|
||||
int size = thumbnailSize();
|
||||
canvas.setFont(StudioTheme.smallFont());
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
if (thumbnail != null) {
|
||||
canvas.drawImage(thumbnail, padding, padding, size, size, null);
|
||||
} else {
|
||||
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||
drawCentered(canvas, "Loading...", padding + size / 2);
|
||||
}
|
||||
canvas.setColor(StudioTheme.TEXT);
|
||||
drawCentered(canvas, "Variant " + (index + 1), padding * 2 + size + metrics.getAscent());
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawCentered(Graphics2D canvas, String text, int baseline) {
|
||||
canvas.drawString(text, (getWidth() - canvas.getFontMetrics().stringWidth(text)) / 2, baseline);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@
|
||||
*/
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
@@ -27,39 +30,82 @@ import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.LongConsumer;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* A small bar floating at the bottom of the object view that steps through the variants of a procedural object and
|
||||
* the seeds they are baked from, and switches between one variant in 3D and all of them side by side. It hides itself
|
||||
* for anything with a single variant and no seed.
|
||||
*/
|
||||
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 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 = new JLabel(" ", SwingConstants.CENTER);
|
||||
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 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 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 onCompareToggled told when the user switches between one variant and all of them
|
||||
*/
|
||||
ObjectVariantPicker(IntConsumer onVariantChosen) {
|
||||
super(new BorderLayout());
|
||||
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));
|
||||
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);
|
||||
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));
|
||||
compareButton = StudioToolbar.button("", "", onCompareToggled);
|
||||
add(variantStepper);
|
||||
add(Box.createHorizontalStrut(StudioTheme.scale(SECTION_GAP)));
|
||||
add(seedStepper);
|
||||
add(packSeedButton);
|
||||
add(Box.createHorizontalStrut(StudioTheme.scale(SECTION_GAP)));
|
||||
add(compareButton);
|
||||
showComparing(false);
|
||||
showVariants(0, 0);
|
||||
}
|
||||
|
||||
private static JLabel label(int width) {
|
||||
JLabel label = new JLabel(" ", SwingConstants.CENTER);
|
||||
label.setFont(StudioTheme.smallFont());
|
||||
label.setForeground(StudioTheme.TEXT);
|
||||
label.setPreferredSize(new Dimension(StudioTheme.scale(width), label.getPreferredSize().height));
|
||||
return label;
|
||||
}
|
||||
|
||||
private static JComponent stepper(JLabel shown, JButton previous, JButton next) {
|
||||
JPanel stepper = new JPanel(new BorderLayout());
|
||||
stepper.setOpaque(false);
|
||||
stepper.add(previous, BorderLayout.WEST);
|
||||
stepper.add(shown, BorderLayout.CENTER);
|
||||
stepper.add(next, BorderLayout.EAST);
|
||||
return stepper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,19 +137,39 @@ final class ObjectVariantPicker extends JPanel {
|
||||
|
||||
void setCoveredLeftWidth(int width) {
|
||||
coveredLeftWidth = width;
|
||||
if (getParent() != null) {
|
||||
getParent().revalidate();
|
||||
}
|
||||
relayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts again at the first of {@code count} variants without telling anyone, since the first is what loads anyway.
|
||||
* Shows variant {@code index} of {@code count} without telling anyone, since the caller already shows it.
|
||||
*/
|
||||
void showVariants(int count) {
|
||||
void showVariants(int count, int index) {
|
||||
variantCount = count;
|
||||
shownIndex = 0;
|
||||
showLabel();
|
||||
setVisible(count > 1);
|
||||
shownIndex = index;
|
||||
shownVariant.setText("Variant " + (index + 1) + " of " + count);
|
||||
variantStepper.setVisible(count > 1);
|
||||
compareButton.setVisible(count > 1);
|
||||
showWhenUseful();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the seed the variants are baked from, with a way back to the pack's own seed once another is chosen.
|
||||
*/
|
||||
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));
|
||||
showWhenUseful();
|
||||
}
|
||||
|
||||
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);
|
||||
relayout();
|
||||
}
|
||||
|
||||
void stepBy(int step) {
|
||||
@@ -111,12 +177,24 @@ final class ObjectVariantPicker extends JPanel {
|
||||
return;
|
||||
}
|
||||
shownIndex = Math.floorMod(shownIndex + step, variantCount);
|
||||
showLabel();
|
||||
showVariants(variantCount, shownIndex);
|
||||
onVariantChosen.accept(shownIndex);
|
||||
}
|
||||
|
||||
private void showLabel() {
|
||||
shownVariant.setText("Variant " + (shownIndex + 1) + " of " + variantCount);
|
||||
private void stepSeedBy(int step) {
|
||||
seed.ifPresent(current -> onSeedChosen.accept(current + step));
|
||||
}
|
||||
|
||||
private void showWhenUseful() {
|
||||
setVisible(variantCount > 1 || seed.isPresent());
|
||||
relayout();
|
||||
}
|
||||
|
||||
private void relayout() {
|
||||
if (getParent() != null) {
|
||||
getParent().revalidate();
|
||||
getParent().repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
/**
|
||||
* The objects the pack's procedural settings generate, one entry per definition, in the folders the pack keeps them in.
|
||||
@@ -63,4 +64,20 @@ final class ProceduralObjectSource implements ObjectSource {
|
||||
ResolvedProceduralPlacement definition = definitionsByKey.get(key);
|
||||
return definition == null ? List.of() : definition.variants();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong seed(String key) {
|
||||
ResolvedProceduralPlacement definition = definitionsByKey.get(key);
|
||||
return definition == null ? OptionalLong.empty() : OptionalLong.of(definition.seed());
|
||||
}
|
||||
|
||||
// The pack's own seed reuses the variants world generation bakes.
|
||||
@Override
|
||||
public List<IrisObject> variants(IrisData data, String key, long seed) {
|
||||
ResolvedProceduralPlacement definition = definitionsByKey.get(key);
|
||||
if (definition == null) {
|
||||
return List.of();
|
||||
}
|
||||
return definition.seed() == seed ? definition.variants() : data.getProceduralObjects().reseeded(definition, seed).variants();
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -18,6 +18,7 @@
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
@@ -73,6 +74,7 @@ public final class ProceduralObjectCatalog {
|
||||
|
||||
private final IrisData data;
|
||||
private final KMap<String, ResolvedProceduralPlacement> byDefinition = new KMap<>();
|
||||
private final KMap<ResolvedProceduralPlacement, Object> definitionByPlacement = new KMap<>();
|
||||
|
||||
public ProceduralObjectCatalog(IrisData data) {
|
||||
this.data = data;
|
||||
@@ -89,7 +91,25 @@ public final class ProceduralObjectCatalog {
|
||||
|
||||
private ResolvedProceduralPlacement placementOf(Object definition) {
|
||||
String identity = definition.getClass().getName() + DEFINITION_JSON.toJson(definition);
|
||||
return byDefinition.computeIfAbsent(identity, ignored -> resolvedFor(definition));
|
||||
return byDefinition.computeIfAbsent(identity, ignored -> {
|
||||
ResolvedProceduralPlacement placement = resolvedFor(definition);
|
||||
definitionByPlacement.put(placement, definition);
|
||||
return placement;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The same definition as {@code placement}, baked from {@code seed} instead of its own, to compare seeds before
|
||||
* choosing one. It is not kept, since a viewer may step through many seeds.
|
||||
*/
|
||||
public ResolvedProceduralPlacement reseeded(ResolvedProceduralPlacement placement, long seed) {
|
||||
Object definition = definitionByPlacement.get(placement);
|
||||
if (definition == null) {
|
||||
throw new IllegalArgumentException("The pack was reloaded since " + placement.name() + " was read");
|
||||
}
|
||||
JsonObject reseededJson = DEFINITION_JSON.toJsonTree(definition).getAsJsonObject();
|
||||
reseededJson.addProperty("seed", seed);
|
||||
return resolvedFor(data.getGson().fromJson(reseededJson, definition.getClass()));
|
||||
}
|
||||
|
||||
private ResolvedProceduralPlacement resolvedFor(Object definition) {
|
||||
|
||||
+2
-1
@@ -160,7 +160,8 @@ public abstract class ResolvedProceduralPlacement {
|
||||
return new RNG(shape.seed() + (index * VARIANT_SEED_STRIDE));
|
||||
}
|
||||
|
||||
protected final long seed() {
|
||||
/** The seed every variant is baked from. */
|
||||
public final long seed() {
|
||||
return shape.seed();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user