Karte: 3 Korrekturen nach User-Feedback

- Weltkarte: Cave-Overlay (brown fill + Kontur) vollständig entfernt,
  war nie gewünscht; drawTunnelOverlay tut nun nichts
- Pfad-Detection: Cave-Maske als Filter entfernt; Grund: MIN_FOOTPRINT_
  Filter in buildCaveMasks (≥40 Pixel bei 1024px) schmeißt schmale
  Tunnel raus und würde Tunnel-Segmente blocken. Stattdessen reiner
  Y-Check: voxelSurface > pathY + 3m (Tunneldecke direkt über dem Weg)
- Höhlenkarte: Pixel-Bild ersetzt durch Vektor-Füllung (gc.fill auf
  tunnelContourPaths) → bleibt bei jedem Zoom-Level scharf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 12:06:52 +02:00
parent f29580cb3b
commit 7f5be2b65d
2 changed files with 26 additions and 51 deletions

View File

@@ -381,15 +381,30 @@ public class CaveMapView extends VBox {
} }
GraphicsContext gc = canvas.getGraphicsContext2D(); GraphicsContext gc = canvas.getGraphicsContext2D();
double w = canvas.getWidth(), h = canvas.getHeight(); double w = canvas.getWidth(), h = canvas.getHeight();
gc.setFill(Color.rgb(14, 14, 14)); gc.setFill(Color.rgb(28, 28, 28)); // passt zu buildCaveMap-Hintergrund 0x1c1c1c
gc.fillRect(0, 0, w, h); gc.fillRect(0, 0, w, h);
if (mapFxImage == null) { if (mapFxImage == null) {
gc.setFill(Color.GRAY); gc.setFill(Color.GRAY);
gc.fillText("Höhlenkarte wird geladen…", 20, 40); gc.fillText("Höhlenkarte wird geladen…", 20, 40);
return; return;
} }
// Vektor-Füllung: scharf bei jedem Zoom-Level
if (!tunnelContourPaths.isEmpty()) {
gc.save();
gc.setFill(Color.rgb(182, 138, 80)); // Dirt-Farbe 0xb68a50
for (float[][] path : tunnelContourPaths) {
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.fill();
}
gc.restore();
} else {
// Pixel-Fallback, falls Konturpfade noch nicht berechnet
gc.setImageSmoothing(true); gc.setImageSmoothing(true);
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale); gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
}
if (cbEntrances.isSelected()) { if (cbEntrances.isSelected()) {
drawTunnelContourOverlay(gc); drawTunnelContourOverlay(gc);
drawTunnelEntrancesOverlay(gc); drawTunnelEntrancesOverlay(gc);

View File

@@ -127,8 +127,6 @@ public class WorldMapView extends VBox {
private List<float[]> buildingFootprints = new ArrayList<>(); private List<float[]> buildingFootprints = new ArrayList<>();
private List<float[][]> cachedPathChains = new ArrayList<>(); private List<float[][]> cachedPathChains = new ArrayList<>();
private List<float[][]> tunnelPaths = new ArrayList<>();
private WritableImage bakedTunnelOverlay = null;
private static final java.util.concurrent.ConcurrentHashMap<String, float[]> MODEL_BOUNDS_CACHE private static final java.util.concurrent.ConcurrentHashMap<String, float[]> MODEL_BOUNDS_CACHE
= new java.util.concurrent.ConcurrentHashMap<>(); = new java.util.concurrent.ConcurrentHashMap<>();
private static final float[] DEFAULT_EXTENTS = {5f, 5f}; private static final float[] DEFAULT_EXTENTS = {5f, 5f};
@@ -411,9 +409,6 @@ public class WorldMapView extends VBox {
List<float[]> newBuildingFootprints = filterBuildingsByCave(allBuildingFootprints, caveMask, false); List<float[]> newBuildingFootprints = filterBuildingsByCave(allBuildingFootprints, caveMask, false);
List<PlacedArea> surfaceAreas = filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false); List<PlacedArea> surfaceAreas = filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false);
List<float[][]> newTunnelPaths = buildTunnelContourPaths(caveMask);
int[] tunnelFillPx = buildTunnelFillPixels(caveMask);
// Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend) // Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend)
final WorldMapRenderModel chunkModel = model; final WorldMapRenderModel chunkModel = model;
Platform.runLater(() -> statusLbl.setText("Generiere Hires-Chunks…")); Platform.runLater(() -> statusLbl.setText("Generiere Hires-Chunks…"));
@@ -435,8 +430,6 @@ public class WorldMapView extends VBox {
forestPaths = newForestPaths; forestPaths = newForestPaths;
buildingFootprints = newBuildingFootprints; buildingFootprints = newBuildingFootprints;
cachedPathChains = newPathChains; cachedPathChains = newPathChains;
tunnelPaths = newTunnelPaths;
bakedTunnelOverlay = toWritableImage(tunnelFillPx, RENDER_SIZE);
fitToView(); fitToView();
redraw(); redraw();
progress.setVisible(false); progress.setVisible(false);
@@ -1046,25 +1039,7 @@ public class WorldMapView extends VBox {
result.add(new float[]{wx, wz, dirX, dirZ, radius}); result.add(new float[]{wx, wz, dirX, dirZ, radius});
} }
private void drawTunnelOverlay(GraphicsContext gc) { private void drawTunnelOverlay(GraphicsContext gc) { /* Kein Tunnel-Overlay auf Weltkarte */ }
double cW = canvas.getWidth(), cH = canvas.getHeight();
if (bakedTunnelOverlay != null) {
gc.save();
gc.setImageSmoothing(false);
gc.drawImage(bakedTunnelOverlay, -panX / scale, -panY / scale, cW / scale, cH / scale, 0, 0, cW, cH);
gc.restore();
}
if (!tunnelPaths.isEmpty()) {
gc.save();
gc.setStroke(Color.rgb(40, 22, 5, 0.90));
gc.setLineWidth(overlayLineWidth() + 0.5);
gc.setLineDashes();
gc.setLineCap(StrokeLineCap.ROUND);
gc.setLineJoin(StrokeLineJoin.ROUND);
for (float[][] path : tunnelPaths) drawCoastPath(gc, path, cW, cH, 8);
gc.restore();
}
}
static List<float[][]> buildTunnelContourPaths(boolean[] caveMask) { static List<float[][]> buildTunnelContourPaths(boolean[] caveMask) {
final int SIZE = CAVE_MASK_SIZE; final int SIZE = CAVE_MASK_SIZE;
@@ -1077,22 +1052,7 @@ public class WorldMapView extends VBox {
return paths.stream().filter(p -> pathPerimeterSq(p) >= MIN_PERI_SQ).collect(Collectors.toList()); return paths.stream().filter(p -> pathPerimeterSq(p) >= MIN_PERI_SQ).collect(Collectors.toList());
} }
private static int[] buildTunnelFillPixels(boolean[] caveMask) { private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model, Path assetRoot) {
final int SIZE = CAVE_MASK_SIZE;
final int OUT = RENDER_SIZE;
final int FILL = 0x60_3D2208; // 37% alpha, very dark earthy brown
int[] px = new int[OUT * OUT];
for (int py = 0; py < OUT; py++) {
int mz = py * SIZE / OUT;
for (int bx = 0; bx < OUT; bx++) {
int mx = bx * SIZE / OUT;
if (caveMask[mz * SIZE + mx]) px[py * OUT + bx] = FILL;
}
}
return px;
}
private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model, Path assetRoot) {
List<float[]> result = new ArrayList<>(); List<float[]> result = new ArrayList<>();
for (PlacedModel pm : model.models()) { for (PlacedModel pm : model.models()) {
if (!isBuilding(pm)) continue; if (!isBuilding(pm)) continue;
@@ -1152,16 +1112,16 @@ public class WorldMapView extends VBox {
/** /**
* Prüft ob ein Knoten unter der Erde liegt. * Prüft ob ein Knoten unter der Erde liegt.
* Bedingung: Position liegt in einem Tunnel-Chunk-Bereich (Cave-Maske) * Bedingung: Das höchste Solid-Voxel in dieser X,Z-Spalte liegt ≥ PATH_UNDERGROUND_CLEARANCE
* UND das höchste Solid in dieser Spalte ist ≥ PATH_UNDERGROUND_CLEARANCE über der Knoten-Y. * über der Knoten-Y — d.h. es gibt Material direkt über dem Weg (Tunneldecke).
* Die Cave-Maske wird bewusst NICHT genutzt: ihr MIN_FOOTPRINT_Filter bei niedrige Auflösung
* filtert schmale Tunnel heraus und würde echte Tunnel-Segmente blockieren.
*/ */
private static boolean isNodeUnderground(float[] node, float[] voxelSurface, boolean[] caveMask) { private static boolean isNodeUnderground(float[] node, float[] voxelSurface,
@SuppressWarnings("unused") boolean[] caveMask) {
if (node.length < 3) return false; if (node.length < 3) return false;
float wy = node[2]; float wy = node[2];
if (Float.isNaN(wy)) return false; if (Float.isNaN(wy)) return false;
// Cave-Maske (Chunk-Level): nur Bereiche nahe Tunneln prüfen
if (caveMask != null && !isCavePoint(caveMask, node[0], node[1])) return false;
// Y-Prüfung: Knoten muss unter festem Voxel-Material liegen
int VS = WorldMapRenderer.VOXEL_SURFACE_RES; int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE; float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
int px = Math.round((node[0] + WH) / WS * (VS - 1)); int px = Math.round((node[0] + WH) / WS * (VS - 1));