-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #81 from hbmartin/curved
Straight line edge conversion for (poly)lines and improved curve conversion
- Loading branch information
Showing
18 changed files
with
438 additions
and
165 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,9 @@ __pycache__/ | |
# C extensions | ||
*.so | ||
|
||
# VSCode config | ||
.vscode | ||
|
||
# Distribution / packaging | ||
.Python | ||
build/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
# flake8: noqa: F401 | ||
from .Arguments import Arguments | ||
from .Rect import Rect | ||
from .SvgParser import parse_nodes_edges_clusters |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,61 +1,77 @@ | ||
from graphviz2drawio.models.Errors import InvalidCbError | ||
import math | ||
from collections.abc import Callable | ||
|
||
from . import LinearRegression | ||
from svg.path import CubicBezier | ||
|
||
linear_min_r2 = 0.9 | ||
LINE_TOLERANCE = 0.01 | ||
|
||
|
||
class Curve: | ||
def __init__(self, start, end, cb, cbset=None) -> None: | ||
"""A complex number representation of a curve. | ||
A curve may either be a straight line or a cubic Bézier as specified by is_bezier. | ||
If the curve is linear then the points list be the polyline anchors. | ||
If the curve is a cubic Bézier then the points list will be the control points. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
start: complex, | ||
end: complex, | ||
*, | ||
is_bezier: bool, | ||
points: list[complex], | ||
) -> None: | ||
"""Complex numbers for start, end, and list of 4 Bezier control points.""" | ||
if cbset is None: | ||
cbset = [] | ||
self.start = start | ||
self.end = end | ||
if cb is not None and len(cb) != 4: # noqa: PLR2004 | ||
raise InvalidCbError | ||
self.cb = cb | ||
self.cbset = cbset | ||
self.start: complex = start | ||
self.end: complex = end | ||
self.is_bezier: bool = is_bezier | ||
self.points: list[complex] = points | ||
|
||
def __str__(self) -> str: | ||
control = "[" + (str(self.cb) if self.cb is not None else "None") + "]" | ||
return f"{self.start} , {control}, {self.end}" | ||
return f"{self.start} -> {self.end} {self.is_bezier} ({self.points})" | ||
|
||
@staticmethod | ||
def is_linear(points: list, threshold: float = linear_min_r2) -> bool: | ||
"""Determine the linearity of complex points for a given threshold. | ||
def is_linear(cb: CubicBezier) -> bool: | ||
"""Determine if the cubic Bézier is a straight line.""" | ||
if cb.start == cb.end: | ||
return False | ||
|
||
Takes a list of complex points and optional minimum R**2 threshold. | ||
Threshold defaults to 0.9. | ||
""" | ||
r2 = LinearRegression.coefficients(points)[2] | ||
return r2 > threshold | ||
y = _line(cb.start, cb.end) | ||
|
||
def cubic_bezier_coordinates(self, t: float) -> complex: | ||
"""Calculate a complex number representing a point along the cubic Bézier curve. | ||
if y is None: | ||
return Curve.is_linear(_rotate_bezier(cb)) | ||
|
||
Takes parametric parameter t where 0 <= t <= 1 | ||
""" | ||
x = Curve._cubic_bezier(self._cb("real"), t) | ||
y = Curve._cubic_bezier(self._cb("imag"), t) | ||
return complex(x, y) | ||
return math.isclose( | ||
y(cb.control1.real), | ||
cb.control1.imag, | ||
rel_tol=LINE_TOLERANCE, | ||
) and math.isclose( | ||
y(cb.control2.real), | ||
cb.control2.imag, | ||
rel_tol=LINE_TOLERANCE, | ||
) | ||
|
||
def _cb(self, prop): | ||
return [getattr(x, prop) for x in self.cb] | ||
|
||
@staticmethod | ||
def _cubic_bezier(p: list, t: float): | ||
"""Calculate the point along the cubic Bézier. | ||
`p` is an ordered list of 4 control points [P0, P1, P2, P3] | ||
`t` is a parametric parameter where 0 <= t <= 1 | ||
implements https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves | ||
B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ where 0 ≤ t ≤1 | ||
""" | ||
return ( | ||
(((1.0 - t) ** 3) * p[0]) | ||
+ (3.0 * t * ((1.0 - t) ** 2) * p[1]) | ||
+ (3.0 * (t**2) * (1.0 - t) * p[2]) | ||
+ ((t**3) * p[3]) | ||
) | ||
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 | ||
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), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,58 @@ | ||
from svg.path import CubicBezier, Move, Path, parse_path | ||
import cmath | ||
|
||
from svg.path import CubicBezier, Path, QuadraticBezier, parse_path | ||
|
||
from ..models.CoordsTranslate import CoordsTranslate | ||
from .bezier import approximate_cubic_bezier_as_quadratic, subdivide_inflections | ||
from .Curve import Curve | ||
|
||
CLOSE_POINT_TOLERANCE = 0.1 | ||
|
||
|
||
class CurveFactory: | ||
def __init__(self, coords) -> None: | ||
def __init__(self, coords: CoordsTranslate) -> None: | ||
super().__init__() | ||
self.coords = coords | ||
|
||
def from_svg(self, svg_path: str) -> Curve: | ||
path: Path | list = parse_path(svg_path) | ||
path: Path = parse_path(svg_path) | ||
points: list[complex] = [] | ||
is_bezier = not all(map(Curve.is_linear, filter(_is_cubic, path))) | ||
|
||
for segment in path: | ||
if isinstance(segment, QuadraticBezier): | ||
points.append(self.coords.complex_translate(segment.control)) | ||
elif isinstance(segment, CubicBezier): | ||
if Curve.is_linear(segment): | ||
points.append(self.coords.complex_translate(segment.start)) | ||
else: | ||
split_cubes = subdivide_inflections( | ||
segment.start, | ||
segment.control1, | ||
segment.control2, | ||
segment.end, | ||
) | ||
split_controls = [ | ||
self.coords.complex_translate( | ||
approximate_cubic_bezier_as_quadratic(*cube)[1], | ||
) | ||
for cube in split_cubes | ||
if cube | ||
] | ||
points.extend(split_controls) | ||
|
||
start = self.coords.complex_translate(path[0].start) | ||
end = self.coords.complex_translate(path[-1].end) | ||
cb = None | ||
cbset = [] | ||
if isinstance(path[0], Move): | ||
path = [path[i] for i in range(1, len(path))] | ||
if isinstance(path[0], CubicBezier): | ||
# TODO: needs to account for multiple bezier in path | ||
points = [path[0].start, path[0].control1, path[0].control2, path[0].end] | ||
if not Curve.is_linear(points): | ||
cb = [self.coords.complex_translate(p) for p in points] | ||
|
||
if len(path) > 1: # set of curves/points | ||
for p in path: | ||
cbset.append( | ||
( | ||
self.coords.translate( | ||
p.start.real, | ||
p.start.imag, | ||
), | ||
self.coords.translate(p.end.real, p.end.imag), | ||
), | ||
) | ||
return Curve(start=start, end=end, cb=cb, cbset=cbset) | ||
|
||
if len(points) > 0 and cmath.isclose( | ||
start, | ||
points[0], | ||
rel_tol=CLOSE_POINT_TOLERANCE, | ||
): | ||
points = points[1:] | ||
|
||
return Curve(start=start, end=end, is_bezier=is_bezier, points=points) | ||
|
||
|
||
def _is_cubic(p): | ||
return isinstance(p, CubicBezier) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,3 +21,5 @@ | |
CURVE_INTERVALS = [0.25, 0.5, 0.75] | ||
|
||
NONE = "none" | ||
CURVED = "curved=1;" | ||
SHARP = "rounded=0;" |
Oops, something went wrong.