diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index f754c73..57c6acd 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -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; diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index fd4b8b3..850b3a6 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -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 soundAreaDragQueue = new ConcurrentLinkedQueue<>(); + public final ConcurrentLinkedQueue areaDragQueue = new ConcurrentLinkedQueue<>(); + public final ConcurrentLinkedQueue locationZoneDragQueue = new ConcurrentLinkedQueue<>(); + // ── Sound-Bereich-Werkzeug ──────────────────────────────────────────────── public record SoundAreaClick(float screenX, float screenY, boolean rightButton) {} public final ConcurrentLinkedQueue 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; diff --git a/blight-editor/src/main/java/de/blight/editor/state/AreaState.java b/blight-editor/src/main/java/de/blight/editor/state/AreaState.java index 92e0017..117d28f 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AreaState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AreaState.java @@ -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 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 xs = toList(a.pointsX()); + List 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() { diff --git a/blight-editor/src/main/java/de/blight/editor/state/LocationZoneState.java b/blight-editor/src/main/java/de/blight/editor/state/LocationZoneState.java index bcfb52d..807b0e5 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/LocationZoneState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/LocationZoneState.java @@ -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 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 xs = toList(z.pointsX()); + List 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() { diff --git a/blight-editor/src/main/java/de/blight/editor/state/SoundAreaState.java b/blight-editor/src/main/java/de/blight/editor/state/SoundAreaState.java index e69f740..78afe20 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SoundAreaState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SoundAreaState.java @@ -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 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 xs = toList(a.pointsX()); + List 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() { diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java index f1b5ef4..e4ed524 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java @@ -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)); diff --git a/blight-map/src/main/map/blight_areas.bla b/blight-map/src/main/map/blight_areas.bla index 0649548..ba50ec6 100644 --- a/blight-map/src/main/map/blight_areas.bla +++ b/blight-map/src/main/map/blight_areas.bla @@ -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