mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +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);
|
||||
controller.submitProbe(
|
||||
frame,
|
||||
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), currentType.drawsPickNoise()),
|
||||
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), hoverExplainsBiomePick()),
|
||||
details -> {
|
||||
QuickHover quick = quickHover;
|
||||
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() {
|
||||
return currentType == RenderType.REGION_PREVIEW ? previewRegionKey : null;
|
||||
}
|
||||
|
||||
@@ -18,258 +18,466 @@
|
||||
|
||||
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.RenderType;
|
||||
import com.volmit.iris.core.loader.IrisRegistrant;
|
||||
import com.volmit.iris.engine.HybridBiomeSelector;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
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
|
||||
* sections of labelled values.
|
||||
* The card that follows the cursor, laid out like IntelliJ's documentation popup: the biome as the title, the steps
|
||||
* 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 SUBTITLE_GAP = 2;
|
||||
private static final int SECTION_GAP = 10;
|
||||
private static final int SECTION_TITLE_GAP = 4;
|
||||
private static final int HEADER_TOP = 10;
|
||||
private static final int HEADER_ICON_SIZE = 14;
|
||||
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 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 region the region the drawn tile shows
|
||||
* @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,
|
||||
RenderType type, boolean detailed, double worldX, double worldZ) {
|
||||
List<Section> sections = new ArrayList<>();
|
||||
if (details != null) {
|
||||
sections.add(terrainSection(details, type));
|
||||
addPickSection(sections, details);
|
||||
}
|
||||
sections.add(locationSection(worldX, worldZ, detailed));
|
||||
List<PickStep> path = details == null ? List.of() : details.pickPath();
|
||||
IrisBiome biome = cardBiome(subject, details, type);
|
||||
List<Group> groups = new ArrayList<>();
|
||||
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,
|
||||
detailed ? null : "Shift for pack files · H highlights");
|
||||
return new MapHoverCard(header(subject, biome, type, path), subtitle(details), pathTitle(path), path, !detailed,
|
||||
groups, hints(type, detailed));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 labelWidth = labelColumnWidth(canvas);
|
||||
int width = Math.max(headerWidth(canvas), labelWidth + StudioTheme.scale(COLUMN_GAP) + valueColumnWidth(canvas)) + padding * 2;
|
||||
int height = contentHeight(canvas) + padding * 2;
|
||||
int contentWidth = contentWidth(card);
|
||||
int width = contentWidth + 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 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 top = y + padding;
|
||||
top = drawLine(canvas, title, StudioTheme.boldFont(), StudioTheme.TEXT, left, top);
|
||||
if (subtitle != null) {
|
||||
top = drawLine(canvas, subtitle, StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT, left,
|
||||
top + StudioTheme.scale(SUBTITLE_GAP));
|
||||
}
|
||||
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));
|
||||
}
|
||||
int top = y + StudioTheme.scale(HEADER_TOP);
|
||||
top = drawHeader(card, left, top, contentWidth);
|
||||
top = drawPath(card, left, top, contentWidth);
|
||||
drawGroups(card, left, top);
|
||||
drawFooter(card, left, y + height - footerHeight);
|
||||
card.dispose();
|
||||
}
|
||||
|
||||
private int labelColumnWidth(Graphics2D canvas) {
|
||||
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
|
||||
return sections.stream().flatMap(section -> section.rows().stream())
|
||||
.mapToInt(row -> metrics.stringWidth(row.label())).max().orElse(0);
|
||||
private int contentWidth(Graphics2D canvas) {
|
||||
int width = Math.max(headerWidth(canvas), pathWidth(canvas));
|
||||
width = Math.max(width, groupsWidth(canvas));
|
||||
width = Math.max(width, footerWidth(canvas));
|
||||
return Math.max(StudioTheme.scale(MINIMUM_CONTENT_WIDTH), width);
|
||||
}
|
||||
|
||||
private int valueColumnWidth(Graphics2D canvas) {
|
||||
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
|
||||
return sections.stream().flatMap(section -> section.rows().stream())
|
||||
.mapToInt(row -> metrics.stringWidth(row.value())).max().orElse(0);
|
||||
// Header: the biome's colour, its name in bold with its kind on the right, and the ground's height below.
|
||||
private int headerTextOffset() {
|
||||
return header.color() == null ? 0 : StudioTheme.scale(HEADER_ICON_SIZE + ICON_GAP);
|
||||
}
|
||||
|
||||
private int headerWidth(Graphics2D canvas) {
|
||||
int width = canvas.getFontMetrics(StudioTheme.boldFont()).stringWidth(title);
|
||||
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||
if (subtitle != null) {
|
||||
width = Math.max(width, small.stringWidth(subtitle));
|
||||
int width = headerTextOffset() + canvas.getFontMetrics(StudioTheme.boldFont()).stringWidth(header.name());
|
||||
if (header.kind() != null) {
|
||||
width += StudioTheme.scale(KIND_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(header.kind());
|
||||
}
|
||||
if (footer != null) {
|
||||
width = Math.max(width, small.stringWidth(footer));
|
||||
if (subtitle != null) {
|
||||
width = Math.max(width, headerTextOffset() + canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(subtitle));
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
private int contentHeight(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);
|
||||
private int headerHeight(Graphics2D canvas) {
|
||||
int height = canvas.getFontMetrics(StudioTheme.boldFont()).getHeight();
|
||||
if (subtitle != null) {
|
||||
height += StudioTheme.scale(SUBTITLE_GAP) + small;
|
||||
}
|
||||
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;
|
||||
height += canvas.getFontMetrics(StudioTheme.smallFont()).getHeight();
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private static int drawLine(Graphics2D canvas, String text, Font font, Color color, int left, int top) {
|
||||
canvas.setFont(font);
|
||||
canvas.setColor(color);
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
canvas.drawString(text, left, top + metrics.getAscent());
|
||||
return top + metrics.getHeight();
|
||||
private int drawHeader(Graphics2D canvas, int left, int top, int contentWidth) {
|
||||
FontMetrics bold = canvas.getFontMetrics(StudioTheme.boldFont());
|
||||
if (header.color() != null) {
|
||||
int iconSize = StudioTheme.scale(HEADER_ICON_SIZE);
|
||||
drawIcon(canvas, header.color(), left, top + (bold.getHeight() - iconSize) / 2, iconSize);
|
||||
}
|
||||
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) {
|
||||
if (subject != null) {
|
||||
return subject.name();
|
||||
// Path: one indented row per step, its value as a chip on the right and, without Shift, the noise bands below it.
|
||||
private int pathWidth(Graphics2D canvas) {
|
||||
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 details.surfaceBiome().getName();
|
||||
}
|
||||
return "Drawing…";
|
||||
return width;
|
||||
}
|
||||
|
||||
// The region view's title already is the region, so only the kind of ground is left to say.
|
||||
private static String subtitle(MapLabels.Label region, MapHoverProbe.Details details, RenderType type) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (type != RenderType.REGION) {
|
||||
String regionName = region != null ? region.name() : details != null ? details.region().getName() : null;
|
||||
if (regionName != null) {
|
||||
parts.add(regionName + " region");
|
||||
private int pathHeight(Graphics2D canvas) {
|
||||
if (path.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int height = groupTitleHeight(canvas);
|
||||
for (PickStep step : path) {
|
||||
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<>();
|
||||
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 cave = details.caveBiome();
|
||||
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) {
|
||||
rows.add(new Row("Cave biome", cave.getName()));
|
||||
rows.add(new Row("Cave biome", cave.getName(), false));
|
||||
}
|
||||
if (details.vanillaBiome() != null) {
|
||||
rows.add(new Row("Minecraft", details.vanillaBiome().replace('_', ' ')));
|
||||
}
|
||||
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())));
|
||||
rows.add(new Row("Minecraft", details.vanillaBiome().replace('_', ' '), false));
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
private static Group locationGroup(double worldX, double worldZ) {
|
||||
int blockX = MapCanvas.floorWorldCoordinate(worldX);
|
||||
int blockZ = MapCanvas.floorWorldCoordinate(worldZ);
|
||||
List<Row> rows = new ArrayList<>();
|
||||
rows.add(new Row("Block", blockX + ", " + blockZ));
|
||||
rows.add(new Row("Chunk", (blockX >> 4) + ", " + (blockZ >> 4)));
|
||||
if (detailed) {
|
||||
rows.add(new Row("Region file", "r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca"));
|
||||
}
|
||||
return new Section("Location", rows);
|
||||
return new Group("Location", List.of(
|
||||
new Row("Block", blockX + ", " + blockZ, false),
|
||||
new Row("Chunk", (blockX >> 4) + ", " + (blockZ >> 4), false),
|
||||
new Row("Region file", "r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca", false)));
|
||||
}
|
||||
|
||||
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<>();
|
||||
addPackFile(rows, subject);
|
||||
if (subject != null && subject.kind() != MapLabels.Kind.REGION) {
|
||||
addPackFile(rows, region);
|
||||
if (biome != null) {
|
||||
rows.add(new Row("Biome", packPath(biome), true));
|
||||
}
|
||||
if (region instanceof IrisRegistrant registrant) {
|
||||
rows.add(new Row("Region", packPath(registrant), true));
|
||||
}
|
||||
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.
|
||||
private static void addPackFile(List<Row> rows, MapLabels.Label label) {
|
||||
if (label == null || !(label.subject() instanceof IrisRegistrant registrant)) {
|
||||
return;
|
||||
}
|
||||
String kind = label.kind() == MapLabels.Kind.REGION ? "Region" : "Biome";
|
||||
private static String packPath(IrisRegistrant registrant) {
|
||||
File file = registrant.getLoadFile();
|
||||
String fileName = file == null ? "" : file.getName();
|
||||
int extensionStart = fileName.lastIndexOf('.');
|
||||
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) {
|
||||
@@ -277,17 +485,19 @@ record MapHoverCard(String title, String subtitle, List<Section> sections, Strin
|
||||
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) {
|
||||
return Character.toUpperCase(text.charAt(0)) + text.substring(1);
|
||||
record Group(String title, List<Row> rows) {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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.IrisComplex;
|
||||
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 org.bukkit.block.Biome;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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 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();
|
||||
IrisRegion previewRegion = previewRegionKey == null ? null : engine.getData().getRegionLoader().load(previewRegionKey);
|
||||
IrisRegion region = previewRegion != null ? previewRegion : complex.getRegionStream().get(worldX, worldZ);
|
||||
IrisBiome surfaceBiome = previewRegion != null
|
||||
? complex.surfaceBiomeAsIfIn(previewRegion, worldX, worldZ)
|
||||
: complex.getBaseBiomeStream().get(worldX, worldZ);
|
||||
IrisBiome caveBiome = complex.getCaveBiomeStream().get(worldX, worldZ);
|
||||
InferredType terrainKind = complex.getBridgeStream().get(worldX, worldZ);
|
||||
IrisComplex.BiomePick surfacePick = complex.surfacePickAsIfIn(region, worldX, worldZ);
|
||||
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
|
||||
double height = complex.getHeightStream().getDouble(worldX, worldZ);
|
||||
double slope = complex.getSlopeStream().getDouble(worldX, worldZ);
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
return new Details(
|
||||
worldX,
|
||||
worldZ,
|
||||
surfaceBiome,
|
||||
caveBiome,
|
||||
surfacePick.biome(),
|
||||
complex.getCaveBiomeStream().get(worldX, worldZ),
|
||||
region,
|
||||
terrainKindName(surfaceBiome, terrainKind),
|
||||
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
|
||||
height,
|
||||
complex.getHeightStream().getDouble(worldX, worldZ),
|
||||
dimension == null ? 0D : dimension.getFluidHeight(),
|
||||
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
|
||||
complex.explainHybridChoice(region, worldX, worldZ),
|
||||
explainPicks ? pickNoise(complex, region, previewRegion, worldX, worldZ) : null
|
||||
pickPath(engine, region, previewRegion != null, explainBiomePick ? surfacePick : null, worldX, worldZ)
|
||||
);
|
||||
}
|
||||
|
||||
// HYBRID terrain picks biomes by height, which the placement line already tells, so only the region noise is left.
|
||||
// The preview shows surface biomes, which the height can move to another biome list; the real map shows base biomes.
|
||||
private static PickNoise pickNoise(IrisComplex complex, IrisRegion region, IrisRegion previewRegion, double x, double z) {
|
||||
Double regionNoise = previewRegion == null ? complex.getRegionStyleStream().get(x, z) : null;
|
||||
if (complex.isHybridTerrain() || region == null) {
|
||||
return new PickNoise(regionNoise, null, Double.NaN, null);
|
||||
// The region preview chose the region itself; the real map picks it by the region noise.
|
||||
private static List<PickStep> pickPath(Engine engine, IrisRegion region, boolean previewed,
|
||||
IrisComplex.BiomePick surfacePick, double x, double z) {
|
||||
List<PickStep> path = new ArrayList<>();
|
||||
path.add(previewed ? previewedRegionStep(engine, region) : regionStep(engine, region, x, z));
|
||||
if (surfacePick == null || surfacePick.type() == null || engine.getComplex().getFocusBiome() != null) {
|
||||
return path;
|
||||
}
|
||||
InferredType biomeType = previewRegion != null
|
||||
? complex.surfacePickAsIfIn(region, x, z).type()
|
||||
: complex.getBridgeStream().get(x, z);
|
||||
return new PickNoise(regionNoise, biomeType, complex.biomePickNoise(region, biomeType, x, z),
|
||||
complex.biomePickedByNoise(region, biomeType, x, z));
|
||||
PickStep biomeStep = engine.getComplex().isHybridTerrain()
|
||||
? heightStep(engine, region, surfacePick.type(), x, z)
|
||||
: noiseStep(engine, region, surfacePick.type(), x, z);
|
||||
if (biomeStep == null) {
|
||||
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 String terrainKindName(IrisBiome surfaceBiome, InferredType continentKind) {
|
||||
InferredType kind = surfaceBiome != null && surfaceBiome.getInferredType() != null
|
||||
? surfaceBiome.getInferredType()
|
||||
: continentKind;
|
||||
if (kind == null) {
|
||||
private static PickStep regionStep(Engine engine, IrisRegion region, double x, double z) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
double noise = complex.getRegionStyleStream().get(x, z);
|
||||
List<Option> regions = complex.noisePickedRegions().stream()
|
||||
.map(candidate -> PickMeasuring.regionOption(engine, candidate))
|
||||
.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;
|
||||
}
|
||||
String name = kind.name().toLowerCase(Locale.ROOT);
|
||||
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
return new PickStep(PickMeasuring.biomeOption(engine, choice.biome()), typeName(type) + ", " + competitionText(choice),
|
||||
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(
|
||||
@@ -104,21 +155,24 @@ final class MapHoverProbe {
|
||||
IrisBiome surfaceBiome,
|
||||
IrisBiome caveBiome,
|
||||
IrisRegion region,
|
||||
String terrainKind,
|
||||
String vanillaBiome,
|
||||
double height,
|
||||
double fluidHeight,
|
||||
double slopeDegrees,
|
||||
HybridBiomeSelector.Choice placement,
|
||||
PickNoise pickNoise
|
||||
List<PickStep> pickPath
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param regionNoise the region noise value, or null when the region preview chose the 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
|
||||
* One step on the way to the spot's biome, starting with its region.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
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) {
|
||||
|
||||
@@ -58,6 +58,11 @@ final class PickMeasuring {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
interface SampleVisitor {
|
||||
void visit(double x, double z);
|
||||
|
||||
@@ -18,26 +18,47 @@
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
/**
|
||||
* Draws the floating cards (legends, hover details, hints) on top of a map, styled like IntelliJ's popups.
|
||||
*/
|
||||
final class StudioOverlay {
|
||||
private static final Color FOOTER_BACKGROUND = new ThemeColor(0xF7F8FA, 0x27292C);
|
||||
|
||||
private StudioOverlay() {
|
||||
}
|
||||
|
||||
static void drawCard(Graphics2D canvas, int x, int y, int width, int height) {
|
||||
Object antialiasing = canvas.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
|
||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
RoundRectangle2D card = new RoundRectangle2D.Double(x + 0.5D, y + 0.5D, width - 1D, height - 1D,
|
||||
drawCard(canvas, x, y, width, height, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
canvas.setColor(StudioTheme.OVERLAY_BACKGROUND);
|
||||
canvas.fill(card);
|
||||
canvas.setColor(StudioTheme.OVERLAY_BORDER);
|
||||
canvas.draw(card);
|
||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
|
||||
card.setColor(StudioTheme.OVERLAY_BACKGROUND);
|
||||
card.fill(outline);
|
||||
if (footerHeight > 0) {
|
||||
int footerTop = y + height - footerHeight;
|
||||
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) {
|
||||
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) {
|
||||
|
||||
Reference in new issue
Block a user