Compare commits

..

2 Commits

Author SHA1 Message Date
08c76a798d Malen und Editieren von Zonen erweitert 2026-09-13 21:33:30 +02:00
6cf7234ab1 Wege System erweitert 2026-09-13 20:39:07 +02:00
10 changed files with 1316 additions and 103 deletions

View File

@@ -931,16 +931,22 @@ public class EditorApp extends Application {
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT) || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
input.deleteSelectedRequested = true; input.deleteSelectedRequested = true;
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) { else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
input.deleteSoundAreaRequested = true; scheduleAutoSave(); if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
else { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
} else if (input.activeLayer == SharedInput.LAYER_AREAS) { } else if (input.activeLayer == SharedInput.LAYER_AREAS) {
input.deleteAreaRequested = true; scheduleAutoSave(); if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
else { input.deleteAreaRequested = true; scheduleAutoSave(); }
} else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES) { } else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES) {
input.deleteLocationZoneRequested = true; scheduleAutoSave(); if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
else { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }
} else if (input.activeLayer == SharedInput.LAYER_ZONEN) { } else if (input.activeLayer == SharedInput.LAYER_ZONEN) {
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
else {
if ("sound".equals(input.zonenSelectedKind)) { input.deleteSoundAreaRequested = true; scheduleAutoSave(); } if ("sound".equals(input.zonenSelectedKind)) { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
else if ("area".equals(input.zonenSelectedKind)) { input.deleteAreaRequested = true; scheduleAutoSave(); } else if ("area".equals(input.zonenSelectedKind)) { input.deleteAreaRequested = true; scheduleAutoSave(); }
else if ("location".equals(input.zonenSelectedKind)) { input.deleteLocationZoneRequested = true; scheduleAutoSave(); } else if ("location".equals(input.zonenSelectedKind)) { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }
} }
}
else if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) else if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK)
input.deletePathNodeRequested = true; input.deletePathNodeRequested = true;
else if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) else if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF)

View File

