Skip to content
Commits on Source (70)
*.aux
*.qmlc
*.val.lock
*.vit.bak
*~
.directory
__pycache__
du.log
MANIFEST
auto/
build/
dist/
doc/sphinx/build
doc/sphinx/build/
doc/sphinx/source/api/
Valentina/Geometry/reference.txt
du.log
examples/output-old/
Patro/QtApplication/rcc/PatroRessource.py
Patro/QtApplication/rcc/patro.rcc
Patro/QtApplication/rcc/icons/36x36/add-black.png
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/output/
examples/patterns/backup/
examples/patterns/layout/
notes/
old-todo.txt
todo.txt
tools/upload-www
trash/
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/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
notes.txt
notes/
open-doc.sh
outdated.txt
ressources
src/
tools/upload-www
trash/
language: python
python:
- "3.5"
env:
before_script:
install:
- pip install -q -r requirements.txt
- pip install .
script:
# fixme:
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2018 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/>.
#
####################################################################################################
__all__ = ['AtomicCounter']
####################################################################################################
import threading
####################################################################################################
class AtomicCounter:
"""A thread-safe incrementing counter.
"""
##############################################
def __init__(self, initial=0):
"""Initialize a new atomic counter to given initial value (default 0)."""
self._value = initial
self._lock = threading.Lock()
##############################################
def __int__(self):
return self._value
##############################################
def increment(self, value=1):
"""Atomically increment the counter by value (default 1) and return the
new value.
"""
with self._lock:
self._value += value
return self._value
##############################################
def set(self, value):
"""Atomically set the counter to a value."""
with self._lock:
if value <= self._value:
raise ValueError
self._value = value
......@@ -26,6 +26,8 @@
__all__ = [
'quadratic_root',
'cubic_root',
'fifth_root',
'fifth_root_normalised',
]
####################################################################################################
......@@ -78,13 +80,28 @@ def cubic_root(a, b, c, d):
def cubic_root_sympy(a, b, c, d):
x = sympy.Symbol('x')
x = sympy.Symbol('x', real=True)
E = a*x**3 + b*x**2 + c*x + d
return [i.n() for i in sympy.real_roots(E, x)]
####################################################################################################
def fifth_root_normalised(a, b, c, d, e):
x = sympy.Symbol('x', real=True)
E = x**5 + a*x**4 + b*x**3 + c*x**2 + d*x + e
return [i.n() for i in sympy.real_roots(E, x)]
####################################################################################################
def fifth_root(*args):
a = args[0]
return fifth_root_normalised(*[x/a for x in args[1:]])
####################################################################################################
def cubic_root_normalised(a, b, c):
# Reference: ???
......
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2018 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/>.
#
####################################################################################################
__all__ = ['ObjectNameMixin', 'ObjectGlobalIdMixin']
####################################################################################################
from .AtomicCounter import AtomicCounter
####################################################################################################
class ObjectNameMixin:
"""Mixin for object with name"""
##############################################
def __init__(self, name=None):
self.name = name
##############################################
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if value is None:
self._name = None
else:
self._name = str(value)
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._name}'.format(self)
##############################################
def __str__(self):
return self._name
####################################################################################################
class ObjectGlobalIdMixin:
"""Mixin for object with a global id"""
__object_counter__ = AtomicCounter(-1)
##############################################
def __init__(self, id=None):
# Note: sub-classes share the same counter !
if id is not None:
ObjectGlobalIdMixin.__object_counter__.set(id)
self._id = id
else:
self._id = ObjectGlobalIdMixin.__object_counter__.increment()
##############################################
@property
def id(self):
return self._id
##############################################
def __int__(self):
return self._id
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._id}'.format(self)
####################################################################################################
class ObjectCkeckedIdMixin:
"""Mixin for object with id"""
##############################################
def __init__(self, id=None):
if id is None:
self._id = self.new_id()
else:
self.check_id(id)
self._id = id
##############################################
@property
def id(self):
return self._id
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._id}'.format(self)
##############################################
def __int__(self):
return self._id
##############################################
def new_id(self):
raise NotImplementedError
##############################################
def check_id(self, id):
return True
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
####################################################################################################
"""Implement Singleton
"""
####################################################################################################
import threading
####################################################################################################
class SingletonMetaClass(type):
"""A singleton metaclass.
This implementation supports subclassing and is thread safe.
"""
##############################################
def __init__(cls, class_name, super_classes, class_attribute_dict):
# It is called just after cls creation in order to complete cls.
# print('MetaSingleton __init__:', cls, class_name, super_classes, class_attribute_dict, sep='\n... ')
type.__init__(cls, class_name, super_classes, class_attribute_dict)
cls._instance = None
cls._rlock = threading.RLock() # A factory function that returns a new reentrant lock object.
##############################################
def __call__(cls, *args, **kwargs):
# It is called when cls is instantiated: cls(...).
# type.__call__ dispatches to the cls.__new__ and cls.__init__ methods.
# print('MetaSingleton __call__:', cls, args, kwargs, sep='\n... ')
with cls._rlock:
if cls._instance is None:
cls._instance = type.__call__(cls, *args, **kwargs)
return cls._instance
####################################################################################################
class singleton:
""" A singleton class decorator.
This implementation doesn't support subclassing.
"""
##############################################
def __init__(self, cls):
# print('singleton __init__: On @ decoration', cls, sep='\n... ')
self._cls = cls
self._instance = None
##############################################
def __call__(self, *args, **kwargs):
# print('singleton __call__: On instance creation', self, args, kwargs, sep='\n... ')
if self._instance is None:
self._instance = self._cls(*args, **kwargs)
return self._instance
####################################################################################################
def singleton_func(cls):
""" A singleton function decorator.
This implementation doesn't support subclassing.
"""
# print('singleton_func: On @ decoration', cls, sep='\n... ')
instances = {}
def get_instance(*args, **kwargs):
# print('singleton_func: On instance creation', cls, sep='\n... ')
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
####################################################################################################
class monostate:
""" A monostate base class.
"""
_shared_state = {}
##############################################
def __new__(cls, *args, **kwargs):
# print('monostate __new__:', cls, args, kwargs, sep='\n... ')
obj = super(monostate, cls).__new__(cls, *args, **kwargs)
obj.__dict__ = cls._shared_state
return obj
......@@ -50,7 +50,7 @@ class XmlFileMixin:
##############################################
def _parse(self):
def parse(self):
"""Parse a XML file and return the etree"""
with open(str(self._path), 'rb') as f:
source = f.read()
......@@ -59,6 +59,24 @@ class XmlFileMixin:
##############################################
@staticmethod
def _get_xpath_element(root, path):
def get_xpath_elements(root, path):
"""Utility function to get elements from a xpath and a root"""
return root.xpath(path)
##############################################
@staticmethod
def get_xpath_element(root, path):
"""Utility function to get an element from a xpath and a root"""
return root.xpath(path)[0]
##############################################
@classmethod
def get_text_element(cls, root, path):
"""Utility function to a text element from a xpath and a root"""
element = cls.get_xpath_element(root, path)
if hasattr(element, 'text'):
return element.text
else:
return None
......@@ -23,9 +23,10 @@
import os
import sys
####################################################################################################
from pathlib import Path
import Patro.Common.Path as PathTools # Fixme: why ?
# Fixme: why ?
import Patro.Common.Path as PathTools # due to Path class
####################################################################################################
......@@ -64,12 +65,12 @@ OS = OsFactory()
####################################################################################################
_this_file = PathTools.to_absolute_path(__file__)
_this_file = Path(__file__).resolve()
class Path:
musica_module_directory = PathTools.parent_directory_of(_this_file, step=2)
config_directory = os.path.dirname(_this_file)
patro_module_directory = _this_file.parents[1]
config_directory = _this_file.parent
####################################################################################################
......
......@@ -21,7 +21,7 @@ handlers:
console:
class: logging.StreamHandler
level: DEBUG
level: INFO
# formatter: ansi
stream: ext://sys.stdout
......
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
"""Module to handle the DXF file format.
"""
####################################################################################################
__all__ = ['DxfImporter']
####################################################################################################
import ezdxf # Python packahe to read/write DXF
from Patro.GeometryEngine.Conic import Circle2D, Ellipse2D, AngularDomain
from Patro.GeometryEngine.Segment import Segment2D
from Patro.GeometryEngine.Spline import BSpline2D
from Patro.GeometryEngine.Vector import Vector2D
from .Polyline import Polyline
####################################################################################################
class DxfImporter:
"""Class to implement a DXF importer.
"""
##############################################
def __init__(self, path):
path = str(path)
self._drawing = ezdxf.readfile(path)
self._model_space = self._drawing.modelspace()
self._items = []
self._read()
##############################################
def __len__(self):
return len(self._items)
def __iter__(self):
return iter(self._items)
def __getitem__(self, slice_):
return self._items[slice_]
##############################################
@staticmethod
def _to_vector(point):
return Vector2D(point[:2])
@classmethod
def _to_vectors(cls, points):
return [cls._to_vector(x) for x in points]
##############################################
def _add(self, item):
self._items.append(item)
##############################################
def _read(self):
for item in self._model_space:
dxf_type = item.dxftype()
if dxf_type == 'LINE':
self._on_line(item)
elif dxf_type in ('CIRCLE', 'ARC'):
self._on_circle(item, dxf_type == 'ARC')
elif dxf_type == 'ELLIPSE':
self._on_ellipse(item)
elif dxf_type == 'LWPOLYLINE':
self._on_polyline(item)
elif dxf_type == 'SPLINE':
self._on_spline(item)
# else skip
##############################################
def _on_line(self, item):
item_dxf = item.dxf
segment = Segment2D(*self._to_vectors((item_dxf.start, item_dxf.end)))
self._add(segment)
##############################################
def _on_circle(self, item, is_arc):
item_dxf = item.dxf
center = self._to_vector(item_dxf.center)
if is_arc:
domain = AngularDomain(item_dxf.start_angle, item_dxf.end_angle)
else:
domain = None
circle = Circle2D(center, item_dxf.radius, domain=domain)
self._add(circle)
##############################################
def _on_ellipse(self, item):
item_dxf = item.dxf
center = self._to_vector(item_dxf.center)
major_axis = self._to_vector(item_dxf.major_axis)
minor_axis = major_axis * item_dxf.ratio
domain = AngularDomain(item_dxf.start_param, item_dxf.end_param, degrees=False)
x_radius, y_radius = major_axis.magnitude, minor_axis.magnitude
angle = major_axis.orientation
if angle == 90:
x_radius, y_radius = y_radius, x_radius
angle = 0
# Fixme: ...
ellipse = Ellipse2D(
center,
x_radius, y_radius,
angle,
domain=domain,
)
self._add(ellipse)
##############################################
def _on_polyline(self, item):
polyline = Polyline(item.closed)
for x, y, s, e, b in item.get_points():
polyline.add(Vector2D(x, y), b)
geometry = polyline.geometry()
self._add(geometry)
##############################################
def _on_spline(self, item):
with item.edit_data() as data:
points = self._to_vectors(data.control_points)
item_dxf = item.dxf
spline = BSpline2D(points, item_dxf.degree, item.closed)
self._add(spline)
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
####################################################################################################
from Patro.Common.Math.Functions import sign
from Patro.GeometryEngine.Conic import Circle2D, AngularDomain
from Patro.GeometryEngine.Segment import Segment2D
from Patro.GeometryEngine.Vector import Vector2D
####################################################################################################
import math
####################################################################################################
class PolylineVertex:
# sagitta of a circular arc is the distance from the center of the arc to the center of its base.
#
# bulge = sagitta / half the length of the chord
# a bulge value of 1 defines a semicircle
#
# The sign of the bulge value defines the side of the bulge:
# positive value (> 0): bulge is right of line (count clockwise)
# negative value (< 0): bulge is left of line (clockwise)
# 0 = no bulge
# points = item.get_points(format='xyseb') # ???
# r = (s**2 + l**2) / (2*s)
# b = s / l
#
# s = b*l
# r = ((b*l)**2 + l**2) / (2*b*l)
# = l/2 * (b + 1/b)
#
# r - s = l/2 * (b + 1/b) - b*l
# = l/2 * (1/b - b)
##############################################
def __init__(self, polyline, index, point, bulge=0):
self._polyline = polyline
self._index = index
self._point = point
self._bulge = bulge
##############################################
@property
def point(self):
return self._point
@property
def x(self):
return self._point.x
@property
def y(self):
return self._point.y
@property
def bulge(self):
return self._bulge
##############################################
@property
def prev(self):
if self._index == 0 and not self._polyline.closed:
return None
return self._polyline[self._index -1]
##############################################
@property
def next(self):
if self._polyline.is_last_index(self._index):
if self._polyline.closed:
return self._polyline[0]
else:
return None
return self._polyline[self._index +1]
##############################################
def __str__(self):
return '({0.x:5.2f} {0.y:5.2f} {0._bulge:5.2f})'.format(self)
##############################################
@property
def segment_vector(self):
return self.next.point - self._point
##############################################
@property
def demi_chord(self):
return self.segment_vector.magnitude / 2
##############################################
@property
def sagitta(self):
return self.bulge * self.demi_chord
##############################################
@property
def bulge_radius(self):
if self._bulge == 0:
return 0
else:
return self.demi_chord/2 * (self._bulge + 1/self._bulge)
##############################################
@property
def sagitta_dual(self):
if self._bulge == 0:
return 0
else:
return self.demi_chord/2 * (1/self._bulge - self._bulge)
##############################################
@property
def angle(self):
if self._bulge == 0:
return 0
else:
return math.degrees(2 * math.atan(self.bulge_radius/self.demi_chord - self._bulge))
####################################################################################################
class Polyline:
##############################################
def __init__(self, closed=False):
self._vertices = []
self._closed = closed
##############################################
@property
def closed(self):
return self._closed
##############################################
def __len__(self):
return len(self._vertices)
def __iter__(self):
return iter(self._vertices)
def __getitem__(self, index):
return self._vertices[index]
def is_last_index(self, index):
return index == len(self) - 1
##############################################
def add(self, *args, **kwargs):
vertex = PolylineVertex(self, len(self), *args, **kwargs)
self._vertices.append(vertex)
##############################################
def __str__(self):
return ' '.join(['{:5.2f}'.format(vertex) for vertex in self])
##############################################
def iter_on_segment(self):
for i in range(len(self._vertices) -1):
yield self._vertices[i], self._vertices[i+1]
if self._closed:
yield self._vertices[-1], self._vertices[0]
##############################################
def geometry(self):
items = []
for vertex1, vertex2 in self.iter_on_segment():
segment = Segment2D(vertex1.point, vertex2.point)
if vertex1.bulge:
segment_center = segment.center
direction = vertex1.segment_vector.normalise()
normal = direction.normal
# offset = vertex1.bulge_radius - vertex1.sagitta
offset = vertex1.sagitta_dual
center = segment_center + normal * sign(vertex1.bulge) * offset
arc = Circle2D(center, vertex1.bulge_radius)
start_angle, stop_angle = [arc.angle_for_point(vertex.point) for vertex in (vertex1, vertex2)]
if start_angle < 0:
start_angle += 360
if stop_angle < 0:
stop_angle += 360
if vertex1.bulge < 0:
start_angle, stop_angle = stop_angle, start_angle
print('bulb', vertex1, vertex2, vertex1.bulge, start_angle, stop_angle)
arc.domain = AngularDomain(start_angle, stop_angle)
# arc = Circle2D(center, vertex1.bulge_radius, domain=AngularDomain(start_angle, stop_angle))
items.append(arc)
else:
items.append(segment)
return items
......@@ -67,7 +67,7 @@ class XmlMeasurement(XmlObjectAdaptator):
####################################################################################################
class VitFile(XmlFileMixin):
class VitFileInternal(XmlFileMixin):
_logger = _module_logger.getChild('VitFile')
......@@ -76,43 +76,52 @@ class VitFile(XmlFileMixin):
def __init__(self, path):
XmlFileMixin.__init__(self, path)
self._measurements = ValentinaMeasurements()
self._read()
self.measurements = ValentinaMeasurements()
self.read()
##############################################
@property
def measurements(self):
return self._measurements
##############################################
def _read(self):
def read(self):
self._logger.info('Load measurements from ' + str(self._path))
tree = self._parse()
tree = self.parse()
measurements = self._measurements
measurements = self.measurements
version = self._get_xpath_element(tree, 'version').text
# self._read_only = self._get_xpath_element(tree, 'read-only').text
# self._notes = self._get_xpath_element(tree, 'notes').text
self.unit = self._get_xpath_element(tree, 'unit').text
self.pattern_making_system = self._get_xpath_element(tree, 'pm_system').text
version = self.get_xpath_element(tree, 'version').text
# self.read_only = self.get_xpath_element(tree, 'read-only').text
# self.notes = self.get_xpath_element(tree, 'notes').text
self.unit = self.get_xpath_element(tree, 'unit').text
self.pattern_making_system = self.get_xpath_element(tree, 'pm_system').text
personal = measurements.personal
personal_element = self._get_xpath_element(tree, 'personal')
personal.last_name = self._get_xpath_element(personal_element, 'family-name').text
personal.first_name = self._get_xpath_element(personal_element, 'given-name').text
personal.birth_date = self._get_xpath_element(personal_element, 'birth-date').text
personal.gender = Gender[self._get_xpath_element(personal_element, 'gender').text.upper()]
personal.email = self._get_xpath_element(personal_element, 'email').text
elements = self._get_xpath_element(tree, 'body-measurements')
personal_element = self.get_xpath_element(tree, 'personal')
personal.last_name = self.get_xpath_element(personal_element, 'family-name').text
personal.first_name = self.get_xpath_element(personal_element, 'given-name').text
personal.birth_date = self.get_xpath_element(personal_element, 'birth-date').text
personal.gender = Gender[self.get_xpath_element(personal_element, 'gender').text.upper()]
personal.email = self.get_xpath_element(personal_element, 'email').text
elements = self.get_xpath_element(tree, 'body-measurements')
for element in elements:
if element.tag == XmlMeasurement.__tag__:
xml_measurement = XmlMeasurement(element)
measurements.add(**xml_measurement.to_dict())
else:
raise NotImplementedError
####################################################################################################
class VitFile:
##############################################
def __init__(self, path):
self._interval = VitFileInternal(path)
##############################################
@property
def measurements(self):
return self._interval.measurements
......@@ -18,7 +18,7 @@
#
####################################################################################################
"""This module implements the val XML file format.
"""This module implements the Valentina val XML file format.
"""
......@@ -31,7 +31,7 @@ from lxml import etree
from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.Pattern.Pattern import Pattern
from .Measurements import VitFile
from .Measurement import VitFile
from .VitFormat import (
Point,
Line,
......@@ -51,6 +51,32 @@ _module_logger = logging.getLogger(__name__)
####################################################################################################
# Last valentina version supported
VAL_VERSION = '0.7.10'
####################################################################################################
class Modeling:
"""Class to implement a modeling mapper."""
##############################################
def __init__(self):
self._id_map = {}
##############################################
def __getitem__(self, id):
return self._id_map[id]
##############################################
def add(self, item):
self._id_map[item.id] = item
####################################################################################################
class Dispatcher:
"""Baseclass to dispatch XML to Python class."""
......@@ -61,15 +87,15 @@ class Dispatcher:
def from_xml(self, element):
tag_class = self.__TAGS__[element.tag]
if tag_class is not None:
return tag_class(element)
tag_cls = self.__TAGS__[element.tag]
if tag_cls is not None:
return tag_cls(element)
else:
raise NotImplementedError
####################################################################################################
class CalculationDispatcher:
class CalculationDispatcher(Dispatcher):
"""Class to implement a dispatcher for calculations."""
......@@ -94,35 +120,35 @@ class CalculationDispatcher:
##############################################
def _register_mapping(self, xml_class):
def _register_mapping(self, xml_cls):
calculation_class = xml_class.__calculation__
if calculation_class:
self._mapping[xml_class] = calculation_class
self._mapping[calculation_class] = xml_class
operation_cls = xml_cls.__operation__
if operation_cls:
self._mapping[xml_cls] = operation_cls
self._mapping[operation_cls] = xml_cls
##############################################
def _init_mapper(self):
for tag_class in self.__TAGS__.values():
if tag_class is not None:
if hasattr(tag_class, '__TYPES__'):
for xml_class in tag_class.__TYPES__.values():
if xml_class is not None:
self._register_mapping(xml_class)
for tag_cls in self.__TAGS__.values():
if tag_cls is not None:
if hasattr(tag_cls, '__TYPES__'):
for xml_cls in tag_cls.__TYPES__.values():
if xml_cls is not None:
self._register_mapping(xml_cls)
else:
self._register_mapping(tag_class)
self._register_mapping(tag_cls)
##############################################
def from_xml(self, element):
tag_class = self.__TAGS__[element.tag]
if hasattr(tag_class, '__TYPES__'):
cls = tag_class.__TYPES__[element.attrib['type']]
tag_cls = self.__TAGS__[element.tag]
if hasattr(tag_cls, '__TYPES__'):
cls = tag_cls.__TYPES__[element.attrib['type']]
else:
cls = tag_class
cls = tag_cls
if cls is not None:
return cls(element)
else:
......@@ -130,34 +156,15 @@ class CalculationDispatcher:
##############################################
def from_calculation(self, calculation):
return self._mapping[calculation.__class__].from_calculation(calculation)
####################################################################################################
class Modeling:
##############################################
def __init__(self):
self._items = []
self._id_map = {}
##############################################
def __getitem__(self, id):
return self._id_map[id]
##############################################
def append(self, item):
self._items.append(item)
self._id_map[item.id] = item
def from_operation(self, operation):
return self._mapping[operation.__class__].from_operation(operation)
####################################################################################################
class ModelingDispatcher(Dispatcher):
"""Class to implement a dispatcher for modeling."""
__TAGS__ = {
'point': ModelingPoint,
'spline': ModelingSpline,
......@@ -167,6 +174,8 @@ class ModelingDispatcher(Dispatcher):
class DetailDispatcher(Dispatcher):
"""Class to implement a dispatcher for detail."""
__TAGS__ = {
'grainline': DetailGrainline,
'patternInfo': DetailPatternInfo,
......@@ -175,65 +184,62 @@ class DetailDispatcher(Dispatcher):
####################################################################################################
class ValFile(XmlFileMixin):
_calculation_dispatcher = CalculationDispatcher()
_modeling_dispatcher = ModelingDispatcher()
_detail_dispatcher = DetailDispatcher()
"""Class to read/write val file."""
####################################################################################################
_logger = _module_logger.getChild('ValFile')
class ValFileReaderInternal(XmlFileMixin):
_calculation_dispatcher = CalculationDispatcher()
_modeling_dispatcher = ModelingDispatcher()
_detail_dispatcher = DetailDispatcher()
"""Class to read val file."""
_logger = _module_logger.getChild('ValFileReader')
##############################################
def __init__(self, path=None):
def __init__(self, path):
# Fixme: path
if path is None:
path = ''
XmlFileMixin.__init__(self, path)
self._vit_file = None
self._pattern = None
# Fixme:
# if path is not None:
if path != '':
self._read()
##############################################
self.root = None
self.attribute = {}
self.vit_file = None
self.pattern = None
def Write(self, path, vit_file, pattern):
# Fixme: write
self._vit_file = vit_file
self._pattern = pattern
self.write(path)
self.read()
##############################################
@property
def measurements(self):
return self._vit_file.measurements
@property
def pattern(self):
return self._pattern
if self.vit_file is not None:
return self.vit_file.measurements
else:
return None
##############################################
def _read(self):
def read(self):
# <?xml version='1.0' encoding='UTF-8'?>
# <?xml version="1.0" encoding="UTF-8"?>
# <pattern>
# <!--Pattern created with Valentina (http://www.valentina-project.org/).-->
# <version>0.4.0</version>
# <!--Pattern created with Valentina v0.6.0.912b (https://valentinaproject.bitbucket.io/).-->
# <version>0.7.10</version>
# <unit>cm</unit>
# <author/>
# <description/>
# <notes/>
# <measurements/>
# <patternName>pattern name</patternName>
# <patternNumber>pattern number</patternNumber>
# <company>company/Designer name</company>
# <patternLabel>
# <line alignment="0" bold="true" italic="false" sfIncrement="4" text="%author%"/>
# </patternLabel>
# <patternMaterials/>
# <measurements>measurements.vit</measurements>
# <increments/>
# <draw name="Pattern piece 1">
# <previewCalculations/>
# <draw name="...">
# <calculation/>
# <modeling/>
# <details/>
......@@ -241,86 +247,182 @@ class ValFile(XmlFileMixin):
# </draw>
# </pattern>
tree = self._parse()
self._logger.info('Read Valentina file "{}"'.format(self.path))
measurements_path = Path(self._get_xpath_element(tree, 'measurements').text)
if not measurements_path.exists():
measurements_path = self._path.parent.joinpath(measurements_path)
self.root = self.parse()
self.read_attributes()
self.read_measurements()
# patternLabel
# patternMaterials
# increments
# previewCalculations
self.pattern = Pattern(self.measurements, self.attribute['unit'])
for piece in self.get_xpath_elements(self.root, 'draw'):
self.read_piece(piece)
##############################################
def read_measurements(self):
measurements_path = self.get_xpath_element(self.root, 'measurements').text
if measurements_path is not None:
measurements_path = Path(measurements_path)
if not measurements_path.exists():
measurements_path = self.path.parent.joinpath(measurements_path)
if not measurements_path.exists():
raise NameError("Cannot find {}".format(measurements_path))
self.vit_file = VitFile(measurements_path)
else:
self.vit_file = None
self._vit_file = VitFile(measurements_path)
##############################################
unit = self._get_xpath_element(tree, 'unit').text
def read_attributes(self):
required_attributes = (
'unit',
)
optional_attributes = (
'description',
'notes',
'patternName',
'patternNumber',
'company',
)
attribute_names = list(required_attributes) + list(optional_attributes)
self.attribute = {name:self.get_text_element(self.root, name) for name in attribute_names}
for name in required_attributes:
if self.attribute[name] is None:
raise NameError('{} is undefined'.format(name))
pattern = Pattern(self._vit_file.measurements, unit)
self._pattern = pattern
##############################################
def read_piece(self, piece):
piece_name = piece.attrib['name']
self._logger.info('Create scope "{}"'.format(piece_name))
scope = self.pattern.add_scope(piece_name)
for element in self._get_xpath_element(tree, 'draw/calculation'):
sketch = scope.sketch
for element in self.get_xpath_element(piece, 'calculation'):
try:
xml_calculation = self._calculation_dispatcher.from_xml(element)
xml_calculation.to_calculation(pattern)
xml_calculation = _calculation_dispatcher.from_xml(element)
operation = xml_calculation.to_operation(sketch)
self._logger.info('Add operation {}'.format(operation))
except NotImplementedError:
self._logger.warning('Not implemented calculation\n' + str(etree.tostring(element)))
pattern.eval()
sketch.eval()
modeling = Modeling()
for element in self._get_xpath_element(tree, 'draw/modeling'):
xml_modeling_item = self._modeling_dispatcher.from_xml(element)
modeling.append(xml_modeling_item)
# print(xml_modeling_item)
details = []
for detail_element in self._get_xpath_element(tree, 'draw/details'):
xml_detail = Detail(modeling, detail_element)
details.append(xml_detail)
print(xml_detail)
for element in detail_element:
if element.tag == 'nodes':
for node in element:
xml_node = DetailNode(node)
# print(xml_node)
xml_detail.append_node(xml_node)
else:
xml_modeling_item = self._detail_dispatcher.from_xml(element)
# Fixme: xml_detail. = xml_modeling_item
# print(xml_modeling_item)
for node, modeling_item in xml_detail.iter_on_nodes():
# print(node.object_id, '->', modeling_item, '->', modeling_item.object_id)
print(node, '->\n', modeling_item, '->\n', pattern.get_calculation(modeling_item.object_id))
for element in self.get_xpath_element(piece, 'modeling'):
xml_modeling_item = _modeling_dispatcher.from_xml(element)
modeling.add(xml_modeling_item)
self._logger.info('Modeling {}'.format(xml_modeling_item))
# details = []
for detail_element in self.get_xpath_element(piece, 'details'):
self.read_detail(scope, modeling, detail_element)
# details.append(xml_detail)
##############################################
def write(self, path=None):
def read_detail(self, scope, modeling, detail_element):
xml_detail = Detail(modeling, detail_element)
self._logger.info('Detail {}'.format(xml_detail))
for element in detail_element:
if element.tag == 'nodes':
for node in element:
xml_node = DetailNode(node)
xml_detail.append_node(xml_node)
else:
xml_modeling_item = _detail_dispatcher.from_xml(element)
# Fixme: xml_detail. = xml_modeling_item
print(xml_modeling_item)
for node, modeling_item in xml_detail.iter_on_nodes():
# print(node.object_id, '->', modeling_item, '->', modeling_item.object_id)
print(node, '->\n', modeling_item, '->\n', scope.sketch.get_operation(modeling_item.object_id))
####################################################################################################
class ValFileReader:
"""Class to read val file."""
##############################################
def __init__(self, path):
self._internal = ValFileReaderInternal(path)
##############################################
@property
def measurements(self):
return self._internal.measurements
@property
def pattern(self):
return self._internal.pattern
####################################################################################################
class ValFileWriter:
"""Class to write val file."""
_logger = _module_logger.getChild('ValFileWriter')
##############################################
def __init__(self, path, vit_file, pattern):
self._path = str(path)
self._vit_file = vit_file
self._pattern = pattern
root = self._build_xml_tree()
self._write(root)
##############################################
def _build_xml_tree(self):
root = etree.Element('pattern')
root.append(etree.Comment('Pattern created with Patro (https://github.com/FabriceSalvaire/Patro)'))
etree.SubElement(root, 'version').text = '0.4.0'
etree.SubElement(root, 'version').text = self.VAL_VERSION
etree.SubElement(root, 'unit').text = self._pattern.unit
etree.SubElement(root, 'author')
etree.SubElement(root, 'description')
etree.SubElement(root, 'notes')
etree.SubElement(root, 'measurements').text = str(self._vit_file.path)
measurements = etree.SubElement(root, 'measurements')
if self._vit_file is not None:
measurements.text = str(self._vit_file.path)
etree.SubElement(root, 'increments')
draw_element = etree.SubElement(root, 'draw') # Fixme:
draw_element.attrib['name'] = 'Pattern piece 1' # Fixme:
for scope in self._pattern.scopes:
draw_element = etree.SubElement(root, 'draw')
draw_element.attrib['name'] = scope.name
calculation_element = etree.SubElement(draw_element, 'calculation')
modeling_element = etree.SubElement(draw_element, 'modeling')
details_element = etree.SubElement(draw_element, 'details')
# group_element = etree.SubElement(draw_element, 'groups')
calculation_element = etree.SubElement(draw_element, 'calculation')
modeling_element = etree.SubElement(draw_element, 'modeling')
details_element = etree.SubElement(draw_element, 'details')
# group_element = etree.SubElement(draw_element, 'groups')
for operation in scope.sketch.operations:
xml_calculation = _calculation_dispatcher.from_operation(operation)
# print(xml_calculation)
# print(xml_calculation.to_xml_string())
calculation_element.append(xml_calculation.to_xml())
return root
##############################################
for calculation in self._pattern.calculations:
xml_calculation = self._calculation_dispatcher.from_calculation(calculation)
# print(xml_calculation)
# print(xml_calculation.to_xml_string())
calculation_element.append(xml_calculation.to_xml())
def _write(self, root):
if path is None:
path = self.path
with open(str(path), 'wb') as f:
with open(self._path, 'wb') as fh:
# ElementTree.write() ?
f.write(etree.tostring(root, pretty_print=True))
fh.write(etree.tostring(root, pretty_print=True))
......@@ -24,6 +24,23 @@ and the calculation API.
The purpose of each XmlObjectAdaptator sub-classes is to serve as a bidirectional adaptor between
the XML format and the API.
Valentina File Format Concept
* all entities which are referenced later in the file are identified by a unique positive integer
over the file, usually incremented from 1.
* a file contains one or several "pieces"
* pieces correspond to independent scopes, one cannot access calculations of another piece
* pieces share the same referential, usually the root point of a piece is placed next
to the previous piece
* a piece has "calculations" and "details"
* a calculations corresponds to a point, a segment, or a Bézier curve ...
* a detail corresponds to a garment piece defined by segments and curves
* one can define several details within a piece
"""
####################################################################################################
......@@ -43,14 +60,65 @@ __all__ = [
####################################################################################################
import Patro.Pattern.Calculation as Calculation
import Patro.Pattern.SketchOperation as SketchOperation
from Patro.Common.Xml.Objectivity import (
Attribute,
BoolAttribute,
IntAttribute, FloatAttribute,
StringAttribute,
XmlObjectAdaptator
)
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicStyle import Colors, StrokeStyle
####################################################################################################
class ColorAttribute(Attribute):
__COLORS__ = (
'black',
'blue',
'cornflowerblue',
'darkBlue',
'darkGreen',
'darkRed',
'darkviolet',
'deeppink',
'deepskyblue',
'goldenrod',
'green',
'lightsalmon',
'lime',
'mediumseagreen',
'orange',
'violet',
'yellow',
)
##############################################
@classmethod
def from_xml(cls, value):
return Colors.ensure_color(value)
####################################################################################################
class StrokeStyleAttribute(Attribute):
__STROKE_STYLE__ = {
'dashDotDotLine': StrokeStyle.DashDotDotLine,
'dashDotLine': StrokeStyle.DashDotLine,
'dashLine': StrokeStyle.DashLine,
'dotLine': StrokeStyle.DotLine,
'hair': StrokeStyle.SolidLine, # should be solid
'none': StrokeStyle.NoPen,
}
##############################################
@classmethod
def from_xml(cls, value):
return cls.__STROKE_STYLE__[value]
####################################################################################################
......@@ -208,22 +276,24 @@ class CalculationMixin:
IntAttribute('id'),
)
__calculation__ = None # calculation's class
__operation__ = None # operation's class
##############################################
def call_calculation_function(self, pattern, kwargs):
return getattr(pattern, self.__calculation__.__name__)(**kwargs)
def call_operation_function(self, sketch, kwargs):
# Fixme: map valentina name -> ...
method = getattr(sketch, self.__operation__.__name__)
return method(**kwargs)
##############################################
def to_calculation(self, pattern):
def to_operation(self, sketch):
raise NotImplementedError
##############################################
@classmethod
def from_calculation(calculation):
def from_operation(operation):
raise NotImplementedError
####################################################################################################
......@@ -240,37 +310,8 @@ class CalculationTypeMixin(CalculationMixin):
class LinePropertiesMixin:
__attributes__ = (
StringAttribute('line_color', 'lineColor'),
StringAttribute('line_style', 'typeLine'),
)
__COLORS__ = (
'black',
'blue',
'cornflowerblue',
'darkBlue',
'darkGreen',
'darkRed',
'darkviolet',
'deeppink',
'deepskyblue',
'goldenrod',
'green',
'lightsalmon',
'lime',
'mediumseagreen',
'orange',
'violet',
'yellow',
)
__LINE_STYLE__ = (
'dashDotDotLine',
'dashDotLine',
'dashLine',
'dotLine',
'hair', # should be solid
'none',
ColorAttribute('line_color', 'lineColor'),
StrokeStyleAttribute('line_style', 'typeLine'),
)
class XyMixin:
......@@ -334,19 +375,19 @@ class PointMixin(CalculationTypeMixin, MxMyMixin):
##############################################
def to_calculation(self, pattern):
def to_operation(self, sketch):
kwargs = self.to_dict(exclude=('mx', 'my')) # id'
kwargs['label_offset'] = Vector2D(self.mx, self.my)
return self.call_calculation_function(pattern, kwargs)
return self.call_operation_function(sketch, kwargs)
##############################################
@classmethod
def from_calculation(cls, calculation):
def from_operation(cls, operation):
kwargs = cls.get_dict(calculation, exclude=('mx', 'my'))
label_offset = calculation.label_offset
kwargs = cls.get_dict(operation, exclude=('mx', 'my'))
label_offset = operation.label_offset
kwargs['mx'] = label_offset.x
kwargs['my'] = label_offset.y
return cls(**kwargs)
......@@ -364,7 +405,7 @@ class AlongLinePoint(PointLinePropertiesMixin, FirstSecondPointMixin, LengthMixi
# length="-Line_Bt_Ct" name="Dt" lineColor="black" type="alongLine" my="0.2"/>
__type__ = 'alongLine'
__calculation__ = Calculation.AlongLinePoint
__operation__ = SketchOperation.AlongLinePoint
####################################################################################################
......@@ -374,7 +415,7 @@ class BissectorPoint(PointLinePropertiesMixin, FirstSecondThirdPointMixin, Lengt
# length="Line_A_X" name="B" lineColor="deepskyblue" type="bisector" my="0.2"/>
__type__ = 'bisector'
# __calculation__ = Calculation.BissectorPoint
# __operation__ = SketchOperation.BissectorPoint
####################################################################################################
......@@ -399,7 +440,7 @@ class EndLinePoint(PointLinePropertiesMixin, BasePointMixin, LengthAngleMixin, X
# lineColor="blue" type="endLine" angle="360" my="0.25"/>
__type__ = 'endLine'
__calculation__ = Calculation.EndLinePoint
__operation__ = SketchOperation.EndLinePoint
####################################################################################################
......@@ -409,7 +450,7 @@ class HeightPoint(PointLinePropertiesMixin, BasePointMixin, Line1Mixin, XmlObjec
# lineColor="mediumseagreen" type="height" my="0.2"/>
__type__ = 'height'
# __calculation__ = Calculation.HeightPoint
# __operation__ = SketchOperation.HeightPoint
####################################################################################################
......@@ -419,7 +460,7 @@ class LineIntersectPoint(PointMixin, Line12Mixin, XmlObjectAdaptator):
# p2Line1="12" p2Line2="14"/>
__type__ = 'lineIntersect'
__calculation__ = Calculation.LineIntersectPoint
__operation__ = SketchOperation.LineIntersectPoint
####################################################################################################
......@@ -429,7 +470,7 @@ class LineIntersectAxisPoint(PointLinePropertiesMixin, BasePointMixin, Line1Mixi
# lineColor="goldenrod" type="lineIntersectAxis" angle="150" my="-1.8"/>
__type__ = 'lineIntersectAxis'
# __calculation__ = Calculation.LineIntersectAxisPoint
# __operation__ = SketchOperation.LineIntersectAxisPoint
####################################################################################################
......@@ -439,7 +480,7 @@ class NormalPoint(PointLinePropertiesMixin, FirstSecondPointMixin, LengthAngleMi
# name="Ct" lineColor="blue" type="normal" angle="0" my="0.1"/>
__type__ = 'normal'
__calculation__ = Calculation.NormalPoint
__operation__ = SketchOperation.NormalPoint
####################################################################################################
......@@ -459,7 +500,7 @@ class PointOfIntersection(PointMixin, FirstSecondPointMixin, XmlObjectAdaptator)
# <point id="14" firstPoint="2" mx="0.1" secondPoint="5" name="XY" type="pointOfIntersection" my="0.2"/>
__type__ = 'pointOfIntersection'
__calculation__ = Calculation.PointOfIntersection
__operation__ = SketchOperation.PointOfIntersection
####################################################################################################
......@@ -482,7 +523,7 @@ class ShoulderPoint(PointLinePropertiesMixin, Line1Mixin, LengthMixin, XmlObject
# p1Line="5" lineColor="lightsalmon" type="shoulder" my="-1.3"/>
__type__ = 'shoulder'
# __calculation__ = Calculation.ShoulderPoint
# __operation__ = SketchOperation.ShoulderPoint
__attributes__ = (
IntAttribute('shoulder_point', 'pShoulder'),
)
......@@ -494,7 +535,7 @@ class SinglePoint(PointMixin, XyMixin, XmlObjectAdaptator):
# <point id="1" mx="0.1" x="0.79375" y="1.05833" name="A" type="single" my="0.2"/>
__type__ = 'single'
__calculation__ = Calculation.SinglePoint
__operation__ = SketchOperation.SinglePoint
####################################################################################################
......@@ -544,18 +585,18 @@ class Line(CalculationMixin, LinePropertiesMixin, FirstSecondPointMixin, XmlObje
# <line id="47" firstPoint="38" typeLine="hair" secondPoint="45" lineColor="blue"/>
__tag__ = 'line'
__calculation__ = Calculation.Line
__operation__ = SketchOperation.Line
##############################################
def to_calculation(self, pattern):
return self.call_calculation_function(pattern, self.to_dict()) # exclude=('id')
def to_operation(self, sketch):
return self.call_operation_function(sketch, self.to_dict()) # exclude=('id')
##############################################
@classmethod
def from_calculation(cls, calculation):
kwargs = cls.get_dict(calculation)
def from_operation(cls, operation):
kwargs = cls.get_dict(operation)
return cls(**kwargs)
####################################################################################################
......@@ -580,18 +621,18 @@ class SimpleInteractiveSpline(SplineMixin, XmlObjectAdaptator):
StringAttribute('angle2'),
StringAttribute('line_color', 'color'),
)
__calculation__ = Calculation.SimpleInteractiveSpline
__operation__ = SketchOperation.SimpleInteractiveSpline
##############################################
def to_calculation(self, pattern):
return self.call_calculation_function(pattern, self.to_dict()) # exclude=('id')
def to_operation(self, sketch):
return self.call_operation_function(sketch, self.to_dict()) # exclude=('id')
##############################################
@classmethod
def from_calculation(cls, calculation):
kwargs = cls.get_dict(calculation)
def from_operation(cls, operation):
kwargs = cls.get_dict(operation)
return cls(**kwargs)
####################################################################################################
......
......@@ -18,99 +18,59 @@
#
####################################################################################################
"""Module to implement Bézier curve.
"""
# C0 = continuous
# G1 = geometric continuity
# Tangents point to the same direction
# C1 = parametric continuity
# Tangents are the same, implies G1
# C2 = curvature continuity
# Tangents and their derivatives are the same
####################################################################################################
from math import log, sqrt, pow
from math import log, sqrt
from Patro.Common.Math.Root import quadratic_root, cubic_root
import numpy as np
from Patro.Common.Math.Root import quadratic_root, cubic_root, fifth_root
from .Interpolation import interpolate_two_points
from .Line import Line2D
from .Primitive import Primitive3P, Primitive4P, Primitive2DMixin
from .Primitive import Primitive3P, Primitive4P, PrimitiveNP, Primitive2DMixin
from .Transformation import AffineTransformation
from .Vector import Vector2D
####################################################################################################
# Fixme: implement intersection
# Fixme:
# max distance to the chord for linear approximation
# fitting
####################################################################################################
class QuadraticBezier2D(Primitive2DMixin, Primitive3P):
class BezierMixin2D(Primitive2DMixin):
"""Class to implements 2D Quadratic Bezier Curve."""
"""Mixin to implements 2D Bezier Curve."""
LineInterpolationPrecision = 0.05
##############################################
def __init__(self, p0, p1, p2):
Primitive3P.__init__(self, p0, p1, p2)
##############################################
def __repr__(self):
return self.__class__.__name__ + '({0._p0}, {0._p1}, {0._p2})'.format(self)
##############################################
@property
def length(self):
# Algorithm:
#
# http://www.gamedev.net/topic/551455-length-of-a-generalized-quadratic-bezier-curve-in-3d
# Dave Eberly Posted October 25, 2009
#
# The quadratic Bezier is
# (x(t),y(t)) = (1-t)^2*(x0,y0) + 2*t*(1-t)*(x1,y1) + t^2*(x2,y2)
#
# The derivative is
# (x'(t),y'(t)) = -2*(1-t)*(x0,y0) + (2-4*t)*(x1,y1) + 2*t*(x2,y2)
#
# The length of the curve for 0 <= t <= 1 is
# Integral[0,1] sqrt((x'(t))^2 + (y'(t))^2) dt
# The integrand is of the form sqrt(c*t^2 + b*t + a)
#
# You have three separate cases: c = 0, c > 0, or c < 0.
# * The case c = 0 is easy.
# * For the case c > 0, an antiderivative is
# (2*c*t+b)*sqrt(c*t^2+b*t+a)/(4*c) + (0.5*k)*log(2*sqrt(c*(c*t^2+b*t+a)) + 2*c*t + b)/sqrt(c)
# where k = 4*c/q with q = 4*a*c - b*b.
# * For the case c < 0, an antiderivative is
# (2*c*t+b)*sqrt(c*t^2+b*t+a)/(4*c) - (0.5*k)*arcsin((2*c*t+b)/sqrt(-q))/sqrt(-c)
A0 = self._p1 - self._p0
A1 = self._p0 - self._p1 * 2 + self._p2
if A1.magnitude_square() != 0:
c = 4 * A1.dot(A1)
b = 8 * A0.dot(A1)
a = 4 * A0.dot(A0)
q = 4 * a * c - b * b
two_cb = 2 * c + b
sum_cba = c + b + a
m0 = 0.25 / c
m1 = q / (8 * c**1.5)
return (m0 * (two_cb * sqrt(sum_cba) - b * sqrt(a)) +
m1 * (log(2 * sqrt(c * sum_cba) + two_cb) - log(2 * sqrt(c * a) + b)))
else:
return 2 * A0.magnitude()
##############################################
def interpolated_length(self, dt=None):
# Length of the curve obtained via line interpolation
if dt is None:
dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude()
dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude
length = 0
t = 0
while t < 1:
t0 = t
t = min(t + dt, 1)
length += (self.point_at_t(t) - self.point_at_t(t0)).magnitude()
length += (self.point_at_t(t) - self.point_at_t(t0)).magnitude
return length
......@@ -170,6 +130,143 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P):
##############################################
def split_at_two_t(self, t1, t2):
if t1 == t2:
return self.point_at_t(t1)
if t2 < t1:
# Fixme: raise ?
t1, t2 = t2, t1
# curve = self
# if t1 > 0:
curve = self.split_at_t(t1)[1] # right
if t2 < 1:
# Interpolate the parameter at t2 in the new curve
t = (t2 - t1) / (1 - t1)
curve = curve.split_at_t(t)[0] # left
return curve
##############################################
def _map_to_line(self, line):
transformation = AffineTransformation.Rotation(-line.v.orientation)
# Fixme: use __vector_cls__
transformation *= AffineTransformation.Translation(Vector2D(0, -line.p.y))
# Fixme: better API ?
return self.clone().transform(transformation)
##############################################
def non_parametric_curve(self, line):
"""Return the non-parametric Bezier curve D(ti, di(t)) where di(t) is the distance of the curve from
the baseline of the fat-line, ti is equally spaced in [0, 1].
"""
ts = np.arange(0, 1, 1/(self.number_of_points-1))
distances = [line.distance_to_line(p) for p in self.points]
points = [Vector2D(t, d) for t, f in zip(ts, distances)]
return self.__class__(*points)
##############################################
def distance_to_point(self, point):
p = self.closest_point(point)
if p is not None:
return (point - p).magnitude
else:
return None
####################################################################################################
class QuadraticBezier2D(BezierMixin2D, Primitive3P):
"""Class to implements 2D Quadratic Bezier Curve."""
# Q(t) = Transformation * Control * Basis * T(t)
#
# / P1x P2x P3x \ / 1 -2 1 \ / 1 \
# Q(t) = Tr | P1y P2x P3x | | 0 2 -2 | | t |
# \ 1 1 1 / \ 0 0 1 / \ t**2 /
#
# Q(t) = P0 * (1 - 2*t + t**2) +
# P1 * ( 2*t - t**2) +
# P2 * t**2
BASIS = np.array((
(1, -2, 1),
(0, 2, -2),
(0, 0, 1),
))
INVERSE_BASIS = np.array((
(-2, 1, -2),
(-1, -3, 1),
(-1, -1, -2),
))
##############################################
def __init__(self, p0, p1, p2):
Primitive3P.__init__(self, p0, p1, p2)
##############################################
def __repr__(self):
return self.__class__.__name__ + '({0._p0}, {0._p1}, {0._p2})'.format(self)
##############################################
@property
def length(self):
# Algorithm:
#
# http://www.gamedev.net/topic/551455-length-of-a-generalized-quadratic-bezier-curve-in-3d
# Dave Eberly Posted October 25, 2009
#
# The quadratic Bezier is
# (x(t),y(t)) = (1-t)^2*(x0,y0) + 2*t*(1-t)*(x1,y1) + t^2*(x2,y2)
#
# The derivative is
# (x'(t),y'(t)) = -2*(1-t)*(x0,y0) + (2-4*t)*(x1,y1) + 2*t*(x2,y2)
#
# The length of the curve for 0 <= t <= 1 is
# Integral[0,1] sqrt((x'(t))^2 + (y'(t))^2) dt
# The integrand is of the form sqrt(c*t^2 + b*t + a)
#
# You have three separate cases: c = 0, c > 0, or c < 0.
# * The case c = 0 is easy.
# * For the case c > 0, an antiderivative is
# (2*c*t+b)*sqrt(c*t^2+b*t+a)/(4*c) + (0.5*k)*log(2*sqrt(c*(c*t^2+b*t+a)) + 2*c*t + b)/sqrt(c)
# where k = 4*c/q with q = 4*a*c - b*b.
# * For the case c < 0, an antiderivative is
# (2*c*t+b)*sqrt(c*t^2+b*t+a)/(4*c) - (0.5*k)*arcsin((2*c*t+b)/sqrt(-q))/sqrt(-c)
A0 = self._p1 - self._p0
A1 = self._p0 - self._p1 * 2 + self._p2
if A1.magnitude_square != 0:
c = 4 * A1.dot(A1)
b = 8 * A0.dot(A1)
a = 4 * A0.dot(A0)
q = 4 * a * c - b * b
two_cb = 2 * c + b
sum_cba = c + b + a
m0 = 0.25 / c
m1 = q / (8 * c**1.5)
return (m0 * (two_cb * sqrt(sum_cba) - b * sqrt(a)) +
m1 * (log(2 * sqrt(c * sum_cba) + two_cb) - log(2 * sqrt(c * a) + b)))
else:
return 2 * A0.magnitude
##############################################
def point_at_t(self, t):
# if 0 < t or 1 < t:
# raise ValueError()
......@@ -193,28 +290,7 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P):
# p = self.point_at_t(t)
return (QuadraticBezier2D(self._p0, p01, p), QuadraticBezier2D(p, p12, self._p2))
##############################################
def split_at_two_t(self, t1, t2):
if t1 == t2:
return self.point_at_t(t1)
if t2 < t1:
# Fixme: raise ?
t1, t2 = t2, t1
# curve = self
# if t1 > 0:
curve = self.split_at_t(t1)[1] # right
if t2 < 1:
# Interpolate the parameter at t2 in the new curve
t = (t2 - t1) / (1 - t1)
curve = curve.split_at_t(t)[0] # left
return curve
##############################################
##############################################
@property
def tangent0(self):
......@@ -246,16 +322,6 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P):
##############################################
def _map_to_line(self, line):
transformation = AffineTransformation.Rotation(-line.v.orientation)
# Fixme: use __vector_cls__
transformation *= AffineTransformation.Translation(Vector2D(0, -line.p.y))
# Fixme: better API ?
return self.clone().transform(transformation)
##############################################
def intersect_line(self, line):
"""Find the intersections of the curve with a line."""
......@@ -314,17 +380,64 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P):
##############################################
def non_parametric_curve(self, line):
def closest_point(self, point):
"""Return the non-parametric Bezier curve D(ti, di(t)) where di(t) is the distance of the curve from
the baseline of the fat-line, ti is equally spaced in [0, 1].
"""
# 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
ts = np.arange(0, 1, 1/(self.number_of_points-1))
distances = [line.distance_to_line(p) for p in self.points]
points = [Vector2D(t, d) for t, f in zip(ts, distances)]
return self.__class__(*points)
# Condition:
# (P - B(t)) . B'(t) = 0 where t in [0,1]
#
# P. B'(t) - B(t). B'(t) = 0
# A = P1 - P0
# B = P2 - P1 - A
# M = P0 - P
# Q(t) = P0*(1-t)**2 + P1*2*t*(1-t) + P2*t**2
# Q'(t) = -2*P0*(1 - t) + 2*P1*(1 - 2*t) + 2*P2*t
# = 2*(A + B*t)
# P0, P1, P2, P, t = symbols('P0 P1 P2 P t')
# Q = P0 * (1-t)**2 + P1 * 2*t*(1-t) + P2 * t**2
# Qp = simplify(Q.diff(t))
# collect(expand((P*Qp - Q*Qp)/-2), t)
# (P0**2 - 4*P0*P1 + 2*P0*P2 + 4*P1**2 - 4*P1*P2 + P2**2) * t**3
# (-3*P0**2 + 9*P0*P1 - 3*P0*P2 - 6*P1**2 + 3*P1*P2) * t**2
# (-P*P0 + 2*P*P1 - P*P2 + 3*P0**2 - 6*P0*P1 + P0*P2 + 2*P1**2) * t
# P*P0 - P*P1 - P0**2 + P0*P1
# factorisation
# (P0 - 2*P1 + P2)**2 * t**3
# 3*(P1 - P0)*(P0 - 2*P1 + P2) * t**2
# ...
# (P0 - P)*(P1 - P0)
# B**2 * t**3
# 3*A*B * t**2
# (2*A**2 + M*B) * t
# M*A
A = self._p1 - self._p0
B = self._p2 - self._p1 - A
M = self._p0 - point
roots = cubic_root(
B.magnitude_square,
3*A.dot(B),
2*A.magnitude_square + M.dot(B),
M.dot(A),
)
t = [root for root in roots if 0 <= root <= 1]
if not t:
return None
elif len(t) > 1:
raise NameError("Found more than on root")
else:
return self.point_at_t(t)
####################################################################################################
......@@ -333,16 +446,36 @@ _Div18Sqrt3 = 18 / _Sqrt3
_OneThird = 1 / 3
_Sqrt3Div36 = _Sqrt3 / 36
class CubicBezier2D(Primitive4P, QuadraticBezier2D):
class CubicBezier2D(BezierMixin2D, Primitive4P):
"""Class to implements 2D Cubic Bezier Curve."""
InterpolationPrecision = 0.001
# Q(t) = Transformation * Control * Basis * T(t)
#
# / P1x P2x P3x P4x \ / 1 -3 3 -1 \ / 1 \
# Q(t) = Tr | P1y P2x P3x P4x | | 0 3 -6 3 | | t |
# | 0 0 0 0 | | 0 0 3 -3 | | t**2 |
# \ 1 1 1 1 / \ 0 0 0 1 / \ t**3 /
BASIS = np.array((
(1, -3, 3, -1),
(0, 3, -6, 3),
(0, 0, 3, -3),
(0, 0, 0, 1),
))
INVERSE_BASIS = np.array((
(1, 1, 1, 1),
(0, 1/3, 2/3, 1),
(0, 0, 1/3, 1),
(0, 0, 0, 1),
))
#######################################
def __init__(self, p0, p1, p2, p3):
Primitive4P.__init__(self, p0, p1, p2, p3)
##############################################
......@@ -352,6 +485,14 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D):
##############################################
def to_spline(self):
from .Spline import CubicUniformSpline2D
basis = np.dot(self.BASIS, CubicUniformSpline2D.INVERSE_BASIS)
points = np.dot(self.geometry_matrix, basis).transpose()
return CubicUniformSpline2D(*points)
##############################################
@property
def length(self):
return self.adaptive_length_approximation()
......@@ -363,14 +504,16 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D):
# raise ValueError()
return (self._p0 +
(self._p1 - self._p0) * 3 * t +
(self._p2 - self._p1 * 2 + self._p0) * 3 * t**2 +
(self._p3 - self._p2 * 3 + self._p1 * 3 - self._p0) * t**3)
(self._p2 - self._p1*2 + self._p0) * 3 * t**2 +
(self._p3 - self._p2*3 + self._p1*3 - self._p0) * t**3)
# interpolate = point_at_t
##############################################
def _q_point(self):
"""Return the control point for mid-point quadratic approximation"""
return (self._p2 * 3 - self._p3 + self._p1 * 3 - self._p0) / 4
return (self._p2*3 - self._p3 + self._p1*3 - self._p0) / 4
##############################################
......@@ -399,7 +542,7 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D):
def _d01(self):
"""Return the distance between 0 and 1 quadratic aproximations"""
return (self._p3 - self._p2 * 3 + self._p1 * 3 - self._p0).magnitude() / 2
return (self._p3 - self._p2 * 3 + self._p1 * 3 - self._p0).magnitude / 2
##############################################
......@@ -786,3 +929,55 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D):
return (3 * ((y3 - y0) * (x1 + x2) - (x3 - x0) * (y1 + y2)
+ y1 * (x0 - x2) - x1 * (y0 - y2)
+ y3 * (x2 + x0 / 3) - x3 * (y2 + y0 / 3)) / 20)
##############################################
def closest_point(self, point):
# Q(t) = (P3 - 3*P2 + 3*P1 - P0) * t**3 +
# 3*(P2 - 2*P1 + P0) * t**2 +
# 3*(P1 - P0) * t +
# P0
# n = P3 - 3*P2 + 3*P1 - P0
# r = 3*(P2 - 2*P1 + P0
# s = 3*(P1 - P0)
# v = P0
# Q(t) = n*t**3 + r*t**2 + s*t + v
# Q'(t) = 3*n*t**2 + 2*r*t + s
# P0, P1, P2, P3, P, t = symbols('P0 P1 P2 P3 P t')
# n, r, s, v = symbols('n r s v')
# Q = n*t**3 + r*t**2 + s*t + v
# Qp = simplify(Q.diff(t))
# collect(expand((P*Qp - Q*Qp)), t)
# -3*n**2 * t**5
# -5*n*r * t**4
# -2*(2*n*s + r**2) * t**3
# 3*(P*n - n*v - r*s) * t**2
# (2*P*r - 2*r*v - s**2) * t
# P*s - s*v
n = self._p3 - self._p2*3 + self._p1*3 - self._p0
r = (self._p2 - self._p1*2 + self._p0)*3
s = (self._p1 - self._p0)*3
v = self._p0
roots = fifth_root(
-3 * n.magnitude_square,
-5 * n.dot(r),
-2 * (2*n.dot(s) + r.magnitude_square),
3 * (point.dot(n) - n.dot(v) - r.dot(s)),
2*point.dot(r) - 2*r.dot(v) - s.magnitude_square,
point.dot(s) - s.dot(v),
)
# Fixme: to func
t = [root for root in roots if 0 <= root <= 1]
if not t:
return None
elif len(t) > 1:
raise NameError("Found more than on root")
else:
return self.point_at_t(t[0])
......@@ -91,7 +91,8 @@ def convex_hull(points):
convex_hull = []
sorted_points = _sort_point_for_graham_scan(points)
for p in sorted_points:
# if we turn clockwise to reach this point, pop the last point from the stack, else, append this point to it.
# if we turn clockwise to reach this point,
# pop the last point from the stack, else, append this point to it.
while len(convex_hull) > 1 and _ccw(convex_hull[-1], convex_hull[-2], p) >= 0: # Fixme: check
convex_hull.pop()
convex_hull.append(p)
......
This diff is collapsed.
......@@ -58,13 +58,13 @@ class Line2D(Primitive2DMixin, Primitive):
def __str__(self):
text = '''Line
Point %s
Vector %s
magnitude %g
str_format = '''Line
Point {0.p}
Vector {0.v}
magnitude {1}
'''
return text % (str(self.p), str(self.v), self.v.magnitude())
return str_format.format(self, self.v.magnitude)
##############################################
......@@ -230,10 +230,12 @@ class Line2D(Primitive2DMixin, Primitive):
"""Return the distance of a point to the line"""
delta = point - self.p
d = delta.deviation_with(self.v)
s = delta.projection_on(self.v)
return (d, s) # distance to line, abscissa
if delta.magnitude_square == 0:
return 0, 0
else:
d = delta.deviation_with(self.v)
s = delta.projection_on(self.v)
return d, s # distance to line, abscissa
##############################################
......
####################################################################################################
#
# 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/>.
#
####################################################################################################
####################################################################################################
__all__ = [
'LinearSegment',
'QuadraticBezierSegment',
'CubicBezierSegment',
'Path2D',
]
####################################################################################################
import math
from .Primitive import Primitive1P, Primitive2DMixin
from .Bezier import QuadraticBezier2D, CubicBezier2D
from .Conic import Circle2D, AngularDomain
from .Segment import Segment2D
from .Vector import Vector2D
####################################################################################################
class PathPart:
##############################################
def __init__(self, path, position):
self._path = path
self._position = position
##############################################
def __repr__(self):
return self.__class__.__name__
##############################################
@property
def path(self):
return self._path
@property
def position(self):
return self._position
@position.setter
def position(self, value):
self._position = int(value)
##############################################
@property
def prev_part(self):
return self._path[self._position -1]
@property
def next_part(self):
return self._path[self._position +1]
##############################################
@property
def start_point(self):
prev_part = self.prev_part
if prev_part is not None:
return prev_part.stop_point
else:
return self._path.p0
##############################################
@property
def stop_point(self):
raise NotImplementedError
@property
def geometry(self):
raise NotImplementedError
##############################################
@property
def bounding_box(self):
return self.geometry.bounding_box
####################################################################################################
class LinearSegment(PathPart):
r"""
Bulge
Let `P0`, `P1`, `P2` the vertices and `R` the bulge radius.
The deflection :math:`\theta = 2 \alpha` at the corner is
.. math::
D_1 \cdot D_0 = (P_2 - P_1) \cdot (P_1 - P_0) = \cos \theta
The bisector direction is
.. math::
Bis = D_1 - D_0 = (P_2 - P_1) - (P_1 - P_0) = P_2 -2 P_1 + P_0
Bulge Center is
.. math::
C = P_1 + Bis \times \frac{R}{\sin \alpha}
Extremities are
\prime P_1 = P_1 - d_0 \times \frac{R}{\tan \alpha}
\prime P_1 = P_1 + d_1 \times \frac{R}{\tan \alpha}
"""
##############################################
def __init__(self, path, position, radius):
super().__init__(path, position)
self._bissector = None
self._direction = None
self.radius = radius
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
##############################################
@property
def points(self):
if self._radius is not None:
start_point = self.bulge_stop_point
else:
start_point = self.start_point
next_part = self.next_part
if isinstance(next_part, LinearSegment) and next_part.radius is not None:
stop_point = next_part.bulge_start_point
else:
stop_point = self.stop_point
return start_point, stop_point
##############################################
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value is not None:
self._radius = float(value)
else:
self._radius = None
##############################################
@property
def direction(self):
if self._direction is None:
self._direction = (self.stop_point - self.start_point).normalise()
return self._direction
@property
def bissector(self):
if self._radius is None:
return None
else:
if self._bissector is None:
# self._bissector = (self.prev_part.direction + self.direction).normalise().normal
self._bissector = (self.direction - self.prev_part.direction).normalise()
return self._bissector
##############################################
@property
def bulge_angle_rad(self):
if self._bulge_angle is None:
angle = self.direction.orientation_with(self.prev_part.direction)
self._bulge_angle = math.radians(angle)
return self._bulge_angle
@property
def bulge_angle(self):
return math.degrees(self.bulge_angle_rad)
@property
def half_bulge_angle(self):
return abs(self.bulge_angle_rad / 2)
##############################################
@property
def bulge_center(self):
if self._bulge_center is None:
offset = self.bissector * self._radius / math.sin(self.half_bulge_angle)
self._bulge_center = self.start_point + offset
return self._bulge_center
##############################################
@property
def bulge_start_point(self):
if self._start_bulge_point is None:
offset = self.prev_part.direction * self._radius / math.tan(self.half_bulge_angle)
self._start_bulge_point = self.start_point - offset
return self._start_bulge_point
@property
def bulge_stop_point(self):
if self._stop_bulge_point is None:
offset = self.direction * self._radius / math.tan(self.half_bulge_angle)
self._stop_bulge_point = self.start_point + offset
return self._stop_bulge_point
##############################################
@property
def bulge_geometry(self):
# Fixme: check start and stop are within segment
arc = Circle2D(self.bulge_center, self._radius)
start_angle, stop_angle = [arc.angle_for_point(point)
for point in (self.bulge_start_point, self.bulge_stop_point)]
if self.bulge_angle < 0:
start_angle, stop_angle = stop_angle, start_angle
arc.domain = AngularDomain(start_angle, stop_angle)
return arc
####################################################################################################
class PathSegment(LinearSegment):
##############################################
def __init__(self, path, position, point, radius=None, absolute=False):
super().__init__(path, position, radius)
self.point = point
self._absolute = bool(absolute)
##############################################
@property
def point(self):
return self._point
@point.setter
def point(self, value):
self._point = Vector2D(value) # self._path.__vector_cls__
##############################################
@property
def stop_point(self):
if self._absolute:
return self._point
else:
return self._point + self.start_point
##############################################
@property
def geometry(self):
# Fixme: cache ???
return Segment2D(*self.points)
####################################################################################################
class DirectionalSegment(LinearSegment):
__angle__ = None
##############################################
def __init__(self, path, position, length, radius=None):
super().__init__(path, position, radius)
self.length = length
##############################################
@property
def length(self):
return self._length
@length.setter
def length(self, value):
self._length = float(value)
##############################################
@property
def stop_point(self):
# Fixme: cache ???
return self.start_point + Vector2D.from_polar(self._length, self.__angle__)
##############################################
@property
def geometry(self):
# Fixme: cache ???
return Segment2D(self.start_point, self.stop_point)
####################################################################################################
class HorizontalSegment(DirectionalSegment):
__angle__ = 0
class VerticalSegment(DirectionalSegment):
__angle__ = 90
class NorthSegment(DirectionalSegment):
__angle__ = 90
class SouthSegment(DirectionalSegment):
__angle__ = -90
class EastSegment(DirectionalSegment):
__angle__ = 0
class WestSegment(DirectionalSegment):
__angle__ = 180
class NorthEastSegment(DirectionalSegment):
__angle__ = 45
class NorthWestSegment(DirectionalSegment):
__angle__ = 180 - 45
class SouthEastSegment(DirectionalSegment):
__angle__ = -45
class SouthWestSegment(DirectionalSegment):
__angle__ = -180 + 45
####################################################################################################
class TwoPointsMixin:
@property
def point1(self):
return self._point1
@point1.setter
def point1(self, value):
self._point1 = Vector2D(value) # self._path.__vector_cls__
@property
def point2(self):
return self._point2
@point2.setter
def point2(self, value):
self._point2 = Vector2D(value)
####################################################################################################
class QuadraticBezierSegment(PathPart, TwoPointsMixin):
# Fixme: abs / inc
##############################################
def __init__(self, path, position, point1, point2):
PathPart.__init__(self, path, position)
self.point1 = point1
self.point2 = point2
##############################################
@property
def stop_point(self):
return self._point2
@property
def points(self):
return (self.start_point, self._point1, self._point2)
##############################################
@property
def geometry(self):
# Fixme: cache ???
return QuadraticBezier2D(self.start_point, self._point1, self._point2)
####################################################################################################
class CubicBezierSegment(PathPart, TwoPointsMixin):
##############################################
def __init__(self, path, position, point1, point2, point3):
PathPart.__init__(self, path, position)
self.point1 = point1
self.point2 = point2
self.point3 = point3
##############################################
@property
def point3(self):
return self._point3
@point3.setter
def point3(self, value):
self._point3 = Vector2D(value) # self._path.__vector_cls__
##############################################
@property
def stop_point(self):
return self._point3
@property
def points(self):
return (self.start_point, self._point1, self._point2, self._point3)
##############################################
@property
def geometry(self):
# Fixme: cache ???
return CubicBezier2D(self.start_point, self._point1, self._point2, self._point3)
####################################################################################################
class Path2D(Primitive2DMixin, Primitive1P):
"""Class to implements 2D Path."""
##############################################
def __init__(self, start_point):
Primitive1P.__init__(self, start_point)
self._parts = []
##############################################
def __len__(self):
return len(self._parts)
def __iter__(self):
return iter(self._parts)
def __getitem__(self, position):
# try:
# return self._parts[slice_]
# except IndexError:
# return None
position = int(position)
if 0 <= position < len(self._parts):
return self._parts[position]
else:
return None
##############################################
def _add_part(self, part_cls, *args, **kwargs):
obj = part_cls(self, len(self._parts), *args, **kwargs)
self._parts.append(obj)
return obj
##############################################
def move_to(self, point):
self.p0 = point
##############################################
def horizontal_to(self, distance, radius=None):
return self._add_part(HorizontalSegment, distance, radius)
def vertical_to(self, distance, radius=None):
return self._add_part(VerticalSegment, distance, radius)
def north_to(self, distance, radius=None):
return self._add_part(NorthSegment, distance, radius)
def south_to(self, distance, radius=None):
return self._add_part(SouthSegment, distance, radius)
def west_to(self, distance, radius=None):
return self._add_part(WestSegment, distance, radius)
def east_to(self, distance, radius=None):
return self._add_part(EastSegment, distance, radius)
def north_east_to(self, distance, radius=None):
return self._add_part(NorthEastSegment, distance, radius)
def south_east_to(self, distance, radius=None):
return self._add_part(SouthEastSegment, distance, radius)
def north_west_to(self, distance, radius=None):
return self._add_part(NorthWestSegment, distance, radius)
def south_west_to(self, distance, radius=None):
return self._add_part(SouthWestSegment, distance, radius)
##############################################
def line_to(self, point, radius=None):
return self._add_part(PathSegment, point, radius)
def close(self, radius=None):
# Fixme: radius must apply to start and stop
return self._add_part(PathSegment, self._p0, radius, absolute=True)
##############################################
def quadratic_to(self, point1, point2):
return self._add_part(QuadraticBezierSegment, point1, point2)
##############################################
def cubic_to(self, point1, point2, point3):
return self._add_part(CubicBezierSegment, point1, point2, point3)