Ingame Karte weitergebaut
This commit is contained in:
@@ -5706,7 +5706,6 @@ public class EditorApp extends Application {
|
||||
if (!worldMapView.isLoaded()) {
|
||||
worldMapView.loadAndRender();
|
||||
} else if (caveMapView != null && caveMapView.isLoaded() && caveMapView.isViewInitialized()) {
|
||||
// Sync nur wenn CaveMap korrekt initialisiert (canvas > 0, zoomLevels gesetzt)
|
||||
final double s = caveMapView.getScale(), px = caveMapView.getPanX(), py = caveMapView.getPanY();
|
||||
javafx.application.Platform.runLater(() -> worldMapView.setView(s, px, py));
|
||||
} else {
|
||||
@@ -5722,7 +5721,6 @@ public class EditorApp extends Application {
|
||||
if (!caveMapView.isLoaded()) {
|
||||
caveMapView.loadAndRender();
|
||||
} else if (worldMapView.isMapReady() && worldMapView.isViewInitialized()) {
|
||||
// Sync nur wenn WorldMap korrekt initialisiert
|
||||
final double s = worldMapView.getScale(), px = worldMapView.getPanX(), py = worldMapView.getPanY();
|
||||
javafx.application.Platform.runLater(() -> caveMapView.setView(s, px, py));
|
||||
} else {
|
||||
@@ -5770,6 +5768,7 @@ public class EditorApp extends Application {
|
||||
});
|
||||
tt.play();
|
||||
});
|
||||
caveMapView.loadAndRender(); // Vorladen beim Start, nicht erst beim Tab-Öffnen
|
||||
worldMapView.setFullscreenCallbacks(
|
||||
() -> {
|
||||
// Placeholder ersetzt worldMapView im Tab → sauberes Detach
|
||||
|
||||
@@ -291,23 +291,32 @@ public class CaveMapView extends VBox {
|
||||
List<PlacedModel> models = PlacedModelIO.load();
|
||||
|
||||
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
|
||||
// Für ingame-Nutzung speichern
|
||||
try {
|
||||
java.nio.file.Path cacheDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".blight", "map_chunks");
|
||||
java.nio.file.Files.createDirectories(cacheDir);
|
||||
ImageIO.write(bi, "PNG", cacheDir.resolve("minimap_cave.png").toFile());
|
||||
} catch (Exception ex) {
|
||||
log.warn("[CaveMap] Konnte minimap_cave.png nicht speichern: {}", ex.getMessage());
|
||||
}
|
||||
List<float[][]> allPaths = WorldMapView.loadPathChains();
|
||||
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
|
||||
List<float[]> allFootprints = WorldMapView.computeBuildingFootprintsSimple(models);
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
|
||||
boolean[] caveMask = WorldMapView.loadCaveMask(mapData);
|
||||
boolean[] caveMask = WorldMapRenderer.buildCaveFootprintMask(
|
||||
mapData, WorldMapRenderer.VOXEL_SURFACE_RES);
|
||||
|
||||
// Pfade: Cave-Maske + Y-basierte Trennung
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne VoxelSurface…"));
|
||||
float[] voxelSurface = WorldMapRenderer.bakeVoxelSurface(mapData);
|
||||
List<float[][]>[] splitChains = WorldMapView.splitPathChains(allPaths, voxelSurface, caveMask);
|
||||
List<float[][]>[] splitChains = WorldMapRenderer.splitPathChains(allPaths, voxelSurface, caveMask);
|
||||
List<float[][]> newContourPaths = WorldMapView.buildTunnelContourPaths(caveMask);
|
||||
List<float[][]> cavePaths = splitChains[1];
|
||||
|
||||
// Gebäude + Areas: Cave-Maske (keine Y-Info)
|
||||
List<float[]> caveFootprints = WorldMapView.filterBuildingsByCave(allFootprints, caveMask, true);
|
||||
List<PlacedArea> caveAreas = WorldMapView.filterAreasByCave(new ArrayList<>(areas), caveMask, true);
|
||||
List<float[]> caveFootprints = WorldMapRenderer.filterBuildingsByCave(allFootprints, caveMask, true);
|
||||
List<PlacedArea> caveAreas = WorldMapRenderer.filterAreasByCave(new ArrayList<>(areas), caveMask, true);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
@@ -317,7 +326,24 @@ public class CaveMapView extends VBox {
|
||||
cachedAreas = caveAreas;
|
||||
buildingFootprints = caveFootprints;
|
||||
cachedMapData = mapData;
|
||||
fitToView();
|
||||
if (zoomLevels == null) {
|
||||
fitToView(); // Erstmalig: Zoom initialisieren
|
||||
} else {
|
||||
double cw = canvas.getWidth(), ch = canvas.getHeight();
|
||||
if (cw > 0 && ch > 0 && mapFxImage != null) {
|
||||
double fit = Math.min(cw / mapFxImage.getWidth(), ch / mapFxImage.getHeight());
|
||||
double ratio = Math.pow(ZOOM_MAX_SCALE / fit, 1.0 / (ZOOM_LEVELS - 1));
|
||||
zoomLevels[0] = fit;
|
||||
for (int i = 1; i < ZOOM_LEVELS; i++) zoomLevels[i] = zoomLevels[i-1] * ratio;
|
||||
int best = 0;
|
||||
double minDiff = Math.abs(zoomLevels[0] - scale);
|
||||
for (int i = 1; i < ZOOM_LEVELS; i++) {
|
||||
double d = Math.abs(zoomLevels[i] - scale);
|
||||
if (d < minDiff) { minDiff = d; best = i; }
|
||||
}
|
||||
zoomLevelIdx = best;
|
||||
}
|
||||
}
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Höhlenkarte bereit.");
|
||||
|
||||
@@ -266,6 +266,9 @@ public class WorldMapView extends VBox {
|
||||
canvas.setOnMouseDragged(e -> {
|
||||
panX = dragStartPanX + (e.getX() - dragStartX);
|
||||
panY = dragStartPanY + (e.getY() - dragStartY);
|
||||
double iw = RENDER_SIZE * scale, ih = iw;
|
||||
panX = Math.max(50 - iw, Math.min(canvas.getWidth() - 50, panX));
|
||||
panY = Math.max(50 - ih, Math.min(canvas.getHeight() - 50, panY));
|
||||
redraw();
|
||||
});
|
||||
canvas.setOnMouseReleased(e -> {
|
||||
@@ -290,6 +293,9 @@ public class WorldMapView extends VBox {
|
||||
scale = zoomLevels[zoomLevelIdx];
|
||||
panX = e.getX() - (e.getX() - panX) * scale / oldScale;
|
||||
panY = e.getY() - (e.getY() - panY) * scale / oldScale;
|
||||
double iw = RENDER_SIZE * scale, ih = iw;
|
||||
panX = Math.max(50 - iw, Math.min(canvas.getWidth() - 50, panX));
|
||||
panY = Math.max(50 - ih, Math.min(canvas.getHeight() - 50, panY));
|
||||
redraw();
|
||||
});
|
||||
|
||||
@@ -337,6 +343,10 @@ public class WorldMapView extends VBox {
|
||||
scale = newScale;
|
||||
panX = newPanX;
|
||||
panY = newPanY;
|
||||
if (zoomLevels == null && canvas.getWidth() > 0 && mapFxImage != null) {
|
||||
double fitScale = Math.min(canvas.getWidth() / RENDER_SIZE, canvas.getHeight() / RENDER_SIZE);
|
||||
computeZoomLevels(fitScale);
|
||||
}
|
||||
if (zoomLevels != null) {
|
||||
int best = 0;
|
||||
double minDiff = Math.abs(zoomLevels[0] - scale);
|
||||
@@ -392,6 +402,12 @@ public class WorldMapView extends VBox {
|
||||
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
||||
log.debug("[WorldMap] Hintergrund-PNG gerendert ({}×{} px, {} ms)",
|
||||
RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1);
|
||||
// Für ingame-Nutzung: Hintergrund-PNG speichern
|
||||
try {
|
||||
javax.imageio.ImageIO.write(bi, "PNG", CHUNK_CACHE_DIR.resolve("minimap_world.png").toFile());
|
||||
} catch (Exception ex) {
|
||||
log.warn("[WorldMap] Konnte minimap_world.png nicht speichern: {}", ex.getMessage());
|
||||
}
|
||||
|
||||
float[] fDensity = computeForestDensity(model);
|
||||
int[] bakedFstPx = computeForestPixels(model, fDensity, 3.0f);
|
||||
@@ -403,14 +419,15 @@ public class WorldMapView extends VBox {
|
||||
List<float[][]> allPathChains = loadPathChains();
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
|
||||
boolean[] caveMask = loadCaveMask(model.mapData());
|
||||
boolean[] caveMask = WorldMapRenderer.buildCaveFootprintMask(
|
||||
model.mapData(), WorldMapRenderer.VOXEL_SURFACE_RES);
|
||||
// Disk-basierte voxelSurface für Pfad-Splitting: Live-Chunks decken oft nicht
|
||||
// den Tunnel-Bereich ab, sodass dort NaN entstehen würde.
|
||||
float[] vsForPaths = WorldMapRenderer.bakeVoxelSurface(model.mapData());
|
||||
List<float[][]>[] splitChains = splitPathChains(allPathChains, vsForPaths, caveMask);
|
||||
List<float[][]>[] splitChains = WorldMapRenderer.splitPathChains(allPathChains, vsForPaths, caveMask);
|
||||
List<float[][]> newPathChains = splitChains[0]; // nur Oberfläche
|
||||
List<float[]> newBuildingFootprints = filterBuildingsByCave(allBuildingFootprints, caveMask, false);
|
||||
List<PlacedArea> surfaceAreas = filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false);
|
||||
List<float[]> newBuildingFootprints = WorldMapRenderer.filterBuildingsByCave(allBuildingFootprints, caveMask, false);
|
||||
List<PlacedArea> surfaceAreas = WorldMapRenderer.filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false);
|
||||
|
||||
// Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend)
|
||||
final WorldMapRenderModel chunkModel = model;
|
||||
@@ -433,7 +450,22 @@ public class WorldMapView extends VBox {
|
||||
forestPaths = newForestPaths;
|
||||
buildingFootprints = newBuildingFootprints;
|
||||
cachedPathChains = newPathChains;
|
||||
fitToView();
|
||||
if (zoomLevels == null) {
|
||||
fitToView(); // Erstmalig: Zoom initialisieren und auf Karte fitten
|
||||
} else {
|
||||
// Erneuter Load (Refresh): Zoom-Levels neu berechnen falls Canvas geändert,
|
||||
// aber aktuelle Scale und Pan-Position beibehalten.
|
||||
double fitScale = canvas.getWidth() > 0
|
||||
? Math.min(canvas.getWidth() / RENDER_SIZE, canvas.getHeight() / RENDER_SIZE) : 1.0;
|
||||
computeZoomLevels(fitScale);
|
||||
int best = 0;
|
||||
double minDiff = Math.abs(zoomLevels[0] - scale);
|
||||
for (int i = 1; i < zoomLevels.length; i++) {
|
||||
double d = Math.abs(zoomLevels[i] - scale);
|
||||
if (d < minDiff) { minDiff = d; best = i; }
|
||||
}
|
||||
zoomLevelIdx = best;
|
||||
}
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Bereit – " + model.areas().size() + " Areas, " + model.waters().size() + " Wasser");
|
||||
@@ -1081,173 +1113,6 @@ private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model
|
||||
static final int CAVE_MASK_SIZE = 1024;
|
||||
|
||||
/** Lädt die Cave-Tunnel-Maske (true = Untergrund/Tunnel). */
|
||||
static boolean[] loadCaveMask(MapData mapData) {
|
||||
// buildCaveFootprintMask bei 2048px: gleiche Auflösung wie die Höhlenkarte.
|
||||
// buildCaveTunnelMask bei 1024px filtert schmale Tunnel (MIN_FOOTPRINT_PX=40 ≈ 160m²).
|
||||
return WorldMapRenderer.buildCaveFootprintMask(mapData, WorldMapRenderer.VOXEL_SURFACE_RES);
|
||||
}
|
||||
|
||||
/** Prüft ob eine Weltposition im Tunnel-Fußabdruck liegt. Erkennt Maskengröße automatisch. */
|
||||
static boolean isCavePoint(boolean[] mask, float wx, float wz) {
|
||||
int SIZE = (int) Math.round(Math.sqrt(mask.length));
|
||||
int px = Math.round((wx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SIZE - 1));
|
||||
int pz = Math.round((wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SIZE - 1));
|
||||
px = Math.max(0, Math.min(SIZE - 1, px));
|
||||
pz = Math.max(0, Math.min(SIZE - 1, pz));
|
||||
return mask[pz * SIZE + px];
|
||||
}
|
||||
|
||||
/**
|
||||
* Teilt Pfad-Ketten an Tunnel-Grenzen auf.
|
||||
* Nutzt die Cave-Chunk-Maske als räumlichen Filter (Chunk-Level-Erkennung)
|
||||
* kombiniert mit Y-Koordinaten-Prüfung gegen die Voxel-Oberfläche.
|
||||
* Rückgabe: Index 0 = Oberflächensegmente, Index 1 = Tunnel-Segmente.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<float[][]>[] splitPathChains(List<float[][]> chains, float[] voxelSurface,
|
||||
boolean[] caveMask) {
|
||||
List<float[][]> surface = new ArrayList<>();
|
||||
List<float[][]> cave = new ArrayList<>();
|
||||
for (float[][] chain : chains) {
|
||||
splitChainByVoxelSurface(chain, voxelSurface, caveMask, surface, cave);
|
||||
}
|
||||
log.warn("[PathSplit] {} Ketten → {} Oberfläche, {} Höhle",
|
||||
chains.size(), surface.size(), cave.size());
|
||||
if (!chains.isEmpty() && chains.get(0).length > 0) {
|
||||
float[] n = chains.get(0)[0];
|
||||
if (n.length >= 3 && voxelSurface != null) {
|
||||
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
|
||||
float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
|
||||
int px = Math.max(0, Math.min(VS-1, Math.round((n[0]+WH)/WS*(VS-1))));
|
||||
int pz = Math.max(0, Math.min(VS-1, Math.round((n[1]+WH)/WS*(VS-1))));
|
||||
float vs = voxelSurface[pz*VS+px];
|
||||
log.warn("[PathSplit] Erster Knoten: X={} Z={} Y={} voxSurf={} underground={}",
|
||||
n[0], n[1], n[2], vs,
|
||||
!Float.isNaN(vs) && vs > n[2] + PATH_UNDERGROUND_CLEARANCE);
|
||||
}
|
||||
}
|
||||
return new List[]{surface, cave};
|
||||
}
|
||||
|
||||
private static final float PATH_UNDERGROUND_CLEARANCE = 3.0f;
|
||||
|
||||
/**
|
||||
* Prüft ob ein Knoten unter der Erde liegt.
|
||||
* Primär: Cave-Footprint-Maske (2048px, erkennt auch Sculpted-Mesh-Tunnel).
|
||||
* Fallback: VoxelSurface Y-Check (nur wenn VoxelChunk-Material direkt über dem Knoten liegt).
|
||||
*/
|
||||
private static boolean isNodeUnderground(float[] node, float[] voxelSurface, boolean[] caveMask) {
|
||||
if (node.length < 2) return false;
|
||||
if (caveMask != null && isCavePoint(caveMask, node[0], node[1])) return true;
|
||||
if (node.length < 3 || Float.isNaN(node[2])) return false;
|
||||
float wy = node[2];
|
||||
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
|
||||
float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
|
||||
int px = Math.max(0, Math.min(VS - 1, Math.round((node[0] + WH) / WS * (VS - 1))));
|
||||
int pz = Math.max(0, Math.min(VS - 1, Math.round((node[1] + WH) / WS * (VS - 1))));
|
||||
float vs = voxelSurface[pz * VS + px];
|
||||
return !Float.isNaN(vs) && vs > wy + PATH_UNDERGROUND_CLEARANCE;
|
||||
}
|
||||
|
||||
private static float[] lerpNode(float[] a, float[] b, float t) {
|
||||
float ax = a[0], az = a[1], ay = a.length > 2 ? a[2] : 0f;
|
||||
float bx = b[0], bz = b[1], by = b.length > 2 ? b[2] : 0f;
|
||||
return new float[]{ax + (bx - ax) * t, az + (bz - az) * t, ay + (by - ay) * t};
|
||||
}
|
||||
|
||||
private static void splitChainByVoxelSurface(float[][] chain, float[] voxelSurface,
|
||||
boolean[] caveMask,
|
||||
List<float[][]> surface, List<float[][]> cave) {
|
||||
if (chain.length < 2) return;
|
||||
List<float[]> current = new ArrayList<>();
|
||||
boolean currentCave = isNodeUnderground(chain[0], voxelSurface, caveMask);
|
||||
current.add(chain[0]);
|
||||
|
||||
for (int i = 0; i < chain.length - 1; i++) {
|
||||
float[] a = chain[i];
|
||||
float[] b = chain[i + 1];
|
||||
boolean bCave = isNodeUnderground(b, voxelSurface, caveMask);
|
||||
|
||||
if (currentCave == bCave) {
|
||||
if (!currentCave) {
|
||||
// Beide Oberfläche: 7-Punkte-Abtastung, um verborgene Tunnel zu erkennen
|
||||
// (kein Knoten im Tunnel, aber das Segment kreuzt ein unterirdisches Gebiet).
|
||||
float[] sample = null;
|
||||
for (int s = 1; s <= 7 && sample == null; s++) {
|
||||
float[] p = lerpNode(a, b, s / 8f);
|
||||
if (isNodeUnderground(p, voxelSurface, caveMask)) { sample = p; }
|
||||
}
|
||||
if (sample != null) {
|
||||
float[] c1 = findPathCrossing(a, sample, voxelSurface, caveMask, false);
|
||||
float[] c2 = findPathCrossing(sample, b, voxelSurface, caveMask, true);
|
||||
current.add(c1);
|
||||
if (current.size() >= 2) surface.add(current.toArray(new float[0][]));
|
||||
current = new ArrayList<>();
|
||||
current.add(c1); current.add(sample); current.add(c2);
|
||||
cave.add(current.toArray(new float[0][]));
|
||||
current = new ArrayList<>();
|
||||
current.add(c2); current.add(b);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
current.add(b);
|
||||
} else {
|
||||
float[] crossing = findPathCrossing(a, b, voxelSurface, caveMask, currentCave);
|
||||
current.add(crossing);
|
||||
if (current.size() >= 2) {
|
||||
(currentCave ? cave : surface).add(current.toArray(new float[0][]));
|
||||
}
|
||||
current = new ArrayList<>();
|
||||
current.add(crossing);
|
||||
current.add(b);
|
||||
currentCave = bCave;
|
||||
}
|
||||
}
|
||||
if (current.size() >= 2) {
|
||||
(currentCave ? cave : surface).add(current.toArray(new float[0][]));
|
||||
}
|
||||
}
|
||||
|
||||
/** Bisektion mit Y-Interpolation: 12 Iterationen ≈ 0.05m Genauigkeit. */
|
||||
private static float[] findPathCrossing(float[] a, float[] b, float[] vs, boolean[] caveMask,
|
||||
boolean aIsUnderground) {
|
||||
float ax = a[0], az = a[1], ay = a.length > 2 ? a[2] : 0f;
|
||||
float bx = b[0], bz = b[1], by = b.length > 2 ? b[2] : 0f;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
float mx = (ax + bx) * 0.5f, mz = (az + bz) * 0.5f, my = (ay + by) * 0.5f;
|
||||
float[] mid = {mx, mz, my};
|
||||
if (isNodeUnderground(mid, vs, caveMask) == aIsUnderground) { ax = mx; az = mz; ay = my; }
|
||||
else { bx = mx; bz = mz; by = my; }
|
||||
}
|
||||
return new float[]{(ax + bx) * 0.5f, (az + bz) * 0.5f, (ay + by) * 0.5f};
|
||||
}
|
||||
|
||||
/** Filtert Gebäude-Footprints: wantCave=true → nur Untergrund, false → nur Oberfläche. */
|
||||
static List<float[]> filterBuildingsByCave(List<float[]> footprints, boolean[] caveMask, boolean wantCave) {
|
||||
List<float[]> result = new ArrayList<>();
|
||||
for (float[] fp : footprints) {
|
||||
if (isCavePoint(caveMask, fp[0], fp[1]) == wantCave) result.add(fp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Filtert Areas anhand des Polygon-Schwerpunkts. */
|
||||
static List<PlacedArea> filterAreasByCave(List<PlacedArea> areas, boolean[] caveMask, boolean wantCave) {
|
||||
List<PlacedArea> result = new ArrayList<>();
|
||||
for (PlacedArea area : areas) {
|
||||
float[] xs = area.pointsX(), zs = area.pointsZ();
|
||||
if (xs == null || xs.length == 0) {
|
||||
if (!wantCave) result.add(area);
|
||||
continue;
|
||||
}
|
||||
float cx = 0, cz = 0;
|
||||
for (int i = 0; i < xs.length; i++) { cx += xs[i]; cz += zs[i]; }
|
||||
cx /= xs.length; cz /= zs.length;
|
||||
if (isCavePoint(caveMask, cx, cz) == wantCave) result.add(area);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static WritableImage toWritableImage(int[] pixels, int size) {
|
||||
WritableImage img = new WritableImage(size, size);
|
||||
img.getPixelWriter().setPixels(0, 0, size, size, PixelFormat.getIntArgbInstance(), pixels, 0, size);
|
||||
|
||||
Reference in New Issue
Block a user