Skip to content
Commits on Source (25)
...@@ -21,39 +21,41 @@ Patro/QtApplication/rcc/icons/36x36/close-black.png ...@@ -21,39 +21,41 @@ Patro/QtApplication/rcc/icons/36x36/close-black.png
Patro/QtApplication/rcc/icons/36x36/delete-black.png Patro/QtApplication/rcc/icons/36x36/delete-black.png
Patro/QtApplication/rcc/icons/36x36/search-black.png Patro/QtApplication/rcc/icons/36x36/search-black.png
examples/patterns/demo-custom-seam-allowance.details.png examples/data/dxf/protection-circulaire-seul-v1.dxf
examples/patterns/demo-custom-seam-allowance.draw.png examples/data/dxf/protection-circulaire.dxf
examples/patterns/detail-demo1.details.png examples/data/dxf/protection-rectangulaire-v1.dxf
examples/patterns/detail-demo1.draw.png examples/data/dxf/protection-rectangulaire-v2.dxf
examples/patterns/flat-city-trouser.draw.png examples/data/dxf/test-dxf-r15-spline.svg
examples/patterns/layout-demo.details.png examples/data/dxf/test-dxf-r15.pdf
examples/patterns/layout-demo.draw.png examples/data/dxf/test-dxf-r15.svg
examples/patterns/operations-demo.draw-1.png examples/data/patterns-svg/veravenus-little-bias-dress.pattern-a0.pdf
examples/patterns/operations-demo.draw-2.png examples/data/patterns-svg/veravenus-little-bias-dress.pattern-a0.svg
examples/patterns/operations-demo.draw.png examples/data/patterns-valentina/backup/
examples/patterns/path-bezier.draw.png examples/data/patterns-valentina/demo-custom-seam-allowance.details.png
examples/data/patterns-valentina/demo-custom-seam-allowance.draw.png
examples/data/patterns-valentina/detail-demo1.details.png
examples/data/patterns-valentina/detail-demo1.draw.png
examples/data/patterns-valentina/flat-city-trouser.draw.png
examples/data/patterns-valentina/layout-demo.details.png
examples/data/patterns-valentina/layout-demo.draw.png
examples/data/patterns-valentina/layout/
examples/data/patterns-valentina/operations-demo.draw-1.png
examples/data/patterns-valentina/operations-demo.draw-2.png
examples/data/patterns-valentina/operations-demo.draw.png
examples/data/patterns-valentina/path-bezier.draw.png
examples/output/ examples/pattern-engine/output-2017/
examples/pattern-engine/output-old/
examples/pattern-engine/output/
output/ output/
examples/dxf/protection-circulaire-seul-v1.dxf
examples/dxf/protection-circulaire.dxf
examples/dxf/protection-rectangulaire-v1.dxf
examples/dxf/protection-rectangulaire-v2.dxf
examples/dxf/test-dxf-r15.pdf
tmp.py
Patro/GraphicEngine/TeX/__MERGE_MUSICA__ Patro/GraphicEngine/TeX/__MERGE_MUSICA__
Patro/Pattern/dev/ Patro/Pattern/dev/
Patro/QtApplication/rcc/icons/36x36-unused/ Patro/QtApplication/rcc/icons/36x36-unused/
devel-experimentations/ devel-experimentations/
doc/references-not-versioned/
doc/sphinx/source/features-all.txt doc/sphinx/source/features-all.txt
examples/output-2017/ git-log.txt
examples/output-old/
examples/patterns/backup/
examples/patterns/layout/
examples/patterns/veravenus-little-bias-dress.pattern-a0.pdf
examples/patterns/veravenus-little-bias-dress.pattern-a0.svg
notes.txt notes.txt
notes/ notes/
open-doc.sh open-doc.sh
......
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2019 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 query the platform for features.
"""
# Look alternative Python package
####################################################################################################
from enum import Enum, auto
import os
import platform
import sys
####################################################################################################
class PlatformType(Enum):
Linux = auto()
Windows = auto()
OSX = auto()
####################################################################################################
class Platform:
"""Class to store platform properties"""
##############################################
def __init__(self):
self.python_version = platform.python_version()
self.os = self._get_os()
self.node = platform.node()
# deprecated in 3.8 see distro package
# self.distribution = ' '.join(platform.dist())
self.machine = platform.machine()
self.architecture = platform.architecture()[0]
# CPU
self.cpu = self._get_cpu()
self.number_of_cores = self._get_number_of_cores()
self.cpu_khz = self._get_cpu_khz()
self.cpu_mhz = int(self._get_cpu_khz()/float(1000)) # rint
# RAM
self.memory_size_kb = self._get_memory_size_kb()
self.memory_size_mb = int(self.memory_size_kb/float(1024)) # rint
##############################################
def _get_os(self):
if os.name in ('nt',):
return PlatformType.Windows
elif sys.platform in ('linux',):
return PlatformType.Linux
# Fixme:
# elif sys.platform in 'osx':
# return PlatformType.OSX
else:
raise RuntimeError('unknown platform: {} / {}'.format(os.name, sys.platform))
##############################################
def _get_cpu(self):
if self.os == PlatformType.Linux:
with open('/proc/cpuinfo', 'rt') as cpuinfo:
for line in cpuinfo:
if 'model name' in line:
s = line.split(':')[1]
return s.strip().rstrip()
elif self.os == PlatformType.Windows:
raise NotImplementedError
##############################################
def _get_number_of_cores(self):
if self.os == PlatformType.Linux:
number_of_cores = 0
with open('/proc/cpuinfo', 'rt') as cpuinfo:
for line in cpuinfo:
if 'processor' in line:
number_of_cores += 1
return number_of_cores
elif self.os == PlatformType.Windows:
return int(os.getenv('NUMBER_OF_PROCESSORS'))
##############################################
def _get_cpu_khz(self):
if self.os == PlatformType.Linux:
with open('/proc/cpuinfo', 'rt') as cpuinfo:
for line in cpuinfo:
if 'cpu MHz' in line:
s = line.split(':')[1]
return int(1000 * float(s))
if self.os == PlatformType.Windows:
raise NotImplementedError
##############################################
def _get_memory_size_kb(self):
if self.os == PlatformType.Linux:
with open('/proc/meminfo', 'rt') as cpuinfo:
for line in cpuinfo:
if 'MemTotal' in line:
s = line.split(':')[1][:-3]
return int(s)
if self.os == PlatformType.Windows:
raise NotImplementedError
##############################################
def __str__(self):
str_template = '''
Platform {0.node}
Hardware:
Machine: {0.machine}
Architecture: {0.architecture}
CPU: {0.cpu}
Number of Cores: {0.number_of_cores}
CPU Frequence: {0.cpu_mhz} MHz
Memory: {0.memory_size_mb} MB
Python: {0.python_version}
'''
return str_template.format(self)
####################################################################################################
class QtPlatform(Platform):
"""Class to store Qt platform properties"""
##############################################
def __init__(self):
super().__init__()
# Fixme: QT_VERSION_STR ...
from PyQt5 import QtCore, QtWidgets
# from QtShim import QtCore, QtWidgets
self.qt_version = QtCore.QT_VERSION_STR
self.pyqt_version = QtCore.PYQT_VERSION_STR
# Screen
# try:
# application = QtWidgets.QApplication.instance()
# self.desktop = application.desktop()
# self.number_of_screens = self.desktop.screenCount()
# except:
# self.desktop = None
# self.number_of_screens = 0
# for i in range(self.number_of_screens):
# self.screens.append(Screen(self, i))
try:
application = QtWidgets.QApplication.instance()
self.screens = [Screen(screen) for screen in application.screens()]
except:
self.screens = []
# OpenGL
self.gl_renderer = None
self.gl_version = None
self.gl_vendor = None
self.gl_extensions = None
##############################################
@property
def number_of_screens(self):
return len(self.screens)
##############################################
def query_opengl(self):
import OpenGL.GL as GL
self.gl_renderer = GL.glGetString(GL.GL_RENDERER)
self.gl_version = GL.glGetString(GL.GL_VERSION)
self.gl_vendor = GL.glGetString(GL.GL_VENDOR)
self.gl_extensions = GL.glGetString(GL.GL_EXTENSIONS)
##############################################
def __str__(self):
# str_template = '''
# OpenGL
# Render: {0.gl_renderer}
# Version: {0.gl_version}
# Vendor: {0.gl_vendor}
# Number of Screens: {0.number_of_screens}
# '''
# message += str_template.format(self)
message = super().__str__()
for screen in self.screens:
message += str(screen)
str_template = '''
Software Versions:
Qt: {0.qt_version}
PyQt: {0.pyqt_version}
'''
message += str_template.format(self)
return message
####################################################################################################
class Screen:
"""Class to store screen properties"""
##############################################
# def __init__(self, platform_obj, screen_id):
def __init__(self, qt_screen):
# self.screen_id = screen_id
# qt_screen_geometry = platform_obj.desktop.screenGeometry(screen_id)
# self.screen_width, self.screen_height = qt_screen_geometry.width(), qt_screen_geometry.height()
# widget = platform_obj.desktop.screen(screen_id)
# self.dpi = widget.physicalDpiX(), widget.physicalDpiY()
## qt_available_geometry = self.desktop.availableGeometry(screen_id)
self.name = qt_screen.name()
size = qt_screen.size()
self.screen_width, self.screen_height = size.width(), size.height()
self.dpi = qt_screen.physicalDotsPerInch()
self.dpi_x = qt_screen.physicalDotsPerInchX()
self.dpi_y = qt_screen.physicalDotsPerInchY()
##############################################
def __str__(self):
str_template = """
Screen {0.name}
geometry {0.screen_width}x{0.screen_height} px
resolution {0.dpi:.2f} dpi
"""
return str_template.format(self)
...@@ -18,9 +18,187 @@ ...@@ -18,9 +18,187 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement Bézier curve. r"""Module to implement Bézier curve.
Definitions
-----------
A Bézier curve is defined by a set of control points :math:`\mathbf{P}_0` through
:math:`\mathbf{P}_n`, where :math:`n` is called its order (:math:`n = 1` for linear, 2 for
quadratic, 3 for cubic etc.). The first and last control points are always the end points of the
curve;
In the following :math:`0 \le t \le 1`.
Linear Bézier Curves
---------------------
Given distinct points :math:`\mathbf{P}_0` and :math:`\mathbf{P}_1`, a linear Bézier curve is simply
a straight line between those two points. The curve is given by
.. math::
\begin{align}
\mathbf{B}(t) &= \mathbf{P}_0 + t (\mathbf{P}_1 - \mathbf{P}_0) \\
&= (1-t) \mathbf{P}_0 + t \mathbf{P}_1
\end{align}
and is equivalent to linear interpolation.
Quadratic Bézier Curves
-----------------------
A quadratic Bézier curve is the path traced by the function :math:`\mathbf{B}(t)`, given points
:math:`\mathbf{P}_0`, :math:`\mathbf{P}_1`, and :math:`\mathbf{P}_2`,
.. math::
\mathbf{B}(t) = (1 - t)[(1 - t) \mathbf{P}_0 + t \mathbf{P}_1] + t [(1 - t) \mathbf{P}_1 + t \mathbf{P}_2]
which can be interpreted as the linear interpolant of corresponding points on the linear Bézier
curves from :math:`\mathbf{P}_0` to :math:`\mathbf{P}_1` and from :math:`\mathbf{P}_1` to
:math:`\mathbf{P}_2` respectively.
Rearranging the preceding equation yields:
.. math::
\mathbf{B}(t) = (1 - t)^{2} \mathbf{P}_0 + 2(1 - t)t \mathbf{P}_1 + t^{2} \mathbf{P}_2
This can be written in a way that highlights the symmetry with respect to :math:`\mathbf{P}_1`:
.. math::
\mathbf{B}(t) = \mathbf{P}_1 + (1 - t)^{2} ( \mathbf{P}_0 - \mathbf{P}_1) + t^{2} (\mathbf{P}_2 - \mathbf{P}_1)
Which immediately gives the derivative of the Bézier curve with respect to `t`:
.. math::
\mathbf{B}'(t) = 2(1 - t) (\mathbf{P}_1 - \mathbf{P}_0) + 2t (\mathbf{P}_2 - \mathbf{P}_1)
from which it can be concluded that the tangents to the curve at :math:`\mathbf{P}_0` and
:math:`\mathbf{P}_2` intersect at :math:`\mathbf{P}_1`. As :math:`t` increases from 0 to 1, the
curve departs from :math:`\mathbf{P}_0` in the direction of :math:`\mathbf{P}_1`, then bends to
arrive at :math:`\mathbf{P}_2` from the direction of :math:`\mathbf{P}_1`.
The second derivative of the Bézier curve with respect to :math:`t` is
.. math::
\mathbf{B}''(t) = 2 (\mathbf{P}_2 - 2 \mathbf{P}_1 + \mathbf{P}_0)
Cubic Bézier Curves
-------------------
Four points :math:`\mathbf{P}_0`, :math:`\mathbf{P}_1`, :math:`\mathbf{P}_2` and
:math:`\mathbf{P}_3` in the plane or in higher-dimensional space define a cubic Bézier curve. The
curve starts at :math:`\mathbf{P}_0` going toward :math:`\mathbf{P}_1` and arrives at
:math:`\mathbf{P}_3` coming from the direction of :math:`\mathbf{P}_2`. Usually, it will not pass
through :math:`\mathbf{P}_1` or :math:`\mathbf{P}_2`; these points are only there to provide
directional information. The distance between :math:`\mathbf{P}_1` and :math:`\mathbf{P}_2`
determines "how far" and "how fast" the curve moves towards :math:`\mathbf{P}_1` before turning
towards :math:`\mathbf{P}_2`.
Writing :math:`\mathbf{B}_{\mathbf P_i,\mathbf P_j,\mathbf P_k}(t)` for the quadratic Bézier curve
defined by points :math:`\mathbf{P}_i`, :math:`\mathbf{P}_j`, and :math:`\mathbf{P}_k`, the cubic
Bézier curve can be defined as an affine combination of two quadratic Bézier curves:
.. math::
\mathbf{B}(t) = (1-t) \mathbf{B}_{\mathbf P_0,\mathbf P_1,\mathbf P_2}(t) +
t \mathbf{B}_{\mathbf P_1,\mathbf P_2,\mathbf P_3}(t)
The explicit form of the curve is:
.. math::
\mathbf{B}(t) = (1-t)^3 \mathbf{P}_0 + 3(1-t)^2t \mathbf{P}_1 + 3(1-t)t^2 \mathbf{P}_2 + t^3\mathbf{P}_3
For some choices of :math:`\mathbf{P}_1` and :math:`\mathbf{P}_2` the curve may intersect itself, or
contain a cusp.
The derivative of the cubic Bézier curve with respect to :math:`t` is
.. math::
\mathbf{B}'(t) = 3(1-t)^2 (\mathbf{P}_1 - \mathbf{P}_0) + 6(1-t)t (\mathbf{P}_2 - \mathbf{P}_1) + 3t^2 (\mathbf{P}_3 - \mathbf{P}_2)
The second derivative of the Bézier curve with respect to :math:`t` is
.. math::
\mathbf{B}''(t) = 6(1-t) (\mathbf{P}_2 - 2 \mathbf{P}_1 + \mathbf{P}_0) + 6t (\mathbf{P}_3 - 2 \mathbf{P}_2 + \mathbf{P}_1)
Recursive definition
--------------------
A recursive definition for the Bézier curve of degree :math:`n` expresses it as a point-to-point
linear combination of a pair of corresponding points in two Bézier curves of degree :math:`n-1`.
Let :math:`\mathbf{B}_{\mathbf{P}_0\mathbf{P}_1\ldots\mathbf{P}_n}` denote the Bézier curve
determined by any selection of points :math:`\mathbf{P}_0`, :math:`\mathbf{P}_1`, :math:`\ldots`,
:math:`\mathbf{P}_{n-1}`.
The recursive definition is
.. math::
\begin{align}
\mathbf{B}_{\mathbf{P}_0}(t) &= \mathbf{P}_0 \\[1em]
\mathbf{B}(t) &= \mathbf{B}_{\mathbf{P}_0\mathbf{P}_1\ldots\mathbf{P}_n}(t) \\
&= (1-t) \mathbf{B}_{\mathbf{P}_0\mathbf{P}_1\ldots\mathbf{P}_{n-1}}(t) +
t \mathbf{B}_{\mathbf{P}_1\mathbf{P}_2\ldots\mathbf{P}_n}(t)
\end{align}
The formula can be expressed explicitly as follows:
.. math::
\begin{align}
\mathbf{B}(t) &= \sum_{i=0}^n b_{i,n}(t) \mathbf{P}_i \\
&= \sum_{i=0}^n {n\choose i}(1 - t)^{n - i}t^i \mathbf{P}_i \\
&= (1 - t)^n \mathbf{P}_0 +
{n\choose 1}(1 - t)^{n - 1}t \mathbf{P}_1 +
\cdots +
{n\choose n - 1}(1 - t)t^{n - 1} \mathbf{P}_{n - 1} +
t^n \mathbf{P}_n
\end{align}
where :math:`b_{i,n}(t)` are the Bernstein basis polynomials of degree :math:`n` and :math:`n
\choose i` are the binomial coefficients.
Degree elevation
----------------
A Bézier curve of degree :math:`n` can be converted into a Bézier curve of degree :math:`n + 1` with
the same shape.
To do degree elevation, we use the equality
.. math::
\mathbf{B}(t) = (1-t) \mathbf{B}(t) + t \mathbf{B}(t)`
Each component :math:`\mathbf{b}_{i,n}(t) \mathbf{P}_i` is multiplied by :math:`(1-t)` and
:math:`t`, thus increasing a degree by one, without changing the value.
For arbitrary :math:`n`, we have
.. math::
\begin{align}
\mathbf{B}(t) &= (1 - t) \sum_{i=0}^n \mathbf{b}_{i,n}(t) \mathbf{P}_i +
t \sum_{i=0}^n \mathbf{b}_{i,n}(t) \mathbf{P}_i \\
&= \sum_{i=0}^n \frac{n + 1 - i}{n + 1} \mathbf{b}_{i, n + 1}(t) \mathbf{P}_i +
\sum_{i=0}^n \frac{i + 1}{n + 1} \mathbf{b}_{i + 1, n + 1}(t) \mathbf{P}_i \\
&= \sum_{i=0}^{n + 1} \mathbf{b}_{i, n + 1}(t)
\left(\frac{i}{n + 1} \mathbf{P}_{i - 1} +
\frac{n + 1 - i}{n + 1} \mathbf{P}_i\right) \\
&= \sum_{i=0}^{n + 1} \mathbf{b}_{i, n + 1}(t) \mathbf{P'}_i
\end{align}
Therefore the new control points are
.. math::
\mathbf{P'}_i = \frac{i}{n + 1} \mathbf{P}_{i - 1} + \frac{n + 1 - i}{n + 1} \mathbf{P}_i
It introduces two arbitrary points :math:`\mathbf{P}_{-1}` and :math:`\mathbf{P}_{n+1}` which are
cancelled in :math:`\mathbf{P'}_i`.
""" """
# Fixme:
# max distance to the chord for linear approximation
# fitting
# C0 = continuous # C0 = continuous
# G1 = geometric continuity # G1 = geometric continuity
# Tangents point to the same direction # Tangents point to the same direction
...@@ -31,6 +209,13 @@ ...@@ -31,6 +209,13 @@
#################################################################################################### ####################################################################################################
__all__ = [
'QuadraticBezier2D',
'CubicBezier2D',
]
####################################################################################################
from math import log, sqrt from math import log, sqrt
import numpy as np import numpy as np
...@@ -44,12 +229,6 @@ from .Vector import Vector2D ...@@ -44,12 +229,6 @@ from .Vector import Vector2D
#################################################################################################### ####################################################################################################
# Fixme:
# max distance to the chord for linear approximation
# fitting
####################################################################################################
class BezierMixin2D(Primitive2DMixin): class BezierMixin2D(Primitive2DMixin):
"""Mixin to implements 2D Bezier Curve.""" """Mixin to implements 2D Bezier Curve."""
...@@ -60,7 +239,7 @@ class BezierMixin2D(Primitive2DMixin): ...@@ -60,7 +239,7 @@ class BezierMixin2D(Primitive2DMixin):
def interpolated_length(self, dt=None): def interpolated_length(self, dt=None):
# Length of the curve obtained via line interpolation """Length of the curve obtained via line interpolation"""
if dt is None: if dt is None:
dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude
...@@ -324,11 +503,14 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P): ...@@ -324,11 +503,14 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P):
def intersect_line(self, line): def intersect_line(self, line):
"""Find the intersections of the curve with a line.""" """Find the intersections of the curve with a line.
# Algorithm: Algorithm
# Apply a transformation to the curve that maps the line onto the X-axis.
# Then we only need to test the Y-values for a zero. * Apply a transformation to the curve that maps the line onto the X-axis.
* Then we only need to test the Y-values for a zero.
"""
# t, p0, p1, p2, p3 = symbols('t p0 p1 p2 p3') # t, p0, p1, p2, p3 = symbols('t p0 p1 p2 p3')
# u = 1 - t # u = 1 - t
...@@ -382,10 +564,15 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P): ...@@ -382,10 +564,15 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P):
def closest_point(self, point): def closest_point(self, point):
# Reference: """Return the closest point on the curve to the given *point*.
# https://hal.archives-ouvertes.fr/inria-00518379/document
# Improved Algebraic Algorithm On Point Projection For Bézier Curves Reference
# Xiao-Diao Chen, Yin Zhou, Zhenyu Shu, Hua Su, Jean-Claude Paul
* https://hal.archives-ouvertes.fr/inria-00518379/document
Improved Algebraic Algorithm On Point Projection For Bézier Curves
Xiao-Diao Chen, Yin Zhou, Zhenyu Shu, Hua Su, Jean-Claude Paul
"""
# Condition: # Condition:
# (P - B(t)) . B'(t) = 0 where t in [0,1] # (P - B(t)) . B'(t) = 0 where t in [0,1]
...@@ -439,6 +626,29 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P): ...@@ -439,6 +626,29 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P):
else: else:
return self.point_at_t(t) return self.point_at_t(t)
##############################################
def to_cubic(self):
r"""Elevate the quadratic Bézier curve to a cubic Bézier cubic with the same shape.
The new control points are
.. math::
\begin{align}
\mathbf{P'}_0 &= \mathbf{P}_0 \\
\mathbf{P'}_1 &= \mathbf{P}_0 + \frac{2}{3} (\mathbf{P}_1 - \mathbf{P}_0) \\
\mathbf{P'}_1 &= \mathbf{P}_2 + \frac{2}{3} (\mathbf{P}_1 - \mathbf{P}_2) \\
\mathbf{P'}_2 &= \mathbf{P}_2
\end{align}
"""
p1 = (self._p0 + self._p1 * 2) / 3
p2 = (self._p2 + self._p1 * 2) / 3
return CubicBezier2D(self._p0, p1, p2, self._p3)
#################################################################################################### ####################################################################################################
_Sqrt3 = sqrt(3) _Sqrt3 = sqrt(3)
...@@ -866,11 +1076,12 @@ class CubicBezier2D(BezierMixin2D, Primitive4P): ...@@ -866,11 +1076,12 @@ class CubicBezier2D(BezierMixin2D, Primitive4P):
*flatness* is the maximum error allowed for the straight line to deviate from the curve. *flatness* is the maximum error allowed for the straight line to deviate from the curve.
""" Reference
# Reference: * Kaspar Fischer and Roger Willcocks http://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf
# Kaspar Fischer and Roger Willcocks http://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf * PostScript Language Reference. Addison- Wesley, third edition, 1999
# PostScript Language Reference. Addison- Wesley, third edition, 1999
"""
# We define the flatness of the curve as the argmax of the distance from the curve to the # We define the flatness of the curve as the argmax of the distance from the curve to the
# line passing by the start and stop point. # line passing by the start and stop point.
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
#################################################################################################### ####################################################################################################
"""Module to compute bounding box and convex hull for a set of points. """Module to compute bounding box and convex hull for a set of points.
""" """
#################################################################################################### ####################################################################################################
...@@ -73,19 +74,24 @@ def _sort_point_for_graham_scan(points): ...@@ -73,19 +74,24 @@ def _sort_point_for_graham_scan(points):
#################################################################################################### ####################################################################################################
def _ccw(p1, p2, p3): def _ccw(p1, p2, p3):
# Three points are a counter-clockwise turn if ccw > 0, clockwise if ccw < 0, and collinear if """Three points are a counter-clockwise turn if ccw > 0, clockwise if ccw < 0, and collinear if ccw
# ccw = 0 because ccw is a determinant that gives twice the signed area of the triangle formed = 0 because ccw is a determinant that gives twice the signed area of the triangle formed by p1,
# by p1, p2 and p3. p2 and p3.
"""
return (p2.x - p1.x)*(p3.y - p1.y) - (p2.y - p1.y)*(p3.x - p1.x) return (p2.x - p1.x)*(p3.y - p1.y) - (p2.y - p1.y)*(p3.x - p1.x)
#################################################################################################### ####################################################################################################
def convex_hull(points): def convex_hull(points):
"""Return the convex hull of the list of points using Graham Scan algorithm.""" """Return the convex hull of the list of points using Graham Scan algorithm.
References
* https://en.wikipedia.org/wiki/Graham_scan
# Reference: Graham Scan Algorithm """
# https://en.wikipedia.org/wiki/Graham_scan
# convex_hull is a stack of points beginning with the leftmost point. # convex_hull is a stack of points beginning with the leftmost point.
convex_hull = [] convex_hull = []
......
...@@ -33,10 +33,15 @@ Valentina Requirements ...@@ -33,10 +33,15 @@ Valentina Requirements
""" """
# Fixme:
#
# Ellipse passing by two points
# https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
#
#################################################################################################### ####################################################################################################
__all__ = [ __all__ = [
'AngularDomain',
'Circle2D', 'Circle2D',
'Ellipse2D', 'Ellipse2D',
] ]
...@@ -48,161 +53,13 @@ from math import fabs, sqrt, radians, pi, cos, sin # , degrees ...@@ -48,161 +53,13 @@ from math import fabs, sqrt, radians, pi, cos, sin # , degrees
import numpy as np import numpy as np
from IntervalArithmetic import Interval2D from Patro.Common.Math.Functions import sign # , epsilon_float
from Patro.Common.Math.Functions import sign, epsilon_float
from .BoundingBox import bounding_box_from_points from .BoundingBox import bounding_box_from_points
from .Line import Line2D from .Line import Line2D
from .Mixin import AngularDomainMixin, CenterMixin, AngularDomain
from .Primitive import Primitive, Primitive2DMixin from .Primitive import Primitive, Primitive2DMixin
from .Segment import Segment2D from .Segment import Segment2D
####################################################################################################
class AngularDomain:
"""Class to define an angular domain"""
##############################################
def __init__(self, start=0, stop=360, degrees=True):
if not degrees:
start = math.degrees(start)
stop = math.degrees(stop)
self.start = start
self.stop = stop
##############################################
def __clone__(self):
return self.__class__(self._start, self._stop)
##############################################
def __repr__(self):
return '{0}({1._start}, {1._stop})'.format(self.__class__.__name__, self)
##############################################
@property
def start(self):
return self._start
@start.setter
def start(self, value):
self._start = float(value)
@property
def stop(self):
return self._stop
@stop.setter
def stop(self, value):
self._stop = float(value)
@property
def start_radians(self):
return radians(self._start)
@property
def stop_radians(self):
return radians(self._stop)
##############################################
@property
def is_null(self):
return self._stop == self._start
@property
def is_closed(self):
return abs(self._stop - self._start) >= 360
@property
def is_over_closed(self):
return abs(self._stop - self._start) > 360
@property
def is_counterclockwise(self):
"""Return True if start <= stop, e.g. 10 <= 300"""
# Fixme: name ???
return self.start <= self.stop
@property
def is_clockwise(self):
"""Return True if stop < start, e.g. 300 < 10"""
return self.stop < self.start
##############################################
@property
def length(self):
"""Return the length for an unitary circle"""
if self.is_closed:
return 2*pi
else:
length = self.stop_radians - self.start_radians
if self.is_counterclockwise:
return length
else:
return 2*pi - length
##############################################
def is_inside(self, angle):
if self.is_counterclockwise:
return self._start <= angle <= self._stop
else:
# Fixme: check !!!
return not(self._stop < angle < self._start)
####################################################################################################
class AngularDomainMixin:
##############################################
@property
def domain(self):
return self._domain
@domain.setter
def domain(self, value):
if value is not None:
self._domain = value # Fixme: AngularDomain() ??
else:
self._domain = None
##############################################
@property
def is_closed(self):
return self._domain is None
##############################################
def start_stop_point(self, start=True):
if self._domain is not None:
angle = self.domain.start if start else self.domain.stop
return self.point_at_angle(angle)
else:
return None
##############################################
@property
def start_point(self):
return self.start_stop_point(start=True)
##############################################
@property
def stop_point(self):
return self.start_stop_point(start=False)
#################################################################################################### ####################################################################################################
class PointNotOnCircleError(ValueError): class PointNotOnCircleError(ValueError):
...@@ -210,7 +67,7 @@ class PointNotOnCircleError(ValueError): ...@@ -210,7 +67,7 @@ class PointNotOnCircleError(ValueError):
#################################################################################################### ####################################################################################################
class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive): class Circle2D(Primitive2DMixin, CenterMixin, AngularDomainMixin, Primitive):
"""Class to implements 2D Circle.""" """Class to implements 2D Circle."""
...@@ -283,18 +140,23 @@ class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive): ...@@ -283,18 +140,23 @@ class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive):
############################################## ##############################################
def __repr__(self): def clone(self):
return '{0}({1._center}, {1._radius}, {1._domain})'.format(self.__class__.__name__, self) return self.__class__(self._center, self._radius, self._domain)
############################################## ##############################################
@property def apply_transformation(self, transformation):
def center(self): self._center = transformation * self._center
return self._center # Fixme: shear -> ellipse
if self._radius is not None:
self._radius = transformation * self._radius
##############################################
def __repr__(self):
return '{0}({1._center}, {1._radius}, {1._domain})'.format(self.__class__.__name__, self)
@center.setter ##############################################
def center(self, value):
self._center = self.__vector_cls__(value)
@property @property
def radius(self): def radius(self):
...@@ -331,7 +193,6 @@ class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive): ...@@ -331,7 +193,6 @@ class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive):
def point_at_angle(self, angle): def point_at_angle(self, angle):
return self.__vector_cls__.from_polar(self._radius, angle) + self._center return self.__vector_cls__.from_polar(self._radius, angle) + self._center
############################################## ##############################################
def point_in_circle_frame(self, point): def point_in_circle_frame(self, point):
...@@ -566,7 +427,7 @@ class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive): ...@@ -566,7 +427,7 @@ class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive):
#################################################################################################### ####################################################################################################
class Ellipse2D(Primitive2DMixin, AngularDomainMixin, Primitive): class Ellipse2D(Primitive2DMixin, CenterMixin, AngularDomainMixin, Primitive):
r"""Class to implements 2D Ellipse. r"""Class to implements 2D Ellipse.
...@@ -611,18 +472,28 @@ class Ellipse2D(Primitive2DMixin, AngularDomainMixin, Primitive): ...@@ -611,18 +472,28 @@ class Ellipse2D(Primitive2DMixin, AngularDomainMixin, Primitive):
############################################## ##############################################
def __repr__(self): def clone(self):
return '{0}({1._center}, {1._x_radius}, {1._x_radius}, {1._angle})'.format(self.__class__.__name__, self) return self.__class__(
self._center,
self._x_radius, self._y_radius,
self._angle,
self._domain,
)
############################################## ##############################################
@property def apply_transformation(self, transformation):
def center(self): self._center = transformation * self._center
return self._center self._x_radius = transformation * self._x_radius
self._y_radius = transformation * self._y_radius
self._bounding_box = None
@center.setter ##############################################
def center(self, value):
self._center = self.__vector_cls__(value) def __repr__(self):
return '{0}({1._center}, {1._x_radius}, {1._x_radius}, {1._angle})'.format(self.__class__.__name__, self)
##############################################
@property @property
def x_radius(self): def x_radius(self):
......
...@@ -18,6 +18,14 @@ ...@@ -18,6 +18,14 @@
# #
#################################################################################################### ####################################################################################################
"""Module to perform interpolation."""
####################################################################################################
__all__ = [
'interpolate_two_points',
]
#################################################################################################### ####################################################################################################
def interpolate_two_points(p0, p1, t): def interpolate_two_points(p0, p1, t):
......
...@@ -18,6 +18,14 @@ ...@@ -18,6 +18,14 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement line.
"""
####################################################################################################
__all__ = ['Line2D']
#################################################################################################### ####################################################################################################
from Patro.Common.IterTools import pairwise from Patro.Common.IterTools import pairwise
......
#################################################################################################### ####################################################################################################
# #
# Patro - A Python library to make patterns for fashion design # Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire # Copyright (C) 2019 Fabrice Salvaire
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -18,172 +18,181 @@ ...@@ -18,172 +18,181 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement mixins.
"""
#################################################################################################### ####################################################################################################
import logging __all__ = [
'AngularDomain',
# from Patro.GeometryPatro.Engine import Segment2D, CubicBezier2D 'AngularDomainMixin',
'CenterMixin',
]
#################################################################################################### ####################################################################################################
_module_logger = logging.getLogger(__name__) import math
from math import radians, pi # , degrees
#################################################################################################### ####################################################################################################
class Vertex: class AngularDomain:
##############################################
def __init__(self, point):
self._point = point """Class to define an angular domain"""
############################################## ##############################################
@property def __init__(self, start=0, stop=360, degrees=True):
def point(self):
return self._point
# def geometry if not degrees:
start = math.degrees(start)
stop = math.degrees(stop)
#################################################################################################### self.start = start
self.stop = stop
class Edge:
############################################## ##############################################
def __init__(self, contour, location): def __clone__(self):
return self.__class__(self._start, self._stop)
##############################################
self._contour = contour def __repr__(self):
self._location = location return '{0}({1._start}, {1._stop})'.format(self.__class__.__name__, self)
############################################## ##############################################
@property @property
def start_point(self): def start(self):
return self._contour._vertexes[self._location] return self._start
@start.setter
def start(self, value):
self._start = float(value)
@property @property
def end_point(self): def stop(self):
return self._contour._vertexes[self._location +1] return self._stop
@stop.setter
def stop(self, value):
self._stop = float(value)
@property @property
def previous(self): def start_radians(self):
# (location = 0) - 1 = -1 return radians(self._start)
return self._contour._edges[self._location -1]
@property @property
def next(self): def stop_radians(self):
location = self._location +1 return radians(self._stop)
if location == self._contour.number_of_edges:
location = 0
return self._contour._edges[location]
############################################## ##############################################
# def __eq__ @property
# def geometry def is_null(self):
return self._stop == self._start
#################################################################################################### @property
def is_closed(self):
return abs(self._stop - self._start) >= 360
class SegmentEdge(Edge): @property
pass def is_over_closed(self):
return abs(self._stop - self._start) > 360
#################################################################################################### @property
def is_counterclockwise(self):
"""Return True if start <= stop, e.g. 10 <= 300"""
# Fixme: name ???
return self.start <= self.stop
class CurvedEdge(Edge): @property
def is_clockwise(self):
"""Return True if stop < start, e.g. 300 < 10"""
return self.stop < self.start
############################################## ##############################################
def __init__(self, contour, location, curve, reverse=False): @property
def length(self):
Edge.__init__(self, contour, location) """Return the length for an unitary circle"""
self._curve = curve if self.is_closed:
self._reverse = reverse return 2*pi
else:
#################################################################################################### length = self.stop_radians - self.start_radians
if self.is_counterclockwise:
# def __init__(self, first_point, second_point): return length
else:
# # Fixme: mixin return 2*pi - length
# self._first_point = first_point
# self._second_point = second_point
# ##############################################
# @property ##############################################
# def first_point(self):
# return self._first_point
# @property def is_inside(self, angle):
# def second_point(self): if self.is_counterclockwise:
# return self._second_point return self._start <= angle <= self._stop
else:
# Fixme: check !!!
return not(self._stop < angle < self._start)
#################################################################################################### ####################################################################################################
class Contour: class AngularDomainMixin:
_logger = _module_logger.getChild('Contour')
############################################## ##############################################
def __init__(self): @property
def domain(self):
return self._domain
self._vertexes = [] @domain.setter
self._edges = [] def domain(self, value):
if value is not None:
self._domain = value # Fixme: AngularDomain() ??
else:
self._domain = None
############################################## ##############################################
@property @property
def start_point(self): def is_closed(self):
return self._vertexes[0] return self._domain is None
@property
def end_point(self):
return self._vertexes[-1]
@property
def number_of_edges(self):
return len(self._edges)
############################################## ##############################################
def iter_on_vertexes(self): def start_stop_point(self, start=True):
return iter(self._vertexes)
def iter_on_edges(self): if self._domain is not None:
return iter(self._edges) angle = self.domain.start if start else self.domain.stop
return self.point_at_angle(angle)
else:
return None
############################################## ##############################################
def add_segment_edge(self, point): @property
def start_point(self):
vertex = Vertex(point) return self.start_stop_point(start=True)
self._vertexes.append(vertex)
self._edges.append(SegmentEdge(self, self.number_of_edges))
############################################## ##############################################
def add_curved_edge(self, curve, reverse=False): @property
def stop_point(self):
start_point = curve.start_point return self.start_stop_point(start=False)
if start_point != self.end_point: # Fixme: implement
raise ValueError()
vertex = Vertex(curve.end_point)
self._vertexes.append(vertex)
self._edges.append(CurvedEdge(self, self.number_of_edges, curve, reverse))
##############################################
# def add_edge(self, edge, reverse=False): ####################################################################################################
# if not isinstance(edge, (Segment2D, CubicBezier2D)): class CenterMixin:
# raise ValueError()
# if reverse: @property
# edge = edge.reverse() def center(self):
return self._center
# self._edges.append(edge) @center.setter
def center(self, value):
self._center = self.__vector_cls__(value)
# for point in edge.iter_on_points(): @property
# self._vertexes.append(point) def points(self):
return (self._center,)
...@@ -18,6 +18,10 @@ ...@@ -18,6 +18,10 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement path.
"""
#################################################################################################### ####################################################################################################
__all__ = [ __all__ = [
...@@ -50,6 +54,11 @@ class PathPart: ...@@ -50,6 +54,11 @@ class PathPart:
############################################## ##############################################
def clone(self, path):
raise NotImplementedError
##############################################
def __repr__(self): def __repr__(self):
return self.__class__.__name__ return self.__class__.__name__
...@@ -81,6 +90,7 @@ class PathPart: ...@@ -81,6 +90,7 @@ class PathPart:
@property @property
def start_point(self): def start_point(self):
# Fixme: cache ???
prev_part = self.prev_part prev_part = self.prev_part
if prev_part is not None: if prev_part is not None:
return prev_part.stop_point return prev_part.stop_point
...@@ -138,6 +148,11 @@ class LinearSegment(PathPart): ...@@ -138,6 +148,11 @@ class LinearSegment(PathPart):
""" """
# Fixme:
#
# If two successive vertices share the same circle, then it should be merged to one.
#
############################################## ##############################################
def __init__(self, path, position, radius): def __init__(self, path, position, radius):
...@@ -151,10 +166,16 @@ class LinearSegment(PathPart): ...@@ -151,10 +166,16 @@ class LinearSegment(PathPart):
if self._radius is not None: if self._radius is not None:
if not isinstance(self.prev_part, LinearSegment): if not isinstance(self.prev_part, LinearSegment):
raise ValueError('Previous path segment must be linear') raise ValueError('Previous path segment must be linear')
self._bulge_angle = None self._reset_cache()
self._bulge_center = None
self._start_bulge_point = None ##############################################
self._stop_bulge_point = None
def _reset_cache(self):
self._bulge_angle = None
self._bulge_center = None
self._start_bulge_point = None
self._stop_bulge_point = None
############################################## ##############################################
...@@ -273,6 +294,18 @@ class PathSegment(LinearSegment): ...@@ -273,6 +294,18 @@ class PathSegment(LinearSegment):
############################################## ##############################################
def clone(self, path):
return self.__class__(path, self._position, self._point, self._radius, self._absolute)
##############################################
def apply_transformation(self, transformation):
self._point = transformation * self._point
if self._radius is not None:
self._radius = transformation * self._radius
##############################################
@property @property
def point(self): def point(self):
return self._point return self._point
...@@ -311,6 +344,18 @@ class DirectionalSegment(LinearSegment): ...@@ -311,6 +344,18 @@ class DirectionalSegment(LinearSegment):
############################################## ##############################################
def apply_transformation(self, transformation):
# Since a rotation will change the direction
# DirectionalSegment must be casted to PathSegment
raise NotImplementedError
##############################################
def clone(self, path):
return self.__class__(path, self._position, self._length, self._radius)
##############################################
@property @property
def length(self): def length(self):
return self._length return self._length
...@@ -321,10 +366,15 @@ class DirectionalSegment(LinearSegment): ...@@ -321,10 +366,15 @@ class DirectionalSegment(LinearSegment):
############################################## ##############################################
@property
def offset(self):
# Fixme: cache ???
return Vector2D.from_polar(self._length, self.__angle__)
@property @property
def stop_point(self): def stop_point(self):
# Fixme: cache ??? # Fixme: cache ???
return self.start_point + Vector2D.from_polar(self._length, self.__angle__) return self.start_point + self.offset
############################################## ##############################################
...@@ -333,6 +383,11 @@ class DirectionalSegment(LinearSegment): ...@@ -333,6 +383,11 @@ class DirectionalSegment(LinearSegment):
# Fixme: cache ??? # Fixme: cache ???
return Segment2D(self.start_point, self.stop_point) return Segment2D(self.start_point, self.stop_point)
##############################################
def to_path_segment(self):
return PathSegment(self._path, self._position, self.offset, self._radius, absolute=False)
#################################################################################################### ####################################################################################################
class HorizontalSegment(DirectionalSegment): class HorizontalSegment(DirectionalSegment):
...@@ -385,6 +440,12 @@ class TwoPointsMixin: ...@@ -385,6 +440,12 @@ class TwoPointsMixin:
def point2(self, value): def point2(self, value):
self._point2 = Vector2D(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, TwoPointsMixin):
...@@ -402,6 +463,11 @@ class QuadraticBezierSegment(PathPart, TwoPointsMixin): ...@@ -402,6 +463,11 @@ class QuadraticBezierSegment(PathPart, TwoPointsMixin):
############################################## ##############################################
def clone(self, path):
return self.__class__(path, self._position, self._point1, self._point2)
##############################################
@property @property
def stop_point(self): def stop_point(self):
return self._point2 return self._point2
...@@ -433,6 +499,17 @@ class CubicBezierSegment(PathPart, TwoPointsMixin): ...@@ -433,6 +499,17 @@ class CubicBezierSegment(PathPart, TwoPointsMixin):
############################################## ##############################################
def clone(self, path):
return self.__class__(path, self._position, self._point1, self._point2, self._point3)
##############################################
def apply_transformation(self, transformation):
TwoPointsMixin.transform(self, transformation)
self._point3 = transformation * self._point3
##############################################
@property @property
def point3(self): def point3(self):
return self._point3 return self._point3
...@@ -474,6 +551,19 @@ class Path2D(Primitive2DMixin, Primitive1P): ...@@ -474,6 +551,19 @@ class Path2D(Primitive2DMixin, Primitive1P):
############################################## ##############################################
def clone(self):
obj = self.__class__(self._p0)
# parts must be added sequentially to the path for bulge check
parts = obj._parts
for part in self._parts:
parts.append(part.clone(obj))
return obj
##############################################
def __len__(self): def __len__(self):
return len(self._parts) return len(self._parts)
...@@ -500,6 +590,22 @@ class Path2D(Primitive2DMixin, Primitive1P): ...@@ -500,6 +590,22 @@ class Path2D(Primitive2DMixin, Primitive1P):
############################################## ##############################################
def apply_transformation(self, transformation):
self._p0 = transformation * self._p0
for i, part in enumerate(self._parts):
if isinstance(part, PathSegment):
part._reset_cache()
if isinstance(part, DirectionalSegment):
# Since a rotation will change the direction
# DirectionalSegment must be casted to PathSegment
part = part.to_path_segment()
self._parts[i] = part
part.apply_transformation(transformation)
##############################################
def move_to(self, point): def move_to(self, point):
self.p0 = point self.p0 = point
...@@ -541,6 +647,7 @@ class Path2D(Primitive2DMixin, Primitive1P): ...@@ -541,6 +647,7 @@ class Path2D(Primitive2DMixin, Primitive1P):
return self._add_part(PathSegment, point, radius) return self._add_part(PathSegment, point, radius)
def close(self, radius=None): def close(self, radius=None):
# Fixme: identify as close for SVG export
# Fixme: radius must apply to start and stop # Fixme: radius must apply to start and stop
return self._add_part(PathSegment, self._p0, radius, absolute=True) return self._add_part(PathSegment, self._p0, radius, absolute=True)
......
...@@ -18,6 +18,13 @@ ...@@ -18,6 +18,13 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement polygon.
"""
####################################################################################################
__all__ = ['Polygon2D']
#################################################################################################### ####################################################################################################
import math import math
......
...@@ -18,6 +18,14 @@ ...@@ -18,6 +18,14 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement polyline.
"""
####################################################################################################
__all__ = ['Polyline2D']
#################################################################################################### ####################################################################################################
from .Primitive import PrimitiveNP, Primitive2DMixin from .Primitive import PrimitiveNP, Primitive2DMixin
......
...@@ -18,6 +18,12 @@ ...@@ -18,6 +18,12 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement base classes for primitives.
"""
####################################################################################################
__all__ = [ __all__ = [
'Primitive', 'Primitive',
'Primitive2DMixin', 'Primitive2DMixin',
...@@ -34,6 +40,8 @@ import collections ...@@ -34,6 +40,8 @@ import collections
import numpy as np import numpy as np
from .BoundingBox import bounding_box_from_points from .BoundingBox import bounding_box_from_points
# Fixme: circular import
# from .Transformation import Transformation2D
#################################################################################################### ####################################################################################################
...@@ -92,6 +100,9 @@ class Primitive: ...@@ -92,6 +100,9 @@ class Primitive:
############################################## ##############################################
# Fixme: part of the API imply points
# it is not true for Path2D
@property @property
def points(self): def points(self):
raise NotImplementedError raise NotImplementedError
...@@ -138,15 +149,54 @@ class Primitive: ...@@ -138,15 +149,54 @@ class Primitive:
############################################## ##############################################
def transform(self, transformation): def transform(self, transformation, clone=False):
"""Apply a transformation to the primitive.
# for point in self.points: If *clone* is set then the primitive is cloned.
"""
obj = self.clone() if clone else self
# for point in obj.points:
# point *= transformation # don't work # point *= transformation # don't work
obj.apply_transformation(transformation)
return obj
##############################################
def apply_transformation(self, transformation):
"""Apply a transformation to the primitive.
If *clone* is set then the primitive is cloned.
"""
# for point in self.points:
# point *= transformation # don't work
self._set_points([transformation*p for p in self.points]) self._set_points([transformation*p for p in self.points])
############################################## ##############################################
def mirror(self, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.Parity(), clone)
def x_mirror(self, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.XReflection(), clone)
def y_mirror(self, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.YReflection(), clone)
def rotate(self, angle, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.Rotation(angle), clone)
def scale(self, x_factor, y_factor=None, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.Scale(x_factor, y_factor), clone)
##############################################
@property @property
def geometry_matrix(self): def geometry_matrix(self):
return np.array(list(self.points)).transpose() return np.array(list(self.points)).transpose()
......
...@@ -18,6 +18,14 @@ ...@@ -18,6 +18,14 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement rectangle.
"""
####################################################################################################
__all__ = ['Rectangle2D']
#################################################################################################### ####################################################################################################
import math import math
......
...@@ -18,6 +18,14 @@ ...@@ -18,6 +18,14 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement segment.
"""
####################################################################################################
__all__ = ['Segment2D']
#################################################################################################### ####################################################################################################
# from .Interpolation import interpolate_two_points # from .Interpolation import interpolate_two_points
......
...@@ -18,8 +18,23 @@ ...@@ -18,8 +18,23 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement transformations like scale, rotation and translation.
"""
#################################################################################################### ####################################################################################################
__all__ = [
'TransformationType',
'Transformation',
'Transformation2D',
'AffineTransformation',
'AffineTransformation2D',
]
####################################################################################################
from enum import Enum, auto
from math import sin, cos, radians, degrees from math import sin, cos, radians, degrees
import numpy as np import numpy as np
...@@ -28,6 +43,23 @@ from .Vector import Vector2D, HomogeneousVector2D ...@@ -28,6 +43,23 @@ from .Vector import Vector2D, HomogeneousVector2D
#################################################################################################### ####################################################################################################
class TransformationType(Enum):
Identity = auto()
Scale = auto() # same scale factor across axes
Shear = auto() # different scale factor
Parity = auto()
XParity = auto()
YParity = auto()
Rotation = auto()
Generic = auto()
####################################################################################################
class Transformation: class Transformation:
__dimension__ = None __dimension__ = None
...@@ -37,12 +69,11 @@ class Transformation: ...@@ -37,12 +69,11 @@ class Transformation:
@classmethod @classmethod
def Identity(cls): def Identity(cls):
return cls(np.identity(cls.__size__), TransformationType.Identity)
return cls(np.identity(cls.__size__))
############################################## ##############################################
def __init__(self, obj): def __init__(self, obj, transformation_type=TransformationType.Generic):
if isinstance(obj, Transformation): if isinstance(obj, Transformation):
if self.same_dimension(obj): if self.same_dimension(obj):
...@@ -57,7 +88,9 @@ class Transformation: ...@@ -57,7 +88,9 @@ class Transformation:
else: else:
array = np.array((self.__size__, self.__size__)) array = np.array((self.__size__, self.__size__))
array[...] = obj array[...] = obj
self._m = np.array(array) self._m = np.array(array)
self._type = transformation_type
############################################## ##############################################
...@@ -73,6 +106,10 @@ class Transformation: ...@@ -73,6 +106,10 @@ class Transformation:
def array(self): def array(self):
return self._m return self._m
@property
def type(self):
return self._type
############################################## ##############################################
def __repr__(self): def __repr__(self):
...@@ -98,6 +135,14 @@ class Transformation: ...@@ -98,6 +135,14 @@ class Transformation:
elif isinstance(obj, Vector2D): elif isinstance(obj, Vector2D):
array = np.matmul(self._m, np.transpose(obj.v)) array = np.matmul(self._m, np.transpose(obj.v))
return Vector2D(array) return Vector2D(array)
elif isinstance(obj, (int, float)):
# Scalar can only be scaled if the frame is not sheared
if self._type in (TransformationType.Identity, TransformationType.Rotation):
return obj
elif self._type not in (TransformationType.Shear, TransformationType.Generic):
return abs(self._m[0,0]) * obj
else:
raise ValueError('Transformation is sheared')
else: else:
raise ValueError raise ValueError
...@@ -106,7 +151,22 @@ class Transformation: ...@@ -106,7 +151,22 @@ class Transformation:
def __imul__(self, obj): def __imul__(self, obj):
if isinstance(obj, Transformation): if isinstance(obj, Transformation):
self._m = np.matmul(self._m, obj.array) if obj._type != TransformationType.Identity:
self._m = np.matmul(self._m, obj.array)
# Fixme: check matrix value ???
# usage identity/rotation, scale/parity test
# metric test ?
# if t in (parity, xparity, yparity) t*t = Id
# if t in (rotation, scale) t*t = t
if self._type == obj._type:
if self._type in (TransformationType.Parity,
TransformationType.XParity,
TransformationType.YParity):
self._type = TransformationType.Identity
elif self._type not in (TransformationType.Rotation, TransformationType.Scale):
self._type = TransformationType.Generic
else: # shear, generic
self._type = TransformationType.Generic
else: else:
raise ValueError raise ValueError
...@@ -128,13 +188,38 @@ class Transformation2D(Transformation): ...@@ -128,13 +188,38 @@ class Transformation2D(Transformation):
c = cos(angle) c = cos(angle)
s = sin(angle) s = sin(angle)
return cls(np.array(((c, -s), (s, c)))) return cls(np.array(((c, -s), (s, c))), TransformationType.Rotation)
############################################## ##############################################
@classmethod @classmethod
def Scale(cls, x_scale, y_scale): def type_for_scale(cls, x_scale, y_scale):
return cls(np.array(((x_scale, 0), (0, y_scale))))
if x_scale == y_scale:
if x_scale == 1:
transformation_type = TransformationType.Identity
elif x_scale == -1:
transformation_type = TransformationType.Parity
else:
transformation_type = TransformationType.Scale
else:
if x_scale == -1 and y_scale == 1:
transformation_type = TransformationType.XParity
elif x_scale == 1 and y_scale == -1:
transformation_type = TransformationType.YParity
else:
transformation_type = TransformationType.Shear
return transformation_type
##############################################
@classmethod
def Scale(cls, x_scale, y_scale=None):
if y_scale is None:
y_scale = x_scale
transformation_type = cls.type_for_scale(x_scale, y_scale)
return cls(np.array(((x_scale, 0), (0, y_scale))), transformation_type)
############################################## ##############################################
...@@ -201,6 +286,7 @@ class AffineTransformation2D(AffineTransformation): ...@@ -201,6 +286,7 @@ class AffineTransformation2D(AffineTransformation):
transformation = cls.Identity() transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Rotation(angle).array transformation.matrix_part[...] = Transformation2D.Rotation(angle).array
transformation._type = TransformationType.Rotation
return transformation return transformation
############################################## ##############################################
...@@ -212,6 +298,7 @@ class AffineTransformation2D(AffineTransformation): ...@@ -212,6 +298,7 @@ class AffineTransformation2D(AffineTransformation):
transformation = cls.Identity() transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Scale(x_scale, y_scale).array transformation.matrix_part[...] = Transformation2D.Scale(x_scale, y_scale).array
transformation._type = cls.type_for_scale(x_scale, y_scale)
return transformation return transformation
####################################### #######################################
......
...@@ -18,6 +18,14 @@ ...@@ -18,6 +18,14 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement triangle.
"""
####################################################################################################
__all__ = ['Triangle2D']
#################################################################################################### ####################################################################################################
import math import math
......
...@@ -18,6 +18,18 @@ ...@@ -18,6 +18,18 @@
# #
#################################################################################################### ####################################################################################################
"""Module to implement vector.
"""
####################################################################################################
__all__ = [
'Vector2D',
'NormalisedVector2D',
'HomogeneousVector2D',
]
#################################################################################################### ####################################################################################################
import math import math
......
...@@ -33,7 +33,15 @@ from Patro.GeometryEngine.Conic import Circle2D, Ellipse2D, AngularDomain ...@@ -33,7 +33,15 @@ from Patro.GeometryEngine.Conic import Circle2D, Ellipse2D, AngularDomain
from Patro.GeometryEngine.Polyline import Polyline2D from Patro.GeometryEngine.Polyline import Polyline2D
from Patro.GeometryEngine.Rectangle import Rectangle2D from Patro.GeometryEngine.Rectangle import Rectangle2D
from Patro.GeometryEngine.Segment import Segment2D from Patro.GeometryEngine.Segment import Segment2D
from Patro.GraphicStyle import Colors, StrokeStyle from .GraphicItemMixin import (
GraphicItem,
PathStyleItemMixin,
PositionMixin,
TwoPositionMixin,
FourPositionMixin,
NPositionMixin,
StartStopAngleMixin,
)
#################################################################################################### ####################################################################################################
...@@ -41,267 +49,6 @@ _module_logger = logging.getLogger(__name__) ...@@ -41,267 +49,6 @@ _module_logger = logging.getLogger(__name__)
#################################################################################################### ####################################################################################################
class GraphicPathStyle:
##############################################
def __init__(self,
stroke_style=StrokeStyle.SolidLine,
line_width=1.0,
stroke_color=Colors.black,
fill_color=None, # only for closed path
):
"""*color* can be a defined color name, a '#rrggbb' string or a :class:`Color` instance.
"""
self.stroke_style = stroke_style
self.line_width = line_width
self.stroke_color = stroke_color
self.fill_color = fill_color
##############################################
def clone(self):
return self.__class__(
self._stroke_style,
self._line_width,
self._stroke_color,
self._fill_color,
)
##############################################
def __repr__(self):
return 'GraphicPathStyle({0._stroke_style}, {0._line_width}, {0._stroke_color}, {0._fill_color})'.format(self)
##############################################
@property
def stroke_style(self):
return self._stroke_style
@stroke_style.setter
def stroke_style(self, value):
self._stroke_style = StrokeStyle(value)
##############################################
@property
def line_width(self):
return self._line_width
@line_width.setter
def line_width(self, value):
self._line_width = value # Fixme: float ???
@property
def line_width_as_float(self):
line_width = self._line_width
# Fixme: use scale ?
if isinstance(line_width, float):
return line_width
else:
line_width = line_width.replace('pt', '')
line_width = line_width.replace('px', '')
return float(line_width)
##############################################
@property
def stroke_color(self):
return self._stroke_color
@stroke_color.setter
def stroke_color(self, value):
self._stroke_color = Colors.ensure_color(value)
##############################################
@property
def fill_color(self):
return self._fill_color
@fill_color.setter
def fill_color(self, value):
self._fill_color = Colors.ensure_color(value)
####################################################################################################
class GraphicBezierStyle(GraphicPathStyle):
##############################################
def __init__(self,
# Fixme: duplicate
stroke_style=StrokeStyle.SolidLine,
line_width=1.0,
stroke_color=Colors.black,
fill_color=None, # only for closed path
#
show_control=False,
control_color=None,
):
super().__init__(stroke_style, line_width, stroke_color, fill_color)
self._show_control = show_control
self._control_color = Colors.ensure_color(control_color)
##############################################
def clone(self):
return self.__class__(
self.stroke_style,
self.line_width,
self.stroke_color,
self.fill_color,
self._show_control,
self._control_color,
)
##############################################
@property
def show_control(self):
return self._show_control
@show_control.setter
def show_control(self, value):
self._show_control = value
##############################################
@property
def control_color(self):
return self._control_color
@control_color.setter
def control_color(self, value):
self._control_color = value
####################################################################################################
class PositionMixin:
##############################################
def __init__(self, position):
# Fixme: could be Vector2D or name
self._position = position # Vector2D(position)
##############################################
@property
def position(self):
return self._position
# @position.setter
# def position(self, value):
# self._position = value
@property
def positions(self):
return (self._position)
@property
def casted_position(self):
return self._scene.cast_position(self._position)
####################################################################################################
class TwoPositionMixin:
##############################################
def __init__(self, position1, position2):
self._position1 = position1
self._position2 = position2
##############################################
@property
def position1(self):
return self._position1
@property
def position2(self):
return self._position2
@property
def positions(self):
return (self._position1, self._position2)
####################################################################################################
class FourPositionMixin(TwoPositionMixin):
##############################################
def __init__(self, position1, position2, position3, position4):
TwoPositionMixin.__init__(self, position1, position2)
self._position3 = position3
self._position4 = position4
##############################################
@property
def position3(self):
return self._position3
@property
def position4(self):
return self._position4
@property
def positions(self):
return (self._position1, self._position2, self._position3, self._position4)
####################################################################################################
class NPositionMixin:
##############################################
def __init__(self, positions):
self._positions = list(positions)
##############################################
@property
def positions(self): # Fixme: versus points
return self._positions # Fixme: iter list ???
####################################################################################################
class StartStopAngleMixin:
##############################################
def __init__(self, start_angle=0, stop_angle=360):
self._start_angle = start_angle
self._stop_angle = stop_angle
##############################################
@property
def start_angle(self):
return self._start_angle
# @start_angle.setter
# def start_angle(self, value):
# self._start_angle = value
@property
def stop_angle(self):
return self._stop_angle
# @stop_angle.setter
# def stop_angle(self, value):
# self._stop_angle = value
####################################################################################################
class CoordinateItem(PositionMixin): class CoordinateItem(PositionMixin):
############################################## ##############################################
...@@ -320,146 +67,6 @@ class CoordinateItem(PositionMixin): ...@@ -320,146 +67,6 @@ class CoordinateItem(PositionMixin):
#################################################################################################### ####################################################################################################
class GraphicItem:
# clipping
# opacity
__subclasses__ = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.__subclasses__.append(cls)
##############################################
def __init__(self, scene, user_data):
self._scene = scene
self._user_data = user_data
self._z_value = 0
self._visible = True
self._selected = False
self._dirty = True
self._geometry = None
self._bounding_box = None
##############################################
@property
def scene(self):
return self._scene
@property
def user_data(self):
return self._user_data
##############################################
def __hash__(self):
return hash(self._user_data)
##############################################
@property
def z_value(self):
return self._z_value
@z_value.setter
def z_value(self, value):
self._z_value = value
@property
def visible(self):
return self._visible
@visible.setter
def visible(self, value):
self._visible = value
@property
def selected(self):
return self._selected
@selected.setter
def selected(self, value):
self._selected = bool(value)
##############################################
@property
def positions(self):
raise NotImplementedError
##############################################
@property
def casted_positions(self):
cast = self._scene.cast_position
return [cast(position) for position in self.positions]
##############################################
@property
def dirty(self):
return self._dirty
@dirty.setter
def dirty(self, value):
if bool(value):
self._dirty = True
else:
self._dirty = True
self._geometry = None
self._bounding_box = None
##############################################
@property
def bounding_box(self):
if self._bounding_box is None:
self._bounding_box = self.get_bounding_box()
return self._bounding_box
@property
def geometry(self):
if self._geometry is None:
self._geometry = self.get_geometry()
self._dirty = False
return self._geometry
##############################################
def get_geometry(self):
raise NotImplementedError
##############################################
def get_bounding_box(self):
return self.geometry.bounding_box
##############################################
def distance_to_point(self, point):
return self.geometry.distance_to_point(point)
####################################################################################################
class Font:
##############################################
def __init__(self, family, point_size):
self.family = family
self.point_size = point_size
####################################################################################################
class TextItem(PositionMixin, GraphicItem): class TextItem(PositionMixin, GraphicItem):
############################################## ##############################################
...@@ -500,28 +107,6 @@ class TextItem(PositionMixin, GraphicItem): ...@@ -500,28 +107,6 @@ class TextItem(PositionMixin, GraphicItem):
#################################################################################################### ####################################################################################################
class PathStyleItemMixin(GraphicItem):
##############################################
def __init__(self, scene, path_style, user_data):
GraphicItem.__init__(self, scene, user_data)
self._path_style = path_style
##############################################
@property
def path_style(self):
return self._path_style
# @path_style.setter
# def path_style(self, value):
# self._path_style = value
####################################################################################################
class CircleItem(PositionMixin, StartStopAngleMixin, PathStyleItemMixin): class CircleItem(PositionMixin, StartStopAngleMixin, PathStyleItemMixin):
############################################## ##############################################
......
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2019 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 graphic scene items mixins.
"""
####################################################################################################
__all__ = [
'GraphicItem',
'PathStyleItemMixin',
'PositionMixin',
'TwoPositionMixin',
'FourPositionMixin',
'NPositionMixin',
'StartStopAngleMixin',
]
####################################################################################################
class GraphicItem:
# clipping
# opacity
__subclasses__ = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.__subclasses__.append(cls)
##############################################
def __init__(self, scene, user_data):
self._scene = scene
self._user_data = user_data
self._z_value = 0
self._visible = True
self._selected = False
self._dirty = True
self._geometry = None
self._bounding_box = None
##############################################
@property
def scene(self):
return self._scene
@property
def user_data(self):
return self._user_data
##############################################
def __hash__(self):
return hash(self._user_data)
##############################################
@property
def z_value(self):
return self._z_value
@z_value.setter
def z_value(self, value):
self._z_value = value
@property
def visible(self):
return self._visible
@visible.setter
def visible(self, value):
self._visible = value
@property
def selected(self):
return self._selected
@selected.setter
def selected(self, value):
self._selected = bool(value)
##############################################
@property
def positions(self):
raise NotImplementedError
##############################################
@property
def casted_positions(self):
cast = self._scene.cast_position
return [cast(position) for position in self.positions]
##############################################
@property
def dirty(self):
return self._dirty
@dirty.setter
def dirty(self, value):
if bool(value):
self._dirty = True
else:
self._dirty = True
self._geometry = None
self._bounding_box = None
##############################################
@property
def bounding_box(self):
if self._bounding_box is None:
self._bounding_box = self.get_bounding_box()
return self._bounding_box
@property
def geometry(self):
if self._geometry is None:
self._geometry = self.get_geometry()
self._dirty = False
return self._geometry
##############################################
def get_geometry(self):
raise NotImplementedError
##############################################
def get_bounding_box(self):
return self.geometry.bounding_box
##############################################
def distance_to_point(self, point):
return self.geometry.distance_to_point(point)
####################################################################################################
class PathStyleItemMixin(GraphicItem):
##############################################
def __init__(self, scene, path_style, user_data):
GraphicItem.__init__(self, scene, user_data)
self._path_style = path_style
##############################################
@property
def path_style(self):
return self._path_style
# @path_style.setter
# def path_style(self, value):
# self._path_style = value
####################################################################################################
class PositionMixin:
##############################################
def __init__(self, position):
# Fixme: could be Vector2D or name
self._position = position # Vector2D(position)
##############################################
@property
def position(self):
return self._position
# @position.setter
# def position(self, value):
# self._position = value
@property
def positions(self):
return (self._position)
@property
def casted_position(self):
return self._scene.cast_position(self._position)
####################################################################################################
class TwoPositionMixin:
##############################################
def __init__(self, position1, position2):
self._position1 = position1
self._position2 = position2
##############################################
@property
def position1(self):
return self._position1
@property
def position2(self):
return self._position2
@property
def positions(self):
return (self._position1, self._position2)
####################################################################################################
class FourPositionMixin(TwoPositionMixin):
##############################################
def __init__(self, position1, position2, position3, position4):
TwoPositionMixin.__init__(self, position1, position2)
self._position3 = position3
self._position4 = position4
##############################################
@property
def position3(self):
return self._position3
@property
def position4(self):
return self._position4
@property
def positions(self):
return (self._position1, self._position2, self._position3, self._position4)
####################################################################################################
class NPositionMixin:
##############################################
def __init__(self, positions):
self._positions = list(positions)
##############################################
@property
def positions(self): # Fixme: versus points
return self._positions # Fixme: iter list ???
####################################################################################################
class StartStopAngleMixin:
##############################################
def __init__(self, start_angle=0, stop_angle=360):
self._start_angle = start_angle
self._stop_angle = stop_angle
##############################################
@property
def start_angle(self):
return self._start_angle
# @start_angle.setter
# def start_angle(self, value):
# self._start_angle = value
@property
def stop_angle(self):
return self._stop_angle
# @stop_angle.setter
# def stop_angle(self, value):
# self._stop_angle = value
##############################################
@property
def is_closed(self):
return abs(self._stop_angle - self.start_angle) >= 360
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2019 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 graphic styles.
"""
# Fixme: get_geometry / as argument
####################################################################################################
__all__= ['GraphicPathStyle', 'GraphicBezierStyle', 'Font']
####################################################################################################
import logging
from Patro.GraphicStyle import Colors, StrokeStyle, CapStyle, JoinStyle
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class GraphicPathStyle:
"""Class to define path style"""
##############################################
def __init__(self, **kwargs):
"""*color* can be a defined color name, a '#rrggbb' string or a :class:`Color` instance.
"""
self.stroke_style = kwargs.get('stroke_style', StrokeStyle.SolidLine)
self.line_width = kwargs.get('line_width', 1.0)
self.stroke_color = kwargs.get('stroke_color', Colors.black)
self.stroke_alpha = kwargs.get('stroke_alpha', 1.0)
self.fill_color = kwargs.get('fill_color', None) # only for closed path
self.fill_alpa = kwargs.get('fill_alpha', 1.0)
# This is default Qt
self.cap_style = kwargs.get('cap_style', CapStyle.SquareCap)
self.join_style = kwargs.get('join_style', JoinStyle.BevelJoin)
##############################################
def _dict_keys(self):
return (
'stroke_style',
'line_width',
'stroke_color',
'fill_color',
)
##############################################
def _to_dict(self):
return {name:getattr(self, '_' + name) for name in self._dict_keys()}
##############################################
def clone(self):
return self.__class__(**self._to_dict())
##############################################
def __repr__(self):
return '{0}({1})'.format(self.__class__.__name__, self._to_dict())
##############################################
@property
def stroke_style(self):
return self._stroke_style
@stroke_style.setter
def stroke_style(self, value):
self._stroke_style = StrokeStyle(value)
##############################################
@property
def line_width(self):
return self._line_width
@line_width.setter
def line_width(self, value):
self._line_width = value # Fixme: float ???
@property
def line_width_as_float(self):
line_width = self._line_width
# Fixme: use scale ?
if isinstance(line_width, float):
return line_width
else:
line_width = line_width.replace('pt', '')
line_width = line_width.replace('px', '')
return float(line_width)
##############################################
@property
def stroke_color(self):
return self._stroke_color
@stroke_color.setter
def stroke_color(self, value):
self._stroke_color = Colors.ensure_color(value)
@property
def stroke_alpha(self):
return self._stroke_alpha
@stroke_alpha.setter
def stroke_alpha(self, value):
self._stroke_alpha = float(value) # Fixme: check < 1
##############################################
@property
def fill_color(self):
return self._fill_color
@fill_color.setter
def fill_color(self, value):
self._fill_color = Colors.ensure_color(value)
@property
def fill_alpha(self):
return self._fill_alpha
@fill_alpha.setter
def fill_alpha(self, value):
self._fill_alpha = float(value)
##############################################
@property
def cap_style(self):
return self._cap_style
@cap_style.setter
def cap_style(self, value):
self._cap_style = CapStyle(value)
@property
def join_style(self):
return self._join_style
@join_style.setter
def join_style(self, value):
self._join_style = JoinStyle(value)
####################################################################################################
class GraphicBezierStyle(GraphicPathStyle):
"""Class to define Bézier curve style"""
##############################################
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.show_control = kwargs.get('show_control', False)
self.control_color = kwargs.get('control_color', None)
##############################################
def _dict_keys(self):
return (
'show_control',
'control_color'
)
##############################################
@property
def show_control(self):
return self._show_control
@show_control.setter
def show_control(self, value):
self._show_control = bool(value)
##############################################
@property
def control_color(self):
return self._control_color
@control_color.setter
def control_color(self, value):
self._control_color = Colors.ensure_color(value)
####################################################################################################
class Font:
"""Class to define font style"""
##############################################
def __init__(self, family, point_size):
self.family = family
self.point_size = point_size