From b7f2fd95480a2b84e1ddc376c969e8c5f9da7cae Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Sun, 27 Jan 2019 23:58:35 +0100 Subject: [PATCH] fixed SVG and path: arc, stringed and closed rectangle --- Patro/FileFormat/Svg/SvgFormat.py | 53 ++- Patro/GeometryEngine/Path.py | 391 +++++++++++++++----- Patro/GraphicEngine/GraphicScene/Scene.py | 71 ++-- examples/file-format/svg/test-svg-import.py | 31 +- 4 files changed, 401 insertions(+), 145 deletions(-) diff --git a/Patro/FileFormat/Svg/SvgFormat.py b/Patro/FileFormat/Svg/SvgFormat.py index 865eb0f..76ba1aa 100644 --- a/Patro/FileFormat/Svg/SvgFormat.py +++ b/Patro/FileFormat/Svg/SvgFormat.py @@ -997,11 +997,7 @@ class PathDataAttribute(StringAttribute): i = next_i # return commands - try: - return cls.to_geometry(commands) - except NotImplementedError: - self._logger.warning('Not Implemented Error on path') - return None + return cls.to_geometry(commands) ############################################## @@ -1031,11 +1027,11 @@ class PathDataAttribute(StringAttribute): @classmethod def to_geometry(cls, commands): - # cls._logger.debug('Path:\n' + str(commands)) + cls._logger.info('Path:\n' + str(commands).replace('), ', '),\n ')) path = None for command, args in commands: command_lower = command.lower() - absolute = command_lower != command + absolute = command_lower != command # Upper case means absolute # if is_lower: # cls._logger.warning('incremental command') # raise NotImplementedError @@ -1045,23 +1041,24 @@ class PathDataAttribute(StringAttribute): # Fixme: m ??? path = Path2D(args) # Vector2D() else: - # Fixme: incremental and (q t a) need Path support if command_lower == 'l': path.line_to(args, absolute=absolute) elif command_lower == 'h': - path.horizontal_to(*args) + path.horizontal_to(*args, absolute=absolute) elif command_lower == 'v': - path.vertical_to(*args) + path.vertical_to(*args, absolute=absolute) elif command_lower == 'c': - path.cubic_to(*cls.as_vector(args)) + path.cubic_to(*cls.as_vector(args), absolute=absolute) elif command_lower == 's': - raise NotImplementedError + path.stringed_quadratic_to(*cls.as_vector(args), absolute=absolute) elif command_lower == 'q': - path.quadratic_to(*cls.as_vector(args)) + path.quadratic_to(*cls.as_vector(args), absolute=absolute) elif command_lower == 't': - raise NotImplementedError + path.stringed_cubic_to(*cls.as_vector(args), absolute=absolute) elif command_lower == 'a': - raise NotImplementedError + radius_x, radius_y, angle, large_arc, sweep, x, y = args + point = Vector2D(x, y) + path.arc_to(point, radius_x, radius_y, angle, bool(large_arc), bool(sweep), absolute=absolute) elif command_lower == 'z': path.close() @@ -1150,6 +1147,32 @@ class Rect(PositionMixin, RadiusMixin, SizeMixin, PathMixin, SvgElementMixin, Xm __tag__ = 'rect' + ############################################## + + @property + def geometry(self): + + # Fixme: width is str + width = float(self.width) + height = float(self.height) + + # Fixme: which one ??? + radius_x = self.rx + radius_y = self.ry + if radius_y == 0: + radius = None + else: + radius = radius_y + + point = Vector2D(self.x, self.y) + path = Path2D(point) + path.horizontal_to(width) + path.vertical_to(height, radius=radius) + path.horizontal_to(-width, radius=radius) + path.close(radius=radius_y, close_radius=radius) + + return path + #################################################################################################### class Stop(XmlObjectAdaptator): diff --git a/Patro/GeometryEngine/Path.py b/Patro/GeometryEngine/Path.py index 57ebe2e..d7b6a9a 100644 --- a/Patro/GeometryEngine/Path.py +++ b/Patro/GeometryEngine/Path.py @@ -47,10 +47,10 @@ class PathPart: ############################################## - def __init__(self, path, position): + def __init__(self, path, index): self._path = path - self._position = position + self._index = index ############################################## @@ -60,7 +60,7 @@ class PathPart: ############################################## def __repr__(self): - return self.__class__.__name__ + return '{0}(@{1._index})'.format(self.__class__.__name__, self) ############################################## @@ -69,22 +69,22 @@ class PathPart: return self._path @property - def position(self): - return self._position + def index(self): + return self._index - @position.setter - def position(self, value): - self._position = int(value) + @index.setter + def index(self, value): + self._index = int(value) ############################################## @property def prev_part(self): - return self._path[self._position -1] + return self._path[self._index -1] @property def next_part(self): - return self._path[self._position +1] + return self._path[self._index +1] ############################################## @@ -115,6 +115,60 @@ class PathPart: #################################################################################################### +class OnePointMixin: + + ############################################## + + @property + def point(self): + return self._point + + @point.setter + def point(self, value): + self._point = Vector2D(value) # self._path.__vector_cls__ + + ############################################## + + @property + def stop_point(self): + if self._absolute: + return self._point + else: + return self._point + self.start_point + + ############################################## + + def apply_transformation(self, transformation): + self._point = transformation * self._point + +#################################################################################################### + +class TwoPointMixin: + + @property + def point1(self): + return self._point1 + + @point1.setter + def point1(self, value): + self._point1 = Vector2D(value) # self._path.__vector_cls__ + + @property + def point2(self): + return self._point2 + + @point2.setter + def point2(self, value): + self._point2 = Vector2D(value) + + ############################################## + + def apply_transformation(self, transformation): + self._point1 = transformation * self._point1 + self._point2 = transformation * self._point2 + +#################################################################################################### + class LinearSegment(PathPart): r""" @@ -155,13 +209,15 @@ class LinearSegment(PathPart): ############################################## - def __init__(self, path, position, radius): + def __init__(self, path, index, radius, closing=False): - super().__init__(path, position) + super().__init__(path, index) self._bissector = None self._direction = None + self._start_radius = False + self._closing = bool(closing) self.radius = radius if self._radius is not None: if not isinstance(self.prev_part, LinearSegment): @@ -179,6 +235,31 @@ class LinearSegment(PathPart): ############################################## + def close(self, radius): + self.radius = radius + self._reset_cache() + self._start_radius = True + print('set close', self, self.__dict__) + + ############################################## + + @property + def prev_part(self): + if self._start_radius: + return self._path.stop_segment # or [-1] don't work + else: + # Fixme: super + return self._path[self._index -1] + + @property + def next_part(self): + if self._closing: + return self._path.start_segment # or [0] + else: + return self._path[self._index +1] + + ############################################## + @property def points(self): @@ -283,48 +364,29 @@ class LinearSegment(PathPart): #################################################################################################### -class PathSegment(LinearSegment): +class PathSegment(OnePointMixin, LinearSegment): ############################################## - def __init__(self, path, position, point, radius=None, absolute=False): - super().__init__(path, position, radius) + def __init__(self, path, index, point, radius=None, absolute=False, closing=False): + super().__init__(path, index, radius, closing) self.point = point self._absolute = bool(absolute) ############################################## def clone(self, path): - return self.__class__(path, self._position, self._point, self._radius, self._absolute) + return self.__class__(path, self._index, self._point, self._radius, self._absolute) ############################################## def apply_transformation(self, transformation): - self._point = transformation * self._point + OnePointMixin.apply_transformation(self, transformation) if self._radius is not None: self._radius = transformation * self._radius ############################################## - @property - def point(self): - return self._point - - @point.setter - def point(self, value): - self._point = Vector2D(value) # self._path.__vector_cls__ - - ############################################## - - @property - def stop_point(self): - if self._absolute: - return self._point - else: - return self._point + self.start_point - - ############################################## - @property def geometry(self): # Fixme: cache ??? @@ -338,8 +400,8 @@ class DirectionalSegment(LinearSegment): ############################################## - def __init__(self, path, position, length, radius=None): - super().__init__(path, position, radius) + def __init__(self, path, index, length, radius=None): + super().__init__(path, index, radius) self.length = length ############################################## @@ -352,7 +414,7 @@ class DirectionalSegment(LinearSegment): ############################################## def clone(self, path): - return self.__class__(path, self._position, self._length, self._radius) + return self.__class__(path, self._index, self._length, self._radius) ############################################## @@ -386,7 +448,7 @@ class DirectionalSegment(LinearSegment): ############################################## def to_path_segment(self): - return PathSegment(self._path, self._position, self.offset, self._radius, absolute=False) + return PathSegment(self._path, self._index, self.offset, self._radius, absolute=False) #################################################################################################### @@ -422,41 +484,15 @@ class SouthWestSegment(DirectionalSegment): #################################################################################################### -class TwoPointsMixin: - - @property - def point1(self): - return self._point1 - - @point1.setter - def point1(self, value): - self._point1 = Vector2D(value) # self._path.__vector_cls__ - - @property - def point2(self): - return self._point2 - - @point2.setter - def point2(self, value): - self._point2 = Vector2D(value) - - ############################################## - - def apply_transformation(self, transformation): - self._point1 = transformation * self._point1 - self._point2 = transformation * self._point2 - -#################################################################################################### - -class QuadraticBezierSegment(PathPart, TwoPointsMixin): +class QuadraticBezierSegment(PathPart, TwoPointMixin): # Fixme: abs / inc ############################################## - def __init__(self, path, position, point1, point2): + def __init__(self, path, index, point1, point2, absolute=False): - PathPart.__init__(self, path, position) + PathPart.__init__(self, path, index) self.point1 = point1 self.point2 = point2 @@ -464,7 +500,7 @@ class QuadraticBezierSegment(PathPart, TwoPointsMixin): ############################################## def clone(self, path): - return self.__class__(path, self._position, self._point1, self._point2) + return self.__class__(path, self._index, self._point1, self._point2) ############################################## @@ -485,13 +521,13 @@ class QuadraticBezierSegment(PathPart, TwoPointsMixin): #################################################################################################### -class CubicBezierSegment(PathPart, TwoPointsMixin): +class CubicBezierSegment(PathPart, TwoPointMixin): ############################################## - def __init__(self, path, position, point1, point2, point3): + def __init__(self, path, index, point1, point2, point3, absolute=False): - PathPart.__init__(self, path, position) + PathPart.__init__(self, path, index) self.point1 = point1 self.point2 = point2 @@ -500,12 +536,12 @@ class CubicBezierSegment(PathPart, TwoPointsMixin): ############################################## def clone(self, path): - return self.__class__(path, self._position, self._point1, self._point2, self._point3) + return self.__class__(path, self._index, self._point1, self._point2, self._point3) ############################################## def apply_transformation(self, transformation): - TwoPointsMixin.apply_transformation(self, transformation) + TwoPointMixin.apply_transformation(self, transformation) self._point3 = transformation * self._point3 ############################################## @@ -537,6 +573,102 @@ class CubicBezierSegment(PathPart, TwoPointsMixin): #################################################################################################### +class StringedQuadtraticBezierSegment(PathPart, TwoPointMixin): + + ############################################## + + def __init__(self, path, index, point1, absolute=False): + + PathPart.__init__(self, path, index) + + self.point1 = point1 + + ############################################## + + def clone(self, path): + return self.__class__(path, self._index, self._point1) + + ############################################## + + @property + def geometry(self): + # Fixme: cache ??? + # Fixme: !!! + return Segment2D(self.start_point, self._point2) + # return CubicBezier2D(self.start_point, self._point1, self._point2, self._point3) + +#################################################################################################### + +class StringedCubicBezierSegment(PathPart, TwoPointMixin): + + ############################################## + + def __init__(self, path, index, point1, point2, absolute=False): + + PathPart.__init__(self, path, index) + + # self.point1 = point1 + + ############################################## + + def clone(self, path): + return self.__class__(path, self._index, self._point1, self._point2) + + ############################################## + + @property + def geometry(self): + # Fixme: cache ??? + # Fixme: !!! + return Segment2D(self.start_point, self._point2) + # return CubicBezier2D(self.start_point, self._point1, self._point2, self._point3) + +#################################################################################################### + +class ArcSegment(OnePointMixin, PathPart): + + ############################################## + + def __init__(self, path, index, point, radius_x, radius_y, angle, large_arc, sweep, absolute=False): + + PathPart.__init__(self, path, index) + + self.point = point + self._absolute = bool(absolute) + + self._large_arc = bool(large_arc) + self._sweep = bool(sweep) + self._radius_x = radius_x + self._radius_y = radius_y + self._angle = angle + + ############################################## + + def clone(self, path): + return self.__class__( + path, + self._index, + self._point, + self._large_arc, self._sweep, + self._radius_x, self._radius_y, + self._angle + ) + + ############################################## + + @property + def points(self): + return self.start_point, self.stop_point + + ############################################## + + @property + def geometry(self): + # Fixme: !!! + return Segment2D(self.start_point, self.stop_point) + +#################################################################################################### + class Path2D(Primitive2DMixin, Primitive1P): """Class to implements 2D Path.""" @@ -547,7 +679,8 @@ class Path2D(Primitive2DMixin, Primitive1P): Primitive1P.__init__(self, start_point) - self._parts = [] + self._parts = [] # Fixme: segment ??? + self._is_closed = False ############################################## @@ -570,16 +703,38 @@ class Path2D(Primitive2DMixin, Primitive1P): def __iter__(self): return iter(self._parts) - def __getitem__(self, position): + def __getitem__(self, index): # try: # return self._parts[slice_] # except IndexError: # return None - position = int(position) - if 0 <= position < len(self._parts): - return self._parts[position] - else: - return None + index = int(index) + number_of_parts = len(self._parts) + if 0 <= index < number_of_parts: + return self._parts[index] + # elif self._is_closed: + # if index == -1: + # return self.start_segment + # elif index == number_of_parts: + # return self.stop_segment + return None + + ############################################## + + @property + def start_segment(self): + # Fixme: start_part ??? + return self._parts[0] + + @property + def stop_segment(self): + return self._parts[-1] + + ############################################## + + @property + def is_closed(self): + return self._is_closed ############################################## @@ -606,16 +761,43 @@ class Path2D(Primitive2DMixin, Primitive1P): ############################################## + @property + def bounding_box(self): + + # Fixme: cache + bounding_box = None + for item in self._parts: + interval = item.geometry.bounding_box + if bounding_box is None: + bounding_box = interval + else: + bounding_box |= interval + return bounding_box + + ############################################## + def move_to(self, point): self.p0 = point ############################################## - def horizontal_to(self, distance, radius=None): - return self._add_part(HorizontalSegment, distance, radius) + def horizontal_to(self, distance, radius=None, absolute=False): + if absolute: + return self._add_part(PathSegment, self.__vector_cls__(distance, 0), radius, + absolute=True) + else: + return self._add_part(HorizontalSegment, distance, radius) + + ############################################## + + def vertical_to(self, distance, radius=None, absolute=False): + if absolute: + return self._add_part(PathSegment, self.__vector_cls__(0, distance), radius, + absolute=True) + else: + return self._add_part(VerticalSegment, distance, radius) - def vertical_to(self, distance, radius=None): - return self._add_part(VerticalSegment, distance, radius) + ############################################## def north_to(self, distance, radius=None): return self._add_part(NorthSegment, distance, radius) @@ -646,17 +828,40 @@ class Path2D(Primitive2DMixin, Primitive1P): def line_to(self, point, radius=None, absolute=False): return self._add_part(PathSegment, point, radius, absolute=absolute) - def close(self, radius=None): - # Fixme: identify as close for SVG export + ############################################## + + def close(self, radius=None, close_radius=None): + # Fixme: identify as close for SVG export <-- meaning ??? # Fixme: radius must apply to start and stop - return self._add_part(PathSegment, self._p0, radius, absolute=True) + closing = close_radius is not None + segment = self._add_part(PathSegment, self._p0, radius, absolute=True, closing=closing) + if closing: + self.start_segment.close(close_radius) + self._is_closed = True + return segment + + ############################################## + + def quadratic_to(self, point1, point2, absolute=True): + return self._add_part(QuadraticBezierSegment, point1, point2, absolute=absolute) + + ############################################## + + def cubic_to(self, point1, point2, point3, absolute=True): + return self._add_part(CubicBezierSegment, point1, point2, point3, absolute=absolute) + + ############################################## + + def stringed_quadratic_to(self, point, absolute=True): + return self._add_part(StringedQuadraticBezierSegment, point, absolute=absolute) ############################################## - def quadratic_to(self, point1, point2): - return self._add_part(QuadraticBezierSegment, point1, point2) + def stringed_cubic_to(self, point1, point2, absolute=True): + return self._add_part(StringedCubicBezierSegment, point1, point2, absolute=absolute) ############################################## - def cubic_to(self, point1, point2, point3): - return self._add_part(CubicBezierSegment, point1, point2, point3) + def arc_to(self, point, radius_x, radius_y, angle, large_arc, sweep, absolute=True): + return self._add_part(ArcSegment, point, radius_x, radius_y, angle, large_arc, sweep, + absolute=absolute) diff --git a/Patro/GraphicEngine/GraphicScene/Scene.py b/Patro/GraphicEngine/GraphicScene/Scene.py index e15c209..26e693b 100644 --- a/Patro/GraphicEngine/GraphicScene/Scene.py +++ b/Patro/GraphicEngine/GraphicScene/Scene.py @@ -375,36 +375,57 @@ class GraphicSceneScope: def add_path(self, path, path_style): items = [] + + def add_bulge(segment): + arc = segment.bulge_geometry + arc_item = self.circle( + arc.center, arc.radius, + path_style, + start_angle=arc.domain.start, + stop_angle=arc.domain.stop, + user_data=segment, + ) + items.append(arc_item) + + def add_by_method(method, segment): + item = method( + *segment.points, + path_style, + user_data=segment, + ) + items.append(item) + + def add_segment(segment): + add_by_method(self.segment, segment) + + def add_quadratic(segment): + add_by_method(self.quadratic_bezier, segment) + + def add_cubic(segment): + add_by_method(self.cubic_bezier, segment) + for segment in path: + item = None if isinstance(segment, Path.LinearSegment): + # if segment._start_radius is True: + # continue if segment.radius is not None: - arc = segment.bulge_geometry - arc_item = self.circle( - arc.center, arc.radius, - path_style, - start_angle=arc.domain.start, - stop_angle=arc.domain.stop, - user_data=segment, - ) - items.append(arc_item) - item = self.segment( - *segment.points, - path_style, - user_data=segment, - ) + add_bulge(segment) + # if segment._closing is True: + # start_segment = path.start_segment + # add_bulge(start_segment) + # add_segment(start_segment) + add_segment(segment) elif isinstance(segment, Path.QuadraticBezierSegment): - item = self.quadratic_bezier( - *segment.points, - path_style, - user_data=segment, - ) + add_quadratic(segment) elif isinstance(segment, Path.CubicBezierSegment): - item = self.cubic_bezier( - *segment.points, - path_style, - user_data=segment, - ) - items.append(item) + add_cubic(segment) + elif isinstance(segment, Path.ArcSegment): + add_segment(segment) + elif isinstance(segment, Path.StringedQuadtraticBezierSegment): + pass + elif isinstance(segment, Path.StringedCubicBezierSegment): + pass ############################################## diff --git a/examples/file-format/svg/test-svg-import.py b/examples/file-format/svg/test-svg-import.py index d82d489..e947cf6 100644 --- a/examples/file-format/svg/test-svg-import.py +++ b/examples/file-format/svg/test-svg-import.py @@ -25,8 +25,10 @@ import logging from pathlib import Path # Disable if executed by patro -# from Patro.Common.Logging import Logging -# Logging.setup_logging() +use_qt = True +if not use_qt: + from Patro.Common.Logging import Logging + Logging.setup_logging() from Patro.FileFormat.Svg import SvgFormat from Patro.FileFormat.Svg.SvgFile import SvgFile, SvgFileInternal @@ -81,15 +83,15 @@ class SceneImporter(SvgFileInternal): ############################################## def on_group(self, group): - pass # self._logger.info('Group: {}\n{}'.format(group.id, group)) + pass ############################################## def on_graphic_item(self, item): - # self._logger.info('Item: {}\n{}'.format(item.id, item)) state = self._dispatcher.state.clone().merge(item) + self._logger.info('Item: {}\n{}'.format(item.id, item)) # self._logger.info('Item State:\n' + str(state)) self._item_counter += 1 @@ -109,19 +111,24 @@ class SceneImporter(SvgFileInternal): if isinstance(item, SvgFormat.Path): # and state.stroke_dasharray is None path = item.path_data - path = path.transform(transformation) - # Fixme: - for part in path: - self._update_bounding_box(part) + if path is not None: # Fixme: + # path = path.transform(transformation) + # Fixme: + for part in path: + self._update_bounding_box(part) + self._scene.add_path(path, path_style) + elif isinstance(item, SvgFormat.Rect): + path = item.geometry self._scene.add_path(path, path_style) #################################################################################################### -# svg_path = find_data_path('svg', 'demo.svg') -svg_path = find_data_path('patterns-svg', 'veravenus-little-bias-dress.pattern-a0.svg') -# svg_path = find_data_path('patterns-svg', 'test.svg') +svg_path = find_data_path('svg', 'demo.svg') +# svg_path = find_data_path('patterns-svg', 'veravenus-little-bias-dress.pattern-a0.svg') +# svg_path = find_data_path('patterns-svg', 'veravenus-little-bias-dress.pattern-a0.no-text-zaggy.svg') # svg_file = SvgFile(svg_path) scene_importer = SceneImporter(svg_path) -application.qml_application.scene = scene_importer.scene +if use_qt: + application.qml_application.scene = scene_importer.scene -- GitLab