@@ -548,6 +548,14 @@ public class SharedInput {
/** JavaFX → JME: Selektierte Wasserfläche löschen. */ /** JavaFX → JME: Selektierte Wasserfläche löschen. */
public volatile boolean deleteWaterRequested = false; public volatile boolean deleteWaterRequested = false;
// ── Zonen-Vertex-Drag (für AreaState / SoundAreaState / LocationZoneState) ─
public record ZoneDrag(float screenX, float screenY, int phase) {
public static final int START = 0, MOVE = 1, END = 2;
}
public final ConcurrentLinkedQueue<ZoneDrag> soundAreaDragQueue = new ConcurrentLinkedQueue<>();
public final ConcurrentLinkedQueue<ZoneDrag> areaDragQueue = new ConcurrentLinkedQueue<>();
public final ConcurrentLinkedQueue<ZoneDrag> locationZoneDragQueue = new ConcurrentLinkedQueue<>();
// ── Sound-Bereich-Werkzeug ──────────────────────────────────────────────── // ── Sound-Bereich-Werkzeug ────────────────────────────────────────────────
public record SoundAreaClick(float screenX, float screenY, boolean rightButton) {} public record SoundAreaClick(float screenX, float screenY, boolean rightButton) {}
public final ConcurrentLinkedQueue<SoundAreaClick> soundAreaClickQueue = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<SoundAreaClick> soundAreaClickQueue = new ConcurrentLinkedQueue<>();
@@ -595,6 +603,12 @@ public class SharedInput {
/** JavaFX → JME: Selektierte Location-Zone löschen. */ /** JavaFX → JME: Selektierte Location-Zone löschen. */
public volatile boolean deleteLocationZoneRequested = false; public volatile boolean deleteLocationZoneRequested = false;
/** JavaFX → JME: Selektierten Zonen-Vertex löschen (Priorität über Zonen-Löschung). */
public volatile boolean deleteZoneVertexRequested = false;
/** JME → JavaFX: true, wenn mindestens eine Zonen-State gerade einen Vertex selektiert hat. */
public volatile boolean zoneVertexIsSelected = false;
/** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */ /** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */
public volatile boolean cancelZoneDrawing = false; public volatile boolean cancelZoneDrawing = false;
@@ -1048,6 +1062,16 @@ public class SharedInput {
public final ConcurrentLinkedQueue<PathNetworkClick> pathNetworkClickQueue = public final ConcurrentLinkedQueue<PathNetworkClick> pathNetworkClickQueue =
new ConcurrentLinkedQueue<>(); new ConcurrentLinkedQueue<>();
/**
* JFX → JME: Drag-Ereignis im Wegnetz-Modus (Knoten verschieben).
* phase: START=0, MOVE=1, END=2.
*/
public record PathNetworkDrag(float screenX, float screenY, int phase) {
public static final int START = 0, MOVE = 1, END = 2;
}
public final ConcurrentLinkedQueue<PathNetworkDrag> pathNetworkDragQueue =
new ConcurrentLinkedQueue<>();
/** JME → JFX: Wegnetz wurde verändert, UI-Refresh nötig. */ /** JME → JFX: Wegnetz wurde verändert, UI-Refresh nötig. */
public volatile boolean pathNetworkChanged = false; public volatile boolean pathNetworkChanged = false;

View File

@@ -46,6 +46,14 @@ public class AreaState extends BaseAppState {
private Geometry inProgGeo = null; private Geometry inProgGeo = null;
private Geometry lastPointMarker = null; private Geometry lastPointMarker = null;
private int draggedZoneIdx = -1;
private int draggedVertexIdx = -1;
private float draggedOrigX, draggedOrigZ;
private int selectedVertexZoneIdx = -1;
private int selectedVertexIdx = -1;
private Geometry selectedVertexMarker = null;
private List<PlacedArea> pendingAreas = null; private List<PlacedArea> pendingAreas = null;
public AreaState(SharedInput input) { public AreaState(SharedInput input) {
@@ -91,6 +99,23 @@ public class AreaState extends BaseAppState {
handleClick(click); handleClick(click);
} }
if (input.deleteZoneVertexRequested && selectedVertexZoneIdx >= 0) {
input.deleteZoneVertexRequested = false;
deleteSelectedVertex();
}
SharedInput.ZoneDrag pendingMove = null;
SharedInput.ZoneDrag drag;
while ((drag = input.areaDragQueue.poll()) != null) {
if (drag.phase() == SharedInput.ZoneDrag.MOVE) {
pendingMove = drag;
} else {
if (pendingMove != null) { handleZoneDrag(pendingMove); pendingMove = null; }
handleZoneDrag(drag);
}
}
if (pendingMove != null) { handleZoneDrag(pendingMove); }
PlacedArea pending = input.pendingArea.getAndSet(null); PlacedArea pending = input.pendingArea.getAndSet(null);
if (pending != null && selectedIdx >= 0) { if (pending != null && selectedIdx >= 0) {
applyProperty(selectedIdx, pending); applyProperty(selectedIdx, pending);
@@ -151,14 +176,22 @@ public class AreaState extends BaseAppState {
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class); LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
if ((sas0 != null && sas0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return; if ((sas0 != null && sas0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
// pass 1: clearly inside // pass 0: near vertex → select vertex
int[] vertHit = findNearestVertex(hitX, hitZ, 4f);
if (vertHit != null) {
selectVertex(vertHit[0], vertHit[1]);
return;
}
deselectVertex();
// pass 1: clearly inside → select
for (int i = 0; i < areas.size(); i++) { for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i); PlacedArea a = areas.get(i);
if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) { if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
selectArea(i); return; selectArea(i); return;
} }
} }
// pass 2: within 50 cm of any edge // pass 2: within 50 cm of any edge → select
for (int i = 0; i < areas.size(); i++) { for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i); PlacedArea a = areas.get(i);
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) { if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
@@ -186,24 +219,311 @@ public class AreaState extends BaseAppState {
} }
} }
private void selectVertex(int zoneIdx, int vertexIdx) {
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.deselectVertex();
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.deselectVertex();
deselectVertex();
selectedVertexZoneIdx = zoneIdx;
selectedVertexIdx = vertexIdx;
input.zoneVertexIsSelected = true;
PlacedArea a = areas.get(zoneIdx);
float vx = a.pointsX()[vertexIdx];
float vz = a.pointsZ()[vertexIdx];
float vy = getHeightAt(vx, vz) + LINE_OFFSET_Y + 0.1f;
float s = 2f;
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
buf.put(vx - s).put(vy).put(vz - s); buf.put(vx + s).put(vy).put(vz + s);
buf.put(vx - s).put(vy).put(vz + s); buf.put(vx + s).put(vy).put(vz - s);
buf.flip();
Mesh mesh = new Mesh();
mesh.setMode(Mesh.Mode.Lines);
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
mesh.updateBound();
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(1f, 1f, 0.2f, 1f));
mat.getAdditionalRenderState().setLineWidth(4f);
selectedVertexMarker = new Geometry("area_sel_vertex", mesh);
selectedVertexMarker.setMaterial(mat);
rootNode.attachChild(selectedVertexMarker);
}
void deselectVertex() {
if (selectedVertexZoneIdx >= 0) input.zoneVertexIsSelected = false;
selectedVertexZoneIdx = -1;
selectedVertexIdx = -1;
if (selectedVertexMarker != null) { rootNode.detachChild(selectedVertexMarker); selectedVertexMarker = null; }
}
private void deleteSelectedVertex() {
if (selectedVertexZoneIdx < 0) return;
int zIdx = selectedVertexZoneIdx;
int vIdx = selectedVertexIdx;
PlacedArea a = areas.get(zIdx);
int n = a.pointsX().length;
float removedX = a.pointsX()[vIdx];
float removedZ = a.pointsZ()[vIdx];
deselectVertex();
if (n <= 3) {
removeArea(zIdx);
} else {
float[] nx = new float[n - 1];
float[] nz = new float[n - 1];
for (int i = 0, j = 0; i < n; i++) {
if (i == vIdx) continue;
nx[j] = a.pointsX()[i]; nz[j] = a.pointsZ()[i]; j++;
}
areas.set(zIdx, new PlacedArea(nx, nz, a.areaId(), a.triggers(), a.labelX(), a.labelZ()));
rebuildAreaGeo(zIdx);
}
deleteMatchingVertex(removedX, removedZ);
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.deleteMatchingVertex(removedX, removedZ);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.deleteMatchingVertex(removedX, removedZ);
}
public void deleteMatchingVertex(float wx, float wz) {
float eps2 = 0.01f;
for (int i = areas.size() - 1; i >= 0; i--) {
PlacedArea a = areas.get(i);
int n = a.pointsX().length;
for (int j = 0; j < n; j++) {
float dx = a.pointsX()[j] - wx;
float dz = a.pointsZ()[j] - wz;
if (dx * dx + dz * dz < eps2) {
if (n <= 3) { removeArea(i); }
else {
float[] nx = new float[n - 1];
float[] nz = new float[n - 1];
for (int k = 0, m = 0; k < n; k++) {
if (k == j) continue;
nx[m] = a.pointsX()[k]; nz[m] = a.pointsZ()[k]; m++;
}
areas.set(i, new PlacedArea(nx, nz, a.areaId(), a.triggers(), a.labelX(), a.labelZ()));
rebuildAreaGeo(i);
}
break;
}
}
}
}
private float[] snapVertex(float x, float z) { private float[] snapVertex(float x, float z) {
float bestDist2 = SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST; float[] snap = new float[]{x, z, SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST};
float bx = x, bz = z; updateSnap(x, z, snap);
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.updateSnap(x, z, snap);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.updateSnap(x, z, snap);
for (int i = 0; i < currX.size(); i++) {
float dx = x - currX.get(i);
float dz = z - currZ.get(i);
float d2 = dx * dx + dz * dz;
if (d2 < snap[2]) { snap[0] = currX.get(i); snap[1] = currZ.get(i); snap[2] = d2; }
}
return new float[]{snap[0], snap[1]};
}
public void updateSnap(float x, float z, float[] snap) {
for (PlacedArea a : areas) { for (PlacedArea a : areas) {
for (int i = 0; i < a.pointsX().length; i++) { for (int i = 0; i < a.pointsX().length; i++) {
float dx = x - a.pointsX()[i]; float dx = x - a.pointsX()[i];
float dz = z - a.pointsZ()[i]; float dz = z - a.pointsZ()[i];
float d2 = dx * dx + dz * dz; float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = a.pointsX()[i]; bz = a.pointsZ()[i]; } if (d2 < snap[2]) { snap[0] = a.pointsX()[i]; snap[1] = a.pointsZ()[i]; snap[2] = d2; }
} }
} }
for (int i = 0; i < currX.size(); i++) { }
float dx = x - currX.get(i);
float dz = z - currZ.get(i); private int[] findNearestVertex(float wx, float wz, float threshold) {
float bestDist2 = threshold * threshold;
int bestZone = -1, bestVert = -1;
for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i);
for (int j = 0; j < a.pointsX().length; j++) {
float dx = wx - a.pointsX()[j];
float dz = wz - a.pointsZ()[j];
float d2 = dx * dx + dz * dz; float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); } if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
}
}
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
}
private int[] findNearestEdge(float wx, float wz, float threshold) {
float bestDist = threshold;
int bestZone = -1, bestEdge = -1;
for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i);
int n = a.pointsX().length;
for (int j = 0; j < n; j++) {
int k = (j + 1) % n;
float dist = distPointToSegXZ(wx, wz,
a.pointsX()[j], a.pointsZ()[j], a.pointsX()[k], a.pointsZ()[k]);
if (dist < bestDist) { bestDist = dist; bestZone = i; bestEdge = j; }
}
}
return (bestZone >= 0) ? new int[]{bestZone, bestEdge} : null;
}
private static float distPointToSegXZ(float px, float pz,
float ax, float az, float bx, float bz) {
float abx = bx - ax, abz = bz - az;
float len2 = abx * abx + abz * abz;
if (len2 < 1e-6f) {
float dx = px - ax, dz = pz - az;
return (float) Math.sqrt(dx * dx + dz * dz);
}
float t = ((px - ax) * abx + (pz - az) * abz) / len2;
t = Math.max(0f, Math.min(1f, t));
float cx = ax + t * abx, cz = az + t * abz;
float dx = px - cx, dz = pz - cz;
return (float) Math.sqrt(dx * dx + dz * dz);
}
private void moveAreaVertex(int zoneIdx, int vertexIdx, float newX, float newZ) {
PlacedArea a = areas.get(zoneIdx);
a.pointsX()[vertexIdx] = newX;
a.pointsZ()[vertexIdx] = newZ;
rebuildAreaGeo(zoneIdx);
}
private void rebuildAreaGeo(int zoneIdx) {
rootNode.detachChild(areaGeos.get(zoneIdx));
PlacedArea a = areas.get(zoneIdx);
List<Float> xs = toList(a.pointsX());
List<Float> zs = toList(a.pointsZ());
ColorRGBA color = (zoneIdx == selectedIdx) ? COLOR_SELECTED : COLOR_NORMAL;
float lineW = (zoneIdx == selectedIdx) ? 8f : 4f;
Geometry geo = buildLineGeo("area_" + zoneIdx, xs, zs, color, Mesh.Mode.LineLoop);
geo.getMaterial().getAdditionalRenderState().setLineWidth(lineW);
rootNode.attachChild(geo);
areaGeos.set(zoneIdx, geo);
}
private void insertAreaVertexOnEdge(int zoneIdx, int edgeStartIdx, float newX, float newZ) {
PlacedArea a = areas.get(zoneIdx);
int n = a.pointsX().length;
int ins = edgeStartIdx + 1;
float[] nx = new float[n + 1];
float[] nz = new float[n + 1];
System.arraycopy(a.pointsX(), 0, nx, 0, ins);
nx[ins] = newX;
System.arraycopy(a.pointsX(), ins, nx, ins + 1, n - ins);
System.arraycopy(a.pointsZ(), 0, nz, 0, ins);
nz[ins] = newZ;
System.arraycopy(a.pointsZ(), ins, nz, ins + 1, n - ins);
areas.set(zoneIdx, new PlacedArea(nx, nz, a.areaId(), a.triggers(), a.labelX(), a.labelZ()));
rebuildAreaGeo(zoneIdx);
}
private void handleZoneDrag(SharedInput.ZoneDrag drag) {
float jmeX = drag.screenX() * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
Vector3f pt = raycastAll(ray);
switch (drag.phase()) {
case SharedInput.ZoneDrag.START -> {
draggedZoneIdx = -1;
if (placing || pt == null) break;
int[] found = findNearestVertex(pt.x, pt.z, SoundAreaState.SNAP_DIST);
if (found != null) {
draggedZoneIdx = found[0];
draggedVertexIdx = found[1];
draggedOrigX = areas.get(draggedZoneIdx).pointsX()[draggedVertexIdx];
draggedOrigZ = areas.get(draggedZoneIdx).pointsZ()[draggedVertexIdx];
} else {
int[] edgeHit = findNearestEdge(pt.x, pt.z, 3f);
if (edgeHit != null) {
insertEdgeVertexInAllZones(pt.x, pt.z, 3f);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.insertSharedEdgeVertex(pt.x, pt.z);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.insertSharedEdgeVertex(pt.x, pt.z);
}
draggedZoneIdx = edgeHit[0];
draggedVertexIdx = edgeHit[1] + 1;
draggedOrigX = pt.x;
draggedOrigZ = pt.z;
}
}
}
case SharedInput.ZoneDrag.MOVE -> {
if (draggedZoneIdx < 0 || pt == null) break;
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
}
draggedOrigX = pt.x;
draggedOrigZ = pt.z;
}
case SharedInput.ZoneDrag.END -> {
if (draggedZoneIdx >= 0 && pt != null) {
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
}
}
draggedZoneIdx = -1;
}
}
}
public void insertSharedEdgeVertex(float wx, float wz) {
insertEdgeVertexInAllZones(wx, wz, 2f);
}
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
float eps2 = 0.05f * 0.05f;
for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i);
float[] xs = a.pointsX(), zs = a.pointsZ();
int n = xs.length;
boolean hasVertex = false;
for (int v = 0; v < n; v++) {
float dx = xs[v] - wx, dz = zs[v] - wz;
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
}
if (hasVertex) continue;
float bestDist = threshold;
int bestEdge = -1;
for (int j = 0; j < n; j++) {
int k = (j + 1) % n;
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
}
if (bestEdge >= 0) insertAreaVertexOnEdge(i, bestEdge, wx, wz);
}
}
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
float eps2 = 0.01f;
for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i);
boolean changed = false;
for (int j = 0; j < a.pointsX().length; j++) {
float dx = a.pointsX()[j] - origX;
float dz = a.pointsZ()[j] - origZ;
if (dx * dx + dz * dz < eps2) {
a.pointsX()[j] = newX;
a.pointsZ()[j] = newZ;
changed = true;
}
}
if (changed) rebuildAreaGeo(i);
} }
return new float[]{bx, bz};
} }
private void closePoly() { private void closePoly() {
@@ -231,6 +551,7 @@ public class AreaState extends BaseAppState {
currZ.clear(); currZ.clear();
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; } if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; } if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
deselectVertex();
} }
private void undoLastPoint() { private void undoLastPoint() {

View File

@@ -44,6 +44,14 @@ public class LocationZoneState extends BaseAppState {
private Geometry inProgGeo = null; private Geometry inProgGeo = null;
private Geometry lastPointMarker = null; private Geometry lastPointMarker = null;
private int draggedZoneIdx = -1;
private int draggedVertexIdx = -1;
private float draggedOrigX, draggedOrigZ;
private int selectedVertexZoneIdx = -1;
private int selectedVertexIdx = -1;
private Geometry selectedVertexMarker = null;
private List<PlacedLocationZone> pendingZones = null; private List<PlacedLocationZone> pendingZones = null;
public LocationZoneState(SharedInput input) { public LocationZoneState(SharedInput input) {
@@ -89,6 +97,18 @@ public class LocationZoneState extends BaseAppState {
handleClick(click); handleClick(click);
} }
SharedInput.ZoneDrag pendingMove = null;
SharedInput.ZoneDrag drag;
while ((drag = input.locationZoneDragQueue.poll()) != null) {
if (drag.phase() == SharedInput.ZoneDrag.MOVE) {
pendingMove = drag;
} else {
if (pendingMove != null) { handleZoneDrag(pendingMove); pendingMove = null; }
handleZoneDrag(drag);
}
}
if (pendingMove != null) { handleZoneDrag(pendingMove); }
PlacedLocationZone pending = input.pendingLocationZone.getAndSet(null); PlacedLocationZone pending = input.pendingLocationZone.getAndSet(null);
if (pending != null && selectedIdx >= 0) { if (pending != null && selectedIdx >= 0) {
applyProperty(selectedIdx, pending); applyProperty(selectedIdx, pending);
@@ -99,6 +119,12 @@ public class LocationZoneState extends BaseAppState {
cancelPoly(); cancelPoly();
} }
if (input.deleteZoneVertexRequested && selectedVertexZoneIdx >= 0) {
input.deleteZoneVertexRequested = false;
input.deleteLocationZoneRequested = false;
deleteSelectedVertex();
}
if (input.deleteLocationZoneRequested) { if (input.deleteLocationZoneRequested) {
input.deleteLocationZoneRequested = false; input.deleteLocationZoneRequested = false;
if (selectedIdx >= 0) removeZone(selectedIdx); if (selectedIdx >= 0) removeZone(selectedIdx);
@@ -149,6 +175,11 @@ public class LocationZoneState extends BaseAppState {
SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class); SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class);
if ((as0 != null && as0.isPlacing()) || (sas0 != null && sas0.isPlacing())) return; if ((as0 != null && as0.isPlacing()) || (sas0 != null && sas0.isPlacing())) return;
// pass 0: near vertex → select vertex
int[] vertHit = findNearestLocVertex(hitX, hitZ, 4f);
if (vertHit != null) { selectVertex(vertHit[0], vertHit[1]); return; }
deselectVertex();
// pass 1: clearly inside // pass 1: clearly inside
for (int i = 0; i < zones.size(); i++) { for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i); PlacedLocationZone z = zones.get(i);
@@ -156,7 +187,7 @@ public class LocationZoneState extends BaseAppState {
selectZone(i); return; selectZone(i); return;
} }
} }
// pass 2: within 50 cm of any edge // pass 2: within 50 cm of any edge → select
for (int i = 0; i < zones.size(); i++) { for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i); PlacedLocationZone z = zones.get(i);
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, z.pointsX(), z.pointsZ(), 0.5f)) { if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, z.pointsX(), z.pointsZ(), 0.5f)) {
@@ -184,24 +215,311 @@ public class LocationZoneState extends BaseAppState {
} }
} }
private void selectVertex(int zoneIdx, int vertexIdx) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.deselectVertex();
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.deselectVertex();
deselectVertex();
selectedVertexZoneIdx = zoneIdx;
selectedVertexIdx = vertexIdx;
input.zoneVertexIsSelected = true;
PlacedLocationZone z = zones.get(zoneIdx);
float vx = z.pointsX()[vertexIdx];
float vz = z.pointsZ()[vertexIdx];
float vy = getHeightAt(vx, vz) + LINE_OFFSET_Y + 0.1f;
float s = 2f;
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
buf.put(vx - s).put(vy).put(vz - s); buf.put(vx + s).put(vy).put(vz + s);
buf.put(vx - s).put(vy).put(vz + s); buf.put(vx + s).put(vy).put(vz - s);
buf.flip();
Mesh mesh = new Mesh();
mesh.setMode(Mesh.Mode.Lines);
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
mesh.updateBound();
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(1f, 1f, 0.2f, 1f));
mat.getAdditionalRenderState().setLineWidth(4f);
selectedVertexMarker = new Geometry("loczone_sel_vertex", mesh);
selectedVertexMarker.setMaterial(mat);
rootNode.attachChild(selectedVertexMarker);
}
void deselectVertex() {
if (selectedVertexZoneIdx >= 0) input.zoneVertexIsSelected = false;
selectedVertexZoneIdx = -1;
selectedVertexIdx = -1;
if (selectedVertexMarker != null) { rootNode.detachChild(selectedVertexMarker); selectedVertexMarker = null; }
}
private void deleteSelectedVertex() {
if (selectedVertexZoneIdx < 0) return;
int zIdx = selectedVertexZoneIdx;
int vIdx = selectedVertexIdx;
PlacedLocationZone z = zones.get(zIdx);
int n = z.pointsX().length;
float removedX = z.pointsX()[vIdx];
float removedZ = z.pointsZ()[vIdx];
deselectVertex();
if (n <= 3) {
removeZone(zIdx);
} else {
float[] nx = new float[n - 1];
float[] nz = new float[n - 1];
for (int i = 0, j = 0; i < n; i++) {
if (i == vIdx) continue;
nx[j] = z.pointsX()[i]; nz[j] = z.pointsZ()[i]; j++;
}
zones.set(zIdx, new PlacedLocationZone(nx, nz, z.nameId(), z.triggers()));
rebuildLocZoneGeo(zIdx);
}
deleteMatchingVertex(removedX, removedZ);
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.deleteMatchingVertex(removedX, removedZ);
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.deleteMatchingVertex(removedX, removedZ);
}
public void deleteMatchingVertex(float wx, float wz) {
float eps2 = 0.01f;
for (int i = zones.size() - 1; i >= 0; i--) {
PlacedLocationZone z = zones.get(i);
int n = z.pointsX().length;
for (int j = 0; j < n; j++) {
float dx = z.pointsX()[j] - wx;
float dz = z.pointsZ()[j] - wz;
if (dx * dx + dz * dz < eps2) {
if (n <= 3) { removeZone(i); }
else {
float[] nx = new float[n - 1];
float[] nz = new float[n - 1];
for (int k = 0, m = 0; k < n; k++) {
if (k == j) continue;
nx[m] = z.pointsX()[k]; nz[m] = z.pointsZ()[k]; m++;
}
zones.set(i, new PlacedLocationZone(nx, nz, z.nameId(), z.triggers()));
rebuildLocZoneGeo(i);
}
break;
}
}
}
}
private float[] snapVertex(float x, float z) { private float[] snapVertex(float x, float z) {
float bestDist2 = SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST; float[] snap = new float[]{x, z, SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST};
float bx = x, bz = z; updateSnap(x, z, snap);
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.updateSnap(x, z, snap);
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.updateSnap(x, z, snap);
for (int i = 0; i < currX.size(); i++) {
float dx = x - currX.get(i);
float dz = z - currZ.get(i);
float d2 = dx * dx + dz * dz;
if (d2 < snap[2]) { snap[0] = currX.get(i); snap[1] = currZ.get(i); snap[2] = d2; }
}
return new float[]{snap[0], snap[1]};
}
public void updateSnap(float x, float z, float[] snap) {
for (PlacedLocationZone zone : zones) { for (PlacedLocationZone zone : zones) {
for (int i = 0; i < zone.pointsX().length; i++) { for (int i = 0; i < zone.pointsX().length; i++) {
float dx = x - zone.pointsX()[i]; float dx = x - zone.pointsX()[i];
float dz = z - zone.pointsZ()[i]; float dz = z - zone.pointsZ()[i];
float d2 = dx * dx + dz * dz; float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = zone.pointsX()[i]; bz = zone.pointsZ()[i]; } if (d2 < snap[2]) { snap[0] = zone.pointsX()[i]; snap[1] = zone.pointsZ()[i]; snap[2] = d2; }
} }
} }
for (int i = 0; i < currX.size(); i++) { }
float dx = x - currX.get(i);
float dz = z - currZ.get(i); private int[] findNearestEdge(float wx, float wz, float threshold) {
float bestDist = threshold;
int bestZone = -1, bestEdge = -1;
for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i);
int n = z.pointsX().length;
for (int j = 0; j < n; j++) {
int k = (j + 1) % n;
float dist = distPointToSegXZ(wx, wz,
z.pointsX()[j], z.pointsZ()[j], z.pointsX()[k], z.pointsZ()[k]);
if (dist < bestDist) { bestDist = dist; bestZone = i; bestEdge = j; }
}
}
return (bestZone >= 0) ? new int[]{bestZone, bestEdge} : null;
}
private static float distPointToSegXZ(float px, float pz,
float ax, float az, float bx, float bz) {
float abx = bx - ax, abz = bz - az;
float len2 = abx * abx + abz * abz;
if (len2 < 1e-6f) {
float dx = px - ax, dz = pz - az;
return (float) Math.sqrt(dx * dx + dz * dz);
}
float t = ((px - ax) * abx + (pz - az) * abz) / len2;
t = Math.max(0f, Math.min(1f, t));
float cx = ax + t * abx, cz = az + t * abz;
float dx = px - cx, dz = pz - cz;
return (float) Math.sqrt(dx * dx + dz * dz);
}
private void moveLocZoneVertex(int zoneIdx, int vertexIdx, float newX, float newZ) {
PlacedLocationZone z = zones.get(zoneIdx);
z.pointsX()[vertexIdx] = newX;
z.pointsZ()[vertexIdx] = newZ;
rebuildLocZoneGeo(zoneIdx);
}
private void rebuildLocZoneGeo(int zoneIdx) {
rootNode.detachChild(zoneGeos.get(zoneIdx));
PlacedLocationZone z = zones.get(zoneIdx);
List<Float> xs = toList(z.pointsX());
List<Float> zs = toList(z.pointsZ());
ColorRGBA color = (zoneIdx == selectedIdx) ? COLOR_SELECTED : COLOR_NORMAL;
float lineW = (zoneIdx == selectedIdx) ? 10f : 6f;
Geometry geo = buildLineGeo("loczone_" + zoneIdx, xs, zs, color, Mesh.Mode.LineLoop);
geo.getMaterial().getAdditionalRenderState().setLineWidth(lineW);
rootNode.attachChild(geo);
zoneGeos.set(zoneIdx, geo);
}
private void insertLocZoneVertexOnEdge(int zoneIdx, int edgeStartIdx, float newX, float newZ) {
PlacedLocationZone z = zones.get(zoneIdx);
int n = z.pointsX().length;
int ins = edgeStartIdx + 1;
float[] nx = new float[n + 1];
float[] nz = new float[n + 1];
System.arraycopy(z.pointsX(), 0, nx, 0, ins);
nx[ins] = newX;
System.arraycopy(z.pointsX(), ins, nx, ins + 1, n - ins);
System.arraycopy(z.pointsZ(), 0, nz, 0, ins);
nz[ins] = newZ;
System.arraycopy(z.pointsZ(), ins, nz, ins + 1, n - ins);
zones.set(zoneIdx, new PlacedLocationZone(nx, nz, z.nameId(), z.triggers()));
rebuildLocZoneGeo(zoneIdx);
}
private void handleZoneDrag(SharedInput.ZoneDrag drag) {
float jmeX = drag.screenX() * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
Vector3f pt = raycastAll(ray);
switch (drag.phase()) {
case SharedInput.ZoneDrag.START -> {
draggedZoneIdx = -1;
if (placing || pt == null) break;
int[] found = findNearestLocVertex(pt.x, pt.z, SoundAreaState.SNAP_DIST);
if (found != null) {
draggedZoneIdx = found[0];
draggedVertexIdx = found[1];
draggedOrigX = zones.get(draggedZoneIdx).pointsX()[draggedVertexIdx];
draggedOrigZ = zones.get(draggedZoneIdx).pointsZ()[draggedVertexIdx];
} else {
int[] edgeHit = findNearestEdge(pt.x, pt.z, 3f);
if (edgeHit != null) {
insertEdgeVertexInAllZones(pt.x, pt.z, 3f);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.insertSharedEdgeVertex(pt.x, pt.z);
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.insertSharedEdgeVertex(pt.x, pt.z);
}
draggedZoneIdx = edgeHit[0];
draggedVertexIdx = edgeHit[1] + 1;
draggedOrigX = pt.x;
draggedOrigZ = pt.z;
}
}
}
case SharedInput.ZoneDrag.MOVE -> {
if (draggedZoneIdx < 0 || pt == null) break;
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
}
draggedOrigX = pt.x;
draggedOrigZ = pt.z;
}
case SharedInput.ZoneDrag.END -> {
if (draggedZoneIdx >= 0 && pt != null) {
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
}
}
draggedZoneIdx = -1;
}
}
}
private int[] findNearestLocVertex(float wx, float wz, float threshold) {
float bestDist2 = threshold * threshold;
int bestZone = -1, bestVert = -1;
for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i);
for (int j = 0; j < z.pointsX().length; j++) {
float dx = wx - z.pointsX()[j];
float dz = wz - z.pointsZ()[j];
float d2 = dx * dx + dz * dz; float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); } if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
}
}
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
}
public void insertSharedEdgeVertex(float wx, float wz) {
insertEdgeVertexInAllZones(wx, wz, 2f);
}
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
float eps2 = 0.05f * 0.05f;
for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i);
float[] xs = z.pointsX(), zs = z.pointsZ();
int n = xs.length;
boolean hasVertex = false;
for (int v = 0; v < n; v++) {
float dx = xs[v] - wx, dz = zs[v] - wz;
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
}
if (hasVertex) continue;
float bestDist = threshold;
int bestEdge = -1;
for (int j = 0; j < n; j++) {
int k = (j + 1) % n;
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
}
if (bestEdge >= 0) insertLocZoneVertexOnEdge(i, bestEdge, wx, wz);
}
}
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
float eps2 = 0.01f;
for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i);
boolean changed = false;
for (int j = 0; j < z.pointsX().length; j++) {
float dx = z.pointsX()[j] - origX;
float dz = z.pointsZ()[j] - origZ;
if (dx * dx + dz * dz < eps2) {
z.pointsX()[j] = newX;
z.pointsZ()[j] = newZ;
changed = true;
}
}
if (changed) rebuildLocZoneGeo(i);
} }
return new float[]{bx, bz};
} }
private void closePoly() { private void closePoly() {
@@ -221,6 +539,7 @@ public class LocationZoneState extends BaseAppState {
currZ.clear(); currZ.clear();
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; } if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; } if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
deselectVertex();
} }
private void undoLastPoint() { private void undoLastPoint() {

View File

@@ -34,25 +34,22 @@ import java.util.List;
* *
* <p>Interaktionsmodell: * <p>Interaktionsmodell:
* <ul> * <ul>
* <li>Linksklick auf leeres Terrain → neuen Knoten platzieren</li> * <li>LMK auf leeres Terrain → neuen Knoten platzieren</li>
* <li>Linksklick auf Knoten → Knoten auswählen; ist bereits einer gewählt, * <li>LMK auf Kante → Kante splitten, neuen Knoten einfügen</li>
* wird eine Kante zwischen beiden erzeugt</li> * <li>LMK auf Knoten → auswählen; ist bereits einer gewählt, Kante erzeugen</li>
* <li>Linksklick auf Terrain mit gewähltem Knoten → neuen Knoten platzieren * <li>LMK Drag auf Knoten → Knoten auf neuer Terrain-Position ablegen</li>
* UND mit dem gewählten Knoten verbinden</li> * <li>RMK auf Knoten → Knoten und seine Kanten löschen</li>
* <li>Rechtsklick auf Knoten → Knoten und alle seine Kanten löschen</li> * <li>RMK auf Terrain → Auswahl aufheben</li>
* <li>Rechtsklick auf Terrain → Auswahl aufheben</li>
* </ul> * </ul>
*
* Änderungen werden sofort in die Datei geschrieben.
*/ */
public class PathNetworkEditorState extends BaseAppState { public class PathNetworkEditorState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(PathNetworkEditorState.class); private static final Logger log = LoggerFactory.getLogger(PathNetworkEditorState.class);
/** Radius in Welt-Einheiten, innerhalb dessen ein Klick einen Knoten trifft. */
private static final float NODE_HIT_RADIUS = 3f; private static final float NODE_HIT_RADIUS = 3f;
private static final float NODE_SPHERE_RADIUS = 0.6f; private static final float NODE_SPHERE_RADIUS = 0.6f;
private static final float EDGE_RADIUS = 0.15f; private static final float EDGE_RADIUS = 0.15f;
private static final float EDGE_HIT_THRESHOLD = 3f;
private final SharedInput input; private final SharedInput input;
private Camera cam; private Camera cam;
@@ -62,8 +59,8 @@ public class PathNetworkEditorState extends BaseAppState {
private final PathNetwork network = new PathNetwork(); private final PathNetwork network = new PathNetwork();
private PathNode selectedNode = null; private PathNode selectedNode = null;
private PathNode draggedNode = null;
// Szene-Graph-Knoten für Visualisierung
private final Node netRoot = new Node("pathnet_root"); private final Node netRoot = new Node("pathnet_root");
private final Node nodeRoot = new Node("pathnet_nodes"); private final Node nodeRoot = new Node("pathnet_nodes");
private final Node edgeRoot = new Node("pathnet_edges"); private final Node edgeRoot = new Node("pathnet_edges");
@@ -83,9 +80,9 @@ public class PathNetworkEditorState extends BaseAppState {
assets = app.getAssetManager(); assets = app.getAssetManager();
rootNode = app.getRootNode(); rootNode = app.getRootNode();
matDefault = mat(new ColorRGBA(1f, 0.85f, 0f, 1f)); // gelb matDefault = mat(new ColorRGBA(1f, 0.85f, 0f, 1f));
matSelected = mat(new ColorRGBA(1f, 0.4f, 0f, 1f)); // orange matSelected = mat(new ColorRGBA(1f, 0.4f, 0f, 1f));
matEdge = mat(new ColorRGBA(0.3f, 0.9f, 0.3f, 1f)); // grün matEdge = mat(new ColorRGBA(0.3f, 0.9f, 0.3f, 1f));
netRoot.attachChild(nodeRoot); netRoot.attachChild(nodeRoot);
netRoot.attachChild(edgeRoot); netRoot.attachChild(edgeRoot);
@@ -111,6 +108,7 @@ public class PathNetworkEditorState extends BaseAppState {
protected void onDisable() { protected void onDisable() {
netRoot.removeFromParent(); netRoot.removeFromParent();
selectedNode = null; selectedNode = null;
draggedNode = null;
} }
public void setTerrain(TerrainQuad t) { this.terrain = t; } public void setTerrain(TerrainQuad t) { this.terrain = t; }
@@ -123,21 +121,31 @@ public class PathNetworkEditorState extends BaseAppState {
while ((click = input.pathNetworkClickQueue.poll()) != null) { while ((click = input.pathNetworkClickQueue.poll()) != null) {
handleClick(click); handleClick(click);
} }
// MOVE-Events kollabieren: nur den aktuellsten pro Frame verarbeiten,
// START/END sofort durchlassen.
SharedInput.PathNetworkDrag pendingMove = null;
SharedInput.PathNetworkDrag drag;
while ((drag = input.pathNetworkDragQueue.poll()) != null) {
if (drag.phase() == SharedInput.PathNetworkDrag.MOVE) {
pendingMove = drag;
} else {
if (pendingMove != null) { handleDrag(pendingMove); pendingMove = null; }
handleDrag(drag);
}
}
if (pendingMove != null) { handleDrag(pendingMove); }
if (input.deletePathNodeRequested) { if (input.deletePathNodeRequested) {
input.deletePathNodeRequested = false; input.deletePathNodeRequested = false;
deleteSelected(); deleteSelected();
} }
} }
// ── Klick ─────────────────────────────────────────────────────────────────
private void handleClick(SharedInput.PathNetworkClick click) { private void handleClick(SharedInput.PathNetworkClick click) {
float jmeX = click.screenX() * (float) input.viewportScaleX; Ray ray = screenToRay(click.screenX(), click.screenY());
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
// Knoten in der Nähe des Strahls suchen
PathNode hitNode = findNodeNearRay(ray); PathNode hitNode = findNodeNearRay(ray);
if (click.rightClick()) { if (click.rightClick()) {
@@ -149,23 +157,26 @@ public class PathNetworkEditorState extends BaseAppState {
private void handleLeftClick(Ray ray, PathNode hitNode) { private void handleLeftClick(Ray ray, PathNode hitNode) {
if (hitNode != null) { if (hitNode != null) {
// Klick auf Knoten
if (selectedNode == null) { if (selectedNode == null) {
select(hitNode); select(hitNode);
} else if (selectedNode == hitNode) { } else if (selectedNode == hitNode) {
select(null); // Abwählen select(null);
} else { } else {
network.addEdge(selectedNode.getUuid(), hitNode.getUuid()); network.addEdge(selectedNode.getUuid(), hitNode.getUuid());
select(hitNode); select(hitNode);
saveAndNotify(); saveAndNotify();
rebuildVisuals(); rebuildVisuals();
} }
} else { return;
// Klick auf Terrain → neuer Knoten }
WorldPoint pt = terrainHit(ray); WorldPoint pt = terrainHit(ray);
if (pt == null) return; if (pt == null) return;
PathNode newNode = new PathNode(pt);
network.addNode(newNode); // Kante in der Nähe? → splitten und neuen Knoten einfügen
PathEdge edgeHit = findEdgeNearPoint(pt);
PathNode newNode = (edgeHit != null) ? insertNodeOnEdge(edgeHit, pt) : newNodeAt(pt);
if (selectedNode != null) { if (selectedNode != null) {
network.addEdge(selectedNode.getUuid(), newNode.getUuid()); network.addEdge(selectedNode.getUuid(), newNode.getUuid());
} }
@@ -173,11 +184,10 @@ public class PathNetworkEditorState extends BaseAppState {
saveAndNotify(); saveAndNotify();
rebuildVisuals(); rebuildVisuals();
} }
}
private void handleRightClick(PathNode hitNode) { private void handleRightClick(PathNode hitNode) {
if (hitNode != null) { if (hitNode != null) {
if (selectedNode == hitNode) selectedNode = null; if (selectedNode == hitNode) { selectedNode = null; }
network.removeNode(hitNode.getUuid()); network.removeNode(hitNode.getUuid());
saveAndNotify(); saveAndNotify();
rebuildVisuals(); rebuildVisuals();
@@ -186,6 +196,83 @@ public class PathNetworkEditorState extends BaseAppState {
} }
} }
// ── Drag (Knoten verschieben) ──────────────────────────────────────────────
private void handleDrag(SharedInput.PathNetworkDrag drag) {
Ray ray = screenToRay(drag.screenX(), drag.screenY());
switch (drag.phase()) {
case SharedInput.PathNetworkDrag.START -> {
PathNode node = findNodeNearRay(ray);
if (node != null) {
draggedNode = node;
select(node);
}
}
case SharedInput.PathNetworkDrag.MOVE -> {
if (draggedNode != null) {
WorldPoint pt = terrainHit(ray);
if (pt != null) {
draggedNode.setPosition(pt);
moveNodeVisual(draggedNode);
}
}
}
case SharedInput.PathNetworkDrag.END -> {
if (draggedNode != null) {
WorldPoint pt = terrainHit(ray);
if (pt != null) { draggedNode.setPosition(pt); }
saveAndNotify();
rebuildVisuals();
draggedNode = null;
}
}
}
}
// ── Kanten-Insertion ───────────────────────────────────────────────────────
private PathEdge findEdgeNearPoint(WorldPoint pt) {
PathEdge best = null;
float bestDist = EDGE_HIT_THRESHOLD;
for (PathEdge e : network.getEdges()) {
PathNode a = network.nodeById(e.getNodeUuidA());
PathNode b = network.nodeById(e.getNodeUuidB());
if (a == null || b == null) continue;
float d = distPointToSegmentXZ(pt, a.getPosition(), b.getPosition());
if (d < bestDist) { bestDist = d; best = e; }
}
return best;
}
private float distPointToSegmentXZ(WorldPoint p, WorldPoint a, WorldPoint b) {
float dx = b.x - a.x, dz = b.z - a.z;
float len2 = dx * dx + dz * dz;
if (len2 < 0.001f) {
float ex = p.x - a.x, ez = p.z - a.z;
return (float) Math.sqrt(ex * ex + ez * ez);
}
float t = Math.max(0f, Math.min(1f, ((p.x - a.x) * dx + (p.z - a.z) * dz) / len2));
float cx = a.x + t * dx, cz = a.z + t * dz;
float ex = p.x - cx, ez = p.z - cz;
return (float) Math.sqrt(ex * ex + ez * ez);
}
private PathNode insertNodeOnEdge(PathEdge edge, WorldPoint pos) {
PathNode newNode = new PathNode(pos);
network.addNode(newNode);
network.removeEdge(edge.getNodeUuidA(), edge.getNodeUuidB());
network.addEdge(edge.getNodeUuidA(), newNode.getUuid());
network.addEdge(newNode.getUuid(), edge.getNodeUuidB());
return newNode;
}
private PathNode newNodeAt(WorldPoint pos) {
PathNode n = new PathNode(pos);
network.addNode(n);
return n;
}
// ── Visualisierung ───────────────────────────────────────────────────────── // ── Visualisierung ─────────────────────────────────────────────────────────
private void rebuildVisuals() { private void rebuildVisuals() {
@@ -193,8 +280,7 @@ public class PathNetworkEditorState extends BaseAppState {
edgeRoot.detachAllChildren(); edgeRoot.detachAllChildren();
for (PathNode n : network.getNodes()) { for (PathNode n : network.getNodes()) {
Geometry g = new Geometry("node_" + n.getUuid(), Geometry g = new Geometry("node_" + n.getUuid(), new Sphere(8, 8, NODE_SPHERE_RADIUS));
new Sphere(8, 8, NODE_SPHERE_RADIUS));
g.setMaterial(n == selectedNode ? matSelected : matDefault); g.setMaterial(n == selectedNode ? matSelected : matDefault);
g.setLocalTranslation(n.getPosition().x, n.getPosition().y + NODE_SPHERE_RADIUS, n.getPosition().z); g.setLocalTranslation(n.getPosition().x, n.getPosition().y + NODE_SPHERE_RADIUS, n.getPosition().z);
g.setUserData("nodeUuid", n.getUuid()); g.setUserData("nodeUuid", n.getUuid());
@@ -210,6 +296,27 @@ public class PathNetworkEditorState extends BaseAppState {
} }
} }
/** Effizienter Update: nur Sphere-Position + betroffene Kanten neu bauen. */
private void moveNodeVisual(PathNode node) {
Geometry sphere = (Geometry) nodeRoot.getChild("node_" + node.getUuid());
if (sphere != null) {
sphere.setLocalTranslation(
node.getPosition().x, node.getPosition().y + NODE_SPHERE_RADIUS, node.getPosition().z);
}
String uuid = node.getUuid();
for (PathEdge e : network.getEdges()) {
if (!e.involves(uuid)) continue;
PathNode a = network.nodeById(e.getNodeUuidA());
PathNode b = network.nodeById(e.getNodeUuidB());
if (a == null || b == null) continue;
String edgeName = "edge_" + a.getUuid() + "_" + b.getUuid();
Spatial old = edgeRoot.getChild(edgeName);
if (old != null) { old.removeFromParent(); }
Geometry g = buildEdgeGeometry(a, b);
if (g != null) { edgeRoot.attachChild(g); }
}
}
private Geometry buildEdgeGeometry(PathNode a, PathNode b) { private Geometry buildEdgeGeometry(PathNode a, PathNode b) {
float ax = a.getPosition().x, ay = a.getPosition().y, az = a.getPosition().z; float ax = a.getPosition().x, ay = a.getPosition().y, az = a.getPosition().z;
float bx = b.getPosition().x, by = b.getPosition().y, bz = b.getPosition().z; float bx = b.getPosition().x, by = b.getPosition().y, bz = b.getPosition().z;
@@ -225,26 +332,30 @@ public class PathNetworkEditorState extends BaseAppState {
g.setMaterial(matEdge); g.setMaterial(matEdge);
g.setLocalTranslation(mid); g.setLocalTranslation(mid);
// Zylinder liegt entlang lokaler Y-Achse.
// Rotationsmatrix: Spalten = wohin lokale Achsen X, Y, Z in Weltkoordinaten zeigen.
// Lokale Y → Richtung des Weges (nx, 0, nz) im XZ-Boden.
// Lokale Z → Welt-Y (oben), damit Zylinder nicht rollt.
// Lokale X = lokale Y × lokale Z = (nz, 0, -nx).
Matrix3f rot = new Matrix3f(); Matrix3f rot = new Matrix3f();
rot.setColumn(0, new Vector3f( nz, 0f, -nx)); // lokales X rot.setColumn(0, new Vector3f( nz, 0f, -nx));
rot.setColumn(1, new Vector3f( nx, 0f, nz)); // lokales Y → dir rot.setColumn(1, new Vector3f( nx, 0f, nz));
rot.setColumn(2, new Vector3f( 0f, 1f, 0f)); // lokales Z → Welt-oben rot.setColumn(2, new Vector3f( 0f, 1f, 0f));
g.setLocalRotation(new Quaternion().fromRotationMatrix(rot)); g.setLocalRotation(new Quaternion().fromRotationMatrix(rot));
return g; return g;
} }
private void updateNodeMaterial(PathNode node) { private void updateNodeMaterial(PathNode node) {
Geometry g = (Geometry) nodeRoot.getChild("node_" + node.getUuid()); Geometry g = (Geometry) nodeRoot.getChild("node_" + node.getUuid());
if (g != null) g.setMaterial(node == selectedNode ? matSelected : matDefault); if (g != null) { g.setMaterial(node == selectedNode ? matSelected : matDefault); }
} }
// ── Hilfsmethoden ────────────────────────────────────────────────────────── // ── Hilfsmethoden ──────────────────────────────────────────────────────────
private Ray screenToRay(float screenX, float screenY) {
float jmeX = screenX * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY;
Vector2f sc = new Vector2f(jmeX, jmeY);
Vector3f near = cam.getWorldCoordinates(sc, 0f);
Vector3f far = cam.getWorldCoordinates(sc, 1f);
return new Ray(near, far.subtract(near).normalizeLocal());
}
private PathNode findNodeNearRay(Ray ray) { private PathNode findNodeNearRay(Ray ray) {
PathNode best = null; PathNode best = null;
float bestDist = NODE_HIT_RADIUS; float bestDist = NODE_HIT_RADIUS;
@@ -273,8 +384,8 @@ public class PathNetworkEditorState extends BaseAppState {
private void select(PathNode node) { private void select(PathNode node) {
PathNode prev = selectedNode; PathNode prev = selectedNode;
selectedNode = node; selectedNode = node;
if (prev != null) updateNodeMaterial(prev); if (prev != null) { updateNodeMaterial(prev); }
if (node != null) updateNodeMaterial(node); if (node != null) { updateNodeMaterial(node); }
} }
private void saveAndNotify() { private void saveAndNotify() {

View File

@@ -48,6 +48,14 @@ public class SoundAreaState extends BaseAppState {
private Geometry inProgGeo = null; private Geometry inProgGeo = null;
private Geometry lastPointMarker = null; private Geometry lastPointMarker = null;
private int draggedZoneIdx = -1;
private int draggedVertexIdx = -1;
private float draggedOrigX, draggedOrigZ;
private int selectedVertexZoneIdx = -1;
private int selectedVertexIdx = -1;
private Geometry selectedVertexMarker = null;
private List<PlacedSoundArea> pendingAreas = null; private List<PlacedSoundArea> pendingAreas = null;
public SoundAreaState(SharedInput input) { public SoundAreaState(SharedInput input) {
@@ -93,6 +101,18 @@ public class SoundAreaState extends BaseAppState {
handleClick(click); handleClick(click);
} }
SharedInput.ZoneDrag pendingMove = null;
SharedInput.ZoneDrag drag;
while ((drag = input.soundAreaDragQueue.poll()) != null) {
if (drag.phase() == SharedInput.ZoneDrag.MOVE) {
pendingMove = drag;
} else {
if (pendingMove != null) { handleZoneDrag(pendingMove); pendingMove = null; }
handleZoneDrag(drag);
}
}
if (pendingMove != null) { handleZoneDrag(pendingMove); }
PlacedSoundArea pending = input.pendingSoundArea.getAndSet(null); PlacedSoundArea pending = input.pendingSoundArea.getAndSet(null);
if (pending != null && selectedIdx >= 0) { if (pending != null && selectedIdx >= 0) {
applyProperty(selectedIdx, pending); applyProperty(selectedIdx, pending);
@@ -103,6 +123,12 @@ public class SoundAreaState extends BaseAppState {
cancelPoly(); cancelPoly();
} }
if (input.deleteZoneVertexRequested && selectedVertexZoneIdx >= 0) {
input.deleteZoneVertexRequested = false;
input.deleteSoundAreaRequested = false;
deleteSelectedVertex();
}
if (input.deleteSoundAreaRequested) { if (input.deleteSoundAreaRequested) {
input.deleteSoundAreaRequested = false; input.deleteSoundAreaRequested = false;
if (selectedIdx >= 0) removeArea(selectedIdx); if (selectedIdx >= 0) removeArea(selectedIdx);
@@ -153,6 +179,11 @@ public class SoundAreaState extends BaseAppState {
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class); LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
if ((as0 != null && as0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return; if ((as0 != null && as0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
// pass 0: near vertex → select vertex
int[] vertHit = findNearestVertex(hitX, hitZ, 4f);
if (vertHit != null) { selectVertex(vertHit[0], vertHit[1]); return; }
deselectVertex();
// pass 1: clearly inside // pass 1: clearly inside
for (int i = 0; i < areas.size(); i++) { for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i); PlacedSoundArea a = areas.get(i);
@@ -160,7 +191,7 @@ public class SoundAreaState extends BaseAppState {
selectArea(i); return; selectArea(i); return;
} }
} }
// pass 2: within 50 cm of any edge // pass 2: within 50 cm of any edge → select
for (int i = 0; i < areas.size(); i++) { for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i); PlacedSoundArea a = areas.get(i);
if (pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) { if (pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
@@ -188,24 +219,311 @@ public class SoundAreaState extends BaseAppState {
} }
} }
private void selectVertex(int zoneIdx, int vertexIdx) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.deselectVertex();
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.deselectVertex();
deselectVertex();
selectedVertexZoneIdx = zoneIdx;
selectedVertexIdx = vertexIdx;
input.zoneVertexIsSelected = true;
PlacedSoundArea a = areas.get(zoneIdx);
float vx = a.pointsX()[vertexIdx];
float vz = a.pointsZ()[vertexIdx];
float vy = getHeightAt(vx, vz) + LINE_OFFSET_Y + 0.1f;
float s = 2f;
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
buf.put(vx - s).put(vy).put(vz - s); buf.put(vx + s).put(vy).put(vz + s);
buf.put(vx - s).put(vy).put(vz + s); buf.put(vx + s).put(vy).put(vz - s);
buf.flip();
Mesh mesh = new Mesh();
mesh.setMode(Mesh.Mode.Lines);
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
mesh.updateBound();
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(1f, 1f, 0.2f, 1f));
mat.getAdditionalRenderState().setLineWidth(4f);
selectedVertexMarker = new Geometry("sound_sel_vertex", mesh);
selectedVertexMarker.setMaterial(mat);
rootNode.attachChild(selectedVertexMarker);
}
void deselectVertex() {
if (selectedVertexZoneIdx >= 0) input.zoneVertexIsSelected = false;
selectedVertexZoneIdx = -1;
selectedVertexIdx = -1;
if (selectedVertexMarker != null) { rootNode.detachChild(selectedVertexMarker); selectedVertexMarker = null; }
}
private void deleteSelectedVertex() {
if (selectedVertexZoneIdx < 0) return;
int zIdx = selectedVertexZoneIdx;
int vIdx = selectedVertexIdx;
PlacedSoundArea a = areas.get(zIdx);
int n = a.pointsX().length;
float removedX = a.pointsX()[vIdx];
float removedZ = a.pointsZ()[vIdx];
deselectVertex();
if (n <= 3) {
removeArea(zIdx);
} else {
float[] nx = new float[n - 1];
float[] nz = new float[n - 1];
for (int i = 0, j = 0; i < n; i++) {
if (i == vIdx) continue;
nx[j] = a.pointsX()[i]; nz[j] = a.pointsZ()[i]; j++;
}
areas.set(zIdx, new PlacedSoundArea(nx, nz, a.soundPath(), a.volume(), a.crossfade()));
rebuildSoundAreaGeo(zIdx);
}
deleteMatchingVertex(removedX, removedZ);
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.deleteMatchingVertex(removedX, removedZ);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.deleteMatchingVertex(removedX, removedZ);
}
public void deleteMatchingVertex(float wx, float wz) {
float eps2 = 0.01f;
for (int i = areas.size() - 1; i >= 0; i--) {
PlacedSoundArea a = areas.get(i);
int n = a.pointsX().length;
for (int j = 0; j < n; j++) {
float dx = a.pointsX()[j] - wx;
float dz = a.pointsZ()[j] - wz;
if (dx * dx + dz * dz < eps2) {
if (n <= 3) { removeArea(i); }
else {
float[] nx = new float[n - 1];
float[] nz = new float[n - 1];
for (int k = 0, m = 0; k < n; k++) {
if (k == j) continue;
nx[m] = a.pointsX()[k]; nz[m] = a.pointsZ()[k]; m++;
}
areas.set(i, new PlacedSoundArea(nx, nz, a.soundPath(), a.volume(), a.crossfade()));
rebuildSoundAreaGeo(i);
}
break;
}
}
}
}
private float[] snapVertex(float x, float z) { private float[] snapVertex(float x, float z) {
float bestDist2 = SNAP_DIST * SNAP_DIST; float[] snap = new float[]{x, z, SNAP_DIST * SNAP_DIST};
float bx = x, bz = z; updateSnap(x, z, snap);
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.updateSnap(x, z, snap);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.updateSnap(x, z, snap);
for (int i = 0; i < currX.size(); i++) {
float dx = x - currX.get(i);
float dz = z - currZ.get(i);
float d2 = dx * dx + dz * dz;
if (d2 < snap[2]) { snap[0] = currX.get(i); snap[1] = currZ.get(i); snap[2] = d2; }
}
return new float[]{snap[0], snap[1]};
}
public void updateSnap(float x, float z, float[] snap) {
for (PlacedSoundArea a : areas) { for (PlacedSoundArea a : areas) {
for (int i = 0; i < a.pointsX().length; i++) { for (int i = 0; i < a.pointsX().length; i++) {
float dx = x - a.pointsX()[i]; float dx = x - a.pointsX()[i];
float dz = z - a.pointsZ()[i]; float dz = z - a.pointsZ()[i];
float d2 = dx * dx + dz * dz; float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = a.pointsX()[i]; bz = a.pointsZ()[i]; } if (d2 < snap[2]) { snap[0] = a.pointsX()[i]; snap[1] = a.pointsZ()[i]; snap[2] = d2; }
} }
} }
for (int i = 0; i < currX.size(); i++) { }
float dx = x - currX.get(i);
float dz = z - currZ.get(i); private int[] findNearestVertex(float wx, float wz, float threshold) {
float bestDist2 = threshold * threshold;
int bestZone = -1, bestVert = -1;
for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i);
for (int j = 0; j < a.pointsX().length; j++) {
float dx = wx - a.pointsX()[j];
float dz = wz - a.pointsZ()[j];
float d2 = dx * dx + dz * dz; float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); } if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
}
}
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
}
private int[] findNearestEdge(float wx, float wz, float threshold) {
float bestDist = threshold;
int bestZone = -1, bestEdge = -1;
for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i);
int n = a.pointsX().length;
for (int j = 0; j < n; j++) {
int k = (j + 1) % n;
float dist = distPointToSegXZ(wx, wz,
a.pointsX()[j], a.pointsZ()[j], a.pointsX()[k], a.pointsZ()[k]);
if (dist < bestDist) { bestDist = dist; bestZone = i; bestEdge = j; }
}
}
return (bestZone >= 0) ? new int[]{bestZone, bestEdge} : null;
}
private static float distPointToSegXZ(float px, float pz,
float ax, float az, float bx, float bz) {
float abx = bx - ax, abz = bz - az;
float len2 = abx * abx + abz * abz;
if (len2 < 1e-6f) {
float dx = px - ax, dz = pz - az;
return (float) Math.sqrt(dx * dx + dz * dz);
}
float t = ((px - ax) * abx + (pz - az) * abz) / len2;
t = Math.max(0f, Math.min(1f, t));
float cx = ax + t * abx, cz = az + t * abz;
float dx = px - cx, dz = pz - cz;
return (float) Math.sqrt(dx * dx + dz * dz);
}
private void moveSoundAreaVertex(int zoneIdx, int vertexIdx, float newX, float newZ) {
PlacedSoundArea a = areas.get(zoneIdx);
a.pointsX()[vertexIdx] = newX;
a.pointsZ()[vertexIdx] = newZ;
rebuildSoundAreaGeo(zoneIdx);
}
private void rebuildSoundAreaGeo(int zoneIdx) {
rootNode.detachChild(areaGeos.get(zoneIdx));
PlacedSoundArea a = areas.get(zoneIdx);
List<Float> xs = toList(a.pointsX());
List<Float> zs = toList(a.pointsZ());
ColorRGBA color = (zoneIdx == selectedIdx) ? COLOR_SELECTED : COLOR_NORMAL;
float lineW = (zoneIdx == selectedIdx) ? 8f : 4f;
Geometry geo = buildLineGeo("sound_area_" + zoneIdx, xs, zs, color, Mesh.Mode.LineLoop);
geo.getMaterial().getAdditionalRenderState().setLineWidth(lineW);
rootNode.attachChild(geo);
areaGeos.set(zoneIdx, geo);
}
private void insertSoundAreaVertexOnEdge(int zoneIdx, int edgeStartIdx, float newX, float newZ) {
PlacedSoundArea a = areas.get(zoneIdx);
int n = a.pointsX().length;
int ins = edgeStartIdx + 1;
float[] nx = new float[n + 1];
float[] nz = new float[n + 1];
System.arraycopy(a.pointsX(), 0, nx, 0, ins);
nx[ins] = newX;
System.arraycopy(a.pointsX(), ins, nx, ins + 1, n - ins);
System.arraycopy(a.pointsZ(), 0, nz, 0, ins);
nz[ins] = newZ;
System.arraycopy(a.pointsZ(), ins, nz, ins + 1, n - ins);
areas.set(zoneIdx, new PlacedSoundArea(nx, nz, a.soundPath(), a.volume(), a.crossfade()));
rebuildSoundAreaGeo(zoneIdx);
}
private void handleZoneDrag(SharedInput.ZoneDrag drag) {
float jmeX = drag.screenX() * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
Vector3f pt = raycastAll(ray);
switch (drag.phase()) {
case SharedInput.ZoneDrag.START -> {
draggedZoneIdx = -1;
if (placing || pt == null) break;
int[] found = findNearestVertex(pt.x, pt.z, SNAP_DIST);
if (found != null) {
draggedZoneIdx = found[0];
draggedVertexIdx = found[1];
draggedOrigX = areas.get(draggedZoneIdx).pointsX()[draggedVertexIdx];
draggedOrigZ = areas.get(draggedZoneIdx).pointsZ()[draggedVertexIdx];
} else {
int[] edgeHit = findNearestEdge(pt.x, pt.z, 3f);
if (edgeHit != null) {
insertEdgeVertexInAllZones(pt.x, pt.z, 3f);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.insertSharedEdgeVertex(pt.x, pt.z);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.insertSharedEdgeVertex(pt.x, pt.z);
}
draggedZoneIdx = edgeHit[0];
draggedVertexIdx = edgeHit[1] + 1;
draggedOrigX = pt.x;
draggedOrigZ = pt.z;
}
}
}
case SharedInput.ZoneDrag.MOVE -> {
if (draggedZoneIdx < 0 || pt == null) break;
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
}
draggedOrigX = pt.x;
draggedOrigZ = pt.z;
}
case SharedInput.ZoneDrag.END -> {
if (draggedZoneIdx >= 0 && pt != null) {
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
}
}
draggedZoneIdx = -1;
}
}
}
public void insertSharedEdgeVertex(float wx, float wz) {
insertEdgeVertexInAllZones(wx, wz, 2f);
}
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
float eps2 = 0.05f * 0.05f;
for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i);
float[] xs = a.pointsX(), zs = a.pointsZ();
int n = xs.length;
boolean hasVertex = false;
for (int v = 0; v < n; v++) {
float dx = xs[v] - wx, dz = zs[v] - wz;
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
}
if (hasVertex) continue;
float bestDist = threshold;
int bestEdge = -1;
for (int j = 0; j < n; j++) {
int k = (j + 1) % n;
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
}
if (bestEdge >= 0) insertSoundAreaVertexOnEdge(i, bestEdge, wx, wz);
}
}
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
float eps2 = 0.01f;
for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i);
boolean changed = false;
for (int j = 0; j < a.pointsX().length; j++) {
float dx = a.pointsX()[j] - origX;
float dz = a.pointsZ()[j] - origZ;
if (dx * dx + dz * dz < eps2) {
a.pointsX()[j] = newX;
a.pointsZ()[j] = newZ;
changed = true;
}
}
if (changed) rebuildSoundAreaGeo(i);
} }
return new float[]{bx, bz};
} }
private void closePoly() { private void closePoly() {
@@ -228,6 +546,7 @@ public class SoundAreaState extends BaseAppState {
currZ.clear(); currZ.clear();
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; } if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; } if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
deselectVertex();
} }
private void undoLastPoint() { private void undoLastPoint() {

View File

@@ -24,6 +24,17 @@ public class ViewportController {
private int editPressAction; private int editPressAction;
private javafx.animation.Timeline editTimer; private javafx.animation.Timeline editTimer;
// Wegnetz-Drag-Tracking
private float pathNetPressX, pathNetPressY;
private boolean pathNetPressPending;
private boolean pathNetDragging;
private boolean pathNetRightPress;
// Zonen-Drag-Tracking
private float zonePressX, zonePressY;
private boolean zonePressPending;
private boolean zoneDragging;
public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) { public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) {
this.input = input; this.input = input;
this.initialImage = jfxImage; this.initialImage = jfxImage;
@@ -140,12 +151,24 @@ public class ViewportController {
} }
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) { if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) {
if (e.getButton() == MouseButton.PRIMARY && !bothDown) { if (!bothDown && (e.getButton() == MouseButton.PRIMARY || e.getButton() == MouseButton.SECONDARY)) {
input.pathNetworkClickQueue.offer( pathNetPressX = (float) e.getX();
new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), false)); pathNetPressY = (float) e.getY();
} else if (e.getButton() == MouseButton.SECONDARY && !bothDown) { pathNetPressPending = true;
input.pathNetworkClickQueue.offer( pathNetDragging = false;
new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), true)); pathNetRightPress = e.getButton() == MouseButton.SECONDARY;
}
return;
}
if (isZoneLayer()) {
if (e.getButton() == MouseButton.PRIMARY && !bothDown && !e.isAltDown()) {
zonePressX = (float) e.getX();
zonePressY = (float) e.getY();
zonePressPending = true;
zoneDragging = false;
} else if (e.getButton() == MouseButton.SECONDARY) {
submitEdit(e.getX(), e.getY(), -1);
} }
return; return;
} }
@@ -225,6 +248,41 @@ public class ViewportController {
return; return;
} }
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK
&& e.isPrimaryButtonDown() && !pathNetRightPress) {
if (pathNetPressPending) {
float ddx = (float)(e.getX() - pathNetPressX);
float ddy = (float)(e.getY() - pathNetPressY);
if (ddx * ddx + ddy * ddy > 16f) {
input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag(
pathNetPressX, pathNetPressY, SharedInput.PathNetworkDrag.START));
pathNetDragging = true;
pathNetPressPending = false;
}
}
if (pathNetDragging) {
input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag(
(float) e.getX(), (float) e.getY(), SharedInput.PathNetworkDrag.MOVE));
}
return;
}
if (isZoneLayer() && e.isPrimaryButtonDown()) {
if (zonePressPending) {
float ddx = (float)(e.getX() - zonePressX);
float ddy = (float)(e.getY() - zonePressY);
if (ddx * ddx + ddy * ddy > 16f) {
submitZoneDrag(zonePressX, zonePressY, SharedInput.ZoneDrag.START);
zoneDragging = true;
zonePressPending = false;
}
}
if (zoneDragging) {
submitZoneDrag((float) e.getX(), (float) e.getY(), SharedInput.ZoneDrag.MOVE);
}
return;
}
// Play-Tool EDIT: Drag-Events an JME3 weiterleiten // Play-Tool EDIT: Drag-Events an JME3 weiterleiten
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
&& input.playToolMode == SharedInput.PlayToolMode.EDIT && input.playToolMode == SharedInput.PlayToolMode.EDIT
@@ -245,6 +303,26 @@ public class ViewportController {
viewport.setOnMouseReleased(e -> { viewport.setOnMouseReleased(e -> {
objDragging = false; objDragging = false;
if (!isObjectMode()) { stopEditTimer(); } if (!isObjectMode()) { stopEditTimer(); }
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) {
if (pathNetDragging) {
input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag(
(float) e.getX(), (float) e.getY(), SharedInput.PathNetworkDrag.END));
pathNetDragging = false;
} else if (pathNetPressPending) {
input.pathNetworkClickQueue.offer(new SharedInput.PathNetworkClick(
pathNetPressX, pathNetPressY, pathNetRightPress));
pathNetPressPending = false;
}
}
if (isZoneLayer()) {
if (zoneDragging) {
submitZoneDrag((float) e.getX(), (float) e.getY(), SharedInput.ZoneDrag.END);
zoneDragging = false;
} else if (zonePressPending) {
submitEdit(zonePressX, zonePressY, +1);
zonePressPending = false;
}
}
if (input.activeLayer == SharedInput.LAYER_WATER) { if (input.activeLayer == SharedInput.LAYER_WATER) {
input.waterDragEnd = true; input.waterDragEnd = true;
} }
@@ -325,6 +403,28 @@ public class ViewportController {
if (editTimer != null) { editTimer.stop(); editTimer = null; } if (editTimer != null) { editTimer.stop(); editTimer = null; }
} }
private boolean isZoneLayer() {
return input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|| input.activeLayer == SharedInput.LAYER_AREAS
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|| input.activeLayer == SharedInput.LAYER_ZONEN;
}
private void submitZoneDrag(float x, float y, int phase) {
SharedInput.ZoneDrag drag = new SharedInput.ZoneDrag(x, y, phase);
switch (input.activeLayer) {
case SharedInput.LAYER_SOUND_AREAS -> input.soundAreaDragQueue.offer(drag);
case SharedInput.LAYER_AREAS -> input.areaDragQueue.offer(drag);
case SharedInput.LAYER_LOCATION_ZONES -> input.locationZoneDragQueue.offer(drag);
case SharedInput.LAYER_ZONEN -> {
input.soundAreaDragQueue.offer(drag);
input.areaDragQueue.offer(drag);
input.locationZoneDragQueue.offer(drag);
}
}
if (phase == SharedInput.ZoneDrag.END) scheduleAutoSave.run();
}
private void submitEdit(double x, double y, int action) { private void submitEdit(double x, double y, int action) {
switch (input.activeLayer) { switch (input.activeLayer) {
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action)); case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));

View File

@@ -30,7 +30,8 @@ public enum AnimationAction {
TALK2, TALK2,
DISAPPOINT, DISAPPOINT,
SECRET, SECRET,
TALK_SITTING; TALK_SITTING,
SWORD_ATTACK_1;
/** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */ /** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */
public String displayName() { public String displayName() {
@@ -60,6 +61,7 @@ public enum AnimationAction {
case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht"); case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht");
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis"); case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)"); case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)");
case SWORD_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1(Schwert)");
}; };
} }
} }

