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
|
||||
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
|
||||
|
||||
Unter einem Schritt können beliebig viele Pfeil-Zeilen stehen:
|
||||
|
||||
@@ -164,16 +164,18 @@
|
||||
<di:waypoint x="1250" y="95"/>
|
||||
</bpmndi:BPMNEdge>
|
||||
<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"/>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="MsgFlow_2_di" bpmnElement="MsgFlow_2">
|
||||
<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 id="MsgFlow_3_di" bpmnElement="MsgFlow_3">
|
||||
<di:waypoint x="1110" y="135"/>
|
||||
<di:waypoint x="1024.5" y="810"/>
|
||||
<di:waypoint x="1110" y="810"/>
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
|
||||
@@ -710,11 +710,13 @@
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="MsgFlow_1_di" bpmnElement="MsgFlow_1">
|
||||
<di:waypoint x="730" y="210"/>
|
||||
<di:waypoint x="1826.67" y="1120"/>
|
||||
<di:waypoint x="730" y="1120"/>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="MsgFlow_2_di" bpmnElement="MsgFlow_2">
|
||||
<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:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
|
||||
@@ -407,6 +407,11 @@ RUECKSPRUNG_STUB = 20
|
||||
# Ohne Lanes: Abstand des seitlichen Kanals vom äußersten Element (MARGIN 80 lässt
|
||||
# links Platz für mehrere Kanäle).
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
lane_top, lane_height, pool_height = lane_geo
|
||||
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:
|
||||
register_anchor(('n', sid), 'S', f'{mfid}_node')
|
||||
register_anchor(('x', target), 'N', f'{mfid}_ext')
|
||||
|
||||
def shape_kind_for(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:
|
||||
edge = ET.SubElement(plane, q('bpmndi', 'BPMNEdge'), {'id': f'{mfid}_di', 'bpmnElement': mfid})
|
||||
ox, oy = spread_anchor(('n', sid), 'S', f'{mfid}_node')
|
||||
tx, ty = spread_anchor(('x', target), 'N', f'{mfid}_ext')
|
||||
points = [(ox, oy), (tx, ty)] if direction == 'out' else [(tx, ty), (ox, oy)]
|
||||
ext_top = ext_pos[target][1]
|
||||
# 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:
|
||||
ET.SubElement(edge, q('di', 'waypoint'), {'x': f'{px:g}', 'y': f'{py:g}'})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests für md_to_bpmn.py - reines Python/pytest, ohne Netzwerk."""
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
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}"
|
||||
|
||||
|
||||
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):
|
||||
"""Prüft, dass die erzeugte .bpmn-Datei sich als valides XML parsen laesst."""
|
||||
src = tmp_path / 'prozess.md'
|
||||
|
||||
@@ -31,6 +31,10 @@ Skills sind übernommen, alles QGIS- und Plugin-Spezifische ist entfernt. Kern i
|
||||
|
||||
## 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
|
||||
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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
- **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
|
||||
(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
|
||||
|
||||
@@ -32,10 +32,7 @@ KANTEN = ("sequenceFlow", "messageFlow")
|
||||
# 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
|
||||
# fehl – dann den Eintrag hier entfernen. Nie verwenden, um neue Fehler zu verstecken (AGENTS.md).
|
||||
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",
|
||||
}
|
||||
BEKANNTE_KREUZUNGEN = {}
|
||||
|
||||
|
||||
def baue(md):
|
||||
|
||||
Reference in New Issue
Block a user