From a0b1e09d7de184fc7592a764f9a0c87ae23f9b1b Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 6 Sep 2026 22:01:36 +0200 Subject: [PATCH] =?UTF-8?q?H=C3=B6hlenkarte:=20Layer-Men=C3=BC,=20Vollbild?= =?UTF-8?q?,=20Refresh=20und=20Zoom-Sync=20mit=20Weltkarte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../main/java/de/blight/editor/EditorApp.java | 67 ++++++- .../java/de/blight/editor/ui/CaveMapView.java | 168 ++++++++++++++---- .../de/blight/editor/ui/WorldMapView.java | 21 +++ 3 files changed, 220 insertions(+), 36 deletions(-) diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 9a8ac03..f8d605b 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -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 diff --git a/blight-editor/src/main/java/de/blight/editor/ui/CaveMapView.java b/blight-editor/src/main/java/de/blight/editor/ui/CaveMapView.java index c318e98..1dd3375 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/CaveMapView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/CaveMapView.java @@ -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 cachedPathChains = new ArrayList<>(); - private List tunnelEntrances = new ArrayList<>(); - private List cachedAreas = new ArrayList<>(); - private List 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 cachedPathChains = new ArrayList<>(); + private List tunnelEntrances = new ArrayList<>(); + private List cachedAreas = new ArrayList<>(); + private List 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 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 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 areas = AreaIO.load(); + MapData mapData = MapIO.load(); + List areas = AreaIO.load(); List models = PlacedModelIO.load(); - BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE); - List paths = WorldMapView.loadPathChains(); - List entrances = WorldMapView.computeTunnelEntrances(mapData); - List footprints = WorldMapView.computeBuildingFootprintsSimple(models); + BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE); + List paths = WorldMapView.loadPathChains(); + List entrances = WorldMapView.computeTunnelEntrances(mapData); + List 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) { diff --git a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java index 525a42d..82faff5 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java @@ -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) {