Skip to content
Commits on Source (25)
......@@ -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/search-black.png
examples/patterns/demo-custom-seam-allowance.details.png
examples/patterns/demo-custom-seam-allowance.draw.png
examples/patterns/detail-demo1.details.png
examples/patterns/detail-demo1.draw.png
examples/patterns/flat-city-trouser.draw.png
examples/patterns/layout-demo.details.png
examples/patterns/layout-demo.draw.png
examples/patterns/operations-demo.draw-1.png
examples/patterns/operations-demo.draw-2.png
examples/patterns/operations-demo.draw.png
examples/patterns/path-bezier.draw.png
examples/data/dxf/protection-circulaire-seul-v1.dxf
examples/data/dxf/protection-circulaire.dxf
examples/data/dxf/protection-rectangulaire-v1.dxf
examples/data/dxf/protection-rectangulaire-v2.dxf
examples/data/dxf/test-dxf-r15-spline.svg
examples/data/dxf/test-dxf-r15.pdf
examples/data/dxf/test-dxf-r15.svg
examples/data/patterns-svg/veravenus-little-bias-dress.pattern-a0.pdf
examples/data/patterns-svg/veravenus-little-bias-dress.pattern-a0.svg
examples/data/patterns-valentina/backup/
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/
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/Pattern/dev/
Patro/QtApplication/rcc/icons/36x36-unused/
devel-experimentations/
doc/references-not-versioned/
doc/sphinx/source/features-all.txt
examples/output-2017/
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
git-log.txt
notes.txt
notes/
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 @@
#
####################################################################################################
"""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
# G1 = geometric continuity
# Tangents point to the same direction
......@@ -31,6 +209,13 @@
####################################################################################################
__all__ = [
'QuadraticBezier2D',
'CubicBezier2D',
]
####################################################################################################
from math import log, sqrt
import numpy as np
......@@ -44,12 +229,6 @@ from .Vector import Vector2D
####################################################################################################
# Fixme:
# max distance to the chord for linear approximation
# fitting
####################################################################################################
class BezierMixin2D(Primitive2DMixin):
"""Mixin to implements 2D Bezier Curve."""
......@@ -60,7 +239,7 @@ class BezierMixin2D(Primitive2DMixin):
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:
dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude
......@@ -324,11 +503,14 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P):
def intersect_line(self, line):
"""Find the intersections of the curve with a line."""
"""Find the intersections of the curve with a line.
# 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.
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.
"""
# t, p0, p1, p2, p3 = symbols('t p0 p1 p2 p3')
# u = 1 - t
......@@ -382,10 +564,15 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P):
def closest_point(self, point):
# Reference:
# 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
"""Return the closest point on the curve to the given *point*.
Reference
* 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:
# (P - B(t)) . B'(t) = 0 where t in [0,1]
......@@ -439,6 +626,29 @@ class QuadraticBezier2D(BezierMixin2D, Primitive3P):
else:
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)
......@@ -866,11 +1076,12 @@ class CubicBezier2D(BezierMixin2D, Primitive4P):
*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
# PostScript Language Reference. Addison- Wesley, third edition, 1999
* Kaspar Fischer and Roger Willcocks http://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf
* 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
# line passing by the start and stop point.
......
......@@ -19,6 +19,7 @@
####################################################################################################
"""Module to compute bounding box and convex hull for a set of points.
"""
####################################################################################################
......@@ -73,19 +74,24 @@ def _sort_point_for_graham_scan(points):
####################################################################################################
def _ccw(p1, p2, p3):
# Three points are a counter-clockwise turn if ccw > 0, clockwise if ccw < 0, and collinear if
# ccw = 0 because ccw is a determinant that gives twice the signed area of the triangle formed
# by p1, p2 and p3.
"""Three points are a counter-clockwise turn if ccw > 0, clockwise if ccw < 0, and collinear if ccw
= 0 because ccw is a determinant that gives twice the signed area of the triangle formed by p1,
p2 and p3.
"""
return (p2.x - p1.x)*(p3.y - p1.y) - (p2.y - p1.y)*(p3.x - p1.x)
####################################################################################################
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 = []
......
......@@ -33,10 +33,15 @@ Valentina Requirements
"""
# Fixme:
#
# Ellipse passing by two points
# https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
#
####################################################################################################
__all__ = [
'AngularDomain',
'Circle2D',
'Ellipse2D',
]
......@@ -48,161 +53,13 @@ from math import fabs, sqrt, radians, pi, cos, sin # , degrees
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 .Line import Line2D
from .Mixin import AngularDomainMixin, CenterMixin, AngularDomain
from .Primitive import Primitive, Primitive2DMixin
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):
......@@ -210,7 +67,7 @@ class PointNotOnCircleError(ValueError):
####################################################################################################
class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive):
class Circle2D(Primitive2DMixin, CenterMixin, AngularDomainMixin, Primitive):
"""Class to implements 2D Circle."""
......@@ -283,18 +140,23 @@ class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive):
##############################################
def __repr__(self):
return '{0}({1._center}, {1._radius}, {1._domain})'.format(self.__class__.__name__, self)
def clone(self):
return self.__class__(self._center, self._radius, self._domain)
##############################################
@property
def center(self):
return self._center
def apply_transformation(self, transformation):
self._center = transformation * 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
def radius(self):
......@@ -331,7 +193,6 @@ class Circle2D(Primitive2DMixin, AngularDomainMixin, Primitive):
def point_at_angle(self, angle):
return self.__vector_cls__.from_polar(self._radius, angle) + self._center
##############################################
def point_in_circle_frame(self, point):
......@@ -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.
......@@ -611,18 +472,28 @@ class Ellipse2D(Primitive2DMixin, AngularDomainMixin, Primitive):
##############################################
def __repr__(self):
return '{0}({1._center}, {1._x_radius}, {1._x_radius}, {1._angle})'.format(self.__class__.__name__, self)
def clone(self):
return self.__class__(
self._center,
self._x_radius, self._y_radius,
self._angle,
self._domain,
)
##############################################
@property
def center(self):
return self._center
def apply_transformation(self, transformation):
self._center = transformation * 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
def x_radius(self):
......
......@@ -18,6 +18,14 @@
#
####################################################################################################
"""Module to perform interpolation."""
####################################################################################################
__all__ = [
'interpolate_two_points',
]
####################################################################################################
def interpolate_two_points(p0, p1, t):
......
......@@ -18,6 +18,14 @@
#
####################################################################################################
"""Module to implement line.
"""
####################################################################################################
__all__ = ['Line2D']
####################################################################################################
from Patro.Common.IterTools import pairwise
......
####################################################################################################
#
# 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
# it under the terms of the GNU General Public License as published by
......@@ -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:
##############################################
def __init__(self, point):
class AngularDomain:
self._point = point
"""Class to define an angular domain"""
##############################################
@property
def point(self):
return self._point
def __init__(self, start=0, stop=360, degrees=True):
# def geometry
if not degrees:
start = math.degrees(start)
stop = math.degrees(stop)
####################################################################################################
class Edge:
self.start = start
self.stop = stop
##############################################
def __init__(self, contour, location):
def __clone__(self):
return self.__class__(self._start, self._stop)
##############################################
self._contour = contour
self._location = location
def __repr__(self):
return '{0}({1._start}, {1._stop})'.format(self.__class__.__name__, self)
##############################################
@property
def start_point(self):
return self._contour._vertexes[self._location]
def start(self):
return self._start
@start.setter
def start(self, value):
self._start = float(value)
@property
def end_point(self):
return self._contour._vertexes[self._location +1]
def stop(self):
return self._stop
@stop.setter
def stop(self, value):
self._stop = float(value)
@property
def previous(self):
# (location = 0) - 1 = -1
return self._contour._edges[self._location -1]
def start_radians(self):
return radians(self._start)
@property
def next(self):
location = self._location +1
if location == self._contour.number_of_edges:
location = 0
return self._contour._edges[location]
def stop_radians(self):
return radians(self._stop)
##############################################
# def __eq__
# def geometry
@property
def is_null(self):
return self._stop == self._start
####################################################################################################
@property
def is_closed(self):
return abs(self._stop - self._start) >= 360
class SegmentEdge(Edge):
pass
@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
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):
Edge.__init__(self, contour, location)
self._curve = curve
self._reverse = reverse
####################################################################################################
# def __init__(self, first_point, second_point):
# # Fixme: mixin
# self._first_point = first_point
# self._second_point = second_point
# ##############################################
@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
# @property
# def first_point(self):
# return self._first_point
##############################################
# @property
# def second_point(self):
# return self._second_point
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 Contour:
_logger = _module_logger.getChild('Contour')
class AngularDomainMixin:
##############################################
def __init__(self):
@property
def domain(self):
return self._domain
self._vertexes = []
self._edges = []
@domain.setter
def domain(self, value):
if value is not None:
self._domain = value # Fixme: AngularDomain() ??
else:
self._domain = None
##############################################
@property
def start_point(self):
return self._vertexes[0]
@property
def end_point(self):
return self._vertexes[-1]
@property
def number_of_edges(self):
return len(self._edges)
def is_closed(self):
return self._domain is None
##############################################
def iter_on_vertexes(self):
return iter(self._vertexes)
def start_stop_point(self, start=True):
def iter_on_edges(self):
return iter(self._edges)
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
##############################################
def add_segment_edge(self, point):
vertex = Vertex(point)
self._vertexes.append(vertex)
self._edges.append(SegmentEdge(self, self.number_of_edges))
@property
def start_point(self):
return self.start_stop_point(start=True)
##############################################
def add_curved_edge(self, curve, reverse=False):
start_point = curve.start_point
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))
##############################################
@property
def stop_point(self):
return self.start_stop_point(start=False)
# def add_edge(self, edge, reverse=False):
####################################################################################################
# if not isinstance(edge, (Segment2D, CubicBezier2D)):
# raise ValueError()
class CenterMixin:
# if reverse:
# edge = edge.reverse()
@property
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():
# self._vertexes.append(point)
@property
def points(self):
return (self._center,)
......@@ -18,6 +18,10 @@
#
####################################################################################################
"""Module to implement path.
"""
####################################################################################################
__all__ = [
......@@ -50,6 +54,11 @@ class PathPart:
##############################################
def clone(self, path):
raise NotImplementedError
##############################################
def __repr__(self):
return self.__class__.__name__
......@@ -81,6 +90,7 @@ class PathPart:
@property
def start_point(self):
# Fixme: cache ???
prev_part = self.prev_part
if prev_part is not None:
return prev_part.stop_point
......@@ -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):
......@@ -151,10 +166,16 @@ class LinearSegment(PathPart):
if self._radius is not None:
if not isinstance(self.prev_part, LinearSegment):
raise ValueError('Previous path segment must be linear')
self._bulge_angle = None
self._bulge_center = None
self._start_bulge_point = None
self._stop_bulge_point = None
self._reset_cache()
##############################################
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):
##############################################
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
def point(self):
return self._point
......@@ -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
def length(self):
return self._length
......@@ -321,10 +366,15 @@ class DirectionalSegment(LinearSegment):
##############################################
@property
def offset(self):
# Fixme: cache ???
return Vector2D.from_polar(self._length, self.__angle__)
@property
def stop_point(self):
# 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):
# Fixme: cache ???
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):
......@@ -385,6 +440,12 @@ class TwoPointsMixin:
def point2(self, value):
self._point2 = Vector2D(value)
##############################################
def apply_transformation(self, transformation):
self._point1 = transformation * self._point1
self._point2 = transformation * self._point2
####################################################################################################
class QuadraticBezierSegment(PathPart, TwoPointsMixin):
......@@ -402,6 +463,11 @@ class QuadraticBezierSegment(PathPart, TwoPointsMixin):
##############################################
def clone(self, path):
return self.__class__(path, self._position, self._point1, self._point2)
##############################################
@property
def stop_point(self):
return self._point2
......@@ -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
def point3(self):
return self._point3
......@@ -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):
return len(self._parts)
......@@ -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):
self.p0 = point
......@@ -541,6 +647,7 @@ class Path2D(Primitive2DMixin, Primitive1P):
return self._add_part(PathSegment, point, radius)
def close(self, radius=None):
# Fixme: identify as close for SVG export
# Fixme: radius must apply to start and stop
return self._add_part(PathSegment, self._p0, radius, absolute=True)
......
......@@ -18,6 +18,13 @@
#
####################################################################################################
"""Module to implement polygon.
"""
####################################################################################################
__all__ = ['Polygon2D']
####################################################################################################
import math
......
......@@ -18,6 +18,14 @@
#
####################################################################################################
"""Module to implement polyline.
"""
####################################################################################################
__all__ = ['Polyline2D']
####################################################################################################
from .Primitive import PrimitiveNP, Primitive2DMixin
......
......@@ -18,6 +18,12 @@
#
####################################################################################################
"""Module to implement base classes for primitives.
"""
####################################################################################################
__all__ = [
'Primitive',
'Primitive2DMixin',
......@@ -34,6 +40,8 @@ import collections
import numpy as np
from .BoundingBox import bounding_box_from_points
# Fixme: circular import
# from .Transformation import Transformation2D
####################################################################################################
......@@ -92,6 +100,9 @@ class Primitive:
##############################################
# Fixme: part of the API imply points
# it is not true for Path2D
@property
def points(self):
raise NotImplementedError
......@@ -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
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])
##############################################
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
def geometry_matrix(self):
return np.array(list(self.points)).transpose()
......
......@@ -18,6 +18,14 @@
#
####################################################################################################
"""Module to implement rectangle.
"""
####################################################################################################
__all__ = ['Rectangle2D']
####################################################################################################
import math
......
......@@ -18,6 +18,14 @@
#
####################################################################################################
"""Module to implement segment.
"""
####################################################################################################
__all__ = ['Segment2D']
####################################################################################################
# from .Interpolation import interpolate_two_points
......
......@@ -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
import numpy as np
......@@ -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:
__dimension__ = None
......@@ -37,12 +69,11 @@ class Transformation:
@classmethod
def Identity(cls):
return cls(np.identity(cls.__size__))
return cls(np.identity(cls.__size__), TransformationType.Identity)
##############################################
def __init__(self, obj):
def __init__(self, obj, transformation_type=TransformationType.Generic):
if isinstance(obj, Transformation):
if self.same_dimension(obj):
......@@ -57,7 +88,9 @@ class Transformation:
else:
array = np.array((self.__size__, self.__size__))
array[...] = obj
self._m = np.array(array)
self._type = transformation_type
##############################################
......@@ -73,6 +106,10 @@ class Transformation:
def array(self):
return self._m
@property
def type(self):
return self._type
##############################################
def __repr__(self):
......@@ -98,6 +135,14 @@ class Transformation:
elif isinstance(obj, Vector2D):
array = np.matmul(self._m, np.transpose(obj.v))
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:
raise ValueError
......@@ -106,7 +151,22 @@ class Transformation:
def __imul__(self, obj):
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:
raise ValueError
......@@ -128,13 +188,38 @@ class Transformation2D(Transformation):
c = cos(angle)
s = sin(angle)
return cls(np.array(((c, -s), (s, c))))
return cls(np.array(((c, -s), (s, c))), TransformationType.Rotation)
##############################################
@classmethod
def Scale(cls, x_scale, y_scale):
return cls(np.array(((x_scale, 0), (0, y_scale))))
def type_for_scale(cls, x_scale, 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):
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Rotation(angle).array
transformation._type = TransformationType.Rotation
return transformation
##############################################
......@@ -212,6 +298,7 @@ class AffineTransformation2D(AffineTransformation):
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Scale(x_scale, y_scale).array
transformation._type = cls.type_for_scale(x_scale, y_scale)
return transformation
#######################################
......
......@@ -18,6 +18,14 @@
#
####################################################################################################
"""Module to implement triangle.
"""
####################################################################################################
__all__ = ['Triangle2D']
####################################################################################################
import math
......
......@@ -18,6 +18,18 @@
#
####################################################################################################
"""Module to implement vector.
"""
####################################################################################################
__all__ = [
'Vector2D',
'NormalisedVector2D',
'HomogeneousVector2D',
]
####################################################################################################
import math
......
......@@ -33,7 +33,15 @@ from Patro.GeometryEngine.Conic import Circle2D, Ellipse2D, AngularDomain
from Patro.GeometryEngine.Polyline import Polyline2D
from Patro.GeometryEngine.Rectangle import Rectangle2D
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__)
####################################################################################################
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):
##############################################
......@@ -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):
##############################################
......@@ -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):
##############################################
......
####################################################################################################
#
# 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