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:
@@ -203,16 +203,22 @@ public class CaveMapView extends VBox {
|
|||||||
List<PlacedModel> models = PlacedModelIO.load();
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
|
|
||||||
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
|
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
|
||||||
List<float[][]> paths = WorldMapView.loadPathChains();
|
List<float[][]> allPaths = WorldMapView.loadPathChains();
|
||||||
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
|
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
|
||||||
List<float[]> footprints = WorldMapView.computeBuildingFootprintsSimple(models);
|
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(() -> {
|
Platform.runLater(() -> {
|
||||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||||
cachedPathChains = paths;
|
cachedPathChains = cavePaths;
|
||||||
tunnelEntrances = entrances;
|
tunnelEntrances = entrances;
|
||||||
cachedAreas = new ArrayList<>(areas);
|
cachedAreas = caveAreas;
|
||||||
buildingFootprints = footprints;
|
buildingFootprints = caveFootprints;
|
||||||
fitToView();
|
fitToView();
|
||||||
redraw();
|
redraw();
|
||||||
progress.setVisible(false);
|
progress.setVisible(false);
|
||||||
|
|||||||
@@ -399,10 +399,17 @@ public class WorldMapView extends VBox {
|
|||||||
List<float[][]> newCliffPaths = buildCliffPaths(model.mapData(), model.voxelSurface());
|
List<float[][]> newCliffPaths = buildCliffPaths(model.mapData(), model.voxelSurface());
|
||||||
List<float[][]> newForestPaths = buildForestOutlinePaths(fDensity, 512, 3.0f);
|
List<float[][]> newForestPaths = buildForestOutlinePaths(fDensity, 512, 3.0f);
|
||||||
|
|
||||||
List<float[]> newBuildingFootprints = computeBuildingFootprints(model, findAssetRoot());
|
List<float[]> allBuildingFootprints = computeBuildingFootprints(model, findAssetRoot());
|
||||||
List<float[][]> newPathChains = loadPathChains();
|
List<float[][]> allPathChains = loadPathChains();
|
||||||
List<float[]> newTunnelEntrances = computeTunnelEntrances(model.mapData());
|
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)
|
// 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…"));
|
||||||
@@ -414,7 +421,7 @@ public class WorldMapView extends VBox {
|
|||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
currentModel = model;
|
currentModel = model;
|
||||||
cachedAreas = new ArrayList<>(model.areas());
|
cachedAreas = surfaceAreas;
|
||||||
cachedLocs = new ArrayList<>(model.locations());
|
cachedLocs = new ArrayList<>(model.locations());
|
||||||
mapBuffered = bi;
|
mapBuffered = bi;
|
||||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||||
@@ -1083,6 +1090,107 @@ public class WorldMapView extends VBox {
|
|||||||
return result;
|
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) {
|
private static WritableImage toWritableImage(int[] pixels, int size) {
|
||||||
WritableImage img = new WritableImage(size, size);
|
WritableImage img = new WritableImage(size, size);
|
||||||
img.getPixelWriter().setPixels(0, 0, size, size, PixelFormat.getIntArgbInstance(), pixels, 0, size);
|
img.getPixelWriter().setPixels(0, 0, size, size, PixelFormat.getIntArgbInstance(), pixels, 0, size);
|
||||||
|
|||||||
Reference in New Issue
Block a user