Skip to content
Commits on Source (70)
*.aux *.aux
*.qmlc
*.val.lock *.val.lock
*.vit.bak
*~ *~
.directory
__pycache__ __pycache__
du.log
MANIFEST MANIFEST
auto/
build/ build/
dist/ dist/
doc/sphinx/build doc/sphinx/build/
doc/sphinx/source/api/ doc/sphinx/source/api/
Valentina/Geometry/reference.txt Patro/QtApplication/rcc/PatroRessource.py
du.log Patro/QtApplication/rcc/patro.rcc
examples/output-old/
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/output/
examples/patterns/backup/ output/
examples/patterns/layout/
notes/ examples/dxf/protection-circulaire-seul-v1.dxf
old-todo.txt examples/dxf/protection-circulaire.dxf
todo.txt examples/dxf/protection-rectangulaire-v1.dxf
tools/upload-www examples/dxf/protection-rectangulaire-v2.dxf
trash/ examples/dxf/test-dxf-r15.pdf
tmp.py
Patro/GraphicEngine/TeX/__MERGE_MUSICA__ 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-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.pdf
examples/patterns/veravenus-little-bias-dress.pattern-a0.svg examples/patterns/veravenus-little-bias-dress.pattern-a0.svg
notes.txt notes.txt
notes/
open-doc.sh open-doc.sh
outdated.txt outdated.txt
ressources 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 @@ ...@@ -26,6 +26,8 @@
__all__ = [ __all__ = [
'quadratic_root', 'quadratic_root',
'cubic_root', 'cubic_root',
'fifth_root',
'fifth_root_normalised',
] ]
#################################################################################################### ####################################################################################################
...@@ -78,13 +80,28 @@ def cubic_root(a, b, c, d): ...@@ -78,13 +80,28 @@ def cubic_root(a, b, c, d):
def cubic_root_sympy(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 E = a*x**3 + b*x**2 + c*x + d
return [i.n() for i in sympy.real_roots(E, x)] 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): def cubic_root_normalised(a, b, c):
# Reference: ??? # 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: ...@@ -50,7 +50,7 @@ class XmlFileMixin:
############################################## ##############################################
def _parse(self): def parse(self):
"""Parse a XML file and return the etree""" """Parse a XML file and return the etree"""
with open(str(self._path), 'rb') as f: with open(str(self._path), 'rb') as f:
source = f.read() source = f.read()
...@@ -59,6 +59,24 @@ class XmlFileMixin: ...@@ -59,6 +59,24 @@ class XmlFileMixin:
############################################## ##############################################
@staticmethod @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""" """Utility function to get an element from a xpath and a root"""
return root.xpath(path)[0] 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 @@ ...@@ -23,9 +23,10 @@
import os import os
import sys 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() ...@@ -64,12 +65,12 @@ OS = OsFactory()
#################################################################################################### ####################################################################################################
_this_file = PathTools.to_absolute_path(__file__) _this_file = Path(__file__).resolve()
class Path: class Path:
musica_module_directory = PathTools.parent_directory_of(_this_file, step=2) patro_module_directory = _this_file.parents[1]
config_directory = os.path.dirname(_this_file) config_directory = _this_file.parent
#################################################################################################### ####################################################################################################
......
...@@ -21,7 +21,7 @@ handlers: ...@@ -21,7 +21,7 @@ handlers:
console: console:
class: logging.StreamHandler class: logging.StreamHandler
level: DEBUG level: INFO
# formatter: ansi # formatter: ansi
stream: ext://sys.stdout 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): ...@@ -67,7 +67,7 @@ class XmlMeasurement(XmlObjectAdaptator):
#################################################################################################### ####################################################################################################
class VitFile(XmlFileMixin): class VitFileInternal(XmlFileMixin):
_logger = _module_logger.getChild('VitFile') _logger = _module_logger.getChild('VitFile')
...@@ -76,43 +76,52 @@ class VitFile(XmlFileMixin): ...@@ -76,43 +76,52 @@ class VitFile(XmlFileMixin):
def __init__(self, path): def __init__(self, path):
XmlFileMixin.__init__(self, path) XmlFileMixin.__init__(self, path)
self._measurements = ValentinaMeasurements() self.measurements = ValentinaMeasurements()
self._read() self.read()
############################################## ##############################################
@property def read(self):
def measurements(self):
return self._measurements
##############################################
def _read(self):
self._logger.info('Load measurements from ' + str(self._path)) 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 version = self.get_xpath_element(tree, 'version').text
# self._read_only = self._get_xpath_element(tree, 'read-only').text # self.read_only = self.get_xpath_element(tree, 'read-only').text
# self._notes = self._get_xpath_element(tree, 'notes').text # self.notes = self.get_xpath_element(tree, 'notes').text
self.unit = self._get_xpath_element(tree, 'unit').text self.unit = self.get_xpath_element(tree, 'unit').text
self.pattern_making_system = self._get_xpath_element(tree, 'pm_system').text self.pattern_making_system = self.get_xpath_element(tree, 'pm_system').text
personal = measurements.personal personal = measurements.personal
personal_element = self._get_xpath_element(tree, 'personal') personal_element = self.get_xpath_element(tree, 'personal')
personal.last_name = self._get_xpath_element(personal_element, 'family-name').text 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.first_name = self.get_xpath_element(personal_element, 'given-name').text
personal.birth_date = self._get_xpath_element(personal_element, 'birth-date').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.gender = Gender[self.get_xpath_element(personal_element, 'gender').text.upper()]
personal.email = self._get_xpath_element(personal_element, 'email').text personal.email = self.get_xpath_element(personal_element, 'email').text
elements = self._get_xpath_element(tree, 'body-measurements') elements = self.get_xpath_element(tree, 'body-measurements')
for element in elements: for element in elements:
if element.tag == XmlMeasurement.__tag__: if element.tag == XmlMeasurement.__tag__:
xml_measurement = XmlMeasurement(element) xml_measurement = XmlMeasurement(element)
measurements.add(**xml_measurement.to_dict()) measurements.add(**xml_measurement.to_dict())
else: else:
raise NotImplementedError raise NotImplementedError
####################################################################################################
class VitFile:
##############################################
def __init__(self, path):
self._interval = VitFileInternal(path)
##############################################
@property
def measurements(self):
return self._interval.measurements
...@@ -18,7 +18,7 @@ ...@@ -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 ...@@ -31,7 +31,7 @@ from lxml import etree
from Patro.Common.Xml.XmlFile import XmlFileMixin from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.Pattern.Pattern import Pattern from Patro.Pattern.Pattern import Pattern
from .Measurements import VitFile from .Measurement import VitFile
from .VitFormat import ( from .VitFormat import (
Point, Point,
Line, Line,
...@@ -51,6 +51,32 @@ _module_logger = logging.getLogger(__name__) ...@@ -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: class Dispatcher:
"""Baseclass to dispatch XML to Python class.""" """Baseclass to dispatch XML to Python class."""
...@@ -61,15 +87,15 @@ class Dispatcher: ...@@ -61,15 +87,15 @@ class Dispatcher:
def from_xml(self, element): def from_xml(self, element):
tag_class = self.__TAGS__[element.tag] tag_cls = self.__TAGS__[element.tag]
if tag_class is not None: if tag_cls is not None:
return tag_class(element) return tag_cls(element)
else: else:
raise NotImplementedError raise NotImplementedError
#################################################################################################### ####################################################################################################
class CalculationDispatcher: class CalculationDispatcher(Dispatcher):
"""Class to implement a dispatcher for calculations.""" """Class to implement a dispatcher for calculations."""
...@@ -94,35 +120,35 @@ class CalculationDispatcher: ...@@ -94,35 +120,35 @@ class CalculationDispatcher:
############################################## ##############################################
def _register_mapping(self, xml_class): def _register_mapping(self, xml_cls):
calculation_class = xml_class.__calculation__ operation_cls = xml_cls.__operation__
if calculation_class: if operation_cls:
self._mapping[xml_class] = calculation_class self._mapping[xml_cls] = operation_cls
self._mapping[calculation_class] = xml_class self._mapping[operation_cls] = xml_cls
############################################## ##############################################
def _init_mapper(self): def _init_mapper(self):
for tag_class in self.__TAGS__.values(): for tag_cls in self.__TAGS__.values():
if tag_class is not None: if tag_cls is not None:
if hasattr(tag_class, '__TYPES__'): if hasattr(tag_cls, '__TYPES__'):
for xml_class in tag_class.__TYPES__.values(): for xml_cls in tag_cls.__TYPES__.values():
if xml_class is not None: if xml_cls is not None:
self._register_mapping(xml_class) self._register_mapping(xml_cls)
else: else:
self._register_mapping(tag_class) self._register_mapping(tag_cls)
############################################## ##############################################
def from_xml(self, element): def from_xml(self, element):
tag_class = self.__TAGS__[element.tag] tag_cls = self.__TAGS__[element.tag]
if hasattr(tag_class, '__TYPES__'): if hasattr(tag_cls, '__TYPES__'):
cls = tag_class.__TYPES__[element.attrib['type']] cls = tag_cls.__TYPES__[element.attrib['type']]
else: else:
cls = tag_class cls = tag_cls
if cls is not None: if cls is not None:
return cls(element) return cls(element)
else: else:
...@@ -130,34 +156,15 @@ class CalculationDispatcher: ...@@ -130,34 +156,15 @@ class CalculationDispatcher:
############################################## ##############################################
def from_calculation(self, calculation): def from_operation(self, operation):
return self._mapping[calculation.__class__].from_calculation(calculation) return self._mapping[operation.__class__].from_operation(operation)
####################################################################################################
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
#################################################################################################### ####################################################################################################
class ModelingDispatcher(Dispatcher): class ModelingDispatcher(Dispatcher):
"""Class to implement a dispatcher for modeling."""
__TAGS__ = { __TAGS__ = {
'point': ModelingPoint, 'point': ModelingPoint,
'spline': ModelingSpline, 'spline': ModelingSpline,
...@@ -167,6 +174,8 @@ class ModelingDispatcher(Dispatcher): ...@@ -167,6 +174,8 @@ class ModelingDispatcher(Dispatcher):
class DetailDispatcher(Dispatcher): class DetailDispatcher(Dispatcher):
"""Class to implement a dispatcher for detail."""
__TAGS__ = { __TAGS__ = {
'grainline': DetailGrainline, 'grainline': DetailGrainline,
'patternInfo': DetailPatternInfo, 'patternInfo': DetailPatternInfo,
...@@ -175,65 +184,62 @@ class DetailDispatcher(Dispatcher): ...@@ -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() """Class to read val file."""
_modeling_dispatcher = ModelingDispatcher()
_detail_dispatcher = DetailDispatcher() _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) 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): self.read()
# Fixme: write
self._vit_file = vit_file
self._pattern = pattern
self.write(path)
############################################## ##############################################
@property @property
def measurements(self): def measurements(self):
return self._vit_file.measurements if self.vit_file is not None:
return self.vit_file.measurements
@property else:
def pattern(self): return None
return self._pattern
############################################## ##############################################
def _read(self): def read(self):
# <?xml version='1.0' encoding='UTF-8'?> # <?xml version="1.0" encoding="UTF-8"?>
# <pattern> # <pattern>
# <!--Pattern created with Valentina (http://www.valentina-project.org/).--> # <!--Pattern created with Valentina v0.6.0.912b (https://valentinaproject.bitbucket.io/).-->
# <version>0.4.0</version> # <version>0.7.10</version>
# <unit>cm</unit> # <unit>cm</unit>
# <author/>
# <description/> # <description/>
# <notes/> # <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/> # <increments/>
# <draw name="Pattern piece 1"> # <previewCalculations/>
# <draw name="...">
# <calculation/> # <calculation/>
# <modeling/> # <modeling/>
# <details/> # <details/>
...@@ -241,86 +247,182 @@ class ValFile(XmlFileMixin): ...@@ -241,86 +247,182 @@ class ValFile(XmlFileMixin):
# </draw> # </draw>
# </pattern> # </pattern>
tree = self._parse() self._logger.info('Read Valentina file "{}"'.format(self.path))
measurements_path = Path(self._get_xpath_element(tree, 'measurements').text) self.root = self.parse()
if not measurements_path.exists(): self.read_attributes()
measurements_path = self._path.parent.joinpath(measurements_path) 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(): if not measurements_path.exists():
raise NameError("Cannot find {}".format(measurements_path)) 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: try:
xml_calculation = self._calculation_dispatcher.from_xml(element) xml_calculation = _calculation_dispatcher.from_xml(element)
xml_calculation.to_calculation(pattern) operation = xml_calculation.to_operation(sketch)
self._logger.info('Add operation {}'.format(operation))
except NotImplementedError: except NotImplementedError:
self._logger.warning('Not implemented calculation\n' + str(etree.tostring(element))) self._logger.warning('Not implemented calculation\n' + str(etree.tostring(element)))
sketch.eval()
pattern.eval()
modeling = Modeling() modeling = Modeling()
for element in self._get_xpath_element(tree, 'draw/modeling'): for element in self.get_xpath_element(piece, 'modeling'):
xml_modeling_item = self._modeling_dispatcher.from_xml(element) xml_modeling_item = _modeling_dispatcher.from_xml(element)
modeling.append(xml_modeling_item) modeling.add(xml_modeling_item)
# print(xml_modeling_item) self._logger.info('Modeling {}'.format(xml_modeling_item))
details = [] # details = []
for detail_element in self._get_xpath_element(tree, 'draw/details'): for detail_element in self.get_xpath_element(piece, 'details'):
xml_detail = Detail(modeling, detail_element) self.read_detail(scope, modeling, detail_element)
details.append(xml_detail) # 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))
############################################## ##############################################
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 = etree.Element('pattern')
root.append(etree.Comment('Pattern created with Patro (https://github.com/FabriceSalvaire/Patro)')) 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, 'unit').text = self._pattern.unit
etree.SubElement(root, 'author') etree.SubElement(root, 'author')
etree.SubElement(root, 'description') etree.SubElement(root, 'description')
etree.SubElement(root, 'notes') 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') etree.SubElement(root, 'increments')
draw_element = etree.SubElement(root, 'draw') # Fixme: for scope in self._pattern.scopes:
draw_element.attrib['name'] = 'Pattern piece 1' # Fixme: 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') for operation in scope.sketch.operations:
modeling_element = etree.SubElement(draw_element, 'modeling') xml_calculation = _calculation_dispatcher.from_operation(operation)
details_element = etree.SubElement(draw_element, 'details') # print(xml_calculation)
# group_element = etree.SubElement(draw_element, 'groups') # print(xml_calculation.to_xml_string())
calculation_element.append(xml_calculation.to_xml())
return root
##############################################
for calculation in self._pattern.calculations: def _write(self, root):
xml_calculation = self._calculation_dispatcher.from_calculation(calculation)
# print(xml_calculation)
# print(xml_calculation.to_xml_string())
calculation_element.append(xml_calculation.to_xml())
if path is None: with open(self._path, 'wb') as fh:
path = self.path
with open(str(path), 'wb') as f:
# ElementTree.write() ? # 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. ...@@ -24,6 +24,23 @@ and the calculation API.
The purpose of each XmlObjectAdaptator sub-classes is to serve as a bidirectional adaptor between The purpose of each XmlObjectAdaptator sub-classes is to serve as a bidirectional adaptor between
the XML format and the API. 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__ = [ ...@@ -43,14 +60,65 @@ __all__ = [
#################################################################################################### ####################################################################################################
import Patro.Pattern.Calculation as Calculation import Patro.Pattern.SketchOperation as SketchOperation
from Patro.Common.Xml.Objectivity import ( from Patro.Common.Xml.Objectivity import (
Attribute,
BoolAttribute, BoolAttribute,
IntAttribute, FloatAttribute, IntAttribute, FloatAttribute,
StringAttribute, StringAttribute,
XmlObjectAdaptator XmlObjectAdaptator
) )
from Patro.GeometryEngine.Vector import Vector2D 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: ...@@ -208,22 +276,24 @@ class CalculationMixin:
IntAttribute('id'), IntAttribute('id'),
) )
__calculation__ = None # calculation's class __operation__ = None # operation's class
############################################## ##############################################
def call_calculation_function(self, pattern, kwargs): def call_operation_function(self, sketch, kwargs):
return getattr(pattern, self.__calculation__.__name__)(**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 raise NotImplementedError
############################################## ##############################################
@classmethod @classmethod
def from_calculation(calculation): def from_operation(operation):
raise NotImplementedError raise NotImplementedError
#################################################################################################### ####################################################################################################
...@@ -240,37 +310,8 @@ class CalculationTypeMixin(CalculationMixin): ...@@ -240,37 +310,8 @@ class CalculationTypeMixin(CalculationMixin):
class LinePropertiesMixin: class LinePropertiesMixin:
__attributes__ = ( __attributes__ = (
StringAttribute('line_color', 'lineColor'), ColorAttribute('line_color', 'lineColor'),
StringAttribute('line_style', 'typeLine'), StrokeStyleAttribute('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',
) )
class XyMixin: class XyMixin:
...@@ -334,19 +375,19 @@ class PointMixin(CalculationTypeMixin, MxMyMixin): ...@@ -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 = self.to_dict(exclude=('mx', 'my')) # id'
kwargs['label_offset'] = Vector2D(self.mx, self.my) kwargs['label_offset'] = Vector2D(self.mx, self.my)
return self.call_calculation_function(pattern, kwargs) return self.call_operation_function(sketch, kwargs)
############################################## ##############################################
@classmethod @classmethod
def from_calculation(cls, calculation): def from_operation(cls, operation):
kwargs = cls.get_dict(calculation, exclude=('mx', 'my')) kwargs = cls.get_dict(operation, exclude=('mx', 'my'))
label_offset = calculation.label_offset label_offset = operation.label_offset
kwargs['mx'] = label_offset.x kwargs['mx'] = label_offset.x
kwargs['my'] = label_offset.y kwargs['my'] = label_offset.y
return cls(**kwargs) return cls(**kwargs)
...@@ -364,7 +405,7 @@ class AlongLinePoint(PointLinePropertiesMixin, FirstSecondPointMixin, LengthMixi ...@@ -364,7 +405,7 @@ class AlongLinePoint(PointLinePropertiesMixin, FirstSecondPointMixin, LengthMixi
# length="-Line_Bt_Ct" name="Dt" lineColor="black" type="alongLine" my="0.2"/> # length="-Line_Bt_Ct" name="Dt" lineColor="black" type="alongLine" my="0.2"/>
__type__ = 'alongLine' __type__ = 'alongLine'
__calculation__ = Calculation.AlongLinePoint __operation__ = SketchOperation.AlongLinePoint
#################################################################################################### ####################################################################################################
...@@ -374,7 +415,7 @@ class BissectorPoint(PointLinePropertiesMixin, FirstSecondThirdPointMixin, Lengt ...@@ -374,7 +415,7 @@ class BissectorPoint(PointLinePropertiesMixin, FirstSecondThirdPointMixin, Lengt
# length="Line_A_X" name="B" lineColor="deepskyblue" type="bisector" my="0.2"/> # length="Line_A_X" name="B" lineColor="deepskyblue" type="bisector" my="0.2"/>
__type__ = 'bisector' __type__ = 'bisector'
# __calculation__ = Calculation.BissectorPoint # __operation__ = SketchOperation.BissectorPoint
#################################################################################################### ####################################################################################################
...@@ -399,7 +440,7 @@ class EndLinePoint(PointLinePropertiesMixin, BasePointMixin, LengthAngleMixin, X ...@@ -399,7 +440,7 @@ class EndLinePoint(PointLinePropertiesMixin, BasePointMixin, LengthAngleMixin, X
# lineColor="blue" type="endLine" angle="360" my="0.25"/> # lineColor="blue" type="endLine" angle="360" my="0.25"/>
__type__ = 'endLine' __type__ = 'endLine'
__calculation__ = Calculation.EndLinePoint __operation__ = SketchOperation.EndLinePoint
#################################################################################################### ####################################################################################################
...@@ -409,7 +450,7 @@ class HeightPoint(PointLinePropertiesMixin, BasePointMixin, Line1Mixin, XmlObjec ...@@ -409,7 +450,7 @@ class HeightPoint(PointLinePropertiesMixin, BasePointMixin, Line1Mixin, XmlObjec
# lineColor="mediumseagreen" type="height" my="0.2"/> # lineColor="mediumseagreen" type="height" my="0.2"/>
__type__ = 'height' __type__ = 'height'
# __calculation__ = Calculation.HeightPoint # __operation__ = SketchOperation.HeightPoint
#################################################################################################### ####################################################################################################
...@@ -419,7 +460,7 @@ class LineIntersectPoint(PointMixin, Line12Mixin, XmlObjectAdaptator): ...@@ -419,7 +460,7 @@ class LineIntersectPoint(PointMixin, Line12Mixin, XmlObjectAdaptator):
# p2Line1="12" p2Line2="14"/> # p2Line1="12" p2Line2="14"/>
__type__ = 'lineIntersect' __type__ = 'lineIntersect'
__calculation__ = Calculation.LineIntersectPoint __operation__ = SketchOperation.LineIntersectPoint
#################################################################################################### ####################################################################################################
...@@ -429,7 +470,7 @@ class LineIntersectAxisPoint(PointLinePropertiesMixin, BasePointMixin, Line1Mixi ...@@ -429,7 +470,7 @@ class LineIntersectAxisPoint(PointLinePropertiesMixin, BasePointMixin, Line1Mixi
# lineColor="goldenrod" type="lineIntersectAxis" angle="150" my="-1.8"/> # lineColor="goldenrod" type="lineIntersectAxis" angle="150" my="-1.8"/>
__type__ = 'lineIntersectAxis' __type__ = 'lineIntersectAxis'
# __calculation__ = Calculation.LineIntersectAxisPoint # __operation__ = SketchOperation.LineIntersectAxisPoint
#################################################################################################### ####################################################################################################
...@@ -439,7 +480,7 @@ class NormalPoint(PointLinePropertiesMixin, FirstSecondPointMixin, LengthAngleMi ...@@ -439,7 +480,7 @@ class NormalPoint(PointLinePropertiesMixin, FirstSecondPointMixin, LengthAngleMi
# name="Ct" lineColor="blue" type="normal" angle="0" my="0.1"/> # name="Ct" lineColor="blue" type="normal" angle="0" my="0.1"/>
__type__ = 'normal' __type__ = 'normal'
__calculation__ = Calculation.NormalPoint __operation__ = SketchOperation.NormalPoint
#################################################################################################### ####################################################################################################
...@@ -459,7 +500,7 @@ class PointOfIntersection(PointMixin, FirstSecondPointMixin, XmlObjectAdaptator) ...@@ -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"/> # <point id="14" firstPoint="2" mx="0.1" secondPoint="5" name="XY" type="pointOfIntersection" my="0.2"/>
__type__ = 'pointOfIntersection' __type__ = 'pointOfIntersection'
__calculation__ = Calculation.PointOfIntersection __operation__ = SketchOperation.PointOfIntersection
#################################################################################################### ####################################################################################################
...@@ -482,7 +523,7 @@ class ShoulderPoint(PointLinePropertiesMixin, Line1Mixin, LengthMixin, XmlObject ...@@ -482,7 +523,7 @@ class ShoulderPoint(PointLinePropertiesMixin, Line1Mixin, LengthMixin, XmlObject
# p1Line="5" lineColor="lightsalmon" type="shoulder" my="-1.3"/> # p1Line="5" lineColor="lightsalmon" type="shoulder" my="-1.3"/>
__type__ = 'shoulder' __type__ = 'shoulder'
# __calculation__ = Calculation.ShoulderPoint # __operation__ = SketchOperation.ShoulderPoint
__attributes__ = ( __attributes__ = (
IntAttribute('shoulder_point', 'pShoulder'), IntAttribute('shoulder_point', 'pShoulder'),
) )
...@@ -494,7 +535,7 @@ class SinglePoint(PointMixin, XyMixin, XmlObjectAdaptator): ...@@ -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"/> # <point id="1" mx="0.1" x="0.79375" y="1.05833" name="A" type="single" my="0.2"/>
__type__ = 'single' __type__ = 'single'
__calculation__ = Calculation.SinglePoint __operation__ = SketchOperation.SinglePoint
#################################################################################################### ####################################################################################################
...@@ -544,18 +585,18 @@ class Line(CalculationMixin, LinePropertiesMixin, FirstSecondPointMixin, XmlObje ...@@ -544,18 +585,18 @@ class Line(CalculationMixin, LinePropertiesMixin, FirstSecondPointMixin, XmlObje
# <line id="47" firstPoint="38" typeLine="hair" secondPoint="45" lineColor="blue"/> # <line id="47" firstPoint="38" typeLine="hair" secondPoint="45" lineColor="blue"/>
__tag__ = 'line' __tag__ = 'line'
__calculation__ = Calculation.Line __operation__ = SketchOperation.Line
############################################## ##############################################
def to_calculation(self, pattern): def to_operation(self, sketch):
return self.call_calculation_function(pattern, self.to_dict()) # exclude=('id') return self.call_operation_function(sketch, self.to_dict()) # exclude=('id')
############################################## ##############################################
@classmethod @classmethod
def from_calculation(cls, calculation): def from_operation(cls, operation):
kwargs = cls.get_dict(calculation) kwargs = cls.get_dict(operation)
return cls(**kwargs) return cls(**kwargs)
#################################################################################################### ####################################################################################################
...@@ -580,18 +621,18 @@ class SimpleInteractiveSpline(SplineMixin, XmlObjectAdaptator): ...@@ -580,18 +621,18 @@ class SimpleInteractiveSpline(SplineMixin, XmlObjectAdaptator):
StringAttribute('angle2'), StringAttribute('angle2'),
StringAttribute('line_color', 'color'), StringAttribute('line_color', 'color'),
) )
__calculation__ = Calculation.SimpleInteractiveSpline __operation__ = SketchOperation.SimpleInteractiveSpline
############################################## ##############################################
def to_calculation(self, pattern): def to_operation(self, sketch):
return self.call_calculation_function(pattern, self.to_dict()) # exclude=('id') return self.call_operation_function(sketch, self.to_dict()) # exclude=('id')
############################################## ##############################################
@classmethod @classmethod
def from_calculation(cls, calculation): def from_operation(cls, operation):
kwargs = cls.get_dict(calculation) kwargs = cls.get_dict(operation)
return cls(**kwargs) return cls(**kwargs)
#################################################################################################### ####################################################################################################
......
...@@ -18,99 +18,59 @@ ...@@ -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 .Interpolation import interpolate_two_points
from .Line import Line2D from .Line import Line2D
from .Primitive import Primitive3P, Primitive4P, Primitive2DMixin from .Primitive import Primitive3P, Primitive4P, PrimitiveNP, Primitive2DMixin
from .Transformation import AffineTransformation from .Transformation import AffineTransformation
from .Vector import Vector2D 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 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): def interpolated_length(self, dt=None):
# Length of the curve obtained via line interpolation # Length of the curve obtained via line interpolation
if dt is None: if dt is None:
dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude() dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude
length = 0 length = 0
t = 0 t = 0
while t < 1: while t < 1:
t0 = t t0 = t
t = min(t + dt, 1) 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 return length
...@@ -170,6 +130,143 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P): ...@@ -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): def point_at_t(self, t):
# if 0 < t or 1 < t: # if 0 < t or 1 < t:
# raise ValueError() # raise ValueError()
...@@ -193,28 +290,7 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P): ...@@ -193,28 +290,7 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P):
# p = self.point_at_t(t) # p = self.point_at_t(t)
return (QuadraticBezier2D(self._p0, p01, p), QuadraticBezier2D(p, p12, self._p2)) 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 @property
def tangent0(self): def tangent0(self):
...@@ -246,16 +322,6 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P): ...@@ -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): def intersect_line(self, line):
"""Find the intersections of the curve with a line.""" """Find the intersections of the curve with a line."""
...@@ -314,17 +380,64 @@ class QuadraticBezier2D(Primitive2DMixin, Primitive3P): ...@@ -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 # Reference:
the baseline of the fat-line, ti is equally spaced in [0, 1]. # 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)) # Condition:
distances = [line.distance_to_line(p) for p in self.points] # (P - B(t)) . B'(t) = 0 where t in [0,1]
points = [Vector2D(t, d) for t, f in zip(ts, distances)] #
return self.__class__(*points) # 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 ...@@ -333,16 +446,36 @@ _Div18Sqrt3 = 18 / _Sqrt3
_OneThird = 1 / 3 _OneThird = 1 / 3
_Sqrt3Div36 = _Sqrt3 / 36 _Sqrt3Div36 = _Sqrt3 / 36
class CubicBezier2D(Primitive4P, QuadraticBezier2D): class CubicBezier2D(BezierMixin2D, Primitive4P):
"""Class to implements 2D Cubic Bezier Curve.""" """Class to implements 2D Cubic Bezier Curve."""
InterpolationPrecision = 0.001 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): def __init__(self, p0, p1, p2, p3):
Primitive4P.__init__(self, p0, p1, p2, p3) Primitive4P.__init__(self, p0, p1, p2, p3)
############################################## ##############################################
...@@ -352,6 +485,14 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D): ...@@ -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 @property
def length(self): def length(self):
return self.adaptive_length_approximation() return self.adaptive_length_approximation()
...@@ -363,14 +504,16 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D): ...@@ -363,14 +504,16 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D):
# raise ValueError() # raise ValueError()
return (self._p0 + return (self._p0 +
(self._p1 - self._p0) * 3 * t + (self._p1 - self._p0) * 3 * t +
(self._p2 - self._p1 * 2 + self._p0) * 3 * t**2 + (self._p2 - self._p1*2 + self._p0) * 3 * t**2 +
(self._p3 - self._p2 * 3 + self._p1 * 3 - self._p0) * t**3) (self._p3 - self._p2*3 + self._p1*3 - self._p0) * t**3)
# interpolate = point_at_t
############################################## ##############################################
def _q_point(self): def _q_point(self):
"""Return the control point for mid-point quadratic approximation""" """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): ...@@ -399,7 +542,7 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D):
def _d01(self): def _d01(self):
"""Return the distance between 0 and 1 quadratic aproximations""" """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): ...@@ -786,3 +929,55 @@ class CubicBezier2D(Primitive4P, QuadraticBezier2D):
return (3 * ((y3 - y0) * (x1 + x2) - (x3 - x0) * (y1 + y2) return (3 * ((y3 - y0) * (x1 + x2) - (x3 - x0) * (y1 + y2)
+ y1 * (x0 - x2) - x1 * (y0 - y2) + y1 * (x0 - x2) - x1 * (y0 - y2)
+ y3 * (x2 + x0 / 3) - x3 * (y2 + y0 / 3)) / 20) + 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): ...@@ -91,7 +91,8 @@ def convex_hull(points):
convex_hull = [] convex_hull = []
sorted_points = _sort_point_for_graham_scan(points) sorted_points = _sort_point_for_graham_scan(points)
for p in sorted_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 while len(convex_hull) > 1 and _ccw(convex_hull[-1], convex_hull[-2], p) >= 0: # Fixme: check
convex_hull.pop() convex_hull.pop()
convex_hull.append(p) convex_hull.append(p)
......
This diff is collapsed.
...@@ -58,13 +58,13 @@ class Line2D(Primitive2DMixin, Primitive): ...@@ -58,13 +58,13 @@ class Line2D(Primitive2DMixin, Primitive):
def __str__(self): def __str__(self):
text = '''Line str_format = '''Line
Point %s Point {0.p}
Vector %s Vector {0.v}
magnitude %g 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): ...@@ -230,10 +230,12 @@ class Line2D(Primitive2DMixin, Primitive):
"""Return the distance of a point to the line""" """Return the distance of a point to the line"""
delta = point - self.p delta = point - self.p
d = delta.deviation_with(self.v) if delta.magnitude_square == 0:
s = delta.projection_on(self.v) return 0, 0
else:
return (d, s) # distance to line, abscissa 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)