Compare commits
2 Commits
320b0834b9
...
08c76a798d
| Author | SHA1 | Date | |
|---|---|---|---|
| 08c76a798d | |||
| 6cf7234ab1 |
@@ -931,16 +931,22 @@ 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 (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;
|
||||
else if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1048,6 +1062,16 @@ public class SharedInput {
|
||||
public final ConcurrentLinkedQueue<PathNetworkClick> pathNetworkClickQueue =
|
||||
new ConcurrentLinkedQueue<>();
|
||||
|
||||
/**
|
||||
* JFX → JME: Drag-Ereignis im Wegnetz-Modus (Knoten verschieben).
|
||||
* phase: START=0, MOVE=1, END=2.
|
||||
*/
|
||||
public record PathNetworkDrag(float screenX, float screenY, int phase) {
|
||||
public static final int START = 0, MOVE = 1, END = 2;
|
||||
}
|
||||
public final ConcurrentLinkedQueue<PathNetworkDrag> pathNetworkDragQueue =
|
||||
new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** JME → JFX: Wegnetz wurde verändert, UI-Refresh nötig. */
|
||||
public volatile boolean pathNetworkChanged = false;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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; bx = currX.get(i); bz = currZ.get(i); }
|
||||
if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
|
||||
}
|
||||
}
|
||||
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
|
||||
}
|
||||
|
||||
private int[] findNearestEdge(float wx, float wz, float threshold) {
|
||||
float bestDist = threshold;
|
||||
int bestZone = -1, bestEdge = -1;
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedArea a = areas.get(i);
|
||||
int n = a.pointsX().length;
|
||||
for (int j = 0; j < n; j++) {
|
||||
int k = (j + 1) % n;
|
||||
float dist = distPointToSegXZ(wx, wz,
|
||||
a.pointsX()[j], a.pointsZ()[j], a.pointsX()[k], a.pointsZ()[k]);
|
||||
if (dist < bestDist) { bestDist = dist; bestZone = i; bestEdge = j; }
|
||||
}
|
||||
}
|
||||
return (bestZone >= 0) ? new int[]{bestZone, bestEdge} : null;
|
||||
}
|
||||
|
||||
private static float distPointToSegXZ(float px, float pz,
|
||||
float ax, float az, float bx, float bz) {
|
||||
float abx = bx - ax, abz = bz - az;
|
||||
float len2 = abx * abx + abz * abz;
|
||||
if (len2 < 1e-6f) {
|
||||
float dx = px - ax, dz = pz - az;
|
||||
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
float t = ((px - ax) * abx + (pz - az) * abz) / len2;
|
||||
t = Math.max(0f, Math.min(1f, t));
|
||||
float cx = ax + t * abx, cz = az + t * abz;
|
||||
float dx = px - cx, dz = pz - cz;
|
||||
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
private void moveAreaVertex(int zoneIdx, int vertexIdx, float newX, float newZ) {
|
||||
PlacedArea a = areas.get(zoneIdx);
|
||||
a.pointsX()[vertexIdx] = newX;
|
||||
a.pointsZ()[vertexIdx] = newZ;
|
||||
rebuildAreaGeo(zoneIdx);
|
||||
}
|
||||
|
||||
private void rebuildAreaGeo(int zoneIdx) {
|
||||
rootNode.detachChild(areaGeos.get(zoneIdx));
|
||||
PlacedArea a = areas.get(zoneIdx);
|
||||
List<Float> xs = toList(a.pointsX());
|
||||
List<Float> zs = toList(a.pointsZ());
|
||||
ColorRGBA color = (zoneIdx == selectedIdx) ? COLOR_SELECTED : COLOR_NORMAL;
|
||||
float lineW = (zoneIdx == selectedIdx) ? 8f : 4f;
|
||||
Geometry geo = buildLineGeo("area_" + zoneIdx, xs, zs, color, Mesh.Mode.LineLoop);
|
||||
geo.getMaterial().getAdditionalRenderState().setLineWidth(lineW);
|
||||
rootNode.attachChild(geo);
|
||||
areaGeos.set(zoneIdx, geo);
|
||||
}
|
||||
|
||||
private void insertAreaVertexOnEdge(int zoneIdx, int edgeStartIdx, float newX, float newZ) {
|
||||
PlacedArea a = areas.get(zoneIdx);
|
||||
int n = a.pointsX().length;
|
||||
int ins = edgeStartIdx + 1;
|
||||
float[] nx = new float[n + 1];
|
||||
float[] nz = new float[n + 1];
|
||||
System.arraycopy(a.pointsX(), 0, nx, 0, ins);
|
||||
nx[ins] = newX;
|
||||
System.arraycopy(a.pointsX(), ins, nx, ins + 1, n - ins);
|
||||
System.arraycopy(a.pointsZ(), 0, nz, 0, ins);
|
||||
nz[ins] = newZ;
|
||||
System.arraycopy(a.pointsZ(), ins, nz, ins + 1, n - ins);
|
||||
areas.set(zoneIdx, new PlacedArea(nx, nz, a.areaId(), a.triggers(), a.labelX(), a.labelZ()));
|
||||
rebuildAreaGeo(zoneIdx);
|
||||
}
|
||||
|
||||
private void handleZoneDrag(SharedInput.ZoneDrag drag) {
|
||||
float jmeX = drag.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
Vector3f pt = raycastAll(ray);
|
||||
|
||||
switch (drag.phase()) {
|
||||
case SharedInput.ZoneDrag.START -> {
|
||||
draggedZoneIdx = -1;
|
||||
if (placing || pt == null) break;
|
||||
int[] found = findNearestVertex(pt.x, pt.z, SoundAreaState.SNAP_DIST);
|
||||
if (found != null) {
|
||||
draggedZoneIdx = found[0];
|
||||
draggedVertexIdx = found[1];
|
||||
draggedOrigX = areas.get(draggedZoneIdx).pointsX()[draggedVertexIdx];
|
||||
draggedOrigZ = areas.get(draggedZoneIdx).pointsZ()[draggedVertexIdx];
|
||||
} else {
|
||||
int[] edgeHit = findNearestEdge(pt.x, pt.z, 3f);
|
||||
if (edgeHit != null) {
|
||||
insertEdgeVertexInAllZones(pt.x, pt.z, 3f);
|
||||
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||
if (sas != null) sas.insertSharedEdgeVertex(pt.x, pt.z);
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null) lzs.insertSharedEdgeVertex(pt.x, pt.z);
|
||||
}
|
||||
draggedZoneIdx = edgeHit[0];
|
||||
draggedVertexIdx = edgeHit[1] + 1;
|
||||
draggedOrigX = pt.x;
|
||||
draggedOrigZ = pt.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
case SharedInput.ZoneDrag.MOVE -> {
|
||||
if (draggedZoneIdx < 0 || pt == null) break;
|
||||
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
}
|
||||
draggedOrigX = pt.x;
|
||||
draggedOrigZ = pt.z;
|
||||
}
|
||||
case SharedInput.ZoneDrag.END -> {
|
||||
if (draggedZoneIdx >= 0 && pt != null) {
|
||||
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
}
|
||||
}
|
||||
draggedZoneIdx = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void insertSharedEdgeVertex(float wx, float wz) {
|
||||
insertEdgeVertexInAllZones(wx, wz, 2f);
|
||||
}
|
||||
|
||||
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
|
||||
float eps2 = 0.05f * 0.05f;
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedArea a = areas.get(i);
|
||||
float[] xs = a.pointsX(), zs = a.pointsZ();
|
||||
int n = xs.length;
|
||||
boolean hasVertex = false;
|
||||
for (int v = 0; v < n; v++) {
|
||||
float dx = xs[v] - wx, dz = zs[v] - wz;
|
||||
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
|
||||
}
|
||||
if (hasVertex) continue;
|
||||
float bestDist = threshold;
|
||||
int bestEdge = -1;
|
||||
for (int j = 0; j < n; j++) {
|
||||
int k = (j + 1) % n;
|
||||
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
|
||||
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
|
||||
}
|
||||
if (bestEdge >= 0) insertAreaVertexOnEdge(i, bestEdge, wx, wz);
|
||||
}
|
||||
}
|
||||
|
||||
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
|
||||
float eps2 = 0.01f;
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedArea a = areas.get(i);
|
||||
boolean changed = false;
|
||||
for (int j = 0; j < a.pointsX().length; j++) {
|
||||
float dx = a.pointsX()[j] - origX;
|
||||
float dz = a.pointsZ()[j] - origZ;
|
||||
if (dx * dx + dz * dz < eps2) {
|
||||
a.pointsX()[j] = newX;
|
||||
a.pointsZ()[j] = newZ;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) rebuildAreaGeo(i);
|
||||
}
|
||||
return new float[]{bx, bz};
|
||||
}
|
||||
|
||||
private void closePoly() {
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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; bx = currX.get(i); bz = currZ.get(i); }
|
||||
if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
|
||||
}
|
||||
}
|
||||
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
|
||||
}
|
||||
|
||||
public void insertSharedEdgeVertex(float wx, float wz) {
|
||||
insertEdgeVertexInAllZones(wx, wz, 2f);
|
||||
}
|
||||
|
||||
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
|
||||
float eps2 = 0.05f * 0.05f;
|
||||
for (int i = 0; i < zones.size(); i++) {
|
||||
PlacedLocationZone z = zones.get(i);
|
||||
float[] xs = z.pointsX(), zs = z.pointsZ();
|
||||
int n = xs.length;
|
||||
boolean hasVertex = false;
|
||||
for (int v = 0; v < n; v++) {
|
||||
float dx = xs[v] - wx, dz = zs[v] - wz;
|
||||
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
|
||||
}
|
||||
if (hasVertex) continue;
|
||||
float bestDist = threshold;
|
||||
int bestEdge = -1;
|
||||
for (int j = 0; j < n; j++) {
|
||||
int k = (j + 1) % n;
|
||||
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
|
||||
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
|
||||
}
|
||||
if (bestEdge >= 0) insertLocZoneVertexOnEdge(i, bestEdge, wx, wz);
|
||||
}
|
||||
}
|
||||
|
||||
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
|
||||
float eps2 = 0.01f;
|
||||
for (int i = 0; i < zones.size(); i++) {
|
||||
PlacedLocationZone z = zones.get(i);
|
||||
boolean changed = false;
|
||||
for (int j = 0; j < z.pointsX().length; j++) {
|
||||
float dx = z.pointsX()[j] - origX;
|
||||
float dz = z.pointsZ()[j] - origZ;
|
||||
if (dx * dx + dz * dz < eps2) {
|
||||
z.pointsX()[j] = newX;
|
||||
z.pointsZ()[j] = newZ;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) rebuildLocZoneGeo(i);
|
||||
}
|
||||
return new float[]{bx, bz};
|
||||
}
|
||||
|
||||
private void closePoly() {
|
||||
@@ -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() {
|
||||
|
||||
@@ -34,25 +34,22 @@ import java.util.List;
|
||||
*
|
||||
* <p>Interaktionsmodell:
|
||||
* <ul>
|
||||
* <li>Linksklick auf leeres Terrain → neuen Knoten platzieren</li>
|
||||
* <li>Linksklick auf Knoten → Knoten auswählen; ist bereits einer gewählt,
|
||||
* wird eine Kante zwischen beiden erzeugt</li>
|
||||
* <li>Linksklick auf Terrain mit gewähltem Knoten → neuen Knoten platzieren
|
||||
* UND mit dem gewählten Knoten verbinden</li>
|
||||
* <li>Rechtsklick auf Knoten → Knoten und alle seine Kanten löschen</li>
|
||||
* <li>Rechtsklick auf Terrain → Auswahl aufheben</li>
|
||||
* <li>LMK auf leeres Terrain → neuen Knoten platzieren</li>
|
||||
* <li>LMK auf Kante → Kante splitten, neuen Knoten einfügen</li>
|
||||
* <li>LMK auf Knoten → auswählen; ist bereits einer gewählt, Kante erzeugen</li>
|
||||
* <li>LMK Drag auf Knoten → Knoten auf neuer Terrain-Position ablegen</li>
|
||||
* <li>RMK auf Knoten → Knoten und seine Kanten löschen</li>
|
||||
* <li>RMK auf Terrain → Auswahl aufheben</li>
|
||||
* </ul>
|
||||
*
|
||||
* Änderungen werden sofort in die Datei geschrieben.
|
||||
*/
|
||||
public class PathNetworkEditorState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PathNetworkEditorState.class);
|
||||
|
||||
/** Radius in Welt-Einheiten, innerhalb dessen ein Klick einen Knoten trifft. */
|
||||
private static final float NODE_HIT_RADIUS = 3f;
|
||||
private static final float NODE_SPHERE_RADIUS = 0.6f;
|
||||
private static final float EDGE_RADIUS = 0.15f;
|
||||
private static final float EDGE_HIT_THRESHOLD = 3f;
|
||||
|
||||
private final SharedInput input;
|
||||
private Camera cam;
|
||||
@@ -62,8 +59,8 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
|
||||
private final PathNetwork network = new PathNetwork();
|
||||
private PathNode selectedNode = null;
|
||||
private PathNode draggedNode = null;
|
||||
|
||||
// Szene-Graph-Knoten für Visualisierung
|
||||
private final Node netRoot = new Node("pathnet_root");
|
||||
private final Node nodeRoot = new Node("pathnet_nodes");
|
||||
private final Node edgeRoot = new Node("pathnet_edges");
|
||||
@@ -83,9 +80,9 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
|
||||
matDefault = mat(new ColorRGBA(1f, 0.85f, 0f, 1f)); // gelb
|
||||
matSelected = mat(new ColorRGBA(1f, 0.4f, 0f, 1f)); // orange
|
||||
matEdge = mat(new ColorRGBA(0.3f, 0.9f, 0.3f, 1f)); // grün
|
||||
matDefault = mat(new ColorRGBA(1f, 0.85f, 0f, 1f));
|
||||
matSelected = mat(new ColorRGBA(1f, 0.4f, 0f, 1f));
|
||||
matEdge = mat(new ColorRGBA(0.3f, 0.9f, 0.3f, 1f));
|
||||
|
||||
netRoot.attachChild(nodeRoot);
|
||||
netRoot.attachChild(edgeRoot);
|
||||
@@ -111,6 +108,7 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
protected void onDisable() {
|
||||
netRoot.removeFromParent();
|
||||
selectedNode = null;
|
||||
draggedNode = null;
|
||||
}
|
||||
|
||||
public void setTerrain(TerrainQuad t) { this.terrain = t; }
|
||||
@@ -123,21 +121,31 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
while ((click = input.pathNetworkClickQueue.poll()) != null) {
|
||||
handleClick(click);
|
||||
}
|
||||
|
||||
// MOVE-Events kollabieren: nur den aktuellsten pro Frame verarbeiten,
|
||||
// START/END sofort durchlassen.
|
||||
SharedInput.PathNetworkDrag pendingMove = null;
|
||||
SharedInput.PathNetworkDrag drag;
|
||||
while ((drag = input.pathNetworkDragQueue.poll()) != null) {
|
||||
if (drag.phase() == SharedInput.PathNetworkDrag.MOVE) {
|
||||
pendingMove = drag;
|
||||
} else {
|
||||
if (pendingMove != null) { handleDrag(pendingMove); pendingMove = null; }
|
||||
handleDrag(drag);
|
||||
}
|
||||
}
|
||||
if (pendingMove != null) { handleDrag(pendingMove); }
|
||||
|
||||
if (input.deletePathNodeRequested) {
|
||||
input.deletePathNodeRequested = false;
|
||||
deleteSelected();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Klick ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private void handleClick(SharedInput.PathNetworkClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
// Knoten in der Nähe des Strahls suchen
|
||||
Ray ray = screenToRay(click.screenX(), click.screenY());
|
||||
PathNode hitNode = findNodeNearRay(ray);
|
||||
|
||||
if (click.rightClick()) {
|
||||
@@ -149,23 +157,26 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
|
||||
private void handleLeftClick(Ray ray, PathNode hitNode) {
|
||||
if (hitNode != null) {
|
||||
// Klick auf Knoten
|
||||
if (selectedNode == null) {
|
||||
select(hitNode);
|
||||
} else if (selectedNode == hitNode) {
|
||||
select(null); // Abwählen
|
||||
select(null);
|
||||
} else {
|
||||
network.addEdge(selectedNode.getUuid(), hitNode.getUuid());
|
||||
select(hitNode);
|
||||
saveAndNotify();
|
||||
rebuildVisuals();
|
||||
}
|
||||
} else {
|
||||
// Klick auf Terrain → neuer Knoten
|
||||
return;
|
||||
}
|
||||
|
||||
WorldPoint pt = terrainHit(ray);
|
||||
if (pt == null) return;
|
||||
PathNode newNode = new PathNode(pt);
|
||||
network.addNode(newNode);
|
||||
|
||||
// Kante in der Nähe? → splitten und neuen Knoten einfügen
|
||||
PathEdge edgeHit = findEdgeNearPoint(pt);
|
||||
PathNode newNode = (edgeHit != null) ? insertNodeOnEdge(edgeHit, pt) : newNodeAt(pt);
|
||||
|
||||
if (selectedNode != null) {
|
||||
network.addEdge(selectedNode.getUuid(), newNode.getUuid());
|
||||
}
|
||||
@@ -173,11 +184,10 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
saveAndNotify();
|
||||
rebuildVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRightClick(PathNode hitNode) {
|
||||
if (hitNode != null) {
|
||||
if (selectedNode == hitNode) selectedNode = null;
|
||||
if (selectedNode == hitNode) { selectedNode = null; }
|
||||
network.removeNode(hitNode.getUuid());
|
||||
saveAndNotify();
|
||||
rebuildVisuals();
|
||||
@@ -186,6 +196,83 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Drag (Knoten verschieben) ──────────────────────────────────────────────
|
||||
|
||||
private void handleDrag(SharedInput.PathNetworkDrag drag) {
|
||||
Ray ray = screenToRay(drag.screenX(), drag.screenY());
|
||||
|
||||
switch (drag.phase()) {
|
||||
case SharedInput.PathNetworkDrag.START -> {
|
||||
PathNode node = findNodeNearRay(ray);
|
||||
if (node != null) {
|
||||
draggedNode = node;
|
||||
select(node);
|
||||
}
|
||||
}
|
||||
case SharedInput.PathNetworkDrag.MOVE -> {
|
||||
if (draggedNode != null) {
|
||||
WorldPoint pt = terrainHit(ray);
|
||||
if (pt != null) {
|
||||
draggedNode.setPosition(pt);
|
||||
moveNodeVisual(draggedNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
case SharedInput.PathNetworkDrag.END -> {
|
||||
if (draggedNode != null) {
|
||||
WorldPoint pt = terrainHit(ray);
|
||||
if (pt != null) { draggedNode.setPosition(pt); }
|
||||
saveAndNotify();
|
||||
rebuildVisuals();
|
||||
draggedNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kanten-Insertion ───────────────────────────────────────────────────────
|
||||
|
||||
private PathEdge findEdgeNearPoint(WorldPoint pt) {
|
||||
PathEdge best = null;
|
||||
float bestDist = EDGE_HIT_THRESHOLD;
|
||||
for (PathEdge e : network.getEdges()) {
|
||||
PathNode a = network.nodeById(e.getNodeUuidA());
|
||||
PathNode b = network.nodeById(e.getNodeUuidB());
|
||||
if (a == null || b == null) continue;
|
||||
float d = distPointToSegmentXZ(pt, a.getPosition(), b.getPosition());
|
||||
if (d < bestDist) { bestDist = d; best = e; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private float distPointToSegmentXZ(WorldPoint p, WorldPoint a, WorldPoint b) {
|
||||
float dx = b.x - a.x, dz = b.z - a.z;
|
||||
float len2 = dx * dx + dz * dz;
|
||||
if (len2 < 0.001f) {
|
||||
float ex = p.x - a.x, ez = p.z - a.z;
|
||||
return (float) Math.sqrt(ex * ex + ez * ez);
|
||||
}
|
||||
float t = Math.max(0f, Math.min(1f, ((p.x - a.x) * dx + (p.z - a.z) * dz) / len2));
|
||||
float cx = a.x + t * dx, cz = a.z + t * dz;
|
||||
float ex = p.x - cx, ez = p.z - cz;
|
||||
return (float) Math.sqrt(ex * ex + ez * ez);
|
||||
}
|
||||
|
||||
private PathNode insertNodeOnEdge(PathEdge edge, WorldPoint pos) {
|
||||
PathNode newNode = new PathNode(pos);
|
||||
network.addNode(newNode);
|
||||
network.removeEdge(edge.getNodeUuidA(), edge.getNodeUuidB());
|
||||
network.addEdge(edge.getNodeUuidA(), newNode.getUuid());
|
||||
network.addEdge(newNode.getUuid(), edge.getNodeUuidB());
|
||||
return newNode;
|
||||
}
|
||||
|
||||
private PathNode newNodeAt(WorldPoint pos) {
|
||||
PathNode n = new PathNode(pos);
|
||||
network.addNode(n);
|
||||
return n;
|
||||
}
|
||||
|
||||
// ── Visualisierung ─────────────────────────────────────────────────────────
|
||||
|
||||
private void rebuildVisuals() {
|
||||
@@ -193,8 +280,7 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
edgeRoot.detachAllChildren();
|
||||
|
||||
for (PathNode n : network.getNodes()) {
|
||||
Geometry g = new Geometry("node_" + n.getUuid(),
|
||||
new Sphere(8, 8, NODE_SPHERE_RADIUS));
|
||||
Geometry g = new Geometry("node_" + n.getUuid(), new Sphere(8, 8, NODE_SPHERE_RADIUS));
|
||||
g.setMaterial(n == selectedNode ? matSelected : matDefault);
|
||||
g.setLocalTranslation(n.getPosition().x, n.getPosition().y + NODE_SPHERE_RADIUS, n.getPosition().z);
|
||||
g.setUserData("nodeUuid", n.getUuid());
|
||||
@@ -210,6 +296,27 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
/** Effizienter Update: nur Sphere-Position + betroffene Kanten neu bauen. */
|
||||
private void moveNodeVisual(PathNode node) {
|
||||
Geometry sphere = (Geometry) nodeRoot.getChild("node_" + node.getUuid());
|
||||
if (sphere != null) {
|
||||
sphere.setLocalTranslation(
|
||||
node.getPosition().x, node.getPosition().y + NODE_SPHERE_RADIUS, node.getPosition().z);
|
||||
}
|
||||
String uuid = node.getUuid();
|
||||
for (PathEdge e : network.getEdges()) {
|
||||
if (!e.involves(uuid)) continue;
|
||||
PathNode a = network.nodeById(e.getNodeUuidA());
|
||||
PathNode b = network.nodeById(e.getNodeUuidB());
|
||||
if (a == null || b == null) continue;
|
||||
String edgeName = "edge_" + a.getUuid() + "_" + b.getUuid();
|
||||
Spatial old = edgeRoot.getChild(edgeName);
|
||||
if (old != null) { old.removeFromParent(); }
|
||||
Geometry g = buildEdgeGeometry(a, b);
|
||||
if (g != null) { edgeRoot.attachChild(g); }
|
||||
}
|
||||
}
|
||||
|
||||
private Geometry buildEdgeGeometry(PathNode a, PathNode b) {
|
||||
float ax = a.getPosition().x, ay = a.getPosition().y, az = a.getPosition().z;
|
||||
float bx = b.getPosition().x, by = b.getPosition().y, bz = b.getPosition().z;
|
||||
@@ -225,26 +332,30 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
g.setMaterial(matEdge);
|
||||
g.setLocalTranslation(mid);
|
||||
|
||||
// Zylinder liegt entlang lokaler Y-Achse.
|
||||
// Rotationsmatrix: Spalten = wohin lokale Achsen X, Y, Z in Weltkoordinaten zeigen.
|
||||
// Lokale Y → Richtung des Weges (nx, 0, nz) im XZ-Boden.
|
||||
// Lokale Z → Welt-Y (oben), damit Zylinder nicht rollt.
|
||||
// Lokale X = lokale Y × lokale Z = (nz, 0, -nx).
|
||||
Matrix3f rot = new Matrix3f();
|
||||
rot.setColumn(0, new Vector3f( nz, 0f, -nx)); // lokales X
|
||||
rot.setColumn(1, new Vector3f( nx, 0f, nz)); // lokales Y → dir
|
||||
rot.setColumn(2, new Vector3f( 0f, 1f, 0f)); // lokales Z → Welt-oben
|
||||
rot.setColumn(0, new Vector3f( nz, 0f, -nx));
|
||||
rot.setColumn(1, new Vector3f( nx, 0f, nz));
|
||||
rot.setColumn(2, new Vector3f( 0f, 1f, 0f));
|
||||
g.setLocalRotation(new Quaternion().fromRotationMatrix(rot));
|
||||
return g;
|
||||
}
|
||||
|
||||
private void updateNodeMaterial(PathNode node) {
|
||||
Geometry g = (Geometry) nodeRoot.getChild("node_" + node.getUuid());
|
||||
if (g != null) g.setMaterial(node == selectedNode ? matSelected : matDefault);
|
||||
if (g != null) { g.setMaterial(node == selectedNode ? matSelected : matDefault); }
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ──────────────────────────────────────────────────────────
|
||||
|
||||
private Ray screenToRay(float screenX, float screenY) {
|
||||
float jmeX = screenX * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY;
|
||||
Vector2f sc = new Vector2f(jmeX, jmeY);
|
||||
Vector3f near = cam.getWorldCoordinates(sc, 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(sc, 1f);
|
||||
return new Ray(near, far.subtract(near).normalizeLocal());
|
||||
}
|
||||
|
||||
private PathNode findNodeNearRay(Ray ray) {
|
||||
PathNode best = null;
|
||||
float bestDist = NODE_HIT_RADIUS;
|
||||
@@ -273,8 +384,8 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
private void select(PathNode node) {
|
||||
PathNode prev = selectedNode;
|
||||
selectedNode = node;
|
||||
if (prev != null) updateNodeMaterial(prev);
|
||||
if (node != null) updateNodeMaterial(node);
|
||||
if (prev != null) { updateNodeMaterial(prev); }
|
||||
if (node != null) { updateNodeMaterial(node); }
|
||||
}
|
||||
|
||||
private void saveAndNotify() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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; bx = currX.get(i); bz = currZ.get(i); }
|
||||
if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
|
||||
}
|
||||
}
|
||||
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
|
||||
}
|
||||
|
||||
private int[] findNearestEdge(float wx, float wz, float threshold) {
|
||||
float bestDist = threshold;
|
||||
int bestZone = -1, bestEdge = -1;
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedSoundArea a = areas.get(i);
|
||||
int n = a.pointsX().length;
|
||||
for (int j = 0; j < n; j++) {
|
||||
int k = (j + 1) % n;
|
||||
float dist = distPointToSegXZ(wx, wz,
|
||||
a.pointsX()[j], a.pointsZ()[j], a.pointsX()[k], a.pointsZ()[k]);
|
||||
if (dist < bestDist) { bestDist = dist; bestZone = i; bestEdge = j; }
|
||||
}
|
||||
}
|
||||
return (bestZone >= 0) ? new int[]{bestZone, bestEdge} : null;
|
||||
}
|
||||
|
||||
private static float distPointToSegXZ(float px, float pz,
|
||||
float ax, float az, float bx, float bz) {
|
||||
float abx = bx - ax, abz = bz - az;
|
||||
float len2 = abx * abx + abz * abz;
|
||||
if (len2 < 1e-6f) {
|
||||
float dx = px - ax, dz = pz - az;
|
||||
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
float t = ((px - ax) * abx + (pz - az) * abz) / len2;
|
||||
t = Math.max(0f, Math.min(1f, t));
|
||||
float cx = ax + t * abx, cz = az + t * abz;
|
||||
float dx = px - cx, dz = pz - cz;
|
||||
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
private void moveSoundAreaVertex(int zoneIdx, int vertexIdx, float newX, float newZ) {
|
||||
PlacedSoundArea a = areas.get(zoneIdx);
|
||||
a.pointsX()[vertexIdx] = newX;
|
||||
a.pointsZ()[vertexIdx] = newZ;
|
||||
rebuildSoundAreaGeo(zoneIdx);
|
||||
}
|
||||
|
||||
private void rebuildSoundAreaGeo(int zoneIdx) {
|
||||
rootNode.detachChild(areaGeos.get(zoneIdx));
|
||||
PlacedSoundArea a = areas.get(zoneIdx);
|
||||
List<Float> xs = toList(a.pointsX());
|
||||
List<Float> zs = toList(a.pointsZ());
|
||||
ColorRGBA color = (zoneIdx == selectedIdx) ? COLOR_SELECTED : COLOR_NORMAL;
|
||||
float lineW = (zoneIdx == selectedIdx) ? 8f : 4f;
|
||||
Geometry geo = buildLineGeo("sound_area_" + zoneIdx, xs, zs, color, Mesh.Mode.LineLoop);
|
||||
geo.getMaterial().getAdditionalRenderState().setLineWidth(lineW);
|
||||
rootNode.attachChild(geo);
|
||||
areaGeos.set(zoneIdx, geo);
|
||||
}
|
||||
|
||||
private void insertSoundAreaVertexOnEdge(int zoneIdx, int edgeStartIdx, float newX, float newZ) {
|
||||
PlacedSoundArea a = areas.get(zoneIdx);
|
||||
int n = a.pointsX().length;
|
||||
int ins = edgeStartIdx + 1;
|
||||
float[] nx = new float[n + 1];
|
||||
float[] nz = new float[n + 1];
|
||||
System.arraycopy(a.pointsX(), 0, nx, 0, ins);
|
||||
nx[ins] = newX;
|
||||
System.arraycopy(a.pointsX(), ins, nx, ins + 1, n - ins);
|
||||
System.arraycopy(a.pointsZ(), 0, nz, 0, ins);
|
||||
nz[ins] = newZ;
|
||||
System.arraycopy(a.pointsZ(), ins, nz, ins + 1, n - ins);
|
||||
areas.set(zoneIdx, new PlacedSoundArea(nx, nz, a.soundPath(), a.volume(), a.crossfade()));
|
||||
rebuildSoundAreaGeo(zoneIdx);
|
||||
}
|
||||
|
||||
private void handleZoneDrag(SharedInput.ZoneDrag drag) {
|
||||
float jmeX = drag.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
Vector3f pt = raycastAll(ray);
|
||||
|
||||
switch (drag.phase()) {
|
||||
case SharedInput.ZoneDrag.START -> {
|
||||
draggedZoneIdx = -1;
|
||||
if (placing || pt == null) break;
|
||||
int[] found = findNearestVertex(pt.x, pt.z, SNAP_DIST);
|
||||
if (found != null) {
|
||||
draggedZoneIdx = found[0];
|
||||
draggedVertexIdx = found[1];
|
||||
draggedOrigX = areas.get(draggedZoneIdx).pointsX()[draggedVertexIdx];
|
||||
draggedOrigZ = areas.get(draggedZoneIdx).pointsZ()[draggedVertexIdx];
|
||||
} else {
|
||||
int[] edgeHit = findNearestEdge(pt.x, pt.z, 3f);
|
||||
if (edgeHit != null) {
|
||||
insertEdgeVertexInAllZones(pt.x, pt.z, 3f);
|
||||
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
AreaState as = getStateManager().getState(AreaState.class);
|
||||
if (as != null) as.insertSharedEdgeVertex(pt.x, pt.z);
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null) lzs.insertSharedEdgeVertex(pt.x, pt.z);
|
||||
}
|
||||
draggedZoneIdx = edgeHit[0];
|
||||
draggedVertexIdx = edgeHit[1] + 1;
|
||||
draggedOrigX = pt.x;
|
||||
draggedOrigZ = pt.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
case SharedInput.ZoneDrag.MOVE -> {
|
||||
if (draggedZoneIdx < 0 || pt == null) break;
|
||||
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
AreaState as = getStateManager().getState(AreaState.class);
|
||||
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
}
|
||||
draggedOrigX = pt.x;
|
||||
draggedOrigZ = pt.z;
|
||||
}
|
||||
case SharedInput.ZoneDrag.END -> {
|
||||
if (draggedZoneIdx >= 0 && pt != null) {
|
||||
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
AreaState as = getStateManager().getState(AreaState.class);
|
||||
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||
}
|
||||
}
|
||||
draggedZoneIdx = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void insertSharedEdgeVertex(float wx, float wz) {
|
||||
insertEdgeVertexInAllZones(wx, wz, 2f);
|
||||
}
|
||||
|
||||
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
|
||||
float eps2 = 0.05f * 0.05f;
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedSoundArea a = areas.get(i);
|
||||
float[] xs = a.pointsX(), zs = a.pointsZ();
|
||||
int n = xs.length;
|
||||
boolean hasVertex = false;
|
||||
for (int v = 0; v < n; v++) {
|
||||
float dx = xs[v] - wx, dz = zs[v] - wz;
|
||||
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
|
||||
}
|
||||
if (hasVertex) continue;
|
||||
float bestDist = threshold;
|
||||
int bestEdge = -1;
|
||||
for (int j = 0; j < n; j++) {
|
||||
int k = (j + 1) % n;
|
||||
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
|
||||
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
|
||||
}
|
||||
if (bestEdge >= 0) insertSoundAreaVertexOnEdge(i, bestEdge, wx, wz);
|
||||
}
|
||||
}
|
||||
|
||||
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
|
||||
float eps2 = 0.01f;
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedSoundArea a = areas.get(i);
|
||||
boolean changed = false;
|
||||
for (int j = 0; j < a.pointsX().length; j++) {
|
||||
float dx = a.pointsX()[j] - origX;
|
||||
float dz = a.pointsZ()[j] - origZ;
|
||||
if (dx * dx + dz * dz < eps2) {
|
||||
a.pointsX()[j] = newX;
|
||||
a.pointsZ()[j] = newZ;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) rebuildSoundAreaGeo(i);
|
||||
}
|
||||
return new float[]{bx, bz};
|
||||
}
|
||||
|
||||
private void closePoly() {
|
||||
@@ -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() {
|
||||
|
||||
@@ -24,6 +24,17 @@ public class ViewportController {
|
||||
private int editPressAction;
|
||||
private javafx.animation.Timeline editTimer;
|
||||
|
||||
// Wegnetz-Drag-Tracking
|
||||
private float pathNetPressX, pathNetPressY;
|
||||
private boolean pathNetPressPending;
|
||||
private boolean pathNetDragging;
|
||||
private boolean pathNetRightPress;
|
||||
|
||||
// Zonen-Drag-Tracking
|
||||
private float zonePressX, zonePressY;
|
||||
private boolean zonePressPending;
|
||||
private boolean zoneDragging;
|
||||
|
||||
public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) {
|
||||
this.input = input;
|
||||
this.initialImage = jfxImage;
|
||||
@@ -140,12 +151,24 @@ public class ViewportController {
|
||||
}
|
||||
|
||||
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) {
|
||||
if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
|
||||
input.pathNetworkClickQueue.offer(
|
||||
new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), false));
|
||||
} else if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
|
||||
input.pathNetworkClickQueue.offer(
|
||||
new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), true));
|
||||
if (!bothDown && (e.getButton() == MouseButton.PRIMARY || e.getButton() == MouseButton.SECONDARY)) {
|
||||
pathNetPressX = (float) e.getX();
|
||||
pathNetPressY = (float) e.getY();
|
||||
pathNetPressPending = true;
|
||||
pathNetDragging = false;
|
||||
pathNetRightPress = e.getButton() == MouseButton.SECONDARY;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isZoneLayer()) {
|
||||
if (e.getButton() == MouseButton.PRIMARY && !bothDown && !e.isAltDown()) {
|
||||
zonePressX = (float) e.getX();
|
||||
zonePressY = (float) e.getY();
|
||||
zonePressPending = true;
|
||||
zoneDragging = false;
|
||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||
submitEdit(e.getX(), e.getY(), -1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -225,6 +248,41 @@ public class ViewportController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK
|
||||
&& e.isPrimaryButtonDown() && !pathNetRightPress) {
|
||||
if (pathNetPressPending) {
|
||||
float ddx = (float)(e.getX() - pathNetPressX);
|
||||
float ddy = (float)(e.getY() - pathNetPressY);
|
||||
if (ddx * ddx + ddy * ddy > 16f) {
|
||||
input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag(
|
||||
pathNetPressX, pathNetPressY, SharedInput.PathNetworkDrag.START));
|
||||
pathNetDragging = true;
|
||||
pathNetPressPending = false;
|
||||
}
|
||||
}
|
||||
if (pathNetDragging) {
|
||||
input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag(
|
||||
(float) e.getX(), (float) e.getY(), SharedInput.PathNetworkDrag.MOVE));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isZoneLayer() && e.isPrimaryButtonDown()) {
|
||||
if (zonePressPending) {
|
||||
float ddx = (float)(e.getX() - zonePressX);
|
||||
float ddy = (float)(e.getY() - zonePressY);
|
||||
if (ddx * ddx + ddy * ddy > 16f) {
|
||||
submitZoneDrag(zonePressX, zonePressY, SharedInput.ZoneDrag.START);
|
||||
zoneDragging = true;
|
||||
zonePressPending = false;
|
||||
}
|
||||
}
|
||||
if (zoneDragging) {
|
||||
submitZoneDrag((float) e.getX(), (float) e.getY(), SharedInput.ZoneDrag.MOVE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Play-Tool EDIT: Drag-Events an JME3 weiterleiten
|
||||
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|
||||
&& input.playToolMode == SharedInput.PlayToolMode.EDIT
|
||||
@@ -245,6 +303,26 @@ public class ViewportController {
|
||||
viewport.setOnMouseReleased(e -> {
|
||||
objDragging = false;
|
||||
if (!isObjectMode()) { stopEditTimer(); }
|
||||
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) {
|
||||
if (pathNetDragging) {
|
||||
input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag(
|
||||
(float) e.getX(), (float) e.getY(), SharedInput.PathNetworkDrag.END));
|
||||
pathNetDragging = false;
|
||||
} else if (pathNetPressPending) {
|
||||
input.pathNetworkClickQueue.offer(new SharedInput.PathNetworkClick(
|
||||
pathNetPressX, pathNetPressY, pathNetRightPress));
|
||||
pathNetPressPending = false;
|
||||
}
|
||||
}
|
||||
if (isZoneLayer()) {
|
||||
if (zoneDragging) {
|
||||
submitZoneDrag((float) e.getX(), (float) e.getY(), SharedInput.ZoneDrag.END);
|
||||
zoneDragging = false;
|
||||
} else if (zonePressPending) {
|
||||
submitEdit(zonePressX, zonePressY, +1);
|
||||
zonePressPending = false;
|
||||
}
|
||||
}
|
||||
if (input.activeLayer == SharedInput.LAYER_WATER) {
|
||||
input.waterDragEnd = true;
|
||||
}
|
||||
@@ -325,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));
|
||||
|
||||
@@ -30,7 +30,8 @@ public enum AnimationAction {
|
||||
TALK2,
|
||||
DISAPPOINT,
|
||||
SECRET,
|
||||
TALK_SITTING;
|
||||
TALK_SITTING,
|
||||
SWORD_ATTACK_1;
|
||||
|
||||
/** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */
|
||||
public String displayName() {
|
||||
@@ -60,6 +61,7 @@ public enum AnimationAction {
|
||||
case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht");
|
||||
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
|
||||
case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)");
|
||||
case SWORD_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1(Schwert)");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,17 +9,27 @@ NODE d886a17b-4ad0-4437-9dbf-6c0df951742f - 252.24966 6.40171 -892.78693
|
||||
NODE caa8813b-d828-4db8-ae77-9cf369309618 - 254.10876 2.00000 -852.84991
|
||||
NODE ff5bb246-2c95-4e76-b592-5881c51dab51 - 254.28955 2.00035 -826.95752
|
||||
NODE 28a87ffd-a39b-4347-a79f-fe5ead071b59 - 291.11404 2.00004 -817.18658
|
||||
NODE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 - 306.78833 2.00000 -777.88641
|
||||
NODE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 - 306.97855 2.00000 -784.28113
|
||||
NODE c29223fa-67a0-4df0-8055-91381ec8c83f - 317.70370 2.00000 -782.45502
|
||||
NODE 43677d9c-a19b-45f2-8eb3-d3368251d0b9 - 349.32700 2.00000 -774.56525
|
||||
NODE aeb4a96f-b1d7-404a-a654-e98f74e5c856 - 299.91858 2.00001 -803.26099
|
||||
NODE 64a2b232-6180-4f6d-9984-82820ac79053 - 298.20966 2.00000 -787.21967
|
||||
NODE 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6 - 288.40808 2.00000 -778.93536
|
||||
NODE 2b4e0e77-30a4-4314-87ad-0d3582feffd0 - 280.04413 2.00000 -766.30432
|
||||
NODE 4f3a890b-a973-41fc-9798-d897209b12d0 - 295.18164 2.00000 -735.89783
|
||||
EDGE c7fe03f3-736b-4d81-accb-27dd6649e000 fa3be430-7714-49ed-9a81-4e4fb45ba564
|
||||
EDGE fa3be430-7714-49ed-9a81-4e4fb45ba564 5618a5ba-d0a5-4897-ab4d-a4a2932edb18
|
||||
EDGE f8f01e21-3963-4530-a5d6-bfdd0cb77233 d886a17b-4ad0-4437-9dbf-6c0df951742f
|
||||
EDGE caa8813b-d828-4db8-ae77-9cf369309618 ff5bb246-2c95-4e76-b592-5881c51dab51
|
||||
EDGE ff5bb246-2c95-4e76-b592-5881c51dab51 28a87ffd-a39b-4347-a79f-fe5ead071b59
|
||||
EDGE 28a87ffd-a39b-4347-a79f-fe5ead071b59 7e3c1080-d37e-4fc9-b489-c0784f34fac1
|
||||
EDGE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 c29223fa-67a0-4df0-8055-91381ec8c83f
|
||||
EDGE c29223fa-67a0-4df0-8055-91381ec8c83f 43677d9c-a19b-45f2-8eb3-d3368251d0b9
|
||||
EDGE 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 f8f01e21-3963-4530-a5d6-bfdd0cb77233
|
||||
EDGE 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 caa8813b-d828-4db8-ae77-9cf369309618
|
||||
EDGE f8f01e21-3963-4530-a5d6-bfdd0cb77233 caa8813b-d828-4db8-ae77-9cf369309618
|
||||
EDGE 28a87ffd-a39b-4347-a79f-fe5ead071b59 aeb4a96f-b1d7-404a-a654-e98f74e5c856
|
||||
EDGE aeb4a96f-b1d7-404a-a654-e98f74e5c856 64a2b232-6180-4f6d-9984-82820ac79053
|
||||
EDGE 64a2b232-6180-4f6d-9984-82820ac79053 7e3c1080-d37e-4fc9-b489-c0784f34fac1
|
||||
EDGE 64a2b232-6180-4f6d-9984-82820ac79053 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6
|
||||
EDGE 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6 2b4e0e77-30a4-4314-87ad-0d3582feffd0
|
||||
EDGE 2b4e0e77-30a4-4314-87ad-0d3582feffd0 4f3a890b-a973-41fc-9798-d897209b12d0
|
||||
|
||||
Reference in New Issue
Block a user