XOR-Gateways mit sichtbarem X ausgeben (#15)

- build_bpmn(): Die BPMNShape jedes exklusiven Gateways bekommt
  isMarkerVisible="true". BPMN lässt das X optional; ohne das Attribut
  zeichnet bpmn-js eine leere Raute. OR-, AND- und ereignisbasierte
  Gateways haben ihr Symbol immer und bleiben unverändert.
- Alle Beispiele neu erzeugt: je XOR-Gateway nur das Attribut ergänzt
  (beispiel_lanes 1, beispiel_prozess 2, Referenzmodell 14), sonst
  byte-gleich.
- Neuer Test test_nur_xor_gateways_zeigen_ihr_x.
- Tests: python -m pytest -q -> 93 bestanden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 12:35:15 +02:00
co-authored by Claude Opus 5.5
parent 2de2e9ffda
commit 25231d94e5
6 changed files with 52 additions and 20 deletions
@@ -89,7 +89,7 @@
<bpmndi:BPMNShape id="Task_S2_di" bpmnElement="Task_S2">
<dc:Bounds x="300" y="320" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S3_di" bpmnElement="Gateway_S3">
<bpmndi:BPMNShape id="Gateway_S3_di" bpmnElement="Gateway_S3" isMarkerVisible="true">
<dc:Bounds x="490" y="335" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SendTask_S4_di" bpmnElement="SendTask_S4">
@@ -64,7 +64,7 @@
<bpmndi:BPMNShape id="Task_S2_di" bpmnElement="Task_S2">
<dc:Bounds x="165" y="250" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S3_di" bpmnElement="Gateway_S3">
<bpmndi:BPMNShape id="Gateway_S3_di" bpmnElement="Gateway_S3" isMarkerVisible="true">
<dc:Bounds x="165" y="420" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SendTask_S4_di" bpmnElement="SendTask_S4">
@@ -73,7 +73,7 @@
<bpmndi:BPMNShape id="Task_S5_di" bpmnElement="Task_S5">
<dc:Bounds x="250" y="590" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S6_di" bpmnElement="Gateway_S6">
<bpmndi:BPMNShape id="Gateway_S6_di" bpmnElement="Gateway_S6" isMarkerVisible="true">
<dc:Bounds x="165" y="760" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S7_di" bpmnElement="Task_S7">
@@ -305,7 +305,7 @@
<bpmndi:BPMNShape id="Task_S5_di" bpmnElement="Task_S5">
<dc:Bounds x="870" y="130" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S6_di" bpmnElement="Gateway_S6">
<bpmndi:BPMNShape id="Gateway_S6_di" bpmnElement="Gateway_S6" isMarkerVisible="true">
<dc:Bounds x="1060" y="145" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S7_di" bpmnElement="Task_S7">
@@ -317,31 +317,31 @@
<bpmndi:BPMNShape id="Task_S9_di" bpmnElement="Task_S9">
<dc:Bounds x="1630" y="130" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S10_di" bpmnElement="Gateway_S10">
<bpmndi:BPMNShape id="Gateway_S10_di" bpmnElement="Gateway_S10" isMarkerVisible="true">
<dc:Bounds x="1820" y="145" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S11_di" bpmnElement="Task_S11">
<dc:Bounds x="2010" y="130" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S12_di" bpmnElement="Gateway_S12">
<bpmndi:BPMNShape id="Gateway_S12_di" bpmnElement="Gateway_S12" isMarkerVisible="true">
<dc:Bounds x="2200" y="145" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S13_di" bpmnElement="Gateway_S13">
<bpmndi:BPMNShape id="Gateway_S13_di" bpmnElement="Gateway_S13" isMarkerVisible="true">
<dc:Bounds x="2390" y="60" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S14_di" bpmnElement="Task_S14">
<dc:Bounds x="2580" y="45" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S15_di" bpmnElement="Gateway_S15">
<bpmndi:BPMNShape id="Gateway_S15_di" bpmnElement="Gateway_S15" isMarkerVisible="true">
<dc:Bounds x="2770" y="60" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S16_di" bpmnElement="Task_S16">
<dc:Bounds x="2960" y="45" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S17_di" bpmnElement="Gateway_S17">
<bpmndi:BPMNShape id="Gateway_S17_di" bpmnElement="Gateway_S17" isMarkerVisible="true">
<dc:Bounds x="3150" y="60" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S18_di" bpmnElement="Gateway_S18">
<bpmndi:BPMNShape id="Gateway_S18_di" bpmnElement="Gateway_S18" isMarkerVisible="true">
<dc:Bounds x="3340" y="60" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S19_di" bpmnElement="Task_S19">
@@ -353,10 +353,10 @@
<bpmndi:BPMNShape id="Task_S21_di" bpmnElement="Task_S21">
<dc:Bounds x="3530" y="765" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S22_di" bpmnElement="Gateway_S22">
<bpmndi:BPMNShape id="Gateway_S22_di" bpmnElement="Gateway_S22" isMarkerVisible="true">
<dc:Bounds x="3910" y="145" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S23_di" bpmnElement="Gateway_S23">
<bpmndi:BPMNShape id="Gateway_S23_di" bpmnElement="Gateway_S23" isMarkerVisible="true">
<dc:Bounds x="3340" y="230" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S24_di" bpmnElement="Task_S24">
@@ -365,10 +365,10 @@
<bpmndi:BPMNShape id="Task_S25_di" bpmnElement="Task_S25">
<dc:Bounds x="3530" y="130" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S26_di" bpmnElement="Gateway_S26">
<bpmndi:BPMNShape id="Gateway_S26_di" bpmnElement="Gateway_S26" isMarkerVisible="true">
<dc:Bounds x="3720" y="230" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S27_di" bpmnElement="Gateway_S27">
<bpmndi:BPMNShape id="Gateway_S27_di" bpmnElement="Gateway_S27" isMarkerVisible="true">
<dc:Bounds x="4100" y="145" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S40_di" bpmnElement="Task_S40">
@@ -377,16 +377,16 @@
<bpmndi:BPMNShape id="Task_S41_di" bpmnElement="Task_S41">
<dc:Bounds x="2580" y="215" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S42_di" bpmnElement="Gateway_S42">
<bpmndi:BPMNShape id="Gateway_S42_di" bpmnElement="Gateway_S42" isMarkerVisible="true">
<dc:Bounds x="2770" y="230" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S43_di" bpmnElement="Task_S43">
<dc:Bounds x="2960" y="215" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S44_di" bpmnElement="Gateway_S44">
<bpmndi:BPMNShape id="Gateway_S44_di" bpmnElement="Gateway_S44" isMarkerVisible="true">
<dc:Bounds x="3150" y="230" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S50_di" bpmnElement="Gateway_S50">
<bpmndi:BPMNShape id="Gateway_S50_di" bpmnElement="Gateway_S50" isMarkerVisible="true">
<dc:Bounds x="4290" y="145" width="50" height="50"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S51_di" bpmnElement="Task_S51">
+6 -3
View File
@@ -884,9 +884,12 @@ def build_bpmn(meta_md, steps, order, externals):
}).append(_bounds_el(0, ex_y, pool_width, 100))
for sid in order:
shape = ET.SubElement(plane, q('bpmndi', 'BPMNShape'), {
'id': f'{node_ids[sid]}_di', 'bpmnElement': node_ids[sid],
})
attrs = {'id': f'{node_ids[sid]}_di', 'bpmnElement': node_ids[sid]}
if steps[sid].type == 'XorGateway':
# BPMN lässt das "X" im exklusiven Gateway optional; ohne dieses
# Attribut zeichnen Werkzeuge wie bpmn-js eine leere Raute (#15).
attrs['isMarkerVisible'] = 'true'
shape = ET.SubElement(plane, q('bpmndi', 'BPMNShape'), attrs)
x, y, w, h = bounds_of(sid)
shape.append(_bounds_el(x, y, w, h))
+26
View File
@@ -398,6 +398,32 @@ def test_kante_zwischen_gateways_zum_westpunkt_eines_linken_ziels_umgeht_die_rau
assert all(a[0] == b[0] or a[1] == b[1] for a, b in zip(punkte, punkte[1:])), "nicht rechtwinklig"
def test_nur_xor_gateways_zeigen_ihr_x():
"""Prüft, dass die Diagrammform jedes exklusiven Gateways isMarkerVisible="true" trägt, damit das „X“ erscheint,
und dass kein anderes Element das Attribut bekommt (Issue #15)."""
text = """
S1 Start: Beginn
-> S2
S2 Gateway XOR: Vollständig?
Ja -> S3
Nein -> S4
S3 Gateway AND: Aufteilen
-> S5
S4 Gateway OR: Welche?
-> S5
S5 Task: Bearbeiten
-> S6
S6 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
xor_ids = {g.get('id') for g in defs.iter(f"{{{m.NS['bpmn']}}}exclusiveGateway")}
assert xor_ids, "Testmodell ohne XOR-Gateway"
for shape in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNShape"):
erwartet = 'true' if shape.get('bpmnElement') in xor_ids else None
assert shape.get('isMarkerVisible') == erwartet, shape.get('bpmnElement')
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'