Kanten zwischen Gateways umgehen die Ziel-Raute (#8)

- orthogonal_points(): Liegt bei Gateway -> Gateway der Stub des Starts
  schon jenseits des Ziel-Anschlusspunkts (z. B. Südpunkt unten, Ziel
  höher mit Nordpunkt), führt die Kante seitlich an der Ziel-Raute vorbei.
  Gleiches für den waagrechten Spiegelfall (Ost -> West).
- _ausweichachse(): Die Ausweichachse liegt im freien Gang direkt vor der
  Ziel-Spalte (AUSWEICHABSTAND 85), nicht in der Mitte zwischen Quelle und
  Ziel – dort stand im Referenzmodell die Aufgabe S43.
- Referenzmodell neu erzeugt: fünf Kanten korrigiert (vorher liefen alle
  fünf durch ein Gateway, gemeldet war nur S12->S13), dabei verschwindet
  auch die Kreuzung Flow_13/S40. Syntaxbeispiele unverändert.
- xfail für referenz_gewannenfestlegung entfernt; drei neue Tests in
  test_md_to_bpmn.py.
- Fallstricke: Ausweichachse; Strukturtests melden nur die erste Kreuzung.
- Tests: python -m pytest -q -> 92 bestanden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 11:09:43 +02:00
co-authored by Claude Opus 5.5
parent 6050ce3296
commit 30b47a0fd1
6 changed files with 97 additions and 13 deletions
@@ -486,7 +486,8 @@
<bpmndi:BPMNEdge id="Flow_13_di" bpmnElement="Flow_13">
<di:waypoint x="2225" y="195"/>
<di:waypoint x="2225" y="220"/>
<di:waypoint x="2415" y="220"/>
<di:waypoint x="2330" y="220"/>
<di:waypoint x="2330" y="35"/>
<di:waypoint x="2415" y="35"/>
<di:waypoint x="2415" y="60"/>
<bpmndi:BPMNLabel>
@@ -541,7 +542,8 @@
<bpmndi:BPMNEdge id="Flow_20_di" bpmnElement="Flow_20">
<di:waypoint x="3175" y="110"/>
<di:waypoint x="3175" y="135"/>
<di:waypoint x="3365" y="135"/>
<di:waypoint x="3280" y="135"/>
<di:waypoint x="3280" y="35"/>
<di:waypoint x="3365" y="35"/>
<di:waypoint x="3365" y="60"/>
<bpmndi:BPMNLabel>
@@ -597,7 +599,8 @@
<bpmndi:BPMNEdge id="Flow_27_di" bpmnElement="Flow_27">
<di:waypoint x="3935" y="195"/>
<di:waypoint x="3935" y="220"/>
<di:waypoint x="4125" y="220"/>
<di:waypoint x="4040" y="220"/>
<di:waypoint x="4040" y="120"/>
<di:waypoint x="4125" y="120"/>
<di:waypoint x="4125" y="145"/>
</bpmndi:BPMNEdge>
@@ -641,7 +644,8 @@
<bpmndi:BPMNEdge id="Flow_33_di" bpmnElement="Flow_33">
<di:waypoint x="4125" y="195"/>
<di:waypoint x="4125" y="220"/>
<di:waypoint x="4315" y="220"/>
<di:waypoint x="4230" y="220"/>
<di:waypoint x="4230" y="120"/>
<di:waypoint x="4315" y="120"/>
<di:waypoint x="4315" y="145"/>
</bpmndi:BPMNEdge>
@@ -658,7 +662,8 @@
<bpmndi:BPMNEdge id="Flow_36_di" bpmnElement="Flow_36">
<di:waypoint x="2795" y="280"/>
<di:waypoint x="2795" y="305"/>
<di:waypoint x="3175" y="305"/>
<di:waypoint x="3090" y="305"/>
<di:waypoint x="3090" y="205"/>
<di:waypoint x="3175" y="205"/>
<di:waypoint x="3175" y="230"/>
<bpmndi:BPMNLabel>
+28 -1
View File
@@ -580,6 +580,21 @@ def _orthogonal_middle(ox, oy, src_side, tx, ty, tgt_side):
return [(ox, oy), (tx, oy), (tx, ty)]
# Abstand der Ausweichachse von der Mitte der Ziel-Spalte bzw. -Zeile: halbe
# Breite der größten Elemente (Aufgabe 100x80) plus halber kleinster Gang
# zwischen zwei Spalten/Zeilen (170 - 100 = 70). Der Gang direkt vor dem Ziel
# ist damit in beiden Layouts frei; die Mitte zwischen Quelle und Ziel nicht,
# dort kann eine Spalte mit einer Aufgabe liegen.
AUSWEICHABSTAND = 85
def _ausweichachse(quelle, ziel):
"""Koordinate im freien Gang vor der Ziel-Spalte (bzw. -Zeile), auf der Seite
der Quelle: dort läuft eine Kante an der Ziel-Raute vorbei, ohne ein anderes
Element zu schneiden."""
return ziel + AUSWEICHABSTAND if quelle > ziel else ziel - AUSWEICHABSTAND
def orthogonal_points(ox, oy, src_side, tx, ty, tgt_side,
src_is_gateway=False, tgt_is_gateway=False):
"""Baut einen rechtwinkligen Kantenverlauf zwischen zwei Anschlusspunkten, der
@@ -627,7 +642,19 @@ def orthogonal_points(ox, oy, src_side, tx, ty, tgt_side,
else:
# Beide Enden sind Rauten: von der Stub-Achse des Starts direkt auf
# die Stub-Achse des Ziels schwenken, jede nur einmal verlassen.
if src_axis == 'v':
# Liegt der Stub des Starts schon jenseits des Ziel-Anschlusspunkts
# (z. B. Ankunft am Nordpunkt eines höher liegenden Ziels), liefe das
# letzte gerade Stück durch die Ziel-Raute - dann seitlich daran
# vorbei über eine freie Ausweichachse (Issue #8).
if src_axis == 'v' and tgt_axis == 'v' and (
(tgt_side == 'N' and sy > ty) or (tgt_side == 'S' and sy < ty)):
mx = _ausweichachse(sx, ex)
points = [(ox, oy), (sx, sy), (mx, sy), (mx, ey), (ex, ey), (tx, ty)]
elif src_axis == 'h' and tgt_axis == 'h' and (
(tgt_side == 'W' and sx > tx) or (tgt_side == 'E' and sx < tx)):
my = _ausweichachse(sy, ey)
points = [(ox, oy), (sx, sy), (sx, my), (ex, my), (ex, ey), (tx, ty)]
elif src_axis == 'v':
points = [(ox, oy), (sx, sy), (ex, sy), (ex, ey), (tx, ty)]
else:
points = [(ox, oy), (sx, sy), (sx, ey), (ex, ey), (tx, ty)]
+45
View File
@@ -353,6 +353,51 @@ def test_kante_fuehrt_nicht_durchs_innere_eines_gateways():
assert _kante_kreuzt_gateway(defs, m.NS) is None
def _streckenzug_kreuzt_raute(punkte, raute):
"""True, wenn ein Stück des Streckenzugs durchs Innere der Raute (x, y, w, h) läuft."""
for (x1, y1), (x2, y2) in zip(punkte, punkte[1:]):
for s in range(41):
t = s / 40
if _liegt_in_rautenflaeche(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t, *raute):
return True
return False
def test_kante_zwischen_gateways_zum_nordpunkt_eines_hoeheren_ziels_umgeht_die_raute():
"""Prüft, dass eine Kante vom Südpunkt eines Gateways zum Nordpunkt eines höher liegenden Gateways
seitlich an der Ziel-Raute vorbei nach oben führt statt senkrecht durch sie hindurch (Issue #8)."""
quelle, ziel = (2200, 145, 50, 50), (2390, 60, 50, 50) # Lage wie S12 -> S13 im Referenzmodell
punkte = m.orthogonal_points(2225, 195, 'S', 2415, 60, 'N', src_is_gateway=True, tgt_is_gateway=True)
assert punkte[0] == (2225, 195) and punkte[-1] == (2415, 60)
assert not _streckenzug_kreuzt_raute(punkte, ziel)
assert not _streckenzug_kreuzt_raute(punkte, quelle)
assert all(a[0] == b[0] or a[1] == b[1] for a, b in zip(punkte, punkte[1:])), "nicht rechtwinklig"
def test_ausweichweg_zwischen_gateways_meidet_die_spalte_dazwischen():
"""Prüft, dass der seitliche Umweg um die Ziel-Raute im freien Gang direkt vor der Ziel-Spalte liegt und
nicht mitten zwischen Quelle und Ziel, wo in einer Spalte dazwischen eine Aufgabe stehen kann."""
aufgabe, ziel = (2960, 215, 100, 80), (3150, 230, 50, 50) # Lage wie S43 und S44 im Referenzmodell
punkte = m.orthogonal_points(2795, 280, 'S', 3175, 230, 'N', src_is_gateway=True, tgt_is_gateway=True)
assert not _streckenzug_kreuzt_raute(punkte, ziel)
x, y, w, h = aufgabe
for (x1, y1), (x2, y2) in zip(punkte, punkte[1:]):
for s in range(41):
px, py = x1 + (x2 - x1) * s / 40, y1 + (y2 - y1) * s / 40
assert not (x < px < x + w and y < py < y + h), f"Kante läuft bei ({px:g}, {py:g}) durch die Aufgabe"
def test_kante_zwischen_gateways_zum_westpunkt_eines_linken_ziels_umgeht_die_raute():
"""Prüft den waagrechten Spiegelfall: vom Ostpunkt eines Gateways zum Westpunkt eines weiter links
liegenden Gateways führt die Kante ober- oder unterhalb an der Ziel-Raute vorbei."""
quelle, ziel = (400, 300, 50, 50), (200, 100, 50, 50)
punkte = m.orthogonal_points(450, 325, 'E', 200, 125, 'W', src_is_gateway=True, tgt_is_gateway=True)
assert punkte[0] == (450, 325) and punkte[-1] == (200, 125)
assert not _streckenzug_kreuzt_raute(punkte, ziel)
assert not _streckenzug_kreuzt_raute(punkte, quelle)
assert all(a[0] == b[0] or a[1] == b[1] for a, b in zip(punkte, punkte[1:])), "nicht rechtwinklig"
def test_ausgabe_ist_wohlgeformtes_xml(tmp_path):
"""Prüft, dass die erzeugte .bpmn-Datei sich als valides XML parsen laesst."""
src = tmp_path / 'prozess.md'
+4
View File
@@ -31,6 +31,10 @@ Skills sind übernommen, alles QGIS- und Plugin-Spezifische ist entfernt. Kern i
## Erledigt
- 25.09.2026 **Keine Kanten mehr durch Gateways im Referenzmodell** (#8): Gateway → Gateway mit Ziel hinter
dem Start-Stub weicht im Gang vor der Ziel-Spalte aus; fünf Kanten der Gewannenfestlegung korrigiert, `xfail`
entfernt. Offen: Rücksprungkanten und Nachrichtenflüsse durch Aufgaben, Beschriftungsüberlappungen.
- 25.09.2026 **Prozessbibliothek als eigenes Repository** (#9, ADR 0004): `prozesse/` ist aus Projektwissen
entfernt, die Modelle liegen im Klon `Prozessbibliothek/` (`workspace.toml`). `pruefe_prozesse.py` und
`einlesen.py` suchen dort; Regeln in `docs/leitfaden/08_prozessbibliothek.md`.
+9 -4
View File
@@ -5,10 +5,15 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
## BPMN und PICTURE (Skill sn-bpmn, md_to_bpmn.py)
- **Kanten durch Gateways sind noch nicht ganz behoben.** Im Referenzmodell Gewannenfestlegung läuft die Kante
S12→S13 (Gateway → Gateway, Ziel oberhalb der Quelle, Ankunft am Nordpunkt) senkrecht durch die Raute von S13.
Der Fix vom 21.09.2026 deckt diese Lage nicht ab. Erfasst als Issue #8 und als `xfail` in
`tests/test_referenzmodelle.py`. (23.09.2026)
- **Gateway → Gateway mit Ziel „hinter“ dem Start-Stub braucht einen seitlichen Umweg.** Liegt der Stub des
Start-Gateways schon jenseits des Ziel-Anschlusspunkts (z. B. Südpunkt unten, Ziel höher mit Nordpunkt), läuft
das letzte gerade Stück durch die Ziel-Raute. `orthogonal_points()` weicht dann über `_ausweichachse()` aus.
Die Achse liegt **im Gang direkt vor der Ziel-Spalte** (Abstand 85 zur Mitte), **nicht in der Mitte zwischen
Quelle und Ziel**: Liegen die beiden zwei Spalten auseinander, steht dort eine Aufgabe (im Referenzmodell S43).
(#8, 25.09.2026)
- **Die Strukturtests melden nur die erste Kreuzung.** `_kante_kreuzt_gateway()` bricht beim ersten Fund ab. Im
Referenzmodell stand deshalb nur S12→S13 im Issue, tatsächlich liefen fünf Kanten durch Gateways. Nach einem
Layout-Fix alle Kanten gegen alle Elemente prüfen, nicht nur den gemeldeten Fall. (#8, 25.09.2026)
- **Zusammenführende Gateways brauchen eine Beschriftung mit Zuständigkeit**, sonst landen sie in der Lane
„Nicht zugeordnet“ (`split_lane()` braucht Text nach dem Doppelpunkt). In BPMN bleiben Zusammenführungen
üblicherweise unbeschriftet – ein Fall für eine spätere Konverteränderung. (23.09.2026, Referenzmodell)
+1 -3
View File
@@ -21,9 +21,7 @@ GATEWAYS = ("exclusiveGateway", "inclusiveGateway", "parallelGateway", "eventBas
# Bekannte, noch nicht behobene Konverterfehler je Referenzmodell. strict=True: Sobald der Fehler behoben ist,
# schlägt der Test als XPASS fehl – dann den Eintrag hier entfernen. Nie verwenden, um neue Fehler zu verstecken.
BEKANNTE_KANTEN_DURCH_GATEWAY = {
"referenz_gewannenfestlegung": "Issue #8: Kante S12→S13 (Ja) läuft senkrecht durch die Raute von S13",
}
BEKANNTE_KANTEN_DURCH_GATEWAY = {}
def mit_bekannten_fehlern(bekannt):