mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Show the pick path in the hover card
Biome title, pick tree with noise bands.
This commit is contained in:
1 parent
97612385e5
commit
86ef09e768
7 files changed
+522
-225
No files matched your search
@@ -1103,7 +1103,7 @@ final class MapCanvas extends JPanel {
|
|||||||
double worldZ = screenToWorldZ(point.y);
|
double worldZ = screenToWorldZ(point.y);
|
||||||
controller.submitProbe(
|
controller.submitProbe(
|
||||||
frame,
|
frame,
|
||||||
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), currentType.drawsPickNoise()),
|
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), hoverExplainsBiomePick()),
|
||||||
details -> {
|
details -> {
|
||||||
QuickHover quick = quickHover;
|
QuickHover quick = quickHover;
|
||||||
if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) {
|
if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) {
|
||||||
@@ -1114,6 +1114,11 @@ final class MapCanvas extends JPanel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These views' hover card names the surface biome, so the whole way to it is worth looking up.
|
||||||
|
private boolean hoverExplainsBiomePick() {
|
||||||
|
return currentType.drawsPickNoise() || currentType == RenderType.HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
private String previewedRegion() {
|
private String previewedRegion() {
|
||||||
return currentType == RenderType.REGION_PREVIEW ? previewRegionKey : null;
|
return currentType == RenderType.REGION_PREVIEW ? previewRegionKey : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,258 +18,466 @@
|
|||||||
|
|
||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.MapHoverProbe.PickStep;
|
||||||
|
import com.volmit.iris.core.gui.NoisePickSpread.Band;
|
||||||
import com.volmit.iris.core.gui.components.MapLabels;
|
import com.volmit.iris.core.gui.components.MapLabels;
|
||||||
import com.volmit.iris.core.gui.components.RenderType;
|
import com.volmit.iris.core.gui.components.RenderType;
|
||||||
import com.volmit.iris.core.loader.IrisRegistrant;
|
import com.volmit.iris.core.loader.IrisRegistrant;
|
||||||
import com.volmit.iris.engine.HybridBiomeSelector;
|
|
||||||
import com.volmit.iris.engine.object.IrisBiome;
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
import java.awt.FontMetrics;
|
import java.awt.FontMetrics;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.Shape;
|
||||||
|
import java.awt.geom.RoundRectangle2D;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The card that follows the cursor: what the drawn map shows there at once, and the rest once probed, in titled
|
* The card that follows the cursor, laid out like IntelliJ's documentation popup: the biome as the title, the steps
|
||||||
* sections of labelled values.
|
* that picked it as a small tree, and with Shift the terrain, location and pack files below.
|
||||||
*/
|
*/
|
||||||
record MapHoverCard(String title, String subtitle, List<Section> sections, String footer) {
|
record MapHoverCard(Header header, String subtitle, String pathTitle, List<PickStep> path, boolean showsBands,
|
||||||
|
List<Group> groups, List<Hint> hints) {
|
||||||
|
private static final int MINIMUM_CONTENT_WIDTH = 260;
|
||||||
private static final int PADDING = 12;
|
private static final int PADDING = 12;
|
||||||
private static final int SUBTITLE_GAP = 2;
|
private static final int HEADER_TOP = 10;
|
||||||
private static final int SECTION_GAP = 10;
|
private static final int HEADER_ICON_SIZE = 14;
|
||||||
private static final int SECTION_TITLE_GAP = 4;
|
private static final int NODE_ICON_SIZE = 12;
|
||||||
|
private static final int ICON_GAP = 8;
|
||||||
|
private static final int KIND_GAP = 16;
|
||||||
|
private static final int GROUP_GAP = 8;
|
||||||
|
private static final int GROUP_TITLE_GAP = 4;
|
||||||
|
private static final int NODE_HEIGHT = 24;
|
||||||
|
private static final int NODE_INDENT = 18;
|
||||||
|
private static final int CHIP_GAP = 16;
|
||||||
|
private static final int CHIP_PADDING_X = 5;
|
||||||
|
private static final int CHIP_PADDING_Y = 1;
|
||||||
|
private static final int BAND_HEIGHT = 4;
|
||||||
|
private static final int BAND_GAP = 6;
|
||||||
|
private static final int MARKER_WIDTH = 2;
|
||||||
|
private static final int MARKER_HEIGHT = 10;
|
||||||
private static final int COLUMN_GAP = 14;
|
private static final int COLUMN_GAP = 14;
|
||||||
private static final int ROW_GAP = 2;
|
private static final int ROW_GAP = 3;
|
||||||
|
private static final int BOTTOM_PADDING = 10;
|
||||||
|
private static final int FOOTER_PADDING_Y = 7;
|
||||||
|
private static final int KEY_PADDING_X = 4;
|
||||||
|
private static final int KEY_GAP = 4;
|
||||||
|
private static final int HINT_GAP = 14;
|
||||||
|
private static final Color ICON_EDGE = new ThemeColor(new Color(0, 0, 0, 38), new Color(255, 255, 255, 38));
|
||||||
|
private static final Color MARKER_EDGE = new Color(0, 0, 0, 200);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing
|
* @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing
|
||||||
|
* @param region the region the drawn tile shows
|
||||||
* @param details the probed facts about the spot, or null until they arrive
|
* @param details the probed facts about the spot, or null until they arrive
|
||||||
* @param detailed whether Shift is held, which adds the pack files
|
* @param detailed whether Shift is held, which trades the noise bands for terrain, location and pack files
|
||||||
*/
|
*/
|
||||||
static MapHoverCard of(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details,
|
static MapHoverCard of(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details,
|
||||||
RenderType type, boolean detailed, double worldX, double worldZ) {
|
RenderType type, boolean detailed, double worldX, double worldZ) {
|
||||||
List<Section> sections = new ArrayList<>();
|
List<PickStep> path = details == null ? List.of() : details.pickPath();
|
||||||
if (details != null) {
|
IrisBiome biome = cardBiome(subject, details, type);
|
||||||
sections.add(terrainSection(details, type));
|
List<Group> groups = new ArrayList<>();
|
||||||
addPickSection(sections, details);
|
|
||||||
}
|
|
||||||
sections.add(locationSection(worldX, worldZ, detailed));
|
|
||||||
if (detailed) {
|
if (detailed) {
|
||||||
addFilesSection(sections, subject, region);
|
addTerrainGroup(groups, details, type);
|
||||||
|
groups.add(locationGroup(worldX, worldZ));
|
||||||
|
addFilesGroup(groups, biome, details != null ? details.region() : region == null ? null : region.subject());
|
||||||
}
|
}
|
||||||
return new MapHoverCard(title(subject, details), subtitle(region, details, type), sections,
|
return new MapHoverCard(header(subject, biome, type, path), subtitle(details), pathTitle(path), path, !detailed,
|
||||||
detailed ? null : "Shift for pack files · H highlights");
|
groups, hints(type, detailed));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the card with its top left corner as close to the given point as fits inside the bounds.
|
* Draws the card with its top left corner as close to the given point as fits inside the bounds.
|
||||||
*/
|
*/
|
||||||
void paint(Graphics2D canvas, int anchorX, int anchorY, int boundsWidth, int boundsHeight) {
|
void paint(Graphics2D canvas, int anchorX, int anchorY, int boundsWidth, int boundsHeight) {
|
||||||
|
Graphics2D card = (Graphics2D) canvas.create();
|
||||||
|
card.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
card.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
int padding = StudioTheme.scale(PADDING);
|
int padding = StudioTheme.scale(PADDING);
|
||||||
int labelWidth = labelColumnWidth(canvas);
|
int contentWidth = contentWidth(card);
|
||||||
int width = Math.max(headerWidth(canvas), labelWidth + StudioTheme.scale(COLUMN_GAP) + valueColumnWidth(canvas)) + padding * 2;
|
int width = contentWidth + padding * 2;
|
||||||
int height = contentHeight(canvas) + padding * 2;
|
int footerHeight = footerHeight(card);
|
||||||
|
int height = StudioTheme.scale(HEADER_TOP) + headerHeight(card) + pathHeight(card) + groupsHeight(card)
|
||||||
|
+ StudioTheme.scale(BOTTOM_PADDING) + footerHeight;
|
||||||
int x = Math.max(padding, Math.min(boundsWidth - width - padding, anchorX));
|
int x = Math.max(padding, Math.min(boundsWidth - width - padding, anchorX));
|
||||||
int y = Math.max(padding, Math.min(boundsHeight - height - padding, anchorY));
|
int y = Math.max(padding, Math.min(boundsHeight - height - padding, anchorY));
|
||||||
StudioOverlay.drawCard(canvas, x, y, width, height);
|
StudioOverlay.drawCard(card, x, y, width, height, footerHeight);
|
||||||
|
|
||||||
int left = x + padding;
|
int left = x + padding;
|
||||||
int top = y + padding;
|
int top = y + StudioTheme.scale(HEADER_TOP);
|
||||||
top = drawLine(canvas, title, StudioTheme.boldFont(), StudioTheme.TEXT, left, top);
|
top = drawHeader(card, left, top, contentWidth);
|
||||||
if (subtitle != null) {
|
top = drawPath(card, left, top, contentWidth);
|
||||||
top = drawLine(canvas, subtitle, StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT, left,
|
drawGroups(card, left, top);
|
||||||
top + StudioTheme.scale(SUBTITLE_GAP));
|
drawFooter(card, left, y + height - footerHeight);
|
||||||
}
|
card.dispose();
|
||||||
for (Section section : sections) {
|
|
||||||
top += StudioTheme.scale(SECTION_GAP);
|
|
||||||
canvas.setColor(StudioTheme.OVERLAY_BORDER);
|
|
||||||
canvas.fillRect(left, top, width - padding * 2, 1);
|
|
||||||
top += StudioTheme.scale(SECTION_GAP) / 2;
|
|
||||||
top = drawLine(canvas, section.title().toUpperCase(Locale.ROOT), StudioTheme.sectionFont(), StudioTheme.MUTED_TEXT,
|
|
||||||
left, top) + StudioTheme.scale(SECTION_TITLE_GAP);
|
|
||||||
for (Row row : section.rows()) {
|
|
||||||
drawLine(canvas, row.label(), StudioTheme.font(), StudioTheme.SECONDARY_TEXT, left, top);
|
|
||||||
top = drawLine(canvas, row.value(), StudioTheme.font(), StudioTheme.TEXT,
|
|
||||||
left + labelWidth + StudioTheme.scale(COLUMN_GAP), top) + StudioTheme.scale(ROW_GAP);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (footer != null) {
|
|
||||||
drawLine(canvas, footer, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, top + StudioTheme.scale(SECTION_GAP));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int labelColumnWidth(Graphics2D canvas) {
|
private int contentWidth(Graphics2D canvas) {
|
||||||
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
|
int width = Math.max(headerWidth(canvas), pathWidth(canvas));
|
||||||
return sections.stream().flatMap(section -> section.rows().stream())
|
width = Math.max(width, groupsWidth(canvas));
|
||||||
.mapToInt(row -> metrics.stringWidth(row.label())).max().orElse(0);
|
width = Math.max(width, footerWidth(canvas));
|
||||||
|
return Math.max(StudioTheme.scale(MINIMUM_CONTENT_WIDTH), width);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int valueColumnWidth(Graphics2D canvas) {
|
// Header: the biome's colour, its name in bold with its kind on the right, and the ground's height below.
|
||||||
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
|
private int headerTextOffset() {
|
||||||
return sections.stream().flatMap(section -> section.rows().stream())
|
return header.color() == null ? 0 : StudioTheme.scale(HEADER_ICON_SIZE + ICON_GAP);
|
||||||
.mapToInt(row -> metrics.stringWidth(row.value())).max().orElse(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int headerWidth(Graphics2D canvas) {
|
private int headerWidth(Graphics2D canvas) {
|
||||||
int width = canvas.getFontMetrics(StudioTheme.boldFont()).stringWidth(title);
|
int width = headerTextOffset() + canvas.getFontMetrics(StudioTheme.boldFont()).stringWidth(header.name());
|
||||||
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
if (header.kind() != null) {
|
||||||
if (subtitle != null) {
|
width += StudioTheme.scale(KIND_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(header.kind());
|
||||||
width = Math.max(width, small.stringWidth(subtitle));
|
|
||||||
}
|
}
|
||||||
if (footer != null) {
|
if (subtitle != null) {
|
||||||
width = Math.max(width, small.stringWidth(footer));
|
width = Math.max(width, headerTextOffset() + canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(subtitle));
|
||||||
}
|
}
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int contentHeight(Graphics2D canvas) {
|
private int headerHeight(Graphics2D canvas) {
|
||||||
int small = canvas.getFontMetrics(StudioTheme.smallFont()).getHeight();
|
|
||||||
int row = canvas.getFontMetrics(StudioTheme.font()).getHeight() + StudioTheme.scale(ROW_GAP);
|
|
||||||
int sectionTitle = canvas.getFontMetrics(StudioTheme.sectionFont()).getHeight() + StudioTheme.scale(SECTION_TITLE_GAP);
|
|
||||||
int height = canvas.getFontMetrics(StudioTheme.boldFont()).getHeight();
|
int height = canvas.getFontMetrics(StudioTheme.boldFont()).getHeight();
|
||||||
if (subtitle != null) {
|
if (subtitle != null) {
|
||||||
height += StudioTheme.scale(SUBTITLE_GAP) + small;
|
height += canvas.getFontMetrics(StudioTheme.smallFont()).getHeight();
|
||||||
}
|
|
||||||
for (Section section : sections) {
|
|
||||||
height += StudioTheme.scale(SECTION_GAP) + StudioTheme.scale(SECTION_GAP) / 2 + sectionTitle + section.rows().size() * row;
|
|
||||||
}
|
|
||||||
if (footer != null) {
|
|
||||||
height += StudioTheme.scale(SECTION_GAP) + small;
|
|
||||||
}
|
}
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int drawLine(Graphics2D canvas, String text, Font font, Color color, int left, int top) {
|
private int drawHeader(Graphics2D canvas, int left, int top, int contentWidth) {
|
||||||
canvas.setFont(font);
|
FontMetrics bold = canvas.getFontMetrics(StudioTheme.boldFont());
|
||||||
canvas.setColor(color);
|
if (header.color() != null) {
|
||||||
FontMetrics metrics = canvas.getFontMetrics();
|
int iconSize = StudioTheme.scale(HEADER_ICON_SIZE);
|
||||||
canvas.drawString(text, left, top + metrics.getAscent());
|
drawIcon(canvas, header.color(), left, top + (bold.getHeight() - iconSize) / 2, iconSize);
|
||||||
return top + metrics.getHeight();
|
}
|
||||||
|
int textLeft = left + headerTextOffset();
|
||||||
|
int baseline = top + bold.getAscent();
|
||||||
|
drawText(canvas, header.name(), StudioTheme.boldFont(), StudioTheme.TEXT, textLeft, baseline);
|
||||||
|
if (header.kind() != null) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
drawText(canvas, header.kind(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT,
|
||||||
|
left + contentWidth - small.stringWidth(header.kind()), baseline);
|
||||||
|
}
|
||||||
|
top += bold.getHeight();
|
||||||
|
if (subtitle != null) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
drawText(canvas, subtitle, StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT, textLeft, top + small.getAscent());
|
||||||
|
top += small.getHeight();
|
||||||
|
}
|
||||||
|
return top;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String title(MapLabels.Label subject, MapHoverProbe.Details details) {
|
// Path: one indented row per step, its value as a chip on the right and, without Shift, the noise bands below it.
|
||||||
if (subject != null) {
|
private int pathWidth(Graphics2D canvas) {
|
||||||
return subject.name();
|
int width = 0;
|
||||||
|
for (int level = 0; level < path.size(); level++) {
|
||||||
|
PickStep step = path.get(level);
|
||||||
|
int row = nodeTextLeft(level) + canvas.getFontMetrics(StudioTheme.font()).stringWidth(step.picked().name() + " ")
|
||||||
|
+ canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(step.reason());
|
||||||
|
if (step.value() != null) {
|
||||||
|
row += StudioTheme.scale(CHIP_GAP) + chipWidth(canvas, step.value());
|
||||||
|
}
|
||||||
|
width = Math.max(width, row);
|
||||||
}
|
}
|
||||||
if (details != null && details.surfaceBiome() != null) {
|
return width;
|
||||||
return details.surfaceBiome().getName();
|
|
||||||
}
|
|
||||||
return "Drawing…";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The region view's title already is the region, so only the kind of ground is left to say.
|
private int pathHeight(Graphics2D canvas) {
|
||||||
private static String subtitle(MapLabels.Label region, MapHoverProbe.Details details, RenderType type) {
|
if (path.isEmpty()) {
|
||||||
List<String> parts = new ArrayList<>();
|
return 0;
|
||||||
if (type != RenderType.REGION) {
|
}
|
||||||
String regionName = region != null ? region.name() : details != null ? details.region().getName() : null;
|
int height = groupTitleHeight(canvas);
|
||||||
if (regionName != null) {
|
for (PickStep step : path) {
|
||||||
parts.add(regionName + " region");
|
height += nodeHeight(step);
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int nodeHeight(PickStep step) {
|
||||||
|
int height = StudioTheme.scale(NODE_HEIGHT);
|
||||||
|
return showsBands && step.showsBands() ? height + StudioTheme.scale(BAND_HEIGHT + BAND_GAP) : height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int nodeTextLeft(int level) {
|
||||||
|
return StudioTheme.scale(level * NODE_INDENT + NODE_ICON_SIZE + ICON_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int drawPath(Graphics2D canvas, int left, int top, int contentWidth) {
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
top = drawGroupTitle(canvas, pathTitle, left, top);
|
||||||
|
for (int level = 0; level < path.size(); level++) {
|
||||||
|
PickStep step = path.get(level);
|
||||||
|
drawNode(canvas, step, left, top, level, contentWidth);
|
||||||
|
if (showsBands && step.showsBands()) {
|
||||||
|
int bandsLeft = left + nodeTextLeft(level);
|
||||||
|
drawBands(canvas, step, bandsLeft, top + StudioTheme.scale(NODE_HEIGHT), left + contentWidth - bandsLeft);
|
||||||
|
}
|
||||||
|
top += nodeHeight(step);
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawNode(Graphics2D canvas, PickStep step, int left, int top, int level, int contentWidth) {
|
||||||
|
int rowHeight = StudioTheme.scale(NODE_HEIGHT);
|
||||||
|
int iconSize = StudioTheme.scale(NODE_ICON_SIZE);
|
||||||
|
drawIcon(canvas, step.picked().color(), left + StudioTheme.scale(level * NODE_INDENT), top + (rowHeight - iconSize) / 2,
|
||||||
|
iconSize);
|
||||||
|
FontMetrics regular = canvas.getFontMetrics(StudioTheme.font());
|
||||||
|
int baseline = top + (rowHeight - regular.getHeight()) / 2 + regular.getAscent();
|
||||||
|
int textLeft = left + nodeTextLeft(level);
|
||||||
|
String name = step.picked().name() + " ";
|
||||||
|
drawText(canvas, name, StudioTheme.font(), StudioTheme.TEXT, textLeft, baseline);
|
||||||
|
drawText(canvas, step.reason(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, textLeft + regular.stringWidth(name),
|
||||||
|
baseline);
|
||||||
|
if (step.value() != null) {
|
||||||
|
drawChip(canvas, step.value(), left + contentWidth - chipWidth(canvas, step.value()), top, rowHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A strip with each option's band of noise in its colour, and a marker where this spot's noise falls.
|
||||||
|
private static void drawBands(Graphics2D canvas, PickStep step, int left, int top, int width) {
|
||||||
|
int height = StudioTheme.scale(BAND_HEIGHT);
|
||||||
|
Shape clip = canvas.getClip();
|
||||||
|
canvas.clip(new RoundRectangle2D.Double(left, top, width, height, height, height));
|
||||||
|
for (Band band : step.bands()) {
|
||||||
|
int from = left + (int) Math.round(band.fromNoise() * width);
|
||||||
|
int to = left + (int) Math.round(band.toNoise() * width);
|
||||||
|
canvas.setColor(new Color(band.option().color()));
|
||||||
|
canvas.fillRect(from, top, Math.max(1, to - from), height);
|
||||||
|
}
|
||||||
|
canvas.setClip(clip);
|
||||||
|
int markerWidth = StudioTheme.scale(MARKER_WIDTH);
|
||||||
|
int markerHeight = StudioTheme.scale(MARKER_HEIGHT);
|
||||||
|
int markerX = left + (int) Math.round(Math.clamp(step.noise(), 0D, 1D) * (width - markerWidth));
|
||||||
|
int markerY = top + (height - markerHeight) / 2;
|
||||||
|
canvas.setColor(MARKER_EDGE);
|
||||||
|
canvas.fillRect(markerX - 1, markerY - 1, markerWidth + 2, markerHeight + 2);
|
||||||
|
canvas.setColor(Color.WHITE);
|
||||||
|
canvas.fillRect(markerX, markerY, markerWidth, markerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Font chipFont() {
|
||||||
|
return StudioTheme.monospacedFont().deriveFont(StudioTheme.smallFont().getSize2D());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int chipWidth(Graphics2D canvas, String value) {
|
||||||
|
return canvas.getFontMetrics(chipFont()).stringWidth(value) + StudioTheme.scale(CHIP_PADDING_X) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styled like an inlay hint in IntelliJ's editor.
|
||||||
|
private static void drawChip(Graphics2D canvas, String value, int left, int rowTop, int rowHeight) {
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics(chipFont());
|
||||||
|
int height = metrics.getHeight() + StudioTheme.scale(CHIP_PADDING_Y) * 2;
|
||||||
|
int top = rowTop + (rowHeight - height) / 2;
|
||||||
|
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
|
||||||
|
int arc = StudioTheme.scale(8);
|
||||||
|
canvas.fillRoundRect(left, top, chipWidth(canvas, value), height, arc, arc);
|
||||||
|
drawText(canvas, value, chipFont(), StudioTheme.SECONDARY_TEXT, left + StudioTheme.scale(CHIP_PADDING_X),
|
||||||
|
top + StudioTheme.scale(CHIP_PADDING_Y) + metrics.getAscent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawIcon(Graphics2D canvas, int rgb, int left, int top, int size) {
|
||||||
|
int arc = Math.max(2, size / 2);
|
||||||
|
canvas.setColor(new Color(rgb));
|
||||||
|
canvas.fillRoundRect(left, top, size, size, arc, arc);
|
||||||
|
canvas.setColor(ICON_EDGE);
|
||||||
|
canvas.drawRoundRect(left, top, size - 1, size - 1, arc, arc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups: sentence-case titles over rows of muted labels and values, all labels in one column.
|
||||||
|
private int labelColumnWidth(Graphics2D canvas) {
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
|
||||||
|
return groups.stream().flatMap(group -> group.rows().stream())
|
||||||
|
.mapToInt(row -> metrics.stringWidth(row.label())).max().orElse(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int groupsWidth(Graphics2D canvas) {
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
|
||||||
|
int valueWidth = groups.stream().flatMap(group -> group.rows().stream())
|
||||||
|
.mapToInt(row -> metrics.stringWidth(row.value())).max().orElse(0);
|
||||||
|
return labelColumnWidth(canvas) + StudioTheme.scale(COLUMN_GAP) + valueWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int groupsHeight(Graphics2D canvas) {
|
||||||
|
int rowHeight = canvas.getFontMetrics(StudioTheme.font()).getHeight() + StudioTheme.scale(ROW_GAP);
|
||||||
|
return groups.stream().mapToInt(group -> groupTitleHeight(canvas) + group.rows().size() * rowHeight).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawGroups(Graphics2D canvas, int left, int top) {
|
||||||
|
FontMetrics regular = canvas.getFontMetrics(StudioTheme.font());
|
||||||
|
int valueLeft = left + labelColumnWidth(canvas) + StudioTheme.scale(COLUMN_GAP);
|
||||||
|
for (Group group : groups) {
|
||||||
|
top = drawGroupTitle(canvas, group.title(), left, top);
|
||||||
|
for (Row row : group.rows()) {
|
||||||
|
int baseline = top + regular.getAscent();
|
||||||
|
drawText(canvas, row.label(), StudioTheme.font(), StudioTheme.SECONDARY_TEXT, left, baseline);
|
||||||
|
drawText(canvas, row.value(), StudioTheme.font(), row.link() ? StudioTheme.LINK : StudioTheme.TEXT, valueLeft,
|
||||||
|
baseline);
|
||||||
|
top += regular.getHeight() + StudioTheme.scale(ROW_GAP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (details != null && details.terrainKind() != null) {
|
|
||||||
parts.add(details.terrainKind());
|
|
||||||
}
|
|
||||||
return parts.isEmpty() ? null : String.join(" · ", parts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Section terrainSection(MapHoverProbe.Details details, RenderType type) {
|
private static int groupTitleHeight(Graphics2D canvas) {
|
||||||
|
return StudioTheme.scale(GROUP_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).getHeight()
|
||||||
|
+ StudioTheme.scale(GROUP_TITLE_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int drawGroupTitle(Graphics2D canvas, String title, int left, int top) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
top += StudioTheme.scale(GROUP_GAP);
|
||||||
|
drawText(canvas, title, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, top + small.getAscent());
|
||||||
|
return top + small.getHeight() + StudioTheme.scale(GROUP_TITLE_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer: the shortcuts as key caps, each followed by what it does.
|
||||||
|
private int footerWidth(Graphics2D canvas) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
int width = 0;
|
||||||
|
for (Hint hint : hints) {
|
||||||
|
width += keyWidth(small, hint.key()) + StudioTheme.scale(KEY_GAP) + small.stringWidth(hint.action());
|
||||||
|
}
|
||||||
|
return width + StudioTheme.scale(HINT_GAP) * Math.max(0, hints.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int footerHeight(Graphics2D canvas) {
|
||||||
|
return canvas.getFontMetrics(StudioTheme.smallFont()).getHeight() + StudioTheme.scale(FOOTER_PADDING_Y) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int keyWidth(FontMetrics small, String key) {
|
||||||
|
return small.stringWidth(key) + StudioTheme.scale(KEY_PADDING_X) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawFooter(Graphics2D canvas, int left, int top) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
int textTop = top + StudioTheme.scale(FOOTER_PADDING_Y);
|
||||||
|
int baseline = textTop + small.getAscent();
|
||||||
|
int arc = StudioTheme.scale(6);
|
||||||
|
for (Hint hint : hints) {
|
||||||
|
int keyWidth = keyWidth(small, hint.key());
|
||||||
|
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
|
||||||
|
canvas.fillRoundRect(left, textTop, keyWidth, small.getHeight(), arc, arc);
|
||||||
|
drawText(canvas, hint.key(), StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT,
|
||||||
|
left + StudioTheme.scale(KEY_PADDING_X), baseline);
|
||||||
|
left += keyWidth + StudioTheme.scale(KEY_GAP);
|
||||||
|
drawText(canvas, hint.action(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, baseline);
|
||||||
|
left += small.stringWidth(hint.action()) + StudioTheme.scale(HINT_GAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawText(Graphics2D canvas, String text, Font font, Color color, int left, int baseline) {
|
||||||
|
canvas.setFont(font);
|
||||||
|
canvas.setColor(color);
|
||||||
|
canvas.drawString(text, left, baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The card is about a biome; the region view's tiles only know the region until the probe finds the biome.
|
||||||
|
private static IrisBiome cardBiome(MapLabels.Label subject, MapHoverProbe.Details details, RenderType type) {
|
||||||
|
if (subject != null && subject.subject() instanceof IrisBiome biome) {
|
||||||
|
return biome;
|
||||||
|
}
|
||||||
|
if (details == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return type == RenderType.CAVE_LAND ? details.caveBiome() : details.surfaceBiome();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Header header(MapLabels.Label subject, IrisBiome biome, RenderType type, List<PickStep> path) {
|
||||||
|
if (biome != null) {
|
||||||
|
return new Header(biome.getName(), colorOf(biome, path), type == RenderType.CAVE_LAND ? "Cave biome" : "Biome");
|
||||||
|
}
|
||||||
|
if (subject != null) {
|
||||||
|
return new Header(subject.name(), null, subject.kind() == MapLabels.Kind.REGION ? "Region" : "Biome");
|
||||||
|
}
|
||||||
|
return new Header("Drawing…", null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer colorOf(IrisBiome biome, List<PickStep> path) {
|
||||||
|
return path.stream()
|
||||||
|
.filter(step -> step.picked().key().equals(biome.getLoadKey()))
|
||||||
|
.reduce((first, last) -> last)
|
||||||
|
.map(step -> step.picked().color())
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String subtitle(MapHoverProbe.Details details) {
|
||||||
|
if (details == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.round(details.height()) + " · " + heightAboveSea(details.height() - details.fluidHeight()) + " · slope "
|
||||||
|
+ Math.round(details.slopeDegrees()) + "°";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String pathTitle(List<PickStep> path) {
|
||||||
|
return path.size() > 1 ? "Picked by" : "Region";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addTerrainGroup(List<Group> groups, MapHoverProbe.Details details, RenderType type) {
|
||||||
|
if (details == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<Row> rows = new ArrayList<>();
|
List<Row> rows = new ArrayList<>();
|
||||||
long height = Math.round(details.height());
|
|
||||||
rows.add(new Row("Height", height + " · " + heightAboveSea(details.height() - details.fluidHeight())));
|
|
||||||
rows.add(new Row("Slope", Math.round(details.slopeDegrees()) + "°"));
|
|
||||||
IrisBiome surface = details.surfaceBiome();
|
IrisBiome surface = details.surfaceBiome();
|
||||||
IrisBiome cave = details.caveBiome();
|
IrisBiome cave = details.caveBiome();
|
||||||
if (type == RenderType.CAVE_LAND && surface != null) {
|
if (type == RenderType.CAVE_LAND && surface != null) {
|
||||||
rows.add(new Row("Surface biome", surface.getName()));
|
rows.add(new Row("Surface biome", surface.getName(), false));
|
||||||
} else if (type != RenderType.CAVE_LAND && cave != null && cave != surface) {
|
} else if (type != RenderType.CAVE_LAND && cave != null && cave != surface) {
|
||||||
rows.add(new Row("Cave biome", cave.getName()));
|
rows.add(new Row("Cave biome", cave.getName(), false));
|
||||||
}
|
}
|
||||||
if (details.vanillaBiome() != null) {
|
if (details.vanillaBiome() != null) {
|
||||||
rows.add(new Row("Minecraft", details.vanillaBiome().replace('_', ' ')));
|
rows.add(new Row("Minecraft", details.vanillaBiome().replace('_', ' '), false));
|
||||||
}
|
|
||||||
return new Section("Terrain", rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addPickSection(List<Section> sections, MapHoverProbe.Details details) {
|
|
||||||
List<Row> rows = new ArrayList<>();
|
|
||||||
MapHoverProbe.PickNoise noise = details.pickNoise();
|
|
||||||
if (noise != null && noise.regionNoise() != null) {
|
|
||||||
rows.add(new Row("Region noise", noiseText(noise.regionNoise())));
|
|
||||||
}
|
|
||||||
if (noise != null && noise.biomeType() != null) {
|
|
||||||
String label = capitalize(noise.biomeType().name().toLowerCase(Locale.ROOT)) + " noise";
|
|
||||||
rows.add(new Row(label, biomeNoiseText(noise, details.surfaceBiome())));
|
|
||||||
}
|
|
||||||
if (details.placement() != null) {
|
|
||||||
rows.add(new Row("Height", placementText(details.placement())));
|
|
||||||
}
|
}
|
||||||
if (!rows.isEmpty()) {
|
if (!rows.isEmpty()) {
|
||||||
sections.add(new Section("Picked by", rows));
|
groups.add(new Group("Terrain", rows));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String biomeNoiseText(MapHoverProbe.PickNoise noise, IrisBiome surface) {
|
private static Group locationGroup(double worldX, double worldZ) {
|
||||||
String value = noiseText(noise.biomeNoise());
|
|
||||||
IrisBiome picked = noise.noisePick();
|
|
||||||
if (picked == null) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (surface == null || picked.getLoadKey().equals(surface.getLoadKey())) {
|
|
||||||
return value + " → " + picked.getName();
|
|
||||||
}
|
|
||||||
return value + " → " + picked.getName() + ", child " + surface.getName() + " took over";
|
|
||||||
}
|
|
||||||
|
|
||||||
// How hybrid terrain chose the base biome; a child biome or a coast can still replace it afterwards.
|
|
||||||
private static String placementText(HybridBiomeSelector.Choice choice) {
|
|
||||||
String height = heightAboveSea(choice.placementHeight()) + " · ";
|
|
||||||
if (choice.fallback()) {
|
|
||||||
return height + "no range covers it, closest wins";
|
|
||||||
}
|
|
||||||
if (choice.rivalCount() == 0) {
|
|
||||||
return height + "only biome in range";
|
|
||||||
}
|
|
||||||
return height + "beat " + choice.rivalCount() + (choice.rivalCount() == 1 ? " other" : " others") + " in range";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Section locationSection(double worldX, double worldZ, boolean detailed) {
|
|
||||||
int blockX = MapCanvas.floorWorldCoordinate(worldX);
|
int blockX = MapCanvas.floorWorldCoordinate(worldX);
|
||||||
int blockZ = MapCanvas.floorWorldCoordinate(worldZ);
|
int blockZ = MapCanvas.floorWorldCoordinate(worldZ);
|
||||||
List<Row> rows = new ArrayList<>();
|
return new Group("Location", List.of(
|
||||||
rows.add(new Row("Block", blockX + ", " + blockZ));
|
new Row("Block", blockX + ", " + blockZ, false),
|
||||||
rows.add(new Row("Chunk", (blockX >> 4) + ", " + (blockZ >> 4)));
|
new Row("Chunk", (blockX >> 4) + ", " + (blockZ >> 4), false),
|
||||||
if (detailed) {
|
new Row("Region file", "r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca", false)));
|
||||||
rows.add(new Row("Region file", "r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca"));
|
|
||||||
}
|
|
||||||
return new Section("Location", rows);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addFilesSection(List<Section> sections, MapLabels.Label subject, MapLabels.Label region) {
|
private static void addFilesGroup(List<Group> groups, IrisBiome biome, Object region) {
|
||||||
List<Row> rows = new ArrayList<>();
|
List<Row> rows = new ArrayList<>();
|
||||||
addPackFile(rows, subject);
|
if (biome != null) {
|
||||||
if (subject != null && subject.kind() != MapLabels.Kind.REGION) {
|
rows.add(new Row("Biome", packPath(biome), true));
|
||||||
addPackFile(rows, region);
|
}
|
||||||
|
if (region instanceof IrisRegistrant registrant) {
|
||||||
|
rows.add(new Row("Region", packPath(registrant), true));
|
||||||
}
|
}
|
||||||
if (!rows.isEmpty()) {
|
if (!rows.isEmpty()) {
|
||||||
sections.add(new Section("Pack files", rows));
|
groups.add(new Group("Files", rows));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The key is the path under the type's folder, so folder, key and extension give the file within the pack.
|
// The key is the path under the type's folder, so folder, key and extension give the file within the pack.
|
||||||
private static void addPackFile(List<Row> rows, MapLabels.Label label) {
|
private static String packPath(IrisRegistrant registrant) {
|
||||||
if (label == null || !(label.subject() instanceof IrisRegistrant registrant)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String kind = label.kind() == MapLabels.Kind.REGION ? "Region" : "Biome";
|
|
||||||
File file = registrant.getLoadFile();
|
File file = registrant.getLoadFile();
|
||||||
String fileName = file == null ? "" : file.getName();
|
String fileName = file == null ? "" : file.getName();
|
||||||
int extensionStart = fileName.lastIndexOf('.');
|
int extensionStart = fileName.lastIndexOf('.');
|
||||||
String extension = extensionStart < 0 ? "" : fileName.substring(extensionStart);
|
String extension = extensionStart < 0 ? "" : fileName.substring(extensionStart);
|
||||||
rows.add(new Row(kind, registrant.getFolderName() + "/" + label.key() + extension));
|
return registrant.getFolderName() + "/" + registrant.getLoadKey() + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Hint> hints(RenderType type, boolean detailed) {
|
||||||
|
List<Hint> hints = new ArrayList<>();
|
||||||
|
if (!detailed) {
|
||||||
|
hints.add(new Hint("Shift", "More"));
|
||||||
|
}
|
||||||
|
hints.add(new Hint("H", "Highlight"));
|
||||||
|
if (type != RenderType.CONTINENT) {
|
||||||
|
hints.add(new Hint("Alt+Click", "Open file"));
|
||||||
|
}
|
||||||
|
return hints;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String heightAboveSea(double height) {
|
private static String heightAboveSea(double height) {
|
||||||
@@ -277,17 +485,19 @@ record MapHoverCard(String title, String subtitle, List<Section> sections, Strin
|
|||||||
return rounded < 0 ? -rounded + " below sea" : rounded + " above sea";
|
return rounded < 0 ? -rounded + " below sea" : rounded + " above sea";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String noiseText(double noise) {
|
/**
|
||||||
return String.format(Locale.ROOT, "%.3f", noise);
|
* @param color the biome's map colour, or null when no step of the path tells it
|
||||||
|
* @param kind what the title names, like "Biome", or null while drawing
|
||||||
|
*/
|
||||||
|
record Header(String name, Integer color, String kind) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String capitalize(String text) {
|
record Group(String title, List<Row> rows) {
|
||||||
return Character.toUpperCase(text.charAt(0)) + text.substring(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
record Section(String title, List<Row> rows) {
|
record Row(String label, String value, boolean link) {
|
||||||
}
|
}
|
||||||
|
|
||||||
record Row(String label, String value) {
|
record Hint(String key, String action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.NoisePickSpread.Band;
|
||||||
|
import com.volmit.iris.core.gui.NoisePickSpread.Option;
|
||||||
import com.volmit.iris.engine.HybridBiomeSelector;
|
import com.volmit.iris.engine.HybridBiomeSelector;
|
||||||
import com.volmit.iris.engine.IrisComplex;
|
import com.volmit.iris.engine.IrisComplex;
|
||||||
import com.volmit.iris.engine.framework.Engine;
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
@@ -27,6 +29,8 @@ import com.volmit.iris.engine.object.IrisDimension;
|
|||||||
import com.volmit.iris.engine.object.IrisRegion;
|
import com.volmit.iris.engine.object.IrisRegion;
|
||||||
import org.bukkit.block.Biome;
|
import org.bukkit.block.Biome;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,61 +45,108 @@ final class MapHoverProbe {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param previewRegionKey the region the region preview treats every spot as part of, or null for the real world
|
* @param previewRegionKey the region the region preview treats every spot as part of, or null for the real world
|
||||||
* @param explainPicks also look up the noise values that picked the region and biome, for the region view
|
* @param explainBiomePick whether the card shows the surface biome, so the whole way to it is worth looking up;
|
||||||
|
* otherwise only how the region was picked
|
||||||
*/
|
*/
|
||||||
static Details sample(Engine engine, double worldX, double worldZ, String previewRegionKey, boolean explainPicks) {
|
static Details sample(Engine engine, double worldX, double worldZ, String previewRegionKey, boolean explainBiomePick) {
|
||||||
IrisComplex complex = engine.getComplex();
|
IrisComplex complex = engine.getComplex();
|
||||||
IrisRegion previewRegion = previewRegionKey == null ? null : engine.getData().getRegionLoader().load(previewRegionKey);
|
IrisRegion previewRegion = previewRegionKey == null ? null : engine.getData().getRegionLoader().load(previewRegionKey);
|
||||||
IrisRegion region = previewRegion != null ? previewRegion : complex.getRegionStream().get(worldX, worldZ);
|
IrisRegion region = previewRegion != null ? previewRegion : complex.getRegionStream().get(worldX, worldZ);
|
||||||
IrisBiome surfaceBiome = previewRegion != null
|
IrisComplex.BiomePick surfacePick = complex.surfacePickAsIfIn(region, worldX, worldZ);
|
||||||
? complex.surfaceBiomeAsIfIn(previewRegion, worldX, worldZ)
|
|
||||||
: complex.getBaseBiomeStream().get(worldX, worldZ);
|
|
||||||
IrisBiome caveBiome = complex.getCaveBiomeStream().get(worldX, worldZ);
|
|
||||||
InferredType terrainKind = complex.getBridgeStream().get(worldX, worldZ);
|
|
||||||
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
|
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
|
||||||
double height = complex.getHeightStream().getDouble(worldX, worldZ);
|
|
||||||
double slope = complex.getSlopeStream().getDouble(worldX, worldZ);
|
double slope = complex.getSlopeStream().getDouble(worldX, worldZ);
|
||||||
IrisDimension dimension = engine.getDimension();
|
IrisDimension dimension = engine.getDimension();
|
||||||
return new Details(
|
return new Details(
|
||||||
worldX,
|
worldX,
|
||||||
worldZ,
|
worldZ,
|
||||||
surfaceBiome,
|
surfacePick.biome(),
|
||||||
caveBiome,
|
complex.getCaveBiomeStream().get(worldX, worldZ),
|
||||||
region,
|
region,
|
||||||
terrainKindName(surfaceBiome, terrainKind),
|
|
||||||
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
|
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
|
||||||
height,
|
complex.getHeightStream().getDouble(worldX, worldZ),
|
||||||
dimension == null ? 0D : dimension.getFluidHeight(),
|
dimension == null ? 0D : dimension.getFluidHeight(),
|
||||||
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
|
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
|
||||||
complex.explainHybridChoice(region, worldX, worldZ),
|
pickPath(engine, region, previewRegion != null, explainBiomePick ? surfacePick : null, worldX, worldZ)
|
||||||
explainPicks ? pickNoise(complex, region, previewRegion, worldX, worldZ) : null
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HYBRID terrain picks biomes by height, which the placement line already tells, so only the region noise is left.
|
// The region preview chose the region itself; the real map picks it by the region noise.
|
||||||
// The preview shows surface biomes, which the height can move to another biome list; the real map shows base biomes.
|
private static List<PickStep> pickPath(Engine engine, IrisRegion region, boolean previewed,
|
||||||
private static PickNoise pickNoise(IrisComplex complex, IrisRegion region, IrisRegion previewRegion, double x, double z) {
|
IrisComplex.BiomePick surfacePick, double x, double z) {
|
||||||
Double regionNoise = previewRegion == null ? complex.getRegionStyleStream().get(x, z) : null;
|
List<PickStep> path = new ArrayList<>();
|
||||||
if (complex.isHybridTerrain() || region == null) {
|
path.add(previewed ? previewedRegionStep(engine, region) : regionStep(engine, region, x, z));
|
||||||
return new PickNoise(regionNoise, null, Double.NaN, null);
|
if (surfacePick == null || surfacePick.type() == null || engine.getComplex().getFocusBiome() != null) {
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
InferredType biomeType = previewRegion != null
|
PickStep biomeStep = engine.getComplex().isHybridTerrain()
|
||||||
? complex.surfacePickAsIfIn(region, x, z).type()
|
? heightStep(engine, region, surfacePick.type(), x, z)
|
||||||
: complex.getBridgeStream().get(x, z);
|
: noiseStep(engine, region, surfacePick.type(), x, z);
|
||||||
return new PickNoise(regionNoise, biomeType, complex.biomePickNoise(region, biomeType, x, z),
|
if (biomeStep == null) {
|
||||||
complex.biomePickedByNoise(region, biomeType, x, z));
|
return path;
|
||||||
|
}
|
||||||
|
path.add(biomeStep);
|
||||||
|
if (!biomeStep.picked().key().equals(surfacePick.biome().getLoadKey())) {
|
||||||
|
path.add(new PickStep(PickMeasuring.biomeOption(engine, surfacePick.biome()), "child", null, List.of(), Double.NaN));
|
||||||
|
}
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A shore biome sits on land the continent stream calls land or sea, so the biome's own type says it best.
|
private static PickStep regionStep(Engine engine, IrisRegion region, double x, double z) {
|
||||||
private static String terrainKindName(IrisBiome surfaceBiome, InferredType continentKind) {
|
IrisComplex complex = engine.getComplex();
|
||||||
InferredType kind = surfaceBiome != null && surfaceBiome.getInferredType() != null
|
double noise = complex.getRegionStyleStream().get(x, z);
|
||||||
? surfaceBiome.getInferredType()
|
List<Option> regions = complex.noisePickedRegions().stream()
|
||||||
: continentKind;
|
.map(candidate -> PickMeasuring.regionOption(engine, candidate))
|
||||||
if (kind == null) {
|
.toList();
|
||||||
|
return new PickStep(PickMeasuring.regionOption(engine, region), "region", noiseText(noise),
|
||||||
|
NoisePickSpread.splitNoise(regions, complex.isLegacyRarity()), noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickStep previewedRegionStep(Engine engine, IrisRegion region) {
|
||||||
|
return new PickStep(PickMeasuring.regionOption(engine, region), "previewed", null, List.of(), Double.NaN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickStep noiseStep(Engine engine, IrisRegion region, InferredType type, double x, double z) {
|
||||||
|
IrisComplex complex = engine.getComplex();
|
||||||
|
double noise = complex.biomePickNoise(region, type, x, z);
|
||||||
|
List<Option> biomes = complex.noisePickedBiomes(region, type).stream()
|
||||||
|
.map(biome -> PickMeasuring.biomeOption(engine, biome))
|
||||||
|
.toList();
|
||||||
|
IrisBiome picked = complex.biomePickedByNoise(region, type, x, z);
|
||||||
|
return new PickStep(PickMeasuring.biomeOption(engine, picked), typeName(type), noiseText(noise),
|
||||||
|
NoisePickSpread.splitNoise(biomes, complex.isLegacyRarity()), noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HYBRID terrain picks by the height a spot is placed at, and only competes where biome ranges share that height.
|
||||||
|
private static PickStep heightStep(Engine engine, IrisRegion region, InferredType type, double x, double z) {
|
||||||
|
HybridBiomeSelector.Choice choice = engine.getComplex().explainHybridChoice(region, type, x, z);
|
||||||
|
if (choice == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String name = kind.name().toLowerCase(Locale.ROOT);
|
return new PickStep(PickMeasuring.biomeOption(engine, choice.biome()), typeName(type) + ", " + competitionText(choice),
|
||||||
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
signedHeightText(choice.placementHeight()), List.of(), Double.NaN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String competitionText(HybridBiomeSelector.Choice choice) {
|
||||||
|
if (choice.fallback()) {
|
||||||
|
return "nearest range";
|
||||||
|
}
|
||||||
|
if (choice.rivalCount() == 0) {
|
||||||
|
return "only fit";
|
||||||
|
}
|
||||||
|
return "beat " + choice.rivalCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String typeName(InferredType type) {
|
||||||
|
return type.name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String noiseText(double noise) {
|
||||||
|
return String.format(Locale.ROOT, "%.3f", noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String signedHeightText(double heightAboveSea) {
|
||||||
|
long rounded = Math.round(heightAboveSea);
|
||||||
|
return rounded < 0 ? "−" + -rounded : "+" + rounded;
|
||||||
}
|
}
|
||||||
|
|
||||||
record Details(
|
record Details(
|
||||||
@@ -104,21 +155,24 @@ final class MapHoverProbe {
|
|||||||
IrisBiome surfaceBiome,
|
IrisBiome surfaceBiome,
|
||||||
IrisBiome caveBiome,
|
IrisBiome caveBiome,
|
||||||
IrisRegion region,
|
IrisRegion region,
|
||||||
String terrainKind,
|
|
||||||
String vanillaBiome,
|
String vanillaBiome,
|
||||||
double height,
|
double height,
|
||||||
double fluidHeight,
|
double fluidHeight,
|
||||||
double slopeDegrees,
|
double slopeDegrees,
|
||||||
HybridBiomeSelector.Choice placement,
|
List<PickStep> pickPath
|
||||||
PickNoise pickNoise
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param regionNoise the region noise value, or null when the region preview chose the region
|
* One step on the way to the spot's biome, starting with its region.
|
||||||
* @param biomeType the biome list the biome noise picked from, or null when no noise picked the biome
|
*
|
||||||
* @param noisePick the biome the noise picked, which a child biome can still replace
|
* @param reason what picked it: its kind, the biome list the noise picked from, or "child"
|
||||||
|
* @param value the noise value or placement height that picked it, or null
|
||||||
|
* @param bands the noise bands of all the options the noise picked from, empty when no noise picked it
|
||||||
*/
|
*/
|
||||||
record PickNoise(Double regionNoise, InferredType biomeType, double biomeNoise, IrisBiome noisePick) {
|
record PickStep(Option picked, String reason, String value, List<Band> bands, double noise) {
|
||||||
|
boolean showsBands() {
|
||||||
|
return !bands.isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,14 @@ final class NoisePickSpread {
|
|||||||
* @param legacyRarity whether the dimension splits the noise the legacy way
|
* @param legacyRarity whether the dimension splits the noise the legacy way
|
||||||
*/
|
*/
|
||||||
static NoisePickSpread of(String title, List<Option> options, boolean legacyRarity) {
|
static NoisePickSpread of(String title, List<Option> options, boolean legacyRarity) {
|
||||||
return new NoisePickSpread(title, legacyRarity ? legacyBands(options) : bands(options));
|
return new NoisePickSpread(title, splitNoise(options, legacyRarity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The band of noise each option gets, in the order the noise meets them.
|
||||||
|
*/
|
||||||
|
static List<Band> splitNoise(List<Option> options, boolean legacyRarity) {
|
||||||
|
return legacyRarity ? legacyBands(options) : bands(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
void add(double noise) {
|
void add(double noise) {
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ final class PickMeasuring {
|
|||||||
return new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity());
|
return new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Option biomeOption(Engine engine, IrisBiome biome) {
|
||||||
|
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
|
||||||
|
biome.getRarity());
|
||||||
|
}
|
||||||
|
|
||||||
static int regionColor(Engine engine, IrisRegion region) {
|
static int regionColor(Engine engine, IrisRegion region) {
|
||||||
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
|
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
|
||||||
}
|
}
|
||||||
@@ -161,11 +166,6 @@ final class PickMeasuring {
|
|||||||
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Option biomeOption(Engine engine, IrisBiome biome) {
|
|
||||||
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
|
|
||||||
biome.getRarity());
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
interface SampleVisitor {
|
interface SampleVisitor {
|
||||||
void visit(double x, double z);
|
void visit(double x, double z);
|
||||||
|
|||||||
@@ -18,26 +18,47 @@
|
|||||||
|
|
||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.Shape;
|
||||||
import java.awt.geom.RoundRectangle2D;
|
import java.awt.geom.RoundRectangle2D;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the floating cards (legends, hover details, hints) on top of a map, styled like IntelliJ's popups.
|
* Draws the floating cards (legends, hover details, hints) on top of a map, styled like IntelliJ's popups.
|
||||||
*/
|
*/
|
||||||
final class StudioOverlay {
|
final class StudioOverlay {
|
||||||
|
private static final Color FOOTER_BACKGROUND = new ThemeColor(0xF7F8FA, 0x27292C);
|
||||||
|
|
||||||
private StudioOverlay() {
|
private StudioOverlay() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void drawCard(Graphics2D canvas, int x, int y, int width, int height) {
|
static void drawCard(Graphics2D canvas, int x, int y, int width, int height) {
|
||||||
Object antialiasing = canvas.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
|
drawCard(canvas, x, y, width, height, 0);
|
||||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
}
|
||||||
RoundRectangle2D card = new RoundRectangle2D.Double(x + 0.5D, y + 0.5D, width - 1D, height - 1D,
|
|
||||||
|
/**
|
||||||
|
* Draws a card whose bottom {@code footerHeight} pixels are a darker bar, like the shortcut bar of IntelliJ's popups.
|
||||||
|
*/
|
||||||
|
static void drawCard(Graphics2D canvas, int x, int y, int width, int height, int footerHeight) {
|
||||||
|
Graphics2D card = (Graphics2D) canvas.create();
|
||||||
|
card.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
RoundRectangle2D outline = new RoundRectangle2D.Double(x + 0.5D, y + 0.5D, width - 1D, height - 1D,
|
||||||
StudioTheme.scale(StudioTheme.OVERLAY_ARC), StudioTheme.scale(StudioTheme.OVERLAY_ARC));
|
StudioTheme.scale(StudioTheme.OVERLAY_ARC), StudioTheme.scale(StudioTheme.OVERLAY_ARC));
|
||||||
canvas.setColor(StudioTheme.OVERLAY_BACKGROUND);
|
card.setColor(StudioTheme.OVERLAY_BACKGROUND);
|
||||||
canvas.fill(card);
|
card.fill(outline);
|
||||||
canvas.setColor(StudioTheme.OVERLAY_BORDER);
|
if (footerHeight > 0) {
|
||||||
canvas.draw(card);
|
int footerTop = y + height - footerHeight;
|
||||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
|
Shape cardClip = card.getClip();
|
||||||
|
card.clip(outline);
|
||||||
|
card.setColor(FOOTER_BACKGROUND);
|
||||||
|
card.fillRect(x, footerTop, width, footerHeight);
|
||||||
|
card.setColor(StudioTheme.OVERLAY_BORDER);
|
||||||
|
card.fillRect(x, footerTop, width, 1);
|
||||||
|
card.setClip(cardClip);
|
||||||
|
}
|
||||||
|
card.setColor(StudioTheme.OVERLAY_BORDER);
|
||||||
|
card.draw(outline);
|
||||||
|
card.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,14 +328,14 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Why HYBRID terrain gives a spot its base biome when the spot belongs to the given region; null for BIOME terrain.
|
* Why HYBRID terrain picks a biome from the region's biomes of this type at a spot; null for BIOME terrain.
|
||||||
*/
|
*/
|
||||||
public HybridBiomeSelector.Choice explainHybridChoice(IrisRegion region, double x, double z) {
|
public HybridBiomeSelector.Choice explainHybridChoice(IrisRegion region, InferredType type, double x, double z) {
|
||||||
if (!hybridTerrain || focusBiome != null) {
|
if (!hybridTerrain || focusBiome != null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return chooseHybridBiome(region, bridgeStream.get(x, z), x, z);
|
return chooseHybridBiome(region, type, x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome biomeInRegion(IrisRegion region, InferredType type, double x, double z) {
|
private IrisBiome biomeInRegion(IrisRegion region, InferredType type, double x, double z) {
|
||||||
|
|||||||
Reference in new issue
Block a user