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 8952c16..97cfa84 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 @@ -602,9 +602,9 @@ public class WorldMapView extends VBox { } gc.clip(); gc.setStroke(Color.rgb(120, 115, 115, 0.45)); - gc.setLineWidth(0.8); + gc.setLineWidth(overlayLineWidth()); gc.setLineDashes(); - double hatchSpacing = 5.5; + double hatchSpacing = overlayLineWidth() * 4.0; for (double d = -cH; d < cW + cH; d += hatchSpacing) { gc.strokeLine(d, 0, d + cH, cH); } @@ -1036,6 +1036,57 @@ public class WorldMapView extends VBox { return result; } + // ── Winding-Korrektur für Clip-Masken ──────────────────────────────────── + // gc.clip() nutzt Non-Zero-Winding: äußere Pfade müssen CCW sein, innere (Löcher) CW. + // Ohne Korrektur füllt clip() den gesamten Innenbereich inkl. eingeschlossener Nicht-Kliff-Gebiete. + + private static float signedArea(float[][] path) { + float a = 0f; + for (int i = 0, n = path.length; i < n; i++) { + float[] p0 = path[i], p1 = path[(i + 1) % n]; + a += p0[0] * p1[1] - p1[0] * p0[1]; + } + return a * 0.5f; + } + + private static boolean pointInPolygon(float px, float py, float[][] poly) { + boolean inside = false; + for (int i = 0, j = poly.length - 1; i < poly.length; j = i++) { + float xi = poly[i][0], yi = poly[i][1], xj = poly[j][0], yj = poly[j][1]; + if (((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi)) { + inside = !inside; + } + } + return inside; + } + + private static float[][] reversePath(float[][] path) { + float[][] r = new float[path.length][]; + for (int i = 0; i < path.length; i++) { r[i] = path[path.length - 1 - i]; } + return r; + } + + /** + * Stellt sicher dass äußere Pfade CCW und innere Pfade (Löcher) CW gewunden sind. + * Nötig damit gc.clip() mit Non-Zero-Rule nur die echte Kliff-Band-Fläche maskiert. + */ + private static List fixWindingForClip(List paths) { + if (paths.size() <= 1) { return paths; } + List result = new ArrayList<>(paths.size()); + for (float[][] path : paths) { + float[] probe = path[path.length / 2]; + int containCount = 0; + for (float[][] other : paths) { + if (other != path && pointInPolygon(probe[0], probe[1], other)) { containCount++; } + } + boolean isHole = (containCount % 2) == 1; + boolean isCCW = signedArea(path) > 0; + boolean wantCCW = !isHole; + result.add(isCCW == wantCCW ? path : reversePath(path)); + } + return result; + } + /** Extrahiert Klippen-Konturen aus Terrain-Höhen als geglättete Weltkoordinaten-Pfade. */ private static List buildCliffPaths(MapData mapData, float[] voxelSurface) { final int SIZE = SEA_MASK_SIZE; @@ -1061,7 +1112,7 @@ public class WorldMapView extends VBox { List segs = new ArrayList<>(); marchingSquares(mask, SIZE, 0.5f, null, null, segs); - return buildSeaCoastPaths(segs.toArray(new float[0][])); + return fixWindingForClip(buildSeaCoastPaths(segs.toArray(new float[0][]))); } /** Extrahiert Wald-Konturen aus der Tree-Density-Map als geglättete Weltkoordinaten-Pfade. */