From ef584c10c77e83440926c0eaab133d99a3ff3352 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Thu, 31 Jan 2019 00:43:56 +0100 Subject: [PATCH] SVG: fixed arc --- Patro/GeometryEngine/Conic.py | 38 +++++++++++++++++---- Patro/GeometryEngine/Path.py | 16 +++++++-- Patro/GeometryEngine/Transformation.py | 7 +++- Patro/GeometryEngine/Vector.py | 8 +++++ Patro/GraphicEngine/GraphicScene/Scene.py | 18 +++++++++- Patro/GraphicEngine/Painter/QtPainter.py | 25 +++++++++----- examples/file-format/svg/test-svg-import.py | 14 ++++++++ 7 files changed, 106 insertions(+), 20 deletions(-) diff --git a/Patro/GeometryEngine/Conic.py b/Patro/GeometryEngine/Conic.py index 2a2ad04..62f9435 100644 --- a/Patro/GeometryEngine/Conic.py +++ b/Patro/GeometryEngine/Conic.py @@ -48,6 +48,8 @@ __all__ = [ #################################################################################################### +import logging + import math from math import fabs, sqrt, radians, pi, cos, sin # , degrees @@ -63,6 +65,10 @@ from .Transformation import Transformation2D #################################################################################################### +_module_logger = logging.getLogger(__name__) + +#################################################################################################### + class PointNotOnCircleError(ValueError): pass @@ -476,6 +482,8 @@ class Ellipse2D(Primitive2DMixin, CenterMixin, AngularDomainMixin, Primitive): """ + _logger = _module_logger.getChild('Ellipse2D') + ############################################## @classmethod @@ -514,11 +522,16 @@ class Ellipse2D(Primitive2DMixin, CenterMixin, AngularDomainMixin, Primitive): radius_x2 = radius_x**2 radius_y2 = radius_y**2 - point1_prime = Transformation2D.Rotation(angle) * (point1 - point2)/2 + # We define a new referential with the origin is set to the middle of P1 — P2 + origin_prime = (point1 + point2)/2 + + # P1 is exprimed in this referential where the ellipse major axis line up with the x axis + point1_prime = Transformation2D.Rotation(-angle) * (point1 - point2)/2 # Ensure radii are large enough radii_scale = point1_prime.x**2/radius_x2 + point1_prime.y**2/radius_y2 if radii_scale > 1: + self._logger.warning('SVG Arc: radii must be scale') radii_scale = math.sqrt(radii_scale) radius_x = radii_scale * radius_x radius_y = radii_scale * radius_y @@ -532,19 +545,32 @@ class Ellipse2D(Primitive2DMixin, CenterMixin, AngularDomainMixin, Primitive): ratio = radius_x/radius_y - sign = -1 if large_arc == sweep else 1 - center_prime = sign * math.sqrt(num / den) * point1_prime.anti_normal.divide(ratio, 1/ratio) - center = Transformation2D.Rotation(-angle) * center_prime + (point1 + point2)/2 + sign = 1 if large_arc != sweep else -1 + # print(point1_prime) + # print(point1_prime.anti_normal) + # print(ratio) + # print(point1_prime.anti_normal.scale(ratio, 1/ratio)) + sign *= -1 # Fixme: solve mirroring artefacts for y-axis pointing to the top + center_prime = sign * math.sqrt(num / den) * point1_prime.anti_normal.scale(ratio, 1/ratio) + + center = Transformation2D.Rotation(angle) * center_prime + origin_prime vector1 = (point1_prime - center_prime).divide(radius_x, radius_y) vector2 = - (point1_prime + center_prime).divide(radius_x, radius_y) theta = cls.__vector_cls__(1, 0).angle_with(vector1) - delta_theta = vector1.angle_with(vector2) % 360 + delta_theta = vector1.angle_with(vector2) + # if theta < 0: + # theta = 180 + theta + # if delta_theta < 0: + # delta_theta = 180 + delta_theta + delta_theta = delta_theta % 360 + # print('theta', theta, delta_theta) if not sweep and delta_theta > 0: delta_theta -= 360 elif sweep and delta_theta < 0: delta_theta += 360 - domain = domain=AngularDomain(theta, theta + delta_theta) + # print('theta', theta, delta_theta, theta + delta_theta) + domain = domain = AngularDomain(theta, theta + delta_theta) return cls(center, radius_x, radius_y, angle, domain) diff --git a/Patro/GeometryEngine/Path.py b/Patro/GeometryEngine/Path.py index 6ba28d0..90fb043 100644 --- a/Patro/GeometryEngine/Path.py +++ b/Patro/GeometryEngine/Path.py @@ -41,7 +41,7 @@ import math from Patro.Common.Math.Functions import sign from .Primitive import Primitive1P, Primitive2DMixin from .Bezier import QuadraticBezier2D, CubicBezier2D -from .Conic import Circle2D, AngularDomain +from .Conic import AngularDomain, Circle2D, Ellipse2D from .Segment import Segment2D from .Vector import Vector2D @@ -839,6 +839,12 @@ class ArcSegment(OnePointMixin, PathPart): ############################################## + def __repr__(self): + template = '{0}(@{1._index} {1._point} rx={1._radius_x} ry={1._radius_y} a={1._angle} la={1._large_arc} s={1._sweep})' + return template.format(self.__class__.__name__, self) + + ############################################## + def to_absolute(self): self._point = self.stop_point self._absolute = True @@ -853,8 +859,12 @@ class ArcSegment(OnePointMixin, PathPart): @property def geometry(self): - # Fixme: !!! - return Segment2D(self.start_point, self.stop_point) + return Ellipse2D.svg_arc( + self.start_point, self.stop_point, + self._radius_x, self._radius_y, + self._angle, + self._large_arc, self._sweep, + ) #################################################################################################### diff --git a/Patro/GeometryEngine/Transformation.py b/Patro/GeometryEngine/Transformation.py index 79d4047..f81279e 100644 --- a/Patro/GeometryEngine/Transformation.py +++ b/Patro/GeometryEngine/Transformation.py @@ -289,7 +289,12 @@ class Transformation2D(Transformation): c = cos(angle) s = sin(angle) - return cls(np.array(((c, -s), (s, c))), TransformationType.Rotation) + return cls( + np.array(( + (c, -s), + (s, c))), + TransformationType.Rotation, + ) ############################################## diff --git a/Patro/GeometryEngine/Vector.py b/Patro/GeometryEngine/Vector.py index 1c80d1c..e508639 100644 --- a/Patro/GeometryEngine/Vector.py +++ b/Patro/GeometryEngine/Vector.py @@ -561,6 +561,14 @@ class Vector2D(Vector2DFloatBase): ############################################## + def scale(self, scale_x, scale_y): + """Scale self by scale""" + obj = self.clone() + obj._v *= np.array((scale_x, scale_y)) + return obj + + ############################################## + def divide(self, scale_x, scale_y): """Scale self by 1/scale""" obj = self.clone() diff --git a/Patro/GraphicEngine/GraphicScene/Scene.py b/Patro/GraphicEngine/GraphicScene/Scene.py index 40b97f7..c42447e 100644 --- a/Patro/GraphicEngine/GraphicScene/Scene.py +++ b/Patro/GraphicEngine/GraphicScene/Scene.py @@ -403,6 +403,22 @@ class GraphicSceneScope: def add_segment(segment): add_by_method(self.segment, segment) + def add_ellipse(segment): + # add_segment(Segment.Segment2D(*segment.points)) + ellipse = segment.geometry + # print(ellipse, ellipse.domain) + arc_item = self.ellipse( + ellipse.center, + ellipse.radius_x, + ellipse.radius_y, + ellipse.angle, + path_style, + start_angle=ellipse.domain.start, + stop_angle=ellipse.domain.stop, + user_data=segment, + ) + items.append(arc_item) + def add_quadratic(segment): add_by_method(self.quadratic_bezier, segment) @@ -426,7 +442,7 @@ class GraphicSceneScope: elif isinstance(segment, Path.CubicBezierSegment): add_cubic(segment) elif isinstance(segment, Path.ArcSegment): - add_segment(segment) + add_ellipse(segment) elif isinstance(segment, Path.StringedQuadtraticBezierSegment): pass elif isinstance(segment, Path.StringedCubicBezierSegment): diff --git a/Patro/GraphicEngine/Painter/QtPainter.py b/Patro/GraphicEngine/Painter/QtPainter.py index 5825c36..135dec9 100644 --- a/Patro/GraphicEngine/Painter/QtPainter.py +++ b/Patro/GraphicEngine/Painter/QtPainter.py @@ -303,20 +303,15 @@ class QtPainter(Painter): ############################################## - def paint_CircleItem(self, item): - - center = self.cast_position(item.position) - radius = self.length_scene_to_viewport(item.radius) - - pen = self._set_pen(item) + def _paint_arc(self, item, center, radius_x, radius_y): if item.is_closed: self._painter.drawEllipse(center, radius, radius) else: # drawArc cannot be filled ! rectangle = QRectF( - center + QPointF(-radius, radius), - center + QPointF(radius, -radius), + center + QPointF(-radius_x, radius_y), + center + QPointF(radius_x, -radius_y), ) start_angle, stop_angle = [int(angle*16) for angle in (item.start_angle, item.stop_angle)] span_angle = stop_angle - start_angle @@ -327,6 +322,17 @@ class QtPainter(Painter): ############################################## + def paint_CircleItem(self, item): + + center = self.cast_position(item.position) + radius = self.length_scene_to_viewport(item.radius) + + pen = self._set_pen(item) + + self._paint_arc(item, center, radius, radius) + + ############################################## + def paint_EllipseItem(self, item): center = self.cast_position(item.position) @@ -335,7 +341,8 @@ class QtPainter(Painter): pen = self._set_pen(item) - self._painter.drawEllipse(center, radius_x, radius_y) + # Fixme: angle !!! + self._paint_arc(item, center, radius_x, radius_y) ############################################## diff --git a/examples/file-format/svg/test-svg-import.py b/examples/file-format/svg/test-svg-import.py index 9aba8d4..81fe224 100644 --- a/examples/file-format/svg/test-svg-import.py +++ b/examples/file-format/svg/test-svg-import.py @@ -97,6 +97,19 @@ class SceneImporter(SvgFileInternal): def on_graphic_item(self, item): + # d=" + # m 31.589881,269.68673 + # a 5,10 0 0 1 7.061333, 0.48142 + # 5,10 0 0 1 -0.229465, 14.12342 + # 5,10 0 0 1 -7.062064, -0.43644 + # 5,10 0 0 1 0.20697 , -14.1248 + # L 35,277.00003 + # Z" + # if item.id == 'ellipse': + # print(item) + # for part in item.path_data: + # print(part) + state = self._dispatcher.state.clone().merge(item) # self._logger.info('Item: {}\n{}'.format(item.id, item)) # self._logger.info('Item State:\n' + str(state)) @@ -115,6 +128,7 @@ class SceneImporter(SvgFileInternal): ) transformation = self._screen_transformation * state.transform + # transformation = state.transform # self._logger.info('Sate Transform\n' + str(transformation)) if isinstance(item, SvgFormat.Path): # and state.stroke_dasharray is None -- GitLab