Wege System erweitert

This commit is contained in:
2026-09-13 20:39:07 +02:00
parent 320b0834b9
commit 6cf7234ab1
5 changed files with 234 additions and 65 deletions

View File

@@ -1048,6 +1048,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;

View File

@@ -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_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_RADIUS = 0.15f;
private static final float EDGE_HIT_THRESHOLD = 3f;
private final SharedInput input;
private Camera cam;
@@ -60,13 +57,13 @@ public class PathNetworkEditorState extends BaseAppState {
private Node rootNode;
private TerrainQuad terrain;
private final PathNetwork network = new PathNetwork();
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");
private final Node netRoot = new Node("pathnet_root");
private final Node nodeRoot = new Node("pathnet_nodes");
private final Node edgeRoot = new Node("pathnet_edges");
private Material matDefault;
private Material matSelected;
@@ -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,35 +157,37 @@ 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
WorldPoint pt = terrainHit(ray);
if (pt == null) return;
PathNode newNode = new PathNode(pt);
network.addNode(newNode);
if (selectedNode != null) {
network.addEdge(selectedNode.getUuid(), newNode.getUuid());
}
select(newNode);
saveAndNotify();
rebuildVisuals();
return;
}
WorldPoint pt = terrainHit(ray);
if (pt == null) return;
// 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());
}
select(newNode);
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() {

View File

@@ -24,6 +24,12 @@ 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;
public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) {
this.input = input;
this.initialImage = jfxImage;
@@ -140,12 +146,12 @@ 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;
}
@@ -225,6 +231,25 @@ 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;
}
// Play-Tool EDIT: Drag-Events an JME3 weiterleiten
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
&& input.playToolMode == SharedInput.PlayToolMode.EDIT
@@ -245,6 +270,17 @@ 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 (input.activeLayer == SharedInput.LAYER_WATER) {
input.waterDragEnd = true;
}