Minimap und ingame Karte - umsetzung begonnen

This commit is contained in:
2026-08-23 22:13:32 +02:00
parent 46b504e050
commit 9238378bc8
16 changed files with 2119 additions and 8 deletions

View File

@@ -82,6 +82,7 @@ public class EditorApp extends Application {
new java.util.concurrent.ConcurrentLinkedQueue<>();
private VBox assetPanel;
private MapObjectsView mapObjectsView;
private de.blight.editor.ui.WorldMapView worldMapView;
private de.blight.editor.ui.ThumbnailManagerView thumbnailManagerView;
private StackPane worldViewport;
private javafx.scene.canvas.Canvas compassCanvas;
@@ -5637,7 +5638,14 @@ public class EditorApp extends Application {
if (isSelected) mapObjectsView.refresh();
});
TabPane tabPane = new TabPane(assetsTab, karteTab);
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
Tab weltkartTab = new Tab("Weltkarte", worldMapView);
weltkartTab.setClosable(false);
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
});
TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab);
tabPane.setStyle("-fx-background-color: #e8e8e8;");
VBox.setVgrow(tabPane, Priority.ALWAYS);

View File

@@ -0,0 +1,342 @@
package de.blight.editor.ui;
import de.blight.common.*;
import de.blight.common.model.Location;
import de.blight.common.map.WorldMapRenderer;
import de.blight.common.map.WorldMapRenderer.RenderInput;
import de.blight.common.map.WorldMapRenderer.RenderOptions;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.*;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
/**
* Interaktive 2D-Weltkarte im Editor-Tab.
* Zoom via Mausrad, Pan via Linksklick-Drag.
*/
public class WorldMapView extends VBox {
private static final int RENDER_SIZE = 2048;
private final Canvas canvas = new Canvas(100, 100);
private final Label statusLbl = new Label("Karte noch nicht geladen");
private final ProgressBar progress = new ProgressBar(-1);
private final StackPane canvasPane = new StackPane(canvas);
private final ToggleButton layerTerrain = layerBtn("Gelände");
private final ToggleButton layerWater = layerBtn("Wasser");
private final ToggleButton layerAreas = layerBtn("Areas");
private final ToggleButton layerZones = layerBtn("Zonen");
private final ToggleButton layerLocations = layerBtn("Orte");
private final ToggleButton layerModels = layerBtn("Modelle");
private WritableImage mapFxImage;
private BufferedImage mapBuffered;
private double panX = 0, panY = 0;
private double scale = 1.0;
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
private final Supplier<Stage> stageSupplier;
private final AtomicBoolean loading = new AtomicBoolean(false);
private static final String[] ASSET_BASES = {
"blight-assets/src/main/resources",
"../blight-assets/src/main/resources",
"assets",
".",
};
private static final String[] TERRAIN_TEX_DEFAULTS = {
"Textures/Terrain/splat/grass.jpg",
"Textures/Terrain/Rock2/rock.jpg",
"Textures/Terrain/splat/dirt.jpg",
"",
};
public WorldMapView(Supplier<Stage> stageSupplier) {
this.stageSupplier = stageSupplier;
buildUi();
}
private static Path findAssetRoot() {
for (String base : ASSET_BASES) {
Path p = Paths.get(base);
if (Files.isDirectory(p.resolve("Textures"))) return p;
}
return Paths.get(ASSET_BASES[0]);
}
private static int[] computeSlotColors(MapData mapData) {
return WorldMapRenderer.computeSlotColors(mapData, findAssetRoot(), TERRAIN_TEX_DEFAULTS);
}
private void buildUi() {
// ── Layer-Toolbar ──────────────────────────────────────────────────────
Button refreshBtn = new Button("Aktualisieren");
refreshBtn.setOnAction(e -> loadAndRender());
Button exportBtn = new Button("Als PNG exportieren…");
exportBtn.setOnAction(e -> exportPng());
ToolBar toolbar = new ToolBar(
new Label("Layer:"),
layerTerrain, layerWater, layerAreas, layerZones, layerLocations, layerModels,
new Separator(),
refreshBtn,
exportBtn
);
// ── Canvas ────────────────────────────────────────────────────────────
canvasPane.setStyle("-fx-background-color: #1a1a2a;");
VBox.setVgrow(canvasPane, Priority.ALWAYS);
canvas.widthProperty().bind(canvasPane.widthProperty());
canvas.heightProperty().bind(canvasPane.heightProperty());
canvas.widthProperty().addListener(obs -> redraw());
canvas.heightProperty().addListener(obs -> redraw());
// Layer-Toggle → neu rendern
layerTerrain.setOnAction(e -> rerender());
layerWater.setOnAction(e -> rerender());
layerAreas.setOnAction(e -> rerender());
layerZones.setOnAction(e -> rerender());
layerLocations.setOnAction(e -> rerender());
layerModels.setOnAction(e -> rerender());
// Pan
canvas.setOnMousePressed(e -> {
dragStartX = e.getX();
dragStartY = e.getY();
dragStartPanX = panX;
dragStartPanY = panY;
});
canvas.setOnMouseDragged(e -> {
panX = dragStartPanX + (e.getX() - dragStartX);
panY = dragStartPanY + (e.getY() - dragStartY);
redraw();
});
// Zoom (Mausrad, Ziel = Mausposition)
canvas.setOnScroll(e -> {
if (e.getDeltaY() == 0) return;
double factor = e.getDeltaY() > 0 ? 1.15 : 1.0 / 1.15;
double oldScale = scale;
scale = Math.max(0.1, Math.min(30.0, scale * factor));
panX = e.getX() - (e.getX() - panX) * scale / oldScale;
panY = e.getY() - (e.getY() - panY) * scale / oldScale;
redraw();
});
// Tooltip mit Weltkoordinaten
canvas.setOnMouseMoved(e -> {
if (mapFxImage == null) return;
float wx = WorldMapRenderer.pixelToWorld(e.getX(), canvas.getWidth(), panX, scale);
float wz = WorldMapRenderer.pixelToWorld(e.getY(), canvas.getHeight(), panY, scale);
statusLbl.setText(String.format("X: %.1f Z: %.1f", wx, wz));
});
// ── Statuszeile ───────────────────────────────────────────────────────
progress.setPrefWidth(160);
progress.setVisible(false);
HBox statusBar = new HBox(8, statusLbl, progress);
statusBar.setAlignment(Pos.CENTER_LEFT);
statusBar.setPadding(new Insets(4, 8, 4, 8));
statusBar.setStyle("-fx-background-color: #2a2a3a;");
statusBar.getStyleClass().add("status-bar");
getChildren().addAll(toolbar, canvasPane, statusBar);
setStyle("-fx-background-color: #1a1a2a;");
}
// ── Öffentliche API ───────────────────────────────────────────────────────
/** True wenn die Karte bereits geladen wurde. */
public boolean isLoaded() { return mapFxImage != null || loading.get(); }
/** Lädt Welt-Daten und rendert die Karte (im Hintergrund). */
public void loadAndRender() {
if (loading.getAndSet(true)) return;
progress.setVisible(true);
statusLbl.setText("Lade Welt-Daten…");
Thread t = new Thread(() -> {
try {
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
List<PlacedLocationZone> zones = LocationZoneIO.load();
List<Location> locs = LocationIO.load();
List<PlacedWater> waters = WaterBodyIO.load();
List<PlacedModel> models = PlacedModelIO.load();
Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
int[] slotColors = computeSlotColors(mapData);
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildOptions());
Platform.runLater(() -> {
mapBuffered = bi;
mapFxImage = SwingFXUtils.toFXImage(bi, null);
fitToView();
redraw();
progress.setVisible(false);
statusLbl.setText("Bereit – " + areas.size() + " Areas, " +
locs.size() + " Orte, " + waters.size() + " Wasser");
loading.set(false);
});
} catch (Exception ex) {
Platform.runLater(() -> {
progress.setVisible(false);
statusLbl.setText("Fehler: " + ex.getMessage());
loading.set(false);
});
}
}, "WorldMapRenderer");
t.setDaemon(true);
t.start();
}
// ── Interne Methoden ──────────────────────────────────────────────────────
private void rerender() {
if (mapBuffered == null) { loadAndRender(); return; }
if (loading.getAndSet(true)) return;
progress.setVisible(true);
statusLbl.setText("Rendere…");
Thread t = new Thread(() -> {
try {
// Bereits geladene Daten nochmal laden, damit Layer-Wechsel korrekt
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
List<PlacedLocationZone> zones = LocationZoneIO.load();
List<Location> locs = LocationIO.load();
List<PlacedWater> waters = WaterBodyIO.load();
List<PlacedModel> models = PlacedModelIO.load();
int[] slotColors2 = computeSlotColors(mapData);
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors2);
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildOptions());
Platform.runLater(() -> {
mapBuffered = bi;
mapFxImage = SwingFXUtils.toFXImage(bi, null);
redraw();
progress.setVisible(false);
statusLbl.setText("Bereit");
loading.set(false);
});
} catch (Exception ex) {
Platform.runLater(() -> {
progress.setVisible(false);
statusLbl.setText("Fehler: " + ex.getMessage());
loading.set(false);
});
}
}, "WorldMapRerender");
t.setDaemon(true);
t.start();
}
private void redraw() {
GraphicsContext gc = canvas.getGraphicsContext2D();
double w = canvas.getWidth(), h = canvas.getHeight();
gc.setFill(javafx.scene.paint.Color.rgb(20, 20, 35));
gc.fillRect(0, 0, w, h);
if (mapFxImage == null) {
gc.setFill(Color.GRAY);
gc.fillText("Karte noch nicht geladen – 'Aktualisieren' klicken", 20, 40);
return;
}
double imgW = mapFxImage.getWidth() * scale;
double imgH = mapFxImage.getHeight() * scale;
gc.drawImage(mapFxImage, panX, panY, imgW, imgH);
}
private void fitToView() {
if (canvas.getWidth() <= 0 || mapFxImage == null) return;
double sw = canvas.getWidth() / mapFxImage.getWidth();
double sh = canvas.getHeight() / mapFxImage.getHeight();
scale = Math.min(sw, sh);
panX = (canvas.getWidth() - mapFxImage.getWidth() * scale) / 2;
panY = (canvas.getHeight() - mapFxImage.getHeight() * scale) / 2;
}
private void exportPng() {
FileChooser fc = new FileChooser();
fc.setTitle("Karte als PNG exportieren");
fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("PNG-Bild", "*.png"));
fc.setInitialFileName("weltkarte.png");
File file = fc.showSaveDialog(stageSupplier.get());
if (file == null) return;
statusLbl.setText("Exportiere 4096×4096 PNG…");
progress.setVisible(true);
Thread t = new Thread(() -> {
try {
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
List<PlacedLocationZone> zones = LocationZoneIO.load();
List<Location> locs = LocationIO.load();
List<PlacedWater> waters = WaterBodyIO.load();
List<PlacedModel> models = PlacedModelIO.load();
int[] slotColors3 = computeSlotColors(mapData);
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors3);
BufferedImage bi = WorldMapRenderer.render(input, 4096, buildOptions());
ImageIO.write(bi, "PNG", file);
Platform.runLater(() -> {
progress.setVisible(false);
statusLbl.setText("Exportiert: " + file.getName());
});
} catch (IOException ex) {
Platform.runLater(() -> {
progress.setVisible(false);
statusLbl.setText("Export-Fehler: " + ex.getMessage());
});
}
}, "MapPngExport");
t.setDaemon(true);
t.start();
}
private RenderOptions buildOptions() {
return new RenderOptions(
layerTerrain.isSelected(),
layerTerrain.isSelected(),
layerWater.isSelected(),
layerAreas.isSelected(),
layerZones.isSelected(),
layerLocations.isSelected(),
layerModels.isSelected()
);
}
private static ToggleButton layerBtn(String label) {
ToggleButton btn = new ToggleButton(label);
btn.setSelected(true);
return btn;
}
}