Malen und Editieren von Zonen erweitert

This commit is contained in:
2026-09-13 21:33:30 +02:00
parent 6cf7234ab1
commit 08c76a798d
7 changed files with 1082 additions and 38 deletions

View File

@@ -931,15 +931,21 @@ public class EditorApp extends Application {
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
input.deleteSelectedRequested = true;
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) {
input.deleteAreaRequested = true; scheduleAutoSave();
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
else { input.deleteAreaRequested = true; scheduleAutoSave(); }
} 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) {
if ("sound".equals(input.zonenSelectedKind)) { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
else if ("area".equals(input.zonenSelectedKind)) { input.deleteAreaRequested = true; scheduleAutoSave(); }
else if ("location".equals(input.zonenSelectedKind)) { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
else {
if ("sound".equals(input.zonenSelectedKind)) { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
else if ("area".equals(input.zonenSelectedKind)) { input.deleteAreaRequested = true; scheduleAutoSave(); }
else if ("location".equals(input.zonenSelectedKind)) { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }
}
}
else if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK)
input.deletePathNodeRequested = true;

View File

@@ -548,6 +548,14 @@ public class SharedInput {
/** JavaFX → JME: Selektierte Wasserfläche löschen. */
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 ────────────────────────────────────────────────
public record SoundAreaClick(float screenX, float screenY, boolean rightButton) {}
public final ConcurrentLinkedQueue<SoundAreaClick> soundAreaClickQueue = new ConcurrentLinkedQueue<>();
@@ -595,6 +603,12 @@ public class SharedInput {
/** JavaFX → JME: Selektierte Location-Zone löschen. */
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). */
public volatile boolean cancelZoneDrawing = false;

View File

@@ -46,6 +46,14 @@ public class AreaState extends BaseAppState {
private Geometry inProgGeo = 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;
public AreaState(SharedInput input) {
@@ -91,6 +99,23 @@ public class AreaState extends BaseAppState {
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);
if (pending != null && selectedIdx >= 0) {
applyProperty(selectedIdx, pending);
@@ -151,14 +176,22 @@ public class AreaState extends BaseAppState {
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
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++) {
PlacedArea a = areas.get(i);
if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
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++) {
PlacedArea a = areas.get(i);
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) {
float bestDist2 = SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST;
float bx = x, bz = z;
float[] snap = new float[]{x, z, SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST};
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 (int i = 0; i < a.pointsX().length; i++) {
float dx = x - a.pointsX()[i];
float dz = z - a.pointsZ()[i];
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);
float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = 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;
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() {
@@ -231,6 +551,7 @@ public class AreaState extends BaseAppState {
currZ.clear();
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
deselectVertex();
}
private void undoLastPoint() {

View File

@@ -44,6 +44,14 @@ public class LocationZoneState extends BaseAppState {
private Geometry inProgGeo = 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;
public LocationZoneState(SharedInput input) {
@@ -89,6 +97,18 @@ public class LocationZoneState extends BaseAppState {
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);
if (pending != null && selectedIdx >= 0) {
applyProperty(selectedIdx, pending);
@@ -99,6 +119,12 @@ public class LocationZoneState extends BaseAppState {
cancelPoly();
}
if (input.deleteZoneVertexRequested && selectedVertexZoneIdx >= 0) {
input.deleteZoneVertexRequested = false;
input.deleteLocationZoneRequested = false;
deleteSelectedVertex();
}
if (input.deleteLocationZoneRequested) {
input.deleteLocationZoneRequested = false;
if (selectedIdx >= 0) removeZone(selectedIdx);
@@ -149,6 +175,11 @@ public class LocationZoneState extends BaseAppState {
SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class);
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
for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i);
@@ -156,7 +187,7 @@ public class LocationZoneState extends BaseAppState {
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++) {
PlacedLocationZone z = zones.get(i);
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) {
float bestDist2 = SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST;
float bx = x, bz = z;
float[] snap = new float[]{x, z, SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST};
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 (int i = 0; i < zone.pointsX().length; i++) {
float dx = x - zone.pointsX()[i];
float dz = z - zone.pointsZ()[i];
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);
float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = 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;
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() {
@@ -221,6 +539,7 @@ public class LocationZoneState extends BaseAppState {
currZ.clear();
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
deselectVertex();
}
private void undoLastPoint() {

View File

@@ -48,6 +48,14 @@ public class SoundAreaState extends BaseAppState {
private Geometry inProgGeo = 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;
public SoundAreaState(SharedInput input) {
@@ -93,6 +101,18 @@ public class SoundAreaState extends BaseAppState {
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);
if (pending != null && selectedIdx >= 0) {
applyProperty(selectedIdx, pending);
@@ -103,6 +123,12 @@ public class SoundAreaState extends BaseAppState {
cancelPoly();
}
if (input.deleteZoneVertexRequested && selectedVertexZoneIdx >= 0) {
input.deleteZoneVertexRequested = false;
input.deleteSoundAreaRequested = false;
deleteSelectedVertex();
}
if (input.deleteSoundAreaRequested) {
input.deleteSoundAreaRequested = false;
if (selectedIdx >= 0) removeArea(selectedIdx);
@@ -153,6 +179,11 @@ public class SoundAreaState extends BaseAppState {
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
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
for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i);
@@ -160,7 +191,7 @@ public class SoundAreaState extends BaseAppState {
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++) {
PlacedSoundArea a = areas.get(i);
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) {
float bestDist2 = SNAP_DIST * SNAP_DIST;
float bx = x, bz = z;
float[] snap = new float[]{x, z, SNAP_DIST * SNAP_DIST};
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 (int i = 0; i < a.pointsX().length; i++) {
float dx = x - a.pointsX()[i];
float dz = z - a.pointsZ()[i];
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);
float d2 = dx * dx + dz * dz;
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = 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;
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() {
@@ -228,6 +546,7 @@ public class SoundAreaState extends BaseAppState {
currZ.clear();
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
deselectVertex();
}
private void undoLastPoint() {

View File

@@ -30,6 +30,11 @@ public class ViewportController {
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) {
this.input = input;
this.initialImage = jfxImage;
@@ -156,6 +161,18 @@ public class ViewportController {
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;
}
if (input.activeLayer == SharedInput.LAYER_STONE
&& input.stoneTool.brushRadius.getValue() == 1.0) {
// Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer
@@ -250,6 +267,22 @@ public class ViewportController {
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
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
&& input.playToolMode == SharedInput.PlayToolMode.EDIT
@@ -281,6 +314,15 @@ public class ViewportController {
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) {
input.waterDragEnd = true;
}
@@ -361,6 +403,28 @@ public class ViewportController {
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) {
switch (input.activeLayer) {
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));

View File

@@ -1,2 +1,3 @@
# 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