Zwischenstand Minimap
This commit is contained in:
@@ -6,6 +6,7 @@ import de.blight.common.model.trigger.TriggerIO;
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class AreaIO {
|
||||
|
||||
@@ -19,7 +20,7 @@ public final class AreaIO {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||
w.write("# polygon\tareaId\ttriggersJson");
|
||||
w.write("# polygon\tareaId\ttriggersJson\tlabelX\tlabelZ");
|
||||
w.newLine();
|
||||
for (PlacedArea a : areas) {
|
||||
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
|
||||
@@ -27,6 +28,10 @@ public final class AreaIO {
|
||||
w.write(a.areaId());
|
||||
w.write('\t');
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +51,9 @@ public final class AreaIO {
|
||||
if (pts[0].length < 3) continue;
|
||||
String areaId = f.length > 1 ? f[1].strip() : "";
|
||||
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) {}
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -26,7 +26,7 @@ public final class LocationIO {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
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();
|
||||
for (Location loc : locations) {
|
||||
w.write(loc.getId());
|
||||
@@ -38,6 +38,12 @@ public final class LocationIO {
|
||||
w.write(String.format(Locale.ROOT, "%.3f", loc.getRadius()));
|
||||
w.write('\t');
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -62,6 +68,9 @@ public final class LocationIO {
|
||||
List<Trigger> triggers = TriggerIO.deserializeList(f[4].strip());
|
||||
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);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,14 @@ public record PlacedArea(
|
||||
float[] pointsX,
|
||||
float[] pointsZ,
|
||||
String areaId,
|
||||
List<Trigger> triggers
|
||||
List<Trigger> triggers,
|
||||
float labelX,
|
||||
float labelZ
|
||||
) {
|
||||
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 java.awt.*;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -42,14 +43,34 @@ public final class WorldMapRenderer {
|
||||
boolean showTerrain,
|
||||
boolean showSplatColors,
|
||||
boolean showWater,
|
||||
boolean showWaterWaves,
|
||||
boolean showAreas,
|
||||
boolean showZones,
|
||||
boolean showLocations,
|
||||
boolean showModels
|
||||
) {
|
||||
public static RenderOptions all() {
|
||||
return new RenderOptions(true, true, true, true, true, true, true);
|
||||
// Compat: alte 7-Parameter-Form → showWaterWaves = showWater
|
||||
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)
|
||||
@@ -147,47 +168,146 @@ public final class WorldMapRenderer {
|
||||
|
||||
// Wasser
|
||||
if (opts.showWater()) {
|
||||
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
|
||||
// Meer: Terrain unter Meeresspiegel
|
||||
// ── Weißfüllung ───────────────────────────────────────────────────────
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
if (heights[py * targetSize + px] < 0f) {
|
||||
img.setRGB(px, py, WATER_RGB);
|
||||
}
|
||||
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF);
|
||||
}
|
||||
}
|
||||
// Wasserflächen: nur dort sichtbar, wo Terrain unter Wasseroberfläche liegt
|
||||
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();
|
||||
int x0 = Math.max(0, bb.x);
|
||||
int y0 = Math.max(0, bb.y);
|
||||
Rectangle bb = poly.getBounds();
|
||||
int x0 = Math.max(0, bb.x), y0 = Math.max(0, bb.y);
|
||||
int x1 = Math.min(targetSize - 1, bb.x + bb.width);
|
||||
int y1 = Math.min(targetSize - 1, bb.y + bb.height);
|
||||
float wh = w.waterHeight();
|
||||
for (int py = y0; py <= y1; py++) {
|
||||
for (int px = x0; px <= x1; px++) {
|
||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh) {
|
||||
img.setRGB(px, py, WATER_RGB);
|
||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||
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
|
||||
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()) {
|
||||
Color c = hashColor(a.areaId());
|
||||
int[] xs = worldToPixels(a.pointsX(), targetSize);
|
||||
int[] ys = worldToPixels(a.pointsZ(), targetSize);
|
||||
gfx.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
|
||||
gfx.fillPolygon(xs, ys, xs.length);
|
||||
gfx.setColor(c);
|
||||
gfx.drawPolygon(xs, ys, xs.length);
|
||||
gfx.setStroke(dashed);
|
||||
gfx.setColor(Color.BLACK);
|
||||
gfx.draw(toPath(xs, ys));
|
||||
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,45 +335,21 @@ public final class WorldMapRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Locations (Kreise + Labels)
|
||||
// Locations (nur Label)
|
||||
if (opts.showLocations()) {
|
||||
gfx.setStroke(new BasicStroke(lineW));
|
||||
int fontSize = Math.max(8, targetSize / 140);
|
||||
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
||||
for (Location loc : input.locations()) {
|
||||
int lx = worldToPixel(loc.getCenterX(), targetSize);
|
||||
int lz = worldToPixel(loc.getCenterZ(), targetSize);
|
||||
int lr = Math.max(4, (int)(loc.getRadius() / WORLD_SIZE * targetSize));
|
||||
|
||||
gfx.setColor(new Color(255, 255, 200, 50));
|
||||
gfx.fillOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
||||
gfx.setColor(new Color(255, 220, 60));
|
||||
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);
|
||||
}
|
||||
if (!loc.isShowOnMap()) continue;
|
||||
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
||||
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
|
||||
float wz = Float.isNaN(loc.getLabelZ()) ? loc.getCenterZ() : loc.getLabelZ();
|
||||
int lx = worldToPixel(wx, targetSize);
|
||||
int lz = worldToPixel(wz, targetSize);
|
||||
drawLabel(gfx, friendlyLocationName(loc.getId()), lx, lz);
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
return img;
|
||||
}
|
||||
@@ -375,46 +471,145 @@ public final class WorldMapRenderer {
|
||||
float lineW = Math.max(1.5f, targetSize / 400f);
|
||||
|
||||
if (opts.showWater()) {
|
||||
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
|
||||
// Meer: Terrain unter Meeresspiegel
|
||||
// ── Weißfüllung ───────────────────────────────────────────────────────
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
if (heights[py * targetSize + px] < 0f) {
|
||||
img.setRGB(px, py, WATER_RGB);
|
||||
}
|
||||
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, 0xFFFFFFFF);
|
||||
}
|
||||
}
|
||||
// Wasserflächen: nur dort sichtbar, wo Terrain unter Wasseroberfläche liegt
|
||||
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();
|
||||
int x0 = Math.max(0, bb.x);
|
||||
int y0 = Math.max(0, bb.y);
|
||||
Rectangle bb = poly.getBounds();
|
||||
int x0 = Math.max(0, bb.x), y0 = Math.max(0, bb.y);
|
||||
int x1 = Math.min(targetSize - 1, bb.x + bb.width);
|
||||
int y1 = Math.min(targetSize - 1, bb.y + bb.height);
|
||||
float wh = w.waterHeight();
|
||||
for (int py = y0; py <= y1; py++) {
|
||||
for (int px = x0; px <= x1; px++) {
|
||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh) {
|
||||
img.setRGB(px, py, WATER_RGB);
|
||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||
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()) {
|
||||
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()) {
|
||||
Color c = hashColor(a.areaId());
|
||||
int[] xs = wrp(a.pointsX(), wx0, rSize, targetSize);
|
||||
int[] ys = wrp(a.pointsZ(), wz0, rSize, targetSize);
|
||||
gfx.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
|
||||
gfx.fillPolygon(xs, ys, xs.length);
|
||||
gfx.setColor(c);
|
||||
gfx.drawPolygon(xs, ys, xs.length);
|
||||
gfx.setStroke(dashed);
|
||||
gfx.setColor(Color.BLACK);
|
||||
gfx.draw(toPath(xs, ys));
|
||||
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,40 +636,19 @@ public final class WorldMapRenderer {
|
||||
}
|
||||
|
||||
if (opts.showLocations()) {
|
||||
gfx.setStroke(new BasicStroke(lineW));
|
||||
int fontSize = Math.max(8, targetSize / 140);
|
||||
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
||||
for (Location loc : input.locations()) {
|
||||
int lx = wrp1(loc.getCenterX(), wx0, rSize, targetSize);
|
||||
int lz = wrp1(loc.getCenterZ(), wz0, rSize, targetSize);
|
||||
int lr = Math.max(4, (int) (loc.getRadius() / rSize * targetSize));
|
||||
gfx.setColor(new Color(255, 255, 200, 50));
|
||||
gfx.fillOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
||||
gfx.setColor(new Color(255, 220, 60));
|
||||
gfx.drawOval(lx - lr, lz - lr, lr * 2, lr * 2);
|
||||
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);
|
||||
}
|
||||
if (!loc.isShowOnMap()) continue;
|
||||
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
||||
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
|
||||
float wz = Float.isNaN(loc.getLabelZ()) ? loc.getCenterZ() : loc.getLabelZ();
|
||||
int lx = wrp1(wx, wx0, rSize, targetSize);
|
||||
int lz = wrp1(wz, wz0, rSize, targetSize);
|
||||
drawLabel(gfx, friendlyLocationName(loc.getId()), lx, lz);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
return img;
|
||||
}
|
||||
@@ -517,19 +691,41 @@ public final class WorldMapRenderer {
|
||||
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) {
|
||||
String s = id.replace("location.", "").replace(".name", "");
|
||||
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.
|
||||
private static int wrp1(float worldCoord, float regionOrigin, float regionSize, int targetSize) {
|
||||
return (int) ((worldCoord - regionOrigin) / regionSize * (targetSize - 1));
|
||||
|
||||
@@ -15,7 +15,10 @@ public class Location {
|
||||
private float centerX;
|
||||
private float centerZ;
|
||||
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. */
|
||||
public String getId() { return name != null ? name.id() : ""; }
|
||||
|
||||
@@ -357,7 +357,8 @@ public class AreaState extends BaseAppState {
|
||||
PlacedArea existing = areas.get(idx);
|
||||
areas.set(idx, new PlacedArea(existing.pointsX(), existing.pointsZ(),
|
||||
updated.areaId(),
|
||||
updated.triggers() != null ? updated.triggers() : existing.triggers()));
|
||||
updated.triggers() != null ? updated.triggers() : existing.triggers(),
|
||||
existing.labelX(), existing.labelZ()));
|
||||
} else {
|
||||
areas.set(idx, updated);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public class LocationEditorView extends BorderPane {
|
||||
|
||||
private TextField idField;
|
||||
private Label nameKeyLabel;
|
||||
private CheckBox showOnMapCheck;
|
||||
private TriggerListEditor triggerEditor;
|
||||
private VBox formContainer;
|
||||
|
||||
@@ -83,6 +84,11 @@ public class LocationEditorView extends BorderPane {
|
||||
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(), () -> {});
|
||||
|
||||
form.getChildren().addAll(
|
||||
@@ -90,6 +96,7 @@ public class LocationEditorView extends BorderPane {
|
||||
new Separator(),
|
||||
row("ID:", idField),
|
||||
nameKeyLabel,
|
||||
showOnMapCheck,
|
||||
sectionTitle("Trigger"),
|
||||
new Separator(),
|
||||
triggerEditor
|
||||
@@ -112,6 +119,7 @@ public class LocationEditorView extends BorderPane {
|
||||
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
||||
idField.setText(shortId);
|
||||
nameKeyLabel.setText("Name-Key: " + (shortId.isBlank() ? "—" : PREFIX + shortId + ".name"));
|
||||
showOnMapCheck.setSelected(loc.isShowOnMap());
|
||||
|
||||
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
||||
triggerEditor = new TriggerListEditor(
|
||||
@@ -124,11 +132,13 @@ public class LocationEditorView extends BorderPane {
|
||||
String fullId = shortId.isBlank() ? "" : PREFIX + shortId;
|
||||
loc.setName(fullId.isBlank() ? null : new TextReference(fullId));
|
||||
loc.setTriggers(triggerEditor.getTriggers());
|
||||
loc.setShowOnMap(showOnMapCheck.isSelected());
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
nameKeyLabel.setText("Name-Key: —");
|
||||
showOnMapCheck.setSelected(true);
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -10,12 +10,18 @@ import javafx.application.Platform;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.*;
|
||||
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.Stage;
|
||||
|
||||
@@ -26,18 +32,32 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
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 Label statusLbl = new Label("Karte noch nicht geladen");
|
||||
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 layerLocations = layerBtn("Orte");
|
||||
private final ToggleButton layerModels = layerBtn("Modelle");
|
||||
private final ToggleButton labelBtn = new ToggleButton("Label");
|
||||
|
||||
private WritableImage mapFxImage;
|
||||
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 scale = 1.0;
|
||||
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
|
||||
@@ -92,22 +119,27 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
// ── Layer-Toolbar ──────────────────────────────────────────────────────
|
||||
Button refreshBtn = new Button("Aktualisieren");
|
||||
refreshBtn.setOnAction(e -> loadAndRender());
|
||||
|
||||
Button exportBtn = new Button("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));
|
||||
|
||||
ToolBar toolbar = new ToolBar(
|
||||
new Label("Layer:"),
|
||||
layerTerrain, layerWater, layerAreas, layerZones, layerLocations, layerModels,
|
||||
new Separator(),
|
||||
new Label("Bearbeiten:"),
|
||||
labelBtn,
|
||||
new Separator(),
|
||||
refreshBtn,
|
||||
exportBtn
|
||||
);
|
||||
|
||||
// ── Canvas ────────────────────────────────────────────────────────────
|
||||
canvasPane.setStyle("-fx-background-color: #1a1a2a;");
|
||||
VBox.setVgrow(canvasPane, Priority.ALWAYS);
|
||||
|
||||
@@ -116,15 +148,16 @@ public class WorldMapView extends VBox {
|
||||
canvas.widthProperty().addListener(obs -> redraw());
|
||||
canvas.heightProperty().addListener(obs -> redraw());
|
||||
|
||||
// Layer-Toggle → neu rendern
|
||||
// Terrain/Wasser/Zonen/Modelle → PNG neu rendern
|
||||
layerTerrain.setOnAction(e -> rerender());
|
||||
layerWater.setOnAction(e -> rerender());
|
||||
layerAreas.setOnAction(e -> rerender());
|
||||
layerZones.setOnAction(e -> rerender());
|
||||
layerLocations.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 -> {
|
||||
dragStartX = e.getX();
|
||||
dragStartY = e.getY();
|
||||
@@ -136,11 +169,16 @@ public class WorldMapView extends VBox {
|
||||
panY = dragStartPanY + (e.getY() - dragStartY);
|
||||
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 -> {
|
||||
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;
|
||||
double oldScale = scale;
|
||||
scale = Math.max(0.1, Math.min(30.0, scale * factor));
|
||||
panX = e.getX() - (e.getX() - panX) * scale / oldScale;
|
||||
@@ -148,15 +186,13 @@ public class WorldMapView extends VBox {
|
||||
redraw();
|
||||
});
|
||||
|
||||
// Tooltip mit Weltkoordinaten
|
||||
canvas.setOnMouseMoved(e -> {
|
||||
if (mapFxImage == null) return;
|
||||
float wx = WorldMapRenderer.pixelToWorld(e.getX(), canvas.getWidth(), panX, scale);
|
||||
float wz = WorldMapRenderer.pixelToWorld(e.getY(), canvas.getHeight(), panY, scale);
|
||||
double wx = (e.getX() - panX) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
|
||||
double wz = (e.getY() - panY) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
|
||||
statusLbl.setText(String.format("X: %.1f Z: %.1f", wx, wz));
|
||||
});
|
||||
|
||||
// ── Statuszeile ───────────────────────────────────────────────────────
|
||||
progress.setPrefWidth(160);
|
||||
progress.setVisible(false);
|
||||
HBox statusBar = new HBox(8, statusLbl, progress);
|
||||
@@ -171,10 +207,8 @@ public class WorldMapView extends VBox {
|
||||
|
||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||
|
||||
/** True wenn die Karte bereits geladen wurde. */
|
||||
public boolean isLoaded() { return mapFxImage != null || loading.get(); }
|
||||
|
||||
/** Lädt Welt-Daten und rendert die Karte (im Hintergrund). */
|
||||
public void loadAndRender() {
|
||||
if (loading.getAndSet(true)) return;
|
||||
progress.setVisible(true);
|
||||
@@ -183,19 +217,26 @@ public class WorldMapView extends VBox {
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
MapData mapData = MapIO.load();
|
||||
List<PlacedArea> areas = AreaIO.load();
|
||||
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
||||
List<Location> locs = LocationIO.load();
|
||||
List<PlacedWater> waters = WaterBodyIO.load();
|
||||
List<PlacedModel> models = PlacedModelIO.load();
|
||||
List<PlacedArea> areas = AreaIO.load();
|
||||
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
||||
List<Location> locs = LocationIO.load();
|
||||
List<PlacedWater> waters = WaterBodyIO.load();
|
||||
List<PlacedModel> models = PlacedModelIO.load();
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
|
||||
|
||||
int[] slotColors = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildOptions());
|
||||
int[] slotColors = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||
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(() -> {
|
||||
cachedAreas = new ArrayList<>(areas);
|
||||
cachedLocs = new ArrayList<>(locs);
|
||||
cachedWaters = new ArrayList<>(waters);
|
||||
seaMask = mask;
|
||||
seaCoastSegs = segs;
|
||||
mapBuffered = bi;
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
fitToView();
|
||||
@@ -227,17 +268,21 @@ public class WorldMapView extends VBox {
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
// Bereits geladene Daten nochmal laden, damit Layer-Wechsel korrekt
|
||||
MapData mapData = MapIO.load();
|
||||
List<PlacedArea> areas = AreaIO.load();
|
||||
List<PlacedLocationZone> zones = LocationZoneIO.load();
|
||||
List<Location> locs = LocationIO.load();
|
||||
List<PlacedWater> waters = WaterBodyIO.load();
|
||||
List<PlacedModel> models = PlacedModelIO.load();
|
||||
int[] slotColors2 = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors2);
|
||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildOptions());
|
||||
int[] slotColors = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||
BufferedImage bi = WorldMapRenderer.render(input, RENDER_SIZE, buildBackgroundOptions());
|
||||
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE);
|
||||
Platform.runLater(() -> {
|
||||
cachedAreas = new ArrayList<>(areas);
|
||||
cachedLocs = new ArrayList<>(locs);
|
||||
cachedWaters = new ArrayList<>(waters);
|
||||
seaMask = mask;
|
||||
mapBuffered = bi;
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
redraw();
|
||||
@@ -261,7 +306,7 @@ public class WorldMapView extends VBox {
|
||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||
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);
|
||||
|
||||
if (mapFxImage == null) {
|
||||
@@ -270,20 +315,364 @@ public class WorldMapView extends VBox {
|
||||
return;
|
||||
}
|
||||
|
||||
double imgW = mapFxImage.getWidth() * scale;
|
||||
double imgH = mapFxImage.getHeight() * scale;
|
||||
gc.drawImage(mapFxImage, panX, panY, imgW, imgH);
|
||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||
|
||||
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() {
|
||||
if (canvas.getWidth() <= 0 || mapFxImage == null) return;
|
||||
double sw = canvas.getWidth() / mapFxImage.getWidth();
|
||||
double sh = canvas.getHeight() / mapFxImage.getHeight();
|
||||
scale = Math.min(sw, sh);
|
||||
scale = Math.min(canvas.getWidth() / mapFxImage.getWidth(), canvas.getHeight() / mapFxImage.getHeight());
|
||||
panX = (canvas.getWidth() - mapFxImage.getWidth() * 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() {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Karte als PNG exportieren");
|
||||
@@ -303,9 +692,9 @@ public class WorldMapView extends VBox {
|
||||
List<Location> locs = LocationIO.load();
|
||||
List<PlacedWater> waters = WaterBodyIO.load();
|
||||
List<PlacedModel> models = PlacedModelIO.load();
|
||||
int[] slotColors3 = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors3);
|
||||
BufferedImage bi = WorldMapRenderer.render(input, 4096, buildOptions());
|
||||
int[] slotColors = computeSlotColors(mapData);
|
||||
RenderInput input = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||
BufferedImage bi = WorldMapRenderer.render(input, 4096, buildExportOptions());
|
||||
ImageIO.write(bi, "PNG", file);
|
||||
Platform.runLater(() -> {
|
||||
progress.setVisible(false);
|
||||
@@ -322,11 +711,27 @@ public class WorldMapView extends VBox {
|
||||
t.start();
|
||||
}
|
||||
|
||||
private RenderOptions buildOptions() {
|
||||
/** Hintergrund-PNG: ohne Areas und Locations (die werden live auf Canvas gezeichnet). */
|
||||
private RenderOptions buildBackgroundOptions() {
|
||||
return new RenderOptions(
|
||||
layerTerrain.isSelected(),
|
||||
layerTerrain.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(),
|
||||
layerZones.isSelected(),
|
||||
layerLocations.isSelected(),
|
||||
|
||||
Reference in New Issue
Block a user