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_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() {

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;
}

View File

@@ -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)");
};
}
}

View File

@@ -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