diff --git a/README.md b/README.md index 92a3cd8..37d356e 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ python -m graphviz2drawio test/directed/hello.gv.txt - [ ] Text on edge alignment #59 - [ ] Text alignment inside of shape - [ ] Support for node with `path` shape #47 +- [ ] Ensure undirected graphs are not drawn with arrows - [ ] Run ruff in CI - [ ] Publish api docs to GH pages - [ ] Restore codecov to test GHA diff --git a/graphviz2drawio/graphviz2drawio.py b/graphviz2drawio/graphviz2drawio.py index 48daa50..592bf85 100755 --- a/graphviz2drawio/graphviz2drawio.py +++ b/graphviz2drawio/graphviz2drawio.py @@ -2,7 +2,7 @@ from pygraphviz import AGraph -from .models import parse_nodes_edges_clusters +from .models.SvgParser import parse_nodes_edges_clusters from .mx.MxGraph import MxGraph diff --git a/graphviz2drawio/models/__init__.py b/graphviz2drawio/models/__init__.py index d3fe148..b43295e 100644 --- a/graphviz2drawio/models/__init__.py +++ b/graphviz2drawio/models/__init__.py @@ -1,4 +1,3 @@ # flake8: noqa: F401 from .Arguments import Arguments from .Rect import Rect -from .SvgParser import parse_nodes_edges_clusters diff --git a/graphviz2drawio/mx/Curve.py b/graphviz2drawio/mx/Curve.py index e62fa87..9d5e043 100644 --- a/graphviz2drawio/mx/Curve.py +++ b/graphviz2drawio/mx/Curve.py @@ -1,8 +1,9 @@ import math +from typing import Callable, Any from svg.path import CubicBezier -from graphviz2drawio.models.Errors import InvalidCbError +from ..models.Errors import InvalidCbError linear_min_r2 = 0.9 @@ -24,15 +25,22 @@ def __str__(self) -> str: return f"{self.start} , {control}, {self.end}" @staticmethod - def is_linear(cb: CubicBezier) -> bool: - m, b = Curve._linear_regression(cb.start, cb.end) - control1_prediction = m * cb.control1.real + b - control2_prediction = m * cb.control2.real + b + def is_linear(cb: CubicBezier, is_rotated: bool = False) -> bool: + line = _line(cb.start, cb.end) + + if line is None: + if is_rotated: # Prevent infinite recursion + return False + return Curve.is_linear(_rotate_bezier(cb), True) return math.isclose( - control1_prediction, cb.control1.imag, abs_tol=threshold + line(cb.control1.real), + cb.control1.imag, + rel_tol=0.1, ) and math.isclose( - control2_prediction, cb.control2.imag, abs_tol=threshold + line(cb.control2.real), + cb.control2.imag, + rel_tol=0.1, ) def cubic_bezier_coordinates(self, t: float) -> complex: @@ -88,3 +96,30 @@ def _derivative_of_cubic_bezier(p: list, t: float): + (6.0 * t * (1.0 - t) * (p[2] - p[1])) + (3.0 * (t**2) * (p[3] - p[2])) ) + + +def _line(start: complex, end: complex) -> Callable[[float], float] | None: + """Calculate the slope and y-intercept of a line.""" + denom = end.real - start.real + if denom == 0: + return None + # Linearity is used to determine if a cubic Bézier is actually a line + # BUT we need to check for vertical lines or vertically oriented Beziers + # Maybe caller should flip x/y and call again? + m = (end.imag - start.imag) / denom + b = start.imag - (m * start.real) + + def y(x: float) -> float: + return (m * x) + b + + return y + + +def _rotate_bezier(cb): + """Reverse imaginary and real parts for all components.""" + return CubicBezier( + complex(cb.start.imag, cb.start.real), + complex(cb.control1.imag, cb.control1.real), + complex(cb.control2.imag, cb.control2.real), + complex(cb.end.imag, cb.end.real), + ) diff --git a/test/test_curve.py b/test/test_curve.py index e69de29..ef6475a 100644 --- a/test/test_curve.py +++ b/test/test_curve.py @@ -0,0 +1,13 @@ +from graphviz2drawio.mx.Curve import _line + + +def test_line(): + line = _line(complex(-5, 10), complex(-3, 4)) + assert line(0) == -5 + assert line(1) == -8 + assert line(2) == -11 + + +def test_line_vertical(): + line = _line(complex(1, 10), complex(1, 4)) + assert line is None diff --git a/test/test_graphs.py b/test/test_graphs.py index 568deea..ed7001c 100644 --- a/test/test_graphs.py +++ b/test/test_graphs.py @@ -92,6 +92,16 @@ def test_polylines() -> None: root = ElementTree.fromstring(xml) check_xml_top(root) + assert False + + +def test_polylines_curved() -> None: + file = "test/undirected/polylines_curved.gv.txt" + xml = graphviz2drawio.convert(file) + + root = ElementTree.fromstring(xml) + check_xml_top(root) + # assert False def test_cluster() -> None: