diff --git a/blight-editor/src/main/java/de/blight/editor/ui/CaveMapView.java b/blight-editor/src/main/java/de/blight/editor/ui/CaveMapView.java index 9a49fc7..925c96b 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/CaveMapView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/CaveMapView.java @@ -198,21 +198,27 @@ public class CaveMapView extends VBox { Thread t = new Thread(() -> { try { - MapData mapData = MapIO.load(); - List areas = AreaIO.load(); + MapData mapData = MapIO.load(); + List areas = AreaIO.load(); List models = PlacedModelIO.load(); - BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE); - List paths = WorldMapView.loadPathChains(); - List entrances = WorldMapView.computeTunnelEntrances(mapData); - List footprints = WorldMapView.computeBuildingFootprintsSimple(models); + BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE); + List allPaths = WorldMapView.loadPathChains(); + List entrances = WorldMapView.computeTunnelEntrances(mapData); + List allFootprints = WorldMapView.computeBuildingFootprintsSimple(models); + + boolean[] caveMask = WorldMapView.loadCaveMask(mapData); + List[] splitChains = WorldMapView.splitPathChains(allPaths, caveMask); + List cavePaths = splitChains[1]; // nur Tunnel-Segmente + List caveFootprints = WorldMapView.filterBuildingsByCave(allFootprints, caveMask, true); + List caveAreas = WorldMapView.filterAreasByCave(new ArrayList<>(areas), caveMask, true); Platform.runLater(() -> { mapFxImage = SwingFXUtils.toFXImage(bi, null); - cachedPathChains = paths; + cachedPathChains = cavePaths; tunnelEntrances = entrances; - cachedAreas = new ArrayList<>(areas); - buildingFootprints = footprints; + cachedAreas = caveAreas; + buildingFootprints = caveFootprints; fitToView(); redraw(); progress.setVisible(false); 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 82faff5..9c3573d 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 @@ -399,9 +399,16 @@ public class WorldMapView extends VBox { List newCliffPaths = buildCliffPaths(model.mapData(), model.voxelSurface()); List newForestPaths = buildForestOutlinePaths(fDensity, 512, 3.0f); - List newBuildingFootprints = computeBuildingFootprints(model, findAssetRoot()); - List newPathChains = loadPathChains(); - List newTunnelEntrances = computeTunnelEntrances(model.mapData()); + List allBuildingFootprints = computeBuildingFootprints(model, findAssetRoot()); + List allPathChains = loadPathChains(); + List newTunnelEntrances = computeTunnelEntrances(model.mapData()); + + Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…")); + boolean[] caveMask = loadCaveMask(model.mapData()); + List[] splitChains = splitPathChains(allPathChains, caveMask); + List newPathChains = splitChains[0]; // nur Oberfläche + List newBuildingFootprints = filterBuildingsByCave(allBuildingFootprints, caveMask, false); + List surfaceAreas = filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false); // Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend) final WorldMapRenderModel chunkModel = model; @@ -414,7 +421,7 @@ public class WorldMapView extends VBox { Platform.runLater(() -> { currentModel = model; - cachedAreas = new ArrayList<>(model.areas()); + cachedAreas = surfaceAreas; cachedLocs = new ArrayList<>(model.locations()); mapBuffered = bi; mapFxImage = SwingFXUtils.toFXImage(bi, null); @@ -1083,6 +1090,107 @@ public class WorldMapView extends VBox { return result; } + // ── Cave-Mask: Trennung Oberfläche / Untergrund ─────────────────────────── + + static final int CAVE_MASK_SIZE = 1024; + + /** Lädt die Cave-Tunnel-Maske (true = Untergrund/Tunnel). */ + static boolean[] loadCaveMask(MapData mapData) { + return WorldMapRenderer.buildCaveTunnelMask(mapData, CAVE_MASK_SIZE); + } + + /** Prüft ob eine Weltposition unterhalb von Tunnel-Voxeln liegt. */ + 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]; + } + + /** + * Teilt Pfad-Ketten an Tunnel-Grenzen auf. + * Rückgabe: Index 0 = Oberflächensegmente, Index 1 = Tunnel-Segmente. + */ + @SuppressWarnings("unchecked") + static List[] splitPathChains(List chains, boolean[] caveMask) { + List surface = new ArrayList<>(); + List cave = new ArrayList<>(); + for (float[][] chain : chains) { + splitChainByCaveMask(chain, caveMask, surface, cave); + } + return new List[]{surface, cave}; + } + + private static void splitChainByCaveMask(float[][] chain, boolean[] caveMask, + List surface, List cave) { + if (chain.length < 2) return; + List current = new ArrayList<>(); + boolean currentCave = isCavePoint(caveMask, chain[0][0], chain[0][1]); + current.add(chain[0]); + + for (int i = 0; i < chain.length - 1; i++) { + float[] a = chain[i]; + float[] b = chain[i + 1]; + boolean bCave = isCavePoint(caveMask, b[0], b[1]); + + if (currentCave == bCave) { + current.add(b); + } else { + // Genauen Grenzpunkt per Bisektion ermitteln + float[] crossing = findCaveCrossing(a, b, 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: findet den Weltpunkt, an dem die Cave-Maske umspringt (12 Iterationen ≈ 0.05m Genauigkeit). */ + private static float[] findCaveCrossing(float[] a, float[] b, boolean[] mask, boolean aIsCave) { + float ax = a[0], az = a[1], bx = b[0], bz = b[1]; + for (int i = 0; i < 12; i++) { + float mx = (ax + bx) * 0.5f, mz = (az + bz) * 0.5f; + if (isCavePoint(mask, mx, mz) == aIsCave) { ax = mx; az = mz; } + else { bx = mx; bz = mz; } + } + return new float[]{(ax + bx) * 0.5f, (az + bz) * 0.5f}; + } + + /** Filtert Gebäude-Footprints: wantCave=true → nur Untergrund, false → nur Oberfläche. */ + static List filterBuildingsByCave(List footprints, boolean[] caveMask, boolean wantCave) { + List 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 filterAreasByCave(List areas, boolean[] caveMask, boolean wantCave) { + List 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);