diff --git a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java index 67a58a2..24b30a4 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java @@ -404,7 +404,10 @@ public class WorldMapView extends VBox { Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…")); boolean[] caveMask = loadCaveMask(model.mapData()); - List[] splitChains = splitPathChains(allPathChains, model.voxelSurface(), caveMask); + // 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[] splitChains = splitPathChains(allPathChains, vsForPaths, caveMask); List newPathChains = splitChains[0]; // nur Oberfläche List newBuildingFootprints = filterBuildingsByCave(allBuildingFootprints, caveMask, false); List surfaceAreas = filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false); @@ -1042,7 +1045,7 @@ public class WorldMapView extends VBox { private void drawTunnelOverlay(GraphicsContext gc) { /* Kein Tunnel-Overlay auf Weltkarte */ } static List buildTunnelContourPaths(boolean[] caveMask) { - final int SIZE = CAVE_MASK_SIZE; + final int SIZE = (int) Math.round(Math.sqrt(caveMask.length)); // 1024 oder 2048 float[] h = new float[SIZE * SIZE]; for (int i = 0; i < caveMask.length; i++) h[i] = caveMask[i] ? -1f : 1f; List segs = new ArrayList<>(); @@ -1079,16 +1082,19 @@ private static List computeBuildingFootprints(WorldMapRenderModel model /** Lädt die Cave-Tunnel-Maske (true = Untergrund/Tunnel). */ static boolean[] loadCaveMask(MapData mapData) { - return WorldMapRenderer.buildCaveTunnelMask(mapData, CAVE_MASK_SIZE); + // 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 unterhalb von Tunnel-Voxeln liegt. */ + /** Prüft ob eine Weltposition im Tunnel-Fußabdruck liegt. Erkennt Maskengröße automatisch. */ static boolean isCavePoint(boolean[] mask, float wx, float wz) { - int px = Math.round((wx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (CAVE_MASK_SIZE - 1)); - int pz = Math.round((wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (CAVE_MASK_SIZE - 1)); - px = Math.max(0, Math.min(CAVE_MASK_SIZE - 1, px)); - pz = Math.max(0, Math.min(CAVE_MASK_SIZE - 1, pz)); - return mask[pz * CAVE_MASK_SIZE + px]; + 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]; } /** @@ -1105,6 +1111,21 @@ private static List computeBuildingFootprints(WorldMapRenderModel model 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}; } @@ -1112,26 +1133,28 @@ private static List computeBuildingFootprints(WorldMapRenderModel model /** * Prüft ob ein Knoten unter der Erde liegt. - * Bedingung: Das höchste Solid-Voxel in dieser X,Z-Spalte liegt ≥ PATH_UNDERGROUND_CLEARANCE - * ü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. + * 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, - @SuppressWarnings("unused") boolean[] caveMask) { - if (node.length < 3) return false; + 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]; - if (Float.isNaN(wy)) return false; int VS = WorldMapRenderer.VOXEL_SURFACE_RES; float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE; - int px = Math.round((node[0] + WH) / WS * (VS - 1)); - int pz = Math.round((node[1] + WH) / WS * (VS - 1)); - px = Math.max(0, Math.min(VS - 1, px)); - pz = Math.max(0, Math.min(VS - 1, pz)); + 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 surface, List cave) { @@ -1146,6 +1169,27 @@ private static List computeBuildingFootprints(WorldMapRenderModel model 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);