Höhlenkarte: Layer-Menü, Vollbild, Refresh und Zoom-Sync mit Weltkarte

- CaveMapView: Layer-MenuButton (Eingänge, Gebäude, Wege, Areas), Refresh-Button, Vollbild-Modus analog WorldMapView
- WorldMapView + CaveMapView: getScale/getPanX/getPanY + setView() für Tab-Wechsel-Sync
- EditorApp: caveMapView als Feld; beim Tab-Wechsel werden Zoom und Position übernommen; Vollbild-Callbacks für Höhlenkarte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 22:01:36 +02:00
parent 5226e07622
commit a0b1e09d7d
3 changed files with 220 additions and 36 deletions

View File

@@ -83,8 +83,10 @@ public class EditorApp extends Application {
new java.util.concurrent.ConcurrentLinkedQueue<>();
private VBox assetPanel;
private Tab weltkartTab;
private Tab caveMapTab;
private MapObjectsView mapObjectsView;
private de.blight.editor.ui.WorldMapView worldMapView;
private de.blight.editor.ui.CaveMapView caveMapView;
private de.blight.editor.ui.ThumbnailManagerView thumbnailManagerView;
private StackPane worldViewport;
private javafx.scene.canvas.Canvas compassCanvas;
@@ -5700,17 +5702,70 @@ public class EditorApp extends Application {
weltkartTab = new Tab("Weltkarte", worldMapView);
weltkartTab.setClosable(false);
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
else if (isSelected) worldMapView.ensureFitToView();
if (isSelected && !worldMapView.isLoaded()) {
worldMapView.loadAndRender();
} else if (isSelected) {
worldMapView.ensureFitToView();
if (caveMapView != null && caveMapView.isLoaded()) {
worldMapView.setView(caveMapView.getScale(), caveMapView.getPanX(), caveMapView.getPanY());
}
}
});
de.blight.editor.ui.CaveMapView caveMapView = new de.blight.editor.ui.CaveMapView(() -> primaryStage);
Tab caveMapTab = new Tab("Höhlenkarte", caveMapView);
caveMapView = new de.blight.editor.ui.CaveMapView(() -> primaryStage);
caveMapTab = new Tab("Höhlenkarte", caveMapView);
caveMapTab.setClosable(false);
caveMapTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
if (isSelected && !caveMapView.isLoaded()) caveMapView.loadAndRender();
else if (isSelected) caveMapView.ensureFitToView();
if (isSelected && !caveMapView.isLoaded()) {
caveMapView.loadAndRender();
} else if (isSelected) {
caveMapView.ensureFitToView();
if (worldMapView.isMapReady()) {
caveMapView.setView(worldMapView.getScale(), worldMapView.getPanX(), worldMapView.getPanY());
}
}
});
caveMapView.setFullscreenCallbacks(
() -> {
final javafx.scene.control.Label placeholder = new javafx.scene.control.Label();
caveMapTab.setContent(placeholder);
assetTabBtn.setVisible(false);
javafx.application.Platform.runLater(() -> {
caveMapView.setMaxWidth(Double.MAX_VALUE);
javafx.scene.layout.StackPane.setAlignment(caveMapView, null);
caveMapView.setTranslateX(0);
try {
centerStack.getChildren().add(caveMapView);
} catch (Exception ex) {
log.error("[FS-cave-enter] add failed", ex);
return;
}
javafx.application.Platform.runLater(() -> {
double w = caveMapView.getWidth();
caveMapView.fitCanvas();
caveMapView.setTranslateX(-w);
javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(300), caveMapView);
tt.setToX(0);
tt.setInterpolator(javafx.animation.Interpolator.EASE_OUT);
tt.play();
});
});
},
() -> {
double w = caveMapView.getWidth();
javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(220), caveMapView);
tt.setToX(-w);
tt.setInterpolator(javafx.animation.Interpolator.EASE_IN);
tt.setOnFinished(ev -> {
centerStack.getChildren().remove(caveMapView);
caveMapView.setTranslateX(0);
caveMapTab.setContent(caveMapView);
assetTabBtn.setVisible(true);
});
tt.play();
});
worldMapView.setFullscreenCallbacks(
() -> {
// Placeholder ersetzt worldMapView im Tab → sauberes Detach

View File

@@ -2,10 +2,10 @@ package de.blight.editor.ui;
import de.blight.common.*;
import de.blight.common.map.WorldMapRenderer;
import de.blight.common.map.WorldMapRenderModel;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.*;
@@ -28,40 +28,80 @@ import org.slf4j.LoggerFactory;
/**
* 2D-Kartenansicht des Höhlen/Tunnel-Innenbereichs.
* Hintergrund: dunkles Grau. Erdfarbe für Lochbereiche.
* Overlays: Tunneleingänge, Wege, Gebäude, Areas.
*/
public class CaveMapView extends VBox {
private static final Logger log = LoggerFactory.getLogger(CaveMapView.class);
private static final int RENDER_SIZE = 2048;
private final Canvas canvas = new Canvas();
private final Label statusLbl = new Label("Höhlenkarte noch nicht geladen.");
private final ProgressBar progress = new ProgressBar();
private final AtomicBoolean loading = new AtomicBoolean(false);
private WritableImage mapFxImage = null;
private List<float[][]> cachedPathChains = new ArrayList<>();
private List<float[]> tunnelEntrances = new ArrayList<>();
private List<PlacedArea> cachedAreas = new ArrayList<>();
private List<float[]> buildingFootprints = new ArrayList<>();
private static final int ZOOM_LEVELS = 5;
private static final double ZOOM_MAX_SCALE = 4.0;
private final Canvas canvas = new Canvas();
private final Label statusLbl = new Label("Höhlenkarte noch nicht geladen.");
private final ProgressBar progress = new ProgressBar();
private final StackPane canvasPane = new StackPane();
private final AtomicBoolean loading = new AtomicBoolean(false);
// Layer-Checkboxen
private final CheckBox cbEntrances = layerCheck("Eingänge", true);
private final CheckBox cbBuildings = layerCheck("Gebäude", true);
private final CheckBox cbPaths = layerCheck("Wege", true);
private final CheckBox cbAreas = layerCheck("Areas", true);
private final Button fullscreenBtn = new Button(">>");
private final Button backBtn = new Button("<<");
private boolean isFullscreen = false;
private double savedScale, savedPanX, savedPanY;
private Runnable enterFullscreenCallback = null;
private Runnable exitFullscreenCallback = null;
private WritableImage mapFxImage = null;
private List<float[][]> cachedPathChains = new ArrayList<>();
private List<float[]> tunnelEntrances = new ArrayList<>();
private List<PlacedArea> cachedAreas = new ArrayList<>();
private List<float[]> buildingFootprints = new ArrayList<>();
private double panX = 0, panY = 0, scale = 1.0;
private double[] zoomLevels = null;
private int zoomLevelIdx = 0;
private boolean autoLoaded = false;
public CaveMapView(Supplier<Stage> stageSupplier) {
ToolBar bar = new ToolBar(makeBtn("Aktualisieren", e -> loadAndRender()));
cbEntrances.selectedProperty().addListener((obs, o, n) -> redraw());
cbBuildings.selectedProperty().addListener((obs, o, n) -> redraw());
cbPaths.selectedProperty().addListener((obs, o, n) -> redraw());
cbAreas.selectedProperty().addListener((obs, o, n) -> redraw());
MenuButton layerMenu = new MenuButton("Layer ▾");
for (CheckBox cb : new CheckBox[]{cbEntrances, cbBuildings, cbPaths, cbAreas}) {
CustomMenuItem item = new CustomMenuItem(cb, false);
item.setHideOnClick(false);
layerMenu.getItems().add(item);
}
Button refreshBtn = new Button("↻");
refreshBtn.setTooltip(new Tooltip("Aktualisieren"));
refreshBtn.setOnAction(e -> loadAndRender());
fullscreenBtn.setTooltip(new Tooltip("Maximieren"));
backBtn.setTooltip(new Tooltip("Minimieren"));
fullscreenBtn.setOnAction(e -> enterFullscreen());
backBtn.setOnAction(e -> exitFullscreen());
backBtn.setVisible(false);
ToolBar bar = new ToolBar(
layerMenu,
new Separator(),
refreshBtn,
new Separator(),
fullscreenBtn,
backBtn
);
progress.setMaxWidth(Double.MAX_VALUE);
progress.setVisible(false);
StackPane canvasPane = new StackPane(canvas);
canvasPane.getChildren().add(canvas);
canvasPane.setStyle("-fx-background-color: #141414;");
VBox.setVgrow(canvasPane, Priority.ALWAYS);
canvas.widthProperty().bind(canvasPane.widthProperty());
@@ -69,15 +109,46 @@ public class CaveMapView extends VBox {
canvas.widthProperty().addListener(obs -> redraw());
canvas.heightProperty().addListener(obs -> redraw());
statusLbl.setPadding(new Insets(4, 8, 4, 8));
setupMouse();
getChildren().addAll(bar, progress, statusLbl, canvasPane);
VBox.setVgrow(canvasPane, Priority.ALWAYS);
setStyle("-fx-background-color: #1a1a1a;");
}
private static Button makeBtn(String label, javafx.event.EventHandler<javafx.event.ActionEvent> h) {
Button b = new Button(label); b.setOnAction(h); return b;
private static CheckBox layerCheck(String label, boolean selected) {
CheckBox cb = new CheckBox(label);
cb.setSelected(selected);
return cb;
}
public void setFullscreenCallbacks(Runnable enter, Runnable exit) {
enterFullscreenCallback = enter;
exitFullscreenCallback = exit;
}
// ── Zoom-Zustand für Sync mit WorldMapView ────────────────────────────────
public double getScale() { return scale; }
public double getPanX() { return panX; }
public double getPanY() { return panY; }
/** Setzt Zoom und Position direkt (für Tab-Wechsel-Sync). */
public void setView(double newScale, double newPanX, double newPanY) {
scale = newScale;
panX = newPanX;
panY = newPanY;
if (zoomLevels != null) {
int best = 0;
double minDiff = Math.abs(zoomLevels[0] - scale);
for (int i = 1; i < zoomLevels.length; i++) {
double diff = Math.abs(zoomLevels[i] - scale);
if (diff < minDiff) { minDiff = diff; best = i; }
}
zoomLevelIdx = best;
}
redraw();
}
private void setupMouse() {
@@ -100,12 +171,25 @@ public class CaveMapView extends VBox {
panY = ds[3] + (e.getY()-ds[1]);
redraw();
});
canvas.setOnMouseMoved(e -> {
if (mapFxImage == null) return;
double wx = (e.getX() - panX) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
double wz = (e.getY() - panY) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
statusLbl.setText(String.format("X: %.1f Z: %.1f", wx, wz));
});
}
public boolean isLoaded() { return mapFxImage != null; }
public void ensureFitToView() { if (mapFxImage != null) fitToView(); }
public void fitCanvas() {
double cw = canvasPane.getWidth(), ch = canvasPane.getHeight();
if (cw <= 0 || ch <= 0 || mapFxImage == null) return;
fitToViewWithSize(cw, ch);
redraw();
}
public void loadAndRender() {
if (!loading.compareAndSet(false, true)) return;
progress.setVisible(true);
@@ -114,14 +198,14 @@ public class CaveMapView extends VBox {
Thread t = new Thread(() -> {
try {
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
List<PlacedModel> models = PlacedModelIO.load();
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
List<float[][]> paths = WorldMapView.loadPathChains();
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
List<float[]> footprints = WorldMapView.computeBuildingFootprintsSimple(models);
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
List<float[][]> paths = WorldMapView.loadPathChains();
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
List<float[]> footprints = WorldMapView.computeBuildingFootprintsSimple(models);
Platform.runLater(() -> {
mapFxImage = SwingFXUtils.toFXImage(bi, null);
@@ -151,6 +235,10 @@ public class CaveMapView extends VBox {
private void fitToView() {
double cw = canvas.getWidth(), ch = canvas.getHeight();
if (cw <= 0 || ch <= 0 || mapFxImage == null) return;
fitToViewWithSize(cw, ch);
}
private void fitToViewWithSize(double cw, double ch) {
double fit = Math.min(cw / mapFxImage.getWidth(), ch / mapFxImage.getHeight());
zoomLevels = new double[ZOOM_LEVELS];
zoomLevels[0] = fit;
@@ -162,6 +250,26 @@ public class CaveMapView extends VBox {
panY = (ch - mapFxImage.getHeight() * scale) / 2.0;
}
private void enterFullscreen() {
isFullscreen = true;
savedScale = scale;
savedPanX = panX;
savedPanY = panY;
fullscreenBtn.setVisible(false);
backBtn.setVisible(true);
if (enterFullscreenCallback != null) enterFullscreenCallback.run();
}
private void exitFullscreen() {
isFullscreen = false;
scale = savedScale;
panX = savedPanX;
panY = savedPanY;
backBtn.setVisible(false);
fullscreenBtn.setVisible(true);
if (exitFullscreenCallback != null) exitFullscreenCallback.run();
}
private void redraw() {
if (!autoLoaded && mapFxImage == null && !loading.get()) {
autoLoaded = true;
@@ -178,10 +286,10 @@ public class CaveMapView extends VBox {
}
gc.setImageSmoothing(true);
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
drawTunnelEntrancesOverlay(gc);
drawBuildingOverlay(gc);
drawPathsOverlay(gc);
drawAreasOverlay(gc);
if (cbEntrances.isSelected()) drawTunnelEntrancesOverlay(gc);
if (cbBuildings.isSelected()) drawBuildingOverlay(gc);
if (cbPaths.isSelected()) drawPathsOverlay(gc);
if (cbAreas.isSelected()) drawAreasOverlay(gc);
}
private double toCanvasX(float wx) {

View File

@@ -331,6 +331,27 @@ public class WorldMapView extends VBox {
public boolean isMapReady() { return mapFxImage != null; }
public String getLoadingStatus() { return statusLbl.getText(); }
public double getScale() { return scale; }
public double getPanX() { return panX; }
public double getPanY() { return panY; }
/** Setzt Zoom und Position direkt (für Tab-Wechsel-Sync). */
public void setView(double newScale, double newPanX, double newPanY) {
scale = newScale;
panX = newPanX;
panY = newPanY;
if (zoomLevels != null) {
int best = 0;
double minDiff = Math.abs(zoomLevels[0] - scale);
for (int i = 1; i < zoomLevels.length; i++) {
double diff = Math.abs(zoomLevels[i] - scale);
if (diff < minDiff) { minDiff = diff; best = i; }
}
zoomLevelIdx = best;
}
redraw();
}
/** Stellt sicher, dass die Karte beim ersten Anzeigen korrekt skaliert/positioniert ist. */
public void ensureFitToView() {
if (mapFxImage != null && zoomLevels == null) {