Beschriftungen und Anmerkungen ohne Überlappung setzen (#14)

- Gateway-Beschriftungen bekommen eine feste Position (BPMNLabel), weil
  PICTURE sie aus der Datei übernimmt: 45° wie im PICTURE-Editor, sonst
  45° rechts vom Ost-Stub, 315°, 135°, 225° – die erste Stelle ohne
  Kante, Kantenbeschriftung oder Element. Gesetzt nach allen Kanten.
- Kantenbeschriftungen waagrecht aus einem Gateway stehen unter der Linie
  (darüber steht die Gateway-Beschriftung).
- Anmerkungen rechts neben dem Pool untereinander, Höhe passend zum Text
  (text_zeilen(), anmerkung_groesse()); Pool ohne Anmerkungen bemessen.
- Beispiele neu erzeugt. Alle 17 Gateway-Beschriftungen auf 45° bzw. 45°
  rechts vom Stub, keine im Lane-Kopf oder in einer Nachbar-Lane.
- Strukturtests für alle Beispiele (Beschriftung ungeschnitten, in der
  eigenen Lane; Anmerkungen ohne Überlappung, neben dem Pool, groß genug)
  und drei Einzeltests.
- Tests: python -m pytest -q -> 117 bestanden, 1 übersprungen (leere
  Parameterliste der bekannten Kreuzungen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 16:10:19 +02:00
co-authored by Claude Opus 5.5
parent d40d49b002
commit 993b6de50c
9 changed files with 339 additions and 24 deletions
+5
View File
@@ -108,6 +108,11 @@ Jeder Schritt ist eine Zeile der Form:
Vollständige Liste inkl. Bedeutung: `Elementkatalog.md`. Vollständige Liste inkl. Bedeutung: `Elementkatalog.md`.
Hinweise stehen im Diagramm rechts neben dem Pool untereinander, in einem
Kästchen passend zur Textlänge. Gateway-Beschriftungen stehen oben rechts an
der Raute (wie im PICTURE-Editor), bei belegtem Platz an der nächsten freien
Stelle.
### Typisierte Ereignisse ### Typisierte Ereignisse
Start-, Zwischen- und Endereignisse können ein zweites Wort für die Art des Start-, Zwischen- und Endereignisse können ein zweites Wort für die Art des
@@ -91,6 +91,9 @@
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S3_di" bpmnElement="Gateway_S3" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S3_di" bpmnElement="Gateway_S3" isMarkerVisible="true">
<dc:Bounds x="490" y="335" width="50" height="50"/> <dc:Bounds x="490" y="335" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="570" y="305.5" width="90" height="42"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SendTask_S4_di" bpmnElement="SendTask_S4"> <bpmndi:BPMNShape id="SendTask_S4_di" bpmnElement="SendTask_S4">
<dc:Bounds x="680" y="235" width="100" height="80"/> <dc:Bounds x="680" y="235" width="100" height="80"/>
@@ -134,7 +137,7 @@
<di:waypoint x="565" y="275"/> <di:waypoint x="565" y="275"/>
<di:waypoint x="680" y="275"/> <di:waypoint x="680" y="275"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="537.5" y="340" width="30" height="14"/> <dc:Bounds x="537.5" y="366" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_5_di" bpmnElement="Flow_5"> <bpmndi:BPMNEdge id="Flow_5_di" bpmnElement="Flow_5">
@@ -66,6 +66,9 @@
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S3_di" bpmnElement="Gateway_S3" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S3_di" bpmnElement="Gateway_S3" isMarkerVisible="true">
<dc:Bounds x="165" y="420" width="50" height="50"/> <dc:Bounds x="165" y="420" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="245" y="390.5" width="90" height="42"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SendTask_S4_di" bpmnElement="SendTask_S4"> <bpmndi:BPMNShape id="SendTask_S4_di" bpmnElement="SendTask_S4">
<dc:Bounds x="80" y="590" width="100" height="80"/> <dc:Bounds x="80" y="590" width="100" height="80"/>
@@ -75,6 +78,9 @@
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S6_di" bpmnElement="Gateway_S6" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S6_di" bpmnElement="Gateway_S6" isMarkerVisible="true">
<dc:Bounds x="165" y="760" width="50" height="50"/> <dc:Bounds x="165" y="760" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="202.5" y="744.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S7_di" bpmnElement="Task_S7"> <bpmndi:BPMNShape id="Task_S7_di" bpmnElement="Task_S7">
<dc:Bounds x="80" y="930" width="100" height="80"/> <dc:Bounds x="80" y="930" width="100" height="80"/>
@@ -115,7 +121,7 @@
<di:waypoint x="240" y="590"/> <di:waypoint x="240" y="590"/>
<di:waypoint x="130" y="590"/> <di:waypoint x="130" y="590"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="212.5" y="425" width="30" height="14"/> <dc:Bounds x="212.5" y="451" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_5_di" bpmnElement="Flow_5"> <bpmndi:BPMNEdge id="Flow_5_di" bpmnElement="Flow_5">
@@ -145,7 +151,7 @@
<di:waypoint x="240" y="930"/> <di:waypoint x="240" y="930"/>
<di:waypoint x="300" y="930"/> <di:waypoint x="300" y="930"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="212.5" y="765" width="30" height="14"/> <dc:Bounds x="212.5" y="791" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_9_di" bpmnElement="Flow_9"> <bpmndi:BPMNEdge id="Flow_9_di" bpmnElement="Flow_9">
@@ -273,22 +273,22 @@
<bpmndi:BPMNDiagram id="Diagram_1"> <bpmndi:BPMNDiagram id="Diagram_1">
<bpmndi:BPMNPlane id="Plane_1" bpmnElement="Collaboration_1"> <bpmndi:BPMNPlane id="Plane_1" bpmnElement="Collaboration_1">
<bpmndi:BPMNShape id="Participant_1_di" bpmnElement="Participant_1" isHorizontal="true"> <bpmndi:BPMNShape id="Participant_1_di" bpmnElement="Participant_1" isHorizontal="true">
<dc:Bounds x="0" y="0" width="5480" height="1060"/> <dc:Bounds x="0" y="0" width="5166" height="1060"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1_di" bpmnElement="Lane_1" isHorizontal="true"> <bpmndi:BPMNShape id="Lane_1_di" bpmnElement="Lane_1" isHorizontal="true">
<dc:Bounds x="0" y="0" width="5480" height="340"/> <dc:Bounds x="0" y="0" width="5166" height="340"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_2_di" bpmnElement="Lane_2" isHorizontal="true"> <bpmndi:BPMNShape id="Lane_2_di" bpmnElement="Lane_2" isHorizontal="true">
<dc:Bounds x="0" y="340" width="5480" height="190"/> <dc:Bounds x="0" y="340" width="5166" height="190"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_3_di" bpmnElement="Lane_3" isHorizontal="true"> <bpmndi:BPMNShape id="Lane_3_di" bpmnElement="Lane_3" isHorizontal="true">
<dc:Bounds x="0" y="530" width="5480" height="190"/> <dc:Bounds x="0" y="530" width="5166" height="190"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_4_di" bpmnElement="Lane_4" isHorizontal="true"> <bpmndi:BPMNShape id="Lane_4_di" bpmnElement="Lane_4" isHorizontal="true">
<dc:Bounds x="0" y="720" width="5480" height="340"/> <dc:Bounds x="0" y="720" width="5166" height="340"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Participant_Ext_1_di" bpmnElement="Participant_Ext_1" isHorizontal="true"> <bpmndi:BPMNShape id="Participant_Ext_1_di" bpmnElement="Participant_Ext_1" isHorizontal="true">
<dc:Bounds x="0" y="1120" width="5480" height="100"/> <dc:Bounds x="0" y="1120" width="5166" height="100"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_S1_di" bpmnElement="StartEvent_S1"> <bpmndi:BPMNShape id="StartEvent_S1_di" bpmnElement="StartEvent_S1">
<dc:Bounds x="110" y="152" width="36" height="36"/> <dc:Bounds x="110" y="152" width="36" height="36"/>
@@ -307,6 +307,9 @@
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S6_di" bpmnElement="Gateway_S6" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S6_di" bpmnElement="Gateway_S6" isMarkerVisible="true">
<dc:Bounds x="1060" y="145" width="50" height="50"/> <dc:Bounds x="1060" y="145" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="1140" y="129.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S7_di" bpmnElement="Task_S7"> <bpmndi:BPMNShape id="Task_S7_di" bpmnElement="Task_S7">
<dc:Bounds x="1250" y="130" width="100" height="80"/> <dc:Bounds x="1250" y="130" width="100" height="80"/>
@@ -319,30 +322,48 @@
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S10_di" bpmnElement="Gateway_S10" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S10_di" bpmnElement="Gateway_S10" isMarkerVisible="true">
<dc:Bounds x="1820" y="145" width="50" height="50"/> <dc:Bounds x="1820" y="145" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="1857.5" y="115.5" width="90" height="42"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S11_di" bpmnElement="Task_S11"> <bpmndi:BPMNShape id="Task_S11_di" bpmnElement="Task_S11">
<dc:Bounds x="2010" y="130" width="100" height="80"/> <dc:Bounds x="2010" y="130" width="100" height="80"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S12_di" bpmnElement="Gateway_S12" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S12_di" bpmnElement="Gateway_S12" isMarkerVisible="true">
<dc:Bounds x="2200" y="145" width="50" height="50"/> <dc:Bounds x="2200" y="145" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="2237.5" y="129.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S13_di" bpmnElement="Gateway_S13" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S13_di" bpmnElement="Gateway_S13" isMarkerVisible="true">
<dc:Bounds x="2390" y="60" width="50" height="50"/> <dc:Bounds x="2390" y="60" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="2470" y="44.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S14_di" bpmnElement="Task_S14"> <bpmndi:BPMNShape id="Task_S14_di" bpmnElement="Task_S14">
<dc:Bounds x="2580" y="45" width="100" height="80"/> <dc:Bounds x="2580" y="45" width="100" height="80"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S15_di" bpmnElement="Gateway_S15" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S15_di" bpmnElement="Gateway_S15" isMarkerVisible="true">
<dc:Bounds x="2770" y="60" width="50" height="50"/> <dc:Bounds x="2770" y="60" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="2807.5" y="44.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S16_di" bpmnElement="Task_S16"> <bpmndi:BPMNShape id="Task_S16_di" bpmnElement="Task_S16">
<dc:Bounds x="2960" y="45" width="100" height="80"/> <dc:Bounds x="2960" y="45" width="100" height="80"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S17_di" bpmnElement="Gateway_S17" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S17_di" bpmnElement="Gateway_S17" isMarkerVisible="true">
<dc:Bounds x="3150" y="60" width="50" height="50"/> <dc:Bounds x="3150" y="60" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="3187.5" y="44.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S18_di" bpmnElement="Gateway_S18" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S18_di" bpmnElement="Gateway_S18" isMarkerVisible="true">
<dc:Bounds x="3340" y="60" width="50" height="50"/> <dc:Bounds x="3340" y="60" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="3377.5" y="44.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S19_di" bpmnElement="Task_S19"> <bpmndi:BPMNShape id="Task_S19_di" bpmnElement="Task_S19">
<dc:Bounds x="3530" y="585" width="100" height="80"/> <dc:Bounds x="3530" y="585" width="100" height="80"/>
@@ -355,9 +376,15 @@
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S22_di" bpmnElement="Gateway_S22" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S22_di" bpmnElement="Gateway_S22" isMarkerVisible="true">
<dc:Bounds x="3910" y="145" width="50" height="50"/> <dc:Bounds x="3910" y="145" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="3947.5" y="129.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S23_di" bpmnElement="Gateway_S23" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S23_di" bpmnElement="Gateway_S23" isMarkerVisible="true">
<dc:Bounds x="3340" y="230" width="50" height="50"/> <dc:Bounds x="3340" y="230" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="3420" y="214.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S24_di" bpmnElement="Task_S24"> <bpmndi:BPMNShape id="Task_S24_di" bpmnElement="Task_S24">
<dc:Bounds x="3530" y="935" width="100" height="80"/> <dc:Bounds x="3530" y="935" width="100" height="80"/>
@@ -367,9 +394,15 @@
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S26_di" bpmnElement="Gateway_S26" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S26_di" bpmnElement="Gateway_S26" isMarkerVisible="true">
<dc:Bounds x="3720" y="230" width="50" height="50"/> <dc:Bounds x="3720" y="230" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="3757.5" y="214.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S27_di" bpmnElement="Gateway_S27" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S27_di" bpmnElement="Gateway_S27" isMarkerVisible="true">
<dc:Bounds x="4100" y="145" width="50" height="50"/> <dc:Bounds x="4100" y="145" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="4137.5" y="129.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S40_di" bpmnElement="Task_S40"> <bpmndi:BPMNShape id="Task_S40_di" bpmnElement="Task_S40">
<dc:Bounds x="2390" y="215" width="100" height="80"/> <dc:Bounds x="2390" y="215" width="100" height="80"/>
@@ -379,15 +412,24 @@
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S42_di" bpmnElement="Gateway_S42" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S42_di" bpmnElement="Gateway_S42" isMarkerVisible="true">
<dc:Bounds x="2770" y="230" width="50" height="50"/> <dc:Bounds x="2770" y="230" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="2807.5" y="200.5" width="90" height="42"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S43_di" bpmnElement="Task_S43"> <bpmndi:BPMNShape id="Task_S43_di" bpmnElement="Task_S43">
<dc:Bounds x="2960" y="215" width="100" height="80"/> <dc:Bounds x="2960" y="215" width="100" height="80"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S44_di" bpmnElement="Gateway_S44" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S44_di" bpmnElement="Gateway_S44" isMarkerVisible="true">
<dc:Bounds x="3150" y="230" width="50" height="50"/> <dc:Bounds x="3150" y="230" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="3187.5" y="214.5" width="90" height="28"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_S50_di" bpmnElement="Gateway_S50" isMarkerVisible="true"> <bpmndi:BPMNShape id="Gateway_S50_di" bpmnElement="Gateway_S50" isMarkerVisible="true">
<dc:Bounds x="4290" y="145" width="50" height="50"/> <dc:Bounds x="4290" y="145" width="50" height="50"/>
<bpmndi:BPMNLabel>
<dc:Bounds x="4327.5" y="101.5" width="90" height="56"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_S51_di" bpmnElement="Task_S51"> <bpmndi:BPMNShape id="Task_S51_di" bpmnElement="Task_S51">
<dc:Bounds x="4480" y="130" width="100" height="80"/> <dc:Bounds x="4480" y="130" width="100" height="80"/>
@@ -402,16 +444,16 @@
<dc:Bounds x="5050" y="152" width="36" height="36"/> <dc:Bounds x="5050" y="152" width="36" height="36"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_H0_di" bpmnElement="TextAnnotation_H0"> <bpmndi:BPMNShape id="TextAnnotation_H0_di" bpmnElement="TextAnnotation_H0">
<dc:Bounds x="5240" y="40" width="160" height="60"/> <dc:Bounds x="5206" y="0" width="250" height="60"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_H3_di" bpmnElement="TextAnnotation_H3"> <bpmndi:BPMNShape id="TextAnnotation_H3_di" bpmnElement="TextAnnotation_H3">
<dc:Bounds x="5240" y="130" width="160" height="60"/> <dc:Bounds x="5206" y="80" width="250" height="266"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_H1_di" bpmnElement="TextAnnotation_H1"> <bpmndi:BPMNShape id="TextAnnotation_H1_di" bpmnElement="TextAnnotation_H1">
<dc:Bounds x="5240" y="220" width="160" height="60"/> <dc:Bounds x="5206" y="366" width="250" height="90"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_H2_di" bpmnElement="TextAnnotation_H2"> <bpmndi:BPMNShape id="TextAnnotation_H2_di" bpmnElement="TextAnnotation_H2">
<dc:Bounds x="5240" y="310" width="160" height="60"/> <dc:Bounds x="5206" y="476" width="250" height="106"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1"> <bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
<di:waypoint x="146" y="170"/> <di:waypoint x="146" y="170"/>
@@ -451,7 +493,7 @@
<di:waypoint x="1845" y="120"/> <di:waypoint x="1845" y="120"/>
<di:waypoint x="1845" y="145"/> <di:waypoint x="1845" y="145"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="1107.5" y="150" width="30" height="14"/> <dc:Bounds x="1107.5" y="176" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_8_di" bpmnElement="Flow_8"> <bpmndi:BPMNEdge id="Flow_8_di" bpmnElement="Flow_8">
@@ -500,7 +542,7 @@
<di:waypoint x="2275" y="255"/> <di:waypoint x="2275" y="255"/>
<di:waypoint x="2390" y="255"/> <di:waypoint x="2390" y="255"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="2247.5" y="150" width="30" height="14"/> <dc:Bounds x="2247.5" y="176" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_15_di" bpmnElement="Flow_15"> <bpmndi:BPMNEdge id="Flow_15_di" bpmnElement="Flow_15">
@@ -519,7 +561,7 @@
<di:waypoint x="2795" y="35"/> <di:waypoint x="2795" y="35"/>
<di:waypoint x="2795" y="60"/> <di:waypoint x="2795" y="60"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="2437.5" y="65" width="30" height="14"/> <dc:Bounds x="2437.5" y="91" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_17_di" bpmnElement="Flow_17"> <bpmndi:BPMNEdge id="Flow_17_di" bpmnElement="Flow_17">
@@ -557,7 +599,7 @@
<di:waypoint x="3365" y="205"/> <di:waypoint x="3365" y="205"/>
<di:waypoint x="3365" y="230"/> <di:waypoint x="3365" y="230"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="3197.5" y="65" width="30" height="14"/> <dc:Bounds x="3197.5" y="91" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_22_di" bpmnElement="Flow_22"> <bpmndi:BPMNEdge id="Flow_22_di" bpmnElement="Flow_22">
@@ -575,7 +617,7 @@
<di:waypoint x="3415" y="805"/> <di:waypoint x="3415" y="805"/>
<di:waypoint x="3530" y="805"/> <di:waypoint x="3530" y="805"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="3387.5" y="65" width="30" height="14"/> <dc:Bounds x="3387.5" y="91" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_24_di" bpmnElement="Flow_24"> <bpmndi:BPMNEdge id="Flow_24_di" bpmnElement="Flow_24">
@@ -619,7 +661,7 @@
<di:waypoint x="3415" y="170"/> <di:waypoint x="3415" y="170"/>
<di:waypoint x="3530" y="170"/> <di:waypoint x="3530" y="170"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="3387.5" y="235" width="132" height="14"/> <dc:Bounds x="3387.5" y="261" width="132" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_30_di" bpmnElement="Flow_30"> <bpmndi:BPMNEdge id="Flow_30_di" bpmnElement="Flow_30">
@@ -675,7 +717,7 @@
<di:waypoint x="2845" y="255"/> <di:waypoint x="2845" y="255"/>
<di:waypoint x="2960" y="255"/> <di:waypoint x="2960" y="255"/>
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="2817.5" y="235" width="30" height="14"/> <dc:Bounds x="2817.5" y="261" width="30" height="14"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_38_di" bpmnElement="Flow_38"> <bpmndi:BPMNEdge id="Flow_38_di" bpmnElement="Flow_38">
+100 -4
View File
@@ -412,6 +412,55 @@ RUECKSPRUNG_SEITE = 30
# im Gang gegenüber den Rücksprungkanten. # im Gang gegenüber den Rücksprungkanten.
NACHRICHT_STUB = 30 NACHRICHT_STUB = 30
NACHRICHT_VERSATZ = 20 NACHRICHT_VERSATZ = 20
# Beschriftungen (#14): grobe Schätzung der Textgröße, damit Kästchen und
# Beschriftungen groß genug sind - PICTURE vergrößert sonst beim Import und
# überlappt Nachbarn. Anmerkungen stehen rechts neben dem Pool.
ZEICHENBREITE = 7
ZEILENHOEHE = 16
ANMERKUNG_BREITE = 250
ANMERKUNG_ABSTAND = 40
GATEWAY_LABEL_BREITE = 90
GATEWAY_LABEL_ZEILE = 14
def text_zeilen(text, breite):
"""Geschätzte Zeilenzahl von text bei Umbruch an Wortgrenzen in einem Feld der
angegebenen Breite (abzüglich 10 Innenrand, ZEICHENBREITE je Zeichen)."""
je_zeile = max(1, int((breite - 10) // ZEICHENBREITE))
zeilen, laenge = 1, 0
for wort in text.split():
if laenge == 0:
laenge = len(wort)
elif laenge + 1 + len(wort) <= je_zeile:
laenge += 1 + len(wort)
else:
zeilen, laenge = zeilen + 1, len(wort)
while laenge > je_zeile: # überlange Wörter brechen hart um
zeilen, laenge = zeilen + 1, laenge - je_zeile
return zeilen
def _rechtecke_ueberlappen(a, b):
"""True, wenn sich die Rechtecke a und b (x, y, w, h) echt überschneiden."""
return a[0] < b[0] + b[2] and b[0] < a[0] + a[2] and a[1] < b[1] + b[3] and b[1] < a[1] + a[3]
def _strecke_schneidet(p, q, box):
"""True, wenn die Strecke p-q durchs Innere des Rechtecks box (x, y, w, h) läuft."""
x, y, w, h = box
(x1, y1), (x2, y2) = p, q
if abs(y1 - y2) < 1e-6: # waagrecht
return y < y1 < y + h and min(x1, x2) < x + w and max(x1, x2) > x
if abs(x1 - x2) < 1e-6: # senkrecht
return x < x1 < x + w and min(y1, y2) < y + h and max(y1, y2) > y
return any(x < x1 + (x2 - x1) * s / 40 < x + w and y < y1 + (y2 - y1) * s / 40 < y + h
for s in range(1, 40))
def anmerkung_groesse(text):
"""Breite und Höhe eines Anmerkungs-Kästchens, passend zur Textlänge."""
return ANMERKUNG_BREITE, max(DEFAULT_SIZE['TextAnnotation'][1],
text_zeilen(text, ANMERKUNG_BREITE) * ZEILENHOEHE + 10)
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,10 +933,14 @@ def build_bpmn(meta_md, steps, order, externals):
def bounds_of(sid): def bounds_of(sid):
st = steps[sid] st = steps[sid]
w, h = DEFAULT_SIZE.get(st.type, (100, 80)) if st.type == 'TextAnnotation':
w, h = anmerkung_groesse(st.label)
else:
w, h = DEFAULT_SIZE.get(st.type, (100, 80))
return x_pos[sid], y_pos[sid], w, h return x_pos[sid], y_pos[sid], w, h
pool_width = max((x_pos[sid] + bounds_of(sid)[2] for sid in order), default=400) + MARGIN im_diagramm = [sid for sid in order if steps[sid].type != 'TextAnnotation']
pool_width = max((x_pos[sid] + bounds_of(sid)[2] for sid in im_diagramm), 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 = {} msg_kanal_x = {}
if not has_lanes and msg_flow_records: if not has_lanes and msg_flow_records:
@@ -910,7 +963,16 @@ def build_bpmn(meta_md, steps, order, externals):
pool_height += band pool_height += band
lane_geo = (lane_top, lane_height, pool_height) lane_geo = (lane_top, lane_height, pool_height)
else: else:
pool_height = max((y_pos[sid] + bounds_of(sid)[3] for sid in order), default=300) + MARGIN pool_height = max((y_pos[sid] + bounds_of(sid)[3] for sid in im_diagramm), default=300) + MARGIN
# Anmerkungen rechts neben dem Pool untereinander, in der Größe ihres Texts:
# so kreuzen sie keine Lane-Grenzen und überlappen sich nicht, auch wenn
# PICTURE die Position streng aus der Datei übernimmt (#14).
anm_y = 0
for sid in order:
if steps[sid].type == 'TextAnnotation':
x_pos[sid], y_pos[sid] = pool_width + ANMERKUNG_ABSTAND, anm_y
anm_y += bounds_of(sid)[3] + ANMERKUNG_ABSTAND / 2
ext_pos = {} ext_pos = {}
if has_collaboration: if has_collaboration:
@@ -931,6 +993,9 @@ def build_bpmn(meta_md, steps, order, externals):
'id': f'{eid}_di', 'bpmnElement': eid, 'isHorizontal': 'true', 'id': f'{eid}_di', 'bpmnElement': eid, 'isHorizontal': 'true',
}).append(_bounds_el(0, ex_y, pool_width, 100)) }).append(_bounds_el(0, ex_y, pool_width, 100))
gateway_shapes = {} # Gateways mit Beschriftung, Position wird zuletzt gewählt
hindernisse = [] # Rechtecke von Elementen und Kantenbeschriftungen
strecken = [] # alle Kantenstücke als ((x1, y1), (x2, y2))
for sid in order: for sid in order:
attrs = {'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': if steps[sid].type == 'XorGateway':
@@ -940,6 +1005,9 @@ def build_bpmn(meta_md, steps, order, externals):
shape = ET.SubElement(plane, q('bpmndi', 'BPMNShape'), attrs) shape = ET.SubElement(plane, q('bpmndi', 'BPMNShape'), attrs)
x, y, w, h = bounds_of(sid) x, y, w, h = bounds_of(sid)
shape.append(_bounds_el(x, y, w, h)) shape.append(_bounds_el(x, y, w, h))
if SHAPE_KIND.get(steps[sid].type) == 'diamond' and steps[sid].label:
gateway_shapes[sid] = shape # Beschriftung erst nach den Kanten, siehe unten
hindernisse.append((x, y, w, h))
# Anschlusspunkte je (Element, Seite) zählen, damit mehrere Kanten an derselben # Anschlusspunkte je (Element, Seite) zählen, damit mehrere Kanten an derselben
# Seite nicht alle im selben Punkt zusammenlaufen (Modellierregel 4). # Seite nicht alle im selben Punkt zusammenlaufen (Modellierregel 4).
@@ -1048,18 +1116,24 @@ def build_bpmn(meta_md, steps, order, externals):
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}'})
strecken.extend(zip(points, points[1:]))
if label: if label:
# explizite Label-Position setzen, um Überlappungen mit Pfeilen/Nachbarlabels # explizite Label-Position setzen, um Überlappungen mit Pfeilen/Nachbarlabels
# zu reduzieren: am ersten Segment, leicht seitlich versetzt # zu reduzieren: am ersten Segment, leicht seitlich versetzt
(p0x, p0y), (p1x, p1y) = points[0], points[1] if len(points) > 1 else points[0] (p0x, p0y), (p1x, p1y) = points[0], points[1] if len(points) > 1 else points[0]
mx, my = (p0x + p1x) / 2, (p0y + p1y) / 2 mx, my = (p0x + p1x) / 2, (p0y + p1y) / 2
if abs(p0y - p1y) < 1e-6: # horizontales Segment -> Label darüber if abs(p0y - p1y) < 1e-6 and SHAPE_KIND.get(steps[src].type) == 'diamond':
# waagrecht aus einem Gateway -> Label darunter; darüber steht
# die Beschriftung des Gateways (#14)
lx, ly = mx - 15, my + 6
elif abs(p0y - p1y) < 1e-6: # horizontales Segment -> Label darüber
lx, ly = mx - 15, my - 20 lx, ly = mx - 15, my - 20
else: # vertikales Segment -> Label seitlich else: # vertikales Segment -> Label seitlich
lx, ly = mx + 8, my - 8 lx, ly = mx + 8, my - 8
bpmn_label = ET.SubElement(edge, q('bpmndi', 'BPMNLabel')) bpmn_label = ET.SubElement(edge, q('bpmndi', 'BPMNLabel'))
bpmn_label.append(_bounds_el(lx, ly, max(30, len(label) * 6), 14)) bpmn_label.append(_bounds_el(lx, ly, max(30, len(label) * 6), 14))
hindernisse.append((lx, ly, max(30, len(label) * 6), 14))
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})
@@ -1084,6 +1158,28 @@ def build_bpmn(meta_md, steps, order, externals):
points = points[::-1] 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}'})
strecken.extend(zip(points, points[1:]))
# Gateway-Beschriftungen zuletzt, wenn alle Kanten und Kantenbeschriftungen
# feststehen: oben rechts (45°) wie im PICTURE-Editor, sonst die nächste freie
# Stelle. Ohne Angabe stünde sie unter der Raute (180°), wo der südliche Pfad
# und dessen Beschriftung sie schneiden; PICTURE übernimmt die Position (#14).
for sid, shape in gateway_shapes.items():
x, y, w, h = bounds_of(sid)
lw = GATEWAY_LABEL_BREITE
lh = text_zeilen(steps[sid].label, lw) * GATEWAY_LABEL_ZEILE
# 45°; 45° rechts vom Ost-Stub (dort steigt oft die eigene Kante auf);
# 315°; 135°; 225°
ecken = [(x + w * 3 / 4, y + h / 4 - lh), (x + w + GATEWAY_STUB + 5, y + h / 4 - lh),
(x + w / 4 - lw, y + h / 4 - lh), (x + w * 3 / 4, y + h * 3 / 4),
(x + w / 4 - lw, y + h * 3 / 4)]
eigene = (x, y, w, h)
frei = [(lx, ly) for lx, ly in ecken
if not any(_strecke_schneidet(a, b, (lx, ly, lw, lh)) for a, b in strecken)
and not any(_rechtecke_ueberlappen((lx, ly, lw, lh), r) for r in hindernisse if r != eigene)]
lx, ly = frei[0] if frei else ecken[0]
ET.SubElement(shape, q('bpmndi', 'BPMNLabel')).append(_bounds_el(lx, ly, lw, lh))
hindernisse.append((lx, ly, lw, lh))
return definitions return definitions
+34
View File
@@ -621,6 +621,40 @@ def test_nachrichtenfluss_quert_kein_element(text):
assert not (x + 1 < px < x + w - 1 and y + 1 < py < y + h - 1), f"{mfid} durch {eid}" assert not (x + 1 < px < x + w - 1 and y + 1 < py < y + h - 1), f"{mfid} durch {eid}"
def test_textschaetzung_bricht_an_wortgrenzen_und_bei_ueberlangen_woertern_um():
"""Prüft die Zeilenschätzung für Beschriftungen: kurzer Text eine Zeile, Umbruch an Wortgrenzen, ein Wort länger
als die Zeile wird hart umbrochen (Issue #14)."""
je_zeile = int((100 - 10) // m.ZEICHENBREITE)
assert m.text_zeilen("Ja", 100) == 1
assert m.text_zeilen("a" * je_zeile + " b", 100) == 2
assert m.text_zeilen("x" * (2 * je_zeile + 1), 100) == 3
def test_gateway_beschriftung_steht_ohne_hindernis_oben_rechts():
"""Prüft, dass ein Gateway ohne Hindernis seine Beschriftung auf 45° (oben rechts) bekommt, wie im PICTURE-Editor,
und nicht darunter, wo der südliche Pfad sie schneiden würde (Issue #14)."""
text = """
S1 Start: Beginn
-> S2
S2 Gateway XOR: Antrag vollständig?
Ja -> S3
S3 Ende: Fertig
"""
defs, bounds, _ = _baue_und_finde(text)
shape = next(s for s in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNShape") if s.get('bpmnElement') == 'Gateway_S2')
b = shape.find(f"{{{m.NS['bpmndi']}}}BPMNLabel").find(f"{{{m.NS['dc']}}}Bounds")
lx, ly, lh = float(b.get('x')), float(b.get('y')), float(b.get('height'))
x, y, w, h = bounds['Gateway_S2']
assert lx == x + w * 3 / 4 and ly + lh == y + h / 4
def test_anmerkung_waechst_mit_ihrem_text():
"""Prüft, dass eine lange Anmerkung ein höheres Kästchen bekommt als eine kurze (Issue #14)."""
kurz_w, kurz_h = m.anmerkung_groesse("Kurz")
lang_w, lang_h = m.anmerkung_groesse("Ein langer Hinweis " * 20)
assert kurz_w == lang_w and lang_h > kurz_h
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'
+4
View File
@@ -31,6 +31,10 @@ Skills sind übernommen, alles QGIS- und Plugin-Spezifische ist entfernt. Kern i
## Erledigt ## Erledigt
- 25.09.2026 **Beschriftungen überlappen nicht mehr** (#14): Gateway-Beschriftungen mit fester Position (45°,
sonst nächste freie Stelle), Anmerkungen rechts neben dem Pool in passender Größe, Kantenbeschriftungen
waagrecht aus Gateways unter der Linie.
- 25.09.2026 **Nachrichtenflüsse senkrecht und an allen Elementen vorbei** (#13): Ende senkrecht unter dem - 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 Start, bei Hindernis über einen freien Gang. Damit gibt es in den Beispielen keine bekannte Kreuzung mehr
(`BEKANNTE_KREUZUNGEN` leer). (`BEKANNTE_KREUZUNGEN` leer).
+10
View File
@@ -5,6 +5,16 @@ 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)
- **PICTURE übernimmt Beschriftungspositionen und Kästchengrößen aus der Datei.** Ohne `BPMNLabel` setzt es die
Gateway-Beschriftung auf 180° (unter die Raute), wo der südliche Pfad und dessen Beschriftung sie schneiden. Im
PICTURE-Editor steht sie auf 45°. Der Konverter setzt die Position deshalb selbst: 45°, sonst 45° rechts vom
Ost-Stub, 315°, 135°, 225° – die erste Stelle, die keine Kante, Kantenbeschriftung oder kein Element schneidet.
Dafür werden die Gateway-Beschriftungen **nach** allen Kanten gesetzt. Zu kleine Anmerkungs-Kästchen vergrößert
PICTURE beim Import, dann überlappen sie Nachbarn und Lane-Grenzen. Deshalb richtet sich die Höhe nach dem Text
(`text_zeilen()`, grobe Schätzung 7 je Zeichen), und die Anmerkungen stehen rechts neben dem Pool.
`GATEWAY_LABEL_BREITE = 90` hängt an `AUSWEICHABSTAND` (#8): Bei 100 ragte die Beschriftung über die
Ausweichachse bei x + 130. Pool-Name über dem Namen der ersten Lane: nur in der Vorschau beobachtet, in PICTURE
kein Problem. (PICTURE 2026.4.1, #14, 25.09.2026)
- **Nachrichtenflüsse enden senkrecht unter ihrem Start, nicht an einem verteilten Punkt.** Der externe Pool ist - **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 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 Anschlusspunkte über die Pool-Oberkante erzeugte schräge Linien quer durch das Diagramm. Steht unter dem Schritt
+115
View File
@@ -121,6 +121,121 @@ def test_bekannte_kreuzung_ist_behoben(modell, kante, element):
assert (kante, element) not in kreuzungen(baue(SKILL / "beispiele" / f"{modell}.md")) assert (kante, element) not in kreuzungen(baue(SKILL / "beispiele" / f"{modell}.md"))
def _box(el):
b = el.find(f"{{{m.NS['dc']}}}Bounds")
return tuple(float(b.get(a)) for a in ("x", "y", "width", "height"))
def _ueberlappen(a, b, rand=0.5):
"""True, wenn sich die Rechtecke a und b (x, y, w, h) echt überschneiden (Berührung zählt nicht)."""
return (a[0] + rand < b[0] + b[2] and b[0] + rand < a[0] + a[2]
and a[1] + rand < b[1] + b[3] and b[1] + rand < a[1] + a[3])
def beschriftungskonflikte(defs):
"""Alle Konflikte einer Gateway-Beschriftung mit Kanten, Kantenbeschriftungen oder anderen Elementen."""
art = {el.get("id"): el.tag.rpartition("}")[2] for el in defs.iter() if el.get("id")}
gw_labels, formen = {}, {}
for shape in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNShape"):
eid = shape.get("bpmnElement")
if art.get(eid) in HINDERNISSE or art.get(eid) == "textAnnotation":
formen[eid] = _box(shape)
label = shape.find(f"{{{m.NS['bpmndi']}}}BPMNLabel")
if art.get(eid) in GATEWAYS and label is not None:
gw_labels[eid] = _box(label)
konflikte = []
for edge in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNEdge"):
kante = edge.get("bpmnElement")
punkte = [(float(wp.get("x")), float(wp.get("y"))) for wp in edge.findall(f"{{{m.NS['di']}}}waypoint")]
label = edge.find(f"{{{m.NS['bpmndi']}}}BPMNLabel")
for gid, (x, y, w, h) in gw_labels.items():
if label is not None and _ueberlappen(_box(label), (x, y, w, h)):
konflikte.append((gid, f"Beschriftung von {kante}"))
for (x1, y1), (x2, y2) in zip(punkte, punkte[1:]):
schritte = max(8, int(math.hypot(x2 - x1, y2 - y1) / 4))
if any(x < x1 + (x2 - x1) * s / schritte < x + w and y < y1 + (y2 - y1) * s / schritte < y + h
for s in range(1, schritte)):
konflikte.append((gid, kante))
break
for gid, box in gw_labels.items():
konflikte += [(gid, eid) for eid, form in formen.items() if eid != gid and _ueberlappen(box, form)]
return sorted(konflikte)
@pytest.mark.parametrize("md", MODELLE, ids=lambda p: p.stem)
def test_gateway_beschriftung_wird_von_nichts_geschnitten(md):
"""Prüft, dass jede Gateway-Beschriftung eine feste Position hat und weder von einer Kante noch von einer
Kantenbeschriftung oder einem anderen Element geschnitten wird (Issue #14; PICTURE übernimmt die Position)."""
defs = baue(md)
art = {el.get("id"): el.tag.rpartition("}")[2] for el in defs.iter() if el.get("id")}
ohne = [s.get("bpmnElement") for s in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNShape")
if art.get(s.get("bpmnElement")) in GATEWAYS and s.find(f"{{{m.NS['bpmndi']}}}BPMNLabel") is None
and next(el for el in defs.iter() if el.get("id") == s.get("bpmnElement")).get("name")]
assert ohne == [], "Gateway mit Namen, aber ohne Beschriftungsposition: " + ", ".join(ohne)
konflikte = beschriftungskonflikte(defs)
assert konflikte == [], "Gateway-Beschriftung geschnitten: " + ", ".join(f"{g} ← {k}" for g, k in konflikte)
@pytest.mark.parametrize("md", [md for md in MODELLE if md.stem != "beispiel_prozess"], ids=lambda p: p.stem)
def test_gateway_beschriftung_bleibt_in_der_eigenen_lane(md):
"""Prüft, dass jede Gateway-Beschriftung vollständig in der Lane ihres Gateways liegt und nicht in den Lane-Kopf
mit den Lane-Namen oder in eine Nachbar-Lane ragt (Issue #14)."""
defs = baue(md)
lanes = [_box(s) for s in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNShape") if s.get("bpmnElement").startswith("Lane_")]
assert lanes, "Modell ohne Lanes"
for shape in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNShape"):
label = shape.find(f"{{{m.NS['bpmndi']}}}BPMNLabel")
if label is None:
continue
x, y, w, h = _box(shape)
lx, ly, lw, lh = _box(label)
lane = next(b for b in lanes if b[1] <= y + h / 2 <= b[1] + b[3])
assert lane[1] <= ly and ly + lh <= lane[1] + lane[3], f"{shape.get('bpmnElement')}: ragt aus der Lane"
assert lx >= lane[0] + m.LANE_HEADER_WIDTH, f"{shape.get('bpmnElement')}: ragt in den Lane-Kopf"
MIT_ANMERKUNGEN = [md for md in MODELLE if " Hinweis:" in md.read_text(encoding="utf-8")]
def test_es_gibt_ein_modell_mit_anmerkungen():
"""Prüft, dass die Anmerkungs-Tests tatsächlich an einem Modell laufen."""
assert MIT_ANMERKUNGEN
@pytest.mark.parametrize("md", MIT_ANMERKUNGEN, ids=lambda p: p.stem)
def test_anmerkungen_ueberlappen_nichts_und_stehen_neben_dem_pool(md):
"""Prüft, dass Anmerkungen (Hinweise) sich nicht gegenseitig überlappen, kein anderes Element überdecken und –
wenn es einen Prozess-Pool gibt – rechts daneben stehen, also keine Lane-Grenze kreuzen (Issue #14)."""
defs = baue(md)
art = {el.get("id"): el.tag.rpartition("}")[2] for el in defs.iter() if el.get("id")}
formen = {s.get("bpmnElement"): _box(s) for s in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNShape")}
anmerkungen = {eid: box for eid, box in formen.items() if art.get(eid) == "textAnnotation"}
ids = sorted(anmerkungen)
for i, a in enumerate(ids):
for b in ids[i + 1:]:
assert not _ueberlappen(anmerkungen[a], anmerkungen[b]), f"{a} überlappt {b}"
for eid, form in formen.items():
if art.get(eid) in HINDERNISSE:
assert not _ueberlappen(anmerkungen[a], form), f"{a} überdeckt {eid}"
if "Participant_1" in formen:
px, _, pw, _ = formen["Participant_1"]
for eid, (x, _, _, _) in anmerkungen.items():
assert x >= px + pw, f"{eid} steht im Pool (x {x:g} < {px + pw:g})"
@pytest.mark.parametrize("md", MIT_ANMERKUNGEN, ids=lambda p: p.stem)
def test_anmerkung_ist_gross_genug_fuer_ihren_text(md):
"""Prüft, dass jedes Anmerkungs-Kästchen seinen Text fassen kann (grob: 7 Einheiten je Zeichen, 16 je Zeile).
Sonst vergrößert PICTURE das Kästchen beim Import, und es überlappt Nachbarn und Lane-Grenzen (Issue #14)."""
defs = baue(md)
formen = {s.get("bpmnElement"): _box(s) for s in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNShape")}
for anm in defs.iter(f"{{{m.NS['bpmn']}}}textAnnotation"):
text = anm.find(f"{{{m.NS['bpmn']}}}text").text or ""
_, _, w, h = formen[anm.get("id")]
zeilen = math.ceil(len(text) * 7 / (w - 10))
assert h >= zeilen * 16, f"{anm.get('id')}: {len(text)} Zeichen brauchen etwa {zeilen} Zeilen, Höhe nur {h:g}"
def test_kreuzungspruefung_findet_eine_kante_durch_eine_aufgabe(): def test_kreuzungspruefung_findet_eine_kante_durch_eine_aufgabe():
"""Prüft die Prüfung selbst: eine absichtlich durch eine Aufgabe gelegte Kante wird gefunden, eine daneben nicht.""" """Prüft die Prüfung selbst: eine absichtlich durch eine Aufgabe gelegte Kante wird gefunden, eine daneben nicht."""
defs = baue(SKILL / "beispiele" / "beispiel_prozess.md") defs = baue(SKILL / "beispiele" / "beispiel_prozess.md")