Newer
Older
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
"""Module to implement a Qt Painter.
"""
####################################################################################################
import logging
import numpy as np
from IntervalArithmetic import Interval2D
from QtShim.QtCore import (
Property, Signal, Slot, QObject,
from QtShim.QtGui import QColor, QFont, QFontMetrics, QImage, QPainter, QPainterPath, QBrush, QPen
# from QtShim.QtQml import qmlRegisterType
from QtShim.QtQuick import QQuickPaintedItem
Fabrice Salvaire
committed
from .Painter import Painter
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicEngine.GraphicScene.Scene import GraphicScene
from Patro.GraphicStyle import StrokeStyle, CapStyle, JoinStyle
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class QtScene(QObject, GraphicScene):
"""Class to add Qt Object features to GraphicScene ."""
_logger = _module_logger.getChild('QtScene')
##############################################
def __init__(self):
QObject.__init__(self)
GraphicScene.__init__(self)
####################################################################################################
class QtPainter(Painter):
"""Class to implement a Qt painter."""
Fabrice Salvaire
committed
None: None, # Fixme: ???
StrokeStyle.NoPen: Qt.NoPen,
StrokeStyle.SolidLine: Qt.SolidLine,
StrokeStyle.DashLine: Qt.DashLine,
StrokeStyle.DotLine: Qt.DotLine,
StrokeStyle.DashDotLine: Qt.DashDotLine,
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')
##############################################
def __init__(self, scene=None):
super().__init__(scene)
Fabrice Salvaire
committed
self._show_grid = True
# self._translation = QPointF(0, 0)
# self._scale = 1
##############################################
# @property
# def translation(self):
# return self._translation
# @translation.setter
# def translation(self, value):
# self._translation = value
# @property
# def scale(self):
# return self._scale
# @scale.setter
# def scale(self, value):
# print('set scale', value)
# self._scale = value
##############################################
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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
##############################################
Fabrice Salvaire
committed
if self._show_grid:
self._paint_grid()
##############################################
Fabrice Salvaire
committed
def length_scene_to_viewport(self, length):
Fabrice Salvaire
committed
@property
def scene_area(self):
Fabrice Salvaire
committed
return QPointF(position.x * self._scale, position.y * self._scale)
# point = QPointF(position.x, position.y)
# point += self._translation
# point *= self._scale
# point = QPointF(point.x(), -point.y())
# return point
##############################################
def cast_position(self, position):
Fabrice Salvaire
committed
"""Cast coordinate, apply scope transformation and convert scene to viewport, *position* can be a
coordinate name string of a:class:`Vector2D`.
"""
position = super().cast_position(position)
return self.scene_to_viewport(position)
##############################################
def _set_pen(self, item):
path_syle = item.path_style
# print('_set_pen', item, path_syle)
Fabrice Salvaire
committed
color = QColor('red') # Fixme: style
color = path_syle.stroke_color
if color is not None:
Fabrice Salvaire
committed
color = QColor(str(color))
color.setAlphaF(path_syle.stroke_alpha)
line_style = self.__STROKE_STYLE__[path_syle.stroke_style]
Fabrice Salvaire
committed
line_width = path_syle.line_width_as_float
# Fixme: selection style
if item.selected:
line_width *= 4
fill_color = path_syle.fill_color
if fill_color is not None:
color = QColor(str(fill_color))
self._painter.setBrush(color)
# return None
else:
self._painter.setBrush(Qt.NoBrush)
# print(item, color, line_style)
Fabrice Salvaire
committed
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)
Fabrice Salvaire
committed
else:
Fabrice Salvaire
committed
QBrush(color),
self.__CAP_STYLE__[path_syle.cap_style],
self.__JOIN_STYLE__[path_syle.join_style],
Fabrice Salvaire
committed
return pen
##############################################
def _paint_grid(self):
area = self.scene_area
# Fixme:
if area is None:
return
Fabrice Salvaire
committed
xinf, xsup = area.x.inf, area.x.sup
yinf, ysup = area.y.inf, area.y.sup
length = min(area.x.length, area.y.length)
Fabrice Salvaire
committed
color = QColor('black')
brush = QBrush(color)
pen = QPen(brush, .75)
self._painter.setPen(pen)
self._painter.setBrush(Qt.NoBrush)
step = max(10**int(math.log10(length)), 10)
small_step = step // 10
self._logger.info('Grid of {}/{} for {:.1f} mm'.format(step, small_step, length))
Fabrice Salvaire
committed
self._paint_axis_grid(xinf, xsup, yinf, ysup, True, step)
self._paint_axis_grid(yinf, ysup, xinf, xsup, False, step)
color = QColor('black')
brush = QBrush(color)
pen = QPen(brush, .25)
self._painter.setPen(pen)
self._painter.setBrush(Qt.NoBrush)
self._paint_axis_grid(xinf, xsup, yinf, ysup, True, small_step)
self._paint_axis_grid(yinf, ysup, xinf, xsup, False, small_step)
Fabrice Salvaire
committed
##############################################
def _paint_axis_grid(self, xinf, xsup, yinf, ysup, is_x, step):
for i in range(int(xinf // step), int(xsup // step) +1):
x = i*step
if xinf <= x <= xsup:
if is_x:
p0 = Vector2D(x, yinf)
p1 = Vector2D(x, ysup)
else:
p0 = Vector2D(yinf, x)
p1 = Vector2D(ysup, x)
p0 = self.cast_position(p0)
p1 = self.cast_position(p1)
self._painter.drawLine(p0, p1)
##############################################
def paint_CoordinateItem(self, item):
self._coordinates[item.name] = self.scene_to_viewport(item.position)
##############################################
def _paint_arc(self, item, center, radius_x, radius_y):
Fabrice Salvaire
committed
if item.is_closed:
self._painter.drawEllipse(center, radius, radius)
else:
# drawArc cannot be filled !
rectangle = QRectF(
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
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)
##############################################
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)
radius_x = self.length_scene_to_viewport(item.radius_x)
radius_y = self.length_scene_to_viewport(item.radius_y)
pen = self._set_pen(item)
# Fixme: angle !!!
self._paint_arc(item, center, radius_x, radius_y)
##############################################
def _paint_cubic(self, item, vertices):
Fabrice Salvaire
committed
pen = self._set_pen(item)
path = QPainterPath()
path.moveTo(vertices[0])
path.cubicTo(*vertices[1:])
self._painter.drawPath(path)
Fabrice Salvaire
committed
path_style = item.path_style
# if path_style.show_control:
if getattr(path_style, 'show_control', False):
color = QColor(str(path_style.control_color))
brush = QBrush(color)
pen = QPen(brush, 1) # Fixme
self._painter.setPen(pen)
self._painter.setBrush(Qt.NoBrush)
path = QPainterPath()
path.moveTo(vertices[0])
for vertex in vertices[1:]:
path.lineTo(vertex)
self._painter.drawPath(path)
# Fixme:
radius = 3
self._painter.setBrush(brush)
for vertex in vertices:
self._painter.drawEllipse(vertex, radius, radius)
self._painter.setBrush(Qt.NoBrush) # Fixme:
##############################################
def paint_QuadraticBezierItem(self, item):
vertices = [self.cast_position(position) for position in item.cubic_positions]
self._paint_cubic(item, vertices)
##############################################
def paint_CubicBezierItem(self, item):
vertices = self.cast_item_positions(item)
self._paint_cubic(item, vertices)
##############################################
def paint_ImageItem(self, item):
vertices = self.cast_item_positions(item)
rec = QRectF(vertices[0], vertices[1])
image = item.image
height, width, bytes_per_line = image.shape
bytes_per_line *= width
qimage = QImage(image, width, height, bytes_per_line, QImage.Format_RGB888)
self._painter.drawImage(rec, qimage)
##############################################
vertices = self.cast_item_positions(item)
self._painter.drawLine(*vertices)
##############################################
Fabrice Salvaire
committed
def paint_PolylineItem(self, item):
self._set_pen(item)
vertices = self.cast_item_positions(item)
path = QPainterPath()
path.moveTo(vertices[0])
for vertex in vertices[1:]:
path.lineTo(vertex)
self._painter.drawPath(path)
##############################################
def paint_TextItem(self, item):
position = self.cast_position(item.position)
font = item.font
qfont = QFont(font.family, font.point_size) # weight, italic = False
# font_metrics = QFontMetrics(qfont)
# height = font_metrics.height()
# width = font_metrics.width(item.text)
self._painter.setFont(qfont)
####################################################################################################
"""Class to implement a viewport."""
_logger = _module_logger.getChild('ViewportArea')
##############################################
def __init__(self):
self._scene = None
# self._width = None
# self._height = None
self._viewport_size = None
self._scale = 1
self._center = None
self._area = None
##############################################
def __bool__(self):
return self._scene is not None
##############################################
@classmethod
def _to_np_array(cls, *args):
if len(args) == 1:
args = args[0]
return np.array(args, dtype=np.float)
##############################################
@classmethod
def _point_to_np(cls, point):
return cls._to_np_array(point.x(), point.y())
##############################################
@property
def viewport_size(self):
return self._viewport_size
# return (self._width, self._height)
@viewport_size.setter
def viewport_size(self, geometry):
# self._width = geometry.width()
# self._height = geometry.height()
self._viewport_size = self._to_np_array(geometry.width(), geometry.height())
if self:
self._update_viewport_area()
##############################################
@property
def scene(self):
return self._scene
@scene.setter
def scene(self, value):
if not isinstance(value, GraphicScene):
raise ValueError
self._scene = value
@property
def scene_area(self):
if self:
return self._scene.bounding_box
else:
return None
##############################################
# @property
# def scale(self):
# return self._scale # px / mm
@property
def scale_px_by_mm(self):
return self._scale
@property
def scale_mm_by_px(self):
return 1 / self._scale
@property
def center(self):
return self._center
# @property
# def center_as_point(self):
# return QPointF(self._center[0], self._center[1])
@property
def area(self):
return self._area
##############################################
def _update_viewport_area(self):
offset = self._viewport_size / 2 * self.scale_mm_by_px
x, y = self._center
dx, dy = offset
self._area = Interval2D(
(x - dx, x + dx),
(y - dy, y + dy),
)
# Fixme: QPointF ???
self._translation = - QPointF(self._area.x.inf, self._area.y.sup)
# self._logger.debug('_update_viewport_area', self._center, self.scale_mm_by_px, self._area)
##############################################
def _compute_scale_to_fit_scene(self, margin=None):
# width_scale = self._width / scene_area.x.length
# height_scale = self._height / scene_area.y.length
# scale = min(width_scale, height_scale)
# scale [px/mm]
# Add 2% to scene for margin
margin_scale = 1 + 2 / 100
axis_scale = self._viewport_size / (self._to_np_array(self.scene_area.size) * margin_scale)
axis = axis_scale.argmin()
scale = axis_scale[axis]
return scale, axis
##############################################
def zoom_at(self, center, scale):
self._center = center
self._scale = scale
self._update_viewport_area()
##############################################
def fit_scene(self):
Fabrice Salvaire
committed
# Fixme: AttributeError: 'NoneType' object has no attribute 'center'
center = self._to_np_array(self.scene_area.center)
scale, axis = self._compute_scale_to_fit_scene()
self.zoom_at(center, scale)
##############################################
def scene_to_viewport(self, position):
point = QPointF(position.x, position.y)
point += self._translation
point *= self._scale
point = QPointF(point.x(), -point.y())
return point
##############################################
def viewport_to_scene(self, position):
point = QPointF(position.x(), -position.y())
point /= self._scale
point -= self._translation
return self._point_to_np(point)
##############################################
def length_scene_to_viewport(self, length):
return length * self._scale
##############################################
def length_viewport_to_scene(self, length):
return length / self._scale
##############################################
def pan_delta_to_scene(self, position):
point = self._point_to_np(position)
point *= self.scale_mm_by_px
return point
####################################################################################################
class QtQuickPaintedSceneItem(QQuickPaintedItem, QtPainter):
"""Class to implement a painter as Qt Quick item"""
_logger = _module_logger.getChild('QtQuickPaintedSceneItem')
##############################################
def __init__(self, parent=None):
QQuickPaintedItem.__init__(self, parent)
QtPainter.__init__(self)
self.setAntialiasing(True)
# self.setRenderTarget(QQuickPaintedItem.Image) # high quality antialiasing
self.setRenderTarget(QQuickPaintedItem.FramebufferObject) # use OpenGL
self._viewport_area = ViewportArea()
##############################################
def geometryChanged(self, new_geometry, old_geometry):
# self._logger.info('geometryChanged', new_geometry, old_geometry)
self._viewport_area.viewport_size = new_geometry
# if self._scene:
# self._update_transformation()
QQuickPaintedItem.geometryChanged(self, new_geometry, old_geometry)
##############################################
# def _update_transformation(self):
# area = self._viewport_area.area
# self.translation = - QPointF(area.x.inf, area.y.sup)
# self.scale = self._viewport_area.scale_px_by_mm # QtPainter
##############################################
Fabrice Salvaire
committed
@property
def scene_area(self):
return self._viewport_area.area
##############################################
def scene_to_viewport(self, position):
return self._viewport_area.scene_to_viewport(position)
##############################################
Fabrice Salvaire
committed
def length_scene_to_viewport(self, length):
return self._viewport_area.length_scene_to_viewport(length)
##############################################
def length_viewport_to_scene(self, length):
return self._viewport_area.length_viewport_to_scene(length)
##############################################
def scene(self):
return self._scene
@scene.setter
def scene(self, scene):
# self._logger.info('QtQuickPaintedSceneItem set scene', scene)
self._logger.info('set scene') # Fixme: don't print ???
self._scene = scene
self._viewport_area.scene = scene
self._viewport_area.fit_scene()
# self._update_transformation()
self.update()
self.sceneChanged.emit()
##############################################
# def zoom(self):
# return self._zoom
# @zoom.setter
# def zoom(self, zoom):
# if self._zoom != zoom:
# print('QtQuickPaintedSceneItem zoom', zoom, self.width(), self.height())
# self._zoom = zoom
# self.set_transformation(zoom)
# self.update()
# self.zoomChanged.emit()
##############################################
return self._viewport_area.scale_px_by_mm
##############################################
def format_coordinate(self, position):
scene_position = self._viewport_area.viewport_to_scene(position)
return '{:.3f}, {:.3f}'.format(scene_position[0], scene_position[1])
##############################################
def zoom_at_center(self, zoom):
self._viewport_area.zoom_at(self._viewport_area.center, zoom)
self.update()
##############################################
def zoom_at(self, position, zoom):
scene_position = self._viewport_area.viewport_to_scene(position)
self._viewport_area.zoom_at(scene_position, zoom)
self.update()
##############################################
def fit_scene(self):
self._viewport_area.fit_scene()
self.update()
##############################################
def pan(self, dxy):
position = self._viewport_area.center + self._viewport_area.pan_delta_to_scene(dxy)
self._viewport_area.zoom_at(position, self._viewport_area.scale_px_by_mm)
self.update()
##############################################
def item_at(self, position, radius_px=10):
scene_position = Vector2D(self._viewport_area.viewport_to_scene(position))
radius = self.length_viewport_to_scene(radius_px)
self._logger.info('Item selection at {} with radius {:1f} mm'.format(scene_position, radius))
items = self._scene.item_at(scene_position, radius)
if items:
distance, nearest_item = items[0]
# print('nearest item at {} #{:6.2f} {} {}'.format(scene_position, len(items), distance, nearest_item.user_data))
# print(' {:6.2f} {}'.format(distance, item.user_data))
####################################################################################################
# qmlRegisterType(QtQuickPaintedSceneItem, 'Patro', 1, 0, 'PaintedSceneItem')