From cf11a50a219469f4814b699897128fc35cd7c604 Mon Sep 17 00:00:00 2001 From: Mario Date: Thu, 3 Sep 2026 18:33:56 +0200 Subject: [PATCH] =?UTF-8?q?Weltkarte=20Wald:=20Outline=20auf=20echter=20De?= =?UTF-8?q?nsity-Isokontur=20statt=20Bin=C3=A4rmaske?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorher: Density → binäre Maske (-1/+1) → MS bei 0.5 → Boundary 75% vom Zellrand entfernt (falscher Offset, unabhängig von echten Density-Werten). Jetzt: h = threshold - density → MS direkt auf Density-Werten bei thr=0 → msLerp interpoliert genau an der echten threshold-Isokontur, identisch zur Fill-Grenze in computeForestPixels. Co-Authored-By: Claude Sonnet 4.6 --- .../de/blight/editor/ui/WorldMapView.java | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) 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 836c16c..c267e9e 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 @@ -1126,32 +1126,21 @@ public class WorldMapView extends VBox { /** Extrahiert Wald-Konturen aus der Tree-Density-Map als geglättete Weltkoordinaten-Pfade. */ private static List buildForestOutlinePaths(float[] density, int fdSize, float threshold) { - // Density-Map: mz=0 = Norden (WH-pm.z()=0 → pm.z()=+WH), mz=fdSize-1 = Süden. - // Marching Squares: mz=0 → msW=-WH (Süden). → Z muss gespiegelt werden. - // Bilinear-Interpolation wie in computeForestPixels → Outline liegt exakt auf Fill-Grenze. - final int SIZE = SEA_MASK_SIZE; - float[] mask = new float[SIZE * SIZE]; - for (int mz = 0; mz < SIZE; mz++) { - // Z-Flip: mz=0 (Süd im MS) → Süd in Density (dz=fdSize-1) - float fz = (float)(SIZE - 1 - mz) / (SIZE - 1) * (fdSize - 1); - int dz0 = (int) fz, dz1 = Math.min(dz0 + 1, fdSize - 1); - float tz = fz - dz0; - for (int mx = 0; mx < SIZE; mx++) { - float fx = (float) mx / (SIZE - 1) * (fdSize - 1); - int dx0 = (int) fx, dx1 = Math.min(dx0 + 1, fdSize - 1); - float tx = fx - dx0; - float d = (density[dz0 * fdSize + dx0] * (1 - tx) + density[dz0 * fdSize + dx1] * tx) * (1 - tz) - + (density[dz1 * fdSize + dx0] * (1 - tx) + density[dz1 * fdSize + dx1] * tx) * tz; - mask[mz * SIZE + mx] = d >= threshold ? -1f : 1f; + // Z-Flip: Density mz=0=Nord, MS mz=0=Süd. + // h = threshold - density → forest (density > threshold) ergibt h < 0 = "inside" für MS. + // MS läuft direkt auf echten Density-Werten mit thr=0 → exakt dieselbe Isokontur wie + // die Füllung in computeForestPixels (keine binäre Maske, kein Boundary-Versatz). + float[] h = new float[fdSize * fdSize]; + for (int mz = 0; mz < fdSize; mz++) { + int dz = fdSize - 1 - mz; + for (int mx = 0; mx < fdSize; mx++) { + h[mz * fdSize + mx] = threshold - density[dz * fdSize + mx]; } } List segs = new ArrayList<>(); - marchingSquares(mask, SIZE, 0.5f, null, null, segs); - // chaikinIter=0: Density-Boundary ist bereits glatt (kreisförmige Kernels), - // kein Chaikin-Schrumpfen → Outline liegt exakt auf der Fill-Grenze. + marchingSquares(h, fdSize, 0f, null, null, segs); List paths = assembleClosedPaths(segs.toArray(new float[0][]), 0); - // Kleinstpolygone filtern: weniger als ~60m Umfang = Artefakt (z.B. isolierte Bäume) final float MIN_PERIMETER_SQ = 60f * 60f; return paths.stream().filter(p -> pathPerimeterSq(p) >= MIN_PERIMETER_SQ) .collect(java.util.stream.Collectors.toList());