openprocess 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cpnpy/__init__.py +46 -0
- openprocess/__init__.py +57 -0
- openprocess/analysis/__init__.py +0 -0
- openprocess/analysis/state_space.py +521 -0
- openprocess/analysis/state_space_process.py +251 -0
- openprocess/cli.py +742 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
- openprocess/exercises/pack.md +14 -0
- openprocess/flow/__init__.py +50 -0
- openprocess/flow/box.py +466 -0
- openprocess/flow/boxes/__init__.py +7 -0
- openprocess/flow/boxes/check.py +119 -0
- openprocess/flow/boxes/compare.py +16 -0
- openprocess/flow/boxes/cpn.py +53 -0
- openprocess/flow/boxes/discover.py +124 -0
- openprocess/flow/boxes/filter.py +80 -0
- openprocess/flow/boxes/input.py +124 -0
- openprocess/flow/boxes/output.py +52 -0
- openprocess/flow/boxes/predict.py +186 -0
- openprocess/flow/boxes/science.py +159 -0
- openprocess/flow/boxes/sweeps.py +18 -0
- openprocess/flow/convert.py +187 -0
- openprocess/flow/datasets.py +198 -0
- openprocess/flow/explain.py +115 -0
- openprocess/flow/library.py +222 -0
- openprocess/flow/record.py +385 -0
- openprocess/flow/runner.py +357 -0
- openprocess/flow/sweep.py +92 -0
- openprocess/flow/types.py +290 -0
- openprocess/flow/workflow.py +628 -0
- openprocess/gui/__init__.py +0 -0
- openprocess/gui/app.py +90 -0
- openprocess/gui/arc_editing.py +295 -0
- openprocess/gui/canvas.py +1414 -0
- openprocess/gui/flow/__init__.py +8 -0
- openprocess/gui/flow/canvas.py +854 -0
- openprocess/gui/flow/page.py +972 -0
- openprocess/gui/flow/templates.py +131 -0
- openprocess/gui/flow/viewers.py +665 -0
- openprocess/gui/items.py +1275 -0
- openprocess/gui/learn/answer_boxes.py +978 -0
- openprocess/gui/learn/concealment.py +91 -0
- openprocess/gui/learn/mode.py +1181 -0
- openprocess/gui/panning.py +241 -0
- openprocess/gui/resources/openprocess-icon.png +0 -0
- openprocess/gui/studio/__init__.py +1 -0
- openprocess/gui/studio/__main__.py +3 -0
- openprocess/gui/studio/app.py +4031 -0
- openprocess/gui/studio/charts.py +115 -0
- openprocess/gui/studio/compare_page.py +487 -0
- openprocess/gui/studio/cpn_page.py +1858 -0
- openprocess/gui/studio/definition_view.py +284 -0
- openprocess/gui/studio/derivation_view.py +421 -0
- openprocess/gui/studio/documents.py +152 -0
- openprocess/gui/studio/dotted_chart.py +1401 -0
- openprocess/gui/studio/file_dialogs.py +143 -0
- openprocess/gui/studio/filter_dialog.py +247 -0
- openprocess/gui/studio/graph_builders.py +176 -0
- openprocess/gui/studio/graph_view.py +682 -0
- openprocess/gui/studio/instances.py +413 -0
- openprocess/gui/studio/log_editor.py +675 -0
- openprocess/gui/studio/log_page.py +800 -0
- openprocess/gui/studio/markdown_view.py +127 -0
- openprocess/gui/studio/mathtext.py +260 -0
- openprocess/gui/studio/ml_highlighter.py +75 -0
- openprocess/gui/studio/model_page.py +760 -0
- openprocess/gui/studio/net_comparison.py +124 -0
- openprocess/gui/studio/notes_overlay.py +275 -0
- openprocess/gui/studio/petri_page.py +844 -0
- openprocess/gui/studio/regions_view.py +502 -0
- openprocess/gui/studio/sidebar.py +149 -0
- openprocess/gui/studio/style.py +503 -0
- openprocess/gui/studio/tool_icons.py +134 -0
- openprocess/gui/studio/updates.py +439 -0
- openprocess/gui/studio/widgets.py +899 -0
- openprocess/gui/studio/workers.py +60 -0
- openprocess/gui/studio/workspace.py +447 -0
- openprocess/gui/theme.py +394 -0
- openprocess/gui/tidy.py +86 -0
- openprocess/io/__init__.py +0 -0
- openprocess/io/cpn_reader.py +389 -0
- openprocess/io/cpn_writer.py +357 -0
- openprocess/learn/__init__.py +23 -0
- openprocess/learn/answers.py +188 -0
- openprocess/learn/checks.py +953 -0
- openprocess/learn/computed.py +1180 -0
- openprocess/learn/context.py +145 -0
- openprocess/learn/exam.py +169 -0
- openprocess/learn/exercise-packs.md +325 -0
- openprocess/learn/importer.py +216 -0
- openprocess/learn/notation.py +474 -0
- openprocess/learn/pack.py +511 -0
- openprocess/learn/sheet.py +296 -0
- openprocess/mining/__init__.py +73 -0
- openprocess/mining/analysis.py +689 -0
- openprocess/mining/columns.py +282 -0
- openprocess/mining/compare_nets.py +246 -0
- openprocess/mining/conformance/__init__.py +0 -0
- openprocess/mining/conformance/alignments.py +263 -0
- openprocess/mining/conformance/quality.py +145 -0
- openprocess/mining/conformance/token_replay.py +252 -0
- openprocess/mining/csv_import.py +222 -0
- openprocess/mining/definitions.py +584 -0
- openprocess/mining/dfg.py +187 -0
- openprocess/mining/discovery/__init__.py +0 -0
- openprocess/mining/discovery/alpha.py +168 -0
- openprocess/mining/discovery/heuristics.py +332 -0
- openprocess/mining/discovery/inductive.py +477 -0
- openprocess/mining/discovery/state_regions.py +62 -0
- openprocess/mining/filtering.py +237 -0
- openprocess/mining/footprint.py +183 -0
- openprocess/mining/invariants.py +191 -0
- openprocess/mining/layout.py +279 -0
- openprocess/mining/log.py +364 -0
- openprocess/mining/petrinet.py +354 -0
- openprocess/mining/playout.py +75 -0
- openprocess/mining/pm4py_bridge.py +82 -0
- openprocess/mining/pnml.py +223 -0
- openprocess/mining/processtree.py +216 -0
- openprocess/mining/regions.py +476 -0
- openprocess/mining/stats.py +160 -0
- openprocess/mining/structure.py +374 -0
- openprocess/mining/transition_system.py +409 -0
- openprocess/mining/xes.py +399 -0
- openprocess/ml/__init__.py +0 -0
- openprocess/ml/ast_nodes.py +332 -0
- openprocess/ml/builtins.py +364 -0
- openprocess/ml/colorsets.py +522 -0
- openprocess/ml/errors.py +60 -0
- openprocess/ml/evaluator.py +754 -0
- openprocess/ml/lexer.py +277 -0
- openprocess/ml/multiset.py +417 -0
- openprocess/ml/parser.py +737 -0
- openprocess/ml/values.py +319 -0
- openprocess/model/__init__.py +0 -0
- openprocess/model/declarations.py +617 -0
- openprocess/model/examples.py +98 -0
- openprocess/model/net.py +701 -0
- openprocess/model/plain.py +192 -0
- openprocess/references.py +280 -0
- openprocess/sim/__init__.py +0 -0
- openprocess/sim/binding.py +620 -0
- openprocess/sim/export.py +66 -0
- openprocess/sim/simulator.py +315 -0
- openprocess/teaching/__init__.py +4 -0
- openprocess/teaching/answers.py +4 -0
- openprocess/teaching/checks.py +5 -0
- openprocess/teaching/pack.py +4 -0
- openprocess/teaching/sheet.py +4 -0
- openprocess-0.7.0.dist-info/METADATA +927 -0
- openprocess-0.7.0.dist-info/RECORD +173 -0
- openprocess-0.7.0.dist-info/WHEEL +5 -0
- openprocess-0.7.0.dist-info/entry_points.txt +6 -0
- openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
- openprocess-0.7.0.dist-info/top_level.txt +2 -0
openprocess/gui/app.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The ``openprocess-cpn`` entry point: the CPN editor on its own.
|
|
2
|
+
|
|
3
|
+
The editor itself is :class:`openprocess.gui.studio.cpn_page.CpnPage`, the same page
|
|
4
|
+
OpenProcess Studio shows for a ``.cpn`` file. ``openprocess-cpn`` simply opens Studio with
|
|
5
|
+
that model (or a new, empty one), so there is one application with one look,
|
|
6
|
+
whether you start from a CPN model or from an event log.
|
|
7
|
+
|
|
8
|
+
:class:`MainWindow` is kept for scripts and tests that want a bare window
|
|
9
|
+
around a single model, without Studio's sidebar.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from PySide6.QtGui import QKeySequence
|
|
18
|
+
from PySide6.QtWidgets import QApplication, QMainWindow
|
|
19
|
+
|
|
20
|
+
from ..model.net import CPNet
|
|
21
|
+
from . import theme
|
|
22
|
+
|
|
23
|
+
APPLICATION_NAME = "OpenProcess"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _empty_model(name: str = "Untitled") -> CPNet:
|
|
27
|
+
net = CPNet(name)
|
|
28
|
+
net.add_declaration("colset UNIT = unit;")
|
|
29
|
+
net.add_declaration("colset INT = int;")
|
|
30
|
+
net.add_page("Top")
|
|
31
|
+
net.compile()
|
|
32
|
+
return net
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MainWindow(QMainWindow):
|
|
36
|
+
"""A window holding one CPN page (no sidebar)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, net: CPNet | None = None, path: Path | None = None) -> None:
|
|
39
|
+
super().__init__()
|
|
40
|
+
from .studio import style
|
|
41
|
+
from .studio.cpn_page import CpnPage
|
|
42
|
+
from .studio.documents import CpnDocument
|
|
43
|
+
self.document = CpnDocument(net if net is not None else _empty_model(),
|
|
44
|
+
path=str(path) if path else None)
|
|
45
|
+
self.page = CpnPage(self.document)
|
|
46
|
+
self.page.setObjectName("studioRoot")
|
|
47
|
+
self.setCentralWidget(self.page)
|
|
48
|
+
self.setStyleSheet(style.stylesheet())
|
|
49
|
+
self.page.status.connect(lambda message: self.statusBar().showMessage(message, 8000))
|
|
50
|
+
self.page.dirty_changed.connect(lambda _dirty: self._update_title())
|
|
51
|
+
self.page.saved.connect(self._update_title)
|
|
52
|
+
menu = self.menuBar().addMenu("&File")
|
|
53
|
+
menu.addAction("Save", QKeySequence.Save, self.page.save)
|
|
54
|
+
menu.addAction("Save As…", QKeySequence.SaveAs, self.page.export)
|
|
55
|
+
view = self.menuBar().addMenu("&View")
|
|
56
|
+
view.addAction("Zoom In", QKeySequence.ZoomIn, self.page.view.zoom_in)
|
|
57
|
+
view.addAction("Zoom Out", QKeySequence.ZoomOut, self.page.view.zoom_out)
|
|
58
|
+
view.addAction("Zoom to Fit", "Ctrl+0", self.page.view.zoom_to_fit)
|
|
59
|
+
self.resize(1440, 900)
|
|
60
|
+
self._update_title()
|
|
61
|
+
|
|
62
|
+
# Convenience accessors used by scripts.
|
|
63
|
+
@property
|
|
64
|
+
def net(self) -> CPNet:
|
|
65
|
+
return self.document.net
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def view(self):
|
|
69
|
+
return self.page.view
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def scene(self):
|
|
73
|
+
return self.page.scene
|
|
74
|
+
|
|
75
|
+
def _update_title(self) -> None:
|
|
76
|
+
name = Path(self.document.path).name if self.document.path else self.net.name
|
|
77
|
+
self.setWindowTitle(f"{name}{' •' if self.document.dirty else ''} — {APPLICATION_NAME}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main(argv: list[str] | None = None) -> int:
|
|
81
|
+
"""``openprocess-cpn [model.cpn]``: open Studio with a CPN model (or a new one)."""
|
|
82
|
+
arguments = list(sys.argv if argv is None else argv)
|
|
83
|
+
from .studio.app import main as studio_main
|
|
84
|
+
if len(arguments) > 1:
|
|
85
|
+
return studio_main(arguments)
|
|
86
|
+
return studio_main(arguments + ["--new-cpn"])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__": # pragma: no cover
|
|
90
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""How arcs are edited on the canvas: the rules of CPN IDE.
|
|
2
|
+
|
|
3
|
+
CPN IDE draws its nets with *diagram-js* (the library behind the bpmn.io
|
|
4
|
+
editors), so an arc there behaves like a diagram-js connection. This module
|
|
5
|
+
restates those rules as small functions on plain ``(x, y)`` tuples; the
|
|
6
|
+
canvas (:mod:`openprocess.gui.canvas`) calls them while you drag.
|
|
7
|
+
|
|
8
|
+
An arc is a list of *waypoints*: the point where it leaves its first node,
|
|
9
|
+
its bendpoints, and the point where it meets its second node. Here the list
|
|
10
|
+
always runs from the transition end to the place end, the order ``.cpn``
|
|
11
|
+
files store bendpoints in.
|
|
12
|
+
|
|
13
|
+
Pressing on an arc and dragging does one of four things, decided by where
|
|
14
|
+
you pressed (:func:`hit_test`):
|
|
15
|
+
|
|
16
|
+
* **on a bend** (within 10 units): move that bend;
|
|
17
|
+
* **on one of the two ends**: drag that end to another node (reconnect);
|
|
18
|
+
* **near the middle of a horizontal or vertical segment**: slide the whole
|
|
19
|
+
segment sideways (:func:`move_segment`);
|
|
20
|
+
* **anywhere else on the line**: add a bend there and drag it.
|
|
21
|
+
|
|
22
|
+
While a bend is dragged it snaps level with, or in line with, its
|
|
23
|
+
neighbours (:func:`snap_bend`). When it is dropped, bends that ended up in a
|
|
24
|
+
straight line with their neighbours disappear (:func:`without_redundant`) --
|
|
25
|
+
that is how a bend is removed: drag it back into line.
|
|
26
|
+
|
|
27
|
+
Moving a node leaves the bends where they are, except the one next to the
|
|
28
|
+
node: if it was level with (or in line with) the arc's end, it stays so
|
|
29
|
+
(:func:`repair_after_move`). Moving both ends of an arc together moves the
|
|
30
|
+
whole arc.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import math
|
|
36
|
+
|
|
37
|
+
Point = tuple[float, float]
|
|
38
|
+
Rect = tuple[float, float, float, float] # left, top, width, height
|
|
39
|
+
|
|
40
|
+
#: How close (in canvas units) a press must be to a bend to grab it, and to
|
|
41
|
+
#: a segment's middle to slide the segment.
|
|
42
|
+
HIT_TOLERANCE = 10.0
|
|
43
|
+
#: A dragged bend snaps to its neighbours' x or y within this distance.
|
|
44
|
+
SNAP_TOLERANCE = 10.0
|
|
45
|
+
#: A moved node's centre snaps to other nodes' centres within this distance.
|
|
46
|
+
NODE_SNAP_TOLERANCE = 7.0
|
|
47
|
+
#: Two points this close in x (or y) count as lined up vertically (or
|
|
48
|
+
#: horizontally).
|
|
49
|
+
ALIGNED_THRESHOLD = 2.0
|
|
50
|
+
#: A bend this close to a straight line through its neighbours is dropped.
|
|
51
|
+
REDUNDANT_ACCURACY = 5.0
|
|
52
|
+
#: After a node move, bends within this distance of either node are dropped.
|
|
53
|
+
OVERLAP_PADDING = 20.0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Small geometry helpers
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
def distance(a: Point, b: Point) -> float:
|
|
60
|
+
return math.hypot(a[0] - b[0], a[1] - b[1])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def aligned(a: Point, b: Point) -> str | None:
|
|
64
|
+
"""``"vertical"`` if ``a`` and ``b`` share x, ``"horizontal"`` if they
|
|
65
|
+
share y (within :data:`ALIGNED_THRESHOLD`), else ``None``."""
|
|
66
|
+
if abs(a[0] - b[0]) <= ALIGNED_THRESHOLD:
|
|
67
|
+
return "vertical"
|
|
68
|
+
if abs(a[1] - b[1]) <= ALIGNED_THRESHOLD:
|
|
69
|
+
return "horizontal"
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def on_line(p: Point, q: Point, r: Point, accuracy: float = REDUNDANT_ACCURACY) -> bool:
|
|
74
|
+
"""Is ``r`` within ``accuracy`` of the straight line through ``p`` and ``q``?"""
|
|
75
|
+
length = distance(p, q)
|
|
76
|
+
if length == 0:
|
|
77
|
+
return distance(p, r) <= accuracy
|
|
78
|
+
cross = (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])
|
|
79
|
+
return abs(cross / length) <= accuracy
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def inside(p: Point, rect: Rect, padding: float = 0.0) -> bool:
|
|
83
|
+
left, top, width, height = rect
|
|
84
|
+
return (left - padding < p[0] < left + width + padding
|
|
85
|
+
and top - padding < p[1] < top + height + padding)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def closest_on_segment(p: Point, a: Point, b: Point) -> Point:
|
|
89
|
+
dx, dy = b[0] - a[0], b[1] - a[1]
|
|
90
|
+
length2 = dx * dx + dy * dy
|
|
91
|
+
if length2 == 0:
|
|
92
|
+
return a
|
|
93
|
+
share = max(0.0, min(1.0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length2))
|
|
94
|
+
return a[0] + dx * share, a[1] + dy * share
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Where did the press land?
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
def hit_test(waypoints: list[Point], p: Point,
|
|
101
|
+
tolerance: float = HIT_TOLERANCE) -> tuple[str, int, Point] | None:
|
|
102
|
+
"""Classify a press at ``p`` on an arc with the given waypoints.
|
|
103
|
+
|
|
104
|
+
Returns ``(kind, index, point)`` or ``None`` when ``p`` is not on the arc:
|
|
105
|
+
|
|
106
|
+
* ``("bend", i, point)`` -- on waypoint ``i``. ``i == 0`` and
|
|
107
|
+
``i == len(waypoints) - 1`` are the two ends (reconnect);
|
|
108
|
+
* ``("segment", i, point)`` -- near the middle of the horizontal or
|
|
109
|
+
vertical segment from waypoint ``i - 1`` to waypoint ``i``;
|
|
110
|
+
* ``("insert", i, point)`` -- elsewhere on that segment: a new bend goes
|
|
111
|
+
in at index ``i``; ``point`` is the spot on the line.
|
|
112
|
+
"""
|
|
113
|
+
for index, waypoint in enumerate(waypoints):
|
|
114
|
+
if distance(waypoint, p) <= tolerance:
|
|
115
|
+
return "bend", index, waypoint
|
|
116
|
+
best: tuple[float, int, Point] | None = None
|
|
117
|
+
for index in range(1, len(waypoints)):
|
|
118
|
+
a, b = waypoints[index - 1], waypoints[index]
|
|
119
|
+
foot = closest_on_segment(p, a, b)
|
|
120
|
+
gap = distance(foot, p)
|
|
121
|
+
if gap <= tolerance and (best is None or gap < best[0]):
|
|
122
|
+
best = (gap, index, foot)
|
|
123
|
+
if best is None:
|
|
124
|
+
return None
|
|
125
|
+
_, index, foot = best
|
|
126
|
+
a, b = waypoints[index - 1], waypoints[index]
|
|
127
|
+
middle = ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
|
|
128
|
+
if aligned(a, b) and abs(foot[0] - middle[0]) <= tolerance \
|
|
129
|
+
and abs(foot[1] - middle[1]) <= tolerance:
|
|
130
|
+
return "segment", index, middle
|
|
131
|
+
return "insert", index, foot
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Dragging a bend
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
def snap_value(value: float, candidates: list[float],
|
|
138
|
+
tolerance: float = SNAP_TOLERANCE) -> float:
|
|
139
|
+
for candidate in candidates:
|
|
140
|
+
if abs(candidate - value) <= tolerance:
|
|
141
|
+
return candidate
|
|
142
|
+
return value
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def snap_bend(p: Point, neighbours: list[Point]) -> Point:
|
|
146
|
+
"""Snap a dragged bend level with / in line with its neighbours.
|
|
147
|
+
|
|
148
|
+
The neighbours are the waypoints on either side; for an end of the arc
|
|
149
|
+
pass the node's centre, as diagram-js does."""
|
|
150
|
+
xs = [n[0] for n in neighbours]
|
|
151
|
+
ys = [n[1] for n in neighbours]
|
|
152
|
+
return snap_value(p[0], xs), snap_value(p[1], ys)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def without_redundant(waypoints: list[Point],
|
|
156
|
+
accuracy: float = REDUNDANT_ACCURACY) -> list[Point]:
|
|
157
|
+
"""Drop bends that lie on the straight line through their neighbours, or
|
|
158
|
+
on top of the next point. The two ends always stay."""
|
|
159
|
+
points = list(waypoints)
|
|
160
|
+
index = 1
|
|
161
|
+
while index < len(points) - 1:
|
|
162
|
+
point = points[index]
|
|
163
|
+
if distance(point, points[index + 1]) == 0 or \
|
|
164
|
+
on_line(points[index - 1], points[index + 1], point, accuracy):
|
|
165
|
+
del points[index]
|
|
166
|
+
else:
|
|
167
|
+
index += 1
|
|
168
|
+
return points
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Sliding a segment
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
def move_segment(waypoints: list[Point], index: int, delta: float,
|
|
175
|
+
first_node: tuple[Point, Rect], last_node: tuple[Point, Rect],
|
|
176
|
+
snap_to: bool = True) -> list[Point]:
|
|
177
|
+
"""Slide the segment from waypoint ``index - 1`` to ``index`` sideways.
|
|
178
|
+
|
|
179
|
+
A horizontal segment moves up or down by ``delta``, a vertical one left
|
|
180
|
+
or right. ``first_node`` / ``last_node`` are ``(centre, rect)`` of the
|
|
181
|
+
nodes at the two ends. Where the segment starts or ends at a node:
|
|
182
|
+
|
|
183
|
+
* if its moved end still lies inside the node, the arc simply meets the
|
|
184
|
+
node at the new height;
|
|
185
|
+
* if it has left the node, a new bend is added, so the arc leaves the
|
|
186
|
+
node straight and then turns -- an S-bend.
|
|
187
|
+
|
|
188
|
+
A bend next to a node that is moved *into* that node is dropped.
|
|
189
|
+
Returns the new waypoint list (ends included, not yet tidied).
|
|
190
|
+
"""
|
|
191
|
+
start, end = waypoints[index - 1], waypoints[index]
|
|
192
|
+
axis = 1 if aligned(start, end) == "horizontal" else 0 # which coordinate moves
|
|
193
|
+
last = len(waypoints) - 1
|
|
194
|
+
# An end of the arc counts from the node's centre (its "docking" point).
|
|
195
|
+
if index - 1 == 0:
|
|
196
|
+
start = _with(start, 1 - axis, first_node[0][1 - axis])
|
|
197
|
+
if index == last:
|
|
198
|
+
end = _with(end, 1 - axis, last_node[0][1 - axis])
|
|
199
|
+
target = start[axis] + delta
|
|
200
|
+
if snap_to:
|
|
201
|
+
references = [start[axis], end[axis]]
|
|
202
|
+
if index - 2 >= 0:
|
|
203
|
+
references.append(waypoints[index - 2][axis])
|
|
204
|
+
if index + 1 <= last:
|
|
205
|
+
references.append(waypoints[index + 1][axis])
|
|
206
|
+
if index - 1 <= 1:
|
|
207
|
+
references.append(first_node[0][axis])
|
|
208
|
+
if index >= last - 1:
|
|
209
|
+
references.append(last_node[0][axis])
|
|
210
|
+
target = snap_value(target, [r for r in references if r != start[axis]])
|
|
211
|
+
new_start, new_end = _with(start, axis, target), _with(end, axis, target)
|
|
212
|
+
|
|
213
|
+
points = list(waypoints)
|
|
214
|
+
points[index - 1], points[index] = new_start, new_end
|
|
215
|
+
# The node end at the start of the segment ...
|
|
216
|
+
if index - 1 == 0:
|
|
217
|
+
if not inside(new_start, first_node[1]):
|
|
218
|
+
points.insert(0, start) # the arc leaves the node, then turns
|
|
219
|
+
index += 1
|
|
220
|
+
elif index - 1 == 1 and inside(new_start, first_node[1]):
|
|
221
|
+
del points[0] # the first bend went into the node
|
|
222
|
+
index -= 1
|
|
223
|
+
# ... and at its end.
|
|
224
|
+
last = len(points) - 1
|
|
225
|
+
if index == last:
|
|
226
|
+
if not inside(new_end, last_node[1]):
|
|
227
|
+
points.append(end)
|
|
228
|
+
elif index == last - 1 and inside(new_end, last_node[1]):
|
|
229
|
+
del points[-1]
|
|
230
|
+
return points
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _with(p: Point, axis: int, value: float) -> Point:
|
|
234
|
+
return (value, p[1]) if axis == 0 else (p[0], value)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# After a node moved
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
def repair_after_move(points: list[Point], new_end: Point,
|
|
241
|
+
moved: Rect, other: Rect) -> list[Point]:
|
|
242
|
+
"""The waypoints of an arc after the node at ``points[0]`` moved.
|
|
243
|
+
|
|
244
|
+
``points`` runs from the moved node's end to the other end; ``new_end``
|
|
245
|
+
is the old end point moved with the node; ``moved`` and ``other`` are
|
|
246
|
+
the two nodes' rectangles at their new places. Only the first bend
|
|
247
|
+
changes: kept level (or in line) with the end if it was. Bends that the
|
|
248
|
+
move put on top of either node are dropped, with everything before them.
|
|
249
|
+
"""
|
|
250
|
+
for _ in range(len(points)): # at most one pass per bend
|
|
251
|
+
if len(points) < 3:
|
|
252
|
+
return [new_end, points[-1]]
|
|
253
|
+
old_end = points[0]
|
|
254
|
+
repaired = list(points)
|
|
255
|
+
repaired[0] = new_end
|
|
256
|
+
bend = repaired[1]
|
|
257
|
+
alignment = aligned(old_end, bend)
|
|
258
|
+
if alignment == "vertical":
|
|
259
|
+
repaired[1] = (new_end[0], bend[1])
|
|
260
|
+
elif alignment == "horizontal":
|
|
261
|
+
repaired[1] = (bend[0], new_end[1])
|
|
262
|
+
for i in range(len(repaired) - 2, 0, -1):
|
|
263
|
+
if inside(repaired[i], moved, OVERLAP_PADDING) or \
|
|
264
|
+
inside(repaired[i], other, OVERLAP_PADDING):
|
|
265
|
+
points = repaired[i:]
|
|
266
|
+
break
|
|
267
|
+
else:
|
|
268
|
+
return repaired
|
|
269
|
+
return [new_end, points[-1]]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def snap_node(centre: Point, targets: list[Point],
|
|
273
|
+
tolerance: float = NODE_SNAP_TOLERANCE) -> tuple[float | None, float | None]:
|
|
274
|
+
"""The x and y a dragged node's centre snaps to (``None`` = no snap):
|
|
275
|
+
other nodes' centres and the corners of right-angled arcs."""
|
|
276
|
+
best_x = best_y = None
|
|
277
|
+
gap_x = gap_y = tolerance + 1e-9
|
|
278
|
+
for tx, ty in targets:
|
|
279
|
+
if abs(tx - centre[0]) < gap_x:
|
|
280
|
+
best_x, gap_x = tx, abs(tx - centre[0])
|
|
281
|
+
if abs(ty - centre[1]) < gap_y:
|
|
282
|
+
best_y, gap_y = ty, abs(ty - centre[1])
|
|
283
|
+
return best_x, best_y
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def corner_bends(waypoints: list[Point]) -> list[Point]:
|
|
287
|
+
"""Bends that start or end a horizontal or vertical segment -- the ones
|
|
288
|
+
nodes snap to, as in CPN IDE."""
|
|
289
|
+
corners = []
|
|
290
|
+
for i in range(1, len(waypoints) - 1):
|
|
291
|
+
point = waypoints[i]
|
|
292
|
+
before, after = waypoints[i - 1], waypoints[i + 1]
|
|
293
|
+
if point[0] in (before[0], after[0]) or point[1] in (before[1], after[1]):
|
|
294
|
+
corners.append(point)
|
|
295
|
+
return corners
|