From 1ef97c677afc0c6532b8115f80eadc2a9fbcbaf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 05:06:45 +0000 Subject: [PATCH] Add signal-side selection when placing signals on an edge Signals are placed via the Signal tool by clicking near a track edge; they are linear-referenced to the nearest edge at the clicked position. Previously the side was hardcoded to the right. Add a "Signalseite" (Links/Rechts) toggle to the toolbar so the side is a deliberate choice for newly created signals (default: Rechts). The controller reads the selected side and passes it to TrackNetwork.addSignal; the status bar reflects the current side. Reuses the existing, unit-tested model (EdgeRef/Side/addSignal); no domain logic changes. Also document the feature under Features/. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SXy8RSYYvXkGbUyDmiLMfb --- ...gnale-erstellen-und-gleiskante-zuordnen.md | 72 +++++++++++++++++++ .../vibeapp/view/NetworkEditorController.java | 28 +++++++- .../com/example/vibeapp/network-editor.fxml | 5 ++ 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 Features/signale-erstellen-und-gleiskante-zuordnen.md diff --git a/Features/signale-erstellen-und-gleiskante-zuordnen.md b/Features/signale-erstellen-und-gleiskante-zuordnen.md new file mode 100644 index 0000000..c140fe3 --- /dev/null +++ b/Features/signale-erstellen-und-gleiskante-zuordnen.md @@ -0,0 +1,72 @@ +# Feature: Signale erstellen und einer Gleiskante zuordnen + +## Übersicht + +- **Status:** Abgeschlossen +- **Autor:** Robin Radica +- **Erstellt am:** 2026-07-09 +- **Letzte Änderung:** 2026-07-09 + +## Ziel / Problemstellung + +Der Editor soll es erlauben, **Signale** als Fachdatum zu erstellen und sie einer +**Gleiskante** (`TrackEdge`) zuzuordnen. Ein Signal hat keine eigenen Koordinaten, +sondern wird per **Linear Referencing** über eine `EdgeRef` an einer Kante, +einer relativen Position (0.0 … 1.0) und einer **Seite** (Links/Rechts) verankert. +Dadurch bleibt es korrekt, wenn Knoten verschoben werden. + +Bisher wurde ein Signal immer auf der rechten Seite abgelegt. Anwender:innen +sollen die Seite beim Platzieren bewusst wählen können. + +## Anforderungen + +### Funktionale Anforderungen + +- [x] Über das Werkzeug **Signal** in der Toolbar lässt sich der Signal-Modus aktivieren. +- [x] Ein Klick nahe einer Gleiskante erzeugt ein Signal und ordnet es der + **nächstgelegenen** Kante an der angeklickten Position zu. +- [x] Klicks, die keiner Kante nahe genug sind, erzeugen kein Signal. +- [x] Die **Seite** (Links/Rechts) ist über einen Toolbar-Schalter „Signalseite“ + wählbar und gilt für neu erstellte Signale (Standard: Rechts). +- [x] Die Statuszeile zeigt die aktuell gewählte Signalseite an. +- [x] Signale werden im Canvas dargestellt (Markierung + Label seitlich der Kante). + +### Nicht-funktionale Anforderungen + +- [x] Domänenlogik (`model`, `geometry`) bleibt frei von JavaFX-Importen und ist + unit-getestet; die Seitenwahl ist reine View-Verdrahtung über bestehende, + getestete Modelloperationen. +- [x] Architektur-Guardrails werden eingehalten (max. 3 Parameter, max. 500 LOC/Klasse). + +## Nutzer / Zielgruppe + +Planer:innen, die ein schematisches Gleisnetz aufbauen und Signale als Fachdaten +an Gleiskanten anbringen. + +## Ein- und Ausgaben + +- **Eingaben:** Werkzeug „Signal“, gewählte Signalseite (Links/Rechts), + Mausklick auf/nahe einer Gleiskante. +- **Ausgaben:** Neues `Signal` im `TrackNetwork`, referenziert über eine `EdgeRef` + (Kante + Position + Seite); Darstellung im Canvas; aktualisierte Statuszeile. + +## Akzeptanzkriterien + +- [x] Mit aktivem Signal-Werkzeug erzeugt ein Klick nahe einer Kante genau ein + Signal auf dieser Kante. +- [x] Ist „Links“ bzw. „Rechts“ gewählt, sitzt das neue Signal auf der jeweiligen + Seite der Kante. +- [x] Ein Klick fernab jeder Kante erzeugt kein Signal. +- [x] `mvn verify` läuft grün (Kompilierung, Tests, Static Analysis). + +## Offene Fragen / Annahmen + +- Umbenennen, Verschieben und Löschen bestehender Signale sowie das Zuordnen zu + einer anderen Kante sind nicht Teil dieses Features und können als Folge-Feature + ergänzt werden. + +## Referenzen + +- Domänenmodell: `Signal`, `EdgeRef`, `Side`, `TrackNetwork.addSignal(...)` +- Geometrie: `Geometry.nearestParam`, `Geometry.distanceToSegment`, `Geometry.offsetPoint` +- View: `NetworkEditorController` (Werkzeug- und Seitenwahl), `NetworkRenderer` (Darstellung) diff --git a/src/main/java/com/example/vibeapp/view/NetworkEditorController.java b/src/main/java/com/example/vibeapp/view/NetworkEditorController.java index ed8bd35..c135a9c 100644 --- a/src/main/java/com/example/vibeapp/view/NetworkEditorController.java +++ b/src/main/java/com/example/vibeapp/view/NetworkEditorController.java @@ -43,6 +43,9 @@ private enum Tool { SELECT, NODE, EDGE, SIGNAL } @FXML private ToggleButton nodeButton; @FXML private ToggleButton edgeButton; @FXML private ToggleButton signalButton; + @FXML private ToggleGroup sideGroup; + @FXML private ToggleButton leftSideButton; + @FXML private ToggleButton rightSideButton; @FXML private Label statusLabel; private TrackNetwork network = SampleNetworks.switzerland(); @@ -77,6 +80,11 @@ private void initialize() { toolGroup.selectedToggleProperty().addListener((obs, old, now) -> onToolChanged(old, now)); toolGroup.selectToggle(selectButton); + leftSideButton.setUserData(Side.LEFT); + rightSideButton.setUserData(Side.RIGHT); + sideGroup.selectedToggleProperty().addListener((obs, old, now) -> onSideChanged(old, now)); + sideGroup.selectToggle(rightSideButton); + canvas.setOnMousePressed(this::onMousePressed); canvas.setOnMouseDragged(this::onMouseDragged); canvas.setOnMouseReleased(event -> draggedNode = null); @@ -98,6 +106,21 @@ private void onToolChanged(Toggle old, Toggle now) { redraw(); } + private void onSideChanged(Toggle old, Toggle now) { + // Never allow a fully deselected side; keep the last choice instead. + if (now == null) { + sideGroup.selectToggle(old); + return; + } + updateStatus(); + } + + /** The side newly placed signals are attached to, driven by the toolbar. */ + private Side selectedSide() { + Toggle selected = sideGroup.getSelectedToggle(); + return selected != null ? (Side) selected.getUserData() : Side.RIGHT; + } + private void onMousePressed(MouseEvent event) { // Ignore secondary/middle clicks so they can't create elements by accident. if (event.getButton() != MouseButton.PRIMARY) { @@ -171,7 +194,7 @@ private void handleSignalClick(double x, double y) { } if (closestEdge != null) { - network.addSignal(closestEdge.id(), closestParam, Side.RIGHT); + network.addSignal(closestEdge.id(), closestParam, selectedSide()); } } @@ -221,7 +244,8 @@ private void updateStatus() { case EDGE -> pendingEdgeNode == null ? "Kante: Startknoten anklicken." : "Kante: Zielknoten anklicken (Start: " + pendingEdgeNode.label() + ")."; - case SIGNAL -> "Signal: Auf eine Gleiskante klicken, um ein Signal zu setzen."; + case SIGNAL -> "Signal: Auf eine Gleiskante klicken, um ein Signal zu setzen (Seite: " + + (selectedSide() == Side.LEFT ? "Links" : "Rechts") + ")."; }; statusLabel.setText(String.format("%s | Knoten: %d Kanten: %d Signale: %d", hint, network.nodes().size(), network.edges().size(), network.signals().size())); diff --git a/src/main/resources/com/example/vibeapp/network-editor.fxml b/src/main/resources/com/example/vibeapp/network-editor.fxml index a982fc1..792251f 100644 --- a/src/main/resources/com/example/vibeapp/network-editor.fxml +++ b/src/main/resources/com/example/vibeapp/network-editor.fxml @@ -17,6 +17,7 @@ + @@ -26,6 +27,10 @@ +