Zwischenstand Minimap
This commit is contained in:
@@ -6,6 +6,7 @@ import de.blight.common.model.trigger.TriggerIO;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public final class AreaIO {
|
public final class AreaIO {
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public final class AreaIO {
|
|||||||
Path p = getPath();
|
Path p = getPath();
|
||||||
Files.createDirectories(p.getParent());
|
Files.createDirectories(p.getParent());
|
||||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||||
w.write("# polygon\tareaId\ttriggersJson");
|
w.write("# polygon\tareaId\ttriggersJson\tlabelX\tlabelZ");
|
||||||
w.newLine();
|
w.newLine();
|
||||||
for (PlacedArea a : areas) {
|
for (PlacedArea a : areas) {
|
||||||
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
|
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
|
||||||
@@ -27,6 +28,10 @@ public final class AreaIO {
|
|||||||
w.write(a.areaId());
|
w.write(a.areaId());
|
||||||
w.write('\t');
|
w.write('\t');
|
||||||
w.write(TriggerIO.serializeList(a.triggers()));
|
w.write(TriggerIO.serializeList(a.triggers()));
|
||||||
|
w.write('\t');
|
||||||
|
w.write(String.format(Locale.ROOT, "%.3f", a.labelX()));
|
||||||
|
w.write('\t');
|
||||||
|
w.write(String.format(Locale.ROOT, "%.3f", a.labelZ()));
|
||||||
w.newLine();
|
w.newLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +51,9 @@ public final class AreaIO {
|
|||||||
if (pts[0].length < 3) continue;
|
if (pts[0].length < 3) continue;
|
||||||
String areaId = f.length > 1 ? f[1].strip() : "";
|
String areaId = f.length > 1 ? f[1].strip() : "";
|
||||||
List<Trigger> triggers = f.length > 2 ? TriggerIO.deserializeList(f[2]) : new ArrayList<>();
|
List<Trigger> triggers = f.length > 2 ? TriggerIO.deserializeList(f[2]) : new ArrayList<>();
|
||||||
list.add(new PlacedArea(pts[0], pts[1], areaId, triggers));
|
float labelX = f.length > 3 ? Float.parseFloat(f[3].strip()) : Float.NaN;
|
||||||
|
float labelZ = f.length > 4 ? Float.parseFloat(f[4].strip()) : Float.NaN;
|
||||||
|
list.add(new PlacedArea(pts[0], pts[1], areaId, triggers, labelX, labelZ));
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public final class LocationIO {
|
|||||||
Path p = getPath();
|
Path p = getPath();
|
||||||
Files.createDirectories(p.getParent());
|
Files.createDirectories(p.getParent());
|
||||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||||
w.write("# nameId\tcenterX\tcenterZ\tradius\ttriggersJson");
|
w.write("# nameId\tcenterX\tcenterZ\tradius\ttriggersJson\tshowOnMap\tlabelX\tlabelZ");
|
||||||
w.newLine();
|
w.newLine();
|
||||||
for (Location loc : locations) {
|
for (Location loc : locations) {
|
||||||
w.write(loc.getId());
|
w.write(loc.getId());
|
||||||
@@ -38,6 +38,12 @@ public final class LocationIO {
|
|||||||
w.write(String.format(Locale.ROOT, "%.3f", loc.getRadius()));
|
w.write(String.format(Locale.ROOT, "%.3f", loc.getRadius()));
|
||||||
w.write('\t');
|
w.write('\t');
|
||||||
w.write(TriggerIO.serializeList(loc.getTriggers()));
|
w.write(TriggerIO.serializeList(loc.getTriggers()));
|
||||||
|
w.write('\t');
|
||||||
|
w.write(Boolean.toString(loc.isShowOnMap()));
|
||||||
|
w.write('\t');
|
||||||
|
w.write(String.format(Locale.ROOT, "%.3f", loc.getLabelX()));
|
||||||
|
w.write('\t');
|
||||||
|
w.write(String.format(Locale.ROOT, "%.3f", loc.getLabelZ()));
|
||||||
w.newLine();
|
w.newLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,6 +68,9 @@ public final class LocationIO {
|
|||||||
List<Trigger> triggers = TriggerIO.deserializeList(f[4].strip());
|
List<Trigger> triggers = TriggerIO.deserializeList(f[4].strip());
|
||||||
loc.setTriggers(triggers);
|
loc.setTriggers(triggers);
|
||||||
}
|
}
|
||||||
|
loc.setShowOnMap(f.length > 5 ? Boolean.parseBoolean(f[5].strip()) : true);
|
||||||
|
loc.setLabelX(f.length > 6 ? Float.parseFloat(f[6].strip()) : Float.NaN);
|
||||||
|
loc.setLabelZ(f.length > 7 ? Float.parseFloat(f[7].strip()) : Float.NaN);
|
||||||
list.add(loc);
|
list.add(loc);
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ public record PlacedArea(
|
|||||||
float[] pointsX,
|
float[] pointsX,
|
||||||
float[] pointsZ,
|
float[] pointsZ,
|
||||||
String areaId,
|
String areaId,
|
||||||
List<Trigger> triggers
|
List<Trigger> triggers,
|
||||||
|
float labelX,
|
||||||
|
float labelZ
|
||||||
) {
|
) {
|
||||||
public PlacedArea(float[] pointsX, float[] pointsZ, String areaId) {
|
public PlacedArea(float[] pointsX, float[] pointsZ, String areaId) {
|
||||||
this(pointsX, pointsZ, areaId, List.of());
|
this(pointsX, pointsZ, areaId, List.of(), Float.NaN, Float.NaN);
|
||||||
|
}
|
||||||
|
public PlacedArea(float[] pointsX, float[] pointsZ, String areaId, List<Trigger> triggers) {
|
||||||
|
this(pointsX, pointsZ, areaId, triggers, Float.NaN, Float.NaN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import de.blight.common.model.Location;
|
|||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -42,14 +43,34 @@ public final class WorldMapRenderer {
|
|||||||
boolean showTerrain,
|
boolean showTerrain,
|
||||||
boolean showSplatColors,
|
boolean showSplatColors,
|
||||||
boolean showWater,
|
boolean showWater,
|
||||||
|
boolean showWaterWaves,
|
||||||
boolean showAreas,
|
boolean showAreas,
|
||||||
boolean showZones,
|
boolean showZones,
|
||||||
boolean showLocations,
|
boolean showLocations,
|
||||||
boolean showModels
|
boolean showModels
|
||||||
) {
|
) {
|
||||||
public static RenderOptions all() {
|
// Compat: alte 7-Parameter-Form → showWaterWaves = showWater
|
||||||
return new RenderOptions(true, true, true, true, true, true, true);
|
public RenderOptions(boolean showTerrain, boolean showSplatColors, boolean showWater,
|
||||||
|
boolean showAreas, boolean showZones, boolean showLocations, boolean showModels) {
|
||||||
|
this(showTerrain, showSplatColors, showWater, showWater,
|
||||||
|
showAreas, showZones, showLocations, showModels);
|
||||||
}
|
}
|
||||||
|
public static RenderOptions all() {
|
||||||
|
return new RenderOptions(true, true, true, true, true, true, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean[] buildSeaMask(MapData m, int size) {
|
||||||
|
int TV = MapData.TERRAIN_VERTS;
|
||||||
|
boolean[] mask = new boolean[size * size];
|
||||||
|
for (int py = 0; py < size; py++) {
|
||||||
|
for (int px = 0; px < size; px++) {
|
||||||
|
int hx = Math.min((int)((float) px / (size - 1) * (TV - 1)), TV - 1);
|
||||||
|
int hz = Math.min((int)((float) py / (size - 1) * (TV - 1)), TV - 1);
|
||||||
|
mask[py * size + px] = m.terrainHeight[hz * TV + hx] < 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
|
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
|
||||||
@@ -147,47 +168,146 @@ public final class WorldMapRenderer {
|
|||||||
|
|
||||||
// Wasser
|
// Wasser
|
||||||
if (opts.showWater()) {
|
if (opts.showWater()) {
|
||||||
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
|
// ── Weißfüllung ───────────────────────────────────────────────────────
|
||||||
// Meer: Terrain unter Meeresspiegel
|
|
||||||
for (int py = 0; py < targetSize; py++) {
|
for (int py = 0; py < targetSize; py++) {
|
||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
if (heights[py * targetSize + px] < 0f) {
|
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF);
|
||||||
img.setRGB(px, py, WATER_RGB);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Wasserflächen: nur dort sichtbar, wo Terrain unter Wasseroberfläche liegt
|
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : input.waters()) {
|
||||||
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
Polygon poly = new Polygon(xs, ys, xs.length);
|
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||||
Rectangle bb = poly.getBounds();
|
Rectangle bb = poly.getBounds();
|
||||||
int x0 = Math.max(0, bb.x);
|
int x0 = Math.max(0, bb.x), y0 = Math.max(0, bb.y);
|
||||||
int y0 = Math.max(0, bb.y);
|
|
||||||
int x1 = Math.min(targetSize - 1, bb.x + bb.width);
|
int x1 = Math.min(targetSize - 1, bb.x + bb.width);
|
||||||
int y1 = Math.min(targetSize - 1, bb.y + bb.height);
|
int y1 = Math.min(targetSize - 1, bb.y + bb.height);
|
||||||
float wh = w.waterHeight();
|
float wh = w.waterHeight();
|
||||||
for (int py = y0; py <= y1; py++) {
|
for (int py = y0; py <= y1; py++) {
|
||||||
for (int px = x0; px <= x1; px++) {
|
for (int px = x0; px <= x1; px++) {
|
||||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh) {
|
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||||
img.setRGB(px, py, WATER_RGB);
|
img.setRGB(px, py, 0xFFFFFFFF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Wellenmarken ──────────────────────────────────────────────────────
|
||||||
|
if (opts.showWaterWaves()) {
|
||||||
|
float wmW = Math.max(20f, targetSize / 70f);
|
||||||
|
float wmA = wmW * 0.2f, spX = wmW * 1.8f, spY = wmW * 1.05f;
|
||||||
|
gfx.setColor(Color.BLACK);
|
||||||
|
gfx.setStroke(new BasicStroke(Math.max(0.7f, lineW * 0.4f),
|
||||||
|
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
|
// Meer: versetzt + gejittert
|
||||||
|
int seaRow = 0;
|
||||||
|
for (float wy = spY * 0.5f; wy < targetSize; wy += spY, seaRow++) {
|
||||||
|
float rowOff = (seaRow & 1) == 1 ? spX * 0.5f : 0f;
|
||||||
|
int seaCol = 0;
|
||||||
|
for (float wx = rowOff; wx + wmW <= targetSize; wx += spX, seaCol++) {
|
||||||
|
float jx = (float)(Math.abs(Math.sin(seaRow * 73.1 + seaCol * 157.3)) * spX * 0.25);
|
||||||
|
float jy = (float)(Math.sin(seaRow * 211.7 + seaCol * 89.5) * spY * 0.2);
|
||||||
|
float ox = wx + jx, oy = wy + jy;
|
||||||
|
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
|
||||||
|
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
|
||||||
|
&& ox >= 0 && ox + wmW < targetSize
|
||||||
|
&& heights[cy * targetSize + cx] < 0f) {
|
||||||
|
Path2D mark = new Path2D.Float();
|
||||||
|
mark.moveTo(ox, oy);
|
||||||
|
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
|
||||||
|
gfx.draw(mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wasserflächen: nur wenn groß genug
|
||||||
|
for (PlacedWater w : input.waters()) {
|
||||||
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
|
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||||
|
Rectangle bb = poly.getBounds();
|
||||||
|
if (bb.width <= wmW * 1.5f || bb.height <= spY) continue;
|
||||||
|
gfx.setClip(poly);
|
||||||
|
int wRow = 0;
|
||||||
|
for (float wy = bb.y + spY * 0.5f; wy < bb.y + bb.height; wy += spY, wRow++) {
|
||||||
|
float rowOff = (wRow & 1) == 1 ? spX * 0.5f : 0f;
|
||||||
|
int wCol = 0;
|
||||||
|
for (float wx = bb.x + rowOff; wx + wmW <= bb.x + bb.width; wx += spX, wCol++) {
|
||||||
|
float jx = (float)(Math.abs(Math.sin(wRow * 73.1 + wCol * 157.3)) * spX * 0.25);
|
||||||
|
float jy = (float)(Math.sin(wRow * 211.7 + wCol * 89.5) * spY * 0.2);
|
||||||
|
float ox = wx + jx, oy = wy + jy;
|
||||||
|
if (ox < bb.x || ox + wmW > bb.x + bb.width) continue;
|
||||||
|
Path2D mark = new Path2D.Float();
|
||||||
|
mark.moveTo(ox, oy);
|
||||||
|
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
|
||||||
|
gfx.draw(mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gfx.setClip(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Uferlinie ─────────────────────────────────────────────────────────
|
||||||
|
if (opts.showWaterWaves()) {
|
||||||
|
gfx.setColor(Color.BLACK);
|
||||||
|
gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
|
// Wasserflächen: Polygon-Umriss
|
||||||
|
for (PlacedWater w : input.waters()) {
|
||||||
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
|
gfx.drawPolygon(xs, ys, xs.length);
|
||||||
|
}
|
||||||
|
// Meer: Pixel-Küstenlinie (morphologisch aufgeweitet)
|
||||||
|
int bw = 3;
|
||||||
|
boolean[] cur = new boolean[targetSize * targetSize];
|
||||||
|
boolean[] nxt = new boolean[targetSize * targetSize];
|
||||||
|
for (int py = 1; py < targetSize - 1; py++) {
|
||||||
|
for (int px = 1; px < targetSize - 1; px++) {
|
||||||
|
if (heights[py * targetSize + px] >= 0f) continue;
|
||||||
|
if (heights[py * targetSize + (px-1)] >= 0f || heights[py * targetSize + (px+1)] >= 0f ||
|
||||||
|
heights[(py-1) * targetSize + px] >= 0f || heights[(py+1) * targetSize + px] >= 0f)
|
||||||
|
cur[py * targetSize + px] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int pass = 1; pass < bw; pass++) {
|
||||||
|
System.arraycopy(cur, 0, nxt, 0, cur.length);
|
||||||
|
for (int py = 1; py < targetSize - 1; py++) {
|
||||||
|
for (int px = 1; px < targetSize - 1; px++) {
|
||||||
|
if (!cur[py * targetSize + px]) continue;
|
||||||
|
if (heights[py * targetSize + (px-1)] < 0f) nxt[py * targetSize + (px-1)] = true;
|
||||||
|
if (heights[py * targetSize + (px+1)] < 0f) nxt[py * targetSize + (px+1)] = true;
|
||||||
|
if (heights[(py-1) * targetSize + px] < 0f) nxt[(py-1) * targetSize + px] = true;
|
||||||
|
if (heights[(py+1) * targetSize + px] < 0f) nxt[(py+1) * targetSize + px] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean[] tmp = cur; cur = nxt; nxt = tmp;
|
||||||
|
}
|
||||||
|
for (int i = 0, n = cur.length; i < n; i++) {
|
||||||
|
if (cur[i]) img.setRGB(i % targetSize, i / targetSize, 0xFF000000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Area-Polygone
|
// Area-Polygone
|
||||||
if (opts.showAreas()) {
|
if (opts.showAreas()) {
|
||||||
gfx.setStroke(new BasicStroke(lineW));
|
float dash = lineW * 7f, gap = lineW * 4f;
|
||||||
|
BasicStroke dashed = new BasicStroke(lineW * 1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
|
||||||
|
10f, new float[]{dash, gap}, 0f);
|
||||||
|
int aFontSize = Math.max(8, targetSize / 160);
|
||||||
|
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
|
||||||
for (PlacedArea a : input.areas()) {
|
for (PlacedArea a : input.areas()) {
|
||||||
Color c = hashColor(a.areaId());
|
|
||||||
int[] xs = worldToPixels(a.pointsX(), targetSize);
|
int[] xs = worldToPixels(a.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(a.pointsZ(), targetSize);
|
int[] ys = worldToPixels(a.pointsZ(), targetSize);
|
||||||
gfx.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
|
gfx.setStroke(dashed);
|
||||||
gfx.fillPolygon(xs, ys, xs.length);
|
gfx.setColor(Color.BLACK);
|
||||||
gfx.setColor(c);
|
gfx.draw(toPath(xs, ys));
|
||||||
gfx.drawPolygon(xs, ys, xs.length);
|
String lbl = friendlyAreaName(a.areaId());
|
||||||
|
if (lbl != null) {
|
||||||
|
float[] cen = centroid(a.pointsX(), a.pointsZ());
|
||||||
|
float lx = Float.isNaN(a.labelX()) ? cen[0] : a.labelX();
|
||||||
|
float lz = Float.isNaN(a.labelZ()) ? cen[1] : a.labelZ();
|
||||||
|
int plx = worldToPixel(lx, targetSize);
|
||||||
|
int plz = worldToPixel(lz, targetSize);
|
||||||
|
drawLabel(gfx, lbl, plx, plz);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,44 +335,20 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locations (Kreise + Labels)
|
// Locations (nur Label)
|
||||||
if (opts.showLocations()) {
|
if (opts.showLocations()) {
|
||||||
gfx.setStroke(new BasicStroke(lineW));
|
|
||||||
int fontSize = Math.max(8, targetSize / 140);
|
int fontSize = Math.max(8, targetSize / 140);
|
||||||
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
||||||
for (Location loc : input.locations()) {
|
for (Location loc : input.locations()) {
|
||||||
int lx = worldToPixel(loc.getCenterX(), targetSize);
|
if (!loc.isShowOnMap()) continue;
|
||||||
int lz = worldToPixel(loc.getCenterZ(), targetSize);
|
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
||||||
int lr = Math.max(4, (int)(loc.getRadius() / WORLD_SIZE * targetSize));
|
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
|
||||||
|
float wz = Float.isNaN(loc.getLabelZ()) ? loc.getCenterZ() : loc.getLabelZ();
|
||||||
gfx.setColor(new Color(255, 255, 200, 50));
|
int lx = worldToPixel(wx, targetSize);
|
||||||
gfx.fillOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
int lz = worldToPixel(wz, targetSize);
|
||||||
gfx.setColor(new Color(255, 220, 60));
|
drawLabel(gfx, friendlyLocationName(loc.getId()), lx, lz);
|
||||||
gfx.drawOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
|
||||||
|
|
||||||
if (targetSize >= 512 && loc.getId() != null && !loc.getId().isEmpty()) {
|
|
||||||
String label = friendlyLocationName(loc.getId());
|
|
||||||
FontMetrics fm = gfx.getFontMetrics();
|
|
||||||
int tw = fm.stringWidth(label);
|
|
||||||
gfx.setColor(new Color(0, 0, 0, 160));
|
|
||||||
gfx.drawString(label, lx - tw / 2 + 1, lz - lr - 3);
|
|
||||||
gfx.setColor(Color.WHITE);
|
|
||||||
gfx.drawString(label, lx - tw / 2, lz - lr - 4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Spawnpunkt
|
|
||||||
int spx = worldToPixel(m.spawnX, targetSize);
|
|
||||||
int spz = worldToPixel(m.spawnZ, targetSize);
|
|
||||||
int cr = Math.max(5, targetSize / 180);
|
|
||||||
gfx.setStroke(new BasicStroke(Math.max(2f, targetSize / 300f)));
|
|
||||||
gfx.setColor(new Color(0, 0, 0, 120));
|
|
||||||
gfx.drawLine(spx - cr + 1, spz + 1, spx + cr + 1, spz + 1);
|
|
||||||
gfx.drawLine(spx + 1, spz - cr + 1, spx + 1, spz + cr + 1);
|
|
||||||
gfx.setColor(new Color(60, 230, 90));
|
|
||||||
gfx.drawLine(spx - cr, spz, spx + cr, spz);
|
|
||||||
gfx.drawLine(spx, spz - cr, spx, spz + cr);
|
|
||||||
|
|
||||||
gfx.dispose();
|
gfx.dispose();
|
||||||
return img;
|
return img;
|
||||||
@@ -375,46 +471,145 @@ public final class WorldMapRenderer {
|
|||||||
float lineW = Math.max(1.5f, targetSize / 400f);
|
float lineW = Math.max(1.5f, targetSize / 400f);
|
||||||
|
|
||||||
if (opts.showWater()) {
|
if (opts.showWater()) {
|
||||||
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
|
// ── Weißfüllung ───────────────────────────────────────────────────────
|
||||||
// Meer: Terrain unter Meeresspiegel
|
|
||||||
for (int py = 0; py < targetSize; py++) {
|
for (int py = 0; py < targetSize; py++) {
|
||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
if (heights[py * targetSize + px] < 0f) {
|
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF);
|
||||||
img.setRGB(px, py, WATER_RGB);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Wasserflächen: nur dort sichtbar, wo Terrain unter Wasseroberfläche liegt
|
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : input.waters()) {
|
||||||
int[] xs = wrp(w.pointsX(), wx0, rSize, targetSize);
|
int[] xs = wrp(w.pointsX(), wx0, rSize, targetSize);
|
||||||
int[] ys = wrp(w.pointsZ(), wz0, rSize, targetSize);
|
int[] ys = wrp(w.pointsZ(), wz0, rSize, targetSize);
|
||||||
Polygon poly = new Polygon(xs, ys, xs.length);
|
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||||
Rectangle bb = poly.getBounds();
|
Rectangle bb = poly.getBounds();
|
||||||
int x0 = Math.max(0, bb.x);
|
int x0 = Math.max(0, bb.x), y0 = Math.max(0, bb.y);
|
||||||
int y0 = Math.max(0, bb.y);
|
|
||||||
int x1 = Math.min(targetSize - 1, bb.x + bb.width);
|
int x1 = Math.min(targetSize - 1, bb.x + bb.width);
|
||||||
int y1 = Math.min(targetSize - 1, bb.y + bb.height);
|
int y1 = Math.min(targetSize - 1, bb.y + bb.height);
|
||||||
float wh = w.waterHeight();
|
float wh = w.waterHeight();
|
||||||
for (int py = y0; py <= y1; py++) {
|
for (int py = y0; py <= y1; py++) {
|
||||||
for (int px = x0; px <= x1; px++) {
|
for (int px = x0; px <= x1; px++) {
|
||||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh) {
|
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||||
img.setRGB(px, py, WATER_RGB);
|
img.setRGB(px, py, 0xFFFFFFFF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Wellenmarken ──────────────────────────────────────────────────────
|
||||||
|
if (opts.showWaterWaves()) {
|
||||||
|
float wmW = Math.max(20f, targetSize / 70f);
|
||||||
|
float wmA = wmW * 0.2f, spX = wmW * 1.8f, spY = wmW * 1.05f;
|
||||||
|
gfx.setColor(Color.BLACK);
|
||||||
|
gfx.setStroke(new BasicStroke(Math.max(0.7f, lineW * 0.4f),
|
||||||
|
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
|
// Meer: versetzt + gejittert
|
||||||
|
int seaRow = 0;
|
||||||
|
for (float wy = spY * 0.5f; wy < targetSize; wy += spY, seaRow++) {
|
||||||
|
float rowOff = (seaRow & 1) == 1 ? spX * 0.5f : 0f;
|
||||||
|
int seaCol = 0;
|
||||||
|
for (float wx = rowOff; wx + wmW <= targetSize; wx += spX, seaCol++) {
|
||||||
|
float jx = (float)(Math.abs(Math.sin(seaRow * 73.1 + seaCol * 157.3)) * spX * 0.25);
|
||||||
|
float jy = (float)(Math.sin(seaRow * 211.7 + seaCol * 89.5) * spY * 0.2);
|
||||||
|
float ox = wx + jx, oy = wy + jy;
|
||||||
|
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
|
||||||
|
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
|
||||||
|
&& ox >= 0 && ox + wmW < targetSize
|
||||||
|
&& heights[cy * targetSize + cx] < 0f) {
|
||||||
|
Path2D mark = new Path2D.Float();
|
||||||
|
mark.moveTo(ox, oy);
|
||||||
|
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
|
||||||
|
gfx.draw(mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wasserflächen: nur wenn groß genug
|
||||||
|
for (PlacedWater w : input.waters()) {
|
||||||
|
int[] xs = wrp(w.pointsX(), wx0, rSize, targetSize);
|
||||||
|
int[] ys = wrp(w.pointsZ(), wz0, rSize, targetSize);
|
||||||
|
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||||
|
Rectangle bb = poly.getBounds();
|
||||||
|
if (bb.width <= wmW * 1.5f || bb.height <= spY) continue;
|
||||||
|
gfx.setClip(poly);
|
||||||
|
int wRow = 0;
|
||||||
|
for (float wy = bb.y + spY * 0.5f; wy < bb.y + bb.height; wy += spY, wRow++) {
|
||||||
|
float rowOff = (wRow & 1) == 1 ? spX * 0.5f : 0f;
|
||||||
|
int wCol = 0;
|
||||||
|
for (float wx = bb.x + rowOff; wx + wmW <= bb.x + bb.width; wx += spX, wCol++) {
|
||||||
|
float jx = (float)(Math.abs(Math.sin(wRow * 73.1 + wCol * 157.3)) * spX * 0.25);
|
||||||
|
float jy = (float)(Math.sin(wRow * 211.7 + wCol * 89.5) * spY * 0.2);
|
||||||
|
float ox = wx + jx, oy = wy + jy;
|
||||||
|
if (ox < bb.x || ox + wmW > bb.x + bb.width) continue;
|
||||||
|
Path2D mark = new Path2D.Float();
|
||||||
|
mark.moveTo(ox, oy);
|
||||||
|
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
|
||||||
|
gfx.draw(mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gfx.setClip(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Uferlinie ─────────────────────────────────────────────────────────
|
||||||
|
if (opts.showWaterWaves()) {
|
||||||
|
gfx.setColor(Color.BLACK);
|
||||||
|
gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
|
// Wasserflächen: Polygon-Umriss
|
||||||
|
for (PlacedWater w : input.waters()) {
|
||||||
|
int[] xs = wrp(w.pointsX(), wx0, rSize, targetSize);
|
||||||
|
int[] ys = wrp(w.pointsZ(), wz0, rSize, targetSize);
|
||||||
|
gfx.drawPolygon(xs, ys, xs.length);
|
||||||
|
}
|
||||||
|
// Meer: Pixel-Küstenlinie (morphologisch aufgeweitet)
|
||||||
|
int bw = 3;
|
||||||
|
boolean[] cur = new boolean[targetSize * targetSize];
|
||||||
|
boolean[] nxt = new boolean[targetSize * targetSize];
|
||||||
|
for (int py = 1; py < targetSize - 1; py++) {
|
||||||
|
for (int px = 1; px < targetSize - 1; px++) {
|
||||||
|
if (heights[py * targetSize + px] >= 0f) continue;
|
||||||
|
if (heights[py * targetSize + (px-1)] >= 0f || heights[py * targetSize + (px+1)] >= 0f ||
|
||||||
|
heights[(py-1) * targetSize + px] >= 0f || heights[(py+1) * targetSize + px] >= 0f)
|
||||||
|
cur[py * targetSize + px] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int pass = 1; pass < bw; pass++) {
|
||||||
|
System.arraycopy(cur, 0, nxt, 0, cur.length);
|
||||||
|
for (int py = 1; py < targetSize - 1; py++) {
|
||||||
|
for (int px = 1; px < targetSize - 1; px++) {
|
||||||
|
if (!cur[py * targetSize + px]) continue;
|
||||||
|
if (heights[py * targetSize + (px-1)] < 0f) nxt[py * targetSize + (px-1)] = true;
|
||||||
|
if (heights[py * targetSize + (px+1)] < 0f) nxt[py * targetSize + (px+1)] = true;
|
||||||
|
if (heights[(py-1) * targetSize + px] < 0f) nxt[(py-1) * targetSize + px] = true;
|
||||||
|
if (heights[(py+1) * targetSize + px] < 0f) nxt[(py+1) * targetSize + px] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean[] tmp = cur; cur = nxt; nxt = tmp;
|
||||||
|
}
|
||||||
|
for (int i = 0, n = cur.length; i < n; i++) {
|
||||||
|
if (cur[i]) img.setRGB(i % targetSize, i / targetSize, 0xFF000000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.showAreas()) {
|
if (opts.showAreas()) {
|
||||||
gfx.setStroke(new BasicStroke(lineW));
|
float dash = lineW * 7f, gap = lineW * 4f;
|
||||||
|
BasicStroke dashed = new BasicStroke(lineW * 1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
|
||||||
|
10f, new float[]{dash, gap}, 0f);
|
||||||
|
int aFontSize = Math.max(8, targetSize / 160);
|
||||||
|
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
|
||||||
for (PlacedArea a : input.areas()) {
|
for (PlacedArea a : input.areas()) {
|
||||||
Color c = hashColor(a.areaId());
|
|
||||||
int[] xs = wrp(a.pointsX(), wx0, rSize, targetSize);
|
int[] xs = wrp(a.pointsX(), wx0, rSize, targetSize);
|
||||||
int[] ys = wrp(a.pointsZ(), wz0, rSize, targetSize);
|
int[] ys = wrp(a.pointsZ(), wz0, rSize, targetSize);
|
||||||
gfx.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
|
gfx.setStroke(dashed);
|
||||||
gfx.fillPolygon(xs, ys, xs.length);
|
gfx.setColor(Color.BLACK);
|
||||||
gfx.setColor(c);
|
gfx.draw(toPath(xs, ys));
|
||||||
gfx.drawPolygon(xs, ys, xs.length);
|
String lbl = friendlyAreaName(a.areaId());
|
||||||
|
if (lbl != null) {
|
||||||
|
float[] cen = centroid(a.pointsX(), a.pointsZ());
|
||||||
|
float lx = Float.isNaN(a.labelX()) ? cen[0] : a.labelX();
|
||||||
|
float lz = Float.isNaN(a.labelZ()) ? cen[1] : a.labelZ();
|
||||||
|
int plx = wrp1(lx, wx0, rSize, targetSize);
|
||||||
|
int plz = wrp1(lz, wz0, rSize, targetSize);
|
||||||
|
drawLabel(gfx, lbl, plx, plz);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,39 +636,18 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opts.showLocations()) {
|
if (opts.showLocations()) {
|
||||||
gfx.setStroke(new BasicStroke(lineW));
|
|
||||||
int fontSize = Math.max(8, targetSize / 140);
|
int fontSize = Math.max(8, targetSize / 140);
|
||||||
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
||||||
for (Location loc : input.locations()) {
|
for (Location loc : input.locations()) {
|
||||||
int lx = wrp1(loc.getCenterX(), wx0, rSize, targetSize);
|
if (!loc.isShowOnMap()) continue;
|
||||||
int lz = wrp1(loc.getCenterZ(), wz0, rSize, targetSize);
|
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
||||||
int lr = Math.max(4, (int) (loc.getRadius() / rSize * targetSize));
|
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
|
||||||
gfx.setColor(new Color(255, 255, 200, 50));
|
float wz = Float.isNaN(loc.getLabelZ()) ? loc.getCenterZ() : loc.getLabelZ();
|
||||||
gfx.fillOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
int lx = wrp1(wx, wx0, rSize, targetSize);
|
||||||
gfx.setColor(new Color(255, 220, 60));
|
int lz = wrp1(wz, wz0, rSize, targetSize);
|
||||||
gfx.drawOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
drawLabel(gfx, friendlyLocationName(loc.getId()), lx, lz);
|
||||||
if (loc.getId() != null && !loc.getId().isEmpty()) {
|
|
||||||
String label = friendlyLocationName(loc.getId());
|
|
||||||
FontMetrics fm = gfx.getFontMetrics();
|
|
||||||
int tw = fm.stringWidth(label);
|
|
||||||
gfx.setColor(new Color(0, 0, 0, 160));
|
|
||||||
gfx.drawString(label, lx - tw / 2 + 1, lz - lr - 3);
|
|
||||||
gfx.setColor(Color.WHITE);
|
|
||||||
gfx.drawString(label, lx - tw / 2, lz - lr - 4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int spx = wrp1(m.spawnX, wx0, rSize, targetSize);
|
|
||||||
int spz = wrp1(m.spawnZ, wz0, rSize, targetSize);
|
|
||||||
int cr = Math.max(5, targetSize / 180);
|
|
||||||
gfx.setStroke(new BasicStroke(Math.max(2f, targetSize / 300f)));
|
|
||||||
gfx.setColor(new Color(0, 0, 0, 120));
|
|
||||||
gfx.drawLine(spx - cr + 1, spz + 1, spx + cr + 1, spz + 1);
|
|
||||||
gfx.drawLine(spx + 1, spz - cr + 1, spx + 1, spz + cr + 1);
|
|
||||||
gfx.setColor(new Color(60, 230, 90));
|
|
||||||
gfx.drawLine(spx - cr, spz, spx + cr, spz);
|
|
||||||
gfx.drawLine(spx, spz - cr, spx, spz + cr);
|
|
||||||
|
|
||||||
gfx.dispose();
|
gfx.dispose();
|
||||||
return img;
|
return img;
|
||||||
@@ -517,19 +691,41 @@ public final class WorldMapRenderer {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Color hashColor(String key) {
|
|
||||||
int hash = key == null ? 0 : key.hashCode();
|
|
||||||
int r = 100 + ((hash & 0xFF0000) >> 16) % 120;
|
|
||||||
int g = 100 + ((hash & 0x00FF00) >> 8) % 120;
|
|
||||||
int b = 100 + ((hash & 0x0000FF)) % 120;
|
|
||||||
return new Color(r, g, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String friendlyLocationName(String id) {
|
private static String friendlyLocationName(String id) {
|
||||||
String s = id.replace("location.", "").replace(".name", "");
|
String s = id.replace("location.", "").replace(".name", "");
|
||||||
return s.isEmpty() ? id : s;
|
return s.isEmpty() ? id : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String friendlyAreaName(String id) {
|
||||||
|
if (id == null || id.isBlank()) return null;
|
||||||
|
String s = id.replace("area.", "");
|
||||||
|
if (s.endsWith(".name")) s = s.substring(0, s.length() - 5);
|
||||||
|
return s.isBlank() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[] centroid(float[] xs, float[] zs) {
|
||||||
|
float cx = 0f, cz = 0f;
|
||||||
|
for (int i = 0; i < xs.length; i++) { cx += xs[i]; cz += zs[i]; }
|
||||||
|
return new float[]{cx / xs.length, cz / xs.length};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path2D toPath(int[] xs, int[] ys) {
|
||||||
|
Path2D p = new Path2D.Float();
|
||||||
|
p.moveTo(xs[0], ys[0]);
|
||||||
|
for (int i = 1; i < xs.length; i++) p.lineTo(xs[i], ys[i]);
|
||||||
|
p.closePath();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawLabel(Graphics2D g, String label, int x, int y) {
|
||||||
|
FontMetrics fm = g.getFontMetrics();
|
||||||
|
int tw = fm.stringWidth(label);
|
||||||
|
g.setColor(new Color(0, 0, 0, 180));
|
||||||
|
g.drawString(label, x - tw / 2 + 1, y + 1);
|
||||||
|
g.setColor(Color.WHITE);
|
||||||
|
g.drawString(label, x - tw / 2, y);
|
||||||
|
}
|
||||||
|
|
||||||
// Konvertiert eine Weltkoordinate in einen Pixel-Index innerhalb einer Region.
|
// Konvertiert eine Weltkoordinate in einen Pixel-Index innerhalb einer Region.
|
||||||
private static int wrp1(float worldCoord, float regionOrigin, float regionSize, int targetSize) {
|
private static int wrp1(float worldCoord, float regionOrigin, float regionSize, int targetSize) {
|
||||||
return (int) ((worldCoord - regionOrigin) / regionSize * (targetSize - 1));
|
return (int) ((worldCoord - regionOrigin) / regionSize * (targetSize - 1));
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public class Location {
|
|||||||
private float centerZ;
|
private float centerZ;
|
||||||
private float radius;
|
private float radius;
|
||||||
private List<Trigger> triggers = new ArrayList<>();
|
private List<Trigger> triggers = new ArrayList<>();
|
||||||
|
private boolean showOnMap = true;
|
||||||
|
private float labelX = Float.NaN;
|
||||||
|
private float labelZ = Float.NaN;
|
||||||
|
|
||||||
/** Leitet die ID aus dem TextReference-Schlüssel ab – eindeutiger Bezeichner der Location. */
|
/** Leitet die ID aus dem TextReference-Schlüssel ab – eindeutiger Bezeichner der Location. */
|
||||||
public String getId() { return name != null ? name.id() : ""; }
|
public String getId() { return name != null ? name.id() : ""; }
|
||||||
|
|||||||
@@ -357,7 +357,8 @@ public class AreaState extends BaseAppState {
|
|||||||
PlacedArea existing = areas.get(idx);
|
PlacedArea existing = areas.get(idx);
|
||||||
areas.set(idx, new PlacedArea(existing.pointsX(), existing.pointsZ(),
|
areas.set(idx, new PlacedArea(existing.pointsX(), existing.pointsZ(),
|
||||||
updated.areaId(),
|
updated.areaId(),
|
||||||
updated.triggers() != null ? updated.triggers() : existing.triggers()));
|
updated.triggers() != null ? updated.triggers() : existing.triggers(),
|
||||||
|
existing.labelX(), existing.labelZ()));
|
||||||
} else {
|
} else {
|
||||||
areas.set(idx, updated);
|
areas.set(idx, updated);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public class LocationEditorView extends BorderPane {
|
|||||||
|
|
||||||
private TextField idField;
|
private TextField idField;
|
||||||
private Label nameKeyLabel;
|
private Label nameKeyLabel;
|
||||||
|
private CheckBox showOnMapCheck;
|
||||||
private TriggerListEditor triggerEditor;
|
private TriggerListEditor triggerEditor;
|
||||||
private VBox formContainer;
|
private VBox formContainer;
|
||||||
|
|
||||||
@@ -83,6 +84,11 @@ public class LocationEditorView extends BorderPane {
|
|||||||
if (!is) { savePause.stop(); persistCurrent(); }
|
if (!is) { savePause.stop(); persistCurrent(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
showOnMapCheck = new CheckBox("Name auf Karte anzeigen");
|
||||||
|
showOnMapCheck.setSelected(true);
|
||||||
|
showOnMapCheck.setStyle("-fx-text-fill: #ccc;");
|
||||||
|
showOnMapCheck.selectedProperty().addListener((obs, o, n) -> scheduleSave());
|
||||||
|
|
||||||
triggerEditor = new TriggerListEditor(List.of(), () -> {});
|
triggerEditor = new TriggerListEditor(List.of(), () -> {});
|
||||||
|
|
||||||
form.getChildren().addAll(
|
form.getChildren().addAll(
|
||||||
@@ -90,6 +96,7 @@ public class LocationEditorView extends BorderPane {
|
|||||||
new Separator(),
|
new Separator(),
|
||||||
row("ID:", idField),
|
row("ID:", idField),
|
||||||
nameKeyLabel,
|
nameKeyLabel,
|
||||||
|
showOnMapCheck,
|
||||||
sectionTitle("Trigger"),
|
sectionTitle("Trigger"),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
triggerEditor
|
triggerEditor
|
||||||
@@ -112,6 +119,7 @@ public class LocationEditorView extends BorderPane {
|
|||||||
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
||||||
idField.setText(shortId);
|
idField.setText(shortId);
|
||||||
nameKeyLabel.setText("Name-Key: " + (shortId.isBlank() ? "—" : PREFIX + shortId + ".name"));
|
nameKeyLabel.setText("Name-Key: " + (shortId.isBlank() ? "—" : PREFIX + shortId + ".name"));
|
||||||
|
showOnMapCheck.setSelected(loc.isShowOnMap());
|
||||||
|
|
||||||
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
||||||
triggerEditor = new TriggerListEditor(
|
triggerEditor = new TriggerListEditor(
|
||||||
@@ -124,11 +132,13 @@ public class LocationEditorView extends BorderPane {
|
|||||||
String fullId = shortId.isBlank() ? "" : PREFIX + shortId;
|
String fullId = shortId.isBlank() ? "" : PREFIX + shortId;
|
||||||
loc.setName(fullId.isBlank() ? null : new TextReference(fullId));
|
loc.setName(fullId.isBlank() ? null : new TextReference(fullId));
|
||||||
loc.setTriggers(triggerEditor.getTriggers());
|
loc.setTriggers(triggerEditor.getTriggers());
|
||||||
|
loc.setShowOnMap(showOnMapCheck.isSelected());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearForm() {
|
private void clearForm() {
|
||||||
idField.clear();
|
idField.clear();
|
||||||
nameKeyLabel.setText("Name-Key: —");
|
nameKeyLabel.setText("Name-Key: —");
|
||||||
|
showOnMapCheck.setSelected(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── List operations ───────────────────────────────────────────────────────
|
// ── List operations ───────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ import javafx.application.Platform;
|
|||||||
import javafx.embed.swing.SwingFXUtils;
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.canvas.Canvas;
|
import javafx.scene.canvas.Canvas;
|
||||||
import javafx.scene.canvas.GraphicsContext;
|
import javafx.scene.canvas.GraphicsContext;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.image.WritableImage;
|
import javafx.scene.image.WritableImage;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.shape.StrokeLineCap;
|
||||||
|
import javafx.scene.shape.StrokeLineJoin;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import javafx.scene.text.FontWeight;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
@@ -26,18 +32,32 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interaktive 2D-Weltkarte im Editor-Tab.
|
* Interaktive 2D-Weltkarte im Editor-Tab.
|
||||||
* Zoom via Mausrad, Pan via Linksklick-Drag.
|
*
|
||||||
|
* Architektur: Terrain/Wasser/Zonen/Modelle als vorgerechnetes PNG (Hintergrund).
|
||||||
|
* Areas und Locations werden als Vektor-Overlay direkt auf den Canvas gezeichnet,
|
||||||
|
* sodass Liniendicke und Schriftgröße beim Zoomen konstant bleiben.
|
||||||
*/
|
*/
|
||||||
public class WorldMapView extends VBox {
|
public class WorldMapView extends VBox {
|
||||||
|
|
||||||
private static final int RENDER_SIZE = 2048;
|
private static final int RENDER_SIZE = 2048;
|
||||||
|
|
||||||
|
// Konstante Bildschirmgrößen für das Canvas-Overlay
|
||||||
|
private static final double AREA_LINE_W = 1.5;
|
||||||
|
private static final double AREA_DASH = 8.0;
|
||||||
|
private static final double AREA_GAP = 5.0;
|
||||||
|
private static final double AREA_CORNER_R = 12.0;
|
||||||
|
private static final double WAVE_W = 20.0; // konstante Wellengröße in Bildschirmpixeln
|
||||||
|
private static final int SEA_MASK_SIZE = 1024;
|
||||||
|
private static final Font AREA_FONT = Font.font("SansSerif", FontWeight.BOLD, 11);
|
||||||
|
private static final Font LOC_FONT = Font.font("SansSerif", FontWeight.BOLD, 12);
|
||||||
|
|
||||||
private final Canvas canvas = new Canvas(100, 100);
|
private final Canvas canvas = new Canvas(100, 100);
|
||||||
private final Label statusLbl = new Label("Karte noch nicht geladen");
|
private final Label statusLbl = new Label("Karte noch nicht geladen");
|
||||||
private final ProgressBar progress = new ProgressBar(-1);
|
private final ProgressBar progress = new ProgressBar(-1);
|
||||||
@@ -49,10 +69,17 @@ public class WorldMapView extends VBox {
|
|||||||
private final ToggleButton layerZones = layerBtn("Zonen");
|
private final ToggleButton layerZones = layerBtn("Zonen");
|
||||||
private final ToggleButton layerLocations = layerBtn("Orte");
|
private final ToggleButton layerLocations = layerBtn("Orte");
|
||||||
private final ToggleButton layerModels = layerBtn("Modelle");
|
private final ToggleButton layerModels = layerBtn("Modelle");
|
||||||
|
private final ToggleButton labelBtn = new ToggleButton("Label");
|
||||||
|
|
||||||
private WritableImage mapFxImage;
|
private WritableImage mapFxImage;
|
||||||
private BufferedImage mapBuffered;
|
private BufferedImage mapBuffered;
|
||||||
|
|
||||||
|
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
||||||
|
private List<Location> cachedLocs = new ArrayList<>();
|
||||||
|
private List<PlacedWater> cachedWaters = new ArrayList<>();
|
||||||
|
private boolean[] seaMask = null;
|
||||||
|
private float[][] seaCoastSegs = null; // [wx1,wz1,wx2,wz2] in Weltkoordinaten
|
||||||
|
|
||||||
private double panX = 0, panY = 0;
|
private double panX = 0, panY = 0;
|
||||||
private double scale = 1.0;
|
private double scale = 1.0;
|
||||||
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
|
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
|
||||||
@@ -92,22 +119,27 @@ public class WorldMapView extends VBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void buildUi() {
|
private void buildUi() {
|
||||||
// ── Layer-Toolbar ──────────────────────────────────────────────────────
|
|
||||||
Button refreshBtn = new Button("Aktualisieren");
|
Button refreshBtn = new Button("Aktualisieren");
|
||||||
refreshBtn.setOnAction(e -> loadAndRender());
|
refreshBtn.setOnAction(e -> loadAndRender());
|
||||||
|
|
||||||
Button exportBtn = new Button("Als PNG exportieren…");
|
Button exportBtn = new Button("Als PNG exportieren…");
|
||||||
exportBtn.setOnAction(e -> exportPng());
|
exportBtn.setOnAction(e -> exportPng());
|
||||||
|
|
||||||
|
labelBtn.setStyle("-fx-font-size: 11;");
|
||||||
|
labelBtn.selectedProperty().addListener((obs, o, n) ->
|
||||||
|
canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT));
|
||||||
|
|
||||||
ToolBar toolbar = new ToolBar(
|
ToolBar toolbar = new ToolBar(
|
||||||
new Label("Layer:"),
|
new Label("Layer:"),
|
||||||
layerTerrain, layerWater, layerAreas, layerZones, layerLocations, layerModels,
|
layerTerrain, layerWater, layerAreas, layerZones, layerLocations, layerModels,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
|
new Label("Bearbeiten:"),
|
||||||
|
labelBtn,
|
||||||
|
new Separator(),
|
||||||
refreshBtn,
|
refreshBtn,
|
||||||
exportBtn
|
exportBtn
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Canvas ────────────────────────────────────────────────────────────
|
|
||||||
canvasPane.setStyle("-fx-background-color: #1a1a2a;");
|
canvasPane.setStyle("-fx-background-color: #1a1a2a;");
|
||||||
VBox.setVgrow(canvasPane, Priority.ALWAYS);
|
VBox.setVgrow(canvasPane, Priority.ALWAYS);
|
||||||
|
|
||||||
@@ -116,15 +148,16 @@ public class WorldMapView extends VBox {
|
|||||||
canvas.widthProperty().addListener(obs -> redraw());
|
canvas.widthProperty().addListener(obs -> redraw());
|
||||||
canvas.heightProperty().addListener(obs -> redraw());
|
canvas.heightProperty().addListener(obs -> redraw());
|
||||||
|
|
||||||
// Layer-Toggle → neu rendern
|
// Terrain/Wasser/Zonen/Modelle → PNG neu rendern
|
||||||
layerTerrain.setOnAction(e -> rerender());
|
layerTerrain.setOnAction(e -> rerender());
|
||||||
layerWater.setOnAction(e -> rerender());
|
layerWater.setOnAction(e -> rerender());
|
||||||
layerAreas.setOnAction(e -> rerender());
|
|
||||||
layerZones.setOnAction(e -> rerender());
|
layerZones.setOnAction(e -> rerender());
|
||||||
layerLocations.setOnAction(e -> rerender());
|
|
||||||
layerModels.setOnAction(e -> rerender());
|
layerModels.setOnAction(e -> rerender());
|
||||||
|
|
||||||
// Pan
|
// Areas und Locations → nur Canvas-Overlay neu zeichnen (kein PNG-Rerender)
|
||||||
|
layerAreas.setOnAction(e -> redraw());
|
||||||
|
layerLocations.setOnAction(e -> redraw());
|
||||||
|
|
||||||
canvas.setOnMousePressed(e -> {
|
canvas.setOnMousePressed(e -> {
|
||||||
dragStartX = e.getX();
|
dragStartX = e.getX();
|
||||||
dragStartY = e.getY();
|
dragStartY = e.getY();
|
||||||
@@ -136,8 +169,13 @@ public class WorldMapView extends VBox {
|
|||||||
panY = dragStartPanY + (e.getY() - dragStartY);
|
panY = dragStartPanY + (e.getY() - dragStartY);
|
||||||
redraw();
|
redraw();
|
||||||
});
|
});
|
||||||
|
canvas.setOnMouseReleased(e -> {
|
||||||
|
if (!labelBtn.isSelected()) return;
|
||||||
|
if (Math.abs(e.getX() - dragStartX) < 5 && Math.abs(e.getY() - dragStartY) < 5) {
|
||||||
|
handleLabelPlacement(e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Zoom (Mausrad, Ziel = Mausposition)
|
|
||||||
canvas.setOnScroll(e -> {
|
canvas.setOnScroll(e -> {
|
||||||
if (e.getDeltaY() == 0) return;
|
if (e.getDeltaY() == 0) return;
|
||||||
double factor = e.getDeltaY() > 0 ? 1.15 : 1.0 / 1.15;
|
double factor = e.getDeltaY() > 0 ? 1.15 : 1.0 / 1.15;
|
||||||
@@ -148,15 +186,13 @@ public class WorldMapView extends VBox {
|
|||||||
redraw();
|
redraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tooltip mit Weltkoordinaten
|
|
||||||
canvas.setOnMouseMoved(e -> {
|
canvas.setOnMouseMoved(e -> {
|
||||||
if (mapFxImage == null) return;
|
if (mapFxImage == null) return;
|
||||||
float wx = WorldMapRenderer.pixelToWorld(e.getX(), canvas.getWidth(), panX, scale);
|
double wx = (e.getX() - panX) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
|
||||||
float wz = WorldMapRenderer.pixelToWorld(e.getY(), canvas.getHeight(), panY, scale);
|
double wz = (e.getY() - panY) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
|
||||||
statusLbl.setText(String.format("X: %.1f Z: %.1f", wx, wz));
|
statusLbl.setText(String.format("X: %.1f Z: %.1f", wx, wz));
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Statuszeile ───────────────────────────────────────────────────────
|
|
||||||
progress.setPrefWidth(160);
|
progress.setPrefWidth(160);
|
||||||
progress.setVisible(false);
|
progress.setVisible(false);
|
||||||
HBox statusBar = new HBox(8, statusLbl, progress);
|
HBox statusBar = new HBox(8, statusLbl, progress);
|
||||||
@@ -171,10 +207,8 @@ public class WorldMapView extends VBox {
|
|||||||
|
|
||||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** True wenn die Karte bereits geladen wurde. */
|
|
||||||
public boolean isLoaded() { return mapFxImage != null || loading.get(); }
|
public boolean isLoaded() { return mapFxImage != null || loading.get(); }
|
||||||
|
|
||||||
/** Lädt Welt-Daten und rendert die Karte (im Hintergrund). */
|
|
||||||
public void loadAndRender() {
|
public void loadAndRender() {
|
||||||
if (loading.getAndSet(true)) return;
|
if (loading.getAndSet(true)) return;
|
||||||
progress.setVisible(true);
|
progress.setVisible(true);
|
||||||
@@ -193,9 +227,16 @@ public class WorldMapView extends VBox {
|
|||||||
|
|
||||||
int[] slotColors = computeSlotColors(mapData);
|
int[] slotColors = computeSlotColors(mapData);
|
||||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildOptions());
|
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildBackgroundOptions());
|
||||||
|
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE);
|
||||||
|
float[][] segs = buildSeaCoastSegs(mask, SEA_MASK_SIZE);
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
cachedAreas = new ArrayList<>(areas);
|
||||||
|
cachedLocs = new ArrayList<>(locs);
|
||||||
|
cachedWaters = new ArrayList<>(waters);
|
||||||
|
seaMask = mask;
|
||||||
|
seaCoastSegs = segs;
|
||||||
mapBuffered = bi;
|
mapBuffered = bi;
|
||||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||||
fitToView();
|
fitToView();
|
||||||
@@ -227,17 +268,21 @@ public class WorldMapView extends VBox {
|
|||||||
|
|
||||||
Thread t = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
// Bereits geladene Daten nochmal laden, damit Layer-Wechsel korrekt
|
|
||||||
MapData mapData = MapIO.load();
|
MapData mapData = MapIO.load();
|
||||||
List<PlacedArea> areas = AreaIO.load();
|
List<PlacedArea> areas = AreaIO.load();
|
||||||
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
||||||
List<Location> locs = LocationIO.load();
|
List<Location> locs = LocationIO.load();
|
||||||
List<PlacedWater> waters = WaterBodyIO.load();
|
List<PlacedWater> waters = WaterBodyIO.load();
|
||||||
List<PlacedModel> models = PlacedModelIO.load();
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
int[] slotColors2 = computeSlotColors(mapData);
|
int[] slotColors = computeSlotColors(mapData);
|
||||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors2);
|
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildOptions());
|
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildBackgroundOptions());
|
||||||
|
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
cachedAreas = new ArrayList<>(areas);
|
||||||
|
cachedLocs = new ArrayList<>(locs);
|
||||||
|
cachedWaters = new ArrayList<>(waters);
|
||||||
|
seaMask = mask;
|
||||||
mapBuffered = bi;
|
mapBuffered = bi;
|
||||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||||
redraw();
|
redraw();
|
||||||
@@ -261,7 +306,7 @@ public class WorldMapView extends VBox {
|
|||||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||||
double w = canvas.getWidth(), h = canvas.getHeight();
|
double w = canvas.getWidth(), h = canvas.getHeight();
|
||||||
|
|
||||||
gc.setFill(javafx.scene.paint.Color.rgb(20, 20, 35));
|
gc.setFill(Color.rgb(20, 20, 35));
|
||||||
gc.fillRect(0, 0, w, h);
|
gc.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
if (mapFxImage == null) {
|
if (mapFxImage == null) {
|
||||||
@@ -270,20 +315,364 @@ public class WorldMapView extends VBox {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double imgW = mapFxImage.getWidth() * scale;
|
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||||
double imgH = mapFxImage.getHeight() * scale;
|
|
||||||
gc.drawImage(mapFxImage, panX, panY, imgW, imgH);
|
if (layerWater.isSelected()) drawWaterOverlay(gc);
|
||||||
|
if (layerAreas.isSelected()) drawAreasOverlay(gc);
|
||||||
|
if (layerLocations.isSelected()) drawLocationsOverlay(gc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Canvas-Vektor-Overlays ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void drawAreasOverlay(GraphicsContext gc) {
|
||||||
|
if (cachedAreas.isEmpty()) return;
|
||||||
|
gc.save();
|
||||||
|
gc.setStroke(Color.BLACK);
|
||||||
|
gc.setLineWidth(AREA_LINE_W);
|
||||||
|
gc.setLineDashes(AREA_DASH, AREA_GAP);
|
||||||
|
gc.setLineDashOffset(0);
|
||||||
|
gc.setLineCap(StrokeLineCap.ROUND);
|
||||||
|
gc.setLineJoin(StrokeLineJoin.ROUND);
|
||||||
|
gc.setFont(AREA_FONT);
|
||||||
|
|
||||||
|
for (PlacedArea a : cachedAreas) {
|
||||||
|
float[] wx = a.pointsX(), wz = a.pointsZ();
|
||||||
|
int n = wx.length;
|
||||||
|
if (n < 2) continue;
|
||||||
|
|
||||||
|
// Weltkoordinaten → Canvas-Pixel
|
||||||
|
double[] cx = new double[n], cz = new double[n];
|
||||||
|
for (int i = 0; i < n; i++) { cx[i] = toCanvasX(wx[i]); cz[i] = toCanvasZ(wz[i]); }
|
||||||
|
|
||||||
|
// Approach- und Departure-Punkte für abgerundete Ecken (quadratische Beziers)
|
||||||
|
double[] apx = new double[n], apz = new double[n];
|
||||||
|
double[] dpx = new double[n], dpz = new double[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
int prev = (i + n - 1) % n, next = (i + 1) % n;
|
||||||
|
double dx1 = cx[i] - cx[prev], dz1 = cz[i] - cz[prev];
|
||||||
|
double dx2 = cx[next] - cx[i], dz2 = cz[next] - cz[i];
|
||||||
|
double l1 = Math.sqrt(dx1*dx1 + dz1*dz1), l2 = Math.sqrt(dx2*dx2 + dz2*dz2);
|
||||||
|
double r = Math.min(AREA_CORNER_R, Math.min(l1, l2) * 0.4);
|
||||||
|
apx[i] = l1 > 0 ? cx[i] - dx1/l1*r : cx[i];
|
||||||
|
apz[i] = l1 > 0 ? cz[i] - dz1/l1*r : cz[i];
|
||||||
|
dpx[i] = l2 > 0 ? cx[i] + dx2/l2*r : cx[i];
|
||||||
|
dpz[i] = l2 > 0 ? cz[i] + dz2/l2*r : cz[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
gc.beginPath();
|
||||||
|
gc.moveTo(apx[0], apz[0]);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
gc.quadraticCurveTo(cx[i], cz[i], dpx[i], dpz[i]);
|
||||||
|
int next = (i + 1) % n;
|
||||||
|
gc.lineTo(apx[next], apz[next]);
|
||||||
|
}
|
||||||
|
gc.closePath();
|
||||||
|
gc.stroke();
|
||||||
|
|
||||||
|
String lbl = areaLabel(a.areaId());
|
||||||
|
if (lbl != null) {
|
||||||
|
float[] cen = centroid(wx, wz);
|
||||||
|
double lx = toCanvasX(Float.isNaN(a.labelX()) ? cen[0] : a.labelX());
|
||||||
|
double lz = toCanvasZ(Float.isNaN(a.labelZ()) ? cen[1] : a.labelZ());
|
||||||
|
drawCanvasLabel(gc, lbl, lx, lz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gc.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLocationsOverlay(GraphicsContext gc) {
|
||||||
|
if (cachedLocs.isEmpty()) return;
|
||||||
|
gc.save();
|
||||||
|
gc.setFont(LOC_FONT);
|
||||||
|
|
||||||
|
for (Location loc : cachedLocs) {
|
||||||
|
if (!loc.isShowOnMap()) continue;
|
||||||
|
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
||||||
|
double lx = toCanvasX(Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX());
|
||||||
|
double lz = toCanvasZ(Float.isNaN(loc.getLabelZ()) ? loc.getCenterZ() : loc.getLabelZ());
|
||||||
|
drawCanvasLabel(gc, locLabel(loc.getId()), lx, lz);
|
||||||
|
}
|
||||||
|
gc.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawWaterOverlay(GraphicsContext gc) {
|
||||||
|
// ── Seewellen (Terrain < 0) ──────────────────────────────────────────────
|
||||||
|
if (seaMask != null && layerWater.isSelected()) {
|
||||||
|
// Wellengröße in Weltkoordinaten (skaliert mit Zoom, damit Bildschirmgröße = WAVE_W)
|
||||||
|
double pxPerWorld = (RENDER_SIZE - 1) * scale / WorldMapRenderer.WORLD_SIZE;
|
||||||
|
double wWave = WAVE_W / pxPerWorld;
|
||||||
|
double wSpX = wWave * 1.8, wSpY = wWave * 1.05, wAmp = WAVE_W * 0.2;
|
||||||
|
|
||||||
|
// Sichtbarer Weltbereich
|
||||||
|
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
||||||
|
double wxMin = toWorldX(0), wxMax = toWorldX(cW);
|
||||||
|
double wzMin = toWorldZ(0), wzMax = toWorldZ(cH);
|
||||||
|
wxMin = Math.max(-WorldMapRenderer.WORLD_HALF, wxMin);
|
||||||
|
wzMin = Math.max(-WorldMapRenderer.WORLD_HALF, wzMin);
|
||||||
|
wxMax = Math.min(WorldMapRenderer.WORLD_HALF, wxMax);
|
||||||
|
wzMax = Math.min(WorldMapRenderer.WORLD_HALF, wzMax);
|
||||||
|
|
||||||
|
gc.setStroke(Color.BLACK);
|
||||||
|
gc.setLineWidth(0.8);
|
||||||
|
gc.setLineDashes(null);
|
||||||
|
|
||||||
|
// Gitter-Startzeile: auf Weltgitter einrasten → Wellen bleiben beim Panning stabil
|
||||||
|
long rowBase = (long)Math.floor((wzMin - wSpY * 0.5) / wSpY);
|
||||||
|
for (long rowIdx = rowBase; rowIdx * wSpY + wSpY * 0.5 < wzMax; rowIdx++) {
|
||||||
|
double wz = rowIdx * wSpY + wSpY * 0.5;
|
||||||
|
double rowOff = (rowIdx & 1L) == 1L ? wSpX * 0.5 : 0;
|
||||||
|
long colBase = (long)Math.floor((wxMin - rowOff) / wSpX);
|
||||||
|
for (long colIdx = colBase; colIdx * wSpX + rowOff < wxMax; colIdx++) {
|
||||||
|
double wx = colIdx * wSpX + rowOff;
|
||||||
|
double jx = Math.abs(Math.sin(rowIdx * 73.1 + colIdx * 157.3)) * wSpX * 0.25;
|
||||||
|
double jy = Math.sin(rowIdx * 211.7 + colIdx * 89.5) * wSpY * 0.2;
|
||||||
|
double owx = wx + jx, owz = wz + jy;
|
||||||
|
// Seekartenmaske prüfen (Mitte und Ende der Welle)
|
||||||
|
int mxS = (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
||||||
|
int mzS = (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
||||||
|
int mxE = (int)((owx + wWave + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
||||||
|
if (mxS < 0 || mxS >= SEA_MASK_SIZE || mzS < 0 || mzS >= SEA_MASK_SIZE) continue;
|
||||||
|
if (!seaMask[mzS * SEA_MASK_SIZE + mxS]) continue;
|
||||||
|
if (mxE < 0 || mxE >= SEA_MASK_SIZE || !seaMask[mzS * SEA_MASK_SIZE + mxE]) continue;
|
||||||
|
// Canvas-Koordinaten; Breite = WAVE_W px, Amplitude = wAmp px
|
||||||
|
double ox = toCanvasX((float)owx), oy = toCanvasZ((float)owz);
|
||||||
|
double ex = ox + WAVE_W;
|
||||||
|
gc.beginPath();
|
||||||
|
gc.moveTo(ox, oy);
|
||||||
|
gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wAmp, ox + WAVE_W*0.7, oy + wAmp, ex, oy);
|
||||||
|
gc.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedWaters.isEmpty()) return;
|
||||||
|
|
||||||
|
final double spX = WAVE_W * 1.8, spY = WAVE_W * 1.05, wmA = WAVE_W * 0.2;
|
||||||
|
|
||||||
|
for (PlacedWater w : cachedWaters) {
|
||||||
|
float[] wx = w.pointsX(), wz = w.pointsZ();
|
||||||
|
int n = wx.length;
|
||||||
|
if (n < 3) continue;
|
||||||
|
|
||||||
|
double[] cx = new double[n], cz = new double[n];
|
||||||
|
for (int i = 0; i < n; i++) { cx[i] = toCanvasX(wx[i]); cz[i] = toCanvasZ(wz[i]); }
|
||||||
|
|
||||||
|
double minX = cx[0], maxX = cx[0], minZ = cz[0], maxZ = cz[0];
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
if (cx[i] < minX) minX = cx[i]; if (cx[i] > maxX) maxX = cx[i];
|
||||||
|
if (cz[i] < minZ) minZ = cz[i]; if (cz[i] > maxZ) maxZ = cz[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wellenmarken: nur wenn Fläche groß genug für mindestens eine Welle
|
||||||
|
if ((maxX - minX) > WAVE_W * 1.5 && (maxZ - minZ) > spY) {
|
||||||
|
gc.save();
|
||||||
|
gc.beginPath();
|
||||||
|
gc.moveTo(cx[0], cz[0]);
|
||||||
|
for (int i = 1; i < n; i++) gc.lineTo(cx[i], cz[i]);
|
||||||
|
gc.closePath();
|
||||||
|
gc.clip();
|
||||||
|
|
||||||
|
gc.setStroke(Color.BLACK);
|
||||||
|
gc.setLineWidth(0.8);
|
||||||
|
gc.setLineDashes(null);
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
for (double wy = minZ + spY * 0.5; wy < maxZ; wy += spY, row++) {
|
||||||
|
double rowOff = (row & 1) == 1 ? spX * 0.5 : 0;
|
||||||
|
int col = 0;
|
||||||
|
for (double wxx = minX + rowOff; wxx + WAVE_W <= maxX; wxx += spX, col++) {
|
||||||
|
// deterministisches Jitter pro Zelle
|
||||||
|
double jx = Math.abs(Math.sin(row * 73.1 + col * 157.3)) * spX * 0.25;
|
||||||
|
double jy = Math.sin(row * 211.7 + col * 89.5) * spY * 0.2;
|
||||||
|
double ox = wxx + jx, oy = wy + jy;
|
||||||
|
if (ox < minX || ox + WAVE_W > maxX || oy < minZ || oy > maxZ) continue;
|
||||||
|
gc.beginPath();
|
||||||
|
gc.moveTo(ox, oy);
|
||||||
|
gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wmA, ox + WAVE_W*0.7, oy + wmA, ox + WAVE_W, oy);
|
||||||
|
gc.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gc.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schwarzer Umriss (konstant 3px)
|
||||||
|
gc.setStroke(Color.BLACK);
|
||||||
|
gc.setLineWidth(3.0);
|
||||||
|
gc.setLineDashes(null);
|
||||||
|
gc.beginPath();
|
||||||
|
gc.moveTo(cx[0], cz[0]);
|
||||||
|
for (int i = 1; i < n; i++) gc.lineTo(cx[i], cz[i]);
|
||||||
|
gc.closePath();
|
||||||
|
gc.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Seeküstenlinie (konstant 3px, weicher Halo gegen Treppeneffekt) ────────
|
||||||
|
if (seaCoastSegs != null && seaCoastSegs.length > 0) {
|
||||||
|
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
||||||
|
gc.setLineDashes(null);
|
||||||
|
gc.setLineCap(StrokeLineCap.ROUND);
|
||||||
|
|
||||||
|
// Halo-Pass: breiter, halbtransparent
|
||||||
|
gc.setStroke(Color.color(0, 0, 0, 0.2));
|
||||||
|
gc.setLineWidth(7.0);
|
||||||
|
for (float[] seg : seaCoastSegs) {
|
||||||
|
double sx1 = toCanvasX(seg[0]), sy1 = toCanvasZ(seg[1]);
|
||||||
|
double sx2 = toCanvasX(seg[2]), sy2 = toCanvasZ(seg[3]);
|
||||||
|
if (Math.max(sx1, sx2) < -8 || Math.min(sx1, sx2) > cW + 8) continue;
|
||||||
|
if (Math.max(sy1, sy2) < -8 || Math.min(sy1, sy2) > cH + 8) continue;
|
||||||
|
gc.strokeLine(sx1, sy1, sx2, sy2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kern-Pass: 3px solid
|
||||||
|
gc.setStroke(Color.BLACK);
|
||||||
|
gc.setLineWidth(3.0);
|
||||||
|
for (float[] seg : seaCoastSegs) {
|
||||||
|
double sx1 = toCanvasX(seg[0]), sy1 = toCanvasZ(seg[1]);
|
||||||
|
double sx2 = toCanvasX(seg[2]), sy2 = toCanvasZ(seg[3]);
|
||||||
|
if (Math.max(sx1, sx2) < -3 || Math.min(sx1, sx2) > cW + 3) continue;
|
||||||
|
if (Math.max(sy1, sy2) < -3 || Math.min(sy1, sy2) > cH + 3) continue;
|
||||||
|
gc.strokeLine(sx1, sy1, sx2, sy2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCanvasLabel(GraphicsContext gc, String text, double x, double y) {
|
||||||
|
Text measure = new Text(text);
|
||||||
|
measure.setFont(gc.getFont());
|
||||||
|
double tw = measure.getLayoutBounds().getWidth();
|
||||||
|
gc.setFill(Color.rgb(0, 0, 0, 0.7));
|
||||||
|
gc.fillText(text, x - tw / 2 + 1, y + 1);
|
||||||
|
gc.setFill(Color.WHITE);
|
||||||
|
gc.fillText(text, x - tw / 2, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Koordinaten-Umrechnung ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private double toCanvasX(float worldX) {
|
||||||
|
return panX + (worldX + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE - 1) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double toCanvasZ(float worldZ) {
|
||||||
|
return panY + (worldZ + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE - 1) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Label-Platzierung ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleLabelPlacement(double canvasX, double canvasY) {
|
||||||
|
float wx = toWorldX(canvasX);
|
||||||
|
float wz = toWorldZ(canvasY);
|
||||||
|
|
||||||
|
// Locations: Treffer innerhalb 2× Radius
|
||||||
|
for (Location loc : cachedLocs) {
|
||||||
|
float dx = wx - loc.getCenterX(), dz = wz - loc.getCenterZ(), r = loc.getRadius();
|
||||||
|
if (dx * dx + dz * dz <= r * r * 4) {
|
||||||
|
loc.setLabelX(wx);
|
||||||
|
loc.setLabelZ(wz);
|
||||||
|
try { LocationIO.save(cachedLocs); } catch (IOException e) { statusLbl.setText("Fehler: " + e.getMessage()); return; }
|
||||||
|
redraw();
|
||||||
|
statusLbl.setText(String.format("Label %s → (%.0f, %.0f)", loc.getId(), wx, wz));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Areas: kleinste enthaltende Area
|
||||||
|
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 (polyContains(a.pointsX(), a.pointsZ(), wx, wz)) {
|
||||||
|
double sz = boundingBoxArea(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 e) { statusLbl.setText("Fehler: " + e.getMessage()); return; }
|
||||||
|
redraw();
|
||||||
|
statusLbl.setText(String.format("Label %s → (%.0f, %.0f)", best.areaId(), wx, wz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[][] buildSeaCoastSegs(boolean[] mask, int size) {
|
||||||
|
List<float[]> segs = new ArrayList<>();
|
||||||
|
for (int mz = 0; mz < size - 1; mz++) {
|
||||||
|
for (int mx = 0; mx < size - 1; mx++) {
|
||||||
|
boolean c = mask[mz * size + mx];
|
||||||
|
boolean r = mask[mz * size + (mx + 1)];
|
||||||
|
boolean d = mask[(mz + 1) * size + mx];
|
||||||
|
float wx0 = (float)(mx / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||||
|
float wx1 = (float)((mx + 1) / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||||
|
float wz0 = (float)(mz / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||||
|
float wz1 = (float)((mz + 1) / (double)(size - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||||
|
float wzM = (wz0 + wz1) * 0.5f;
|
||||||
|
float wxM = (wx0 + wx1) * 0.5f;
|
||||||
|
// Horizontale Kante zwischen (mz,mx) und (mz+1,mx)
|
||||||
|
if (c != d) segs.add(new float[]{wx0, wzM, wx1, wzM});
|
||||||
|
// Vertikale Kante zwischen (mz,mx) und (mz,mx+1)
|
||||||
|
if (c != r) segs.add(new float[]{wxM, wz0, wxM, wz1});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segs.toArray(new float[0][]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void fitToView() {
|
private void fitToView() {
|
||||||
if (canvas.getWidth() <= 0 || mapFxImage == null) return;
|
if (canvas.getWidth() <= 0 || mapFxImage == null) return;
|
||||||
double sw = canvas.getWidth() / mapFxImage.getWidth();
|
scale = Math.min(canvas.getWidth() / mapFxImage.getWidth(), canvas.getHeight() / mapFxImage.getHeight());
|
||||||
double sh = canvas.getHeight() / mapFxImage.getHeight();
|
|
||||||
scale = Math.min(sw, sh);
|
|
||||||
panX = (canvas.getWidth() - mapFxImage.getWidth() * scale) / 2;
|
panX = (canvas.getWidth() - mapFxImage.getWidth() * scale) / 2;
|
||||||
panY = (canvas.getHeight() - mapFxImage.getHeight() * scale) / 2;
|
panY = (canvas.getHeight() - mapFxImage.getHeight() * scale) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean polyContains(float[] xs, float[] zs, float px, float pz) {
|
||||||
|
boolean inside = false;
|
||||||
|
int n = xs.length;
|
||||||
|
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
if ((zs[i] > pz) != (zs[j] > pz) &&
|
||||||
|
px < (xs[j] - xs[i]) * (pz - zs[i]) / (zs[j] - zs[i]) + xs[i]) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double boundingBoxArea(float[] xs, float[] zs) {
|
||||||
|
float mnX = xs[0], mxX = xs[0], mnZ = zs[0], mxZ = zs[0];
|
||||||
|
for (int i = 1; i < xs.length; i++) {
|
||||||
|
if (xs[i] < mnX) mnX = xs[i]; if (xs[i] > mxX) mxX = xs[i];
|
||||||
|
if (zs[i] < mnZ) mnZ = zs[i]; if (zs[i] > mxZ) mxZ = zs[i];
|
||||||
|
}
|
||||||
|
return (double)(mxX - mnX) * (mxZ - mnZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[] centroid(float[] xs, float[] zs) {
|
||||||
|
float cx = 0, cz = 0;
|
||||||
|
for (int i = 0; i < xs.length; i++) { cx += xs[i]; cz += zs[i]; }
|
||||||
|
return new float[]{cx / xs.length, cz / zs.length};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String areaLabel(String id) {
|
||||||
|
if (id == null || id.isBlank()) return null;
|
||||||
|
String s = id.replace("area.", "");
|
||||||
|
if (s.endsWith(".name")) s = s.substring(0, s.length() - 5);
|
||||||
|
return s.isBlank() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String locLabel(String id) {
|
||||||
|
String s = id.replace("location.", "").replace(".name", "");
|
||||||
|
return s.isEmpty() ? id : s;
|
||||||
|
}
|
||||||
|
|
||||||
private void exportPng() {
|
private void exportPng() {
|
||||||
FileChooser fc = new FileChooser();
|
FileChooser fc = new FileChooser();
|
||||||
fc.setTitle("Karte als PNG exportieren");
|
fc.setTitle("Karte als PNG exportieren");
|
||||||
@@ -303,9 +692,9 @@ public class WorldMapView extends VBox {
|
|||||||
List<Location> locs = LocationIO.load();
|
List<Location> locs = LocationIO.load();
|
||||||
List<PlacedWater> waters = WaterBodyIO.load();
|
List<PlacedWater> waters = WaterBodyIO.load();
|
||||||
List<PlacedModel> models = PlacedModelIO.load();
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
int[] slotColors3 = computeSlotColors(mapData);
|
int[] slotColors = computeSlotColors(mapData);
|
||||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors3);
|
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||||
BufferedImage bi = WorldMapRenderer.render(input, 4096, buildOptions());
|
BufferedImage bi = WorldMapRenderer.render(input, 4096, buildExportOptions());
|
||||||
ImageIO.write(bi, "PNG", file);
|
ImageIO.write(bi, "PNG", file);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
progress.setVisible(false);
|
progress.setVisible(false);
|
||||||
@@ -322,11 +711,27 @@ public class WorldMapView extends VBox {
|
|||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderOptions buildOptions() {
|
/** Hintergrund-PNG: ohne Areas und Locations (die werden live auf Canvas gezeichnet). */
|
||||||
|
private RenderOptions buildBackgroundOptions() {
|
||||||
return new RenderOptions(
|
return new RenderOptions(
|
||||||
layerTerrain.isSelected(),
|
layerTerrain.isSelected(),
|
||||||
layerTerrain.isSelected(),
|
layerTerrain.isSelected(),
|
||||||
layerWater.isSelected(),
|
layerWater.isSelected(),
|
||||||
|
false, // Wellen im Editor über Canvas gezeichnet
|
||||||
|
false,
|
||||||
|
layerZones.isSelected(),
|
||||||
|
false,
|
||||||
|
layerModels.isSelected()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Export-PNG: alle Layer gebacken. */
|
||||||
|
private RenderOptions buildExportOptions() {
|
||||||
|
return new RenderOptions(
|
||||||
|
layerTerrain.isSelected(),
|
||||||
|
layerTerrain.isSelected(),
|
||||||
|
layerWater.isSelected(),
|
||||||
|
true, // Wellen im Export-PNG gebacken
|
||||||
layerAreas.isSelected(),
|
layerAreas.isSelected(),
|
||||||
layerZones.isSelected(),
|
layerZones.isSelected(),
|
||||||
layerLocations.isSelected(),
|
layerLocations.isSelected(),
|
||||||
|
|||||||
Reference in New Issue
Block a user