View File

@@ -1,2 +1,3 @@
# polygon areaId triggersJson labelX labelZ # polygon areaId triggersJson labelX labelZ
255.852,-828.388;281.186,-815.228;272.373,-782.936;237.423,-778.259;223.212,-812.720;227.232,-839.121 area.strand [] NaN NaN 255.852,-828.388;281.186,-815.228;272.373,-782.936;256.916,-772.353;237.419,-778.301;223.212,-812.720;227.232,-839.121 area.strand [] NaN NaN
270.596,-733.012;256.916,-772.353;237.423,-778.259;239.408,-750.931 [] NaN NaN

View File

@@ -9,17 +9,27 @@ NODE d886a17b-4ad0-4437-9dbf-6c0df951742f - 252.24966 6.40171 -892.78693
NODE caa8813b-d828-4db8-ae77-9cf369309618 - 254.10876 2.00000 -852.84991 NODE caa8813b-d828-4db8-ae77-9cf369309618 - 254.10876 2.00000 -852.84991
NODE ff5bb246-2c95-4e76-b592-5881c51dab51 - 254.28955 2.00035 -826.95752 NODE ff5bb246-2c95-4e76-b592-5881c51dab51 - 254.28955 2.00035 -826.95752
NODE 28a87ffd-a39b-4347-a79f-fe5ead071b59 - 291.11404 2.00004 -817.18658 NODE 28a87ffd-a39b-4347-a79f-fe5ead071b59 - 291.11404 2.00004 -817.18658
NODE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 - 306.78833 2.00000 -777.88641 NODE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 - 306.97855 2.00000 -784.28113
NODE c29223fa-67a0-4df0-8055-91381ec8c83f - 317.70370 2.00000 -782.45502 NODE c29223fa-67a0-4df0-8055-91381ec8c83f - 317.70370 2.00000 -782.45502
NODE 43677d9c-a19b-45f2-8eb3-d3368251d0b9 - 349.32700 2.00000 -774.56525 NODE 43677d9c-a19b-45f2-8eb3-d3368251d0b9 - 349.32700 2.00000 -774.56525
NODE aeb4a96f-b1d7-404a-a654-e98f74e5c856 - 299.91858 2.00001 -803.26099
NODE 64a2b232-6180-4f6d-9984-82820ac79053 - 298.20966 2.00000 -787.21967
NODE 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6 - 288.40808 2.00000 -778.93536
NODE 2b4e0e77-30a4-4314-87ad-0d3582feffd0 - 280.04413 2.00000 -766.30432
NODE 4f3a890b-a973-41fc-9798-d897209b12d0 - 295.18164 2.00000 -735.89783
EDGE c7fe03f3-736b-4d81-accb-27dd6649e000 fa3be430-7714-49ed-9a81-4e4fb45ba564 EDGE c7fe03f3-736b-4d81-accb-27dd6649e000 fa3be430-7714-49ed-9a81-4e4fb45ba564
EDGE fa3be430-7714-49ed-9a81-4e4fb45ba564 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 EDGE fa3be430-7714-49ed-9a81-4e4fb45ba564 5618a5ba-d0a5-4897-ab4d-a4a2932edb18
EDGE f8f01e21-3963-4530-a5d6-bfdd0cb77233 d886a17b-4ad0-4437-9dbf-6c0df951742f EDGE f8f01e21-3963-4530-a5d6-bfdd0cb77233 d886a17b-4ad0-4437-9dbf-6c0df951742f
EDGE caa8813b-d828-4db8-ae77-9cf369309618 ff5bb246-2c95-4e76-b592-5881c51dab51 EDGE caa8813b-d828-4db8-ae77-9cf369309618 ff5bb246-2c95-4e76-b592-5881c51dab51
EDGE ff5bb246-2c95-4e76-b592-5881c51dab51 28a87ffd-a39b-4347-a79f-fe5ead071b59 EDGE ff5bb246-2c95-4e76-b592-5881c51dab51 28a87ffd-a39b-4347-a79f-fe5ead071b59
EDGE 28a87ffd-a39b-4347-a79f-fe5ead071b59 7e3c1080-d37e-4fc9-b489-c0784f34fac1
EDGE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 c29223fa-67a0-4df0-8055-91381ec8c83f EDGE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 c29223fa-67a0-4df0-8055-91381ec8c83f
EDGE c29223fa-67a0-4df0-8055-91381ec8c83f 43677d9c-a19b-45f2-8eb3-d3368251d0b9 EDGE c29223fa-67a0-4df0-8055-91381ec8c83f 43677d9c-a19b-45f2-8eb3-d3368251d0b9
EDGE 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 f8f01e21-3963-4530-a5d6-bfdd0cb77233 EDGE 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 f8f01e21-3963-4530-a5d6-bfdd0cb77233
EDGE 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 caa8813b-d828-4db8-ae77-9cf369309618 EDGE 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 caa8813b-d828-4db8-ae77-9cf369309618
EDGE f8f01e21-3963-4530-a5d6-bfdd0cb77233 caa8813b-d828-4db8-ae77-9cf369309618 EDGE f8f01e21-3963-4530-a5d6-bfdd0cb77233 caa8813b-d828-4db8-ae77-9cf369309618
EDGE 28a87ffd-a39b-4347-a79f-fe5ead071b59 aeb4a96f-b1d7-404a-a654-e98f74e5c856
EDGE aeb4a96f-b1d7-404a-a654-e98f74e5c856 64a2b232-6180-4f6d-9984-82820ac79053
EDGE 64a2b232-6180-4f6d-9984-82820ac79053 7e3c1080-d37e-4fc9-b489-c0784f34fac1
EDGE 64a2b232-6180-4f6d-9984-82820ac79053 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6
EDGE 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6 2b4e0e77-30a4-4314-87ad-0d3582feffd0
EDGE 2b4e0e77-30a4-4314-87ad-0d3582feffd0 4f3a890b-a973-41fc-9798-d897209b12d0