Weltkarte: Render-Modell-Trennung, UI-Umbau und Kamera-Indikator
- WorldMapRenderModel: neues Record als Zwischenschicht zwischen I/O und Rendering - WorldMapRenderer: render() akzeptiert Modell direkt; rückwärtskompatible Wrapper-Signatur für MinimapState - WorldMapView: Layer-Auswahl als MenuButton mit Checkboxen; Vollbild-Modus (centerStack-Swap); Kamera-Indikator mit FOV-Kegel (120ms-Timer, unabhängig vom Karten-Rendering); Linksklick teleportiert JME3-Kamera unter Beibehaltung der Terrain-Höhendifferenz - EditorApp: CameraInfoSupplier, TeleportCallback und FullscreenCallbacks verdrahtet - MinimapState, Logback: Debug-Logging für Modell-Build und Render-Zyklen - Map-Daten: Zwischenstand Weltdaten Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 73 KiB |
@@ -0,0 +1,27 @@
|
|||||||
|
package de.blight.common.map;
|
||||||
|
|
||||||
|
import de.blight.common.*;
|
||||||
|
import de.blight.common.model.Location;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fertig vorberechnetes Karten-Modell: enthält alle Rohdaten + die im Editor
|
||||||
|
* vorcompilierten Overlay-Informationen (SeaMask, Küstenpfade, Baum-Cluster).
|
||||||
|
* WorldMapRenderer.render() arbeitet nur noch gegen dieses Modell.
|
||||||
|
*/
|
||||||
|
public record WorldMapRenderModel(
|
||||||
|
// Terrain-Daten + Polygon-Objekte (werden vom Renderer für das PNG genutzt)
|
||||||
|
MapData mapData,
|
||||||
|
List<PlacedArea> areas,
|
||||||
|
List<PlacedLocationZone> zones,
|
||||||
|
List<Location> locations,
|
||||||
|
List<PlacedWater> waters,
|
||||||
|
List<PlacedModel> models,
|
||||||
|
int[] slotColorsRGB,
|
||||||
|
// Vorberechnete Overlay-Daten (werden vom Canvas genutzt, nicht vom PNG-Renderer)
|
||||||
|
boolean[] seaMask, // Wasser-Pixel-Maske bei SEA_MASK_SIZE-Auflösung
|
||||||
|
float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung)
|
||||||
|
List<float[][]> coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.)
|
||||||
|
List<List<PlacedModel>> treeClusters
|
||||||
|
) {}
|
||||||
@@ -62,17 +62,25 @@ public final class WorldMapRenderer {
|
|||||||
|
|
||||||
public static boolean[] buildSeaMask(MapData m, int size) {
|
public static boolean[] buildSeaMask(MapData m, int size) {
|
||||||
int TV = MapData.TERRAIN_VERTS;
|
int TV = MapData.TERRAIN_VERTS;
|
||||||
|
int UV = MapData.UPPER_VERTS;
|
||||||
boolean[] mask = new boolean[size * size];
|
boolean[] mask = new boolean[size * size];
|
||||||
for (int py = 0; py < size; py++) {
|
for (int py = 0; py < size; py++) {
|
||||||
for (int px = 0; px < size; px++) {
|
for (int px = 0; px < size; px++) {
|
||||||
int hx = Math.min((int)((float) px / (size - 1) * (TV - 1)), TV - 1);
|
int hx = Math.min((int)((float) px / (size - 1) * (TV - 1)), TV - 1);
|
||||||
int hz = Math.min((int)((float) py / (size - 1) * (TV - 1)), TV - 1);
|
int hz = Math.min((int)((float) py / (size - 1) * (TV - 1)), TV - 1);
|
||||||
mask[py * size + px] = m.terrainHeight[hz * TV + hx] < 0f;
|
int ux = Math.min((int)((float) px / (size - 1) * (UV - 1)), UV - 1);
|
||||||
|
int uz = Math.min((int)((float) py / (size - 1) * (UV - 1)), UV - 1);
|
||||||
|
float h = m.terrainHeight[hz * TV + hx];
|
||||||
|
float upper = m.upperTop[uz * UV + ux];
|
||||||
|
if (upper > 0f && upper > h) { h = upper; }
|
||||||
|
mask[py * size + px] = h < 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final int WATER_COLOR = 0xFF_ADD8E6;
|
||||||
|
|
||||||
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
|
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
|
||||||
private static final int[] DEF_SLOT_R = { 71, 115, 140, 204, 90, 130, 110, 180 };
|
private static final int[] DEF_SLOT_R = { 71, 115, 140, 204, 90, 130, 110, 180 };
|
||||||
private static final int[] DEF_SLOT_G = { 148, 82, 115, 184, 80, 90, 60, 100 };
|
private static final int[] DEF_SLOT_G = { 148, 82, 115, 184, 80, 90, 60, 100 };
|
||||||
@@ -80,15 +88,17 @@ public final class WorldMapRenderer {
|
|||||||
|
|
||||||
private WorldMapRenderer() {}
|
private WorldMapRenderer() {}
|
||||||
|
|
||||||
public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) {
|
/** Hauptmethode: rendert das Hintergrund-PNG aus einem vorberechneten Modell. */
|
||||||
MapData m = input.mapData();
|
public static BufferedImage render(WorldMapRenderModel model, int targetSize, RenderOptions opts) {
|
||||||
|
MapData m = model.mapData();
|
||||||
int TV = MapData.TERRAIN_VERTS;
|
int TV = MapData.TERRAIN_VERTS;
|
||||||
int SS = MapData.SPLAT_SIZE;
|
int SS = MapData.SPLAT_SIZE;
|
||||||
int[] slotR = slotChannel(input, 0);
|
int[] slotR = slotChannel(model.slotColorsRGB(), 0);
|
||||||
int[] slotG = slotChannel(input, 1);
|
int[] slotG = slotChannel(model.slotColorsRGB(), 1);
|
||||||
int[] slotB = slotChannel(input, 2);
|
int[] slotB = slotChannel(model.slotColorsRGB(), 2);
|
||||||
|
|
||||||
// ── 1. Heightmap auf Zielauflösung samplen ────────────────────────────
|
// ── 1. Heightmap auf Zielauflösung samplen ────────────────────────────
|
||||||
|
int UV = MapData.UPPER_VERTS;
|
||||||
float[] heights = new float[targetSize * targetSize];
|
float[] heights = new float[targetSize * targetSize];
|
||||||
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
|
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
|
||||||
|
|
||||||
@@ -96,7 +106,11 @@ public final class WorldMapRenderer {
|
|||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
int hx = Math.min((int)((float) px / (targetSize - 1) * (TV - 1)), TV - 1);
|
int hx = Math.min((int)((float) px / (targetSize - 1) * (TV - 1)), TV - 1);
|
||||||
int hz = Math.min((int)((float) py / (targetSize - 1) * (TV - 1)), TV - 1);
|
int hz = Math.min((int)((float) py / (targetSize - 1) * (TV - 1)), TV - 1);
|
||||||
|
int ux = Math.min((int)((float) px / (targetSize - 1) * (UV - 1)), UV - 1);
|
||||||
|
int uz = Math.min((int)((float) py / (targetSize - 1) * (UV - 1)), UV - 1);
|
||||||
float h = m.terrainHeight[hz * TV + hx];
|
float h = m.terrainHeight[hz * TV + hx];
|
||||||
|
float upper = m.upperTop[uz * UV + ux];
|
||||||
|
if (upper > 0f && upper > h) { h = upper; }
|
||||||
heights[py * targetSize + px] = h;
|
heights[py * targetSize + px] = h;
|
||||||
if (h < minH) minH = h;
|
if (h < minH) minH = h;
|
||||||
if (h > maxH) maxH = h;
|
if (h > maxH) maxH = h;
|
||||||
@@ -161,6 +175,9 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kuwahara-Filter: lässt das Terrain wie gemalt wirken
|
||||||
|
if (opts.showSplatColors()) { applyKuwahara(img, 3); }
|
||||||
|
|
||||||
// ── 3. Vektor-Overlays ────────────────────────────────────────────────
|
// ── 3. Vektor-Overlays ────────────────────────────────────────────────
|
||||||
Graphics2D gfx = img.createGraphics();
|
Graphics2D gfx = img.createGraphics();
|
||||||
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
@@ -171,10 +188,10 @@ public final class WorldMapRenderer {
|
|||||||
// ── Weißfüllung ───────────────────────────────────────────────────────
|
// ── Weißfüllung ───────────────────────────────────────────────────────
|
||||||
for (int py = 0; py < targetSize; py++) {
|
for (int py = 0; py < targetSize; py++) {
|
||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF);
|
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : model.waters()) {
|
||||||
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
Polygon poly = new Polygon(xs, ys, xs.length);
|
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||||
@@ -186,7 +203,7 @@ public final class WorldMapRenderer {
|
|||||||
for (int py = y0; py <= y1; py++) {
|
for (int py = y0; py <= y1; py++) {
|
||||||
for (int px = x0; px <= x1; px++) {
|
for (int px = x0; px <= x1; px++) {
|
||||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||||
img.setRGB(px, py, 0xFFFFFFFF);
|
img.setRGB(px, py, WATER_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +236,7 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Wasserflächen: nur wenn groß genug
|
// Wasserflächen: nur wenn groß genug
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : model.waters()) {
|
||||||
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
Polygon poly = new Polygon(xs, ys, xs.length);
|
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||||
@@ -250,7 +267,7 @@ public final class WorldMapRenderer {
|
|||||||
gfx.setColor(Color.BLACK);
|
gfx.setColor(Color.BLACK);
|
||||||
gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
// Wasserflächen: Polygon-Umriss
|
// Wasserflächen: Polygon-Umriss
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : model.waters()) {
|
||||||
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
gfx.drawPolygon(xs, ys, xs.length);
|
gfx.drawPolygon(xs, ys, xs.length);
|
||||||
@@ -293,7 +310,7 @@ public final class WorldMapRenderer {
|
|||||||
10f, new float[]{dash, gap}, 0f);
|
10f, new float[]{dash, gap}, 0f);
|
||||||
int aFontSize = Math.max(8, targetSize / 160);
|
int aFontSize = Math.max(8, targetSize / 160);
|
||||||
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
|
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
|
||||||
for (PlacedArea a : input.areas()) {
|
for (PlacedArea a : model.areas()) {
|
||||||
int[] xs = worldToPixels(a.pointsX(), targetSize);
|
int[] xs = worldToPixels(a.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(a.pointsZ(), targetSize);
|
int[] ys = worldToPixels(a.pointsZ(), targetSize);
|
||||||
gfx.setStroke(dashed);
|
gfx.setStroke(dashed);
|
||||||
@@ -314,7 +331,7 @@ public final class WorldMapRenderer {
|
|||||||
// Location-Zonen
|
// Location-Zonen
|
||||||
if (opts.showZones()) {
|
if (opts.showZones()) {
|
||||||
gfx.setStroke(new BasicStroke(lineW));
|
gfx.setStroke(new BasicStroke(lineW));
|
||||||
for (PlacedLocationZone z : input.zones()) {
|
for (PlacedLocationZone z : model.zones()) {
|
||||||
int[] xs = worldToPixels(z.pointsX(), targetSize);
|
int[] xs = worldToPixels(z.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(z.pointsZ(), targetSize);
|
int[] ys = worldToPixels(z.pointsZ(), targetSize);
|
||||||
gfx.setColor(new Color(240, 190, 40, 70));
|
gfx.setColor(new Color(240, 190, 40, 70));
|
||||||
@@ -324,13 +341,13 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modell-Punkte
|
|
||||||
if (opts.showModels()) {
|
if (opts.showModels()) {
|
||||||
int dotR = Math.max(1, targetSize / 600);
|
int dotR = Math.max(1, targetSize / 600);
|
||||||
gfx.setColor(new Color(160, 80, 20, 200));
|
gfx.setColor(new Color(160, 80, 20, 200));
|
||||||
for (PlacedModel model : input.models()) {
|
for (PlacedModel pm : model.models()) {
|
||||||
int mx = worldToPixel(model.x(), targetSize);
|
if (isTree(pm)) { continue; }
|
||||||
int mz = worldToPixel(model.z(), targetSize);
|
int mx = worldToPixel(pm.x(), targetSize);
|
||||||
|
int mz = worldToPixel(pm.z(), targetSize);
|
||||||
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +356,7 @@ public final class WorldMapRenderer {
|
|||||||
if (opts.showLocations()) {
|
if (opts.showLocations()) {
|
||||||
int fontSize = Math.max(8, targetSize / 140);
|
int fontSize = Math.max(8, targetSize / 140);
|
||||||
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
||||||
for (Location loc : input.locations()) {
|
for (Location loc : model.locations()) {
|
||||||
if (!loc.isShowOnMap()) continue;
|
if (!loc.isShowOnMap()) continue;
|
||||||
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
||||||
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
|
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
|
||||||
@@ -354,6 +371,15 @@ public final class WorldMapRenderer {
|
|||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rückwärts-kompatibel: baut ein minimales Modell (ohne Overlay-Daten) und delegiert. */
|
||||||
|
public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) {
|
||||||
|
return render(new WorldMapRenderModel(
|
||||||
|
input.mapData(), input.areas(), input.zones(), input.locations(),
|
||||||
|
input.waters(), input.models(), input.slotColorsRGB(),
|
||||||
|
null, null, null, null
|
||||||
|
), targetSize, opts);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rendert einen rechteckigen Weltausschnitt als {@link BufferedImage}.
|
* Rendert einen rechteckigen Weltausschnitt als {@link BufferedImage}.
|
||||||
* Koordinatenursprung und Skalierung passen sich dem Ausschnitt an,
|
* Koordinatenursprung und Skalierung passen sich dem Ausschnitt an,
|
||||||
@@ -383,16 +409,21 @@ public final class WorldMapRenderer {
|
|||||||
float minH = 0f, maxH = 1f;
|
float minH = 0f, maxH = 1f;
|
||||||
|
|
||||||
if (opts.showTerrain() || opts.showWater()) {
|
if (opts.showTerrain() || opts.showWater()) {
|
||||||
|
int UV2 = MapData.UPPER_VERTS;
|
||||||
heights = new float[targetSize * targetSize];
|
heights = new float[targetSize * targetSize];
|
||||||
minH = Float.MAX_VALUE;
|
minH = Float.MAX_VALUE;
|
||||||
maxH = -Float.MAX_VALUE;
|
maxH = -Float.MAX_VALUE;
|
||||||
for (int py = 0; py < targetSize; py++) {
|
for (int py = 0; py < targetSize; py++) {
|
||||||
float wz = wz0 + (float) py / (targetSize - 1) * rSize;
|
float wz = wz0 + (float) py / (targetSize - 1) * rSize;
|
||||||
int hz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
int hz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
||||||
|
int uz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (UV2 - 1)), 0, UV2 - 1);
|
||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
float wx = wx0 + (float) px / (targetSize - 1) * rSize;
|
float wx = wx0 + (float) px / (targetSize - 1) * rSize;
|
||||||
int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
||||||
|
int ux = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (UV2 - 1)), 0, UV2 - 1);
|
||||||
float h = m.terrainHeight[hz * TV + hx];
|
float h = m.terrainHeight[hz * TV + hx];
|
||||||
|
float upper = m.upperTop[uz * UV2 + ux];
|
||||||
|
if (upper > 0f && upper > h) { h = upper; }
|
||||||
heights[py * targetSize + px] = h;
|
heights[py * targetSize + px] = h;
|
||||||
if (h < minH) { minH = h; }
|
if (h < minH) { minH = h; }
|
||||||
if (h > maxH) { maxH = h; }
|
if (h > maxH) { maxH = h; }
|
||||||
@@ -403,9 +434,9 @@ public final class WorldMapRenderer {
|
|||||||
// ── Terrain (optional) ────────────────────────────────────────────────
|
// ── Terrain (optional) ────────────────────────────────────────────────
|
||||||
if (opts.showTerrain()) {
|
if (opts.showTerrain()) {
|
||||||
float heightRange = Math.max(0.01f, maxH - minH);
|
float heightRange = Math.max(0.01f, maxH - minH);
|
||||||
int[] sR = slotChannel(input, 0);
|
int[] sR = slotChannel(input.slotColorsRGB(), 0);
|
||||||
int[] sG = slotChannel(input, 1);
|
int[] sG = slotChannel(input.slotColorsRGB(), 1);
|
||||||
int[] sB = slotChannel(input, 2);
|
int[] sB = slotChannel(input.slotColorsRGB(), 2);
|
||||||
|
|
||||||
for (int py = 0; py < targetSize; py++) {
|
for (int py = 0; py < targetSize; py++) {
|
||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
@@ -465,6 +496,8 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.showSplatColors()) { applyKuwahara(img, 3); }
|
||||||
|
|
||||||
// ── Vektor-Overlays ───────────────────────────────────────────────────
|
// ── Vektor-Overlays ───────────────────────────────────────────────────
|
||||||
Graphics2D gfx = img.createGraphics();
|
Graphics2D gfx = img.createGraphics();
|
||||||
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
@@ -474,7 +507,7 @@ public final class WorldMapRenderer {
|
|||||||
// ── Weißfüllung ───────────────────────────────────────────────────────
|
// ── Weißfüllung ───────────────────────────────────────────────────────
|
||||||
for (int py = 0; py < targetSize; py++) {
|
for (int py = 0; py < targetSize; py++) {
|
||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF);
|
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : input.waters()) {
|
||||||
@@ -489,7 +522,7 @@ public final class WorldMapRenderer {
|
|||||||
for (int py = y0; py <= y1; py++) {
|
for (int py = y0; py <= y1; py++) {
|
||||||
for (int px = x0; px <= x1; px++) {
|
for (int px = x0; px <= x1; px++) {
|
||||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||||
img.setRGB(px, py, 0xFFFFFFFF);
|
img.setRGB(px, py, WATER_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,6 +662,7 @@ public final class WorldMapRenderer {
|
|||||||
int dotR = Math.max(1, targetSize / 600);
|
int dotR = Math.max(1, targetSize / 600);
|
||||||
gfx.setColor(new Color(160, 80, 20, 200));
|
gfx.setColor(new Color(160, 80, 20, 200));
|
||||||
for (PlacedModel model : input.models()) {
|
for (PlacedModel model : input.models()) {
|
||||||
|
if (isTree(model)) { continue; }
|
||||||
int mx = wrp1(model.x(), wx0, rSize, targetSize);
|
int mx = wrp1(model.x(), wx0, rSize, targetSize);
|
||||||
int mz = wrp1(model.z(), wz0, rSize, targetSize);
|
int mz = wrp1(model.z(), wz0, rSize, targetSize);
|
||||||
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
||||||
@@ -680,13 +714,12 @@ public final class WorldMapRenderer {
|
|||||||
|
|
||||||
// Gibt den R-, G- oder B-Kanal (channel=0/1/2) aller 8 Splatmap-Slots zurück.
|
// Gibt den R-, G- oder B-Kanal (channel=0/1/2) aller 8 Splatmap-Slots zurück.
|
||||||
// slotColorsRGB: 24 Werte (8 Slots × 3), 12 Werte (4 Slots, Upper-Layer = Defaults) oder null.
|
// slotColorsRGB: 24 Werte (8 Slots × 3), 12 Werte (4 Slots, Upper-Layer = Defaults) oder null.
|
||||||
private static int[] slotChannel(RenderInput input, int channel) {
|
private static int[] slotChannel(int[] slotColorsRGB, int channel) {
|
||||||
int[] rgb = input.slotColorsRGB();
|
|
||||||
int[] def = channel == 0 ? DEF_SLOT_R : (channel == 1 ? DEF_SLOT_G : DEF_SLOT_B);
|
int[] def = channel == 0 ? DEF_SLOT_R : (channel == 1 ? DEF_SLOT_G : DEF_SLOT_B);
|
||||||
if (rgb == null || rgb.length < 12) { return def; }
|
if (slotColorsRGB == null || slotColorsRGB.length < 12) { return def; }
|
||||||
int[] out = new int[8];
|
int[] out = new int[8];
|
||||||
for (int s = 0; s < 8; s++) {
|
for (int s = 0; s < 8; s++) {
|
||||||
out[s] = (rgb.length >= (s + 1) * 3) ? rgb[s * 3 + channel] : def[s];
|
out[s] = (slotColorsRGB.length >= (s + 1) * 3) ? slotColorsRGB[s * 3 + channel] : def[s];
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -802,4 +835,53 @@ public final class WorldMapRenderer {
|
|||||||
float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f;
|
float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f;
|
||||||
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
|
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isTree(PlacedModel m) {
|
||||||
|
return m.modelPath().replace('\\', '/').toLowerCase().contains("/trees/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kuwahara-Filter: Für jeden Pixel das Quadranten-Fenster mit der kleinsten
|
||||||
|
* Varianz wählen und dessen Mittelwert setzen → Ölgemälde-/gemalt-Effekt.
|
||||||
|
* Radius r=3 → Fenster 7×7, 4 Quadranten je 4×4.
|
||||||
|
*/
|
||||||
|
private static void applyKuwahara(BufferedImage img, int r) {
|
||||||
|
int w = img.getWidth(), h = img.getHeight();
|
||||||
|
int[] src = img.getRGB(0, 0, w, h, null, 0, w);
|
||||||
|
int[] dst = new int[src.length];
|
||||||
|
|
||||||
|
for (int y = 0; y < h; y++) {
|
||||||
|
for (int x = 0; x < w; x++) {
|
||||||
|
float bestVar = Float.MAX_VALUE;
|
||||||
|
int bestPacked = src[y * w + x];
|
||||||
|
|
||||||
|
// 4 Quadranten: [xOff0..xOff1] × [yOff0..yOff1]
|
||||||
|
int[][] quads = {{-r,-r,0,0},{0,-r,r,0},{-r,0,0,r},{0,0,r,r}};
|
||||||
|
for (int[] q : quads) {
|
||||||
|
int x0 = Math.max(0, x+q[0]), y0 = Math.max(0, y+q[1]);
|
||||||
|
int x1 = Math.min(w-1, x+q[2]), y1 = Math.min(h-1, y+q[3]);
|
||||||
|
float sumR=0,sumG=0,sumB=0, sum2R=0,sum2G=0,sum2B=0;
|
||||||
|
int cnt = 0;
|
||||||
|
for (int qy = y0; qy <= y1; qy++) {
|
||||||
|
for (int qx = x0; qx <= x1; qx++) {
|
||||||
|
int p = src[qy * w + qx];
|
||||||
|
float pr = (p>>16)&0xFF, pg = (p>>8)&0xFF, pb = p&0xFF;
|
||||||
|
sumR+=pr; sumG+=pg; sumB+=pb;
|
||||||
|
sum2R+=pr*pr; sum2G+=pg*pg; sum2B+=pb*pb;
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float inv = 1f / cnt;
|
||||||
|
float mR=sumR*inv, mG=sumG*inv, mB=sumB*inv;
|
||||||
|
float var = (sum2R*inv - mR*mR) + (sum2G*inv - mG*mG) + (sum2B*inv - mB*mB);
|
||||||
|
if (var < bestVar) {
|
||||||
|
bestVar = var;
|
||||||
|
bestPacked = (clamp((int)mR)<<16) | (clamp((int)mG)<<8) | clamp((int)mB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst[y * w + x] = bestPacked | 0xFF000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img.setRGB(0, 0, w, h, dst, 0, w);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5639,11 +5639,36 @@ public class EditorApp extends Application {
|
|||||||
});
|
});
|
||||||
|
|
||||||
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
|
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
|
||||||
Tab weltkartTab = new Tab("Weltkarte", worldMapView);
|
worldMapView.setVoxelChunkSupplier(() -> {
|
||||||
|
de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null
|
||||||
|
: jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class);
|
||||||
|
return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll();
|
||||||
|
});
|
||||||
|
worldMapView.setCameraInfoSupplier(() ->
|
||||||
|
new de.blight.editor.ui.WorldMapView.CameraInfo(
|
||||||
|
input.camX, input.camY, input.camZ, input.camYaw));
|
||||||
|
worldMapView.setTeleportCallback(pos -> {
|
||||||
|
input.pendingGotoX = pos[0];
|
||||||
|
input.pendingGotoY = pos[1];
|
||||||
|
input.pendingGotoZ = pos[2];
|
||||||
|
});
|
||||||
|
|
||||||
|
final Tab weltkartTab = new Tab("Weltkarte", worldMapView);
|
||||||
weltkartTab.setClosable(false);
|
weltkartTab.setClosable(false);
|
||||||
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||||
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
|
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);
|
||||||
|
},
|
||||||
|
() -> {
|
||||||
|
// Erst aus centerStack lösen (worldViewport zurück), dann in Tab setzen
|
||||||
|
setCenterView(worldViewport);
|
||||||
|
weltkartTab.setContent(worldMapView);
|
||||||
|
});
|
||||||
|
|
||||||
TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab);
|
TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab);
|
||||||
tabPane.setStyle("-fx-background-color: #e8e8e8;");
|
tabPane.setStyle("-fx-background-color: #e8e8e8;");
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ package de.blight.editor.ui;
|
|||||||
import de.blight.common.*;
|
import de.blight.common.*;
|
||||||
import de.blight.common.model.Location;
|
import de.blight.common.model.Location;
|
||||||
import de.blight.common.map.WorldMapRenderer;
|
import de.blight.common.map.WorldMapRenderer;
|
||||||
import de.blight.common.map.WorldMapRenderer.RenderInput;
|
|
||||||
import de.blight.common.map.WorldMapRenderer.RenderOptions;
|
import de.blight.common.map.WorldMapRenderer.RenderOptions;
|
||||||
|
import de.blight.common.map.WorldMapRenderModel;
|
||||||
|
|
||||||
|
import javafx.animation.Animation;
|
||||||
|
import javafx.animation.KeyFrame;
|
||||||
|
import javafx.animation.Timeline;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.embed.swing.SwingFXUtils;
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
|
import javafx.util.Duration;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
@@ -15,6 +19,7 @@ import javafx.scene.canvas.Canvas;
|
|||||||
import javafx.scene.canvas.GraphicsContext;
|
import javafx.scene.canvas.GraphicsContext;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.image.WritableImage;
|
import javafx.scene.image.WritableImage;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.shape.StrokeLineCap;
|
import javafx.scene.shape.StrokeLineCap;
|
||||||
@@ -32,10 +37,21 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import javafx.geometry.VPos;
|
||||||
|
import javafx.scene.text.TextAlignment;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interaktive 2D-Weltkarte im Editor-Tab.
|
* Interaktive 2D-Weltkarte im Editor-Tab.
|
||||||
@@ -46,6 +62,8 @@ import java.util.function.Supplier;
|
|||||||
*/
|
*/
|
||||||
public class WorldMapView extends VBox {
|
public class WorldMapView extends VBox {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WorldMapView.class);
|
||||||
|
|
||||||
private static final int RENDER_SIZE = 2048;
|
private static final int RENDER_SIZE = 2048;
|
||||||
|
|
||||||
// Konstante Bildschirmgrößen für das Canvas-Overlay
|
// Konstante Bildschirmgrößen für das Canvas-Overlay
|
||||||
@@ -63,30 +81,57 @@ public class WorldMapView extends VBox {
|
|||||||
private final ProgressBar progress = new ProgressBar(-1);
|
private final ProgressBar progress = new ProgressBar(-1);
|
||||||
private final StackPane canvasPane = new StackPane(canvas);
|
private final StackPane canvasPane = new StackPane(canvas);
|
||||||
|
|
||||||
private final ToggleButton layerTerrain = layerBtn("Gelände");
|
// Layer-Checkboxen (im MenuButton gebündelt)
|
||||||
private final ToggleButton layerWater = layerBtn("Wasser");
|
private final CheckBox cbTerrain = layerCheck("Gelände");
|
||||||
private final ToggleButton layerAreas = layerBtn("Areas");
|
private final CheckBox cbWater = layerCheck("Wasser");
|
||||||
private final ToggleButton layerZones = layerBtn("Zonen");
|
private final CheckBox cbAreas = layerCheck("Areas");
|
||||||
private final ToggleButton layerLocations = layerBtn("Orte");
|
private final CheckBox cbZones = layerCheck("Zonen");
|
||||||
private final ToggleButton layerModels = layerBtn("Modelle");
|
private final CheckBox cbLocations = layerCheck("Orte");
|
||||||
|
private final CheckBox cbModels = layerCheck("Modelle");
|
||||||
private final ToggleButton labelBtn = new ToggleButton("Label");
|
private final ToggleButton labelBtn = new ToggleButton("Label");
|
||||||
|
|
||||||
|
private final Button fullscreenBtn = new Button("⤢ Vollbild");
|
||||||
|
private final Button backBtn = new Button("< Zurück");
|
||||||
|
private boolean isFullscreen = false;
|
||||||
|
|
||||||
|
/** Kamera-Zustand (Position + Blickrichtung) für die Karten-Überlagerung. */
|
||||||
|
public record CameraInfo(float x, float y, float z, float yawDeg) {}
|
||||||
|
|
||||||
private WritableImage mapFxImage;
|
private WritableImage mapFxImage;
|
||||||
private BufferedImage mapBuffered;
|
private BufferedImage mapBuffered;
|
||||||
|
|
||||||
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
||||||
private List<Location> cachedLocs = new ArrayList<>();
|
private List<Location> cachedLocs = new ArrayList<>();
|
||||||
private List<PlacedWater> cachedWaters = new ArrayList<>();
|
private WorldMapRenderModel currentModel = null;
|
||||||
private boolean[] seaMask = null;
|
private boolean autoLoaded = false;
|
||||||
private float[][] seaCoastSegs = null; // [wx1,wz1,wx2,wz2] in Weltkoordinaten
|
|
||||||
|
|
||||||
private double panX = 0, panY = 0;
|
private double panX = 0, panY = 0;
|
||||||
private double scale = 1.0;
|
private double scale = 1.0;
|
||||||
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
|
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
|
||||||
|
|
||||||
private final Supplier<Stage> stageSupplier;
|
private final Supplier<Stage> stageSupplier;
|
||||||
|
private Supplier<List<VoxelChunk>> voxelChunkSupplier = null;
|
||||||
|
private Supplier<CameraInfo> cameraInfoSupplier = null;
|
||||||
|
private Consumer<float[]> teleportCallback = null; // [worldX, worldY, worldZ]
|
||||||
|
private Runnable enterFullscreenCallback = null;
|
||||||
|
private Runnable exitFullscreenCallback = null;
|
||||||
private final AtomicBoolean loading = new AtomicBoolean(false);
|
private final AtomicBoolean loading = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
/** Verknüpft den Live-Voxel-Chunk-Zustand des Editors mit der Kartenansicht. */
|
||||||
|
public void setVoxelChunkSupplier(Supplier<List<VoxelChunk>> s) { this.voxelChunkSupplier = s; }
|
||||||
|
|
||||||
|
/** Liefert aktuelle Kamera-Position und Blickrichtung für die Kartenanzeige. */
|
||||||
|
public void setCameraInfoSupplier(Supplier<CameraInfo> s) { cameraInfoSupplier = s; }
|
||||||
|
|
||||||
|
/** Callback für Kamera-Teleport per Linksklick; erhält [worldX, worldY, worldZ]. */
|
||||||
|
public void setTeleportCallback(Consumer<float[]> cb) { teleportCallback = cb; }
|
||||||
|
|
||||||
|
/** Callbacks für Vollbild-Modus ein/aus (typisch: Eltern-Layout anpassen). */
|
||||||
|
public void setFullscreenCallbacks(Runnable enter, Runnable exit) {
|
||||||
|
enterFullscreenCallback = enter;
|
||||||
|
exitFullscreenCallback = exit;
|
||||||
|
}
|
||||||
|
|
||||||
private static final String[] ASSET_BASES = {
|
private static final String[] ASSET_BASES = {
|
||||||
"blight-assets/src/main/resources",
|
"blight-assets/src/main/resources",
|
||||||
"../blight-assets/src/main/resources",
|
"../blight-assets/src/main/resources",
|
||||||
@@ -119,7 +164,23 @@ public class WorldMapView extends VBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void buildUi() {
|
private void buildUi() {
|
||||||
Button refreshBtn = new Button("Aktualisieren");
|
// ── Layer-Auswahl als MenuButton mit Checkboxen ───────────────────────
|
||||||
|
cbTerrain.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
|
||||||
|
cbWater.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}) {
|
||||||
|
CustomMenuItem item = new CustomMenuItem(cb, false);
|
||||||
|
item.setHideOnClick(false);
|
||||||
|
layerMenu.getItems().add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Buttons ───────────────────────────────────────────────────────────
|
||||||
|
Button refreshBtn = new Button("↻ Modell aktualisieren");
|
||||||
refreshBtn.setOnAction(e -> loadAndRender());
|
refreshBtn.setOnAction(e -> loadAndRender());
|
||||||
|
|
||||||
Button exportBtn = new Button("Als PNG exportieren…");
|
Button exportBtn = new Button("Als PNG exportieren…");
|
||||||
@@ -129,15 +190,21 @@ public class WorldMapView extends VBox {
|
|||||||
labelBtn.selectedProperty().addListener((obs, o, n) ->
|
labelBtn.selectedProperty().addListener((obs, o, n) ->
|
||||||
canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT));
|
canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT));
|
||||||
|
|
||||||
|
fullscreenBtn.setOnAction(e -> enterFullscreen());
|
||||||
|
backBtn.setOnAction(e -> exitFullscreen());
|
||||||
|
backBtn.setVisible(false);
|
||||||
|
|
||||||
ToolBar toolbar = new ToolBar(
|
ToolBar toolbar = new ToolBar(
|
||||||
new Label("Layer:"),
|
layerMenu,
|
||||||
layerTerrain, layerWater, layerAreas, layerZones, layerLocations, layerModels,
|
|
||||||
new Separator(),
|
new Separator(),
|
||||||
new Label("Bearbeiten:"),
|
new Label("Bearbeiten:"),
|
||||||
labelBtn,
|
labelBtn,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
refreshBtn,
|
refreshBtn,
|
||||||
exportBtn
|
exportBtn,
|
||||||
|
new Separator(),
|
||||||
|
fullscreenBtn,
|
||||||
|
backBtn
|
||||||
);
|
);
|
||||||
|
|
||||||
canvasPane.setStyle("-fx-background-color: #1a1a2a;");
|
canvasPane.setStyle("-fx-background-color: #1a1a2a;");
|
||||||
@@ -148,16 +215,6 @@ public class WorldMapView extends VBox {
|
|||||||
canvas.widthProperty().addListener(obs -> redraw());
|
canvas.widthProperty().addListener(obs -> redraw());
|
||||||
canvas.heightProperty().addListener(obs -> redraw());
|
canvas.heightProperty().addListener(obs -> redraw());
|
||||||
|
|
||||||
// Terrain/Wasser/Zonen/Modelle → PNG neu rendern
|
|
||||||
layerTerrain.setOnAction(e -> rerender());
|
|
||||||
layerWater.setOnAction(e -> rerender());
|
|
||||||
layerZones.setOnAction(e -> rerender());
|
|
||||||
layerModels.setOnAction(e -> rerender());
|
|
||||||
|
|
||||||
// Areas und Locations → nur Canvas-Overlay neu zeichnen (kein PNG-Rerender)
|
|
||||||
layerAreas.setOnAction(e -> redraw());
|
|
||||||
layerLocations.setOnAction(e -> redraw());
|
|
||||||
|
|
||||||
canvas.setOnMousePressed(e -> {
|
canvas.setOnMousePressed(e -> {
|
||||||
dragStartX = e.getX();
|
dragStartX = e.getX();
|
||||||
dragStartY = e.getY();
|
dragStartY = e.getY();
|
||||||
@@ -170,9 +227,14 @@ public class WorldMapView extends VBox {
|
|||||||
redraw();
|
redraw();
|
||||||
});
|
});
|
||||||
canvas.setOnMouseReleased(e -> {
|
canvas.setOnMouseReleased(e -> {
|
||||||
if (!labelBtn.isSelected()) return;
|
boolean wasDrag = Math.abs(e.getX() - dragStartX) >= 5
|
||||||
if (Math.abs(e.getX() - dragStartX) < 5 && Math.abs(e.getY() - dragStartY) < 5) {
|
|| Math.abs(e.getY() - dragStartY) >= 5;
|
||||||
|
if (!wasDrag) {
|
||||||
|
if (labelBtn.isSelected()) {
|
||||||
handleLabelPlacement(e.getX(), e.getY());
|
handleLabelPlacement(e.getX(), e.getY());
|
||||||
|
} else if (e.getButton() == MouseButton.PRIMARY) {
|
||||||
|
handleMapClick(e.getX(), e.getY());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,12 +265,23 @@ public class WorldMapView extends VBox {
|
|||||||
|
|
||||||
getChildren().addAll(toolbar, canvasPane, statusBar);
|
getChildren().addAll(toolbar, canvasPane, statusBar);
|
||||||
setStyle("-fx-background-color: #1a1a2a;");
|
setStyle("-fx-background-color: #1a1a2a;");
|
||||||
|
|
||||||
|
// Kamera-Indikator: regelmäßig neu zeichnen (unabhängig vom Karten-Rendering)
|
||||||
|
Timeline cameraRefresh = new Timeline(
|
||||||
|
new KeyFrame(Duration.millis(120), e -> {
|
||||||
|
if (cameraInfoSupplier != null && mapFxImage != null && !isFullscreen) {
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
cameraRefresh.setCycleCount(Animation.INDEFINITE);
|
||||||
|
cameraRefresh.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
public boolean isLoaded() { return mapFxImage != null || loading.get(); }
|
public boolean isLoaded() { return mapFxImage != null || loading.get(); }
|
||||||
|
|
||||||
|
/** Lädt alle Weltdaten neu und baut das Render-Modell (teuer: I/O + Berechnungen). */
|
||||||
public void loadAndRender() {
|
public void loadAndRender() {
|
||||||
if (loading.getAndSet(true)) return;
|
if (loading.getAndSet(true)) return;
|
||||||
progress.setVisible(true);
|
progress.setVisible(true);
|
||||||
@@ -216,73 +289,73 @@ public class WorldMapView extends VBox {
|
|||||||
|
|
||||||
Thread t = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
|
log.debug("[WorldMap] Modell-Build gestartet (I/O + Vorberechnungen)");
|
||||||
|
long t0 = System.currentTimeMillis();
|
||||||
|
|
||||||
MapData mapData = MapIO.load();
|
MapData mapData = MapIO.load();
|
||||||
List<PlacedArea> areas = AreaIO.load();
|
List<PlacedArea> areas = AreaIO.load();
|
||||||
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
||||||
List<Location> locs = LocationIO.load();
|
List<Location> locs = LocationIO.load();
|
||||||
List<PlacedWater> waters = WaterBodyIO.load();
|
List<PlacedWater> waters = WaterBodyIO.load();
|
||||||
List<PlacedModel> models = PlacedModelIO.load();
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
|
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)",
|
||||||
|
model.areas().size(), model.locations().size(),
|
||||||
|
model.waters().size(), model.models().size(),
|
||||||
|
System.currentTimeMillis() - t0);
|
||||||
|
|
||||||
Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
|
Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
|
||||||
|
long t1 = System.currentTimeMillis();
|
||||||
int[] slotColors = computeSlotColors(mapData);
|
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
||||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
log.debug("[WorldMap] Hintergrund-PNG gerendert ({}×{} px, {} ms)",
|
||||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildBackgroundOptions());
|
RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1);
|
||||||
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE);
|
|
||||||
float[][] segs = buildSeaCoastSegs(mask, SEA_MASK_SIZE);
|
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
cachedAreas = new ArrayList<>(areas);
|
currentModel = model;
|
||||||
cachedLocs = new ArrayList<>(locs);
|
cachedAreas = new ArrayList<>(model.areas());
|
||||||
cachedWaters = new ArrayList<>(waters);
|
cachedLocs = new ArrayList<>(model.locations());
|
||||||
seaMask = mask;
|
|
||||||
seaCoastSegs = segs;
|
|
||||||
mapBuffered = bi;
|
mapBuffered = bi;
|
||||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||||
fitToView();
|
fitToView();
|
||||||
redraw();
|
redraw();
|
||||||
progress.setVisible(false);
|
progress.setVisible(false);
|
||||||
statusLbl.setText("Bereit – " + areas.size() + " Areas, " +
|
statusLbl.setText("Bereit – " + model.areas().size() + " Areas, " +
|
||||||
locs.size() + " Orte, " + waters.size() + " Wasser");
|
model.locations().size() + " Orte, " + model.waters().size() + " Wasser");
|
||||||
loading.set(false);
|
loading.set(false);
|
||||||
|
log.debug("[WorldMap] Gesamt-Ladezeit: {} ms", System.currentTimeMillis() - t0);
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
log.error("[WorldMap] Fehler beim Modell-Build", ex);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
progress.setVisible(false);
|
progress.setVisible(false);
|
||||||
statusLbl.setText("Fehler: " + ex.getMessage());
|
statusLbl.setText("Fehler: " + ex.getMessage());
|
||||||
loading.set(false);
|
loading.set(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, "WorldMapRenderer");
|
}, "WorldMapModelBuilder");
|
||||||
t.setDaemon(true);
|
t.setDaemon(true);
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Interne Methoden ──────────────────────────────────────────────────────
|
// ── Interne Methoden ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void rerender() {
|
/** Rendert das PNG aus dem gecachten Modell neu (kein I/O, kein Modell-Rebuild). */
|
||||||
if (mapBuffered == null) { loadAndRender(); return; }
|
private void rerenderFromModel() {
|
||||||
|
if (currentModel == null) { loadAndRender(); return; }
|
||||||
if (loading.getAndSet(true)) return;
|
if (loading.getAndSet(true)) return;
|
||||||
progress.setVisible(true);
|
progress.setVisible(true);
|
||||||
statusLbl.setText("Rendere…");
|
statusLbl.setText("Rendere…");
|
||||||
|
|
||||||
|
WorldMapRenderModel model = currentModel;
|
||||||
Thread t = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
try {
|
log.debug("[WorldMap] PNG-Rerender aus gecachtem Modell gestartet ({}×{})", RENDER_SIZE, RENDER_SIZE);
|
||||||
MapData mapData = MapIO.load();
|
long t0 = System.currentTimeMillis();
|
||||||
List<PlacedArea> areas = AreaIO.load();
|
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
||||||
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
log.debug("[WorldMap] PNG-Rerender fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||||
List<Location> locs = LocationIO.load();
|
|
||||||
List<PlacedWater> waters = WaterBodyIO.load();
|
|
||||||
List<PlacedModel> models = PlacedModelIO.load();
|
|
||||||
int[] slotColors = computeSlotColors(mapData);
|
|
||||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
|
||||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildBackgroundOptions());
|
|
||||||
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE);
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
cachedAreas = new ArrayList<>(areas);
|
|
||||||
cachedLocs = new ArrayList<>(locs);
|
|
||||||
cachedWaters = new ArrayList<>(waters);
|
|
||||||
seaMask = mask;
|
|
||||||
mapBuffered = bi;
|
mapBuffered = bi;
|
||||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||||
redraw();
|
redraw();
|
||||||
@@ -290,19 +363,18 @@ public class WorldMapView extends VBox {
|
|||||||
statusLbl.setText("Bereit");
|
statusLbl.setText("Bereit");
|
||||||
loading.set(false);
|
loading.set(false);
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
progress.setVisible(false);
|
|
||||||
statusLbl.setText("Fehler: " + ex.getMessage());
|
|
||||||
loading.set(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, "WorldMapRerender");
|
}, "WorldMapRerender");
|
||||||
t.setDaemon(true);
|
t.setDaemon(true);
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void redraw() {
|
private void redraw() {
|
||||||
|
// Beim ersten Anzeigen der Weltkarte automatisch Modell laden
|
||||||
|
if (!autoLoaded && currentModel == null && !loading.get()) {
|
||||||
|
autoLoaded = true;
|
||||||
|
loadAndRender();
|
||||||
|
}
|
||||||
|
|
||||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||||
double w = canvas.getWidth(), h = canvas.getHeight();
|
double w = canvas.getWidth(), h = canvas.getHeight();
|
||||||
|
|
||||||
@@ -311,15 +383,17 @@ public class WorldMapView extends VBox {
|
|||||||
|
|
||||||
if (mapFxImage == null) {
|
if (mapFxImage == null) {
|
||||||
gc.setFill(Color.GRAY);
|
gc.setFill(Color.GRAY);
|
||||||
gc.fillText("Karte noch nicht geladen – 'Aktualisieren' klicken", 20, 40);
|
gc.fillText("Karte wird geladen…", 20, 40);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||||
|
|
||||||
if (layerWater.isSelected()) drawWaterOverlay(gc);
|
if (cbWater.isSelected()) drawWaterOverlay(gc);
|
||||||
if (layerAreas.isSelected()) drawAreasOverlay(gc);
|
if (cbAreas.isSelected()) drawAreasOverlay(gc);
|
||||||
if (layerLocations.isSelected()) drawLocationsOverlay(gc);
|
if (cbLocations.isSelected()) drawLocationsOverlay(gc);
|
||||||
|
if (cbModels.isSelected()) drawTreeOverlay(gc);
|
||||||
|
if (!isFullscreen) drawCameraIndicator(gc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Canvas-Vektor-Overlays ────────────────────────────────────────────────
|
// ── Canvas-Vektor-Overlays ────────────────────────────────────────────────
|
||||||
@@ -396,8 +470,13 @@ public class WorldMapView extends VBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void drawWaterOverlay(GraphicsContext gc) {
|
private void drawWaterOverlay(GraphicsContext gc) {
|
||||||
|
if (currentModel == null) return;
|
||||||
|
boolean[] seaMask = currentModel.seaMask();
|
||||||
|
float[] terrainSamples = currentModel.terrainSamples();
|
||||||
|
List<float[][]> seaCoastPaths = currentModel.coastPaths();
|
||||||
|
|
||||||
// ── Seewellen (Terrain < 0) ──────────────────────────────────────────────
|
// ── Seewellen (Terrain < 0) ──────────────────────────────────────────────
|
||||||
if (seaMask != null && layerWater.isSelected()) {
|
if (seaMask != null && cbWater.isSelected()) {
|
||||||
// Wellengröße in Weltkoordinaten (skaliert mit Zoom, damit Bildschirmgröße = WAVE_W)
|
// Wellengröße in Weltkoordinaten (skaliert mit Zoom, damit Bildschirmgröße = WAVE_W)
|
||||||
double pxPerWorld = (RENDER_SIZE - 1) * scale / WorldMapRenderer.WORLD_SIZE;
|
double pxPerWorld = (RENDER_SIZE - 1) * scale / WorldMapRenderer.WORLD_SIZE;
|
||||||
double wWave = WAVE_W / pxPerWorld;
|
double wWave = WAVE_W / pxPerWorld;
|
||||||
@@ -445,15 +524,18 @@ public class WorldMapView extends VBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedWaters.isEmpty()) return;
|
List<PlacedWater> waters = currentModel.waters();
|
||||||
|
if (waters.isEmpty()) return;
|
||||||
|
|
||||||
final double spX = WAVE_W * 1.8, spY = WAVE_W * 1.05, wmA = WAVE_W * 0.2;
|
final double spX = WAVE_W * 1.8, spY = WAVE_W * 1.05, wmA = WAVE_W * 0.2;
|
||||||
|
|
||||||
for (PlacedWater w : cachedWaters) {
|
for (PlacedWater w : waters) {
|
||||||
float[] wx = w.pointsX(), wz = w.pointsZ();
|
float[] wx = w.pointsX(), wz = w.pointsZ();
|
||||||
int n = wx.length;
|
int n = wx.length;
|
||||||
if (n < 3) continue;
|
if (n < 3) continue;
|
||||||
|
|
||||||
|
float wh = w.waterHeight();
|
||||||
|
|
||||||
double[] cx = new double[n], cz = new double[n];
|
double[] cx = new double[n], cz = new double[n];
|
||||||
for (int i = 0; i < n; i++) { cx[i] = toCanvasX(wx[i]); cz[i] = toCanvasZ(wz[i]); }
|
for (int i = 0; i < n; i++) { cx[i] = toCanvasX(wx[i]); cz[i] = toCanvasZ(wz[i]); }
|
||||||
|
|
||||||
@@ -468,7 +550,7 @@ public class WorldMapView extends VBox {
|
|||||||
gc.save();
|
gc.save();
|
||||||
gc.beginPath();
|
gc.beginPath();
|
||||||
gc.moveTo(cx[0], cz[0]);
|
gc.moveTo(cx[0], cz[0]);
|
||||||
for (int i = 1; i < n; i++) gc.lineTo(cx[i], cz[i]);
|
for (int i = 1; i < n; i++) { gc.lineTo(cx[i], cz[i]); }
|
||||||
gc.closePath();
|
gc.closePath();
|
||||||
gc.clip();
|
gc.clip();
|
||||||
|
|
||||||
@@ -481,11 +563,17 @@ public class WorldMapView extends VBox {
|
|||||||
double rowOff = (row & 1) == 1 ? spX * 0.5 : 0;
|
double rowOff = (row & 1) == 1 ? spX * 0.5 : 0;
|
||||||
int col = 0;
|
int col = 0;
|
||||||
for (double wxx = minX + rowOff; wxx + WAVE_W <= maxX; wxx += spX, col++) {
|
for (double wxx = minX + rowOff; wxx + WAVE_W <= maxX; wxx += spX, col++) {
|
||||||
// deterministisches Jitter pro Zelle
|
|
||||||
double jx = Math.abs(Math.sin(row * 73.1 + col * 157.3)) * spX * 0.25;
|
double jx = Math.abs(Math.sin(row * 73.1 + col * 157.3)) * spX * 0.25;
|
||||||
double jy = Math.sin(row * 211.7 + col * 89.5) * spY * 0.2;
|
double jy = Math.sin(row * 211.7 + col * 89.5) * spY * 0.2;
|
||||||
double ox = wxx + jx, oy = wy + jy;
|
double ox = wxx + jx, oy = wy + jy;
|
||||||
if (ox < minX || ox + WAVE_W > maxX || oy < minZ || oy > maxZ) continue;
|
if (ox < minX || ox + WAVE_W > maxX || oy < minZ || oy > maxZ) { continue; }
|
||||||
|
// Höhenprüfung: Terrain muss unter waterHeight liegen
|
||||||
|
if (terrainSamples != null) {
|
||||||
|
float owx = toWorldX(ox), owz = toWorldZ(oy);
|
||||||
|
int mxS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1))));
|
||||||
|
int mzS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1))));
|
||||||
|
if (terrainSamples[mzS * SEA_MASK_SIZE + mxS] >= wh) { continue; }
|
||||||
|
}
|
||||||
gc.beginPath();
|
gc.beginPath();
|
||||||
gc.moveTo(ox, oy);
|
gc.moveTo(ox, oy);
|
||||||
gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wmA, ox + WAVE_W*0.7, oy + wmA, ox + WAVE_W, oy);
|
gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wmA, ox + WAVE_W*0.7, oy + wmA, ox + WAVE_W, oy);
|
||||||
@@ -495,44 +583,22 @@ public class WorldMapView extends VBox {
|
|||||||
gc.restore();
|
gc.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schwarzer Umriss (konstant 3px)
|
|
||||||
gc.setStroke(Color.BLACK);
|
|
||||||
gc.setLineWidth(3.0);
|
|
||||||
gc.setLineDashes(null);
|
|
||||||
gc.beginPath();
|
|
||||||
gc.moveTo(cx[0], cz[0]);
|
|
||||||
for (int i = 1; i < n; i++) gc.lineTo(cx[i], cz[i]);
|
|
||||||
gc.closePath();
|
|
||||||
gc.stroke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Seeküstenlinie (konstant 3px, weicher Halo gegen Treppeneffekt) ────────
|
// ── Seeküstenlinie – verbundene Pfade, runde Joins, weicher Halo ────────────
|
||||||
if (seaCoastSegs != null && seaCoastSegs.length > 0) {
|
if (seaCoastPaths != null && !seaCoastPaths.isEmpty()) {
|
||||||
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
||||||
gc.setLineDashes(null);
|
gc.setLineDashes(null);
|
||||||
gc.setLineCap(StrokeLineCap.ROUND);
|
gc.setLineCap(StrokeLineCap.ROUND);
|
||||||
|
gc.setLineJoin(StrokeLineJoin.ROUND);
|
||||||
|
|
||||||
// Halo-Pass: breiter, halbtransparent
|
gc.setStroke(Color.color(0, 0, 0, 0.22));
|
||||||
gc.setStroke(Color.color(0, 0, 0, 0.2));
|
|
||||||
gc.setLineWidth(7.0);
|
gc.setLineWidth(7.0);
|
||||||
for (float[] seg : seaCoastSegs) {
|
for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 8); }
|
||||||
double sx1 = toCanvasX(seg[0]), sy1 = toCanvasZ(seg[1]);
|
|
||||||
double sx2 = toCanvasX(seg[2]), sy2 = toCanvasZ(seg[3]);
|
|
||||||
if (Math.max(sx1, sx2) < -8 || Math.min(sx1, sx2) > cW + 8) continue;
|
|
||||||
if (Math.max(sy1, sy2) < -8 || Math.min(sy1, sy2) > cH + 8) continue;
|
|
||||||
gc.strokeLine(sx1, sy1, sx2, sy2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kern-Pass: 3px solid
|
|
||||||
gc.setStroke(Color.BLACK);
|
gc.setStroke(Color.BLACK);
|
||||||
gc.setLineWidth(3.0);
|
gc.setLineWidth(3.0);
|
||||||
for (float[] seg : seaCoastSegs) {
|
for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 4); }
|
||||||
double sx1 = toCanvasX(seg[0]), sy1 = toCanvasZ(seg[1]);
|
|
||||||
double sx2 = toCanvasX(seg[2]), sy2 = toCanvasZ(seg[3]);
|
|
||||||
if (Math.max(sx1, sx2) < -3 || Math.min(sx1, sx2) > cW + 3) continue;
|
|
||||||
if (Math.max(sy1, sy2) < -3 || Math.min(sy1, sy2) > cH + 3) continue;
|
|
||||||
gc.strokeLine(sx1, sy1, sx2, sy2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,28 +669,336 @@ public class WorldMapView extends VBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float[][] buildSeaCoastSegs(boolean[] mask, int size) {
|
// Zoom-Schwelle: darüber Einzelbaum-Symbole, darunter Wald-Cluster-Symbol
|
||||||
List<float[]> segs = new ArrayList<>();
|
private static final double TREE_INDIVIDUAL_SCALE = 3.0;
|
||||||
for (int mz = 0; mz < size - 1; mz++) {
|
// Farbe der Baum-/Waldsymbole
|
||||||
for (int mx = 0; mx < size - 1; mx++) {
|
private static final Color TREE_COLOR = Color.rgb(25, 90, 25);
|
||||||
boolean c = mask[mz * size + mx];
|
|
||||||
boolean r = mask[mz * size + (mx + 1)];
|
private void drawTreeOverlay(GraphicsContext gc) {
|
||||||
boolean d = mask[(mz + 1) * size + mx];
|
if (currentModel == null) { return; }
|
||||||
float wx0 = (float)(mx / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
List<List<PlacedModel>> treeClusters = currentModel.treeClusters();
|
||||||
float wx1 = (float)((mx + 1) / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
if (treeClusters == null || treeClusters.isEmpty()) { return; }
|
||||||
float wz0 = (float)(mz / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
||||||
float wz1 = (float)((mz + 1) / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
|
||||||
float wzM = (wz0 + wz1) * 0.5f;
|
gc.save();
|
||||||
float wxM = (wx0 + wx1) * 0.5f;
|
gc.setFill(TREE_COLOR);
|
||||||
// Horizontale Kante zwischen (mz,mx) und (mz+1,mx)
|
gc.setTextAlign(TextAlignment.CENTER);
|
||||||
if (c != d) segs.add(new float[]{wx0, wzM, wx1, wzM});
|
gc.setTextBaseline(VPos.CENTER);
|
||||||
// Vertikale Kante zwischen (mz,mx) und (mz,mx+1)
|
|
||||||
if (c != r) segs.add(new float[]{wxM, wz0, wxM, wz1});
|
if (scale >= TREE_INDIVIDUAL_SCALE) {
|
||||||
|
// Einzelne Bäume – ein Symbol pro Baum
|
||||||
|
gc.setFont(Font.font("SansSerif", 11));
|
||||||
|
for (List<PlacedModel> cluster : treeClusters) {
|
||||||
|
for (PlacedModel t : cluster) {
|
||||||
|
double cx = toCanvasX(t.x()), cz = toCanvasZ(t.z());
|
||||||
|
if (cx < -15 || cx > cW + 15 || cz < -15 || cz > cH + 15) { continue; }
|
||||||
|
gc.fillText("♣", cx, cz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Wald-Modus – ein Symbol pro Cluster (am Schwerpunkt)
|
||||||
|
gc.setFont(Font.font("SansSerif", FontWeight.BOLD, 14));
|
||||||
|
for (List<PlacedModel> cluster : treeClusters) {
|
||||||
|
double sumX = 0, sumZ = 0;
|
||||||
|
for (PlacedModel t : cluster) { sumX += t.x(); sumZ += t.z(); }
|
||||||
|
double cx = toCanvasX((float)(sumX / cluster.size()));
|
||||||
|
double cz = toCanvasZ((float)(sumZ / cluster.size()));
|
||||||
|
if (cx < -20 || cx > cW + 20 || cz < -20 || cz > cH + 20) { continue; }
|
||||||
|
gc.fillText("♣", cx, cz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gc.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<PlacedModel>> clusterTreeModels(List<PlacedModel> trees, float radius) {
|
||||||
|
int n = trees.size();
|
||||||
|
int[] parent = new int[n];
|
||||||
|
for (int i = 0; i < n; i++) { parent[i] = i; }
|
||||||
|
float r2 = radius * radius;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
for (int j = i + 1; j < n; j++) {
|
||||||
|
float dx = trees.get(i).x() - trees.get(j).x();
|
||||||
|
float dz = trees.get(i).z() - trees.get(j).z();
|
||||||
|
if (dx*dx + dz*dz <= r2) {
|
||||||
|
int pi = ufFind(parent, i), pj = ufFind(parent, j);
|
||||||
|
if (pi != pj) { parent[pi] = pj; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<Integer, List<PlacedModel>> groups = new HashMap<>();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
groups.computeIfAbsent(ufFind(parent, i), k -> new ArrayList<>()).add(trees.get(i));
|
||||||
|
}
|
||||||
|
return new ArrayList<>(groups.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ufFind(int[] parent, int i) {
|
||||||
|
while (parent[i] != i) { parent[i] = parent[parent[i]]; i = parent[i]; }
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest alle VoxelChunks (aus dem Live-Supplier oder von Disk) und schreibt die
|
||||||
|
* höchsten soliden Voxel-Y-Werte in mapData.upperTop.
|
||||||
|
*/
|
||||||
|
private void bakeVoxelHeights(MapData mapData) {
|
||||||
|
List<VoxelChunk> chunks = voxelChunkSupplier != null
|
||||||
|
? voxelChunkSupplier.get()
|
||||||
|
: VoxelChunkIO.loadAll();
|
||||||
|
if (chunks.isEmpty()) { return; }
|
||||||
|
int UV = MapData.UPPER_VERTS;
|
||||||
|
float WH = WorldMapRenderer.WORLD_HALF;
|
||||||
|
float WS = WorldMapRenderer.WORLD_SIZE;
|
||||||
|
for (VoxelChunk chunk : chunks) {
|
||||||
|
if (chunk.isEmpty()) { continue; }
|
||||||
|
for (int lz = 0; lz < VoxelChunk.SIZE; lz++) {
|
||||||
|
float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz);
|
||||||
|
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 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; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldMapRenderModel buildRenderModel(
|
||||||
|
MapData mapData,
|
||||||
|
List<PlacedArea> areas, List<PlacedLocationZone> zones,
|
||||||
|
List<Location> locs, List<PlacedWater> waters,
|
||||||
|
List<PlacedModel> models) {
|
||||||
|
int[] slotColors = computeSlotColors(mapData);
|
||||||
|
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE);
|
||||||
|
float[] tSamp = buildTerrainSamples(mapData, SEA_MASK_SIZE);
|
||||||
|
List<float[][]> paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[] buildTerrainSamples(MapData mapData, int size) {
|
||||||
|
int TV = MapData.TERRAIN_VERTS;
|
||||||
|
int UV = MapData.UPPER_VERTS;
|
||||||
|
float[] s = 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 h = mapData.terrainHeight[hz * TV + hx];
|
||||||
|
float upper = mapData.upperTop[uz * UV + ux];
|
||||||
|
if (upper > 0f && upper > h) { h = upper; }
|
||||||
|
s[mz * size + mx] = h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
int TV = MapData.TERRAIN_VERTS;
|
||||||
|
int UV = MapData.UPPER_VERTS;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<float[]> segs = new ArrayList<>();
|
||||||
|
marchingSquares(h, size, 0f, null, null, segs);
|
||||||
|
for (PlacedWater w : waters) {
|
||||||
|
marchingSquares(h, size, w.waterHeight(), w.pointsX(), w.pointsZ(), segs);
|
||||||
|
}
|
||||||
return segs.toArray(new float[0][]);
|
return segs.toArray(new float[0][]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void marchingSquares(float[] h, int size, float thr,
|
||||||
|
float[] polyX, float[] polyZ, List<float[]> out) {
|
||||||
|
for (int mz = 0; mz < size - 1; mz++) {
|
||||||
|
for (int mx = 0; mx < size - 1; mx++) {
|
||||||
|
float hTL = h[mz*size + mx], hTR = h[mz*size + (mx+1)];
|
||||||
|
float hBL = h[(mz+1)*size + mx], hBR = h[(mz+1)*size + (mx+1)];
|
||||||
|
float wx0 = msW(mx, size), wx1 = msW(mx+1, size);
|
||||||
|
float wz0 = msW(mz, size), wz1 = msW(mz+1, size);
|
||||||
|
|
||||||
|
boolean iTL, iTR, iBL, iBR;
|
||||||
|
if (polyX != null) {
|
||||||
|
iTL = hTL < thr && polyContains(polyX, polyZ, wx0, wz0);
|
||||||
|
iTR = hTR < thr && polyContains(polyX, polyZ, wx1, wz0);
|
||||||
|
iBL = hBL < thr && polyContains(polyX, polyZ, wx0, wz1);
|
||||||
|
iBR = hBR < thr && polyContains(polyX, polyZ, wx1, wz1);
|
||||||
|
} else {
|
||||||
|
iTL = hTL < thr; iTR = hTR < thr;
|
||||||
|
iBL = hBL < thr; iBR = hBR < thr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = (iTL?8:0)|(iTR?4:0)|(iBR?2:0)|(iBL?1:0);
|
||||||
|
if (idx == 0 || idx == 15) { continue; }
|
||||||
|
|
||||||
|
// Interpolierte Schnittpunkte auf den vier Zellkanten
|
||||||
|
float xTop = msLerp(wx0, wx1, thr, hTL, hTR); // obere Kante
|
||||||
|
float xBot = msLerp(wx0, wx1, thr, hBL, hBR); // untere Kante
|
||||||
|
float zLft = msLerp(wz0, wz1, thr, hTL, hBL); // linke Kante
|
||||||
|
float zRgt = msLerp(wz0, wz1, thr, hTR, hBR); // rechte Kante
|
||||||
|
|
||||||
|
switch (idx) {
|
||||||
|
case 1: case 14: out.add(new float[]{wx0,zLft, xBot,wz1}); break;
|
||||||
|
case 2: case 13: out.add(new float[]{xBot,wz1, wx1,zRgt}); break;
|
||||||
|
case 4: case 11: out.add(new float[]{xTop,wz0, wx1,zRgt}); break;
|
||||||
|
case 8: case 7: out.add(new float[]{wx0,zLft, xTop,wz0}); break;
|
||||||
|
case 3: case 12: out.add(new float[]{wx0,zLft, wx1,zRgt}); break;
|
||||||
|
case 6: case 9: out.add(new float[]{xTop,wz0, xBot,wz1}); break;
|
||||||
|
case 5: {
|
||||||
|
float hC = (hTL+hTR+hBL+hBR)*0.25f;
|
||||||
|
if (hC >= thr) { out.add(new float[]{wx0,zLft,xTop,wz0}); out.add(new float[]{xBot,wz1,wx1,zRgt}); }
|
||||||
|
else { out.add(new float[]{wx0,zLft,xBot,wz1}); out.add(new float[]{xTop,wz0,wx1,zRgt}); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 10: {
|
||||||
|
float hC = (hTL+hTR+hBL+hBR)*0.25f;
|
||||||
|
if (hC >= thr) { out.add(new float[]{xTop,wz0,wx1,zRgt}); out.add(new float[]{wx0,zLft,xBot,wz1}); }
|
||||||
|
else { out.add(new float[]{wx0,zLft,xTop,wz0}); out.add(new float[]{xBot,wz1,wx1,zRgt}); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float msLerp(float from, float to, float thr, float hA, float hB) {
|
||||||
|
float d = hB - hA;
|
||||||
|
if (Math.abs(d) < 1e-6f) { return (from + to) * 0.5f; }
|
||||||
|
return from + Math.max(0f, Math.min(1f, (thr - hA) / d)) * (to - from);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float msW(int idx, int size) {
|
||||||
|
return (float)(idx / (double)(size-1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbindet Rohsegmente zu geschlossenen Küstenpfaden.
|
||||||
|
private static List<float[][]> buildSeaCoastPaths(float[][] segs) {
|
||||||
|
if (segs == null || segs.length == 0) { return Collections.emptyList(); }
|
||||||
|
|
||||||
|
Map<Long, List<Integer>> adj = new HashMap<>(segs.length * 4);
|
||||||
|
for (int i = 0; i < segs.length; i++) {
|
||||||
|
adj.computeIfAbsent(coastPtKey(segs[i][0], segs[i][1]), k -> new ArrayList<>()).add(i);
|
||||||
|
adj.computeIfAbsent(coastPtKey(segs[i][2], segs[i][3]), k -> new ArrayList<>()).add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean[] used = new boolean[segs.length];
|
||||||
|
List<float[][]> result = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int start = 0; start < segs.length; start++) {
|
||||||
|
if (used[start]) { continue; }
|
||||||
|
used[start] = true;
|
||||||
|
|
||||||
|
List<float[]> fwd = new ArrayList<>();
|
||||||
|
List<float[]> bwd = new ArrayList<>();
|
||||||
|
fwd.add(new float[]{segs[start][2], segs[start][3]});
|
||||||
|
bwd.add(new float[]{segs[start][0], segs[start][1]});
|
||||||
|
coastExtend(fwd, segs, adj, used);
|
||||||
|
coastExtend(bwd, segs, adj, used);
|
||||||
|
|
||||||
|
float[][] raw = new float[bwd.size() + fwd.size()][];
|
||||||
|
for (int i = 0; i < bwd.size(); i++) { raw[i] = bwd.get(bwd.size() - 1 - i); }
|
||||||
|
for (int i = 0; i < fwd.size(); i++) { raw[bwd.size() + i] = fwd.get(i); }
|
||||||
|
result.add(chaikin(simplifyCoastPath(raw), 3));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entfernt kollineare Zwischenpunkte (lange H/V-Läufe → ein Segment).
|
||||||
|
private static float[][] simplifyCoastPath(float[][] path) {
|
||||||
|
int n = path.length;
|
||||||
|
// Letzten Punkt entfernen wenn er gleich dem ersten ist (geschlossene Schleife)
|
||||||
|
int len = (n > 1 && Math.abs(path[0][0]-path[n-1][0]) < 0.01f
|
||||||
|
&& Math.abs(path[0][1]-path[n-1][1]) < 0.01f) ? n - 1 : n;
|
||||||
|
if (len <= 2) { return path; }
|
||||||
|
List<float[]> out = new ArrayList<>(len);
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
float[] a = path[(i + len - 1) % len], b = path[i], c = path[(i + 1) % len];
|
||||||
|
float cross = (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0]);
|
||||||
|
if (Math.abs(cross) > 1e-3f) { out.add(b); }
|
||||||
|
}
|
||||||
|
return out.isEmpty() ? new float[][]{path[0]} : out.toArray(new float[0][]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chaikin-Eckenschnitt: glättet Treppenstufen zu Kurven.
|
||||||
|
private static float[][] chaikin(float[][] path, int iterations) {
|
||||||
|
float[][] cur = path;
|
||||||
|
for (int iter = 0; iter < iterations; iter++) {
|
||||||
|
int n = cur.length;
|
||||||
|
float[][] next = new float[n * 2][];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
float[] p0 = cur[i], p1 = cur[(i + 1) % n];
|
||||||
|
next[i*2] = new float[]{ p0[0]*0.75f + p1[0]*0.25f, p0[1]*0.75f + p1[1]*0.25f };
|
||||||
|
next[i*2+1] = new float[]{ p0[0]*0.25f + p1[0]*0.75f, p0[1]*0.25f + p1[1]*0.75f };
|
||||||
|
}
|
||||||
|
cur = next;
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void coastExtend(List<float[]> pts, float[][] segs, Map<Long, List<Integer>> adj, boolean[] used) {
|
||||||
|
for (;;) {
|
||||||
|
float[] p = pts.get(pts.size() - 1);
|
||||||
|
List<Integer> nb = adj.get(coastPtKey(p[0], p[1]));
|
||||||
|
int next = -1;
|
||||||
|
if (nb != null) { for (int idx : nb) { if (!used[idx]) { next = idx; break; } } }
|
||||||
|
if (next < 0) { break; }
|
||||||
|
used[next] = true;
|
||||||
|
float[] seg = segs[next];
|
||||||
|
boolean atA = Math.abs(seg[0] - p[0]) < 0.1f && Math.abs(seg[1] - p[1]) < 0.1f;
|
||||||
|
pts.add(atA ? new float[]{seg[2], seg[3]} : new float[]{seg[0], seg[1]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long coastPtKey(float x, float z) {
|
||||||
|
return ((long)Math.round(x * 4)) << 32 | (Math.round(z * 4) & 0xFFFFFFFFL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeichnet einen Küstenpfad als einzelnen geschlossenen Canvas-Pfad.
|
||||||
|
// margin: Viewport-Puffer in Pixeln für Culling.
|
||||||
|
private void drawCoastPath(GraphicsContext gc, float[][] path, double cW, double cH, double margin) {
|
||||||
|
if (path.length < 2) { return; }
|
||||||
|
double bx0 = Double.MAX_VALUE, bx1 = -Double.MAX_VALUE;
|
||||||
|
double by0 = Double.MAX_VALUE, by1 = -Double.MAX_VALUE;
|
||||||
|
for (float[] pt : path) {
|
||||||
|
double px = toCanvasX(pt[0]), py = toCanvasZ(pt[1]);
|
||||||
|
if (px < bx0) bx0 = px; if (px > bx1) bx1 = px;
|
||||||
|
if (py < by0) by0 = py; if (py > by1) by1 = py;
|
||||||
|
}
|
||||||
|
if (bx1 < -margin || bx0 > cW + margin || by1 < -margin || by0 > cH + margin) { return; }
|
||||||
|
gc.beginPath();
|
||||||
|
gc.moveTo(toCanvasX(path[0][0]), toCanvasZ(path[0][1]));
|
||||||
|
for (int i = 1; i < path.length; i++) { gc.lineTo(toCanvasX(path[i][0]), toCanvasZ(path[i][1])); }
|
||||||
|
gc.closePath();
|
||||||
|
gc.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void fitToView() {
|
private void fitToView() {
|
||||||
@@ -681,21 +1055,22 @@ public class WorldMapView extends VBox {
|
|||||||
File file = fc.showSaveDialog(stageSupplier.get());
|
File file = fc.showSaveDialog(stageSupplier.get());
|
||||||
if (file == null) return;
|
if (file == null) return;
|
||||||
|
|
||||||
|
if (currentModel == null) {
|
||||||
|
statusLbl.setText("Kein Modell geladen – bitte erst 'Modell aktualisieren'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
statusLbl.setText("Exportiere 4096×4096 PNG…");
|
statusLbl.setText("Exportiere 4096×4096 PNG…");
|
||||||
progress.setVisible(true);
|
progress.setVisible(true);
|
||||||
|
|
||||||
|
WorldMapRenderModel model = currentModel;
|
||||||
Thread t = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
MapData mapData = MapIO.load();
|
log.debug("[WorldMap] Export-PNG gestartet (4096×4096, Ziel: {})", file.getName());
|
||||||
List<PlacedArea> areas = AreaIO.load();
|
long t0 = System.currentTimeMillis();
|
||||||
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
BufferedImage bi = WorldMapRenderer.render(model, 4096, buildExportOptions());
|
||||||
List<Location> locs = LocationIO.load();
|
|
||||||
List<PlacedWater> waters = WaterBodyIO.load();
|
|
||||||
List<PlacedModel> models = PlacedModelIO.load();
|
|
||||||
int[] slotColors = computeSlotColors(mapData);
|
|
||||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
|
||||||
BufferedImage bi = WorldMapRenderer.render(input, 4096, buildExportOptions());
|
|
||||||
ImageIO.write(bi, "PNG", file);
|
ImageIO.write(bi, "PNG", file);
|
||||||
|
log.debug("[WorldMap] Export-PNG fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
progress.setVisible(false);
|
progress.setVisible(false);
|
||||||
statusLbl.setText("Exportiert: " + file.getName());
|
statusLbl.setText("Exportiert: " + file.getName());
|
||||||
@@ -714,34 +1089,116 @@ public class WorldMapView extends VBox {
|
|||||||
/** Hintergrund-PNG: ohne Areas und Locations (die werden live auf Canvas gezeichnet). */
|
/** Hintergrund-PNG: ohne Areas und Locations (die werden live auf Canvas gezeichnet). */
|
||||||
private RenderOptions buildBackgroundOptions() {
|
private RenderOptions buildBackgroundOptions() {
|
||||||
return new RenderOptions(
|
return new RenderOptions(
|
||||||
layerTerrain.isSelected(),
|
cbTerrain.isSelected(),
|
||||||
layerTerrain.isSelected(),
|
cbTerrain.isSelected(),
|
||||||
layerWater.isSelected(),
|
cbWater.isSelected(),
|
||||||
false, // Wellen im Editor über Canvas gezeichnet
|
false, // Wellen im Editor über Canvas gezeichnet
|
||||||
false,
|
false,
|
||||||
layerZones.isSelected(),
|
cbZones.isSelected(),
|
||||||
false,
|
false,
|
||||||
layerModels.isSelected()
|
cbModels.isSelected()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Export-PNG: alle Layer gebacken. */
|
/** Export-PNG: alle Layer gebacken. */
|
||||||
private RenderOptions buildExportOptions() {
|
private RenderOptions buildExportOptions() {
|
||||||
return new RenderOptions(
|
return new RenderOptions(
|
||||||
layerTerrain.isSelected(),
|
cbTerrain.isSelected(),
|
||||||
layerTerrain.isSelected(),
|
cbTerrain.isSelected(),
|
||||||
layerWater.isSelected(),
|
cbWater.isSelected(),
|
||||||
true, // Wellen im Export-PNG gebacken
|
true, // Wellen im Export-PNG gebacken
|
||||||
layerAreas.isSelected(),
|
cbAreas.isSelected(),
|
||||||
layerZones.isSelected(),
|
cbZones.isSelected(),
|
||||||
layerLocations.isSelected(),
|
cbLocations.isSelected(),
|
||||||
layerModels.isSelected()
|
cbModels.isSelected()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ToggleButton layerBtn(String label) {
|
private static CheckBox layerCheck(String label) {
|
||||||
ToggleButton btn = new ToggleButton(label);
|
CheckBox cb = new CheckBox(label);
|
||||||
btn.setSelected(true);
|
cb.setSelected(true);
|
||||||
return btn;
|
return cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Vollbild ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void enterFullscreen() {
|
||||||
|
isFullscreen = true;
|
||||||
|
fullscreenBtn.setVisible(false);
|
||||||
|
backBtn.setVisible(true);
|
||||||
|
if (enterFullscreenCallback != null) {
|
||||||
|
enterFullscreenCallback.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exitFullscreen() {
|
||||||
|
isFullscreen = false;
|
||||||
|
backBtn.setVisible(false);
|
||||||
|
fullscreenBtn.setVisible(true);
|
||||||
|
if (exitFullscreenCallback != null) {
|
||||||
|
exitFullscreenCallback.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kamera-Indikator ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void drawCameraIndicator(GraphicsContext gc) {
|
||||||
|
if (cameraInfoSupplier == null || mapFxImage == null) return;
|
||||||
|
CameraInfo ci = cameraInfoSupplier.get();
|
||||||
|
double cx = toCanvasX(ci.x());
|
||||||
|
double cz = toCanvasZ(ci.z());
|
||||||
|
// Yaw 0° = Blick in -Z. Canvas-Winkel: angle = -(yaw + 90°) in Rad
|
||||||
|
double angle = -(Math.toRadians(ci.yawDeg()) + Math.PI / 2.0);
|
||||||
|
double coneLen = 55.0;
|
||||||
|
double halfFov = Math.toRadians(22.5); // 45° FOV / 2
|
||||||
|
|
||||||
|
double lx = cx + coneLen * Math.cos(angle - halfFov);
|
||||||
|
double lz = cz + coneLen * Math.sin(angle - halfFov);
|
||||||
|
double rx = cx + coneLen * Math.cos(angle + halfFov);
|
||||||
|
double rz = cz + coneLen * Math.sin(angle + halfFov);
|
||||||
|
|
||||||
|
gc.save();
|
||||||
|
gc.setLineDashes(null);
|
||||||
|
// Kegel-Fläche
|
||||||
|
gc.setFill(Color.rgb(255, 220, 0, 0.25));
|
||||||
|
gc.fillPolygon(new double[]{cx, lx, rx}, new double[]{cz, lz, rz}, 3);
|
||||||
|
// Kegel-Rand
|
||||||
|
gc.setStroke(Color.rgb(255, 220, 0, 0.85));
|
||||||
|
gc.setLineWidth(1.5);
|
||||||
|
gc.strokePolygon(new double[]{cx, lx, rx}, new double[]{cz, lz, rz}, 3);
|
||||||
|
// Positions-Punkt
|
||||||
|
gc.setFill(Color.rgb(255, 220, 0));
|
||||||
|
gc.fillOval(cx - 4.5, cz - 4.5, 9, 9);
|
||||||
|
gc.setStroke(Color.BLACK);
|
||||||
|
gc.setLineWidth(1.0);
|
||||||
|
gc.strokeOval(cx - 4.5, cz - 4.5, 9, 9);
|
||||||
|
gc.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kamera-Teleport ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleMapClick(double canvasX, double canvasY) {
|
||||||
|
if (teleportCallback == null) return;
|
||||||
|
float wx = toWorldX(canvasX);
|
||||||
|
float wz = toWorldZ(canvasY);
|
||||||
|
float dstH = getTerrainH(wx, wz);
|
||||||
|
float targetY = dstH + 20f;
|
||||||
|
if (cameraInfoSupplier != null) {
|
||||||
|
CameraInfo ci = cameraInfoSupplier.get();
|
||||||
|
float srcH = getTerrainH(ci.x(), ci.z());
|
||||||
|
targetY = dstH + (ci.y() - srcH);
|
||||||
|
}
|
||||||
|
teleportCallback.accept(new float[]{wx, targetY, wz});
|
||||||
|
}
|
||||||
|
|
||||||
|
private float getTerrainH(float worldX, float worldZ) {
|
||||||
|
if (currentModel == null) return 0f;
|
||||||
|
float[] samples = currentModel.terrainSamples();
|
||||||
|
if (samples == null) return 0f;
|
||||||
|
int mx = Math.max(0, Math.min(SEA_MASK_SIZE - 1,
|
||||||
|
(int)((worldX + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1))));
|
||||||
|
int mz = Math.max(0, Math.min(SEA_MASK_SIZE - 1,
|
||||||
|
(int)((worldZ + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1))));
|
||||||
|
return samples[mz * SEA_MASK_SIZE + mx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- Karten-Rendering: Modell-Build und PNG-Render auf DEBUG -->
|
||||||
|
<logger name="de.blight.editor.ui.WorldMapView" level="DEBUG"/>
|
||||||
|
|
||||||
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
||||||
<logger name="com.jme3" level="WARN"/>
|
<logger name="com.jme3" level="WARN"/>
|
||||||
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
||||||
|
|||||||
@@ -533,6 +533,8 @@ public class MinimapState extends BaseAppState {
|
|||||||
Files.createDirectories(dir);
|
Files.createDirectories(dir);
|
||||||
Path png = dir.resolve("minimap_world.png");
|
Path png = dir.resolve("minimap_world.png");
|
||||||
|
|
||||||
|
log.debug("[Minimap] Lade Weltdaten für Render-Modell…");
|
||||||
|
long t0 = System.currentTimeMillis();
|
||||||
MapData mapData = MapIO.load();
|
MapData mapData = MapIO.load();
|
||||||
List<PlacedArea> areas = AreaIO.load();
|
List<PlacedArea> areas = AreaIO.load();
|
||||||
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
||||||
@@ -541,6 +543,8 @@ public class MinimapState extends BaseAppState {
|
|||||||
List<PlacedModel> models = PlacedModelIO.load();
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
int[] slotColors = computeSlotColors(mapData, root);
|
int[] slotColors = computeSlotColors(mapData, root);
|
||||||
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||||
|
log.debug("[Minimap] Render-Modell bereit – {} Areas, {} Orte, {} Wasser ({} ms)",
|
||||||
|
areas.size(), locs.size(), waters.size(), System.currentTimeMillis() - t0);
|
||||||
|
|
||||||
boolean needsRender = !Files.exists(png);
|
boolean needsRender = !Files.exists(png);
|
||||||
if (!needsRender) {
|
if (!needsRender) {
|
||||||
@@ -553,9 +557,11 @@ public class MinimapState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
if (needsRender) {
|
if (needsRender) {
|
||||||
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
||||||
|
long t1 = System.currentTimeMillis();
|
||||||
BufferedImage bi = WorldMapRenderer.render(renderInput, TEXTURE_SIZE, RenderOptions.all());
|
BufferedImage bi = WorldMapRenderer.render(renderInput, TEXTURE_SIZE, RenderOptions.all());
|
||||||
ImageIO.write(bi, "PNG", png.toFile());
|
ImageIO.write(bi, "PNG", png.toFile());
|
||||||
log.info("[Minimap] Weltkarte gespeichert: {}", png);
|
log.debug("[Minimap] Weltkarte gerendert und gespeichert ({} ms): {}",
|
||||||
|
System.currentTimeMillis() - t1, png);
|
||||||
} else {
|
} else {
|
||||||
log.info("[Minimap] Gecachte Weltkarte: {}", png);
|
log.info("[Minimap] Gecachte Weltkarte: {}", png);
|
||||||
}
|
}
|
||||||
@@ -645,8 +651,12 @@ public class MinimapState extends BaseAppState {
|
|||||||
final float capHU = vr / WORLD_SIZE;
|
final float capHU = vr / WORLD_SIZE;
|
||||||
|
|
||||||
Thread t = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
|
log.debug("[Minimap] Vektor-Layer neu rendern – Zentrum ({}/{}), Radius {}",
|
||||||
|
Math.round(wx), Math.round(wz), Math.round(vr));
|
||||||
|
long t0 = System.currentTimeMillis();
|
||||||
RenderOptions opts = new RenderOptions(false, false, true, true, true, true, true);
|
RenderOptions opts = new RenderOptions(false, false, true, true, true, true, true);
|
||||||
BufferedImage bi = WorldMapRenderer.renderRegion(renderInput, VEC_TEX_SIZE, opts, wx, wz, vr);
|
BufferedImage bi = WorldMapRenderer.renderRegion(renderInput, VEC_TEX_SIZE, opts, wx, wz, vr);
|
||||||
|
log.debug("[Minimap] Vektor-Layer fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||||
app.enqueue(() -> {
|
app.enqueue(() -> {
|
||||||
updateVectorTexture(bi);
|
updateVectorTexture(bi);
|
||||||
vecRenderU = capU;
|
vecRenderU = capU;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
||||||
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
||||||
|
<!-- Minimap: Modell-Build und Render-Zyklen auf DEBUG -->
|
||||||
|
<logger name="de.blight.game.state.MinimapState" level="DEBUG"/>
|
||||||
|
|
||||||
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
||||||
<logger name="com.jme3" level="WARN"/>
|
<logger name="com.jme3" level="WARN"/>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "b26f42df-7897-4840-9e7f-5f35329aa0a6",
|
||||||
|
"benchType": "Simple",
|
||||||
|
"sitzX": 0.30349,
|
||||||
|
"sitzY": 2.5,
|
||||||
|
"sitzZ": -5.00573,
|
||||||
|
"sitzRotY": 1.5707964,
|
||||||
|
"sitzSet": true
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -11,3 +11,5 @@ Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828
|
|||||||
Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec
|
Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec
|
||||||
Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
|
Models/imported/wolf.j3o -1.25617 2.00000 -1.86424 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
|
Models/imported/bank1.j3o 0.30349 2.00000 -5.00573 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH b26f42df-7897-4840-9e7f-5f35329aa0a6
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user