Nachrichtenflüsse senkrecht und an Elementen vorbei führen (#13)
- Nachrichtenflüsse enden senkrecht unter ihrem Startpunkt an der Oberkante des externen Pools statt an einem verteilten Punkt (keine schrägen Linien mehr); die Anschlussverteilung am externen Pool entfällt. - Steht unter dem Schritt ein Element: kurz nach unten (NACHRICHT_STUB), dann in einen freien Gang – mit Lanes neben der Spalte, versetzt zu den Rücksprungkanten (NACHRICHT_VERSATZ); ohne Lanes rechts neben allen Elementen, der Pool wird dafür breiter. - beispiel_lanes und Referenzmodell neu erzeugt: die beiden bekannten Kreuzungen (S5, S24) sind behoben, BEKANNTE_KREUZUNGEN ist leer. - Vier neue Tests (rechtwinklig, keine Kreuzung; je mit und ohne Lanes). - Belastungstest (342 Varianten): Kreuzungen durch Nachrichtenflüsse vorher 182, jetzt 0; schräge Nachrichtenflüsse vorher 600, jetzt 0. - Tests: python -m pytest -q -> 106 bestanden, 1 übersprungen (leere Parameterliste von test_bekannte_kreuzung_ist_behoben). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -150,6 +150,11 @@ Nachrichtenflüsse sind nur zwischen Pools erlaubt (siehe `Modellierregeln.md`)
|
|||||||
– das Ziel muss immer ein per `Extern:` deklarierter Name sein, nie eine
|
– das Ziel muss immer ein per `Extern:` deklarierter Name sein, nie eine
|
||||||
Schritt-ID im selben Pool.
|
Schritt-ID im selben Pool.
|
||||||
|
|
||||||
|
Der Konverter zeichnet Nachrichtenflüsse senkrecht vom Schritt hinunter zum
|
||||||
|
externen Pool. Steht darunter ein anderes Element, weicht der Fluss vorher in
|
||||||
|
einen freien Gang aus (mit Lanes neben der Spalte, ohne Lanes rechts neben dem
|
||||||
|
Diagramm).
|
||||||
|
|
||||||
### Verzweigungen / Sprünge
|
### Verzweigungen / Sprünge
|
||||||
|
|
||||||
Unter einem Schritt können beliebig viele Pfeil-Zeilen stehen:
|
Unter einem Schritt können beliebig viele Pfeil-Zeilen stehen:
|
||||||
|
|||||||
@@ -164,16 +164,18 @@
|
|||||||
<di:waypoint x="1250" y="95"/>
|
<di:waypoint x="1250" y="95"/>
|
||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
<bpmndi:BPMNEdge id="MsgFlow_1_di" bpmnElement="MsgFlow_1">
|
<bpmndi:BPMNEdge id="MsgFlow_1_di" bpmnElement="MsgFlow_1">
|
||||||
<di:waypoint x="341.5" y="810"/>
|
<di:waypoint x="128" y="810"/>
|
||||||
<di:waypoint x="128" y="113"/>
|
<di:waypoint x="128" y="113"/>
|
||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
<bpmndi:BPMNEdge id="MsgFlow_2_di" bpmnElement="MsgFlow_2">
|
<bpmndi:BPMNEdge id="MsgFlow_2_di" bpmnElement="MsgFlow_2">
|
||||||
<di:waypoint x="746.667" y="315"/>
|
<di:waypoint x="746.667" y="315"/>
|
||||||
<di:waypoint x="683" y="810"/>
|
<di:waypoint x="746.667" y="345"/>
|
||||||
|
<di:waypoint x="845" y="345"/>
|
||||||
|
<di:waypoint x="845" y="810"/>
|
||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
<bpmndi:BPMNEdge id="MsgFlow_3_di" bpmnElement="MsgFlow_3">
|
<bpmndi:BPMNEdge id="MsgFlow_3_di" bpmnElement="MsgFlow_3">
|
||||||
<di:waypoint x="1110" y="135"/>
|
<di:waypoint x="1110" y="135"/>
|
||||||
<di:waypoint x="1024.5" y="810"/>
|
<di:waypoint x="1110" y="810"/>
|
||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
</bpmndi:BPMNPlane>
|
</bpmndi:BPMNPlane>
|
||||||
</bpmndi:BPMNDiagram>
|
</bpmndi:BPMNDiagram>
|
||||||
|
|||||||
@@ -710,11 +710,13 @@
|
|||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
<bpmndi:BPMNEdge id="MsgFlow_1_di" bpmnElement="MsgFlow_1">
|
<bpmndi:BPMNEdge id="MsgFlow_1_di" bpmnElement="MsgFlow_1">
|
||||||
<di:waypoint x="730" y="210"/>
|
<di:waypoint x="730" y="210"/>
|
||||||
<di:waypoint x="1826.67" y="1120"/>
|
<di:waypoint x="730" y="1120"/>
|
||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
<bpmndi:BPMNEdge id="MsgFlow_2_di" bpmnElement="MsgFlow_2">
|
<bpmndi:BPMNEdge id="MsgFlow_2_di" bpmnElement="MsgFlow_2">
|
||||||
<di:waypoint x="3010" y="125"/>
|
<di:waypoint x="3010" y="125"/>
|
||||||
<di:waypoint x="3653.33" y="1120"/>
|
<di:waypoint x="3010" y="155"/>
|
||||||
|
<di:waypoint x="3125" y="155"/>
|
||||||
|
<di:waypoint x="3125" y="1120"/>
|
||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
</bpmndi:BPMNPlane>
|
</bpmndi:BPMNPlane>
|
||||||
</bpmndi:BPMNDiagram>
|
</bpmndi:BPMNDiagram>
|
||||||
|
|||||||
@@ -407,6 +407,11 @@ RUECKSPRUNG_STUB = 20
|
|||||||
# Ohne Lanes: Abstand des seitlichen Kanals vom äußersten Element (MARGIN 80 lässt
|
# Ohne Lanes: Abstand des seitlichen Kanals vom äußersten Element (MARGIN 80 lässt
|
||||||
# links Platz für mehrere Kanäle).
|
# links Platz für mehrere Kanäle).
|
||||||
RUECKSPRUNG_SEITE = 30
|
RUECKSPRUNG_SEITE = 30
|
||||||
|
# Nachrichtenflüsse (#13): senkrechtes Stück unter dem Schritt (länger als bei
|
||||||
|
# Rücksprüngen, damit sich die waagrechten Stücke nicht überdecken) und Versatz
|
||||||
|
# im Gang gegenüber den Rücksprungkanten.
|
||||||
|
NACHRICHT_STUB = 30
|
||||||
|
NACHRICHT_VERSATZ = 20
|
||||||
GROESSTE_BREITE = max(w for typ, (w, _) in DEFAULT_SIZE.items() if typ not in UNCONNECTED_TYPES)
|
GROESSTE_BREITE = max(w for typ, (w, _) in DEFAULT_SIZE.items() if typ not in UNCONNECTED_TYPES)
|
||||||
|
|
||||||
|
|
||||||
@@ -884,6 +889,16 @@ def build_bpmn(meta_md, steps, order, externals):
|
|||||||
|
|
||||||
pool_width = max((x_pos[sid] + bounds_of(sid)[2] for sid in order), default=400) + MARGIN
|
pool_width = max((x_pos[sid] + bounds_of(sid)[2] for sid in order), default=400) + MARGIN
|
||||||
back_flow_ids = [flow_id for flow_id, _, _, _, is_back in flows if is_back]
|
back_flow_ids = [flow_id for flow_id, _, _, _, is_back in flows if is_back]
|
||||||
|
msg_kanal_x = {}
|
||||||
|
if not has_lanes and msg_flow_records:
|
||||||
|
# Ohne Lanes steht unter jedem Schritt der nächste: Nachrichtenflüsse
|
||||||
|
# laufen rechts neben allen Elementen (und den Rücksprung-Kanälen)
|
||||||
|
# hinunter; der Pool wird so breit, dass diese Kanäle darin liegen (#13).
|
||||||
|
rechts_aussen = max(x_pos[s] + bounds_of(s)[2] for s in order if steps[s].type not in UNCONNECTED_TYPES)
|
||||||
|
basis = rechts_aussen + RUECKSPRUNG_SEITE + RUECKSPRUNG_ABSTAND * len(back_flow_ids)
|
||||||
|
for k, (mfid, _, _, _) in enumerate(msg_flow_records):
|
||||||
|
msg_kanal_x[mfid] = basis + RUECKSPRUNG_ABSTAND * k
|
||||||
|
pool_width = max(pool_width, max(msg_kanal_x.values()) + RUECKSPRUNG_SEITE)
|
||||||
if has_lanes:
|
if has_lanes:
|
||||||
lane_top, lane_height, pool_height = lane_geo
|
lane_top, lane_height, pool_height = lane_geo
|
||||||
if back_flow_ids:
|
if back_flow_ids:
|
||||||
@@ -979,7 +994,6 @@ def build_bpmn(meta_md, steps, order, externals):
|
|||||||
|
|
||||||
for mfid, sid, direction, target in msg_flow_records:
|
for mfid, sid, direction, target in msg_flow_records:
|
||||||
register_anchor(('n', sid), 'S', f'{mfid}_node')
|
register_anchor(('n', sid), 'S', f'{mfid}_node')
|
||||||
register_anchor(('x', target), 'N', f'{mfid}_ext')
|
|
||||||
|
|
||||||
def shape_kind_for(key):
|
def shape_kind_for(key):
|
||||||
kind, ident = key
|
kind, ident = key
|
||||||
@@ -1050,8 +1064,24 @@ def build_bpmn(meta_md, steps, order, externals):
|
|||||||
for mfid, sid, direction, target in msg_flow_records:
|
for mfid, sid, direction, target in msg_flow_records:
|
||||||
edge = ET.SubElement(plane, q('bpmndi', 'BPMNEdge'), {'id': f'{mfid}_di', 'bpmnElement': mfid})
|
edge = ET.SubElement(plane, q('bpmndi', 'BPMNEdge'), {'id': f'{mfid}_di', 'bpmnElement': mfid})
|
||||||
ox, oy = spread_anchor(('n', sid), 'S', f'{mfid}_node')
|
ox, oy = spread_anchor(('n', sid), 'S', f'{mfid}_node')
|
||||||
tx, ty = spread_anchor(('x', target), 'N', f'{mfid}_ext')
|
ext_top = ext_pos[target][1]
|
||||||
points = [(ox, oy), (tx, ty)] if direction == 'out' else [(tx, ty), (ox, oy)]
|
# Senkrecht hinunter zum externen Pool; jeder Punkt an dessen Oberkante
|
||||||
|
# ist ein gültiger Anschluss. Steht darunter ein Element im Weg, erst
|
||||||
|
# kurz nach unten und seitlich in einen freien Gang (#13).
|
||||||
|
im_weg = [o for o in order if o != sid and steps[o].type not in UNCONNECTED_TYPES
|
||||||
|
and x_pos[o] < ox < x_pos[o] + bounds_of(o)[2]
|
||||||
|
and y_pos[o] + bounds_of(o)[3] > oy and y_pos[o] < ext_top]
|
||||||
|
if not im_weg:
|
||||||
|
points = [(ox, oy), (ox, ext_top)]
|
||||||
|
else:
|
||||||
|
if has_lanes:
|
||||||
|
gang_x = x_pos[sid] + (GROESSTE_BREITE + LANE_COLUMN_WIDTH) / 2 + NACHRICHT_VERSATZ
|
||||||
|
else:
|
||||||
|
gang_x = msg_kanal_x[mfid]
|
||||||
|
unter = oy + NACHRICHT_STUB
|
||||||
|
points = [(ox, oy), (ox, unter), (gang_x, unter), (gang_x, ext_top)]
|
||||||
|
if direction != 'out':
|
||||||
|
points = points[::-1]
|
||||||
for px, py in points:
|
for px, py in points:
|
||||||
ET.SubElement(edge, q('di', 'waypoint'), {'x': f'{px:g}', 'y': f'{py:g}'})
|
ET.SubElement(edge, q('di', 'waypoint'), {'x': f'{px:g}', 'y': f'{py:g}'})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Tests für md_to_bpmn.py - reines Python/pytest, ohne Netzwerk."""
|
"""Tests für md_to_bpmn.py - reines Python/pytest, ohne Netzwerk."""
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -565,6 +566,61 @@ def test_ruecksprungkante_ohne_lanes_quert_keinen_nachbarn_in_der_zeile(fall, qu
|
|||||||
assert not (x + 1 < px < x + w - 1 and y + 1 < py < y + h - 1), f"Rücksprung durch {eid}"
|
assert not (x + 1 < px < x + w - 1 and y + 1 < py < y + h - 1), f"Rücksprung durch {eid}"
|
||||||
|
|
||||||
|
|
||||||
|
NACHRICHTEN_OHNE_LANES = """
|
||||||
|
Extern: Bürger
|
||||||
|
S1 Start Nachricht: Antrag geht ein
|
||||||
|
<~ Bürger
|
||||||
|
-> S2
|
||||||
|
S2 Task: Prüfen
|
||||||
|
-> S3
|
||||||
|
S3 SendTask: Rückfrage senden
|
||||||
|
~> Bürger
|
||||||
|
-> S4
|
||||||
|
S4 Task: Entscheiden
|
||||||
|
-> S5
|
||||||
|
S5 Ende: Fertig
|
||||||
|
"""
|
||||||
|
|
||||||
|
NACHRICHTEN_MIT_LANES = (Path(__file__).parent / "beispiele" / "beispiel_lanes.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _nachrichtenfluesse(text):
|
||||||
|
"""Baut das Modell und liefert (Bounds je Element-Id, Wegpunkte je Nachrichtenfluss-Id)."""
|
||||||
|
defs, bounds, _ = _baue_und_finde(text)
|
||||||
|
wege = {}
|
||||||
|
for edge in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNEdge"):
|
||||||
|
if edge.get('bpmnElement').startswith('MsgFlow_'):
|
||||||
|
wege[edge.get('bpmnElement')] = [(float(w.get('x')), float(w.get('y')))
|
||||||
|
for w in edge.findall(f"{{{m.NS['di']}}}waypoint")]
|
||||||
|
return bounds, wege
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("text", [NACHRICHTEN_OHNE_LANES, NACHRICHTEN_MIT_LANES], ids=["ohne Lanes", "mit Lanes"])
|
||||||
|
def test_nachrichtenfluesse_sind_rechtwinklig(text):
|
||||||
|
"""Prüft, dass jeder Nachrichtenfluss nur aus waagrechten und senkrechten Stücken besteht, statt schräg zum
|
||||||
|
externen Pool zu laufen (Issue #13)."""
|
||||||
|
_, wege = _nachrichtenfluesse(text)
|
||||||
|
assert wege
|
||||||
|
for mfid, weg in wege.items():
|
||||||
|
for (x1, y1), (x2, y2) in zip(weg, weg[1:]):
|
||||||
|
assert abs(x1 - x2) < 1e-6 or abs(y1 - y2) < 1e-6, f"{mfid} schräg von ({x1:g}, {y1:g}) nach ({x2:g}, {y2:g})"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("text", [NACHRICHTEN_OHNE_LANES, NACHRICHTEN_MIT_LANES], ids=["ohne Lanes", "mit Lanes"])
|
||||||
|
def test_nachrichtenfluss_quert_kein_element(text):
|
||||||
|
"""Prüft, dass kein Nachrichtenfluss durch ein Element läuft, das unter seinem Schritt steht – ohne Lanes steht
|
||||||
|
jeder Folgeschritt genau darunter (Issue #13)."""
|
||||||
|
bounds, wege = _nachrichtenfluesse(text)
|
||||||
|
for mfid, weg in wege.items():
|
||||||
|
for eid, (x, y, w, h) in bounds.items():
|
||||||
|
if not eid.startswith(('Task_', 'SendTask_', 'Gateway_', 'StartEvent_', 'EndEvent_')):
|
||||||
|
continue
|
||||||
|
for (x1, y1), (x2, y2) in zip(weg, weg[1:]):
|
||||||
|
for s in range(1, 60):
|
||||||
|
px, py = x1 + (x2 - x1) * s / 60, y1 + (y2 - y1) * s / 60
|
||||||
|
assert not (x + 1 < px < x + w - 1 and y + 1 < py < y + h - 1), f"{mfid} durch {eid}"
|
||||||
|
|
||||||
|
|
||||||
def test_ausgabe_ist_wohlgeformtes_xml(tmp_path):
|
def test_ausgabe_ist_wohlgeformtes_xml(tmp_path):
|
||||||
"""Prüft, dass die erzeugte .bpmn-Datei sich als valides XML parsen laesst."""
|
"""Prüft, dass die erzeugte .bpmn-Datei sich als valides XML parsen laesst."""
|
||||||
src = tmp_path / 'prozess.md'
|
src = tmp_path / 'prozess.md'
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ Skills sind übernommen, alles QGIS- und Plugin-Spezifische ist entfernt. Kern i
|
|||||||
|
|
||||||
## Erledigt
|
## Erledigt
|
||||||
|
|
||||||
|
- 25.09.2026 **Nachrichtenflüsse senkrecht und an allen Elementen vorbei** (#13): Ende senkrecht unter dem
|
||||||
|
Start, bei Hindernis über einen freien Gang. Damit gibt es in den Beispielen keine bekannte Kreuzung mehr
|
||||||
|
(`BEKANNTE_KREUZUNGEN` leer).
|
||||||
|
|
||||||
- 25.09.2026 **Rücksprungkanten laufen an allen Elementen vorbei** (#12): mit Lanes über Gänge zwischen den
|
- 25.09.2026 **Rücksprungkanten laufen an allen Elementen vorbei** (#12): mit Lanes über Gänge zwischen den
|
||||||
Spalten und einen Kanal im eigenen Pool, ohne Lanes seitlich an allen Elementen vorbei. Vorbestehend und offen:
|
Spalten und einen Kanal im eigenen Pool, ohne Lanes seitlich an allen Elementen vorbei. Vorbestehend und offen:
|
||||||
Vorwärtskanten bei Querverbindungen zwischen Zweigen (eigenes Issue).
|
Vorwärtskanten bei Querverbindungen zwischen Zweigen (eigenes Issue).
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
|
|||||||
|
|
||||||
## BPMN und PICTURE (Skill sn-bpmn, md_to_bpmn.py)
|
## BPMN und PICTURE (Skill sn-bpmn, md_to_bpmn.py)
|
||||||
|
|
||||||
|
- **Nachrichtenflüsse enden senkrecht unter ihrem Start, nicht an einem verteilten Punkt.** Der externe Pool ist
|
||||||
|
ein breites Rechteck, jeder Punkt an seiner Oberkante ist ein gültiger Anschluss. Die frühere Verteilung der
|
||||||
|
Anschlusspunkte über die Pool-Oberkante erzeugte schräge Linien quer durch das Diagramm. Steht unter dem Schritt
|
||||||
|
ein Element, geht der Fluss über einen Gang: mit Lanes 20 neben dem Gang der Rücksprungkanten und mit längerem
|
||||||
|
Stub (30 statt 20), damit sich beide nicht überdecken; ohne Lanes rechts neben allen Elementen, dafür wird der
|
||||||
|
Pool breiter. Belastungstest mit 342 Varianten: vorher 182 Fälle mit Kreuzung, jetzt keiner. (#13, 25.09.2026)
|
||||||
- **Rücksprungkanten brauchen je Layout eine eigene Führung.** Mit Lanes stehen die Elemente in festen Spalten
|
- **Rücksprungkanten brauchen je Layout eine eigene Führung.** Mit Lanes stehen die Elemente in festen Spalten
|
||||||
(linksbündig, 190 auseinander). Der Rückweg läuft senkrecht nur in den Gängen zwischen den Spalten und waagrecht
|
(linksbündig, 190 auseinander). Der Rückweg läuft senkrecht nur in den Gängen zwischen den Spalten und waagrecht
|
||||||
in einem Kanalband, das am Boden der letzten Lane **nach** der Platzierung angehängt wird. So bleiben die
|
in einem Kanalband, das am Boden der letzten Lane **nach** der Platzierung angehängt wird. So bleiben die
|
||||||
|
|||||||
@@ -32,10 +32,7 @@ KANTEN = ("sequenceFlow", "messageFlow")
|
|||||||
# Bekannte, per Issue erfasste Konverterfehler: (Modell, Kante, Element) -> Grund mit Issue-Nummer.
|
# Bekannte, per Issue erfasste Konverterfehler: (Modell, Kante, Element) -> Grund mit Issue-Nummer.
|
||||||
# Jedes Paar bekommt einen eigenen xfail(strict=True)-Test; sobald der Fehler behoben ist, schlägt dieser als XPASS
|
# Jedes Paar bekommt einen eigenen xfail(strict=True)-Test; sobald der Fehler behoben ist, schlägt dieser als XPASS
|
||||||
# fehl – dann den Eintrag hier entfernen. Nie verwenden, um neue Fehler zu verstecken (AGENTS.md).
|
# fehl – dann den Eintrag hier entfernen. Nie verwenden, um neue Fehler zu verstecken (AGENTS.md).
|
||||||
BEKANNTE_KREUZUNGEN = {
|
BEKANNTE_KREUZUNGEN = {}
|
||||||
("beispiel_lanes", "MsgFlow_2", "Task_S5"): "Issue #13: Nachrichtenfluss zum externen Pool läuft durch S5",
|
|
||||||
("referenz_gewannenfestlegung", "MsgFlow_2", "Task_S24"): "Issue #13: Nachrichtenfluss von S16 läuft durch S24",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def baue(md):
|
def baue(md):
|
||||||
|
|||||||
Reference in New Issue
Block a user