Karte: Pfade/Gebäude/Areas nach Oberfläche vs. Untergrund trennen

Pfad-Ketten werden exakt an Tunnel-Grenzen per Bisektion aufgesplittet
(12 Iterationen ≈ 0.05m Genauigkeit). Gebäude und Areas werden anhand
des Footprint-/Schwerpunkt-Punkts der Cave-Maske zugeordnet.

- WorldMapView: zeigt nur Oberflächen-Segmente (splitChains[0])
- CaveMapView:  zeigt nur Tunnel-Segmente (splitChains[1])
- Cave-Maske: buildCaveTunnelMask bei 1024px, einmal pro Lade-Zyklus

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 22:19:20 +02:00
parent 6e7e94371e
commit 174579cd0b
2 changed files with 127 additions and 13 deletions

View File

@@ -198,21 +198,27 @@ public class CaveMapView extends VBox {
Thread t = new Thread(() -> {
try {
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load();
List<PlacedModel> models = PlacedModelIO.load();
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
List<float[][]> paths = WorldMapView.loadPathChains();
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
List<float[]> footprints = WorldMapView.computeBuildingFootprintsSimple(models);
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
List<float[][]> allPaths = WorldMapView.loadPathChains();
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
List<float[]> allFootprints = WorldMapView.computeBuildingFootprintsSimple(models);
boolean[] caveMask = WorldMapView.loadCaveMask(mapData);
List<float[][]>[] splitChains = WorldMapView.splitPathChains(allPaths, caveMask);
List<float[][]> cavePaths = splitChains[1]; // nur Tunnel-Segmente
List<float[]> caveFootprints = WorldMapView.filterBuildingsByCave(allFootprints, caveMask, true);
List<PlacedArea> 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);

View File

@@ -399,9 +399,16 @@ public class WorldMapView extends VBox {
List<float[][]> newCliffPaths = buildCliffPaths(model.mapData(), model.voxelSurface());
List<float[][]> newForestPaths = buildForestOutlinePaths(fDensity, 512, 3.0f);
List<float[]> newBuildingFootprints = computeBuildingFootprints(model, findAssetRoot());
List<float[][]> newPathChains = loadPathChains();
List<float[]> newTunnelEntrances = computeTunnelEntrances(model.mapData());
List<float[]> allBuildingFootprints = computeBuildingFootprints(model, findAssetRoot());
List<float[][]> allPathChains = loadPathChains();
List<float[]> newTunnelEntrances = computeTunnelEntrances(model.mapData());
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
boolean[] caveMask = loadCaveMask(model.mapData());
List<float[][]>[] splitChains = splitPathChains(allPathChains, 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);
// 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<float[][]>[] splitPathChains(List<float[][]> chains, boolean[] caveMask) {
List<float[][]> surface = new ArrayList<>();
List<float[][]> 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<float[][]> surface, List<float[][]> cave) {
if (chain.length < 2) return;
List<float[]> 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<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);