Malen und Editieren von Zonen erweitert
This commit is contained in:
@@ -931,15 +931,21 @@ public class EditorApp extends Application {
|
|||||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
|
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
|
||||||
input.deleteSelectedRequested = true;
|
input.deleteSelectedRequested = true;
|
||||||
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
|
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
|
||||||
input.deleteSoundAreaRequested = true; scheduleAutoSave();
|
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
|
||||||
|
else { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
|
||||||
} else if (input.activeLayer == SharedInput.LAYER_AREAS) {
|
} else if (input.activeLayer == SharedInput.LAYER_AREAS) {
|
||||||
input.deleteAreaRequested = true; scheduleAutoSave();
|
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
|
||||||
|
else { input.deleteAreaRequested = true; scheduleAutoSave(); }
|
||||||
} else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES) {
|
} else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES) {
|
||||||
input.deleteLocationZoneRequested = true; scheduleAutoSave();
|
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
|
||||||
|
else { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }
|
||||||
} else if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
} else if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||||
if ("sound".equals(input.zonenSelectedKind)) { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
|
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
|
||||||
else if ("area".equals(input.zonenSelectedKind)) { input.deleteAreaRequested = true; scheduleAutoSave(); }
|
else {
|
||||||
else if ("location".equals(input.zonenSelectedKind)) { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }
|
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)
|
else if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK)
|
||||||
input.deletePathNodeRequested = true;
|
input.deletePathNodeRequested = true;
|
||||||
|
|||||||
@@ -548,6 +548,14 @@ public class SharedInput {
|
|||||||
/** JavaFX → JME: Selektierte Wasserfläche löschen. */
|
/** JavaFX → JME: Selektierte Wasserfläche löschen. */
|
||||||
public volatile boolean deleteWaterRequested = false;
|
public volatile boolean deleteWaterRequested = false;
|
||||||
|
|
||||||
|
// ── Zonen-Vertex-Drag (für AreaState / SoundAreaState / LocationZoneState) ─
|
||||||
|
public record ZoneDrag(float screenX, float screenY, int phase) {
|
||||||
|
public static final int START = 0, MOVE = 1, END = 2;
|
||||||
|
}
|
||||||
|
public final ConcurrentLinkedQueue<ZoneDrag> soundAreaDragQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
public final ConcurrentLinkedQueue<ZoneDrag> areaDragQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
public final ConcurrentLinkedQueue<ZoneDrag> locationZoneDragQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
// ── Sound-Bereich-Werkzeug ────────────────────────────────────────────────
|
// ── Sound-Bereich-Werkzeug ────────────────────────────────────────────────
|
||||||
public record SoundAreaClick(float screenX, float screenY, boolean rightButton) {}
|
public record SoundAreaClick(float screenX, float screenY, boolean rightButton) {}
|
||||||
public final ConcurrentLinkedQueue<SoundAreaClick> soundAreaClickQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<SoundAreaClick> soundAreaClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
@@ -595,6 +603,12 @@ public class SharedInput {
|
|||||||
/** JavaFX → JME: Selektierte Location-Zone löschen. */
|
/** JavaFX → JME: Selektierte Location-Zone löschen. */
|
||||||
public volatile boolean deleteLocationZoneRequested = false;
|
public volatile boolean deleteLocationZoneRequested = false;
|
||||||
|
|
||||||
|
/** JavaFX → JME: Selektierten Zonen-Vertex löschen (Priorität über Zonen-Löschung). */
|
||||||
|
public volatile boolean deleteZoneVertexRequested = false;
|
||||||
|
|
||||||
|
/** JME → JavaFX: true, wenn mindestens eine Zonen-State gerade einen Vertex selektiert hat. */
|
||||||
|
public volatile boolean zoneVertexIsSelected = false;
|
||||||
|
|
||||||
/** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */
|
/** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */
|
||||||
public volatile boolean cancelZoneDrawing = false;
|
public volatile boolean cancelZoneDrawing = false;
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ public class AreaState extends BaseAppState {
|
|||||||
private Geometry inProgGeo = null;
|
private Geometry inProgGeo = null;
|
||||||
private Geometry lastPointMarker = null;
|
private Geometry lastPointMarker = null;
|
||||||
|
|
||||||
|
private int draggedZoneIdx = -1;
|
||||||
|
private int draggedVertexIdx = -1;
|
||||||
|
private float draggedOrigX, draggedOrigZ;
|
||||||
|
|
||||||
|
private int selectedVertexZoneIdx = -1;
|
||||||
|
private int selectedVertexIdx = -1;
|
||||||
|
private Geometry selectedVertexMarker = null;
|
||||||
|
|
||||||
private List<PlacedArea> pendingAreas = null;
|
private List<PlacedArea> pendingAreas = null;
|
||||||
|
|
||||||
public AreaState(SharedInput input) {
|
public AreaState(SharedInput input) {
|
||||||
@@ -91,6 +99,23 @@ public class AreaState extends BaseAppState {
|
|||||||
handleClick(click);
|
handleClick(click);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.deleteZoneVertexRequested && selectedVertexZoneIdx >= 0) {
|
||||||
|
input.deleteZoneVertexRequested = false;
|
||||||
|
deleteSelectedVertex();
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedInput.ZoneDrag pendingMove = null;
|
||||||
|
SharedInput.ZoneDrag drag;
|
||||||
|
while ((drag = input.areaDragQueue.poll()) != null) {
|
||||||
|
if (drag.phase() == SharedInput.ZoneDrag.MOVE) {
|
||||||
|
pendingMove = drag;
|
||||||
|
} else {
|
||||||
|
if (pendingMove != null) { handleZoneDrag(pendingMove); pendingMove = null; }
|
||||||
|
handleZoneDrag(drag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pendingMove != null) { handleZoneDrag(pendingMove); }
|
||||||
|
|
||||||
PlacedArea pending = input.pendingArea.getAndSet(null);
|
PlacedArea pending = input.pendingArea.getAndSet(null);
|
||||||
if (pending != null && selectedIdx >= 0) {
|
if (pending != null && selectedIdx >= 0) {
|
||||||
applyProperty(selectedIdx, pending);
|
applyProperty(selectedIdx, pending);
|
||||||
@@ -151,14 +176,22 @@ public class AreaState extends BaseAppState {
|
|||||||
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
|
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
|
||||||
if ((sas0 != null && sas0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
|
if ((sas0 != null && sas0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
|
||||||
|
|
||||||
// pass 1: clearly inside
|
// pass 0: near vertex → select vertex
|
||||||
|
int[] vertHit = findNearestVertex(hitX, hitZ, 4f);
|
||||||
|
if (vertHit != null) {
|
||||||
|
selectVertex(vertHit[0], vertHit[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deselectVertex();
|
||||||
|
|
||||||
|
// pass 1: clearly inside → select
|
||||||
for (int i = 0; i < areas.size(); i++) {
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
PlacedArea a = areas.get(i);
|
PlacedArea a = areas.get(i);
|
||||||
if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
|
if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
|
||||||
selectArea(i); return;
|
selectArea(i); return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pass 2: within 50 cm of any edge
|
// pass 2: within 50 cm of any edge → select
|
||||||
for (int i = 0; i < areas.size(); i++) {
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
PlacedArea a = areas.get(i);
|
PlacedArea a = areas.get(i);
|
||||||
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
|
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
|
||||||
@@ -186,24 +219,311 @@ public class AreaState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void selectVertex(int zoneIdx, int vertexIdx) {
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.deselectVertex();
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.deselectVertex();
|
||||||
|
deselectVertex();
|
||||||
|
selectedVertexZoneIdx = zoneIdx;
|
||||||
|
selectedVertexIdx = vertexIdx;
|
||||||
|
input.zoneVertexIsSelected = true;
|
||||||
|
PlacedArea a = areas.get(zoneIdx);
|
||||||
|
float vx = a.pointsX()[vertexIdx];
|
||||||
|
float vz = a.pointsZ()[vertexIdx];
|
||||||
|
float vy = getHeightAt(vx, vz) + LINE_OFFSET_Y + 0.1f;
|
||||||
|
float s = 2f;
|
||||||
|
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||||
|
buf.put(vx - s).put(vy).put(vz - s); buf.put(vx + s).put(vy).put(vz + s);
|
||||||
|
buf.put(vx - s).put(vy).put(vz + s); buf.put(vx + s).put(vy).put(vz - s);
|
||||||
|
buf.flip();
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(Mesh.Mode.Lines);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
|
||||||
|
mesh.updateBound();
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(1f, 1f, 0.2f, 1f));
|
||||||
|
mat.getAdditionalRenderState().setLineWidth(4f);
|
||||||
|
selectedVertexMarker = new Geometry("area_sel_vertex", mesh);
|
||||||
|
selectedVertexMarker.setMaterial(mat);
|
||||||
|
rootNode.attachChild(selectedVertexMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deselectVertex() {
|
||||||
|
if (selectedVertexZoneIdx >= 0) input.zoneVertexIsSelected = false;
|
||||||
|
selectedVertexZoneIdx = -1;
|
||||||
|
selectedVertexIdx = -1;
|
||||||
|
if (selectedVertexMarker != null) { rootNode.detachChild(selectedVertexMarker); selectedVertexMarker = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelectedVertex() {
|
||||||
|
if (selectedVertexZoneIdx < 0) return;
|
||||||
|
int zIdx = selectedVertexZoneIdx;
|
||||||
|
int vIdx = selectedVertexIdx;
|
||||||
|
PlacedArea a = areas.get(zIdx);
|
||||||
|
int n = a.pointsX().length;
|
||||||
|
float removedX = a.pointsX()[vIdx];
|
||||||
|
float removedZ = a.pointsZ()[vIdx];
|
||||||
|
deselectVertex();
|
||||||
|
if (n <= 3) {
|
||||||
|
removeArea(zIdx);
|
||||||
|
} else {
|
||||||
|
float[] nx = new float[n - 1];
|
||||||
|
float[] nz = new float[n - 1];
|
||||||
|
for (int i = 0, j = 0; i < n; i++) {
|
||||||
|
if (i == vIdx) continue;
|
||||||
|
nx[j] = a.pointsX()[i]; nz[j] = a.pointsZ()[i]; j++;
|
||||||
|
}
|
||||||
|
areas.set(zIdx, new PlacedArea(nx, nz, a.areaId(), a.triggers(), a.labelX(), a.labelZ()));
|
||||||
|
rebuildAreaGeo(zIdx);
|
||||||
|
}
|
||||||
|
deleteMatchingVertex(removedX, removedZ);
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.deleteMatchingVertex(removedX, removedZ);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.deleteMatchingVertex(removedX, removedZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteMatchingVertex(float wx, float wz) {
|
||||||
|
float eps2 = 0.01f;
|
||||||
|
for (int i = areas.size() - 1; i >= 0; i--) {
|
||||||
|
PlacedArea a = areas.get(i);
|
||||||
|
int n = a.pointsX().length;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
float dx = a.pointsX()[j] - wx;
|
||||||
|
float dz = a.pointsZ()[j] - wz;
|
||||||
|
if (dx * dx + dz * dz < eps2) {
|
||||||
|
if (n <= 3) { removeArea(i); }
|
||||||
|
else {
|
||||||
|
float[] nx = new float[n - 1];
|
||||||
|
float[] nz = new float[n - 1];
|
||||||
|
for (int k = 0, m = 0; k < n; k++) {
|
||||||
|
if (k == j) continue;
|
||||||
|
nx[m] = a.pointsX()[k]; nz[m] = a.pointsZ()[k]; m++;
|
||||||
|
}
|
||||||
|
areas.set(i, new PlacedArea(nx, nz, a.areaId(), a.triggers(), a.labelX(), a.labelZ()));
|
||||||
|
rebuildAreaGeo(i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private float[] snapVertex(float x, float z) {
|
private float[] snapVertex(float x, float z) {
|
||||||
float bestDist2 = SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST;
|
float[] snap = new float[]{x, z, SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST};
|
||||||
float bx = x, bz = z;
|
updateSnap(x, z, snap);
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.updateSnap(x, z, snap);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.updateSnap(x, z, snap);
|
||||||
|
for (int i = 0; i < currX.size(); i++) {
|
||||||
|
float dx = x - currX.get(i);
|
||||||
|
float dz = z - currZ.get(i);
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < snap[2]) { snap[0] = currX.get(i); snap[1] = currZ.get(i); snap[2] = d2; }
|
||||||
|
}
|
||||||
|
return new float[]{snap[0], snap[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSnap(float x, float z, float[] snap) {
|
||||||
for (PlacedArea a : areas) {
|
for (PlacedArea a : areas) {
|
||||||
for (int i = 0; i < a.pointsX().length; i++) {
|
for (int i = 0; i < a.pointsX().length; i++) {
|
||||||
float dx = x - a.pointsX()[i];
|
float dx = x - a.pointsX()[i];
|
||||||
float dz = z - a.pointsZ()[i];
|
float dz = z - a.pointsZ()[i];
|
||||||
float d2 = dx * dx + dz * dz;
|
float d2 = dx * dx + dz * dz;
|
||||||
if (d2 < bestDist2) { bestDist2 = d2; bx = a.pointsX()[i]; bz = a.pointsZ()[i]; }
|
if (d2 < snap[2]) { snap[0] = a.pointsX()[i]; snap[1] = a.pointsZ()[i]; snap[2] = d2; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int i = 0; i < currX.size(); i++) {
|
}
|
||||||
float dx = x - currX.get(i);
|
|
||||||
float dz = z - currZ.get(i);
|
private int[] findNearestVertex(float wx, float wz, float threshold) {
|
||||||
float d2 = dx * dx + dz * dz;
|
float bestDist2 = threshold * threshold;
|
||||||
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); }
|
int bestZone = -1, bestVert = -1;
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedArea a = areas.get(i);
|
||||||
|
for (int j = 0; j < a.pointsX().length; j++) {
|
||||||
|
float dx = wx - a.pointsX()[j];
|
||||||
|
float dz = wz - a.pointsZ()[j];
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] findNearestEdge(float wx, float wz, float threshold) {
|
||||||
|
float bestDist = threshold;
|
||||||
|
int bestZone = -1, bestEdge = -1;
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedArea a = areas.get(i);
|
||||||
|
int n = a.pointsX().length;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
int k = (j + 1) % n;
|
||||||
|
float dist = distPointToSegXZ(wx, wz,
|
||||||
|
a.pointsX()[j], a.pointsZ()[j], a.pointsX()[k], a.pointsZ()[k]);
|
||||||
|
if (dist < bestDist) { bestDist = dist; bestZone = i; bestEdge = j; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (bestZone >= 0) ? new int[]{bestZone, bestEdge} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float distPointToSegXZ(float px, float pz,
|
||||||
|
float ax, float az, float bx, float bz) {
|
||||||
|
float abx = bx - ax, abz = bz - az;
|
||||||
|
float len2 = abx * abx + abz * abz;
|
||||||
|
if (len2 < 1e-6f) {
|
||||||
|
float dx = px - ax, dz = pz - az;
|
||||||
|
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
float t = ((px - ax) * abx + (pz - az) * abz) / len2;
|
||||||
|
t = Math.max(0f, Math.min(1f, t));
|
||||||
|
float cx = ax + t * abx, cz = az + t * abz;
|
||||||
|
float dx = px - cx, dz = pz - cz;
|
||||||
|
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveAreaVertex(int zoneIdx, int vertexIdx, float newX, float newZ) {
|
||||||
|
PlacedArea a = areas.get(zoneIdx);
|
||||||
|
a.pointsX()[vertexIdx] = newX;
|
||||||
|
a.pointsZ()[vertexIdx] = newZ;
|
||||||
|
rebuildAreaGeo(zoneIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildAreaGeo(int zoneIdx) {
|
||||||
|
rootNode.detachChild(areaGeos.get(zoneIdx));
|
||||||
|
PlacedArea a = areas.get(zoneIdx);
|
||||||
|
List<Float> xs = toList(a.pointsX());
|
||||||
|
List<Float> zs = toList(a.pointsZ());
|
||||||
|
ColorRGBA color = (zoneIdx == selectedIdx) ? COLOR_SELECTED : COLOR_NORMAL;
|
||||||
|
float lineW = (zoneIdx == selectedIdx) ? 8f : 4f;
|
||||||
|
Geometry geo = buildLineGeo("area_" + zoneIdx, xs, zs, color, Mesh.Mode.LineLoop);
|
||||||
|
geo.getMaterial().getAdditionalRenderState().setLineWidth(lineW);
|
||||||
|
rootNode.attachChild(geo);
|
||||||
|
areaGeos.set(zoneIdx, geo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertAreaVertexOnEdge(int zoneIdx, int edgeStartIdx, float newX, float newZ) {
|
||||||
|
PlacedArea a = areas.get(zoneIdx);
|
||||||
|
int n = a.pointsX().length;
|
||||||
|
int ins = edgeStartIdx + 1;
|
||||||
|
float[] nx = new float[n + 1];
|
||||||
|
float[] nz = new float[n + 1];
|
||||||
|
System.arraycopy(a.pointsX(), 0, nx, 0, ins);
|
||||||
|
nx[ins] = newX;
|
||||||
|
System.arraycopy(a.pointsX(), ins, nx, ins + 1, n - ins);
|
||||||
|
System.arraycopy(a.pointsZ(), 0, nz, 0, ins);
|
||||||
|
nz[ins] = newZ;
|
||||||
|
System.arraycopy(a.pointsZ(), ins, nz, ins + 1, n - ins);
|
||||||
|
areas.set(zoneIdx, new PlacedArea(nx, nz, a.areaId(), a.triggers(), a.labelX(), a.labelZ()));
|
||||||
|
rebuildAreaGeo(zoneIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleZoneDrag(SharedInput.ZoneDrag drag) {
|
||||||
|
float jmeX = drag.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
Vector3f pt = raycastAll(ray);
|
||||||
|
|
||||||
|
switch (drag.phase()) {
|
||||||
|
case SharedInput.ZoneDrag.START -> {
|
||||||
|
draggedZoneIdx = -1;
|
||||||
|
if (placing || pt == null) break;
|
||||||
|
int[] found = findNearestVertex(pt.x, pt.z, SoundAreaState.SNAP_DIST);
|
||||||
|
if (found != null) {
|
||||||
|
draggedZoneIdx = found[0];
|
||||||
|
draggedVertexIdx = found[1];
|
||||||
|
draggedOrigX = areas.get(draggedZoneIdx).pointsX()[draggedVertexIdx];
|
||||||
|
draggedOrigZ = areas.get(draggedZoneIdx).pointsZ()[draggedVertexIdx];
|
||||||
|
} else {
|
||||||
|
int[] edgeHit = findNearestEdge(pt.x, pt.z, 3f);
|
||||||
|
if (edgeHit != null) {
|
||||||
|
insertEdgeVertexInAllZones(pt.x, pt.z, 3f);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.insertSharedEdgeVertex(pt.x, pt.z);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.insertSharedEdgeVertex(pt.x, pt.z);
|
||||||
|
}
|
||||||
|
draggedZoneIdx = edgeHit[0];
|
||||||
|
draggedVertexIdx = edgeHit[1] + 1;
|
||||||
|
draggedOrigX = pt.x;
|
||||||
|
draggedOrigZ = pt.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SharedInput.ZoneDrag.MOVE -> {
|
||||||
|
if (draggedZoneIdx < 0 || pt == null) break;
|
||||||
|
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
}
|
||||||
|
draggedOrigX = pt.x;
|
||||||
|
draggedOrigZ = pt.z;
|
||||||
|
}
|
||||||
|
case SharedInput.ZoneDrag.END -> {
|
||||||
|
if (draggedZoneIdx >= 0 && pt != null) {
|
||||||
|
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draggedZoneIdx = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertSharedEdgeVertex(float wx, float wz) {
|
||||||
|
insertEdgeVertexInAllZones(wx, wz, 2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
|
||||||
|
float eps2 = 0.05f * 0.05f;
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedArea a = areas.get(i);
|
||||||
|
float[] xs = a.pointsX(), zs = a.pointsZ();
|
||||||
|
int n = xs.length;
|
||||||
|
boolean hasVertex = false;
|
||||||
|
for (int v = 0; v < n; v++) {
|
||||||
|
float dx = xs[v] - wx, dz = zs[v] - wz;
|
||||||
|
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
|
||||||
|
}
|
||||||
|
if (hasVertex) continue;
|
||||||
|
float bestDist = threshold;
|
||||||
|
int bestEdge = -1;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
int k = (j + 1) % n;
|
||||||
|
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
|
||||||
|
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
|
||||||
|
}
|
||||||
|
if (bestEdge >= 0) insertAreaVertexOnEdge(i, bestEdge, wx, wz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
|
||||||
|
float eps2 = 0.01f;
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedArea a = areas.get(i);
|
||||||
|
boolean changed = false;
|
||||||
|
for (int j = 0; j < a.pointsX().length; j++) {
|
||||||
|
float dx = a.pointsX()[j] - origX;
|
||||||
|
float dz = a.pointsZ()[j] - origZ;
|
||||||
|
if (dx * dx + dz * dz < eps2) {
|
||||||
|
a.pointsX()[j] = newX;
|
||||||
|
a.pointsZ()[j] = newZ;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) rebuildAreaGeo(i);
|
||||||
}
|
}
|
||||||
return new float[]{bx, bz};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closePoly() {
|
private void closePoly() {
|
||||||
@@ -231,6 +551,7 @@ public class AreaState extends BaseAppState {
|
|||||||
currZ.clear();
|
currZ.clear();
|
||||||
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
||||||
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||||
|
deselectVertex();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void undoLastPoint() {
|
private void undoLastPoint() {
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ public class LocationZoneState extends BaseAppState {
|
|||||||
private Geometry inProgGeo = null;
|
private Geometry inProgGeo = null;
|
||||||
private Geometry lastPointMarker = null;
|
private Geometry lastPointMarker = null;
|
||||||
|
|
||||||
|
private int draggedZoneIdx = -1;
|
||||||
|
private int draggedVertexIdx = -1;
|
||||||
|
private float draggedOrigX, draggedOrigZ;
|
||||||
|
|
||||||
|
private int selectedVertexZoneIdx = -1;
|
||||||
|
private int selectedVertexIdx = -1;
|
||||||
|
private Geometry selectedVertexMarker = null;
|
||||||
|
|
||||||
private List<PlacedLocationZone> pendingZones = null;
|
private List<PlacedLocationZone> pendingZones = null;
|
||||||
|
|
||||||
public LocationZoneState(SharedInput input) {
|
public LocationZoneState(SharedInput input) {
|
||||||
@@ -89,6 +97,18 @@ public class LocationZoneState extends BaseAppState {
|
|||||||
handleClick(click);
|
handleClick(click);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SharedInput.ZoneDrag pendingMove = null;
|
||||||
|
SharedInput.ZoneDrag drag;
|
||||||
|
while ((drag = input.locationZoneDragQueue.poll()) != null) {
|
||||||
|
if (drag.phase() == SharedInput.ZoneDrag.MOVE) {
|
||||||
|
pendingMove = drag;
|
||||||
|
} else {
|
||||||
|
if (pendingMove != null) { handleZoneDrag(pendingMove); pendingMove = null; }
|
||||||
|
handleZoneDrag(drag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pendingMove != null) { handleZoneDrag(pendingMove); }
|
||||||
|
|
||||||
PlacedLocationZone pending = input.pendingLocationZone.getAndSet(null);
|
PlacedLocationZone pending = input.pendingLocationZone.getAndSet(null);
|
||||||
if (pending != null && selectedIdx >= 0) {
|
if (pending != null && selectedIdx >= 0) {
|
||||||
applyProperty(selectedIdx, pending);
|
applyProperty(selectedIdx, pending);
|
||||||
@@ -99,6 +119,12 @@ public class LocationZoneState extends BaseAppState {
|
|||||||
cancelPoly();
|
cancelPoly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.deleteZoneVertexRequested && selectedVertexZoneIdx >= 0) {
|
||||||
|
input.deleteZoneVertexRequested = false;
|
||||||
|
input.deleteLocationZoneRequested = false;
|
||||||
|
deleteSelectedVertex();
|
||||||
|
}
|
||||||
|
|
||||||
if (input.deleteLocationZoneRequested) {
|
if (input.deleteLocationZoneRequested) {
|
||||||
input.deleteLocationZoneRequested = false;
|
input.deleteLocationZoneRequested = false;
|
||||||
if (selectedIdx >= 0) removeZone(selectedIdx);
|
if (selectedIdx >= 0) removeZone(selectedIdx);
|
||||||
@@ -149,6 +175,11 @@ public class LocationZoneState extends BaseAppState {
|
|||||||
SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class);
|
SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class);
|
||||||
if ((as0 != null && as0.isPlacing()) || (sas0 != null && sas0.isPlacing())) return;
|
if ((as0 != null && as0.isPlacing()) || (sas0 != null && sas0.isPlacing())) return;
|
||||||
|
|
||||||
|
// pass 0: near vertex → select vertex
|
||||||
|
int[] vertHit = findNearestLocVertex(hitX, hitZ, 4f);
|
||||||
|
if (vertHit != null) { selectVertex(vertHit[0], vertHit[1]); return; }
|
||||||
|
deselectVertex();
|
||||||
|
|
||||||
// pass 1: clearly inside
|
// pass 1: clearly inside
|
||||||
for (int i = 0; i < zones.size(); i++) {
|
for (int i = 0; i < zones.size(); i++) {
|
||||||
PlacedLocationZone z = zones.get(i);
|
PlacedLocationZone z = zones.get(i);
|
||||||
@@ -156,7 +187,7 @@ public class LocationZoneState extends BaseAppState {
|
|||||||
selectZone(i); return;
|
selectZone(i); return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pass 2: within 50 cm of any edge
|
// pass 2: within 50 cm of any edge → select
|
||||||
for (int i = 0; i < zones.size(); i++) {
|
for (int i = 0; i < zones.size(); i++) {
|
||||||
PlacedLocationZone z = zones.get(i);
|
PlacedLocationZone z = zones.get(i);
|
||||||
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, z.pointsX(), z.pointsZ(), 0.5f)) {
|
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, z.pointsX(), z.pointsZ(), 0.5f)) {
|
||||||
@@ -184,24 +215,311 @@ public class LocationZoneState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void selectVertex(int zoneIdx, int vertexIdx) {
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.deselectVertex();
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.deselectVertex();
|
||||||
|
deselectVertex();
|
||||||
|
selectedVertexZoneIdx = zoneIdx;
|
||||||
|
selectedVertexIdx = vertexIdx;
|
||||||
|
input.zoneVertexIsSelected = true;
|
||||||
|
PlacedLocationZone z = zones.get(zoneIdx);
|
||||||
|
float vx = z.pointsX()[vertexIdx];
|
||||||
|
float vz = z.pointsZ()[vertexIdx];
|
||||||
|
float vy = getHeightAt(vx, vz) + LINE_OFFSET_Y + 0.1f;
|
||||||
|
float s = 2f;
|
||||||
|
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||||
|
buf.put(vx - s).put(vy).put(vz - s); buf.put(vx + s).put(vy).put(vz + s);
|
||||||
|
buf.put(vx - s).put(vy).put(vz + s); buf.put(vx + s).put(vy).put(vz - s);
|
||||||
|
buf.flip();
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(Mesh.Mode.Lines);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
|
||||||
|
mesh.updateBound();
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(1f, 1f, 0.2f, 1f));
|
||||||
|
mat.getAdditionalRenderState().setLineWidth(4f);
|
||||||
|
selectedVertexMarker = new Geometry("loczone_sel_vertex", mesh);
|
||||||
|
selectedVertexMarker.setMaterial(mat);
|
||||||
|
rootNode.attachChild(selectedVertexMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deselectVertex() {
|
||||||
|
if (selectedVertexZoneIdx >= 0) input.zoneVertexIsSelected = false;
|
||||||
|
selectedVertexZoneIdx = -1;
|
||||||
|
selectedVertexIdx = -1;
|
||||||
|
if (selectedVertexMarker != null) { rootNode.detachChild(selectedVertexMarker); selectedVertexMarker = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelectedVertex() {
|
||||||
|
if (selectedVertexZoneIdx < 0) return;
|
||||||
|
int zIdx = selectedVertexZoneIdx;
|
||||||
|
int vIdx = selectedVertexIdx;
|
||||||
|
PlacedLocationZone z = zones.get(zIdx);
|
||||||
|
int n = z.pointsX().length;
|
||||||
|
float removedX = z.pointsX()[vIdx];
|
||||||
|
float removedZ = z.pointsZ()[vIdx];
|
||||||
|
deselectVertex();
|
||||||
|
if (n <= 3) {
|
||||||
|
removeZone(zIdx);
|
||||||
|
} else {
|
||||||
|
float[] nx = new float[n - 1];
|
||||||
|
float[] nz = new float[n - 1];
|
||||||
|
for (int i = 0, j = 0; i < n; i++) {
|
||||||
|
if (i == vIdx) continue;
|
||||||
|
nx[j] = z.pointsX()[i]; nz[j] = z.pointsZ()[i]; j++;
|
||||||
|
}
|
||||||
|
zones.set(zIdx, new PlacedLocationZone(nx, nz, z.nameId(), z.triggers()));
|
||||||
|
rebuildLocZoneGeo(zIdx);
|
||||||
|
}
|
||||||
|
deleteMatchingVertex(removedX, removedZ);
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.deleteMatchingVertex(removedX, removedZ);
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.deleteMatchingVertex(removedX, removedZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteMatchingVertex(float wx, float wz) {
|
||||||
|
float eps2 = 0.01f;
|
||||||
|
for (int i = zones.size() - 1; i >= 0; i--) {
|
||||||
|
PlacedLocationZone z = zones.get(i);
|
||||||
|
int n = z.pointsX().length;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
float dx = z.pointsX()[j] - wx;
|
||||||
|
float dz = z.pointsZ()[j] - wz;
|
||||||
|
if (dx * dx + dz * dz < eps2) {
|
||||||
|
if (n <= 3) { removeZone(i); }
|
||||||
|
else {
|
||||||
|
float[] nx = new float[n - 1];
|
||||||
|
float[] nz = new float[n - 1];
|
||||||
|
for (int k = 0, m = 0; k < n; k++) {
|
||||||
|
if (k == j) continue;
|
||||||
|
nx[m] = z.pointsX()[k]; nz[m] = z.pointsZ()[k]; m++;
|
||||||
|
}
|
||||||
|
zones.set(i, new PlacedLocationZone(nx, nz, z.nameId(), z.triggers()));
|
||||||
|
rebuildLocZoneGeo(i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private float[] snapVertex(float x, float z) {
|
private float[] snapVertex(float x, float z) {
|
||||||
float bestDist2 = SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST;
|
float[] snap = new float[]{x, z, SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST};
|
||||||
float bx = x, bz = z;
|
updateSnap(x, z, snap);
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.updateSnap(x, z, snap);
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.updateSnap(x, z, snap);
|
||||||
|
for (int i = 0; i < currX.size(); i++) {
|
||||||
|
float dx = x - currX.get(i);
|
||||||
|
float dz = z - currZ.get(i);
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < snap[2]) { snap[0] = currX.get(i); snap[1] = currZ.get(i); snap[2] = d2; }
|
||||||
|
}
|
||||||
|
return new float[]{snap[0], snap[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSnap(float x, float z, float[] snap) {
|
||||||
for (PlacedLocationZone zone : zones) {
|
for (PlacedLocationZone zone : zones) {
|
||||||
for (int i = 0; i < zone.pointsX().length; i++) {
|
for (int i = 0; i < zone.pointsX().length; i++) {
|
||||||
float dx = x - zone.pointsX()[i];
|
float dx = x - zone.pointsX()[i];
|
||||||
float dz = z - zone.pointsZ()[i];
|
float dz = z - zone.pointsZ()[i];
|
||||||
float d2 = dx * dx + dz * dz;
|
float d2 = dx * dx + dz * dz;
|
||||||
if (d2 < bestDist2) { bestDist2 = d2; bx = zone.pointsX()[i]; bz = zone.pointsZ()[i]; }
|
if (d2 < snap[2]) { snap[0] = zone.pointsX()[i]; snap[1] = zone.pointsZ()[i]; snap[2] = d2; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int i = 0; i < currX.size(); i++) {
|
}
|
||||||
float dx = x - currX.get(i);
|
|
||||||
float dz = z - currZ.get(i);
|
private int[] findNearestEdge(float wx, float wz, float threshold) {
|
||||||
float d2 = dx * dx + dz * dz;
|
float bestDist = threshold;
|
||||||
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); }
|
int bestZone = -1, bestEdge = -1;
|
||||||
|
for (int i = 0; i < zones.size(); i++) {
|
||||||
|
PlacedLocationZone z = zones.get(i);
|
||||||
|
int n = z.pointsX().length;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
int k = (j + 1) % n;
|
||||||
|
float dist = distPointToSegXZ(wx, wz,
|
||||||
|
z.pointsX()[j], z.pointsZ()[j], z.pointsX()[k], z.pointsZ()[k]);
|
||||||
|
if (dist < bestDist) { bestDist = dist; bestZone = i; bestEdge = j; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (bestZone >= 0) ? new int[]{bestZone, bestEdge} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float distPointToSegXZ(float px, float pz,
|
||||||
|
float ax, float az, float bx, float bz) {
|
||||||
|
float abx = bx - ax, abz = bz - az;
|
||||||
|
float len2 = abx * abx + abz * abz;
|
||||||
|
if (len2 < 1e-6f) {
|
||||||
|
float dx = px - ax, dz = pz - az;
|
||||||
|
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
float t = ((px - ax) * abx + (pz - az) * abz) / len2;
|
||||||
|
t = Math.max(0f, Math.min(1f, t));
|
||||||
|
float cx = ax + t * abx, cz = az + t * abz;
|
||||||
|
float dx = px - cx, dz = pz - cz;
|
||||||
|
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveLocZoneVertex(int zoneIdx, int vertexIdx, float newX, float newZ) {
|
||||||
|
PlacedLocationZone z = zones.get(zoneIdx);
|
||||||
|
z.pointsX()[vertexIdx] = newX;
|
||||||
|
z.pointsZ()[vertexIdx] = newZ;
|
||||||
|
rebuildLocZoneGeo(zoneIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildLocZoneGeo(int zoneIdx) {
|
||||||
|
rootNode.detachChild(zoneGeos.get(zoneIdx));
|
||||||
|
PlacedLocationZone z = zones.get(zoneIdx);
|
||||||
|
List<Float> xs = toList(z.pointsX());
|
||||||
|
List<Float> zs = toList(z.pointsZ());
|
||||||
|
ColorRGBA color = (zoneIdx == selectedIdx) ? COLOR_SELECTED : COLOR_NORMAL;
|
||||||
|
float lineW = (zoneIdx == selectedIdx) ? 10f : 6f;
|
||||||
|
Geometry geo = buildLineGeo("loczone_" + zoneIdx, xs, zs, color, Mesh.Mode.LineLoop);
|
||||||
|
geo.getMaterial().getAdditionalRenderState().setLineWidth(lineW);
|
||||||
|
rootNode.attachChild(geo);
|
||||||
|
zoneGeos.set(zoneIdx, geo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertLocZoneVertexOnEdge(int zoneIdx, int edgeStartIdx, float newX, float newZ) {
|
||||||
|
PlacedLocationZone z = zones.get(zoneIdx);
|
||||||
|
int n = z.pointsX().length;
|
||||||
|
int ins = edgeStartIdx + 1;
|
||||||
|
float[] nx = new float[n + 1];
|
||||||
|
float[] nz = new float[n + 1];
|
||||||
|
System.arraycopy(z.pointsX(), 0, nx, 0, ins);
|
||||||
|
nx[ins] = newX;
|
||||||
|
System.arraycopy(z.pointsX(), ins, nx, ins + 1, n - ins);
|
||||||
|
System.arraycopy(z.pointsZ(), 0, nz, 0, ins);
|
||||||
|
nz[ins] = newZ;
|
||||||
|
System.arraycopy(z.pointsZ(), ins, nz, ins + 1, n - ins);
|
||||||
|
zones.set(zoneIdx, new PlacedLocationZone(nx, nz, z.nameId(), z.triggers()));
|
||||||
|
rebuildLocZoneGeo(zoneIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleZoneDrag(SharedInput.ZoneDrag drag) {
|
||||||
|
float jmeX = drag.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
Vector3f pt = raycastAll(ray);
|
||||||
|
|
||||||
|
switch (drag.phase()) {
|
||||||
|
case SharedInput.ZoneDrag.START -> {
|
||||||
|
draggedZoneIdx = -1;
|
||||||
|
if (placing || pt == null) break;
|
||||||
|
int[] found = findNearestLocVertex(pt.x, pt.z, SoundAreaState.SNAP_DIST);
|
||||||
|
if (found != null) {
|
||||||
|
draggedZoneIdx = found[0];
|
||||||
|
draggedVertexIdx = found[1];
|
||||||
|
draggedOrigX = zones.get(draggedZoneIdx).pointsX()[draggedVertexIdx];
|
||||||
|
draggedOrigZ = zones.get(draggedZoneIdx).pointsZ()[draggedVertexIdx];
|
||||||
|
} else {
|
||||||
|
int[] edgeHit = findNearestEdge(pt.x, pt.z, 3f);
|
||||||
|
if (edgeHit != null) {
|
||||||
|
insertEdgeVertexInAllZones(pt.x, pt.z, 3f);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.insertSharedEdgeVertex(pt.x, pt.z);
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.insertSharedEdgeVertex(pt.x, pt.z);
|
||||||
|
}
|
||||||
|
draggedZoneIdx = edgeHit[0];
|
||||||
|
draggedVertexIdx = edgeHit[1] + 1;
|
||||||
|
draggedOrigX = pt.x;
|
||||||
|
draggedOrigZ = pt.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SharedInput.ZoneDrag.MOVE -> {
|
||||||
|
if (draggedZoneIdx < 0 || pt == null) break;
|
||||||
|
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
}
|
||||||
|
draggedOrigX = pt.x;
|
||||||
|
draggedOrigZ = pt.z;
|
||||||
|
}
|
||||||
|
case SharedInput.ZoneDrag.END -> {
|
||||||
|
if (draggedZoneIdx >= 0 && pt != null) {
|
||||||
|
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||||
|
if (sas != null) sas.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draggedZoneIdx = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] findNearestLocVertex(float wx, float wz, float threshold) {
|
||||||
|
float bestDist2 = threshold * threshold;
|
||||||
|
int bestZone = -1, bestVert = -1;
|
||||||
|
for (int i = 0; i < zones.size(); i++) {
|
||||||
|
PlacedLocationZone z = zones.get(i);
|
||||||
|
for (int j = 0; j < z.pointsX().length; j++) {
|
||||||
|
float dx = wx - z.pointsX()[j];
|
||||||
|
float dz = wz - z.pointsZ()[j];
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertSharedEdgeVertex(float wx, float wz) {
|
||||||
|
insertEdgeVertexInAllZones(wx, wz, 2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
|
||||||
|
float eps2 = 0.05f * 0.05f;
|
||||||
|
for (int i = 0; i < zones.size(); i++) {
|
||||||
|
PlacedLocationZone z = zones.get(i);
|
||||||
|
float[] xs = z.pointsX(), zs = z.pointsZ();
|
||||||
|
int n = xs.length;
|
||||||
|
boolean hasVertex = false;
|
||||||
|
for (int v = 0; v < n; v++) {
|
||||||
|
float dx = xs[v] - wx, dz = zs[v] - wz;
|
||||||
|
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
|
||||||
|
}
|
||||||
|
if (hasVertex) continue;
|
||||||
|
float bestDist = threshold;
|
||||||
|
int bestEdge = -1;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
int k = (j + 1) % n;
|
||||||
|
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
|
||||||
|
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
|
||||||
|
}
|
||||||
|
if (bestEdge >= 0) insertLocZoneVertexOnEdge(i, bestEdge, wx, wz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
|
||||||
|
float eps2 = 0.01f;
|
||||||
|
for (int i = 0; i < zones.size(); i++) {
|
||||||
|
PlacedLocationZone z = zones.get(i);
|
||||||
|
boolean changed = false;
|
||||||
|
for (int j = 0; j < z.pointsX().length; j++) {
|
||||||
|
float dx = z.pointsX()[j] - origX;
|
||||||
|
float dz = z.pointsZ()[j] - origZ;
|
||||||
|
if (dx * dx + dz * dz < eps2) {
|
||||||
|
z.pointsX()[j] = newX;
|
||||||
|
z.pointsZ()[j] = newZ;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) rebuildLocZoneGeo(i);
|
||||||
}
|
}
|
||||||
return new float[]{bx, bz};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closePoly() {
|
private void closePoly() {
|
||||||
@@ -221,6 +539,7 @@ public class LocationZoneState extends BaseAppState {
|
|||||||
currZ.clear();
|
currZ.clear();
|
||||||
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
||||||
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||||
|
deselectVertex();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void undoLastPoint() {
|
private void undoLastPoint() {
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ public class SoundAreaState extends BaseAppState {
|
|||||||
private Geometry inProgGeo = null;
|
private Geometry inProgGeo = null;
|
||||||
private Geometry lastPointMarker = null;
|
private Geometry lastPointMarker = null;
|
||||||
|
|
||||||
|
private int draggedZoneIdx = -1;
|
||||||
|
private int draggedVertexIdx = -1;
|
||||||
|
private float draggedOrigX, draggedOrigZ;
|
||||||
|
|
||||||
|
private int selectedVertexZoneIdx = -1;
|
||||||
|
private int selectedVertexIdx = -1;
|
||||||
|
private Geometry selectedVertexMarker = null;
|
||||||
|
|
||||||
private List<PlacedSoundArea> pendingAreas = null;
|
private List<PlacedSoundArea> pendingAreas = null;
|
||||||
|
|
||||||
public SoundAreaState(SharedInput input) {
|
public SoundAreaState(SharedInput input) {
|
||||||
@@ -93,6 +101,18 @@ public class SoundAreaState extends BaseAppState {
|
|||||||
handleClick(click);
|
handleClick(click);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SharedInput.ZoneDrag pendingMove = null;
|
||||||
|
SharedInput.ZoneDrag drag;
|
||||||
|
while ((drag = input.soundAreaDragQueue.poll()) != null) {
|
||||||
|
if (drag.phase() == SharedInput.ZoneDrag.MOVE) {
|
||||||
|
pendingMove = drag;
|
||||||
|
} else {
|
||||||
|
if (pendingMove != null) { handleZoneDrag(pendingMove); pendingMove = null; }
|
||||||
|
handleZoneDrag(drag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pendingMove != null) { handleZoneDrag(pendingMove); }
|
||||||
|
|
||||||
PlacedSoundArea pending = input.pendingSoundArea.getAndSet(null);
|
PlacedSoundArea pending = input.pendingSoundArea.getAndSet(null);
|
||||||
if (pending != null && selectedIdx >= 0) {
|
if (pending != null && selectedIdx >= 0) {
|
||||||
applyProperty(selectedIdx, pending);
|
applyProperty(selectedIdx, pending);
|
||||||
@@ -103,6 +123,12 @@ public class SoundAreaState extends BaseAppState {
|
|||||||
cancelPoly();
|
cancelPoly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.deleteZoneVertexRequested && selectedVertexZoneIdx >= 0) {
|
||||||
|
input.deleteZoneVertexRequested = false;
|
||||||
|
input.deleteSoundAreaRequested = false;
|
||||||
|
deleteSelectedVertex();
|
||||||
|
}
|
||||||
|
|
||||||
if (input.deleteSoundAreaRequested) {
|
if (input.deleteSoundAreaRequested) {
|
||||||
input.deleteSoundAreaRequested = false;
|
input.deleteSoundAreaRequested = false;
|
||||||
if (selectedIdx >= 0) removeArea(selectedIdx);
|
if (selectedIdx >= 0) removeArea(selectedIdx);
|
||||||
@@ -153,6 +179,11 @@ public class SoundAreaState extends BaseAppState {
|
|||||||
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
|
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
|
||||||
if ((as0 != null && as0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
|
if ((as0 != null && as0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
|
||||||
|
|
||||||
|
// pass 0: near vertex → select vertex
|
||||||
|
int[] vertHit = findNearestVertex(hitX, hitZ, 4f);
|
||||||
|
if (vertHit != null) { selectVertex(vertHit[0], vertHit[1]); return; }
|
||||||
|
deselectVertex();
|
||||||
|
|
||||||
// pass 1: clearly inside
|
// pass 1: clearly inside
|
||||||
for (int i = 0; i < areas.size(); i++) {
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
PlacedSoundArea a = areas.get(i);
|
PlacedSoundArea a = areas.get(i);
|
||||||
@@ -160,7 +191,7 @@ public class SoundAreaState extends BaseAppState {
|
|||||||
selectArea(i); return;
|
selectArea(i); return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pass 2: within 50 cm of any edge
|
// pass 2: within 50 cm of any edge → select
|
||||||
for (int i = 0; i < areas.size(); i++) {
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
PlacedSoundArea a = areas.get(i);
|
PlacedSoundArea a = areas.get(i);
|
||||||
if (pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
|
if (pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
|
||||||
@@ -188,24 +219,311 @@ public class SoundAreaState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void selectVertex(int zoneIdx, int vertexIdx) {
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.deselectVertex();
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.deselectVertex();
|
||||||
|
deselectVertex();
|
||||||
|
selectedVertexZoneIdx = zoneIdx;
|
||||||
|
selectedVertexIdx = vertexIdx;
|
||||||
|
input.zoneVertexIsSelected = true;
|
||||||
|
PlacedSoundArea a = areas.get(zoneIdx);
|
||||||
|
float vx = a.pointsX()[vertexIdx];
|
||||||
|
float vz = a.pointsZ()[vertexIdx];
|
||||||
|
float vy = getHeightAt(vx, vz) + LINE_OFFSET_Y + 0.1f;
|
||||||
|
float s = 2f;
|
||||||
|
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||||
|
buf.put(vx - s).put(vy).put(vz - s); buf.put(vx + s).put(vy).put(vz + s);
|
||||||
|
buf.put(vx - s).put(vy).put(vz + s); buf.put(vx + s).put(vy).put(vz - s);
|
||||||
|
buf.flip();
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(Mesh.Mode.Lines);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
|
||||||
|
mesh.updateBound();
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(1f, 1f, 0.2f, 1f));
|
||||||
|
mat.getAdditionalRenderState().setLineWidth(4f);
|
||||||
|
selectedVertexMarker = new Geometry("sound_sel_vertex", mesh);
|
||||||
|
selectedVertexMarker.setMaterial(mat);
|
||||||
|
rootNode.attachChild(selectedVertexMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deselectVertex() {
|
||||||
|
if (selectedVertexZoneIdx >= 0) input.zoneVertexIsSelected = false;
|
||||||
|
selectedVertexZoneIdx = -1;
|
||||||
|
selectedVertexIdx = -1;
|
||||||
|
if (selectedVertexMarker != null) { rootNode.detachChild(selectedVertexMarker); selectedVertexMarker = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelectedVertex() {
|
||||||
|
if (selectedVertexZoneIdx < 0) return;
|
||||||
|
int zIdx = selectedVertexZoneIdx;
|
||||||
|
int vIdx = selectedVertexIdx;
|
||||||
|
PlacedSoundArea a = areas.get(zIdx);
|
||||||
|
int n = a.pointsX().length;
|
||||||
|
float removedX = a.pointsX()[vIdx];
|
||||||
|
float removedZ = a.pointsZ()[vIdx];
|
||||||
|
deselectVertex();
|
||||||
|
if (n <= 3) {
|
||||||
|
removeArea(zIdx);
|
||||||
|
} else {
|
||||||
|
float[] nx = new float[n - 1];
|
||||||
|
float[] nz = new float[n - 1];
|
||||||
|
for (int i = 0, j = 0; i < n; i++) {
|
||||||
|
if (i == vIdx) continue;
|
||||||
|
nx[j] = a.pointsX()[i]; nz[j] = a.pointsZ()[i]; j++;
|
||||||
|
}
|
||||||
|
areas.set(zIdx, new PlacedSoundArea(nx, nz, a.soundPath(), a.volume(), a.crossfade()));
|
||||||
|
rebuildSoundAreaGeo(zIdx);
|
||||||
|
}
|
||||||
|
deleteMatchingVertex(removedX, removedZ);
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.deleteMatchingVertex(removedX, removedZ);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.deleteMatchingVertex(removedX, removedZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteMatchingVertex(float wx, float wz) {
|
||||||
|
float eps2 = 0.01f;
|
||||||
|
for (int i = areas.size() - 1; i >= 0; i--) {
|
||||||
|
PlacedSoundArea a = areas.get(i);
|
||||||
|
int n = a.pointsX().length;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
float dx = a.pointsX()[j] - wx;
|
||||||
|
float dz = a.pointsZ()[j] - wz;
|
||||||
|
if (dx * dx + dz * dz < eps2) {
|
||||||
|
if (n <= 3) { removeArea(i); }
|
||||||
|
else {
|
||||||
|
float[] nx = new float[n - 1];
|
||||||
|
float[] nz = new float[n - 1];
|
||||||
|
for (int k = 0, m = 0; k < n; k++) {
|
||||||
|
if (k == j) continue;
|
||||||
|
nx[m] = a.pointsX()[k]; nz[m] = a.pointsZ()[k]; m++;
|
||||||
|
}
|
||||||
|
areas.set(i, new PlacedSoundArea(nx, nz, a.soundPath(), a.volume(), a.crossfade()));
|
||||||
|
rebuildSoundAreaGeo(i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private float[] snapVertex(float x, float z) {
|
private float[] snapVertex(float x, float z) {
|
||||||
float bestDist2 = SNAP_DIST * SNAP_DIST;
|
float[] snap = new float[]{x, z, SNAP_DIST * SNAP_DIST};
|
||||||
float bx = x, bz = z;
|
updateSnap(x, z, snap);
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.updateSnap(x, z, snap);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.updateSnap(x, z, snap);
|
||||||
|
for (int i = 0; i < currX.size(); i++) {
|
||||||
|
float dx = x - currX.get(i);
|
||||||
|
float dz = z - currZ.get(i);
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < snap[2]) { snap[0] = currX.get(i); snap[1] = currZ.get(i); snap[2] = d2; }
|
||||||
|
}
|
||||||
|
return new float[]{snap[0], snap[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSnap(float x, float z, float[] snap) {
|
||||||
for (PlacedSoundArea a : areas) {
|
for (PlacedSoundArea a : areas) {
|
||||||
for (int i = 0; i < a.pointsX().length; i++) {
|
for (int i = 0; i < a.pointsX().length; i++) {
|
||||||
float dx = x - a.pointsX()[i];
|
float dx = x - a.pointsX()[i];
|
||||||
float dz = z - a.pointsZ()[i];
|
float dz = z - a.pointsZ()[i];
|
||||||
float d2 = dx * dx + dz * dz;
|
float d2 = dx * dx + dz * dz;
|
||||||
if (d2 < bestDist2) { bestDist2 = d2; bx = a.pointsX()[i]; bz = a.pointsZ()[i]; }
|
if (d2 < snap[2]) { snap[0] = a.pointsX()[i]; snap[1] = a.pointsZ()[i]; snap[2] = d2; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int i = 0; i < currX.size(); i++) {
|
}
|
||||||
float dx = x - currX.get(i);
|
|
||||||
float dz = z - currZ.get(i);
|
private int[] findNearestVertex(float wx, float wz, float threshold) {
|
||||||
float d2 = dx * dx + dz * dz;
|
float bestDist2 = threshold * threshold;
|
||||||
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); }
|
int bestZone = -1, bestVert = -1;
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedSoundArea a = areas.get(i);
|
||||||
|
for (int j = 0; j < a.pointsX().length; j++) {
|
||||||
|
float dx = wx - a.pointsX()[j];
|
||||||
|
float dz = wz - a.pointsZ()[j];
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < bestDist2) { bestDist2 = d2; bestZone = i; bestVert = j; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (bestZone >= 0) ? new int[]{bestZone, bestVert} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] findNearestEdge(float wx, float wz, float threshold) {
|
||||||
|
float bestDist = threshold;
|
||||||
|
int bestZone = -1, bestEdge = -1;
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedSoundArea a = areas.get(i);
|
||||||
|
int n = a.pointsX().length;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
int k = (j + 1) % n;
|
||||||
|
float dist = distPointToSegXZ(wx, wz,
|
||||||
|
a.pointsX()[j], a.pointsZ()[j], a.pointsX()[k], a.pointsZ()[k]);
|
||||||
|
if (dist < bestDist) { bestDist = dist; bestZone = i; bestEdge = j; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (bestZone >= 0) ? new int[]{bestZone, bestEdge} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float distPointToSegXZ(float px, float pz,
|
||||||
|
float ax, float az, float bx, float bz) {
|
||||||
|
float abx = bx - ax, abz = bz - az;
|
||||||
|
float len2 = abx * abx + abz * abz;
|
||||||
|
if (len2 < 1e-6f) {
|
||||||
|
float dx = px - ax, dz = pz - az;
|
||||||
|
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
float t = ((px - ax) * abx + (pz - az) * abz) / len2;
|
||||||
|
t = Math.max(0f, Math.min(1f, t));
|
||||||
|
float cx = ax + t * abx, cz = az + t * abz;
|
||||||
|
float dx = px - cx, dz = pz - cz;
|
||||||
|
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveSoundAreaVertex(int zoneIdx, int vertexIdx, float newX, float newZ) {
|
||||||
|
PlacedSoundArea a = areas.get(zoneIdx);
|
||||||
|
a.pointsX()[vertexIdx] = newX;
|
||||||
|
a.pointsZ()[vertexIdx] = newZ;
|
||||||
|
rebuildSoundAreaGeo(zoneIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildSoundAreaGeo(int zoneIdx) {
|
||||||
|
rootNode.detachChild(areaGeos.get(zoneIdx));
|
||||||
|
PlacedSoundArea a = areas.get(zoneIdx);
|
||||||
|
List<Float> xs = toList(a.pointsX());
|
||||||
|
List<Float> zs = toList(a.pointsZ());
|
||||||
|
ColorRGBA color = (zoneIdx == selectedIdx) ? COLOR_SELECTED : COLOR_NORMAL;
|
||||||
|
float lineW = (zoneIdx == selectedIdx) ? 8f : 4f;
|
||||||
|
Geometry geo = buildLineGeo("sound_area_" + zoneIdx, xs, zs, color, Mesh.Mode.LineLoop);
|
||||||
|
geo.getMaterial().getAdditionalRenderState().setLineWidth(lineW);
|
||||||
|
rootNode.attachChild(geo);
|
||||||
|
areaGeos.set(zoneIdx, geo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertSoundAreaVertexOnEdge(int zoneIdx, int edgeStartIdx, float newX, float newZ) {
|
||||||
|
PlacedSoundArea a = areas.get(zoneIdx);
|
||||||
|
int n = a.pointsX().length;
|
||||||
|
int ins = edgeStartIdx + 1;
|
||||||
|
float[] nx = new float[n + 1];
|
||||||
|
float[] nz = new float[n + 1];
|
||||||
|
System.arraycopy(a.pointsX(), 0, nx, 0, ins);
|
||||||
|
nx[ins] = newX;
|
||||||
|
System.arraycopy(a.pointsX(), ins, nx, ins + 1, n - ins);
|
||||||
|
System.arraycopy(a.pointsZ(), 0, nz, 0, ins);
|
||||||
|
nz[ins] = newZ;
|
||||||
|
System.arraycopy(a.pointsZ(), ins, nz, ins + 1, n - ins);
|
||||||
|
areas.set(zoneIdx, new PlacedSoundArea(nx, nz, a.soundPath(), a.volume(), a.crossfade()));
|
||||||
|
rebuildSoundAreaGeo(zoneIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleZoneDrag(SharedInput.ZoneDrag drag) {
|
||||||
|
float jmeX = drag.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||||
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
Vector3f pt = raycastAll(ray);
|
||||||
|
|
||||||
|
switch (drag.phase()) {
|
||||||
|
case SharedInput.ZoneDrag.START -> {
|
||||||
|
draggedZoneIdx = -1;
|
||||||
|
if (placing || pt == null) break;
|
||||||
|
int[] found = findNearestVertex(pt.x, pt.z, SNAP_DIST);
|
||||||
|
if (found != null) {
|
||||||
|
draggedZoneIdx = found[0];
|
||||||
|
draggedVertexIdx = found[1];
|
||||||
|
draggedOrigX = areas.get(draggedZoneIdx).pointsX()[draggedVertexIdx];
|
||||||
|
draggedOrigZ = areas.get(draggedZoneIdx).pointsZ()[draggedVertexIdx];
|
||||||
|
} else {
|
||||||
|
int[] edgeHit = findNearestEdge(pt.x, pt.z, 3f);
|
||||||
|
if (edgeHit != null) {
|
||||||
|
insertEdgeVertexInAllZones(pt.x, pt.z, 3f);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.insertSharedEdgeVertex(pt.x, pt.z);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.insertSharedEdgeVertex(pt.x, pt.z);
|
||||||
|
}
|
||||||
|
draggedZoneIdx = edgeHit[0];
|
||||||
|
draggedVertexIdx = edgeHit[1] + 1;
|
||||||
|
draggedOrigX = pt.x;
|
||||||
|
draggedOrigZ = pt.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SharedInput.ZoneDrag.MOVE -> {
|
||||||
|
if (draggedZoneIdx < 0 || pt == null) break;
|
||||||
|
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
}
|
||||||
|
draggedOrigX = pt.x;
|
||||||
|
draggedOrigZ = pt.z;
|
||||||
|
}
|
||||||
|
case SharedInput.ZoneDrag.END -> {
|
||||||
|
if (draggedZoneIdx >= 0 && pt != null) {
|
||||||
|
moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
if (input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||||
|
AreaState as = getStateManager().getState(AreaState.class);
|
||||||
|
if (as != null) as.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||||
|
if (lzs != null) lzs.moveMatchingVertices(draggedOrigX, draggedOrigZ, pt.x, pt.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draggedZoneIdx = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertSharedEdgeVertex(float wx, float wz) {
|
||||||
|
insertEdgeVertexInAllZones(wx, wz, 2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertEdgeVertexInAllZones(float wx, float wz, float threshold) {
|
||||||
|
float eps2 = 0.05f * 0.05f;
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedSoundArea a = areas.get(i);
|
||||||
|
float[] xs = a.pointsX(), zs = a.pointsZ();
|
||||||
|
int n = xs.length;
|
||||||
|
boolean hasVertex = false;
|
||||||
|
for (int v = 0; v < n; v++) {
|
||||||
|
float dx = xs[v] - wx, dz = zs[v] - wz;
|
||||||
|
if (dx * dx + dz * dz < eps2) { hasVertex = true; break; }
|
||||||
|
}
|
||||||
|
if (hasVertex) continue;
|
||||||
|
float bestDist = threshold;
|
||||||
|
int bestEdge = -1;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
int k = (j + 1) % n;
|
||||||
|
float dist = distPointToSegXZ(wx, wz, xs[j], zs[j], xs[k], zs[k]);
|
||||||
|
if (dist < bestDist) { bestDist = dist; bestEdge = j; }
|
||||||
|
}
|
||||||
|
if (bestEdge >= 0) insertSoundAreaVertexOnEdge(i, bestEdge, wx, wz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveMatchingVertices(float origX, float origZ, float newX, float newZ) {
|
||||||
|
float eps2 = 0.01f;
|
||||||
|
for (int i = 0; i < areas.size(); i++) {
|
||||||
|
PlacedSoundArea a = areas.get(i);
|
||||||
|
boolean changed = false;
|
||||||
|
for (int j = 0; j < a.pointsX().length; j++) {
|
||||||
|
float dx = a.pointsX()[j] - origX;
|
||||||
|
float dz = a.pointsZ()[j] - origZ;
|
||||||
|
if (dx * dx + dz * dz < eps2) {
|
||||||
|
a.pointsX()[j] = newX;
|
||||||
|
a.pointsZ()[j] = newZ;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) rebuildSoundAreaGeo(i);
|
||||||
}
|
}
|
||||||
return new float[]{bx, bz};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closePoly() {
|
private void closePoly() {
|
||||||
@@ -228,6 +546,7 @@ public class SoundAreaState extends BaseAppState {
|
|||||||
currZ.clear();
|
currZ.clear();
|
||||||
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
||||||
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||||
|
deselectVertex();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void undoLastPoint() {
|
private void undoLastPoint() {
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ public class ViewportController {
|
|||||||
private boolean pathNetDragging;
|
private boolean pathNetDragging;
|
||||||
private boolean pathNetRightPress;
|
private boolean pathNetRightPress;
|
||||||
|
|
||||||
|
// Zonen-Drag-Tracking
|
||||||
|
private float zonePressX, zonePressY;
|
||||||
|
private boolean zonePressPending;
|
||||||
|
private boolean zoneDragging;
|
||||||
|
|
||||||
public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) {
|
public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.initialImage = jfxImage;
|
this.initialImage = jfxImage;
|
||||||
@@ -156,6 +161,18 @@ public class ViewportController {
|
|||||||
return;
|
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
|
if (input.activeLayer == SharedInput.LAYER_STONE
|
||||||
&& input.stoneTool.brushRadius.getValue() == 1.0) {
|
&& input.stoneTool.brushRadius.getValue() == 1.0) {
|
||||||
// Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer
|
// Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer
|
||||||
@@ -250,6 +267,22 @@ public class ViewportController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isZoneLayer() && e.isPrimaryButtonDown()) {
|
||||||
|
if (zonePressPending) {
|
||||||
|
float ddx = (float)(e.getX() - zonePressX);
|
||||||
|
float ddy = (float)(e.getY() - zonePressY);
|
||||||
|
if (ddx * ddx + ddy * ddy > 16f) {
|
||||||
|
submitZoneDrag(zonePressX, zonePressY, SharedInput.ZoneDrag.START);
|
||||||
|
zoneDragging = true;
|
||||||
|
zonePressPending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (zoneDragging) {
|
||||||
|
submitZoneDrag((float) e.getX(), (float) e.getY(), SharedInput.ZoneDrag.MOVE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Play-Tool EDIT: Drag-Events an JME3 weiterleiten
|
// Play-Tool EDIT: Drag-Events an JME3 weiterleiten
|
||||||
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|
||||||
&& input.playToolMode == SharedInput.PlayToolMode.EDIT
|
&& input.playToolMode == SharedInput.PlayToolMode.EDIT
|
||||||
@@ -281,6 +314,15 @@ public class ViewportController {
|
|||||||
pathNetPressPending = false;
|
pathNetPressPending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isZoneLayer()) {
|
||||||
|
if (zoneDragging) {
|
||||||
|
submitZoneDrag((float) e.getX(), (float) e.getY(), SharedInput.ZoneDrag.END);
|
||||||
|
zoneDragging = false;
|
||||||
|
} else if (zonePressPending) {
|
||||||
|
submitEdit(zonePressX, zonePressY, +1);
|
||||||
|
zonePressPending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (input.activeLayer == SharedInput.LAYER_WATER) {
|
if (input.activeLayer == SharedInput.LAYER_WATER) {
|
||||||
input.waterDragEnd = true;
|
input.waterDragEnd = true;
|
||||||
}
|
}
|
||||||
@@ -361,6 +403,28 @@ public class ViewportController {
|
|||||||
if (editTimer != null) { editTimer.stop(); editTimer = null; }
|
if (editTimer != null) { editTimer.stop(); editTimer = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isZoneLayer() {
|
||||||
|
return input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|
||||||
|
|| input.activeLayer == SharedInput.LAYER_AREAS
|
||||||
|
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|
||||||
|
|| input.activeLayer == SharedInput.LAYER_ZONEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitZoneDrag(float x, float y, int phase) {
|
||||||
|
SharedInput.ZoneDrag drag = new SharedInput.ZoneDrag(x, y, phase);
|
||||||
|
switch (input.activeLayer) {
|
||||||
|
case SharedInput.LAYER_SOUND_AREAS -> input.soundAreaDragQueue.offer(drag);
|
||||||
|
case SharedInput.LAYER_AREAS -> input.areaDragQueue.offer(drag);
|
||||||
|
case SharedInput.LAYER_LOCATION_ZONES -> input.locationZoneDragQueue.offer(drag);
|
||||||
|
case SharedInput.LAYER_ZONEN -> {
|
||||||
|
input.soundAreaDragQueue.offer(drag);
|
||||||
|
input.areaDragQueue.offer(drag);
|
||||||
|
input.locationZoneDragQueue.offer(drag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (phase == SharedInput.ZoneDrag.END) scheduleAutoSave.run();
|
||||||
|
}
|
||||||
|
|
||||||
private void submitEdit(double x, double y, int action) {
|
private void submitEdit(double x, double y, int action) {
|
||||||
switch (input.activeLayer) {
|
switch (input.activeLayer) {
|
||||||
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
|
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
# polygon areaId triggersJson labelX labelZ
|
# polygon areaId triggersJson labelX labelZ
|
||||||
255.852,-828.388;281.186,-815.228;272.373,-782.936;237.423,-778.259;223.212,-812.720;227.232,-839.121 area.strand [] NaN NaN
|
255.852,-828.388;281.186,-815.228;272.373,-782.936;256.916,-772.353;237.419,-778.301;223.212,-812.720;227.232,-839.121 area.strand [] NaN NaN
|
||||||
|
270.596,-733.012;256.916,-772.353;237.423,-778.259;239.408,-750.931 [] NaN NaN
|
||||||
|
|||||||
Reference in New Issue
Block a user