Skip to content
......@@ -24,8 +24,11 @@
####################################################################################################
__class__ = ['GraphicScene']
__all__ = [
'GraphicScene',
# sphinx
'GraphicSceneScope',
]
####################################################################################################
......@@ -33,6 +36,18 @@ import logging
import rtree
from Patro.GeometryEngine import (
Bezier,
Conic,
Line,
Path,
Polygon,
Polyline,
Rectangle,
Segment,
Spline,
Triangle,
)
from Patro.GeometryEngine.Transformation import AffineTransformation2D
from Patro.GeometryEngine.Vector import Vector2D
from . import GraphicItem
......@@ -256,12 +271,104 @@ class GraphicSceneScope:
##############################################
def add_geometry(self, item, path_style):
"""Add a geometry primitive"""
ctor = None
points = None
args = []
args_tail = [path_style]
kwargs = dict(user_data=item)
# Bezier
if isinstance(item, Bezier.QuadraticBezier2D):
# ctor = self._scene.quadratic_bezier
# raise NotImplementedError
ctor = self._scene.cubic_bezier
points = list(item.to_cubic().points)
elif isinstance(item, Bezier.CubicBezierItem):
ctor = self._scene.cubic_bezier
# Conic
elif isinstance(item, Conic.Circle2D):
ctor = self._scene.circle
args = [item.radius]
if item.domain:
kwargs['start_angle'] = item.domain.start
kwargs['stop_angle'] = item.domain.stop
elif isinstance(item, Conic.Ellipse2D):
ctor = self._scene.ellipse
args = [item.x_radius, item.y_radius, item.angle]
# Line
elif isinstance(item, Line.Line2D):
# Fixme: extent ???
raise NotImplementedError
# Path
elif isinstance(item, Path.Path2D):
raise NotImplementedError
# Polygon
elif isinstance(item, Path.Polygon2D):
# Fixme: to path
raise NotImplementedError
# Polyline
elif isinstance(item, Polyline.Polyline2D):
ctor = self._scene.polyline
# fixme: to path
# Rectangle
elif isinstance(item, Rectangle.Rectangle2D):
ctor = self._scene.rectangle
# Fixme: to path
# Segment
if isinstance(item, Segment.Segment2D):
ctor = self._scene.segment
# Spline
elif isinstance(item, Spline.BSpline2D):
return self._add_spline(item, path_style)
# Triangle
if isinstance(item, Triangle.Triangle2D):
# Fixme: to path
raise NotImplementedError
# Not implemented
else:
raise NotImplementedError
if ctor is not None:
if points is None:
points = list(item.points)
return ctor(*points, *args, *args_tail, **kwargs)
##############################################
def add_spline(self, item, path_style):
return [
self._scene.cubic_bezier(*bezier.points, path_style, user_data=item)
for bezier in item.to_bezier()
]
##############################################
def bezier_path(self, points, degree, *args, **kwargs):
"""Add a Bézier curve with the given control points and degree"""
if degree == 1:
method = self.segment
elif degree == 2:
# Fixme:
method = self.quadratic_bezier
raise NotImplementedError
elif degree == 3:
method = self.cubic_bezier
else:
......
......@@ -28,7 +28,7 @@ from IntervalArithmetic import Interval2D
from QtShim.QtCore import (
Property, Signal, Slot, QObject,
QRectF, QSizeF, QPointF, Qt,
QRectF, QSize, QSizeF, QPointF, Qt,
)
from QtShim.QtGui import QColor, QFont, QFontMetrics, QImage, QPainter, QPainterPath, QBrush, QPen
# from QtShim.QtQml import qmlRegisterType
......@@ -37,7 +37,7 @@ from QtShim.QtQuick import QQuickPaintedItem
from .Painter import Painter
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicEngine.GraphicScene.Scene import GraphicScene
from Patro.GraphicStyle import StrokeStyle
from Patro.GraphicStyle import StrokeStyle, CapStyle, JoinStyle
####################################################################################################
......@@ -70,6 +70,19 @@ class QtPainter(Painter):
StrokeStyle.DashDotDotLine: Qt.DashDotDotLine,
}
__CAP_STYLE__ = {
CapStyle.FlatCap: Qt.FlatCap,
CapStyle.SquareCap: Qt.SquareCap,
CapStyle.RoundCap: Qt.RoundCap,
}
__JOIN_STYLE__ = {
JoinStyle.MiterJoin: Qt.MiterJoin,
JoinStyle.BevelJoin: Qt.BevelJoin,
JoinStyle.RoundJoin: Qt.RoundJoin,
JoinStyle.SvgMiterJoin: Qt.SvgMiterJoin,
}
_logger = _module_logger.getChild('QtPainter')
##############################################
......@@ -106,6 +119,38 @@ class QtPainter(Painter):
##############################################
def to_svg(self, path, scale=10, dpi=100, title='', description=''):
"""Render the scene to SVG"""
from QtShim.QtSvg import QSvgGenerator
generator = QSvgGenerator()
generator.setFileName(str(path))
generator.setTitle(str(title))
generator.setDescription(str(description))
generator.setResolution(dpi)
# Fixme: scale
# Scale applied to (x,y) and radius but not line with
self._scale = scale
bounding_box = self._scene.bounding_box
size = QSize(*[x*self._scale for x in bounding_box.size])
view_box = QRectF(*[x*self._scale for x in bounding_box.rect])
generator.setSize(size)
generator.setViewBox(view_box)
painter = QPainter()
painter.begin(generator)
self.paint(painter)
painter.end()
self._scale = None
##############################################
def paint(self, painter):
self._logger.info('paint')
......@@ -118,15 +163,16 @@ class QtPainter(Painter):
##############################################
def length_scene_to_viewport(self, length):
raise NotImplementedError
return length * self._scale
@property
def scene_area(self):
raise NotImplementedError
return None
def scene_to_viewport(self, position):
return QPointF(position.x * self._scale, position.y * self._scale)
# Note: painter.scale apply to text as well
raise NotImplementedError
# point = QPointF(position.x, position.y)
# point += self._translation
......@@ -156,6 +202,7 @@ class QtPainter(Painter):
color = path_syle.stroke_color
if color is not None:
color = QColor(str(color))
color.setAlphaF(path_syle.stroke_alpha)
else:
color = None
line_style = self.__STROKE_STYLE__[path_syle.stroke_style]
......@@ -165,34 +212,41 @@ class QtPainter(Painter):
if item.selected:
line_width *= 4
fill_color = path_syle.fill_color
if fill_color is not None:
color = QColor(str(fill_color))
color.setAlphaF(path_syle.fill_alpha)
self._painter.setBrush(color)
# return None
else:
self._painter.setBrush(Qt.NoBrush)
# print(item, color, line_style)
if color is None or line_style is StrokeStyle.NoPen:
# invisible item
pen = QPen(Qt.NoPen)
# print('Warning Pen:', item, item.user_data, color, line_style)
return None
else:
pen = QPen(
QBrush(color),
line_width,
line_style,
self.__CAP_STYLE__[path_syle.cap_style],
self.__JOIN_STYLE__[path_syle.join_style],
)
self._painter.setPen(pen)
return pen
fill_color = path_syle.fill_color
if fill_color is not None:
color = QColor(str(fill_color))
self._painter.setBrush(color)
else:
self._painter.setBrush(Qt.NoBrush)
return None
##############################################
def _paint_grid(self):
area = self.scene_area
# Fixme:
if area is None:
return
xinf, xsup = area.x.inf, area.x.sup
yinf, ysup = area.y.inf, area.y.sup
......@@ -247,16 +301,20 @@ class QtPainter(Painter):
pen = self._set_pen(item)
rectangle = QRectF(
center + QPointF(-radius, radius),
center + QPointF(radius, -radius),
)
start_angle, stop_angle = [int(angle*16) for angle in (item.start_angle, item.stop_angle)]
span_angle = stop_angle - start_angle
if span_angle < 0:
span_angle = 5760 + span_angle
self._painter.drawArc(rectangle, start_angle, span_angle)
## self._painter.drawArc(center.x, center.y, radius, radius, 0, 360)
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),
)
start_angle, stop_angle = [int(angle*16) for angle in (item.start_angle, item.stop_angle)]
span_angle = stop_angle - start_angle
if span_angle < 0:
span_angle = 5760 + span_angle
self._painter.drawArc(rectangle, start_angle, span_angle)
# self._painter.drawArc(center.x, center.y, radius, radius, start_angle, stop_angle)
##############################################
......@@ -480,6 +538,7 @@ class ViewportArea:
def fit_scene(self):
# Fixme: AttributeError: 'NoneType' object has no attribute 'center'
if self:
center = np.array(self.scene_area.center, dtype=np.float)
scale, axis = self._compute_scale_to_fit_scene()
......
......@@ -28,6 +28,10 @@ __all__ = ['Color', 'ColorDataBase']
####################################################################################################
import colorsys
####################################################################################################
class Color:
"""Class to define a colour
......@@ -59,19 +63,32 @@ class Color:
if number_of_args == 1:
color = args[0]
if isinstance(str, Color):
self._red = color.red
self._green = color.green
self._blue = color.blue
self._red = color.red_float
self._green = color.green_float
self._blue = color.blue_float
else:
rgb = str(color)
if not rgb.startswith('#'):
raise ValueError('Invalid color {}'.format(rgb))
rgb = rgb[1:]
self._red = int(rgb[:2], 16)
self._green = int(rgb[2:4], 16)
self._blue = int(rgb[-2:], 16)
red, green, blue = rgb[:2], rgb[2:4], rgb[-2:]
self._red, self._green, self._blue = [self._to_float(int(x, 16))
for x in (red, green, blue)]
elif number_of_args == 3:
self._red, self._green, self._blue = [int(arg) for arg in args]
self._red, self._green, self._blue = [self._check_value(arg) for arg in args]
else:
self._red, self._green, self._blue = 0
if 'hue' in kwargs:
if 'light' in kwargs:
hue, light, saturation = [kwargs[x] for x in ('hue', 'light', 'saturation')]
red, green, blue = colorsys.hls_to_rgb(hue, light, saturation)
elif 'value' in kwargs:
hue, value, saturation = [kwargs[x] for x in ('hue', 'value', 'saturation')]
red, green, blue = colorsys.hsv_to_rgb(hue, saturation, value)
else:
raise ValueError('Missing color parameter')
self._red, self._green, self._blue = [self._check_value(x) for x in (red, green, blue)]
# self._name = kwargs.get('name', None)
if 'name' in kwargs:
......@@ -87,7 +104,7 @@ class Color:
##############################################
def __str__(self):
return self.__STR_FORMAT__.format(self._red, self._green, self._blue)
return self.__STR_FORMAT__.format(self.red, self.green, self.blue)
##############################################
......@@ -96,43 +113,81 @@ class Color:
##############################################
@staticmethod
def _to_int(x):
return int(x*255)
@staticmethod
def _to_float(x):
return x/255
##############################################
def _check_value(self, value):
if isinstance(value, int):
if 0 <= value <= 255:
return value
return self._to_float(value)
if isinstance(value, float):
if 0 <= value <= 1:
return int(value * 255)
# return int(value * 255)
return value # keep float
raise ValueError('Invalid colour {}'.format(value))
##############################################
@property
def red(self):
def red_float(self):
return self._red
@property
def red(self):
return self._to_int(self._red)
@red.setter
def red(self, value):
self._red = self._check_value(value)
@property
def green(self):
def green_float(self):
return self._green
@property
def green(self):
return self._to_int(self._green)
@green.setter
def green(self, value):
self._green = self._check_value(value)
@property
def blue(self):
def blue_float(self):
return self._blue
@property
def blue(self):
return self._to_int(self._blue)
@blue.setter
def blue(self, value):
self._blue = self._check_value(value)
##############################################
# note hue and saturation is ambiguous
@property
def hls(self):
return colorsys.rgb_to_hls(self._red, self._green, self._blue)
@property
def hsv(self):
return colorsys.rgb_to_hsv(self._red, self._green, self._blue)
##############################################
@property
def name(self):
return self._name
......
......@@ -26,7 +26,7 @@ This module import :class:`Color.Colors`.
####################################################################################################
__all__ = ['Colors', 'StrokeStyle']
__all__ = ['Colors', 'StrokeStyle', 'CapStyle', 'JoinStyle']
####################################################################################################
......@@ -41,9 +41,38 @@ class StrokeStyle(Enum):
"""Enum class to define stroke styles"""
NoPen = auto()
NoPen = auto() # Inivisble ?
SolidLine = auto()
DashLine = auto()
DotLine = auto()
DashDotLine = auto()
DashDotDotLine = auto()
# Custom
####################################################################################################
class CapStyle(Enum):
"""Enum class to define cap styles"""
#: a square line end that does not cover the end point of the line
FlatCap = auto()
#: a square line end that covers the end point and extends beyond it by half the line width
SquareCap = auto()
#: a rounded line end.
RoundCap = auto()
####################################################################################################
class JoinStyle(Enum):
"""Enum class to define join styles"""
#! The outer edges of the lines are extended to meet at an angle, and this area is filled.
MiterJoin = auto()
#: The triangular notch between the two lines is filled.
BevelJoin = auto()
#: A circular arc between the two lines is filled.
RoundJoin = auto()
#: A miter join corresponding to the definition of a miter join in the SVG 1.2 Tiny specification.
SvgMiterJoin = auto()
......@@ -26,7 +26,7 @@
import logging
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicEngine.GraphicScene.GraphicItem import GraphicPathStyle, Font
from Patro.GraphicEngine.GraphicScene.GraphicStyle import GraphicPathStyle, Font
from Patro.GraphicEngine.GraphicScene.Scene import GraphicScene
from . import SketchOperation
from .Calculator import Calculator
......
......@@ -40,6 +40,7 @@ from QtShim.QtQml import qmlRegisterUncreatableType
from QtShim.QtQuick import QQuickPaintedItem, QQuickView
# from QtShim.QtQuickControls2 import QQuickStyle
from Patro.Common.Platform import QtPlatform
from Patro.GraphicEngine.Painter.QtPainter import QtScene, QtQuickPaintedSceneItem
from .rcc import PatroRessource
......@@ -129,6 +130,9 @@ class Application(QObject):
self._engine = QQmlApplicationEngine()
self._qml_application = QmlApplication(self)
self._platform = QtPlatform()
# self._logger.info('\n' + str(self._platform))
self._scene = None
# self._load_translation()
......@@ -154,6 +158,10 @@ class Application(QObject):
def qml_application(self):
return self._qml_application
@property
def platform(self):
return self._platform
##############################################
@classmethod
......
......@@ -35,3 +35,6 @@
.. |Valentina| replace:: Valentina
.. _Valentina: https://bitbucket.org/dismine/valentina
.. |Tikz| replace:: Tikz
.. _Tikz: https://ctan.org/pkg/pgf?lang=en
......@@ -139,16 +139,38 @@ pygments_style = 'sphinx'
# Options for Autodoc
#
# http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html
# Show both class-level docstring and __init__ docstring in class documentation
autoclass_content = 'both'
autodoc_default_flags = [
'members',
'undoc-members',
# 'private-members',
# 'special-members',
# 'inherited-members',
# 'show-inheritance',
]
autodoc_member_order = 'alphabetical' # groupwise bysource
# autodoc_default_flags = [
# 'members',
# 'undoc-members',
#
# # 'members',
# # 'undoc-members',
# # 'private-members',
# # 'special-members',
# # 'inherited-members',
# # 'show-inheritance',
# # 'ignore-module-all',
# # 'exclude-members',
# ]
autodoc_default_options = {
'members': None,
# 'member-order': 'alphabetical' ,
'undoc-members': None,
# 'private-members': ,
# 'special-members': ,
# 'inherited-members': ,
# 'show-inheritance': ,
'ignore-module-all': None,
# 'exclude-members': ,
}
####################################################################################################
#
......
......@@ -63,14 +63,27 @@ A painter is responsible to render the scene on the screen or a graphic file for
engine is able to render on the following:
* show drawing on screen with : |Matplotlib|_, |Qt|_
* export drawing to : PDF, SVG, DXF, LaTeX Tikz
* export tiled pattern on A4 sheets : PDF, LaTeX Tikz
* export drawing to : SVG, PDF, DXF, LaTeX |Tikz|_
* export tiled pattern on A4 sheets : PDF, LaTeX |Tikz|_
.. duplicated note
.. note:: PDF and SVG format are convertible to each other without data loss
(font handling require more attention).
.. note:: The |Inkscape|_ free software is able to import from / export to a lot of file formats
like SVG, PDF, DXF and to render the drawing to an image format. This job can be done in
batch from command line.
Also the graphic engine is able to render a DXF made of these graphic items: line, circle, arc,
ellipse, lwpolyline and spline.
For expert, the LaTeX output can be used to modify the drawing using the power of the |Tikz|_ (PGF)
graphic package.
Implementation details:
* SVG can be rendered using the SVG and Qt painter
* PDF export is implemented with the help of the |Reportlab|_ package
* DXF import/export is implemented with the help of the |ezdxf|_ package of `Manfred Moitzi
<https://github.com/mozman>`_
......
......@@ -6,4 +6,8 @@
* `Valentina developed by Roman Telezhynskyi <https://bitbucket.org/dismine/valentina>`_
* https://inkstitch.org — An open source machine embroidery design platform based on Inkscape.
* https://github.com/EmbroidePy/pyembroidery — pyembroidery library for reading and writing a variety of embroidery formats.
* https://github.com/Embroidermodder/Embroidermodder — Free machine embroidery software supporting a variety of formats.
.. Seamly2D
......@@ -10,7 +10,7 @@ from Patro.GeometryEngine.Conic import Circle2D, Ellipse2D
from Patro.GeometryEngine.Segment import Segment2D
from Patro.GeometryEngine.Spline import BSpline2D
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicEngine.GraphicScene.GraphicItem import GraphicPathStyle, GraphicBezierStyle
from Patro.GraphicEngine.GraphicScene.GraphicStyle import GraphicPathStyle, GraphicBezierStyle
from Patro.GraphicEngine.Painter.QtPainter import QtScene
from Patro.GraphicStyle import Colors, StrokeStyle
......