Minimap und ingame Karte - umsetzung begonnen
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user