Arbeiten an der Karte - neuer Render Modus mit Zwischenschicht, Küstenlinien, Wasserfälle
This commit is contained in:
@@ -81,6 +81,7 @@ public class EditorApp extends Application {
|
||||
private final java.util.concurrent.ConcurrentLinkedQueue<String> consoleBuffer =
|
||||
new java.util.concurrent.ConcurrentLinkedQueue<>();
|
||||
private VBox assetPanel;
|
||||
private Tab weltkartTab;
|
||||
private MapObjectsView mapObjectsView;
|
||||
private de.blight.editor.ui.WorldMapView worldMapView;
|
||||
private de.blight.editor.ui.ThumbnailManagerView thumbnailManagerView;
|
||||
@@ -360,8 +361,8 @@ public class EditorApp extends Application {
|
||||
private final java.util.Deque<File> modelImportQueue = new java.util.ArrayDeque<>();
|
||||
|
||||
// Asset-Overlay-Zustand
|
||||
private boolean assetOverlayOpen = false;
|
||||
private Button assetTabBtn;
|
||||
private boolean assetOverlayOpen = false;
|
||||
private Button assetTabBtn;
|
||||
private StackPane centerStack; // persistenter Wrapper für alle 3D-Ansichten
|
||||
|
||||
// Toolbar-Buttons (müssen vom Status-Poller erreichbar sein)
|
||||
@@ -5653,21 +5654,59 @@ public class EditorApp extends Application {
|
||||
input.pendingGotoZ = pos[2];
|
||||
});
|
||||
|
||||
final Tab weltkartTab = new Tab("Weltkarte", worldMapView);
|
||||
weltkartTab = new Tab("Weltkarte", worldMapView);
|
||||
weltkartTab.setClosable(false);
|
||||
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
|
||||
});
|
||||
worldMapView.setFullscreenCallbacks(
|
||||
() -> {
|
||||
// Erst aus Tab lösen, dann in centerStack einsetzen
|
||||
weltkartTab.setContent(new javafx.scene.control.Label(""));
|
||||
setCenterView(worldMapView);
|
||||
// Placeholder ersetzt worldMapView im Tab → sauberes Detach
|
||||
final javafx.scene.control.Label placeholder = new javafx.scene.control.Label();
|
||||
weltkartTab.setContent(placeholder);
|
||||
assetTabBtn.setVisible(false);
|
||||
// Nach einem Pulse hat der TabPane-Skin den Swap verarbeitet
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
log.debug("[FS-enter] parent after detach: {}", worldMapView.getParent());
|
||||
worldMapView.setMaxWidth(Double.MAX_VALUE);
|
||||
// KEIN explizites Alignment → null = CENTER → fillWidth=true → View füllt volle Breite
|
||||
StackPane.setAlignment(worldMapView, null);
|
||||
worldMapView.setTranslateX(0);
|
||||
try {
|
||||
centerStack.getChildren().add(worldMapView);
|
||||
} catch (Exception ex) {
|
||||
log.error("[FS-enter] add failed", ex);
|
||||
return;
|
||||
}
|
||||
// Nach nächstem Layout-Pass hat centerStack die View korrekt dimensioniert
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
double w = worldMapView.getWidth();
|
||||
log.debug("[FS-enter] post-layout: w={} h={}", w, worldMapView.getHeight());
|
||||
// Karte auf volle Canvas-Größe einpassen (scale/pan), dann animieren
|
||||
worldMapView.fitCanvas();
|
||||
worldMapView.setTranslateX(-w);
|
||||
javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(
|
||||
javafx.util.Duration.millis(300), worldMapView);
|
||||
tt.setToX(0);
|
||||
tt.setInterpolator(javafx.animation.Interpolator.EASE_OUT);
|
||||
tt.play();
|
||||
});
|
||||
});
|
||||
},
|
||||
() -> {
|
||||
// Erst aus centerStack lösen (worldViewport zurück), dann in Tab setzen
|
||||
setCenterView(worldViewport);
|
||||
weltkartTab.setContent(worldMapView);
|
||||
// Zurück nach links schieben, dann in Tab zurücklegen
|
||||
double w = worldMapView.getWidth();
|
||||
javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(
|
||||
javafx.util.Duration.millis(220), worldMapView);
|
||||
tt.setToX(-w);
|
||||
tt.setInterpolator(javafx.animation.Interpolator.EASE_IN);
|
||||
tt.setOnFinished(ev -> {
|
||||
centerStack.getChildren().remove(worldMapView);
|
||||
worldMapView.setTranslateX(0);
|
||||
weltkartTab.setContent(worldMapView);
|
||||
assetTabBtn.setVisible(true);
|
||||
});
|
||||
tt.play();
|
||||
});
|
||||
|
||||
TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab);
|
||||
|
||||
@@ -82,16 +82,17 @@ public class WorldMapView extends VBox {
|
||||
private final StackPane canvasPane = new StackPane(canvas);
|
||||
|
||||
// Layer-Checkboxen (im MenuButton gebündelt)
|
||||
private final CheckBox cbTerrain = layerCheck("Gelände");
|
||||
private final CheckBox cbWater = layerCheck("Wasser");
|
||||
private final CheckBox cbAreas = layerCheck("Areas");
|
||||
private final CheckBox cbZones = layerCheck("Zonen");
|
||||
private final CheckBox cbLocations = layerCheck("Orte");
|
||||
private final CheckBox cbModels = layerCheck("Modelle");
|
||||
private final CheckBox cbTerrain = layerCheck("Gelände");
|
||||
private final CheckBox cbWater = layerCheck("Wasser");
|
||||
private final CheckBox cbWaterfalls = layerCheck("Wasserfälle");
|
||||
private final CheckBox cbAreas = layerCheck("Areas");
|
||||
private final CheckBox cbZones = layerCheck("Zonen");
|
||||
private final CheckBox cbLocations = layerCheck("Orte");
|
||||
private final CheckBox cbModels = layerCheck("Modelle");
|
||||
private final ToggleButton labelBtn = new ToggleButton("Label");
|
||||
|
||||
private final Button fullscreenBtn = new Button("⤢ Vollbild");
|
||||
private final Button backBtn = new Button("< Zurück");
|
||||
private final Button fullscreenBtn = new Button(">>");
|
||||
private final Button backBtn = new Button("<<");
|
||||
private boolean isFullscreen = false;
|
||||
|
||||
/** Kamera-Zustand (Position + Blickrichtung) für die Karten-Überlagerung. */
|
||||
@@ -107,6 +108,7 @@ public class WorldMapView extends VBox {
|
||||
|
||||
private double panX = 0, panY = 0;
|
||||
private double scale = 1.0;
|
||||
private double savedScale, savedPanX, savedPanY;
|
||||
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
|
||||
|
||||
private final Supplier<Stage> stageSupplier;
|
||||
@@ -167,25 +169,31 @@ public class WorldMapView extends VBox {
|
||||
// ── Layer-Auswahl als MenuButton mit Checkboxen ───────────────────────
|
||||
cbTerrain.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
|
||||
cbWater.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
|
||||
cbWaterfalls.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
|
||||
cbZones.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
|
||||
cbModels.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbAreas.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbLocations.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
|
||||
MenuButton layerMenu = new MenuButton("Layer ▾");
|
||||
for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbAreas, cbZones, cbLocations, cbModels}) {
|
||||
for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbWaterfalls, cbAreas, cbZones, cbLocations, cbModels}) {
|
||||
CustomMenuItem item = new CustomMenuItem(cb, false);
|
||||
item.setHideOnClick(false);
|
||||
layerMenu.getItems().add(item);
|
||||
}
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────
|
||||
Button refreshBtn = new Button("↻ Modell aktualisieren");
|
||||
Button refreshBtn = new Button("↻");
|
||||
refreshBtn.setTooltip(new javafx.scene.control.Tooltip("Modell aktualisieren"));
|
||||
refreshBtn.setOnAction(e -> loadAndRender());
|
||||
|
||||
Button exportBtn = new Button("Als PNG exportieren…");
|
||||
Button exportBtn = new Button("⇩");
|
||||
exportBtn.setTooltip(new javafx.scene.control.Tooltip("Als PNG exportieren…"));
|
||||
exportBtn.setOnAction(e -> exportPng());
|
||||
|
||||
fullscreenBtn.setTooltip(new javafx.scene.control.Tooltip("Maximieren"));
|
||||
backBtn.setTooltip(new javafx.scene.control.Tooltip("Minimieren"));
|
||||
|
||||
labelBtn.setStyle("-fx-font-size: 11;");
|
||||
labelBtn.selectedProperty().addListener((obs, o, n) ->
|
||||
canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT));
|
||||
@@ -293,18 +301,20 @@ public class WorldMapView extends VBox {
|
||||
long t0 = System.currentTimeMillis();
|
||||
|
||||
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();
|
||||
bakeVoxelHeights(mapData);
|
||||
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();
|
||||
List<PlacedWaterfall> waterfalls = WaterfallIO.load();
|
||||
float[] voxelSurf = bakeVoxelHeights(mapData);
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne Modell…"));
|
||||
WorldMapRenderModel model = buildRenderModel(mapData, areas, zones, locs, waters, models);
|
||||
log.debug("[WorldMap] Modell fertig – {} Areas, {} Orte, {} Wasser, {} Modelle ({} ms)",
|
||||
WorldMapRenderModel model = buildRenderModel(
|
||||
mapData, areas, zones, locs, waters, models, waterfalls, voxelSurf);
|
||||
log.debug("[WorldMap] Modell fertig – {} Areas, {} Orte, {} Wasser, {} Wasserfälle, {} Modelle ({} ms)",
|
||||
model.areas().size(), model.locations().size(),
|
||||
model.waters().size(), model.models().size(),
|
||||
model.waters().size(), model.waterfalls().size(), model.models().size(),
|
||||
System.currentTimeMillis() - t0);
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
|
||||
@@ -740,55 +750,77 @@ public class WorldMapView extends VBox {
|
||||
|
||||
/**
|
||||
* Liest alle VoxelChunks (aus dem Live-Supplier oder von Disk) und schreibt die
|
||||
* höchsten soliden Voxel-Y-Werte in mapData.upperTop.
|
||||
* höchsten soliden Voxel-Y-Werte in mapData.upperTop (für SeaMask/TerrainSamples)
|
||||
* und gibt zusätzlich ein voxelSurface-Array zurück, das den tatsächlichen
|
||||
* Oberflächen-Y pro UV-Zelle speichert — NaN wo kein Voxel, sonst der echte
|
||||
* Top-Y-Wert (kann auch unter terrainHeight liegen, z. B. bei Canyons).
|
||||
*/
|
||||
private void bakeVoxelHeights(MapData mapData) {
|
||||
private float[] bakeVoxelHeights(MapData mapData) {
|
||||
// voxelSurface bei voller Render-Auflösung (1 Zelle = 1 Welteinheit) backen,
|
||||
// damit keine 8×8-Pixel-Blöcke entstehen.
|
||||
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
|
||||
float[] voxelSurface = new float[VS * VS];
|
||||
java.util.Arrays.fill(voxelSurface, Float.NaN);
|
||||
List<VoxelChunk> chunks = voxelChunkSupplier != null
|
||||
? voxelChunkSupplier.get()
|
||||
: VoxelChunkIO.loadAll();
|
||||
if (chunks.isEmpty()) { return; }
|
||||
int UV = MapData.UPPER_VERTS;
|
||||
// Nach dem Backen sind Live-Chunks geleert → Fallback auf Disk (inkl. .blvc.prebake)
|
||||
boolean allEmpty = chunks.stream().allMatch(VoxelChunk::isEmpty);
|
||||
if (allEmpty) { chunks = VoxelChunkIO.loadAll(); }
|
||||
if (chunks.isEmpty()) { return voxelSurface; }
|
||||
float WH = WorldMapRenderer.WORLD_HALF;
|
||||
float WS = WorldMapRenderer.WORLD_SIZE;
|
||||
int UV = MapData.UPPER_VERTS;
|
||||
for (VoxelChunk chunk : chunks) {
|
||||
if (chunk.isEmpty()) { continue; }
|
||||
for (int lz = 0; lz < VoxelChunk.SIZE; lz++) {
|
||||
float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz);
|
||||
int vsPz = Math.round((worldZ + WH) / WS * (VS - 1));
|
||||
if (vsPz < 0 || vsPz >= VS) { continue; }
|
||||
int uz = Math.round((worldZ + WH) / WS * (UV - 1));
|
||||
if (uz < 0 || uz >= UV) { continue; }
|
||||
for (int lx = 0; lx < VoxelChunk.SIZE; lx++) {
|
||||
float worldX = VoxelChunk.toWorldX(chunk.cx, lx);
|
||||
int vsPx = Math.round((worldX + WH) / WS * (VS - 1));
|
||||
if (vsPx < 0 || vsPx >= VS) { continue; }
|
||||
int ux = Math.round((worldX + WH) / WS * (UV - 1));
|
||||
if (ux < 0 || ux >= UV) { continue; }
|
||||
// Oberste solide Voxel-Y in dieser Spalte suchen
|
||||
for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) {
|
||||
if (chunk.getDensity(lx, ly, lz) > 0) {
|
||||
float topY = VoxelChunk.toWorldY(chunk.cy, ly);
|
||||
int idx = uz * UV + ux;
|
||||
if (topY > mapData.upperTop[idx]) { mapData.upperTop[idx] = topY; }
|
||||
int vsIdx = vsPz * VS + vsPx;
|
||||
if (Float.isNaN(voxelSurface[vsIdx]) || topY > voxelSurface[vsIdx]) {
|
||||
voxelSurface[vsIdx] = topY;
|
||||
}
|
||||
// upperTop auf UV-Raster aktualisieren (für SeaMask / renderRegion)
|
||||
if (uz >= 0 && uz < UV && ux >= 0 && ux < UV) {
|
||||
int uvIdx = uz * UV + ux;
|
||||
if (topY > mapData.upperTop[uvIdx]) { mapData.upperTop[uvIdx] = topY; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return voxelSurface;
|
||||
}
|
||||
|
||||
private WorldMapRenderModel buildRenderModel(
|
||||
MapData mapData,
|
||||
List<PlacedArea> areas, List<PlacedLocationZone> zones,
|
||||
List<Location> locs, List<PlacedWater> waters,
|
||||
List<PlacedModel> models) {
|
||||
List<PlacedModel> models, List<PlacedWaterfall> waterfalls,
|
||||
float[] voxelSurface) {
|
||||
int[] slotColors = computeSlotColors(mapData);
|
||||
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE);
|
||||
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE, voxelSurface);
|
||||
float[] tSamp = buildTerrainSamples(mapData, SEA_MASK_SIZE);
|
||||
List<float[][]> paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE));
|
||||
List<float[][]> paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE, voxelSurface));
|
||||
List<PlacedModel> treeList = models.stream()
|
||||
.filter(WorldMapRenderer::isTree).collect(Collectors.toList());
|
||||
List<List<PlacedModel>> tClusters = clusterTreeModels(treeList, 40f);
|
||||
return new WorldMapRenderModel(
|
||||
mapData, areas, zones, locs, waters, models, slotColors,
|
||||
mask, tSamp, paths, tClusters);
|
||||
mapData, areas, zones, locs, waters, models, waterfalls, slotColors,
|
||||
mask, voxelSurface, tSamp, paths, tClusters);
|
||||
}
|
||||
|
||||
private static float[] buildTerrainSamples(MapData mapData, int size) {
|
||||
@@ -812,19 +844,27 @@ public class WorldMapView extends VBox {
|
||||
|
||||
// Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen.
|
||||
// Endpunkte liegen am echten Höhen-Nulldurchgang → keine Treppenstufen auf Diagonalen.
|
||||
private static float[][] buildCoastSegsMS(MapData mapData, List<PlacedWater> waters, int size) {
|
||||
private static float[][] buildCoastSegsMS(MapData mapData, List<PlacedWater> waters, int size,
|
||||
float[] voxelSurface) {
|
||||
int TV = MapData.TERRAIN_VERTS;
|
||||
int UV = MapData.UPPER_VERTS;
|
||||
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
|
||||
float[] h = new float[size * size];
|
||||
for (int mz = 0; mz < size; mz++) {
|
||||
for (int mx = 0; mx < size; mx++) {
|
||||
int hx = Math.min((int)(mx / (double)(size-1) * (TV-1)), TV-1);
|
||||
int hz = Math.min((int)(mz / (double)(size-1) * (TV-1)), TV-1);
|
||||
int ux = Math.min((int)(mx / (double)(size-1) * (UV-1)), UV-1);
|
||||
int uz = Math.min((int)(mz / (double)(size-1) * (UV-1)), UV-1);
|
||||
float base = mapData.terrainHeight[hz * TV + hx];
|
||||
float upper = mapData.upperTop[uz * UV + ux];
|
||||
h[mz * size + mx] = (upper > 0f && upper > base) ? upper : base;
|
||||
float base = mapData.terrainHeight[hz * TV + hx];
|
||||
// Voxel-Oberfläche hochauflösend abgreifen (falls vorhanden)
|
||||
float vs = Float.NaN;
|
||||
if (voxelSurface != null) {
|
||||
int vsPx = Math.min((int)(mx / (double)(size-1) * (VS-1)), VS-1);
|
||||
int vsPz = Math.min((int)(mz / (double)(size-1) * (VS-1)), VS-1);
|
||||
vs = voxelSurface[vsPz * VS + vsPx];
|
||||
}
|
||||
// Kein upperTop-Fallback: bakeVoxelHeights kontaminiert upperTop auf
|
||||
// UV-Auflösung (8 Welteinheiten/Zelle) → würde Küste verfälschen.
|
||||
// Übereinstimmung mit seaPixels-Logik im Renderer.
|
||||
h[mz * size + mx] = Float.isNaN(vs) ? base : Math.max(base, vs);
|
||||
}
|
||||
}
|
||||
List<float[]> segs = new ArrayList<>();
|
||||
@@ -1092,11 +1132,12 @@ public class WorldMapView extends VBox {
|
||||
cbTerrain.isSelected(),
|
||||
cbTerrain.isSelected(),
|
||||
cbWater.isSelected(),
|
||||
false, // Wellen im Editor über Canvas gezeichnet
|
||||
false, // Wellen im Editor über Canvas gezeichnet
|
||||
false,
|
||||
cbZones.isSelected(),
|
||||
false,
|
||||
cbModels.isSelected()
|
||||
cbModels.isSelected(),
|
||||
cbWaterfalls.isSelected()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1106,11 +1147,12 @@ public class WorldMapView extends VBox {
|
||||
cbTerrain.isSelected(),
|
||||
cbTerrain.isSelected(),
|
||||
cbWater.isSelected(),
|
||||
true, // Wellen im Export-PNG gebacken
|
||||
true, // Wellen im Export-PNG gebacken
|
||||
cbAreas.isSelected(),
|
||||
cbZones.isSelected(),
|
||||
cbLocations.isSelected(),
|
||||
cbModels.isSelected()
|
||||
cbModels.isSelected(),
|
||||
cbWaterfalls.isSelected()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1122,8 +1164,22 @@ public class WorldMapView extends VBox {
|
||||
|
||||
// ── Vollbild ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Passt Scale+Pan an die aktuelle Canvas-Größe an (zentriert, ganz sichtbar). */
|
||||
public void fitCanvas() {
|
||||
double cw = canvasPane.getWidth();
|
||||
double ch = canvasPane.getHeight();
|
||||
if (cw <= 0 || ch <= 0 || mapFxImage == null) return;
|
||||
scale = Math.min(cw / RENDER_SIZE, ch / RENDER_SIZE);
|
||||
panX = (cw - RENDER_SIZE * scale) / 2.0;
|
||||
panY = (ch - RENDER_SIZE * scale) / 2.0;
|
||||
redraw();
|
||||
}
|
||||
|
||||
private void enterFullscreen() {
|
||||
isFullscreen = true;
|
||||
savedScale = scale;
|
||||
savedPanX = panX;
|
||||
savedPanY = panY;
|
||||
fullscreenBtn.setVisible(false);
|
||||
backBtn.setVisible(true);
|
||||
if (enterFullscreenCallback != null) {
|
||||
@@ -1133,6 +1189,9 @@ public class WorldMapView extends VBox {
|
||||
|
||||
private void exitFullscreen() {
|
||||
isFullscreen = false;
|
||||
scale = savedScale;
|
||||
panX = savedPanX;
|
||||
panY = savedPanY;
|
||||
backBtn.setVisible(false);
|
||||
fullscreenBtn.setVisible(true);
|
||||
if (exitFullscreenCallback != null) {
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
<!-- Karten-Rendering: Modell-Build und PNG-Render auf DEBUG -->
|
||||
<logger name="de.blight.editor.ui.WorldMapView" level="DEBUG"/>
|
||||
<!-- Weltkarte-Vollbild: Layout-Debugging -->
|
||||
<logger name="de.blight.editor.EditorApp" level="DEBUG"/>
|
||||
|
||||
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
||||
<logger name="com.jme3" level="WARN"/>
|
||||
|
||||
Reference in New Issue
Block a user