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:
@@ -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"))
|
||||
|
||||
|
||||
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():
|
||||
"""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")
|
||||
|
||||
Reference in New Issue
Block a user