Weltkarte Klippen: Schraffierung nur im Kliff-Band via Winding-Korrektur

- fixWindingForClip(): äußere Pfade CCW, innere Pfade (Löcher/Berg-Inneres) CW
  → gc.clip() füllt mit Non-Zero-Rule nur das echte Kliff-Band, nicht den
  gesamten eingeschlossenen Bereich inkl. Berggipfel
- Schraffierungs-Liniendicke = overlayLineWidth() (identisch zu Konturlinie)
- Schraffierungs-Abstand = 4× Liniendicke → konstante ~25% Flächendeckung
  über alle Zoom-Stufen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 18:06:30 +02:00
parent 972d5d6e0b
commit 8d6da6b013

View File

@@ -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<float[][]> fixWindingForClip(List<float[][]> paths) {
if (paths.size() <= 1) { return paths; }
List<float[][]> 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<float[][]> buildCliffPaths(MapData mapData, float[] voxelSurface) {
final int SIZE = SEA_MASK_SIZE;
@@ -1061,7 +1112,7 @@ public class WorldMapView extends VBox {
List<float[]> 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. */