Karte: Y-basierter Pfad-Split, Label/Export in Höhlenkarte, Sync-Fix
Pfad-Trennung (Oberfläche vs. Untergrund): - loadPathChains() speichert jetzt Y-Koordinate an Index 2 - splitPathChains() prüft voxelSurface(x,z) > node_y + 3m statt 2D-Cave-Maske; erkennt damit alle Tunnel-Typen korrekt - Bisektion interpoliert Y für präzisen Grenzpunkt Höhlenkarte Features: - Label-Toggle (Bearbeiten-Modus für Area-Labels) + Klick-Handler - PNG-Export (⇩) rendert 4096×4096 via buildCaveMap() - mapData wird gecacht für späteren Export ohne Reload Zoom/Position-Sync: - Tab-Listener liest Werte vor dem Wechsel, übergibt sie per Platform.runLater() NACH JavaFX-Layout → kein Sprung mehr Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5702,13 +5702,15 @@ public class EditorApp extends Application {
|
||||
weltkartTab = new Tab("Weltkarte", worldMapView);
|
||||
weltkartTab.setClosable(false);
|
||||
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected && !worldMapView.isLoaded()) {
|
||||
if (!isSelected) return;
|
||||
if (!worldMapView.isLoaded()) {
|
||||
worldMapView.loadAndRender();
|
||||
} else if (isSelected) {
|
||||
} else if (caveMapView != null && caveMapView.isLoaded()) {
|
||||
// Sync nach Layout-Pass damit Canvas-Größe korrekt ist
|
||||
final double s = caveMapView.getScale(), px = caveMapView.getPanX(), py = caveMapView.getPanY();
|
||||
javafx.application.Platform.runLater(() -> worldMapView.setView(s, px, py));
|
||||
} else {
|
||||
worldMapView.ensureFitToView();
|
||||
if (caveMapView != null && caveMapView.isLoaded()) {
|
||||
worldMapView.setView(caveMapView.getScale(), caveMapView.getPanX(), caveMapView.getPanY());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5716,13 +5718,15 @@ public class EditorApp extends Application {
|
||||
caveMapTab = new Tab("Höhlenkarte", caveMapView);
|
||||
caveMapTab.setClosable(false);
|
||||
caveMapTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected && !caveMapView.isLoaded()) {
|
||||
if (!isSelected) return;
|
||||
if (!caveMapView.isLoaded()) {
|
||||
caveMapView.loadAndRender();
|
||||
} else if (isSelected) {
|
||||
} else if (worldMapView.isMapReady()) {
|
||||
// Sync nach Layout-Pass damit Canvas-Größe korrekt ist
|
||||
final double s = worldMapView.getScale(), px = worldMapView.getPanX(), py = worldMapView.getPanY();
|
||||
javafx.application.Platform.runLater(() -> caveMapView.setView(s, px, py));
|
||||
} else {
|
||||
caveMapView.ensureFitToView();
|
||||
if (worldMapView.isMapReady()) {
|
||||
caveMapView.setView(worldMapView.getScale(), worldMapView.getPanX(), worldMapView.getPanY());
|
||||
}
|
||||
}
|
||||
});
|
||||
caveMapView.setFullscreenCallbacks(
|
||||
|
||||
@@ -6,6 +6,7 @@ import de.blight.common.map.WorldMapRenderer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.control.*;
|
||||
@@ -15,9 +16,13 @@ import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.ArcType;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.scene.shape.StrokeLineJoin;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -42,15 +47,18 @@ public class CaveMapView extends VBox {
|
||||
private final StackPane canvasPane = new StackPane();
|
||||
private final AtomicBoolean loading = new AtomicBoolean(false);
|
||||
|
||||
private final Supplier<Stage> stageSupplier;
|
||||
|
||||
// Layer-Checkboxen
|
||||
private final CheckBox cbEntrances = layerCheck("Eingänge", true);
|
||||
private final CheckBox cbBuildings = layerCheck("Gebäude", true);
|
||||
private final CheckBox cbPaths = layerCheck("Wege", true);
|
||||
private final CheckBox cbAreas = layerCheck("Areas", true);
|
||||
|
||||
private final Button fullscreenBtn = new Button(">>");
|
||||
private final Button backBtn = new Button("<<");
|
||||
private boolean isFullscreen = false;
|
||||
private final ToggleButton labelBtn = new ToggleButton("Label");
|
||||
private final Button fullscreenBtn = new Button(">>");
|
||||
private final Button backBtn = new Button("<<");
|
||||
private boolean isFullscreen = false;
|
||||
private double savedScale, savedPanX, savedPanY;
|
||||
private Runnable enterFullscreenCallback = null;
|
||||
private Runnable exitFullscreenCallback = null;
|
||||
@@ -60,6 +68,7 @@ public class CaveMapView extends VBox {
|
||||
private List<float[]> tunnelEntrances = new ArrayList<>();
|
||||
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
||||
private List<float[]> buildingFootprints = new ArrayList<>();
|
||||
private MapData cachedMapData = null; // für Export
|
||||
|
||||
private double panX = 0, panY = 0, scale = 1.0;
|
||||
private double[] zoomLevels = null;
|
||||
@@ -67,6 +76,8 @@ public class CaveMapView extends VBox {
|
||||
private boolean autoLoaded = false;
|
||||
|
||||
public CaveMapView(Supplier<Stage> stageSupplier) {
|
||||
this.stageSupplier = stageSupplier;
|
||||
|
||||
cbEntrances.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbBuildings.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbPaths.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
@@ -83,6 +94,14 @@ public class CaveMapView extends VBox {
|
||||
refreshBtn.setTooltip(new Tooltip("Aktualisieren"));
|
||||
refreshBtn.setOnAction(e -> loadAndRender());
|
||||
|
||||
Button exportBtn = new Button("⇩");
|
||||
exportBtn.setTooltip(new Tooltip("Als PNG exportieren…"));
|
||||
exportBtn.setOnAction(e -> exportPng());
|
||||
|
||||
labelBtn.setStyle("-fx-font-size: 11;");
|
||||
labelBtn.selectedProperty().addListener((obs, o, n) ->
|
||||
canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT));
|
||||
|
||||
fullscreenBtn.setTooltip(new Tooltip("Maximieren"));
|
||||
backBtn.setTooltip(new Tooltip("Minimieren"));
|
||||
fullscreenBtn.setOnAction(e -> enterFullscreen());
|
||||
@@ -92,7 +111,11 @@ public class CaveMapView extends VBox {
|
||||
ToolBar bar = new ToolBar(
|
||||
layerMenu,
|
||||
new Separator(),
|
||||
new Label("Bearbeiten:"),
|
||||
labelBtn,
|
||||
new Separator(),
|
||||
refreshBtn,
|
||||
exportBtn,
|
||||
new Separator(),
|
||||
fullscreenBtn,
|
||||
backBtn
|
||||
@@ -134,7 +157,6 @@ public class CaveMapView extends VBox {
|
||||
public double getPanX() { return panX; }
|
||||
public double getPanY() { return panY; }
|
||||
|
||||
/** Setzt Zoom und Position direkt (für Tab-Wechsel-Sync). */
|
||||
public void setView(double newScale, double newPanX, double newPanY) {
|
||||
scale = newScale;
|
||||
panX = newPanX;
|
||||
@@ -171,6 +193,12 @@ public class CaveMapView extends VBox {
|
||||
panY = ds[3] + (e.getY()-ds[1]);
|
||||
redraw();
|
||||
});
|
||||
canvas.setOnMouseReleased(e -> {
|
||||
boolean wasDrag = Math.abs(e.getX() - ds[0]) >= 5 || Math.abs(e.getY() - ds[1]) >= 5;
|
||||
if (!wasDrag && labelBtn.isSelected()) {
|
||||
handleLabelPlacement(e.getX(), e.getY());
|
||||
}
|
||||
});
|
||||
canvas.setOnMouseMoved(e -> {
|
||||
if (mapFxImage == null) return;
|
||||
double wx = (e.getX() - panX) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
|
||||
@@ -179,6 +207,60 @@ public class CaveMapView extends VBox {
|
||||
});
|
||||
}
|
||||
|
||||
private void handleLabelPlacement(double canvasX, double canvasY) {
|
||||
if (cachedAreas.isEmpty()) return;
|
||||
float wx = toWorldX(canvasX);
|
||||
float wz = toWorldZ(canvasY);
|
||||
PlacedArea best = null;
|
||||
int bestIdx = -1;
|
||||
double bestSize = Double.MAX_VALUE;
|
||||
for (int i = 0; i < cachedAreas.size(); i++) {
|
||||
PlacedArea a = cachedAreas.get(i);
|
||||
if (WorldMapView.polyContainsStatic(a.pointsX(), a.pointsZ(), wx, wz)) {
|
||||
double sz = WorldMapView.boundingBoxAreaStatic(a.pointsX(), a.pointsZ());
|
||||
if (sz < bestSize) { bestSize = sz; best = a; bestIdx = i; }
|
||||
}
|
||||
}
|
||||
if (best != null) {
|
||||
cachedAreas.set(bestIdx, new PlacedArea(best.pointsX(), best.pointsZ(),
|
||||
best.areaId(), best.triggers(), wx, wz));
|
||||
try { AreaIO.save(cachedAreas); } catch (IOException ex) { statusLbl.setText("Fehler: " + ex.getMessage()); return; }
|
||||
redraw();
|
||||
statusLbl.setText(String.format("Label %s → (%.0f, %.0f)", best.areaId(), wx, wz));
|
||||
}
|
||||
}
|
||||
|
||||
private float toWorldX(double canvasX) {
|
||||
return (float)((canvasX - panX) / scale / (RENDER_SIZE - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||
}
|
||||
private float toWorldZ(double canvasZ) {
|
||||
return (float)((canvasZ - panY) / scale / (RENDER_SIZE - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||
}
|
||||
|
||||
private void exportPng() {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Höhlenkarte als PNG exportieren");
|
||||
fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("PNG-Bild", "*.png"));
|
||||
fc.setInitialFileName("hoehlenkarte.png");
|
||||
File file = fc.showSaveDialog(stageSupplier.get());
|
||||
if (file == null) return;
|
||||
if (cachedMapData == null) { statusLbl.setText("Karte noch nicht geladen."); return; }
|
||||
statusLbl.setText("Exportiere 4096×4096 PNG…");
|
||||
progress.setVisible(true);
|
||||
MapData md = cachedMapData;
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
BufferedImage bi = WorldMapRenderer.buildCaveMap(md, 4096);
|
||||
ImageIO.write(bi, "PNG", file);
|
||||
Platform.runLater(() -> { progress.setVisible(false); statusLbl.setText("Exportiert: " + file.getName()); });
|
||||
} catch (IOException ex) {
|
||||
Platform.runLater(() -> { progress.setVisible(false); statusLbl.setText("Export-Fehler: " + ex.getMessage()); });
|
||||
}
|
||||
}, "CaveMapPngExport");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
public boolean isLoaded() { return mapFxImage != null; }
|
||||
|
||||
public void ensureFitToView() { if (mapFxImage != null) fitToView(); }
|
||||
@@ -207,11 +289,18 @@ public class CaveMapView extends VBox {
|
||||
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(() -> statusLbl.setText("Berechne Cave-Maske…"));
|
||||
boolean[] caveMask = WorldMapView.loadCaveMask(mapData);
|
||||
|
||||
// Pfade: Y-basierte Trennung (voxelSurface > path_y + clearance)
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne VoxelSurface…"));
|
||||
float[] voxelSurface = WorldMapRenderer.bakeVoxelSurface(mapData);
|
||||
List<float[][]>[] splitChains = WorldMapView.splitPathChains(allPaths, voxelSurface);
|
||||
List<float[][]> cavePaths = splitChains[1];
|
||||
|
||||
// Gebäude + Areas: Cave-Maske (keine Y-Info)
|
||||
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);
|
||||
@@ -219,6 +308,7 @@ public class CaveMapView extends VBox {
|
||||
tunnelEntrances = entrances;
|
||||
cachedAreas = caveAreas;
|
||||
buildingFootprints = caveFootprints;
|
||||
cachedMapData = mapData;
|
||||
fitToView();
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
@@ -298,6 +388,13 @@ public class CaveMapView extends VBox {
|
||||
if (cbAreas.isSelected()) drawAreasOverlay(gc);
|
||||
}
|
||||
|
||||
private double toCanvasX(float wx) {
|
||||
return panX + (wx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale;
|
||||
}
|
||||
private double toCanvasZ(float wz) {
|
||||
return panY + (wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale;
|
||||
}
|
||||
|
||||
private double overlayLineWidth() {
|
||||
return switch (zoomLevelIdx) {
|
||||
case 3 -> 2.5;
|
||||
@@ -314,17 +411,12 @@ public class CaveMapView extends VBox {
|
||||
};
|
||||
}
|
||||
|
||||
private double toCanvasX(float wx) {
|
||||
return panX + (wx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale;
|
||||
}
|
||||
private double toCanvasZ(float wz) {
|
||||
return panY + (wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale;
|
||||
}
|
||||
|
||||
private void drawTunnelEntrancesOverlay(GraphicsContext gc) {
|
||||
if (tunnelEntrances.isEmpty()) return;
|
||||
gc.save();
|
||||
gc.setFill(Color.BLACK);
|
||||
gc.setStroke(Color.BLACK);
|
||||
gc.setLineWidth(1.0);
|
||||
for (float[] te : tunnelEntrances) {
|
||||
double r = Math.max(4.0, Math.min(
|
||||
te[4] / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale, 20.0));
|
||||
|
||||
@@ -404,8 +404,8 @@ public class WorldMapView extends VBox {
|
||||
List<float[]> newTunnelEntrances = computeTunnelEntrances(model.mapData());
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
|
||||
boolean[] caveMask = loadCaveMask(model.mapData());
|
||||
List<float[][]>[] splitChains = splitPathChains(allPathChains, caveMask);
|
||||
boolean[] caveMask = loadCaveMask(model.mapData());
|
||||
List<float[][]>[] splitChains = splitPathChains(allPathChains, model.voxelSurface());
|
||||
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);
|
||||
@@ -837,14 +837,14 @@ public class WorldMapView extends VBox {
|
||||
|
||||
List<float[]> chain = new ArrayList<>();
|
||||
de.blight.common.path.PathNode startNode = nodeMap.get(jUuid);
|
||||
chain.add(new float[]{startNode.getPosition().x, startNode.getPosition().z});
|
||||
chain.add(new float[]{startNode.getPosition().x, startNode.getPosition().z, startNode.getPosition().y});
|
||||
|
||||
String prev = jUuid, curr = nb;
|
||||
while (curr != null) {
|
||||
visitedEdges.add(pathEdgeKey(prev, curr));
|
||||
de.blight.common.path.PathNode currNode = nodeMap.get(curr);
|
||||
if (currNode == null) break;
|
||||
chain.add(new float[]{currNode.getPosition().x, currNode.getPosition().z});
|
||||
chain.add(new float[]{currNode.getPosition().x, currNode.getPosition().z, currNode.getPosition().y});
|
||||
if (junctions.contains(curr)) break;
|
||||
String next = null;
|
||||
for (String n2 : adj.getOrDefault(curr, java.util.Collections.emptyList())) {
|
||||
@@ -1109,36 +1109,53 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
|
||||
/**
|
||||
* Teilt Pfad-Ketten an Tunnel-Grenzen auf.
|
||||
* Teilt Pfad-Ketten an Tunnel-Grenzen auf (Y-basiert, präzise).
|
||||
* Nutzt die Y-Koordinate der Pfad-Knoten (Index 2) und prüft ob sie unterhalb
|
||||
* von solidem Voxel-Material liegen (voxelSurface > node_y + MIN_CEILING).
|
||||
* Rückgabe: Index 0 = Oberflächensegmente, Index 1 = Tunnel-Segmente.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<float[][]>[] splitPathChains(List<float[][]> chains, boolean[] caveMask) {
|
||||
static List<float[][]>[] splitPathChains(List<float[][]> chains, float[] voxelSurface) {
|
||||
List<float[][]> surface = new ArrayList<>();
|
||||
List<float[][]> cave = new ArrayList<>();
|
||||
for (float[][] chain : chains) {
|
||||
splitChainByCaveMask(chain, caveMask, surface, cave);
|
||||
splitChainByVoxelSurface(chain, voxelSurface, surface, cave);
|
||||
}
|
||||
return new List[]{surface, cave};
|
||||
}
|
||||
|
||||
private static void splitChainByCaveMask(float[][] chain, boolean[] caveMask,
|
||||
List<float[][]> surface, List<float[][]> cave) {
|
||||
private static final float PATH_UNDERGROUND_CLEARANCE = 3.0f;
|
||||
|
||||
private static boolean isNodeUnderground(float[] node, float[] voxelSurface) {
|
||||
if (node.length < 3) 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));
|
||||
float vs = voxelSurface[pz * VS + px];
|
||||
return !Float.isNaN(vs) && vs > wy + PATH_UNDERGROUND_CLEARANCE;
|
||||
}
|
||||
|
||||
private static void splitChainByVoxelSurface(float[][] chain, float[] voxelSurface,
|
||||
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]);
|
||||
List<float[]> current = new ArrayList<>();
|
||||
boolean currentCave = isNodeUnderground(chain[0], voxelSurface);
|
||||
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]);
|
||||
boolean bCave = isNodeUnderground(b, voxelSurface);
|
||||
|
||||
if (currentCave == bCave) {
|
||||
current.add(b);
|
||||
} else {
|
||||
// Genauen Grenzpunkt per Bisektion ermitteln
|
||||
float[] crossing = findCaveCrossing(a, b, caveMask, currentCave);
|
||||
float[] crossing = findPathCrossing(a, b, voxelSurface, currentCave);
|
||||
current.add(crossing);
|
||||
if (current.size() >= 2) {
|
||||
(currentCave ? cave : surface).add(current.toArray(new float[0][]));
|
||||
@@ -1154,15 +1171,17 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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];
|
||||
/** Bisektion mit Y-Interpolation: 12 Iterationen ≈ 0.05m Genauigkeit. */
|
||||
private static float[] findPathCrossing(float[] a, float[] b, float[] vs, boolean aIsUnderground) {
|
||||
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;
|
||||
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; }
|
||||
float mx = (ax + bx) * 0.5f, mz = (az + bz) * 0.5f, my = (ay + by) * 0.5f;
|
||||
float[] mid = {mx, mz, my};
|
||||
if (isNodeUnderground(mid, vs) == aIsUnderground) { ax = mx; az = mz; ay = my; }
|
||||
else { bx = mx; bz = mz; by = my; }
|
||||
}
|
||||
return new float[]{(ax + bx) * 0.5f, (az + bz) * 0.5f};
|
||||
return new float[]{(ax + bx) * 0.5f, (az + bz) * 0.5f, (ay + by) * 0.5f};
|
||||
}
|
||||
|
||||
/** Filtert Gebäude-Footprints: wantCave=true → nur Untergrund, false → nur Oberfläche. */
|
||||
@@ -1774,6 +1793,9 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean polyContainsStatic(float[] xs, float[] zs, float px, float pz) { return polyContains(xs, zs, px, pz); }
|
||||
static double boundingBoxAreaStatic(float[] xs, float[] zs) { return boundingBoxArea(xs, zs); }
|
||||
|
||||
private static boolean polyContains(float[] xs, float[] zs, float px, float pz) {
|
||||
boolean inside = false;
|
||||
int n = xs.length;
|
||||
|
||||
Reference in New Issue
Block a user