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:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user