pax_global_header 0000666 0000000 0000000 00000000064 13424433274 0014520 g ustar 00root root 0000000 0000000 52 comment=ef584c10c77e83440926c0eaab133d99a3ff3352
Patro-master-Patro/ 0000775 0000000 0000000 00000000000 13424433274 0014525 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/ 0000775 0000000 0000000 00000000000 13424433274 0015612 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Common/ 0000775 0000000 0000000 00000000000 13424433274 0017042 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Common/ArgparseAction.py 0000664 0000000 0000000 00000003465 13424433274 0022326 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement argparse actions.
"""
####################################################################################################
__all__ = [
'PathAction',
]
####################################################################################################
import argparse
from pathlib import Path
####################################################################################################
class PathAction(argparse.Action):
"""Class to implement argparse action for path."""
##############################################
def __call__(self, parser, namespace, values, option_string=None):
if values is not None:
if isinstance(values, list):
path = [Path(x) for x in values]
else:
path = Path(values)
else:
path = None
setattr(namespace, self.dest, path)
Patro-master-Patro/Patro/Common/AtomicCounter.py 0000664 0000000 0000000 00000004150 13424433274 0022170 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
__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
Patro-master-Patro/Patro/Common/Datetime.py 0000664 0000000 0000000 00000002772 13424433274 0021160 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
from datetime import datetime, date
####################################################################################################
def ensure_date(x):
if isinstance(x, date):
return x
else:
# "1970-01-01"
return datetime.strptime(str(x), '%Y-%m-%d').date()
####################################################################################################
def ensure_datetime(x):
if isinstance(x, datetime):
return x
else:
# "1970-01-01T12:00:00.123Z"
return datetime.strptime(str(x), '%Y-%m-%dT%H:%M:%S.%fZ')
Patro-master-Patro/Patro/Common/Graph/ 0000775 0000000 0000000 00000000000 13424433274 0020103 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Common/Graph/DirectedAcyclicGraph.py 0000664 0000000 0000000 00000011756 13424433274 0024464 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This module implements a directed acyclic graph.
"""
####################################################################################################
# import logging
####################################################################################################
class DirectedAcyclicGraphNode:
"""Class to define a node of a DAG."""
##############################################
def __init__(self, node_id, data=None):
self._node_id = node_id
self._data = data
self._ancestors = set()
self._descendants = set()
##############################################
def __repr__(self):
return '{} {}'.format(self.__class__.__name__, self._node_id)
##############################################
@property
def node_id(self):
return self._node_id
@property
def data(self):
return self._data
@property
def ancestor(self):
return self._ancestor
@property
def descendants(self):
return self._descendants
##############################################
@property
def is_root(self):
return not self._ancestors
##############################################
@property
def is_leaf(self):
return not self._descendants
##############################################
def disconnect_ancestor(self, node):
self._ancestors.remove(node)
node.descendants.remove(self)
##############################################
def connect_ancestor(self, node):
self._ancestors.add(node)
node._descendants.add(self)
##############################################
def breadth_first_search(self):
# Fixme: Name ?
queue = [self]
visited = set((self,))
while queue:
node = queue.pop(0)
yield node
for descendant in node._descendants:
if descendant not in visited:
queue.append(descendant)
visited.add(descendant)
####################################################################################################
class DirectedAcyclicGraph:
"""Class to implement a DAG."""
##############################################
def __init__(self):
self._nodes = {}
##############################################
def __iter__(self):
return iter(self._nodes.values())
##############################################
def __getitem__(self, node_id):
return self._nodes[node_id]
##############################################
def add_node(self, node_id, **kwargs):
if node_id not in self._nodes:
node = DirectedAcyclicGraphNode(node_id, **kwargs)
self._nodes[node_id] = node
return node
else:
raise NameError("Node {} is already registered".format(node_id))
##############################################
def add_edge(self, ancestor, descendant):
descendant.connect_ancestor(ancestor)
##############################################
def roots(self):
return [node for node in self if node.is_root]
##############################################
def leafs(self):
return [node for node in self if node.is_leaf]
##############################################
def topological_sort(self):
sorted_list = [] # reversed
unmarked_nodes = set(self._nodes.values())
marked_nodes = set()
temporary_marked_nodes = set()
def visit(node):
if node in temporary_marked_nodes:
raise NameError('Not a DAG')
if node not in marked_nodes:
temporary_marked_nodes.add(node)
for descendant in node._descendants:
visit(descendant)
marked_nodes.add(node)
temporary_marked_nodes.remove(node)
sorted_list.append(node)
while unmarked_nodes:
node = unmarked_nodes.pop()
visit(node)
sorted_list.reverse()
return sorted_list
Patro-master-Patro/Patro/Common/Graph/__init__.py 0000664 0000000 0000000 00000001745 13424433274 0022223 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements graph algorithms.
"""
Patro-master-Patro/Patro/Common/IterTools.py 0000664 0000000 0000000 00000013441 13424433274 0021343 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
from itertools import tee
####################################################################################################
def pairwise(iterable):
"""Return a generator which generate a pair wise list from an iterable. s -> (s[0],s[1]),
(s[1],s[2]), ... (s[N-1],s[N])
"""
prev = iterable[0]
for x in iterable[1:]:
yield prev, x
prev = x
####################################################################################################
def multiwise(iterable, n=2):
"""Return a generator which generate a multi wise list from an iterable. s ->
(s[0],s[1],s[2],...), (s[1],s[2],s[3],...), ... (...,s[N-2],s[N-1],s[N])
Examples::
a = (1,2,3,4,5)
list(multiwise(a, n=1))
# [(1,), (2,), (3,), (4,), (5,)]
list(multiwise(a, n=2))
# [(1, 2), (2, 3), (3, 4), (4, 5)]
list(multiwise(a, n=3))
# [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
# list(multiwise(a, n=4))
# [(1, 2, 3, 4), (2, 3, 4, 5)]
list(multiwise(a, n=5))
# [(1, 2, 3, 4, 5)]
list(multiwise(a, n=6))
# []
list(multiwise(a, n=0))
# []
"""
iterators = tee(iterable, n) # return n iterators on iterable
# increment the iterators according to their positions
for i, iterator in enumerate(iterators):
for j in range(i):
next(iterator, None)
# return the aggregate
return zip(*iterators)
####################################################################################################
def multiwise_interval(iterable_size, n=2):
if n:
upper_index = iterable_size -1
offset = n -1
upper_start_index = upper_index -offset
if upper_start_index:
return [slice(start_index, start_index +n) for start_index in range(upper_start_index +1)]
elif upper_start_index == 0:
return [slice(0, iterable_size),]
return ()
####################################################################################################
def closed_pairwise(iterable):
"""Return a generator which generate a closed pair wise list from an iterable. s -> (s[0],s[1]),
(s[1],s[2]), ... (s[N], s[0])
"""
closed_iterable = list(iterable) + [iterable[0]]
# Fixme: duplicated code
prev = closed_iterable[0]
for x in closed_iterable[1:]:
yield prev, x
prev = x
####################################################################################################
class PairWiseManipulator(object):
"""This class is a template to manipulate an iterable with a pair wise iterator concept.
The method :meth:`do` must be implemented in super-class.
"""
##############################################
def __init__(self):
self._iterable = None
##############################################
def _index_max(self):
"""Return the index max of the list."""
return len(self._iterable) -1
##############################################
def next(self):
"""Increment the index position."""
self._index += 1
##############################################
def end(self):
"""Test if the index position is at the end of the list."""
return self._index == self._index_max()
##############################################
def pair(self):
"""Return the pair from the current index position."""
return self._iterable[self._index:self._index+2]
##############################################
def del_item(self):
"""Delete the item at the current index position."""
del self._iterable[self._index]
##############################################
def del_next_item(self):
"""Delete the item at the next index position."""
del self._iterable[self._index +1]
##############################################
def apply(self, iterable):
"""Iterate over the iterable and call the method :meth:`do` at each iteration until the last
position is reached. The index position is incremented if the method return :obj:`True`.
"""
self._iterable = iterable
self._index = 0
while True:
if self.do():
self.next()
if self.end():
break
##############################################
def do(self):
"""Method called by method :meth:`apply` to manipulate the list. Must return a boolean."""
raise NotImplementedError
####################################################################################################
def accumulate(iterable):
"""Accumulate the values of an iterable to a new array."""
accumulator = 0
array = []
for x in iterable:
accumulator += x
array.append(accumulator)
return array
Patro-master-Patro/Patro/Common/Logging/ 0000775 0000000 0000000 00000000000 13424433274 0020430 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Common/Logging/Logging.py 0000664 0000000 0000000 00000004415 13424433274 0022374 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import logging
import logging.config
import os
import yaml
####################################################################################################
import Patro.Config.ConfigInstall as ConfigInstall
####################################################################################################
def setup_logging(application_name='Patro',
config_file=ConfigInstall.Logging.default_config_file):
logging_config_file_name = ConfigInstall.Logging.find(config_file)
logging_config = yaml.load(open(logging_config_file_name, 'r'))
if ConfigInstall.OS.on_linux:
# Fixme: \033 is not interpreted in YAML
formatter_config = logging_config['formatters']['ansi']['format']
logging_config['formatters']['ansi']['format'] = formatter_config.replace('', '\033')
if ConfigInstall.OS.on_windows:
formatter = 'simple'
else:
formatter = 'ansi'
logging_config['handlers']['console']['formatter'] = formatter
logging.config.dictConfig(logging_config)
logger = logging.getLogger(application_name)
if 'PatroLogLevel' in os.environ:
numeric_level = getattr(logging, os.environ['PatroLogLevel'], None)
logger.setLevel(numeric_level)
return logger
Patro-master-Patro/Patro/Common/Logging/__init__.py 0000664 0000000 0000000 00000001747 13424433274 0022552 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements logging facilities.
"""
Patro-master-Patro/Patro/Common/Math/ 0000775 0000000 0000000 00000000000 13424433274 0017733 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Common/Math/Functions.py 0000664 0000000 0000000 00000004606 13424433274 0022263 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import math
####################################################################################################
def rint(f):
return int(round(f))
####################################################################################################
def ceil_int(f):
return int(math.ceil(f))
####################################################################################################
def middle(a, b):
return .5*(a + b)
####################################################################################################
def cmp(a, b):
return (a > b) - (a < b)
####################################################################################################
def sign(x):
# Fixme: sign_of ?
# return cmp(x, 0)
return math.copysign(1.0, x)
####################################################################################################
def epsilon_float(a, b, epsilon = 1e-3):
return abs(a-b) <= epsilon
####################################################################################################
def trignometric_clamp(x):
"""Clamp *x* in the range [-1.,1]."""
if x > 1.:
return 1.
elif x < -1.:
return -1.
else:
return x
####################################################################################################
def is_in_trignometric_range(x):
return -1. <= x <= 1
Patro-master-Patro/Patro/Common/Math/Root.py 0000664 0000000 0000000 00000013567 13424433274 0021244 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This module implements root finding for second and third degree equation.
"""
####################################################################################################
__all__ = [
'quadratic_root',
'cubic_root',
'fifth_root',
'fifth_root_normalised',
]
####################################################################################################
from math import acos, cos, pi, sqrt
try:
import sympy
except ImportError:
sympy = None
from .Functions import sign
####################################################################################################
def quadratic_root(a, b, c):
# https://en.wikipedia.org/wiki/Quadratic_equation
if a == 0 and b == 0:
return None
if a == 0:
return - c / b
D = b**2 - 4*a*c
if D < 0:
return None # not real
b = -b
s = 1 / (2*a)
if D > 0:
# Fixme: sign of b ???
r1 = (b - sqrt(D)) * s
r2 = (b + sqrt(D)) * s
return r1, r2
else:
return b * s
####################################################################################################
def cubic_root(a, b, c, d):
if a == 0:
return quadratic_root(b, c, d)
else:
return cubic_root_sympy(a, b, c, d)
####################################################################################################
def x_symbol():
return sympy.Symbol('x', real=True)
def real_roots(expression, x):
return [i.n() for i in sympy.real_roots(expression, x)]
####################################################################################################
def cubic_root_sympy(a, b, c, d):
x = x_symbol()
E = a*x**3 + b*x**2 + c*x + d
return real_roots(E, x)
####################################################################################################
def cubic_root_normalised(a, b, c):
x = x_symbol()
E = x**3 + a*x**2 + b*x + c
return real_roots(E, x)
####################################################################################################
def fourth_root_normalised(a, b, c, d):
x = x_symbol()
E = x**4 + a*x**3 + b*x**2 + c*x + d
return real_roots(E, x)
####################################################################################################
def fifth_root_sympy(a, b, c, d, e, f):
x = x_symbol()
E = a*x**5 + b*x**4 + c*x**3 + d*x**2 + e*x + f
return real_roots(E, x)
####################################################################################################
def fifth_root_normalised(a, b, c, d, e):
x = x_symbol()
E = x**5 + a*x**4 + b*x**3 + c*x**2 + d*x + e
return real_roots(E, x)
####################################################################################################
def fifth_root(*args):
# Fixme: RuntimeWarning: divide by zero encountered in double_scalars
a = args[0]
if a == 0:
return fifth_root_sympy(*args)
else:
return fifth_root_normalised(*[x/a for x in args[1:]])
####################################################################################################
def cubic_root_normalised(a, b, c):
# Reference: ???
# converted from haskell http://hackage.haskell.org/package/cubicbezier-0.6.0.5
q = (a**2 - 3*b) / 9
q3 = q**3
m2sqrtQ = -2 * sqrt(q)
r = (2*a**3 - 9*a*b + 27*c) / 54
r2 = r**2
d = - sign(r)*((abs(r) + sqrt(r2-q3))**1/3) # Fixme: sqrt ??
if d == 0:
e = 0
else:
e = q/d
if r2 < q3:
t = acos(r/sqrt(q3))
return [
m2sqrtQ * cos(t/3) - a/3,
m2sqrtQ * cos((t + 2*pi)/3) - a/3,
m2sqrtQ * cos((t - 2*pi)/3) - a/3,
]
else:
return [d + e - a/3]
####################################################################################################
def _cubic_root(a, b, c, d):
# https://en.wikipedia.org/wiki/Cubic_function
# x, a, b, c, d = symbols('x a b c d')
# solveset(x**3+b*x**2+c*x+d, x)
# D0 = b**2 - 3*c
# D1 = 2*b**3 - 9*b*c + 27*d
# DD = D1**2 - 4*D0**3
# C = ((D1 + sqrt(DD) /2)**(1/3)
# - (b + C + D0/C ) /3
# - (b + (-1/2 - sqrt(3)*I/2)*C + D0/((-1/2 - sqrt(3)*I/2)*C) ) /3
# - (b + (-1/2 + sqrt(3)*I/2)*C + D0/((-1/2 + sqrt(3)*I/2)*C) ) /3
# Fixme: divide by a ???
D = 18*a*b*c*d - 4*b**3*d + b**2*c**2 - 4*a*c**3 - 27*a**2*d**2
D0 = b**2 - 3*a*c
if D == 0:
if D0 == 0:
return - b / (3*a) # triple root
else:
r1 = (9*a*d - b*c) / (2*D0) # double root
r2 = (4*a*b*c - 9*a**2*d - b**3) / (a*D0) # simple root
return r1, r2
else:
D1 = 2*b**3 - 9*a*b*c + 27*a**2*d
# DD = - D / (27*a**2)
DD = D1**2 - 4*D0**3
# Fixme: need more info ...
# can have 3 real roots, e.g. 3*x**3 - 25*x**2 + 27*x + 9
# C1 = pow((D1 +- sqrt(DD))/2, 1/3)
# r = - (b + C + D0/C) / (3*a)
raise NotImplementedError
Patro-master-Patro/Patro/Common/Math/__init__.py 0000664 0000000 0000000 00000001744 13424433274 0022052 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements math facilities.
"""
Patro-master-Patro/Patro/Common/Object.py 0000664 0000000 0000000 00000007307 13424433274 0020631 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
__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-master-Patro/Patro/Common/Path.py 0000664 0000000 0000000 00000004030 13424433274 0020305 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import os
####################################################################################################
def to_absolute_path(path):
# Expand ~ . and Remove trailing '/'
return os.path.abspath(os.path.expanduser(path))
####################################################################################################
def parent_directory_of(file_name, step=1):
directory = file_name
for i in range(step):
directory = os.path.dirname(directory)
return directory
####################################################################################################
def find(file_name, directories):
if isinstance(directories, bytes):
directories = (directories,)
for directory in directories:
for directory_path, sub_directories, file_names in os.walk(directory):
if file_name in file_names:
return os.path.join(directory_path, file_name)
raise NameError("File %s not found in directories %s" % (file_name, str(directories)))
Patro-master-Patro/Patro/Common/Platform.py 0000664 0000000 0000000 00000020570 13424433274 0021204 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to query the platform for features.
"""
# Look alternative Python package
####################################################################################################
from enum import Enum, auto
import os
import platform
import sys
####################################################################################################
class PlatformType(Enum):
Linux = auto()
Windows = auto()
OSX = auto()
####################################################################################################
class Platform:
"""Class to store platform properties"""
##############################################
def __init__(self):
self.python_version = platform.python_version()
self.os = self._get_os()
self.node = platform.node()
# deprecated in 3.8 see distro package
# self.distribution = ' '.join(platform.dist())
self.machine = platform.machine()
self.architecture = platform.architecture()[0]
# CPU
self.cpu = self._get_cpu()
self.number_of_cores = self._get_number_of_cores()
self.cpu_khz = self._get_cpu_khz()
self.cpu_mhz = int(self._get_cpu_khz()/float(1000)) # rint
# RAM
self.memory_size_kb = self._get_memory_size_kb()
self.memory_size_mb = int(self.memory_size_kb/float(1024)) # rint
##############################################
def _get_os(self):
if os.name in ('nt',):
return PlatformType.Windows
elif sys.platform in ('linux',):
return PlatformType.Linux
# Fixme:
# elif sys.platform in 'osx':
# return PlatformType.OSX
else:
raise RuntimeError('unknown platform: {} / {}'.format(os.name, sys.platform))
##############################################
def _get_cpu(self):
if self.os == PlatformType.Linux:
with open('/proc/cpuinfo', 'rt') as cpuinfo:
for line in cpuinfo:
if 'model name' in line:
s = line.split(':')[1]
return s.strip().rstrip()
elif self.os == PlatformType.Windows:
raise NotImplementedError
##############################################
def _get_number_of_cores(self):
if self.os == PlatformType.Linux:
number_of_cores = 0
with open('/proc/cpuinfo', 'rt') as cpuinfo:
for line in cpuinfo:
if 'processor' in line:
number_of_cores += 1
return number_of_cores
elif self.os == PlatformType.Windows:
return int(os.getenv('NUMBER_OF_PROCESSORS'))
##############################################
def _get_cpu_khz(self):
if self.os == PlatformType.Linux:
with open('/proc/cpuinfo', 'rt') as cpuinfo:
for line in cpuinfo:
if 'cpu MHz' in line:
s = line.split(':')[1]
return int(1000 * float(s))
if self.os == PlatformType.Windows:
raise NotImplementedError
##############################################
def _get_memory_size_kb(self):
if self.os == PlatformType.Linux:
with open('/proc/meminfo', 'rt') as cpuinfo:
for line in cpuinfo:
if 'MemTotal' in line:
s = line.split(':')[1][:-3]
return int(s)
if self.os == PlatformType.Windows:
raise NotImplementedError
##############################################
def __str__(self):
str_template = '''
Platform {0.node}
Hardware:
Machine: {0.machine}
Architecture: {0.architecture}
CPU: {0.cpu}
Number of Cores: {0.number_of_cores}
CPU Frequence: {0.cpu_mhz} MHz
Memory: {0.memory_size_mb} MB
Python: {0.python_version}
'''
return str_template.format(self)
####################################################################################################
class QtPlatform(Platform):
"""Class to store Qt platform properties"""
##############################################
def __init__(self):
super().__init__()
# Fixme: QT_VERSION_STR ...
from PyQt5 import QtCore, QtWidgets
# from QtShim import QtCore, QtWidgets
self.qt_version = QtCore.QT_VERSION_STR
self.pyqt_version = QtCore.PYQT_VERSION_STR
# Screen
# try:
# application = QtWidgets.QApplication.instance()
# self.desktop = application.desktop()
# self.number_of_screens = self.desktop.screenCount()
# except:
# self.desktop = None
# self.number_of_screens = 0
# for i in range(self.number_of_screens):
# self.screens.append(Screen(self, i))
try:
application = QtWidgets.QApplication.instance()
self.screens = [Screen(screen) for screen in application.screens()]
except:
self.screens = []
# OpenGL
self.gl_renderer = None
self.gl_version = None
self.gl_vendor = None
self.gl_extensions = None
##############################################
@property
def number_of_screens(self):
return len(self.screens)
##############################################
def query_opengl(self):
import OpenGL.GL as GL
self.gl_renderer = GL.glGetString(GL.GL_RENDERER)
self.gl_version = GL.glGetString(GL.GL_VERSION)
self.gl_vendor = GL.glGetString(GL.GL_VENDOR)
self.gl_extensions = GL.glGetString(GL.GL_EXTENSIONS)
##############################################
def __str__(self):
# str_template = '''
# OpenGL
# Render: {0.gl_renderer}
# Version: {0.gl_version}
# Vendor: {0.gl_vendor}
# Number of Screens: {0.number_of_screens}
# '''
# message += str_template.format(self)
message = super().__str__()
for screen in self.screens:
message += str(screen)
str_template = '''
Software Versions:
Qt: {0.qt_version}
PyQt: {0.pyqt_version}
'''
message += str_template.format(self)
return message
####################################################################################################
class Screen:
"""Class to store screen properties"""
##############################################
# def __init__(self, platform_obj, screen_id):
def __init__(self, qt_screen):
# self.screen_id = screen_id
# qt_screen_geometry = platform_obj.desktop.screenGeometry(screen_id)
# self.screen_width, self.screen_height = qt_screen_geometry.width(), qt_screen_geometry.height()
# widget = platform_obj.desktop.screen(screen_id)
# self.dpi = widget.physicalDpiX(), widget.physicalDpiY()
## qt_available_geometry = self.desktop.availableGeometry(screen_id)
self.name = qt_screen.name()
size = qt_screen.size()
self.screen_width, self.screen_height = size.width(), size.height()
self.dpi = qt_screen.physicalDotsPerInch()
self.dpi_x = qt_screen.physicalDotsPerInchX()
self.dpi_y = qt_screen.physicalDotsPerInchY()
##############################################
def __str__(self):
str_template = """
Screen {0.name}
geometry {0.screen_width}x{0.screen_height} px
resolution {0.dpi:.2f} dpi
"""
return str_template.format(self)
Patro-master-Patro/Patro/Common/Singleton.py 0000664 0000000 0000000 00000010173 13424433274 0021360 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
"""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
Patro-master-Patro/Patro/Common/Xml/ 0000775 0000000 0000000 00000000000 13424433274 0017602 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Common/Xml/Objectivity.py 0000664 0000000 0000000 00000026607 13424433274 0022462 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
# cf. also http://lxml.de/objectify.html
####################################################################################################
__all__ = [
'BoolAttribute',
'FloatAttribute',
'FloatListAttribute',
'IntAttribute',
'StringAttribute',
'XmlObjectAdaptator',
]
####################################################################################################
import logging
# from collections import OrderedDict
from lxml import etree
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class Attribute:
"""Class to define XML element attribtes"""
##############################################
def __init__(self, py_attribute, xml_attribute=None, default=None):
self._py_attribute = py_attribute
if xml_attribute is None:
self._xml_attribute = py_attribute
else:
self._xml_attribute = xml_attribute
self._default = default
##############################################
@property
def py_attribute(self):
return self._py_attribute
@property
def py_cls_attribute(self):
"""Return the private identifier used for :class:`XmlObjectAdaptator`"""
return '_' + self._py_attribute
@property
def xml_attribute(self):
return self._xml_attribute
@property
def default(self):
return self._default
##############################################
def __repr__(self):
return '{} {}'.format(self.__class__.__name__, self._py_attribute)
##############################################
@classmethod
def from_xml(cls, value):
"""Convert a value from XML to Python"""
raise NotImplementedError
##############################################
@classmethod
def to_xml(cls, value):
"""Convert a value from Python to XML"""
return str(value)
##############################################
def set_property(self, cls):
"""Define a property for this attribute in :class:`XmlObjectAdaptator`"""
py_cls_attribute = self.py_cls_attribute
setattr(cls,
self.py_attribute,
property(lambda self: getattr(self, py_cls_attribute),
lambda self, value: setattr(self, py_cls_attribute, value),
))
##############################################
def get_attribute(self, instance):
"""Get an attribute of an :class:`XmlObjectAdaptator` instance"""
return getattr(instance, self.py_cls_attribute)
##############################################
def set_attribute(self, instance, value):
"""Set an attribute of an :class:`XmlObjectAdaptator` instance"""
setattr(instance, self.py_cls_attribute, value)
####################################################################################################
class BoolAttribute(Attribute):
##############################################
@classmethod
def from_xml(cls, value):
if value == "true" or value == "1":
return True
elif value == "false" or value == "0":
return False
else:
raise ValueError("Incorrect boolean value {}".format(value))
##############################################
@classmethod
def to_xml(cls, value):
return 'true' if value else 'false'
####################################################################################################
class IntAttribute(Attribute):
##############################################
@classmethod
def from_xml(cls, value):
return int(value)
####################################################################################################
class FloatAttribute(Attribute):
##############################################
@classmethod
def from_xml(cls, value):
return float(value)
####################################################################################################
class FloatListAttribute(Attribute):
##############################################
@classmethod
def from_xml(self, value):
if value == 'none' or value is None:
return None
elif isinstance(value, (tuple, list)): # Python value
return value
else:
if ' ' in value:
separator = ' '
elif ',' in value:
separator = ','
else:
return [float(value)]
return [float(x) for x in value.split(separator)]
##############################################
@classmethod
def to_xml(cls, value):
return ' '.join([str(x) for x in value])
####################################################################################################
class StringAttribute(Attribute):
##############################################
@classmethod
def from_xml(cls, value):
return str(value)
####################################################################################################
class XmlObjectAdaptatorMetaClass(type):
"""Metaclass to collect attributes from super-classes and define a property for each attribute"""
_logger = _module_logger.getChild('XmlObjectAdaptatorMetaClass')
##############################################
def __init__(cls, class_name, super_classes, class_attribute_dict):
# cls._logger.info(str((cls, class_name, super_classes, class_attribute_dict)))
type.__init__(cls, class_name, super_classes, class_attribute_dict)
# Collect attributes from super-classes and update
super_attributes = cls.register_from_super_class(super_classes)
cls.__attributes__ = super_attributes + list(cls.__attributes__)
# Define a property for each attribute
for attribute in cls.__attributes__:
# cls._logger.info('Register {}'.format(attribute))
attribute.set_property(cls)
##############################################
def register_from_super_class(cls, super_classes):
"""Collect attributes from super-classes"""
# Fixme: use set ???
super_attributes = []
for super_class in super_classes:
# __mro__ = [cls, ..., object]
super_attributes += cls.register_from_super_class(super_class.__mro__[1:-1]) # super_class.__subclasses__()
if hasattr(super_class, '__attributes__'):
super_attributes += list(super_class.__attributes__)
return super_attributes
####################################################################################################
class XmlObjectAdaptator(metaclass=XmlObjectAdaptatorMetaClass):
"""Class to implement an object oriented adaptor for XML elements."""
__tag__ = None # XML tag
__attributes__ = ()
##############################################
def __init__(self, *args, **kwargs):
if args:
self._init_from_xml(args[0])
elif kwargs:
self._init_from_kwargs(kwargs)
##############################################
def __repr__(self):
return '{} {}'.format(self.__class__.__name__, self.to_dict())
##############################################
def _init_from_xml(self, xml_element):
xml_attributes = xml_element.attrib
for attribute in self.__attributes__:
xml_attribute = attribute.xml_attribute
if xml_attribute in xml_attributes:
value = attribute.from_xml(xml_attributes[xml_attribute])
else:
value = attribute.default
attribute.set_attribute(self, value)
##############################################
def _init_from_kwargs(self, kwargs):
for attribute in self.__attributes__:
py_attribute = attribute.py_attribute
# Fixme: duplicated code
if py_attribute in kwargs:
value = attribute.from_xml(kwargs[py_attribute])
else:
value = attribute.default
attribute.set_attribute(self, value)
##############################################
@classmethod
def get_dict(cls, instance, exclude=()):
"""Return a dict containing the attributes"""
return {
attribute.py_attribute:getattr(instance, attribute.py_attribute)
for attribute in cls.__attributes__
if attribute.py_attribute not in exclude
}
##############################################
def to_dict(self, exclude=()):
"""Return a dict containing the attributes"""
return {attribute.py_attribute:attribute.get_attribute(self)
for attribute in self.__attributes__
if attribute.py_attribute not in exclude
}
##############################################
def to_xml(self, **kwargs):
"""Return an etree element"""
# attributes = {attribute.xml_attribute:str(attribute.get_attribute(self)) for attribute in self.__attributes__}
attributes = {}
for attribute in self.__attributes__:
value = attribute.get_attribute(self)
if value is not None:
attributes[attribute.xml_attribute] = attribute.to_xml(value)
attributes.update(kwargs)
return etree.Element(self.__tag__, **attributes)
##############################################
def to_xml_string(self):
"""Return a XML string"""
return etree.tostring(self.to_xml())
##############################################
# def __getattribute__(self, name):
# object.__getattribute__(self, '_' + name)
####################################################################################################
class TextXmlObjectAdaptator(XmlObjectAdaptator):
"""Class to implement an object oriented adaptor for text XML elements."""
##############################################
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.text = kwargs.get('text', '')
##############################################
def _init_from_xml(self, xml_element):
super()._init_from_xml(xml_element)
self.text = str(xml_element.text)
##############################################
def to_xml(self, **kwargs):
element = super().to_xml(**kwargs)
element.text = self.text
return element
Patro-master-Patro/Patro/Common/Xml/XmlFile.py 0000664 0000000 0000000 00000006014 13424433274 0021515 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import logging
from pathlib import Path
from lxml import etree
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class XmlFileMixin:
"""Class mixin to parse a XML file using lxml module"""
# _logger = _module_logger.getChild('XmlFile')
##############################################
def __init__(self, path, data=None):
if path is not None:
self._path = Path(path)
else:
self._path = None
self._data = data
##############################################
@property
def path(self):
return self._path
##############################################
def parse(self):
"""Parse a XML file and return the etree"""
data = self._data
if data is None:
with open(str(self._path), 'rb') as f:
source = f.read()
else:
if isinstance(data, bytes):
source = data
else:
source = bytes(str(self._data).strip(), 'utf-8')
return etree.fromstring(source)
##############################################
@staticmethod
def get_xpath_elements(root, path):
"""Utility function to get elements from a xpath and a root"""
return root.xpath(path)
##############################################
@staticmethod
def get_xpath_element(root, path):
"""Utility function to get an element from a xpath and a root"""
return root.xpath(path)[0]
##############################################
@classmethod
def get_text_element(cls, root, path):
"""Utility function to a text element from a xpath and a root"""
element = cls.get_xpath_element(root, path)
if hasattr(element, 'text'):
return element.text
else:
return None
Patro-master-Patro/Patro/Common/Xml/__init__.py 0000664 0000000 0000000 00000001743 13424433274 0021720 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements XML facilities.
"""
Patro-master-Patro/Patro/Common/__init__.py 0000664 0000000 0000000 00000000000 13424433274 0021141 0 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Config/ 0000775 0000000 0000000 00000000000 13424433274 0017017 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Config/ConfigInstall.py 0000664 0000000 0000000 00000005015 13424433274 0022126 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import os
import sys
from pathlib import Path
# Fixme: why ?
import Patro.Common.Path as PathTools # due to Path class
####################################################################################################
class OsFactory:
##############################################
def __init__(self):
if sys.platform.startswith('linux'):
self._name = 'linux'
elif sys.platform.startswith('win'):
self._name = 'windows'
elif sys.platform.startswith('darwin'):
self._name = 'osx'
##############################################
@property
def name(self):
return self._name
@property
def on_linux(self):
return self._name == 'linux'
@property
def on_windows(self):
return self._name == 'windows'
@property
def on_osx(self):
return self._name == 'osx'
OS = OsFactory()
####################################################################################################
_this_file = Path(__file__).resolve()
class Path:
patro_module_directory = _this_file.parents[1]
config_directory = _this_file.parent
####################################################################################################
class Logging:
default_config_file = 'logging.yml'
directories = (Path.config_directory,)
##############################################
@staticmethod
def find(config_file):
return PathTools.find(config_file, Logging.directories)
Patro-master-Patro/Patro/Config/__init__.py 0000664 0000000 0000000 00000000000 13424433274 0021116 0 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Config/logging.yml 0000664 0000000 0000000 00000002207 13424433274 0021171 0 ustar 00root root 0000000 0000000 ####################################################################################################
version: 1
####################################################################################################
formatters:
simple:
format: '%(asctime)s - %(name)s - %(module)s.%(levelname)s - %(message)s'
ansi:
# RESET_SEQ = "\033[0m"
# COLOR_SEQ = "\033[1;%dm"
# BOLD_SEQ = "\033[1m"
format: '[1;32m%(asctime)s[0m - [1;34m%(name)s.%(funcName)s[0m - [1;31m%(levelname)s[0m - %(message)s'
####################################################################################################
handlers:
console:
class: logging.StreamHandler
level: INFO
# formatter: ansi
stream: ext://sys.stdout
####################################################################################################
root:
level: DEBUG
#level: INFO
#level: WARNING
handlers: [console]
####################################################################################################
# loggers:
# Patro:
# level: DEBUG
# #level: INFO
# #level: WARNING
# handlers: [console]
Patro-master-Patro/Patro/FileFormat/ 0000775 0000000 0000000 00000000000 13424433274 0017642 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/FileFormat/Dxf/ 0000775 0000000 0000000 00000000000 13424433274 0020363 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/FileFormat/Dxf/Importer.py 0000664 0000000 0000000 00000011746 13424433274 0022547 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""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)
radius_x, radius_y = major_axis.magnitude, minor_axis.magnitude
angle = major_axis.orientation
if angle == 90:
radius_x, radius_y = radius_y, radius_x
angle = 0
# Fixme: ...
ellipse = Ellipse2D(
center,
radius_x, radius_y,
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-master-Patro/Patro/FileFormat/Dxf/Polyline.py 0000664 0000000 0000000 00000016103 13424433274 0022531 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
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
Patro-master-Patro/Patro/FileFormat/Dxf/__init__.py 0000664 0000000 0000000 00000000000 13424433274 0022462 0 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/FileFormat/Svg/ 0000775 0000000 0000000 00000000000 13424433274 0020401 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/FileFormat/Svg/SvgFile.py 0000664 0000000 0000000 00000031517 13424433274 0022321 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This modules implements the SVG file format.
Import Algorithm:
* text is informative, connect text to path
* short lines or polygon are sewing markers
* line with a small polygon at extremities is a grainline
* expect pieces are delimited by a path
* check for paths sharing vertexes and stroke style
"""
####################################################################################################
import logging
from lxml import etree
from IntervalArithmetic import Interval2D
from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.GeometryEngine.Transformation import AffineTransformation2D
from . import SvgFormat
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class RenderState:
# Fixme: convert type !!!
STATES = [name for name in SvgFormat.PresentationAttributes.__dict__.keys()
if not name.startswith('_')]
##############################################
@classmethod
def to_python(cls, value):
# Fixme: move ???
if isinstance(value, str):
if value == 'none':
return None
else:
try:
float_value = float(value)
if '.' in value:
return float_value
else:
return int(float_value)
except ValueError:
pass
return value
##############################################
def __init__(self, item=None):
# Init from item else use default value
for state in self.STATES:
if item is not None and hasattr(item, state):
value = self.to_python(getattr(item, state))
else:
value = getattr(SvgFormat.PresentationAttributes, state)
setattr(self, state, value)
##############################################
def clone(self):
return self.__class__(self)
##############################################
def to_dict(self, all=False):
if all:
return {state:getattr(self, state) for state in self.STATES}
else:
d = {}
for state in self.STATES:
value = getattr(self, state)
if value is not None:
d[state] = value
return d
##############################################
def merge(self, item):
for state in self.STATES:
if hasattr(item, state):
value = getattr(item, state)
if state == 'transform':
if value is not None:
# Transform matrix is composed from top to item
# thus left to right
self.transform = self.transform * value
elif state == 'style':
pass
else:
setattr(self, state, self.to_python(value))
# Merge style
style = getattr(item, 'style', None)
if style is not None:
for pair in style.split(';'):
state, value = [x.strip() for x in pair.split(':')]
state = state.replace('-', '_')
if state == 'transform':
self.transform = self.transform * value
else:
setattr(self, state, self.to_python(value))
return self
##############################################
def __str__(self):
return str(self.to_dict())
####################################################################################################
class RenderStateStack:
##############################################
def __init__(self):
self._stack = [RenderState()]
##############################################
@property
def state(self):
return self._stack[-1]
##############################################
def push(self, kwargs):
new_state = self.state.clone()
new_state.merge(kwargs)
self._stack.append(new_state)
##############################################
def pop(self):
self._stack.pop()
####################################################################################################
class SvgDispatcher:
"""Class to dispatch XML to Python class."""
__TAGS__ = {}
for cls_name in SvgFormat.__all__:
cls = getattr(SvgFormat, cls_name)
__TAGS__[cls.__tag__] = cls
__TAGS_TO_READ__ = [
# 'svg', # implicit
# 'anchor',
# 'altGlyph',
# 'altGlyphDef',
# 'altGlyphItem',
# 'animate',
# 'animateMotion',
# 'animateTransform',
'circle',
#? 'clipPath',
# 'colorProfile',
# 'cursor',
# 'defs',
# 'desc',
'ellipse',
# 'feBlend',
# 'group', # implicit
# 'image',
'line',
# 'linearGradient',
# 'marker',
# 'mask',
'path',
# 'pattern',
'polyline',
'polygon',
# 'radialGradient',
'rect',
# 'stop',
#! 'text',
# 'textRef',
#! 'textSpan',
# 'use',
]
_logger = _module_logger.getChild('SvgDispatcher')
##############################################
def __init__(self, reader):
self._reader =reader
self.reset()
# self.on_root(root)
##############################################
def reset(self):
self._state_stack = RenderStateStack()
##############################################
@property
def state(self):
return self._state_stack.state
##############################################
def element_tag(self, element):
tag = element.tag
if '{' in tag:
tag = tag[tag.find('}')+1:]
return tag
##############################################
def from_xml(self, element):
tag = self.element_tag(element)
tag_class = self.__TAGS__[tag]
if tag_class is not None:
# self._logger.info('\n{} / {}'.format(element, tag_class))
return tag_class(element)
else:
raise NotImplementedError
##############################################
def on_root(self, root):
for element in root:
tag = self.element_tag(element)
if tag == 'g':
self.on_group(element)
elif tag in self.__TAGS_TO_READ__:
self.on_graphic_item(element)
##############################################
def on_group(self, element):
group = self.from_xml(element)
# self._logger.info('Group: {}\n{}'.format(group.id, group))
self._reader.on_group(group)
self._state_stack.push(group)
# self._logger.info('State:\n' + str(self.state))
self.on_root(element)
##############################################
def on_graphic_item(self, element):
item = self.from_xml(element)
# self._logger.info('Item: {}\n{}'.format(item.id, item))
self._reader.on_graphic_item(item)
####################################################################################################
class SvgFileMixin:
SVG_DOCTYPE = ''
SVG_xmlns = 'http://www.w3.org/2000/svg'
SVG_xmlns_xlink = 'http://www.w3.org/1999/xlink'
SVG_version = '1.1'
####################################################################################################
class SvgFileInternal(XmlFileMixin, SvgFileMixin):
"""Class to read/write SVG file."""
_logger = _module_logger.getChild('SvgFile')
__dispatcher_cls__ = SvgDispatcher
##############################################
def __init__(self, path, data=None):
super().__init__(path, data)
# Fixme: API
# purpose of dispatcher, where must be state ???
self._dispatcher = self.__dispatcher_cls__(self)
self._read()
##############################################
def _read(self):
#
#
#
tree = self.parse()
svg_root = self._dispatcher.from_xml(tree)
self.on_svg_root(svg_root)
self._dispatcher.on_root(tree)
##############################################
@property
def view_box(self):
return self._view_box
@property
def width(self):
return self._width
@property
def height(self):
return self._height
##############################################
def on_svg_root(self, svg_root):
x_inf, y_inf, x_sup, y_sup = svg_root.view_box
self._view_box = Interval2D((x_inf, x_sup), (y_inf, y_sup))
self._width = svg_root.width
self._height = svg_root.height
##############################################
def on_group(self, group):
self._logger.info('Group: {}\n{}'.format(group.id, group))
##############################################
def on_graphic_item(self, item):
self._logger.info('Item: {}\n{}'.format(item.id, item))
state = self._dispatcher.state.clone().merge(item)
self._logger.info('Item State:\n' + str(state))
####################################################################################################
class SvgFileWriter(SvgFileMixin):
"""Class to write a SVF file."""
_logger = _module_logger.getChild('SvgFileWriter')
COMMENT = 'Pattern created with Patro (https://github.com/FabriceSalvaire/Patro)'
##############################################
def __init__(self, path, paper, root_tree, transformation=None):
self._path = str(path)
self._write(paper, root_tree, transformation)
##############################################
@classmethod
def _new_root(cls, paper):
nsmap = {
None: cls.SVG_xmlns,
'xlink': cls.SVG_xmlns_xlink,
}
root = etree.Element('svg', nsmap=nsmap)
attrib = root.attrib
attrib['version'] = cls.SVG_version
# Set document dimension and user space unit to mm
# see https://mpetroff.net/2013/08/analysis-of-svg-units
attrib['width'] = '{:.3f}mm'.format(paper.width)
attrib['height'] = '{:.3f}mm'.format(paper.height)
attrib['viewBox'] = '0 0 {:.3f} {:.3f}'.format(paper.width, paper.height)
# Fixme: from conf
root.append(etree.Comment(cls.COMMENT))
return root
##############################################
def _write(self, paper, root_tree, transformation=None):
root = self._new_root(paper)
# Fixme: implement tree, look at lxml
if transformation:
# transform text as well !!!
group = SvgFormat.Group(transform=transformation).to_xml()
root.append(group)
else:
group = root
for element in root_tree:
group.append(element.to_xml())
tree = etree.ElementTree(root)
tree.write(self._path,
pretty_print=True,
xml_declaration=True,
encoding='utf-8',
standalone=False,
doctype=self.SVG_DOCTYPE,
)
####################################################################################################
class SvgFile:
##############################################
def __init__(self, path):
self._interval = SvgFileInternal(path)
Patro-master-Patro/Patro/FileFormat/Svg/SvgFormat.py 0000664 0000000 0000000 00000123730 13424433274 0022671 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This modules implements the `SVG `_ file format.
References:
* `SVG 1.1 (Second Edition) W3C Recommendation 16 August 2011 `_
"""
####################################################################################################
__all__ = [
'Svg',
'Anchor',
'AltGlyph',
'AltGlyphDef',
'AltGlyphItem',
'Animate',
'AnimateMotion',
'AnimateTransform',
'Circle',
'ClipPath',
'ColorProfile',
'Cursor',
'Defs',
'Desc',
'Ellipse',
'FeBlend',
'Group',
'Image',
'Line',
'LinearGradient',
'Marker',
'Mask',
'Path',
'Pattern',
'Polyline',
'Polygon',
'RadialGradient',
'Rect',
'Stop',
'Text',
'TextRef',
'TextSpan',
'Use',
]
####################################################################################################
import logging
from Patro.Common.Xml.Objectivity import (
# BoolAttribute,
IntAttribute, FloatAttribute,
FloatListAttribute,
StringAttribute,
XmlObjectAdaptator,
TextXmlObjectAdaptator,
)
# Fixme: should we mix SVG format and ... ???
from Patro.GeometryEngine.Path import Path2D
from Patro.GeometryEngine.Transformation import AffineTransformation2D
from Patro.GeometryEngine.Vector import Vector2D
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
def split_space_list(value):
return [x for x in value.split(' ') if x]
####################################################################################################
#
# Conditional Processing Attributes
# requiredFeatures
# requiredExtensions
# systemLanguage
#
####################################################################################################
####################################################################################################
#
# Core Attribute
# id
# xml:base
# xml:lang
# xml:space
#
####################################################################################################
class IdMixin:
"""Core attribute"""
__attributes__ = (
StringAttribute('id'),
)
####################################################################################################
#
# Graphical Event Attributes
# onactivate
# onclick
# onfocusin
# onfocusout
# onload
# onmousedown
# onmousemove
# onmouseout
# onmouseover
# onmouseup
#
####################################################################################################
####################################################################################################
class PresentationAttributes:
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
# Fixme: type !!!
alignment_baseline = None # check
baseline_shift = None # check
clip = None # check
clip_path = None # check
clip_rule = None # check
color = None # check
color_interpolation = None # check
color_interpolation_filters = None # check
color_profile = None # check
color_rendering = None # check
cursor = None # check
direction = None # check
display = None # check
dominant_baseline = None # check
enable_background = None # check
fill = None
fill_opacity = 1
fill_rule = 'nonzero'
#! filter = None # check
flood_color = None # check
flood_opacity = None # check
font_family = None # check
font_size = None # check
font_size_adjust = None # check
font_stretch = None # check
font_style = None # check
font_variant = None # check
font_weight = None # check
glyph_orientation_horizontal = None # check
glyph_orientation_vertical = None # check
image_rendering = None # check
kerning = None # check
letter_spacing = None # check
lighting_color = None # check
marker_end = None # check
marker_mid = None # check
marker_start = None # check
mask = None # check
opacity = 1
overflow = None # check
paint_order = None
pointer_events = None # check
shape_rendering = None # check
stop_color = None # check
stop_opacity = None # check
stroke = None
stroke_dasharray = None # check
stroke_dashoffset = None # check
stroke_linecap = 'butt'
stroke_linejoin = 'miter'
stroke_miterlimit = 4
stroke_opacity = 1
stroke_width = 1 # px
style = None
text_anchor = None # check
text_decoration = None # check
text_rendering = None # check
transform = AffineTransformation2D.Identity()
unicode_bidi = None # check
vector_effect = None
visibility = None # check
word_spacing = None # check
writing_mode = None # check
####################################################################################################
class InheritAttribute(StringAttribute):
##############################################
@classmethod
def from_xml(cls, value):
if value == 'inherit':
return value
else:
return cls._from_xml(value)
##############################################
@classmethod
def _from_xml(cls, value):
raise NotImplementedError
####################################################################################################
class NumberAttribute(InheritAttribute):
@classmethod
def _from_xml(cls, value):
return float(value)
####################################################################################################
class PercentValue:
##############################################
def __init__(self, value):
self._value = float(value) / 100
##############################################
def __float__(self):
return self._value
####################################################################################################
class UnitValue:
##############################################
def __init__(self, value, unit):
self._value = float(value)
self._unit = unit
##############################################
def __float__(self):
return self._value
##############################################
def __str__(self):
return self._unit
####################################################################################################
class PercentLengthAttribute(InheritAttribute):
##############################################
@classmethod
def _from_xml(cls, value):
# length ::= number ("em" | "ex" | "px" | "in" | "cm" | "mm" | "pt" | "pc" | "%")?
if value.endswith('%'):
return PercentValue(value[:-1])
elif value[-1].isalpha():
return UnitValue(value[:-2], value[-2])
else:
return float(value)
##############################################
@classmethod
def _to_xml(cls, value):
# Fixme: ok ???
if isinstance(value, PercentValue):
return '{}%'.format(float(value))
elif isinstance(value, UnitValue):
return '{}{}'.format(float(value), value.unit)
else:
return str(value)
####################################################################################################
class ColorMixin:
__attributes__ = (
StringAttribute('fill'), # none inherit red #ffbb00
StringAttribute('stroke'),
)
####################################################################################################
class StrokeMixin:
__attributes__ = (
StringAttribute('stroke_line_cap', 'stroke-linecap'),
StringAttribute('stroke_line_join', 'stroke-linejoin'),
NumberAttribute('stroke_miter_limit', 'stroke-miterlimit'),
PercentLengthAttribute('stroke_width', 'stroke-width'),
FloatListAttribute('stroke_dasharray', 'stroke-dasharray') # stroke-dasharray="20,10,5,5,5,10"
)
LINE_CAP_STYLE = (
'butt',
'round',
'square',
'inherit',
)
LINE_JOIN_STYLE = (
'miter',
'round',
'bevel',
'inherit',
)
####################################################################################################
class PathMixin(ColorMixin, StrokeMixin):
pass
####################################################################################################
class FontMixin:
__attributes__ = (
StringAttribute('font_size', 'font-size'),
StringAttribute('font_family', 'font-family'),
)
# font-face-format
# font-face-name
# font-face-src
# font-face-uri
# text-anchor
# glyph Defines the graphics for a given glyph
# glyphRef Defines a possible glyph to use
# hkern
####################################################################################################
####################################################################################################
class StyleMixin:
__attributes__ = (
StringAttribute('style'),
)
####################################################################################################
#
# Shared attributes
#
####################################################################################################
class PositionMixin:
__attributes__ = (
FloatAttribute('x'),
FloatAttribute('y'),
)
####################################################################################################
class CenterMixin:
__attributes__ = (
FloatAttribute('cx'), # x-axis center of the circle
FloatAttribute('cy'),
)
####################################################################################################
class DeltaMixin:
__attributes__ = (
FloatAttribute('dx'),
FloatAttribute('dy'),
)
####################################################################################################
class RadiusMixin:
__attributes__ = (
FloatAttribute('rx'), # x-axis radius (to round the element)
FloatAttribute('ry'),
)
####################################################################################################
class PointsMixin:
__attributes__ = (
StringAttribute('points'), # points="200,10 250,190 160,210"
)
####################################################################################################
class SizeMixin:
__attributes__ = (
StringAttribute('height'),
StringAttribute('width'),
)
####################################################################################################
class TransformAttribute(StringAttribute):
TRANSFORM = (
'matrix',
'rotate',
'scale',
'skewX',
'skewY',
'translate'
)
##############################################
@classmethod
def from_xml(cls, value):
if isinstance(value, AffineTransformation2D):
# Python value
return value
else:
transforms = []
for transform in split_space_list(value):
pos0 = value.find('(')
pos1 = value.find(')')
if pos0 == -1 or pos1 != len(value) -1:
raise ValueError
transform_type = value[:pos0]
values = [float(x) for x in value[pos0+1:-1].split(',')]
transforms.append((transform_type, values))
# Fixme:
# return transforms
return cls.to_python(transforms, concat=True)
##############################################
@classmethod
def to_xml(cls, value):
# Fixme: wrong if value is AffineTransformation2D !!!
# Fixme: to func
return 'matrix({})'.format(' '.join([str(x) for x in value.to_list()]))
##############################################
@classmethod
def to_python(cls, transforms, concat=True):
def complete(values, size):
return values + [0]*(size - len(values))
global_transform = AffineTransformation2D.Identity()
py_transforms = []
for name, values in transforms:
transform = None
if name == 'matrix':
array = [values[i] for i in (0, 2, 4, 1, 3, 5)] + [0, 0, 1]
transform = AffineTransformation2D(array)
elif name == 'translate':
vector = Vector2D(complete(values, 2))
transform = AffineTransformation2D.Translation(vector)
elif name == 'scale':
transform = AffineTransformation2D.Scale(*values)
elif name == 'rotate':
angle, *vector = complete(values, 3)
vector = Vector2D(vector)
transform = AffineTransformation2D.RotationAt(vector, angle)
elif name == 'skewX':
angle = values[0]
raise NotImplementedError
elif name == 'skewY':
angle = values[0]
raise NotImplementedError
else:
raise NotImplementedError
if concat:
global_transform = transform * global_transform
else:
py_transforms.append(transform)
if concat:
return global_transform
else:
return py_transforms
####################################################################################################
class TransformMixin:
__attributes__ = (
TransformAttribute('transform'), # matrix(1,0,0,-1,0,1.)
)
####################################################################################################
####################################################################################################
# The available filter elements in SVG are:
#
# - filter for combining images
# - filter for color transforms
#
#
#
#
#
#
#
#
#
#
# - filter for drop shadows
#
#
#
# - filter for lighting
# - filter for lighting
# - filter for lighting
####################################################################################################
class SvgElementMixin(IdMixin, StyleMixin, TransformMixin):
pass
####################################################################################################
#
# Svg Root Element
#
####################################################################################################
class Svg(PositionMixin, SizeMixin, XmlObjectAdaptator):
"""Creates an SVG document fragment"""
__tag__ = 'svg'
# xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
__attributes__ = (
StringAttribute('version'),
FloatListAttribute('view_box', 'viewBox'),
# the points "seen" in this SVG drawing area. 4 values separated by white space or
# commas. (min x, min y, width, height)
StringAttribute('preserve_aspect_ratio', 'preserveAspectRatio'),
# 'none' or any of the 9 combinations of 'xVALYVAL' where VAL is 'min', 'mid' or 'max'.
# (default xMidYMid)
StringAttribute('zoom_and_pan', 'zoomAndPan')
# 'magnify' or 'disable'. Magnify option allows users to pan and zoom your file
# (default magnify)
)
# x="top left corner when embedded (default 0)"
# y="top left corner when embedded (default 0)"
# width="the width of the svg fragment (default 100%)"
# height="the height of the svg fragment (default 100%)"
####################################################################################################
#
# SVG Elements by name
#
####################################################################################################
class Anchor(XmlObjectAdaptator):
"""Creates a link around SVG elements"""
__tag__ = 'a'
# xlink:show
# xlink:actuate
# xlink:href
# target
####################################################################################################
class AltGlyph(PositionMixin, DeltaMixin, XmlObjectAdaptator):
"""Provides control over the glyphs used to render particular character data"""
__tag__ = 'altGlyph'
# rotate
# glyphRef
# format
# xlink:href
####################################################################################################
class AltGlyphDef(IdMixin, XmlObjectAdaptator):
"""Defines a substitution set for glyphs"""
__tag__= 'altGlyphDef'
####################################################################################################
class AltGlyphItem(IdMixin, XmlObjectAdaptator):
"""Defines a candidate set of glyph substitutions"""
__tag__ = 'altGlyphItem'
####################################################################################################
class Animate(XmlObjectAdaptator):
"""Defines how an attribute of an element changes over time"""
__tag__ = 'animate'
# attributeName="the name of the target attribute"
# by="a relative offset value"
# from="the starting value"
# to="the ending value"
# dur="the duration"
# repeatCount="the number of time the animation will take place"
####################################################################################################
class AnimateMotion(XmlObjectAdaptator):
"""Causes a referenced element to move along a motion path"""
__tag__ = 'animateMotion'
# calcMode="the interpolation mode for the animation. Can be 'discrete', 'linear', 'paced', 'spline'"
# path="the motion path"
# keyPoints="how far along the motion path the object shall move at the moment in time"
# rotate="applies a rotation transformation"
# xlink:href="an URI reference to the element which defines the motion path"
####################################################################################################
class AnimateTransform(XmlObjectAdaptator):
"""Animates a transformation attribute on a target element, thereby allowing animations to control
translation, scaling, rotation and/or skewing
"""
__tag__ = 'animateTransform'
# by="a relative offset value"
# from="the starting value"
# to="the ending value"
# type="the type of transformation which is to have its values change over time. Can be 'translate', 'scale', 'rotate', 'skewX', 'skewY'"
####################################################################################################
class Circle(CenterMixin, PathMixin, SvgElementMixin, XmlObjectAdaptator):
"""Defines a circle"""
__tag__ = 'circle'
__attributes__ = (
FloatAttribute('r'), # circle's radius. Required.
)
####################################################################################################
class ClipPath(XmlObjectAdaptator):
"""Clipping is about hiding what normally would be drawn. The stencil which defines what is and
what isn't drawn is called a clipping path
"""
__tag__ = 'clipPath'
# clip-path="the referenced clipping path is intersected with the referencing clipping path"
# clipPathUnits="'userSpaceOnUse' or 'objectBoundingBox'. The second value makes units of children a fraction of the object bounding box which uses the mask (default: 'userSpaceOnUse')"
####################################################################################################
class ColorProfile(XmlObjectAdaptator):
"""Specifies a color profile description (when the document is styled using CSS)"""
__tag__ = 'color-profile'
# local="the unique ID for a locally stored color profile"
# name=""
# rendering-intent="auto|perceptual|relative-colorimetric|saturation|absolute-colorimetric"
# xlink:href="the URI of an ICC profile resource"
####################################################################################################
class Cursor(PositionMixin, XmlObjectAdaptator):
"""Defines a platform-independent custom cursor"""
__tag__ = 'cursor'
# x="the x-axis top-left corner of the cursor (default is 0)"
# y="the y-axis top-left corner of the cursor (default is 0)"
# xlink:href="the URI of the image to use as the cursor
####################################################################################################
class Defs(XmlObjectAdaptator):
"""A container for referenced elements"""
__tag__ = 'defs'
####################################################################################################
class Desc(XmlObjectAdaptator):
"""A text-only description for container elements or graphic elements in SVG (user agents may
display the text as a tooltip)"""
__tag__ = 'desc'
####################################################################################################
class Ellipse(CenterMixin, PathMixin, StyleMixin, XmlObjectAdaptator):
"""Defines an ellipse"""
__tag__ = 'ellipse'
__attributes__ = (
FloatAttribute('rx'),
FloatAttribute('ry'),
)
####################################################################################################
class FeBlend(XmlObjectAdaptator):
"""Composes two objects together according to a certain blending mode"""
__tag__ = 'feBlend'
# mode="the image blending modes: normal|multiply|screen|darken|lighten"
# in="identifies input for the given filter primitive: SourceGraphic | SourceAlpha | BackgroundImage | BackgroundAlpha | FillPaint | StrokePaint | "
# in2="the second input image to the blending operation"
####################################################################################################
class Group(FontMixin, PathMixin, SvgElementMixin, XmlObjectAdaptator):
"""Used to group together elements"""
__tag__ = 'g'
__attributes__ = (
StringAttribute('clip_path', 'clip-path', None),
StringAttribute('data_name', 'data-name'),
#fill="the fill color for the group"
#opacity="the opacity for the group"
)
####################################################################################################
class Image(PositionMixin, SizeMixin, XmlObjectAdaptator):
"""Defines an image"""
__tag__ = 'image'
# x="the x-axis top-left corner of the image"
# y="the y-axis top-left corner of the image"
# width="the width of the image". Required.
# height="the height of the image". Required.
# xlink:href="the path to the image". Required.
####################################################################################################
class Line(PathMixin, SvgElementMixin, XmlObjectAdaptator):
"""Defines a line"""
__tag__ = 'line'
__attributes__ = (
FloatAttribute('x1'), # x start point of the line
FloatAttribute('y1'),
FloatAttribute('x2'), # x end point of the line
FloatAttribute('y2'),
)
####################################################################################################
class LinearGradient(IdMixin, XmlObjectAdaptator):
"""Defines a linear gradient. Linear gradients fill the object by using a vector, and can be defined
as horizontal, vertical or angular gradients.
"""
__tag__ = 'linearGradient'
# id="the unique id used to reference this pattern. Required to reference it"
# gradientUnits="'userSpaceOnUse' or 'objectBoundingBox'. Use the view box or object to determine relative position of vector points. (Default 'objectBoundingBox')"
# gradientTransform="the transformation to apply to the gradient"
# x1="the x start point of the gradient vector (number or % - 0% is default)"
# y1="the y start point of the gradient vector. (0% default)"
# x2="the x end point of the gradient vector. (100% default)"
# y2="the y end point of the gradient vector. (0% default)"
# spreadMethod="'pad' or 'reflect' or 'repeat'"
# xlink:href="reference to another gradient whose attribute values are used as defaults and stops included. Recursive"
####################################################################################################
class Marker(XmlObjectAdaptator):
"""Markers can be placed on the vertices of lines, polylines, polygons and paths. These elements can
use the marker attributes "marker-start", "marker-mid" and "marker-end"' which inherit by
default or can be set to 'none' or the URI of a defined marker. You must first define the marker
before you can reference it via its URI. Any kind of shape can be put inside marker. They are
drawn on top of the element they are attached to markerUnits="'strokeWidth' or
'userSpaceOnUse'. If 'strokeWidth' is used then one unit equals one stroke width. Otherwise, the
marker does not scale and uses the the same view units as the referencing element (default
'strokeWidth')
"""
__tag__ = 'marker'
# refx="the position where the marker connects with the vertex (default 0)"
# refy="the position where the marker connects with the vertex (default 0)"
# orient="'auto' or an angle to always show the marker at. 'auto' will compute an angle that makes the x-axis a tangent of the vertex (default 0)"
# markerWidth="the width of the marker (default 3)"
# markerHeight="the height of the marker (default 3)"
# viewBox="the points "seen" in this SVG drawing area. 4 values separated by white space or commas. (min x, min y, width, height)"
####################################################################################################
class Mask(PositionMixin, SizeMixin, XmlObjectAdaptator):
"""Masking is a combination of opacity values and clipping. Like clipping you can use shapes, text
or paths to define sections of the mask. The default state of a mask is fully transparent which
is the opposite of clipping plane. The graphics in a mask sets how opaque portions of the mask
are
"""
__tag__ = 'mask'
# maskUnits="'userSpaceOnUse' or 'objectBoundingBox'. Set whether the clipping plane is relative the full view port or object (default: 'objectBoundingBox')"
# maskContentUnits="Use the second with percentages to make mask graphic positions relative the object. 'userSpaceOnUse' or 'objectBoundingBox' (default: 'userSpaceOnUse')"
# x="the clipping plane of the mask (default: -10%)"
# y="the clipping plane of the mask (default: -10%)"
# width="the clipping plane of the mask (default: 120%)"
# height="the clipping plane of the mask (default: 120%)"
####################################################################################################
class PathDataAttribute(StringAttribute):
"""Define a path data attribute.
Path data can contain newline characters and thus can be broken up into multiple lines to
improve readability. Because of line length limitations with certain related tools, it is
recommended that SVG generators split long path data strings across multiple lines, with each
line not exceeding 255 characters. Also note that newline characters are only allowed at certain
places within path data.
The syntax of path data is concise in order to allow for minimal file size and efficient
downloads, since many SVG files will be dominated by their path data. Some of the ways that SVG
attempts to minimize the size of path data are as follows:
* All instructions are expressed as one character (e.g., a moveto is expressed as an M).
* Superfluous white space and separators such as commas can be eliminated
(e.g., "M 100 100 L 200 200" contains unnecessary spaces and
could be expressed more compactly as "M100 100L200 200").
* The command letter can be eliminated on subsequent commands if the same command
is used multiple times in a row (e.g., you can drop the second
"L" in "M 100 200 L 200 100 L -100 -200" and use
"M 100 200 L 200 100 -100 -200" instead).
* Relative versions of all commands are available (uppercase means absolute coordinates,
lowercase means relative coordinates).
* Alternate forms of lineto are available to optimize the special cases of horizontal and
vertical lines (absolute and relative).
* Alternate forms of curve are available to optimize the special cases where some
of the control points on the current segment can be determined automatically
from the control points on the previous segment.
The following commands are available for path data:
* Move to
M x y
m dx dy
* Line to
L x y
l dx dy
* Horizontal Line to
H x
h dx
* Vertical Line to
V y
v dy
* Cubic Bézier Curve
C x1 y1, x2 y2, x y
c dx1 dy1, dx2 dy2, dx dy
* Smooth Cubic Bézier Curve
S x2 y2, x y
s dx2 dy2, dx dy"
* Quadratic Bézier Curve
Q x1 y1, x y
q dx1 dy1, dx dy
* Smooth Quadratic Bézier Curve
T x y
t dx dy
* Elliptical Arc
A rx ry x-axis-rotation large-arc-flag sweep-flag x y
a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy
* Close Path
Z
"""
NUMBER_OF_ARGS = {
'm':2,
'l':2,
'h':1,
'v':1,
'c':6,
's':4,
'q':4,
't':3,
'a':7,
'z':0,
}
COMMANDS = ''.join(NUMBER_OF_ARGS.keys())
_logger = _module_logger.getChild('PathDataAttribute')
##############################################
@classmethod
def from_xml(cls, svg_path):
# cls._logger.info('SVG path:\n'+ svg_path)
# Replace comma separator by space
cleaned_svg_path = svg_path.replace(',', ' ')
# Add space after letter
data_path = ''
for c in cleaned_svg_path:
data_path += c
if c.isalpha:
data_path += ' '
# Convert float values
parts = []
for part in split_space_list(cleaned_svg_path):
if not(len(part) == 1 and part.isalpha()):
part = float(part)
parts.append(part)
commands = []
command = None # last command
number_of_args = None
i = 0
while i < len(parts):
part = parts[i]
if isinstance(part, str):
command = part
command_lower = command.lower()
if command_lower not in cls.COMMANDS:
raise ValueError("Invalid path instruction: '{}' in\n{}".format(command, svg_path))
number_of_args = cls.NUMBER_OF_ARGS[command_lower]
i += 1 # move to first arg
# else repeated instruction
next_i = i + number_of_args
args = parts[i:next_i]
commands.append((command, args))
i = next_i
# for implicit line to
if command == 'm':
command = 'l'
elif command == 'M':
command = 'L'
# return commands
# Fixme: do later ???
return cls.to_geometry(commands)
##############################################
@classmethod
def to_xml(cls, value):
path_data = ''
for command in value:
if path_data:
path_data += ' '
path_data += ' '.join(list(command[0]) + [str(x) for x in command[1]])
return path_data
##############################################
@classmethod
def as_vector(cls, args):
number_of_args = len(args)
number_of_vectors = number_of_args // 2
if number_of_args != number_of_vectors * 2:
raise ValueError('len(args) is not // 2: {}'.format(number_of_args))
return [Vector2D(args[i:i+2]) for i in range(0, number_of_args, 2)]
##############################################
@classmethod
def to_geometry(cls, commands):
# cls._logger.info('Path:\n' + str(commands).replace('), ', '),\n '))
path = None
for command, args in commands:
command_lower = command.lower()
absolute = command_lower != command # Upper case means absolute
# if is_lower:
# cls._logger.warning('incremental command')
# raise NotImplementedError
if path is None:
if command_lower != 'm':
raise NameError('Path must start with m')
path = Path2D(args) # Vector2D()
else:
if command_lower == 'l':
path.line_to(args, absolute=absolute)
elif command == 'h':
path.horizontal_to(*args, absolute=False)
elif command == 'H':
path.absolute_horizontal_to(*args)
elif command_lower == 'v':
path.vertical_to(*args, absolute=absolute)
elif command == 'V':
path.absolute_vertical_to(*args)
elif command_lower == 'c':
path.cubic_to(*cls.as_vector(args), absolute=absolute)
elif command_lower == 's':
path.stringed_quadratic_to(*cls.as_vector(args), absolute=absolute)
elif command_lower == 'q':
path.quadratic_to(*cls.as_vector(args), absolute=absolute)
elif command_lower == 't':
path.stringed_cubic_to(*cls.as_vector(args), absolute=absolute)
elif command_lower == 'a':
radius_x, radius_y, angle, large_arc, sweep, x, y = args
point = Vector2D(x, y)
path.arc_to(point, radius_x, radius_y, angle, bool(large_arc), bool(sweep), absolute=absolute)
elif command_lower == 'z':
path.close()
return path
####################################################################################################
class Path(PathMixin, SvgElementMixin, XmlObjectAdaptator):
"""Defines a path"""
__tag__ = 'path'
__attributes__ = (
PathDataAttribute('path_data', 'd'), # a set of commands which define the path
FloatAttribute('path_length', 'pathLength'),
# If present, the path will be scaled so that the computed path length of the points equals
# this value
)
####################################################################################################
class Pattern(IdMixin, PositionMixin, SizeMixin, XmlObjectAdaptator):
"""Defines the coordinates you want the view to show and the size of the view. Then you add shapes
into your pattern. The pattern repeats when an edge of the view box (viewing area) is hit
"""
__tag__ = 'pattern'
# id="the unique id used to reference this pattern." Required.
# patternUnits="'userSpaceOnUse' or 'objectBoundingBox'. The second value makes units of x, y, width, height a fraction (or %) of the object bounding box which uses the pattern."
# patternContentUnits="'userSpaceOnUse' or 'objectBoundingBox'"
# patternTransform="allows the whole pattern to be transformed"
# x="pattern's offset from the top-left corner (default 0)"
# y="pattern's offset from the top-left corner. (default 0)"
# width="the width of the pattern tile (default 100%)"
# height="the height of the pattern tile (default 100%)"
# viewBox="the points "seen" in this SVG drawing area. 4 values separated by white space or commas. (min x, min y, width, height)"
# xlink:href="reference to another pattern whose attribute values are used as defaults and any children are inherited. Recursive"
####################################################################################################
class Polyline(PointsMixin, PathMixin, SvgElementMixin, XmlObjectAdaptator):
"""Defines a graphic that contains at least three sides"""
__tag__ = 'polyline'
####################################################################################################
class Polygon(Polyline, XmlObjectAdaptator):
"""Defines any shape that consists of only straight lines"""
__tag__ = 'polyline'
# fill-rule="part of the FillStroke presentation attributes"
####################################################################################################
class RadialGradient(XmlObjectAdaptator):
"""Defines a radial gradient. Radial gradients are created by taking a circle and smoothly changing
values between gradient stops from the focus point to the outside radius.
"""
__tag__ = 'radialGradient'
# gradientUnits="'userSpaceOnUse' or 'objectBoundingBox'. Use the view box or object to determine relative position of vector points. (Default 'objectBoundingBox')"
# gradientTransform="the transformation to apply to the gradient"
# cx="the center point of the gradient (number or % - 50% is default)"
# cy="the center point of the gradient. (50% default)"
# r="the radius of the gradient. (50% default)"
# fx="the focus point of the gradient. (0% default)"
# fy="The focus point of the gradient. (0% default)"
# spreadMethod="'pad' or 'reflect' or 'repeat'"
# xlink:href="Reference to another gradient whose attribute values are used as defaults and stops included. Recursive"
####################################################################################################
class Rect(PositionMixin, RadiusMixin, SizeMixin, PathMixin, SvgElementMixin, XmlObjectAdaptator):
"""Defines a rectangle"""
__tag__ = 'rect'
##############################################
@property
def geometry(self):
# Fixme: width is str
width = float(self.width)
height = float(self.height)
# Fixme: which one ???
radius_x = self.rx
radius_y = self.ry
if radius_y == 0:
radius = None
else:
radius = radius_y
point = Vector2D(self.x, self.y)
path = Path2D(point)
path.horizontal_to(width)
path.vertical_to(height, radius=radius)
path.horizontal_to(-width, radius=radius)
path.close(radius=radius, close_radius=radius)
return path
####################################################################################################
class Stop(XmlObjectAdaptator):
"""The stops for a gradient"""
__tag__ = 'stop'
# offset="the offset for this stop (0 to 1/0% to 100%)". Required.
# stop-color="the color of this stop"
# stop-opacity="the opacity of this stop (0 to 1)"
####################################################################################################
class Style(TextXmlObjectAdaptator):
"""Defines style"""
__tag__ = 'style'
####################################################################################################
class Text(PositionMixin, DeltaMixin, FontMixin, ColorMixin, SvgElementMixin, TextXmlObjectAdaptator):
"""Defines a text"""
__tag__ = 'text'
# x="a list of x-axis positions. The nth x-axis position is given to the nth character in the text. If there are additional characters after the positions run out they are placed after the last character. 0 is default"
# y="a list of y-axis positions. (see x). 0 is default"
# dx="a list of lengths which moves the characters relative to the absolute position of the last glyph drawn. (see x)"
# dy="a list of lengths which moves the characters relative to the absolute position of the last glyph drawn. (see x)"
# rotate="a list of rotations. The nth rotation is performed on the nth character. Additional characters are NOT given the last rotation value"
# textLength="a target length for the text that the SVG viewer will attempt to display the text between by adjusting the spacing and/or the glyphs. (default: The text's normal length)"
# lengthAdjust="tells the viewer what to adjust to try to accomplish rendering the text if the length is specified. The two values are 'spacing' and 'spacingAndGlyphs'"
__attributes__ = (
# Fixme: common ???
StringAttribute('_class', 'class', None),
StringAttribute('style'),
)
####################################################################################################
class TextRef(XmlObjectAdaptator):
"""References any element in the SVG document and reuse it"""
__tag__ = 'tref'
####################################################################################################
class TextSpan(Text, XmlObjectAdaptator):
"""Identical to the element but can be nested inside text tags and inside itself"""
__tag__ = 'textspan'
####################################################################################################
class Use(PositionMixin, SizeMixin, XmlObjectAdaptator):
"""Uses a URI to reference a , or other graphical element with a unique id attribute and
replicate it. The copy is only a reference to the original so only the original exists in the
document. Any change to the original affects all copies.
"""
__tag__ = 'use'
# x="the x-axis top-left corner of the cloned element"
# y="the y-axis top-left corner of the cloned element"
# width="the width of the cloned element"
# height="the height of the cloned element"
# xlink:href="a URI reference to the cloned element"
Patro-master-Patro/Patro/FileFormat/Svg/__init__.py 0000664 0000000 0000000 00000000000 13424433274 0022500 0 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/FileFormat/Valentina/ 0000775 0000000 0000000 00000000000 13424433274 0021563 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/FileFormat/Valentina/Measurement.py 0000664 0000000 0000000 00000011177 13424433274 0024431 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import logging
from Patro.Common.Xml.Objectivity import StringAttribute, XmlObjectAdaptator
from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.Measurement.ValentinaMeasurement import ValentinaMeasurements
from Patro.Measurement.PersonalData import Gender
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
#
#
#
# 0.3.3
# false
#
# cm
# 998
#
# ..
# ...
# ...
# ...
#
#
#
#
#
#
####################################################################################################
class XmlMeasurement(XmlObjectAdaptator):
__tag__ = 'm'
__attributes__ = (
StringAttribute('name'),
StringAttribute('value'),
StringAttribute('full_name', default=''),
StringAttribute('description', default=''),
)
####################################################################################################
class VitFileInternal(XmlFileMixin):
_logger = _module_logger.getChild('VitFile')
##############################################
def __init__(self, path):
super().__init__(path)
self.measurements = ValentinaMeasurements()
self.read()
##############################################
def read(self):
self._logger.info('Load measurements from ' + str(self._path))
tree = self.parse()
measurements = self.measurements
version = self.get_xpath_element(tree, 'version').text
# self.read_only = self.get_xpath_element(tree, 'read-only').text
# self.notes = self.get_xpath_element(tree, 'notes').text
self.unit = self.get_xpath_element(tree, 'unit').text
self.pattern_making_system = self.get_xpath_element(tree, 'pm_system').text
personal = measurements.personal
personal_element = self.get_xpath_element(tree, 'personal')
personal.last_name = self.get_xpath_element(personal_element, 'family-name').text
personal.first_name = self.get_xpath_element(personal_element, 'given-name').text
personal.birth_date = self.get_xpath_element(personal_element, 'birth-date').text
personal.gender = Gender[self.get_xpath_element(personal_element, 'gender').text.upper()]
personal.email = self.get_xpath_element(personal_element, 'email').text
elements = self.get_xpath_element(tree, 'body-measurements')
for element in elements:
if element.tag == XmlMeasurement.__tag__:
xml_measurement = XmlMeasurement(element)
measurements.add(**xml_measurement.to_dict())
else:
raise NotImplementedError
####################################################################################################
class VitFile:
##############################################
def __init__(self, path):
self._interval = VitFileInternal(path)
##############################################
@property
def measurements(self):
return self._interval.measurements
Patro-master-Patro/Patro/FileFormat/Valentina/Pattern.py 0000664 0000000 0000000 00000033002 13424433274 0023550 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This module implements the Valentina val XML file format.
"""
####################################################################################################
import logging
from pathlib import Path
from lxml import etree
from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.Pattern.Pattern import Pattern
from .Measurement import VitFile
from .VitFormat import (
Point,
Line,
Spline,
ModelingPoint,
ModelingSpline,
Detail,
DetailData,
DetailPatternInfo,
DetailGrainline,
DetailNode,
)
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
# Last valentina version supported
VAL_VERSION = '0.7.10'
####################################################################################################
class Modeling:
"""Class to implement a modeling mapper."""
##############################################
def __init__(self):
self._id_map = {}
##############################################
def __getitem__(self, id):
return self._id_map[id]
##############################################
def add(self, item):
self._id_map[item.id] = item
####################################################################################################
class Dispatcher:
"""Baseclass to dispatch XML to Python class."""
__TAGS__ = {}
##############################################
def from_xml(self, element):
tag_cls = self.__TAGS__[element.tag]
if tag_cls is not None:
return tag_cls(element)
else:
raise NotImplementedError
####################################################################################################
class CalculationDispatcher(Dispatcher):
"""Class to implement a dispatcher for calculations."""
_logger = _module_logger.getChild('CalculationDispatcher')
__TAGS__ = {
'arc': None,
'ellipse': None,
'line': Line,
'operation': None,
'point': Point,
'spline': Spline,
}
##############################################
def __init__(self):
# Fixme: could be done in class definition
self._mapping = {} # used for Calculation -> XML
self._init_mapper()
##############################################
def _register_mapping(self, xml_cls):
operation_cls = xml_cls.__operation__
if operation_cls:
self._mapping[xml_cls] = operation_cls
self._mapping[operation_cls] = xml_cls
##############################################
def _init_mapper(self):
for tag_cls in self.__TAGS__.values():
if tag_cls is not None:
if hasattr(tag_cls, '__TYPES__'):
for xml_cls in tag_cls.__TYPES__.values():
if xml_cls is not None:
self._register_mapping(xml_cls)
else:
self._register_mapping(tag_cls)
##############################################
def from_xml(self, element):
tag_cls = self.__TAGS__[element.tag]
if hasattr(tag_cls, '__TYPES__'):
cls = tag_cls.__TYPES__[element.attrib['type']]
else:
cls = tag_cls
if cls is not None:
return cls(element)
else:
raise NotImplementedError
##############################################
def from_operation(self, operation):
return self._mapping[operation.__class__].from_operation(operation)
####################################################################################################
class ModelingDispatcher(Dispatcher):
"""Class to implement a dispatcher for modeling."""
__TAGS__ = {
'point': ModelingPoint,
'spline': ModelingSpline,
}
####################################################################################################
class DetailDispatcher(Dispatcher):
"""Class to implement a dispatcher for detail."""
__TAGS__ = {
'grainline': DetailGrainline,
'patternInfo': DetailPatternInfo,
'data': DetailData,
}
####################################################################################################
_calculation_dispatcher = CalculationDispatcher()
_modeling_dispatcher = ModelingDispatcher()
_detail_dispatcher = DetailDispatcher()
####################################################################################################
class ValFileReaderInternal(XmlFileMixin):
"""Class to read val file."""
_logger = _module_logger.getChild('ValFileReader')
##############################################
def __init__(self, path):
XmlFileMixin.__init__(self, path)
self.root = None
self.attribute = {}
self.vit_file = None
self.pattern = None
self.read()
##############################################
@property
def measurements(self):
if self.vit_file is not None:
return self.vit_file.measurements
else:
return None
##############################################
def read(self):
#
#
#
# 0.7.10
# cm
#
#
# pattern name
# pattern number
# company/Designer name
#
#
#
#
# measurements.vit
#
#
#
#
#
#
#
#
#
self._logger.info('Read Valentina file "{}"'.format(self.path))
self.root = self.parse()
self.read_attributes()
self.read_measurements()
# patternLabel
# patternMaterials
# increments
# previewCalculations
self.pattern = Pattern(self.measurements, self.attribute['unit'])
for piece in self.get_xpath_elements(self.root, 'draw'):
self.read_piece(piece)
##############################################
def read_measurements(self):
measurements_path = self.get_xpath_element(self.root, 'measurements').text
if measurements_path is not None:
measurements_path = Path(measurements_path)
if not measurements_path.exists():
measurements_path = self.path.parent.joinpath(measurements_path)
if not measurements_path.exists():
raise NameError("Cannot find {}".format(measurements_path))
self.vit_file = VitFile(measurements_path)
else:
self.vit_file = None
##############################################
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))
##############################################
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)
sketch = scope.sketch
for element in self.get_xpath_element(piece, 'calculation'):
try:
xml_calculation = _calculation_dispatcher.from_xml(element)
operation = xml_calculation.to_operation(sketch)
self._logger.info('Add operation {}'.format(operation))
except NotImplementedError:
self._logger.warning('Not implemented calculation\n' + str(etree.tostring(element)))
sketch.eval()
modeling = Modeling()
for element in self.get_xpath_element(piece, 'modeling'):
xml_modeling_item = _modeling_dispatcher.from_xml(element)
modeling.add(xml_modeling_item)
self._logger.info('Modeling {}'.format(xml_modeling_item))
# details = []
for detail_element in self.get_xpath_element(piece, 'details'):
self.read_detail(scope, modeling, detail_element)
# details.append(xml_detail)
##############################################
def read_detail(self, scope, modeling, detail_element):
xml_detail = Detail(modeling, detail_element)
self._logger.info('Detail {}'.format(xml_detail))
for element in detail_element:
if element.tag == 'nodes':
for node in element:
xml_node = DetailNode(node)
xml_detail.append_node(xml_node)
else:
xml_modeling_item = _detail_dispatcher.from_xml(element)
# Fixme: xml_detail. = xml_modeling_item
print(xml_modeling_item)
for node, modeling_item in xml_detail.iter_on_nodes():
# print(node.object_id, '->', modeling_item, '->', modeling_item.object_id)
print(node, '->\n', modeling_item, '->\n', scope.sketch.get_operation(modeling_item.object_id))
####################################################################################################
class ValFileReader:
"""Class to read val file."""
##############################################
def __init__(self, path):
self._internal = ValFileReaderInternal(path)
##############################################
@property
def measurements(self):
return self._internal.measurements
@property
def pattern(self):
return self._internal.pattern
####################################################################################################
class ValFileWriter:
"""Class to write val file."""
_logger = _module_logger.getChild('ValFileWriter')
##############################################
def __init__(self, path, vit_file, pattern):
self._path = str(path)
self._vit_file = vit_file
self._pattern = pattern
root = self._build_xml_tree()
self._write(root)
##############################################
def _build_xml_tree(self):
root = etree.Element('pattern')
root.append(etree.Comment('Pattern created with Patro (https://github.com/FabriceSalvaire/Patro)'))
etree.SubElement(root, 'version').text = self.VAL_VERSION
etree.SubElement(root, 'unit').text = self._pattern.unit
etree.SubElement(root, 'author')
etree.SubElement(root, 'description')
etree.SubElement(root, 'notes')
measurements = etree.SubElement(root, 'measurements')
if self._vit_file is not None:
measurements.text = str(self._vit_file.path)
etree.SubElement(root, 'increments')
for scope in self._pattern.scopes:
draw_element = etree.SubElement(root, 'draw')
draw_element.attrib['name'] = scope.name
calculation_element = etree.SubElement(draw_element, 'calculation')
modeling_element = etree.SubElement(draw_element, 'modeling')
details_element = etree.SubElement(draw_element, 'details')
# group_element = etree.SubElement(draw_element, 'groups')
for operation in scope.sketch.operations:
xml_calculation = _calculation_dispatcher.from_operation(operation)
# print(xml_calculation)
# print(xml_calculation.to_xml_string())
calculation_element.append(xml_calculation.to_xml())
return root
##############################################
def _write(self, root):
with open(self._path, 'wb') as fh:
# ElementTree.write() ?
fh.write(etree.tostring(root, pretty_print=True))
Patro-master-Patro/Patro/FileFormat/Valentina/VitFormat.py 0000664 0000000 0000000 00000064336 13424433274 0024064 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This module implements the val XML file format and is designed so as to decouple the XML format
and the calculation API.
The purpose of each XmlObjectAdaptator sub-classes is to serve as a bidirectional adaptor between
the XML format and the API.
Valentina File Format Concept
* all entities which are referenced later in the file are identified by a unique positive integer
over the file, usually incremented from 1.
* a file contains one or several "pieces"
* pieces correspond to independent scopes, one cannot access calculations of another piece
* pieces share the same referential, usually the root point of a piece is placed next
to the previous piece
* a piece has "calculations" and "details"
* a calculations corresponds to a point, a segment, or a Bézier curve ...
* a detail corresponds to a garment piece defined by segments and curves
* one can define several details within a piece
"""
####################################################################################################
__all__ = [
'Point',
'Line',
'Spline',
'ModelingPoint',
'ModelingSpline',
'Detail',
'DetailData',
'DetailPatternInfo',
'DetailGrainline',
'DetailNode',
]
####################################################################################################
import Patro.Pattern.SketchOperation as SketchOperation
from Patro.Common.Xml.Objectivity import (
Attribute,
BoolAttribute,
IntAttribute, FloatAttribute,
StringAttribute,
XmlObjectAdaptator
)
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicStyle import Colors, StrokeStyle
####################################################################################################
class ColorAttribute(Attribute):
__COLORS__ = (
'black',
'blue',
'cornflowerblue',
'darkBlue',
'darkGreen',
'darkRed',
'darkviolet',
'deeppink',
'deepskyblue',
'goldenrod',
'green',
'lightsalmon',
'lime',
'mediumseagreen',
'orange',
'violet',
'yellow',
)
##############################################
@classmethod
def from_xml(cls, value):
return Colors.ensure_color(value)
####################################################################################################
class StrokeStyleAttribute(Attribute):
__STROKE_STYLE__ = {
'dashDotDotLine': StrokeStyle.DashDotDotLine,
'dashDotLine': StrokeStyle.DashDotLine,
'dashLine': StrokeStyle.DashLine,
'dotLine': StrokeStyle.DotLine,
'hair': StrokeStyle.SolidLine, # should be solid
'none': StrokeStyle.NoPen,
}
##############################################
@classmethod
def from_xml(cls, value):
return cls.__STROKE_STYLE__[value]
####################################################################################################
class ValentinaBuiltInVariables:
# defined in libs/ifc/ifcdef.cpp
current_length = 'CurrentLength'
current_seam_allowance = 'CurrentSeamAllowance'
angle_line = 'AngleLine_'
increment = 'Increment_'
line = 'Line_'
measurement = 'M_'
seg = 'Seg_'
arc = 'ARC_'
elarc = 'ELARC_'
spl = 'SPL_'
angle1 = 'Angle1'
angle2 = 'Angle2'
c1_length = 'C1Length'
c2_length = 'C2Length'
radius = 'Radius'
rotation = 'Rotation'
spl_path = 'SplPath'
angle1_arc = angle1 + arc
angle1_elarc = angle1 + elarc
angle1_spl_path = angle1 + spl_path
angle1_spl = angle1 + spl
angle2_arc = angle2 + arc
angle2_elarc = angle2 + elarc
angle2_spl_path = angle2 + spl_path
angle2_spl = angle2 + spl
c1_length_spl_path = c1_length + spl_path
c1_length_spl = c1_length + spl
c2_length_spl_path = c2_length + spl_path
c2_length_spl = c2_length + spl
radius1_elarc = radius + '1' + elarc
radius2_elarc = radius + '2' + elarc
radius_arc = radius + arc
rotation_elarc = rotation + elarc
####################################################################################################
VALENTINA_ATTRIBUTES = (
'aScale',
'angle',
'angle1',
'angle2',
'arc',
'axisP1',
'axisP2',
'axisType',
'baseLineP1',
'baseLineP2',
'basePoint',
'c1Center',
'c1Radius',
'c2Center',
'c2Radius',
'cCenter',
'cRadius',
'center',
'closed',
'color',
'crossPoint',
'curve',
'curve1',
'curve2',
'cut',
'dartP1',
'dartP2',
'dartP3',
'duplicate',
'firstArc',
'firstPoint',
'firstToCountour',
'forbidFlipping',
'forceFlipping',
'hCrossPoint',
'height',
'idObject',
'inLayout',
'kAsm1',
'kAsm2',
'kCurve',
'lastToCountour',
'length',
'length1',
'length2',
'lineColor',
'mx',
'mx1',
'mx2',
'my',
'my1',
'my2',
'name',
'name1',
'name2',
'p1Line',
'p1Line1',
'p1Line2',
'p2Line',
'p2Line1',
'p2Line2',
'pShoulder',
'pSpline',
'pathPoint',
'penStyle',
'placeLabelType',
'point1',
'point2',
'point3',
'point4',
'radius',
'radius1',
'radius2',
'rotationAngle',
'secondArc',
'secondPoint',
'showLabel',
'showLabel1',
'showLabel2',
'suffix',
'tangent',
'thirdPoint',
'type',
'typeLine',
'vCrossPoint',
'version',
'width',
'x',
'y',
)
####################################################################################################
class MxMyMixin:
__attributes__ = (
FloatAttribute('mx'),
FloatAttribute('my'),
)
####################################################################################################
class CalculationMixin:
__attributes__ = (
IntAttribute('id'),
)
__operation__ = None # operation's class
##############################################
def call_operation_function(self, sketch, kwargs):
# Fixme: map valentina name -> ...
method = getattr(sketch, self.__operation__.__name__)
return method(**kwargs)
##############################################
def to_operation(self, sketch):
raise NotImplementedError
##############################################
@classmethod
def from_operation(operation):
raise NotImplementedError
####################################################################################################
class CalculationTypeMixin(CalculationMixin):
##############################################
def to_xml(self):
return XmlObjectAdaptator.to_xml(self, type=self.__type__)
####################################################################################################
class LinePropertiesMixin:
__attributes__ = (
ColorAttribute('line_color', 'lineColor'),
StrokeStyleAttribute('line_style', 'typeLine'),
)
class XyMixin:
__attributes__ = (
StringAttribute('x'),
StringAttribute('y'),
)
class FirstSecondPointMixin:
__attributes__ = (
IntAttribute('first_point', 'firstPoint'),
IntAttribute('second_point', 'secondPoint'),
)
class FirstSecondThirdPointMixin(FirstSecondPointMixin):
__attributes__ = (
IntAttribute('third_point', 'thirdPoint'),
)
class BasePointMixin:
__attributes__ = (
IntAttribute('base_point', 'basePoint'),
)
class Line1Mixin:
__attributes__ = (
IntAttribute('point1_line1', 'p1Line1'),
IntAttribute('point2_line1', 'p2Line1'),
)
class Line2Mixin:
__attributes__ = (
IntAttribute('point1_line2', 'p1Line2'),
IntAttribute('point2_line2', 'p2Line2'),
)
class Line12Mixin(Line1Mixin, Line2Mixin):
pass
class LengthMixin:
__attributes__ = (
StringAttribute('length'),
)
class AngleMixin:
__attributes__ = (
StringAttribute('angle'),
)
class LengthAngleMixin(LengthMixin, AngleMixin):
pass
####################################################################################################
class PointMixin(CalculationTypeMixin, MxMyMixin):
__tag__ = 'point'
__attributes__ = (
StringAttribute('name'),
)
##############################################
def to_operation(self, sketch):
kwargs = self.to_dict(exclude=('mx', 'my')) # id'
kwargs['label_offset'] = Vector2D(self.mx, self.my)
return self.call_operation_function(sketch, kwargs)
##############################################
@classmethod
def from_operation(cls, operation):
kwargs = cls.get_dict(operation, exclude=('mx', 'my'))
label_offset = operation.label_offset
kwargs['mx'] = label_offset.x
kwargs['my'] = label_offset.y
return cls(**kwargs)
####################################################################################################
class PointLinePropertiesMixin(PointMixin, LinePropertiesMixin):
pass
####################################################################################################
class AlongLinePoint(PointLinePropertiesMixin, FirstSecondPointMixin, LengthMixin, XmlObjectAdaptator):
#
__type__ = 'alongLine'
__operation__ = SketchOperation.AlongLinePoint
####################################################################################################
class BissectorPoint(PointLinePropertiesMixin, FirstSecondThirdPointMixin, LengthMixin, XmlObjectAdaptator):
#
__type__ = 'bisector'
# __operation__ = SketchOperation.BissectorPoint
####################################################################################################
# __type__ = 'curveIntersectAxis'
#
# __type__ = 'cutArc'
#
# __type__ = 'cutSpline'
#
# __type__ = 'cutSplinePath'
#
####################################################################################################
class EndLinePoint(PointLinePropertiesMixin, BasePointMixin, LengthAngleMixin, XmlObjectAdaptator):
#
__type__ = 'endLine'
__operation__ = SketchOperation.EndLinePoint
####################################################################################################
class HeightPoint(PointLinePropertiesMixin, BasePointMixin, Line1Mixin, XmlObjectAdaptator):
#
__type__ = 'height'
# __operation__ = SketchOperation.HeightPoint
####################################################################################################
class LineIntersectPoint(PointMixin, Line12Mixin, XmlObjectAdaptator):
#
__type__ = 'lineIntersect'
__operation__ = SketchOperation.LineIntersectPoint
####################################################################################################
class LineIntersectAxisPoint(PointLinePropertiesMixin, BasePointMixin, Line1Mixin, AngleMixin, XmlObjectAdaptator):
#
__type__ = 'lineIntersectAxis'
# __operation__ = SketchOperation.LineIntersectAxisPoint
####################################################################################################
class NormalPoint(PointLinePropertiesMixin, FirstSecondPointMixin, LengthAngleMixin, XmlObjectAdaptator):
#
__type__ = 'normal'
__operation__ = SketchOperation.NormalPoint
####################################################################################################
# __type__ = 'pointFromArcAndTangent'
#
# __type__ = 'pointFromCircleAndTangent'
#
# __type__ = 'pointOfContact'
#
####################################################################################################
class PointOfIntersection(PointMixin, FirstSecondPointMixin, XmlObjectAdaptator):
#
__type__ = 'pointOfIntersection'
__operation__ = SketchOperation.PointOfIntersection
####################################################################################################
# __type__ = 'pointOfIntersectionArcs'
#
# __type__ = 'pointOfIntersectionCircles'
#
# __type__ = 'pointOfIntersectionCurves'
#
####################################################################################################
class ShoulderPoint(PointLinePropertiesMixin, Line1Mixin, LengthMixin, XmlObjectAdaptator):
#
__type__ = 'shoulder'
# __operation__ = SketchOperation.ShoulderPoint
__attributes__ = (
IntAttribute('shoulder_point', 'pShoulder'),
)
####################################################################################################
class SinglePoint(PointMixin, XyMixin, XmlObjectAdaptator):
#
__type__ = 'single'
__operation__ = SketchOperation.SinglePoint
####################################################################################################
# __type__ = 'triangle'
#
# __type__ = 'trueDarts'
#
####################################################################################################
class Point:
# We cannot use a metaclass to auto-register due to XmlObjectAdaptator (right ?)
__TYPES__ = {
'alongLine': AlongLinePoint,
'bisector': None,
'curveIntersectAxis': None,
'cutArc': None,
'cutSpline': None,
'cutSplinePath': None,
'endLine': EndLinePoint,
'height': None,
'lineIntersect': LineIntersectPoint,
'lineIntersectAxis': None,
'normal': NormalPoint,
'pointFromArcAndTangent': None,
'pointFromCircleAndTangent': None,
'pointOfContact': None,
'pointOfIntersection': PointOfIntersection,
'pointOfIntersectionArcs': None,
'pointOfIntersectionCircles': None,
'pointOfIntersectionCurves': None,
'shoulder': None,
'single': SinglePoint,
'triangle': None,
'trueDarts': None,
}
####################################################################################################
class Line(CalculationMixin, LinePropertiesMixin, FirstSecondPointMixin, XmlObjectAdaptator):
#
__tag__ = 'line'
__operation__ = SketchOperation.Line
##############################################
def to_operation(self, sketch):
return self.call_operation_function(sketch, self.to_dict()) # exclude=('id')
##############################################
@classmethod
def from_operation(cls, operation):
kwargs = cls.get_dict(operation)
return cls(**kwargs)
####################################################################################################
class SplineMixin(CalculationTypeMixin):
__tag__ = 'spline'
####################################################################################################
class SimpleInteractiveSpline(SplineMixin, XmlObjectAdaptator):
#
__type__ = 'simpleInteractive'
__attributes__ = (
IntAttribute('first_point', 'point1'),
IntAttribute('second_point', 'point4'),
StringAttribute('length1'),
StringAttribute('length2'),
StringAttribute('angle1'),
StringAttribute('angle2'),
StringAttribute('line_color', 'color'),
)
__operation__ = SketchOperation.SimpleInteractiveSpline
##############################################
def to_operation(self, sketch):
return self.call_operation_function(sketch, self.to_dict()) # exclude=('id')
##############################################
@classmethod
def from_operation(cls, operation):
kwargs = cls.get_dict(operation)
return cls(**kwargs)
####################################################################################################
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
####################################################################################################
class Spline:
# We cannot use a metaclass to auto-register due to XmlObjectAdaptator (right ?)
__TYPES__ = {
'cubicBezier': None,
'cubicBezierPath': None,
'pathInteractive': None,
'simpleInteractive': SimpleInteractiveSpline,
}
####################################################################################################
#
#
# __ARC_TYPE__ = (
# 'arcWithLength',
# 'simple',
# )
#
# __ELLIPSE_TYPE__ = (
# 'simple',
# )
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
# __OPERATION_TYPE__ = (
# 'flippingByAxis',
# 'flippingByLine',
# 'moving',
# 'rotation',
# )
####################################################################################################
class ModelingItemMixin:
__attributes__ = (
IntAttribute('id'),
IntAttribute('object_id', 'idObject'),
StringAttribute('type'),
BoolAttribute('in_use', 'inUse'),
)
###################################################################################################
class ModelingPoint(ModelingItemMixin, MxMyMixin, XmlObjectAdaptator):
#
pass
####################################################################################################
class ModelingSpline(ModelingItemMixin, XmlObjectAdaptator):
#
pass
####################################################################################################
class Detail(MxMyMixin, XmlObjectAdaptator):
#
__attributes__ = (
IntAttribute('id'),
IntAttribute('version'),
BoolAttribute('forbidFlipping'),
IntAttribute('width'),
BoolAttribute('united'),
StringAttribute('name'),
BoolAttribute('inLayout'),
BoolAttribute('seamAllowance'),
)
##############################################
def __init__(self, modeling, *args, **kwargs):
XmlObjectAdaptator.__init__(self, *args, **kwargs)
self._modeling = modeling
self._nodes = []
##############################################
def append_node(self, node):
self._nodes.append(node)
##############################################
def iter_on_nodes(self):
for node in self._nodes:
yield node, self._modeling[node.object_id]
####################################################################################################
class VisibleRotationMixin:
__attributes__ = (
BoolAttribute('visible'),
IntAttribute('rotation'),
)
####################################################################################################
class HeightWidthMixin:
__attributes__ = (
IntAttribute('height'),
IntAttribute('width'),
)
####################################################################################################
class FontSizeMixin:
__attributes__ = (
IntAttribute('fontSize'),
)
####################################################################################################
class DetailData(HeightWidthMixin, MxMyMixin, FontSizeMixin, VisibleRotationMixin, XmlObjectAdaptator):
#
__attributes__ = (
StringAttribute('letter'),
)
####################################################################################################
class DetailPatternInfo(HeightWidthMixin, MxMyMixin, FontSizeMixin, VisibleRotationMixin, XmlObjectAdaptator):
#
pass
####################################################################################################
class DetailGrainline(MxMyMixin, VisibleRotationMixin, XmlObjectAdaptator):
#
__attributes__ = (
IntAttribute('arrows'),
IntAttribute('length'),
)
####################################################################################################
class DetailNode(XmlObjectAdaptator):
#
#
__attributes__ = (
IntAttribute('object_id', 'idObject'),
StringAttribute('type'),
BoolAttribute('reverse'),
)
Patro-master-Patro/Patro/FileFormat/Valentina/__init__.py 0000664 0000000 0000000 00000002207 13424433274 0023675 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements reader and writer for .val and .vit Valentina file formats.
It is designed so as to hide Valentina file format implementation details and to act as a bridge
with the Pattern API.
"""
Patro-master-Patro/Patro/FileFormat/__init__.py 0000664 0000000 0000000 00000000000 13424433274 0021741 0 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/GeometryEngine/ 0000775 0000000 0000000 00000000000 13424433274 0020533 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/GeometryEngine/Bezier.py 0000664 0000000 0000000 00000074020 13424433274 0022330 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
r"""Module to implement Bézier curve.
For resources on Bézier curve see :ref:`this section `.
"""
####################################################################################################
#
# Notes: algorithm details are on bezier.rst
#
####################################################################################################
# Fixme:
# max distance to the chord for linear approximation
# fitting
# 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
####################################################################################################
__all__ = [
'QuadraticBezier2D',
'CubicBezier2D',
]
####################################################################################################
import logging
from math import log, sqrt
import numpy as np
from Patro.Common.Math.Root import quadratic_root, cubic_root, fifth_root
from .Interpolation import interpolate_two_points
from .Line import Line2D
from .Primitive import Primitive3P, Primitive4P, PrimitiveNP, Primitive2DMixin
from .Transformation import AffineTransformation
from .Vector import Vector2D
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class BezierMixin2D(Primitive2DMixin):
"""Mixin to implements 2D Bezier Curve."""
LineInterpolationPrecision = 0.05
_logger = _module_logger.getChild('BezierMixin2D')
##############################################
def interpolated_length(self, dt=None):
"""Length of the curve obtained via line interpolation"""
if dt is None:
dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude
length = 0
t = 0
while t < 1:
t0 = t
t = min(t + dt, 1)
length += (self.point_at_t(t) - self.point_at_t(t0)).magnitude
return length
##############################################
def length_at_t(self, t, cache=False):
"""Compute the length of the curve at *t*."""
if cache: # lookup cache
if not hasattr(self, '_length_cache'):
self._length_cache = {}
length = self._length_cache.get(t, None)
if length is not None:
return length
length = self.split_at_t(t).length
if cache: # save
self._length_cache[t] = length
return length
##############################################
def t_at_length(self, length, precision=1e-6):
"""Compute t for the given length. Length must lie in [0, curve length] range]. """
if length < 0:
raise ValueError('Negative length')
if length == 0:
return 0
curve_length = self.length # Fixme: cache ?
if (curve_length - length) <= precision:
return 1
if length > curve_length:
raise ValueError('Out of length')
# Search t for length using dichotomy
# convergence rate :
# 10 iterations corresponds to curve length / 1024
# 16 / 65536
# start range
inf = 0
sup = 1
while True:
middle = (sup + inf) / 2
length_at_middle = self.length_at_t(middle, cache=True) # Fixme: out of memory, use LRU ???
# exit condition
if abs(length_at_middle - length) <= precision:
return middle
elif length_at_middle < length:
inf = middle
else: # length < length_at_middle
sup = middle
##############################################
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."""
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),
))
_logger = _module_logger.getChild('QuadraticBezier2D')
##############################################
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):
r"""Compute the length of the curve.
For more details see :ref:`this section `.
"""
A0 = self._p1 - self._p0
A1 = self._p0 - self._p1 * 2 + self._p2
if A1.magnitude_square != 0:
c = 4 * A1.dot(A1)
b = 8 * A0.dot(A1)
a = 4 * A0.dot(A0)
q = 4 * a * c - b * b
two_cb = 2 * c + b
sum_cba = c + b + a
m0 = 0.25 / c
m1 = q / (8 * c**1.5)
return (m0 * (two_cb * sqrt(sum_cba) - b * sqrt(a)) +
m1 * (log(2 * sqrt(c * sum_cba) + two_cb) - log(2 * sqrt(c * a) + b)))
else:
return 2 * A0.magnitude
##############################################
def point_at_t(self, t):
# if 0 < t or 1 < t:
# raise ValueError()
u = 1 - t
return self._p0 * u**2 + self._p1 * 2 * t * u + self._p2 * t**2
##############################################
def split_at_t(self, t):
"""Split the curve at given position"""
if t <= 0:
return None, self
elif t >= 1:
return self, None
else:
p01 = interpolate_two_points(self._p0, self._p1, t)
p12 = interpolate_two_points(self._p1, self._p2, t)
p = interpolate_two_points(p01, p12, t) # p = p012
# p = self.point_at_t(t)
return (QuadraticBezier2D(self._p0, p01, p), QuadraticBezier2D(p, p12, self._p2))
##############################################
@property
def tangent0(self):
return (self._p1 - self._p0).normalise()
##############################################
@property
def tangent1(self):
return (self._p2 - self._p1).normalise()
##############################################
@property
def normal0(self):
return self.tangent0.normal()
##############################################
@property
def tangent1(self):
return self.tangent1.normal()
##############################################
def tangent_at(self, t):
u = 1 - t
return (self._p1 - self._p0) * u + (self._p2 - self._p1) * t
##############################################
def intersect_line(self, line):
"""Find the intersections of the curve with a line.
For more details see :ref:`this section `.
"""
curve = self._map_to_line(line)
p0 = curve.p0.y
p1 = curve.p1.y
p2 = curve.p2.y
return quadratic_root(
p2 - 2*p1 + p0, # t**2
2*(p1 - p0), # t
p0,
)
### a = p0 - 2*p1 + p2 # t**2
### # b = 2*(-p0 + p1) # t
### b = -p0 + p1 # was / 2 !!!
### c = p0
###
### # discriminant = b**2 - 4*a*c
### # discriminant = 4 * (p1**2 - p0*p2)
### discriminant = p1**2 - p0*p2 # was / 4 !!!
###
### if discriminant < 0:
### return None
### elif discriminant == 0:
### return -b / a # dropped 2
### else:
### # dropped 2
### y1 = (-b - sqrt(discriminant)) / a
### y2 = (-b + sqrt(discriminant)) / a
### return y1, y2
##############################################
def fat_line(self):
line = Line2D.from_two_points(self._p0, self._p3)
d1 = line.distance_to_line(self._p1)
d_min = min(0, d1 / 2)
d_max = max(0, d1 / 2)
return (line, d_min, d_max)
##############################################
def closest_point(self, point):
"""Return the closest point on the curve to the given *point*.
For more details see :ref:`this section `.
"""
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:
self._logger.warning("Found more than one root {} for {} and point {}".format(t, self, point))
return None
else:
return self.point_at_t(t)
##############################################
def to_cubic(self):
r"""Elevate the quadratic Bézier curve to a cubic Bézier cubic with the same shape.
For more details see :ref:`this section `.
"""
p1 = (self._p0 + self._p1 * 2) / 3
p2 = (self._p2 + self._p1 * 2) / 3
return CubicBezier2D(self._p0, p1, p2, self._p2)
####################################################################################################
_Sqrt3 = sqrt(3)
_Div18Sqrt3 = 18 / _Sqrt3
_OneThird = 1 / 3
_Sqrt3Div36 = _Sqrt3 / 36
class CubicBezier2D(BezierMixin2D, Primitive4P):
"""Class to implements 2D Cubic Bezier Curve."""
InterpolationPrecision = 0.001
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),
))
_logger = _module_logger.getChild('CubicMixin2D')
#######################################
def __init__(self, p0, p1, p2, p3):
Primitive4P.__init__(self, p0, p1, p2, p3)
##############################################
def __repr__(self):
return self.__class__.__name__ + '({0._p0}, {0._p1}, {0._p2}, {0._p3})'.format(self)
##############################################
def to_spline(self):
from .Spline import CubicUniformSpline2D
basis = np.dot(self.BASIS, CubicUniformSpline2D.INVERSE_BASIS)
points = np.dot(self.point_array, basis).transpose()
return CubicUniformSpline2D(*points)
##############################################
@property
def length(self):
return self.adaptive_length_approximation()
##############################################
def point_at_t(self, t):
# if 0 < t or 1 < t:
# raise ValueError()
return (self._p0 +
(self._p1 - self._p0) * 3 * t +
(self._p2 - self._p1*2 + self._p0) * 3 * t**2 +
(self._p3 - self._p2*3 + self._p1*3 - self._p0) * t**3)
# interpolate = point_at_t
##############################################
def _q_point(self):
"""Return the control point for mid-point quadratic approximation"""
return (self._p2*3 - self._p3 + self._p1*3 - self._p0) / 4
##############################################
def mid_point_quadratic_approximation(self):
"""Return the mid-point quadratic approximation"""
p1 = self._q_point()
return QuadraticBezier2D(self._p0, p1, self._p3)
##############################################
def split_at_t(self, t):
"""Split the curve at given position"""
p01 = interpolate_two_points(self._p0, self._p1, t)
p12 = interpolate_two_points(self._p1, self._p2, t)
p23 = interpolate_two_points(self._p2, self._p3, t)
p012 = interpolate_two_points(p01, p12, t)
p123 = interpolate_two_points(p12, p23, t)
p = interpolate_two_points(p012, p123, t) # p = p0123
# p = self.point_at_t(t)
return (CubicBezier2D(self._p0, p01, p012, p), CubicBezier2D(p, p123, p23, self._p3))
##############################################
def _d01(self):
"""Return the distance between 0 and 1 quadratic aproximations"""
return (self._p3 - self._p2 * 3 + self._p1 * 3 - self._p0).magnitude / 2
##############################################
def _t_max(self):
"""Return the split point for adaptive quadratic approximation"""
return (_Div18Sqrt3 * self.InterpolationPrecision / self._d01())**_OneThird
##############################################
def q_length(self):
"""Return the length of the mid-point quadratic approximation"""
return self.mid_point_quadratic_approximation().length
##############################################
def adaptive_length_approximation(self):
"""Return the length of the adaptive quadratic approximation"""
segments = []
segment = self
t_max = segment._t_max()
while t_max < 1:
split = segment.split_at_t(t_max)
segments.append(split[0])
segment = split[1]
t_max = segment._t_max()
segments.append(segment)
return sum([segment.q_length() for segment in segments])
##############################################
@property
def tangent1(self):
return (self._p3 - self._p2).normalise()
##############################################
def tangent_at(self, t):
u = 1 - t
return (self._p1 - self._p0) * u**2 + (self._p2 - self._p1) * 2 * t * u + (self._p3 - self._p2) * t**2
##############################################
def intersect_line(self, line):
"""Find the intersections of the curve with a line."""
# Algorithm: same as for quadratic
# u = 1 - t
# B = p0 * u**3 +
# p1 * 3 * u**2 * t +
# p2 * 3 * u * t**2 +
# p3 * t**3
# B = p0 +
# (p1 - p0) * 3 * t +
# (p2 - p1 * 2 + p0) * 3 * t**2 +
# (p3 - p2 * 3 + p1 * 3 - p0) * t**3
# solveset(B, t)
curve = self._map_to_line(line)
p0 = curve.p0.y
p1 = curve.p1.y
p2 = curve.p2.y
p3 = curve.p3.y
return cubic_root(
p3 - 3*p2 + 3*p1 - p0,
3 * (p2 - p1 * 2 + p0),
3 * (p1 - p0),
p0,
)
##############################################
def fat_line(self):
line = Line2D.from_two_points(self._p0, self._p3)
d1 = line.distance_to_line(self._p1)
d2 = line.distance_to_line(self._p2)
if d1*d2 > 0:
factor = 3 / 4
else:
factor = 4 / 9
d_min = factor * min(0, d1, d2)
d_max = factor * max(0, d1, d2)
return (line, d_min, d_max)
##############################################
def _clipping_convex_hull(self):
line_03 = Line2D(self._p0, self._p3)
d1 = line_03.distance_to_line(self._p1)
d2 = line_03.distance_to_line(self._p2)
# Check if p1 and p2 are on the same side of the line [p0, p3]
if d1 * d2 < 0:
# p1 and p2 lie on different sides of [p0, p3].
# The hull is a quadrilateral and line [p0, p3] is not part of the hull.
# The top part includes p1, we will reverse it later if that is not the case.
hull = [
[self._p0, self._p1, self._p3], # top part
[self._p0, self._p2, self._p3] # bottom part
]
flip = d1 < 0
else:
# p1 and p2 lie on the same sides of [p0, p3]. The hull can be a triangle or a
# quadrilateral and line [p0, p3] is part of the hull. Check if the hull is a triangle
# or a quadrilateral. Also, if at least one of the distances for p1 or p2, from line
# [p0, p3] is zero then hull must at most have 3 vertices.
# Fixme: check cross product
y0, y1, y2, y3 = [p.y for p in self.points]
if abs(d1) < abs(d2):
pmax = p2;
# apex is y0 in this case, and the other apex point is y3
# vector yapex -> yapex2 or base vector which is already part of the hull
# V30xV10 * V10xV20
cross_product = ((y1 - y0) - (y3 - y0)/3) * (2*(y1 - y0) - (y2 - y0)) /3
else:
pmax = p1;
# apex is y3 and the other apex point is y0
# vector yapex -> yapex2 or base vector which is already part of the hull
# V32xV30 * V32xV31
cross_product = ((y3 - y2) - (y3 - y0)/3) * (2*(y3 - y2) - (y3 + y1)) /3
# Compare cross products of these vectors to determine if the point is in the triangle
# [p3, pmax, p0], or if it is a quadrilateral.
has_null_distance = d1 == 0 or d2 == 0 # Fixme: don't need to compute cross_product
if cross_product < 0 or has_null_distance:
# hull is a triangle
hull = [
[self._p0, pmax, self._p3], # top part is a triangle
[self._p0, self._p3], # bottom part is just an edge
]
else:
hull = [
[self._p0, self._p1, self._p2, self._p3], # top part is a quadrilateral
[self._p0, self._p3], # bottom part is just an edge
]
flip = d1 < 0 if d1 else d2 < 0
if flip:
hull.reverse()
return hull
##############################################
@staticmethod
def _clip_convex_hull(hull_top, hull_bottom, d_min, d_max) :
# Top /----
# / ---/
# / /
# d_max -------------------*---
# / / t_max
# t_min / /
# d_min -------*---------------
# / /
# / ----/ Bottom
# p0 /----
if (hull_top[0].y < d_min):
# Left of hull is below d_min,
# walk through the hull until it enters the region between d_min and d_max
return self._clip_convex_hull_part(hull_top, True, d_min);
elif (hull_bottom[0].y > d_max) :
# Left of hull is above d_max,
# walk through the hull until it enters the region between d_min and d_max
return self._clip_convex_hull_part(hull_bottom, False, d_max);
else :
# Left of hull is between d_min and d_max, no clipping possible
return hull_top[0].x; # Fixme: 0 ???
##############################################
@staticmethod
def _clip_convex_hull_part(part, top, threshold) :
"""Clip the bottom or top part of the convex hull.
*part* is a list of points, *top* is a boolean flag to indicate if it corresponds to the top
part, *threshold* is d_min if top part else d_max.
"""
# Walk on the edges
px = part[0].x;
py = part[0].y;
for i in range(1, len(part)):
qx = part[i].x;
qy = part[i].y;
if (qy >= threshold if top else qy <= threshold):
# compute a linear interpolation
# threshold = s * (t - px) + py
# t = (threshold - py) / s + px
return px + (threshold - py) * (qx - px) / (qy - py);
px = qx;
py = qy;
return None; # no intersection
##############################################
@staticmethod
def _instersect_curve(
curve1, curve2,
t_min=0, t_max=1,
u_min=0, u_max=1,
old_delta_t=1,
reverse=False, # flag to indicate that 1 <-> 2 when we store locations
recursion=0, # number of recursions
recursion_limit=32,
t_limit=0.8,
locations=[],
) :
"""Compute the intersection of two Bézier curves.
Code inspired from
* https://github.com/paperjs/paper.js/blob/master/src/path/Curve.js
* http://nbviewer.jupyter.org/gist/hkrish/0a128f21a5b9e5a7a914 The Bezier Clipping Algorithm
* https://gist.github.com/hkrish/5ef0f2da7f9882341ee5 hkrish/bezclip_manual.py
"""
# Note:
# see https://github.com/paperjs/paper.js/issues/565
# It was determined that more than 20 recursions are needed sometimes, depending on the
# delta_t threshold values further below when determining which curve converges the
# least. He also recommended a threshold of 0.5 instead of the initial 0.8
if recursion > recursion_limit:
return
tolerance = 1e-5
epsilon = 1e-10
# t_min_new = 0.
# t_max_new = 0.
# delta_t = 0.
# NOTE: the recursion threshold of 4 is needed to prevent this issue from occurring:
# https://github.com/paperjs/paper.js/issues/571
# when two curves share an end point
if curve1.p0.x == curve1.p3.x and u_max - u_min <= epsilon and recursion > 4:
# The fat-line of curve1 has converged to a point, the clipping is not reliable.
# Return the value we have even though we will miss the precision.
t_max_new = t_min_new = (t_max + t_min) / 2
delta_t = 0
else :
# Compute the fat-line for curve1:
# a baseline and two offsets which completely encloses the curve
fatline, d_min, d_max = curve1.fat_line()
# Calculate a non-parametric bezier curve D(ti, di(t)) where di(t) is the distance of curve2 from
# the baseline, ti is equally spaced in [0, 1]
non_parametric_curve = curve2.non_parametric_curve(fatline)
# Get the top and bottom parts of the convex-hull
top, bottom = non_parametric_curve._clip_convex_hull()
# Clip the convex-hull with d_min and d_max
t_min_clip = self.clip_convex_hull(top, bottom, d_min, d_max);
top.reverse()
bottom.reverse()
t_max_clip = clipConvexHull(top, bottom, d_min, d_max);
# No intersections if one of the t values is None
if t_min_clip is None or t_max_clip is None:
return
# Clip curve2 with the fat-line for curve1
curve2 = curve2.split_at_two_t(t_min_clip, t_max_clip)
delta_t = t_max_clip - t_min_clip
# t_min and t_max are within the range [0, 1]
# We need to project it to the original parameter range
t_min_new = t_max * t_min_clip + t_min * (1 - t_min_clip)
t_max_new = t_max * t_max_clip + t_min * (1 - t_max_clip)
delta_t_new = t_max_new - t_min_new
delta_u = u_max - u_min
# Check if we need to subdivide the curves
if old_delta_t > t_limit and delta_t > t_limit:
# Subdivide the curve which has converged the least.
args = (delta_t, not reverse, recursion+1, recursion_limit, t_limit, locations)
if delta_u < delta_t_new: # curve2 < curve1
parts = curve1.split_at_t(0.5)
t = t_min_new + delta_t_new / 2
self._intersect_curve(curve2, parts[0], u_min, u_max, t_min_new, t, *args)
self._intersect_curve(curve2, parts[1], u_min, u_max, t, t_max_new, *args)
else :
parts = curve2.split_at_t(0.5)
t = u_min + delta_u / 2
self._intersect_curve(parts[0], curve1, u_min, t, t_min_new, t_max_new, *args)
self._intersect_curve(parts[1], curve1, t, u_max, t_min_new, t_max_new, *args)
elif max(delta_u, delta_t_new) < tolerance:
# We have isolated the intersection with sufficient precision
t1 = t_min_new + delta_t_new / 2
t2 = u_min + delta_u / 2
if reverse:
t1, t2 = t2, t1
p1 = curve1.point_at_t(t1)
p2 = curve2.point_at_t(t2)
locations.append([t1, point1, t2, point2])
else:
args = (delta_t, not reverse, recursion+1, recursion_limit, t_limit)
self._intersect_curve(curve2, curve1, locations, u_min, u_max, t_min_new, t_max_new, *args)
##############################################
def is_flat_enough(self, flatness):
r"""Determines if a curve is sufficiently flat, meaning it appears as a straight line and has
curve-time that is enough linear, as specified by the given *flatness* parameter.
For more details see :ref:`this section `.
"""
u = 3*self._p1 - 2*self._p0 - self._p3
v = 3*self._p2 - 2*self._p3 - self._p0
criterion = max(u.x**2, v.x**2) + max(u.y**2, v.y**2)
threshold = 16 * flatness**2
self._logger.warning("is flat {} <= {} with flatness {}".format(criterion, threshold, flatness))
return criterion <= threshold
##############################################
@property
def area(self):
"""Compute the area delimited by the curve and the segment across the start and stop point."""
# Reference: http://objectmix.com/graphics/133553-area-closed-bezier-curve.html BUT DEAD LINK
# Proof using divergence theorem ???
# Fixme: any proof !
x0, y0 = list(self._p0)
x1, y1 = list(self._p1)
x2, y2 = list(self._p2)
x3, y3 = list(self._p3)
return (3 * ((y3 - y0) * (x1 + x2) - (x3 - x0) * (y1 + y2)
+ y1 * (x0 - x2) - x1 * (y0 - y2)
+ y3 * (x2 + x0 / 3) - x3 * (y2 + y0 / 3)) / 20)
##############################################
def closest_point(self, point):
"""Return the closest point on the curve to the given *point*.
For more details see :ref:`this section `.
"""
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:
# Fixme:
# Found more than one root [0, 0.516373783749732]
# for CubicBezier2D(
# Vector2D[1394.4334 1672.0004], Vector2D[1394.4334 1672.0004],
# Vector2D[1585.0004 1624.9634], Vector2D[1585.0004 1622.0004])
# and point Vector2D[1495.11502887 1649.7386517 ]
# raise NameError("Found more than one root: {}".format(t))
self._logger.warning("Found more than one root {} for {} and point {}".format(t, self, point))
# self._logger.warning("is flat {}".format(self.is_flat_enough(.1)))
if len(t) == 2 and t[0] == 0:
return self.point_at_t(t[1])
else:
return None
else:
return self.point_at_t(t[0])
Patro-master-Patro/Patro/GeometryEngine/BoundingBox.py 0000664 0000000 0000000 00000007217 13424433274 0023332 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to compute bounding box and convex hull for a set of points.
"""
####################################################################################################
__all__ = [
'bounding_box_from_points',
'convex_hull',
]
####################################################################################################
import functools
####################################################################################################
def bounding_box_from_points(points):
"""Return the bounding box of the list of points."""
bounding_box = None
for point in points:
if bounding_box is None:
bounding_box = point.bounding_box
else:
bounding_box |= point.bounding_box
return bounding_box
####################################################################################################
def _sort_point_for_graham_scan(points):
def sort_by_y(p0, p1):
return p0.x < p1.x if (p0.y == p1.y) else p0.y < p1.y
# sort by ascending y
sorted_points = sorted(points, key=functools.cmp_to_key(sort_by_y))
# sort by ascending slope with p0
p0 = sorted_points[0]
x0 = p0.x
y0 = p0.y
def slope(p):
# return (p - p0).tan
return (p.y - y0) / (p.x - x0)
def sort_by_slope(p0, p1):
s0 = slope(p0)
s1 = slope(p1)
return p0.x < p1.x if (s0 == s1) else s0 < s1
return sorted_points[0] + sorted(sorted_points[1:], key=cmp_to_key(sort_by_slope))
####################################################################################################
def _ccw(p1, p2, p3):
"""Three points are a counter-clockwise turn if ccw > 0, clockwise if ccw < 0, and collinear if ccw
= 0 because ccw is a determinant that gives twice the signed area of the triangle formed by p1,
p2 and p3.
"""
return (p2.x - p1.x)*(p3.y - p1.y) - (p2.y - p1.y)*(p3.x - p1.x)
####################################################################################################
def convex_hull(points):
"""Return the convex hull of the list of points using Graham Scan algorithm.
References
* https://en.wikipedia.org/wiki/Graham_scan
"""
# convex_hull is a stack of points beginning with the leftmost point.
convex_hull = []
sorted_points = _sort_point_for_graham_scan(points)
for p in sorted_points:
# if we turn clockwise to reach this point,
# pop the last point from the stack, else, append this point to it.
while len(convex_hull) > 1 and _ccw(convex_hull[-1], convex_hull[-2], p) >= 0: # Fixme: check
convex_hull.pop()
convex_hull.append(p)
# the stack is now a representation of the convex hull, return it.
return convex_hull
Patro-master-Patro/Patro/GeometryEngine/Conic.py 0000664 0000000 0000000 00000071621 13424433274 0022147 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement conic geometry like circle and ellipse.
*Valentina Requirements*
* circle with angular domain
* circle with start angle and arc length
* curvilinear distance on circle
* line-circle intersection
* circle-circle intersection
* point constructed from a virtual circle and a point on a tangent : right triangle
* point from tangent circle and segment ???
* ellipse with angular domain and rotation
"""
# Fixme:
#
# Ellipse passing by two points
# https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
#
####################################################################################################
__all__ = [
'Circle2D',
'Ellipse2D',
]
####################################################################################################
import logging
import math
from math import fabs, sqrt, radians, pi, cos, sin # , degrees
import numpy as np
from Patro.Common.Math.Functions import sign # , epsilon_float
from .BoundingBox import bounding_box_from_points
from .Line import Line2D
from .Mixin import AngularDomainMixin, CenterMixin, AngularDomain
from .Primitive import Primitive, Primitive2DMixin
from .Segment import Segment2D
from .Transformation import Transformation2D
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class PointNotOnCircleError(ValueError):
pass
####################################################################################################
class Circle2D(Primitive2DMixin, CenterMixin, AngularDomainMixin, Primitive):
"""Class to implements 2D Circle."""
##############################################
@classmethod
def from_two_points(cls, center, point):
"""Construct a circle from a center point and passing by another point"""
return cls(center, (point - center).magnitude)
##############################################
@classmethod
def from_triangle_circumcenter(cls, triangle):
"""Construct a circle passing by three point"""
return cls.from_two_points(triangle.circumcenter, triangle.p0)
##############################################
@classmethod
def from_triangle_in_circle(cls, triangle):
"""Construct the in circle of a triangle"""
return triangle.in_circle
##############################################
# @classmethod
# def from_start_angle_distance(cls, center, radius, start_angle, distance):
# """Construct a circle from a center point, a starting angle and a distance point"""
# if distance > 2*pi*radius:
# domain = None
# else:
# stop_angle = start_angle + math.degrees(distance / radius)
# domain = AngularDomain(start_angle, stop_angle)
# return cls(center, radius, domain)
##############################################
# Fixme: tangent constructs ...
##############################################
def __init__(self, center, radius,
domain=None,
diameter=False,
start_angle=None,
distance=None,
):
"""Construct a 2D circle from a center point and a radius.
If the circle is not closed, *domain* is a an :class:`AngularDomain` instance in degrees.
If *start_angle and *distance* is given then the stop angle is computed from them.
"""
if diameter:
radius /= 2
self._radius = radius
self.center = center
if start_angle is not None and distance is not None:
if distance > 2*pi*radius:
self._domain = None
else:
stop_angle = start_angle + math.degrees(distance / radius)
self._domain = AngularDomain(start_angle, stop_angle)
else:
self.domain = domain # Fixme: name ???
##############################################
def clone(self):
return self.__class__(self._center, self._radius, self._domain)
##############################################
def apply_transformation(self, transformation):
self._center = transformation * self._center
# Fixme: shear -> ellipse
if self._radius is not None:
self._radius = transformation * self._radius
##############################################
def __repr__(self):
return '{0}({1._center}, {1._radius}, {1._domain})'.format(self.__class__.__name__, self)
##############################################
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
@property
def diameter(self):
return self._radius * 2
##############################################
@property
def eccentricity(self):
return 1
@property
def perimeter(self):
if self._domain is not None:
return 2*pi * self._radius
else:
return self._radius * self._domain.length
@property
def area(self):
# Fixme: domain
return pi * self._radius**2
##############################################
def point_at_angle(self, angle):
return self.__vector_cls__.from_polar(self._radius, angle) + self._center
##############################################
def point_in_circle_frame(self, point):
return point - self._center
##############################################
def angle_for_point(self, point):
offset = self.point_in_circle_frame(point)
# distance = offset.magnitude_square
# if not epsilon_float(distance, self._radius**2):
# raise PointNotOnCircleError # ValueError('Point is not on circle')
# Fixme:
orientation = offset.orientation
if orientation < 0:
orientation = 360 + orientation
return orientation
##############################################
def point_at_distance(self, distance):
angle = math.degrees(distance / self._radius)
return self.point_at_angle(angle)
##############################################
def tangent_at_angle(self, angle):
point = self.__vector_cls__.from_polar(self._radius, angle) + self._center
tangent = (point - self._center).normal
return Line2D(point, tangent)
##############################################
@property
def bounding_box(self):
# Fixme: wrong for arc
return self._center.bounding_box.enlarge(self._radius)
##############################################
def signed_distance_to_point(self, point):
# d = |P - C| - R
# < 0 if inside
# = 0 on circle
# > 0 if outside
return (point - self._center).magnitude - self._radius
##############################################
def _circle_distance_to_point(self, point):
return abs(self.signed_distance_to_point(point))
##############################################
def distance_to_point(self, point):
if self._domain is not None:
# Fixme: check !!!
# try:
angle = self.angle_for_point(point)
# print('distance_to_circle', point, angle)
if self._domain.is_inside(angle):
# print('point is inside')
return self._circle_distance_to_point(point)
# except PointNotOnCircleError:
# pass
# print('point is outside')
return min([(point - vertex).magnitude for vertex in (self.start_point, self.stop_point)])
else:
return self._circle_distance_to_point(point)
##############################################
def is_point_inside(self, point):
return (point - self._center).magnitude_square <= self._radius**2
##############################################
def intersect_segment(self, segment):
r"""Compute the intersection of a circle and a segment.
Reference
* http://mathworld.wolfram.com/Circle-LineIntersection.html
* Rhoad et al. 1984, p. 429
* Rhoad, R.; Milauskas, G.; and Whipple, R. Geometry for Enjoyment and Challenge,
* rev. ed. Evanston, IL: McDougal, Littell & Company, 1984.
System of equations
.. math::
\begin{split}
x^2 + y^2 = r^2 \\
dx \times y = dy \times x - D
\end{split}
where
.. math::
\begin{align}
dx &= x1 - x0 \\
dy &= y1 - y0 \\
D &= x0 \times y1 - x1 \times y0
\end{align}
"""
# Fixme: check domain !!!
dx = segment.vector.x
dy = segment.vector.y
dr2 = dx**2 + dy**2
p0 = segment.p0 - self.center
p1 = segment.p1 - self.center
D = p0.cross_product(p1)
# from sympy import *
# x, y, dx, dy, D, r = symbols('x y dx dy D r')
# system = [x**2 + y**2 - r**2, dx*y - dy*x + D]
# vars = [x, y]
# solution = nonlinsolve(system, vars)
# solution.subs(dx**2 + dy**2, dr**2)
Vector2D = self.__vector_cls__
discriminant = self.radius**2 * dr2 - D**2
if discriminant < 0:
return None
elif discriminant == 0: # tangent line
x = ( D * dy ) / dr2
y = (- D * dx ) / dr2
return Vector2D(x, y) + self.center
else: # intersection
x_a = D * dy
y_a = -D * dx
x_b = sign(dy) * dx * sqrt(discriminant)
y_b = fabs(dy) * sqrt(discriminant)
x0 = (x_a - x_b) / dr2
y0 = (y_a - y_b) / dr2
x1 = (x_a + x_b) / dr2
y1 = (y_a + y_b) / dr2
p0 = Vector2D(x0, y0) + self.center
p1 = Vector2D(x1, y1) + self.center
return p0, p1
##############################################
def intersect_circle(self, circle):
# Fixme: check domain !!!
# http://mathworld.wolfram.com/Circle-CircleIntersection.html
v = circle.center - self.center
d = sign(v.x) * v.magnitude
# Equations
# x**2 + y**2 = R**2
# (x-d)**2 + y**2 = r**2
x = (d**2 - circle.radius**2 + self.radius**2) / (2*d)
y2 = self.radius**2 - x**2
if y2 < 0:
return None
else:
p = self.center + v.normalise() * x
if y2 == 0:
return p
else:
n = v.normal() * sqrt(y2)
return p - n, p - n
##############################################
def bezier_approximation(self):
# http://spencermortensen.com/articles/bezier-circle/
# > First approximation:
#
# 1) The endpoints of the cubic Bézier curve must coincide with the endpoints of the
# circular arc, and their first derivatives must agree there.
#
# 2) The midpoint of the cubic Bézier curve must lie on the circle.
#
# B(t) = (1-t)**3 * P0 + 3*(1-t)**2*t * P1 + 3*(1-t)*t**2 * P2 + t**3 * P3
#
# For an unitary circle : P0 = (0,1) P1 = (c,1) P2 = (1,c) P3 = (1, 0)
#
# The second constraint provides the value of c = 4/3 * (sqrt(2) - 1)
#
# The maximum radial drift is 0.027253 % with this approximation.
# In this approximation, the Bézier curve always falls outside the circle, except
# momentarily when it dips in to touch the circle at the midpoint and endpoints.
#
# >Better approximation:
#
# 2) The maximum radial distance from the circle to the Bézier curve must be as small as
# possible.
#
# The first constraint yields the parametric form of the Bézier curve:
# B(t) = (x,y), where:
# x(t) = 3*c*(1-t)**2*t + 3*(1-t)*t**2 + t**3
# y(t) = 3*c*t**2*(1-t) + 3*t*(1-t)**2 + (1-t)**3
#
# The radial distance from the arc to the Bézier curve is: d(t) = sqrt(x**2 + y**2) - 1
#
# The Bézier curve touches the right circular arc at its initial endpoint, then drifts
# outside the arc, inside, outside again, and finally returns to touch the arc at its
# endpoint.
#
# roots of d : 0, (3*c +- sqrt(-9*c**2 - 24*c + 16) - 2)/(6*c - 4), 1
#
# This radial distance function, d(t), has minima at t = 0, 1/2, 1,
# and maxima at t = 1/2 +- sqrt(12 - 20*c - 3*c**22)/(4 - 6*c)
#
# Because the Bézier curve is symmetric about t = 1/2 , the two maxima have the same
# value. The radial deviation is minimized when the magnitude of this maximum is equal to
# the magnitude of the minimum at t = 1/2.
#
# This gives the ideal value for c = 0.551915024494
# The maximum radial drift is 0.019608 % with this approximation.
# P0 = (0,1) P1 = (c,1) P2 = (1,c) P3 = (1,0)
# P0 = (1,0) P1 = (1,-c) P2 = (c,-1) P3 = (0,-1)
# P0 = (0,-1) P1 = (-c,-1) P2 = (-1,-c) P3 = (-1,0)
# P0 = (-1,0) P1 = (-1,c) P2 = (-c,1) P3 = (0,1)
raise NotImplementedError
####################################################################################################
class Ellipse2D(Primitive2DMixin, CenterMixin, AngularDomainMixin, Primitive):
r"""Class to implements 2D Ellipse.
A general ellipse in 2D is represented by a center point `C`, an orthonormal set of
axis-direction vectors :math:`{U_0 , U_1 }`, and associated extents :math:`e_i` with :math:`e_0
\ge e_1 > 0`. The ellipse points are
.. math::
\begin{equation}
P = C + x_0 U_0 + x_1 U_1
\end{equation}
where
.. math::
\begin{equation}
\left(\frac{x_0}{e_0}\right)^2 + \left(\frac{x_1}{e_1}\right)^2 = 1
\end{equation}
If :math:`e_0 = e_1`, then the ellipse is a circle with center `C` and radius :math:`e_0`.
The orthonormality of the axis directions and Equation (1) imply :math:`x_i = U_i \dot (P −
C)`. Substituting this into Equation (2) we obtain
.. math::
(P − C)^T M (P − C) = 1
where :math:`M = R D R^T`, `R` is an orthogonal matrix whose columns are :math:`U_0` and
:math:`U_1` , and `D` is a diagonal matrix whose diagonal entries are :math:`1/e_0^2` and
:math:`1/e_1^2`.
An ellipse can also be parameterised by an angle :math:`\theta`
.. math::
\begin{pmatrix} x \\ y \end{pmatrix} =
\begin{bmatrix}
\cos\phi & \sin\phi \\
-\sin\phi & \cos\phi
\end{bmatrix}
\begin{pmatrix} r_x \cos\theta \\ r_y \sin\theta \end{pmatrix}
+ \begin{pmatrix} C_x \\ C_y \end{pmatrix}
where :math:`\phi` is the angle from the x-axis, :math:`r_x` is the semi-major and :math:`r_y`
semi-minor axes.
"""
_logger = _module_logger.getChild('Ellipse2D')
##############################################
@classmethod
def svg_arc(cls, point1, point2, radius_x, radius_y, angle, large_arc, sweep):
"""Implement SVG Arc.
Parameters
* *point1* is the start point and *point2* is the end point.
* *radius_x* and *radius_y* are the radii of the ellipse, also known as its semi-major and
semi-minor axes.
* *angle* is the angle from the x-axis of the current coordinate system to the x-axis of the ellipse.
* if the *large arc* flag is unset then arc spanning less than or equal to 180 degrees is
chosen, else an arc spanning greater than 180 degrees is chosen.
* if the *sweep* flag is unset then the line joining centre to arc sweeps through decreasing
angles, else if it sweeps through increasing angles.
References
* https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
* https://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
"""
# Ensure radii are non-zero
if radius_x == 0 or radius_y == 0:
return Segment2D(point1, point2)
# Ensure radii are positive
radius_x = abs(radius_x)
radius_y = abs(radius_y)
# step 1
radius_x2 = radius_x**2
radius_y2 = radius_y**2
# We define a new referential with the origin is set to the middle of P1 — P2
origin_prime = (point1 + point2)/2
# P1 is exprimed in this referential where the ellipse major axis line up with the x axis
point1_prime = Transformation2D.Rotation(-angle) * (point1 - point2)/2
# Ensure radii are large enough
radii_scale = point1_prime.x**2/radius_x2 + point1_prime.y**2/radius_y2
if radii_scale > 1:
self._logger.warning('SVG Arc: radii must be scale')
radii_scale = math.sqrt(radii_scale)
radius_x = radii_scale * radius_x
radius_y = radii_scale * radius_y
radius_x2 = radius_x**2
radius_y2 = radius_y**2
# step 2
den = radius_x2 * point1_prime.y**2 + radius_y2 * point1_prime.x**2
num = radius_x2*radius_y2 - den
ratio = radius_x/radius_y
sign = 1 if large_arc != sweep else -1
# print(point1_prime)
# print(point1_prime.anti_normal)
# print(ratio)
# print(point1_prime.anti_normal.scale(ratio, 1/ratio))
sign *= -1 # Fixme: solve mirroring artefacts for y-axis pointing to the top
center_prime = sign * math.sqrt(num / den) * point1_prime.anti_normal.scale(ratio, 1/ratio)
center = Transformation2D.Rotation(angle) * center_prime + origin_prime
vector1 = (point1_prime - center_prime).divide(radius_x, radius_y)
vector2 = - (point1_prime + center_prime).divide(radius_x, radius_y)
theta = cls.__vector_cls__(1, 0).angle_with(vector1)
delta_theta = vector1.angle_with(vector2)
# if theta < 0:
# theta = 180 + theta
# if delta_theta < 0:
# delta_theta = 180 + delta_theta
delta_theta = delta_theta % 360
# print('theta', theta, delta_theta)
if not sweep and delta_theta > 0:
delta_theta -= 360
elif sweep and delta_theta < 0:
delta_theta += 360
# print('theta', theta, delta_theta, theta + delta_theta)
domain = domain = AngularDomain(theta, theta + delta_theta)
return cls(center, radius_x, radius_y, angle, domain)
#######################################
def __init__(self, center, radius_x, radius_y, angle, domain=None):
self.center = center
self.radius_x = radius_x
self.radius_y = radius_y
self.angle = angle
self.domain = domain
self._bounding_box = None
##############################################
def clone(self):
return self.__class__(
self._center,
self._radius_x, self._radius_y,
self._angle,
self._domain,
)
##############################################
def apply_transformation(self, transformation):
self._center = transformation * self._center
self._radius_x = transformation * self._radius_x
self._radius_y = transformation * self._radius_y
self._bounding_box = None
##############################################
def __repr__(self):
return '{0}({1._center}, {1._radius_x}, {1._radius_x}, {1._angle})'.format(self.__class__.__name__, self)
##############################################
@property
def radius_x(self):
return self._radius_x
@radius_x.setter
def radius_x(self, value):
self._radius_x = float(value)
@property
def radius_y(self):
return self._radius_y
@radius_y.setter
def radius_y(self, value):
self._radius_y = float(value)
@property
def angle(self):
return self._angle
@angle.setter
def angle(self, value):
self._angle = float(value)
@property
def major_vector(self):
# Fixme: x < y
return self.__vector_cls__.from_polar(self._angle, self._radius_x)
@property
def minor_vector(self):
# Fixme: x < y
return self.__vector_cls__.from_polar(self._angle + 90, self._radius_y)
##############################################
@property
def eccentricity(self):
# focal distance
# c = sqrt(self._radius_x**2 - self._radius_y**2)
# e = c / a
return sqrt(1 - (self._radius_y/self._radius_x)**2)
##############################################
def matrix(self):
# unit circle -> scale(a, b) -> rotation -> translation(xc, yc)
angle = radians(self._angle)
c = cos(angle)
s = sin(angle)
c2 = c**2
s2 = s**2
a = self._radius_x
b = self._radius_y
a2 = a**2
b2 = b**2
xc = self._center.x
yc = self._center.y
xc2 = xc**2
yc2 = yc**2
A = a2*s + b2*c2
B = 2*(b2 - a2)*c*s
C = a2*c2 * b2*s2
D = -2*A*xc - B*yc
E = -B*xc - 2*C*yc
F = A*xc2 + B*xc*yc + C*yc2 - a2*b2
return np.array((
( A, B/2, D/2),
(B/2, C, E/2),
(D/2, E/2, F),
))
##############################################
def point_in_ellipse_frame(self, point):
return (point - self._center).rotate(-self._angle)
def point_from_ellipse_frame(self, point):
return self._center + point.rotate(self._angle)
##############################################
def point_at_angle(self, angle):
# point = self.__vector_cls__.from_ellipse(self._radius_x, self._radius_y, angle)
# return self.point_from_ellipse_frame(point)
point = self.__vector_cls__.from_ellipse(self._radius_x, self._radius_y, self._angle + angle)
return self._center + point
##############################################
@property
def bounding_box(self):
if self._bounding_box is None:
radius_x, radius_y = self._radius_x, self._radius_y
if self._angle == 0:
bounding_box = self._center.bounding_box
bounding_box.x.enlarge(radius_x)
bounding_box.y.enlarge(radius_y)
self._bounding_box = bounding_box
else:
angle_x = self._angle
angle_y = angle_x + 90
Vector2D = self.__vector_cls__
points = [self._center + offset for offset in (
Vector2D.from_polar(angle_x, radius_x),
Vector2D.from_polar(angle_x, -radius_x),
Vector2D.from_polar(angle_y, radius_y),
Vector2D.from_polar(angle_y, -radius_y),
)]
self._bounding_box = bounding_box_from_points(points)
return self._bounding_box
##############################################
@staticmethod
def _robust_length(x, y):
if x < y:
x, y = y, x
return abs(x) * math.sqrt(1 + (y/x)**2)
##############################################
def _distance_point_bisection(self, r0, z0, z1, g):
n0 = r0 * z0
s0 = z1 - 1
if g < 0:
s1 = 0
else:
s1 = self._robust_length(n0 , z1) - 1
s = 0
MAX_ITERATION = 1074 # for double
for i in range(MAX_ITERATION):
s = (s0 + s1) / 2
if s == s0 or s == s1:
break
ratio0 = n0 / (s + r0)
ratio1 = z1 / (s + 1)
g = ratio0**2 + ratio1**2 -1
if g > 0:
s0 = s
elif g < 0:
s1 = s
else:
break
return s
##############################################
def _eberly_distance(self, point):
"""Compute distance to point using the algorithm described in
Distance from a Point to an Ellipse, an Ellipsoid, or a Hyperellipsoid
David Eberly, Geometric Tools, Redmond WA 98052
September 28, 2018
https://www.geometrictools.com/Documentation/Documentation.html
https://www.geometrictools.com/Documentation/DistancePointEllipseEllipsoid.pdf
The point is expressed in the ellipse coordinate system.
The preconditions are e0 ≥ e1 > 0, y0 ≥ 0, and y1 ≥ 0.
"""
# Fixme: make a 3D plot to check the algorithm on a 2D grid and rotated ellipse
y0, y1 = point
e0, e1 = self._radius_x, self._radius_y
if y1 > 0:
if y0 > 0:
z0 = y0 / e0
z1 = y1 / e1
g = z0**2 + z1**2 - 1
if g != 0:
r0 = (e0 / e1)**2
sbar = self._distance_point_bisection(r0, z0, z1, g)
x0 = r0 * y0 / (sbar + r0)
x1 = y1 / (sbar + 1)
distance = math.sqrt((x0 - y0)**2 + (x1 - y1)**2)
else:
x0 = y0
x1 = y1
distance = 0
else:
# y0 == 0
x0 = 0
x1 = e1
distance = abs(y1 - e1)
else:
# y1 == 0
numer0 = e0 * y0
denom0 = e0**2 - e1**2
if numer0 < denom0:
xde0 = numer0 / denom0
x0 = e0 * xde0
x1 = e1 * math.sqrt(1 - xde0**2)
distance = math.sqrt((x0 - y0)**2 + x1**2)
else:
x0 = e0
x1 = 0
distance = abs(y0 - e0)
return distance, self.__vector_cls__(x0, x1)
##############################################
def distance_to_point(self, point, return_point=False, is_inside=False):
# Fixme: can be transform the problem to a circle using transformation ???
point_in_frame = self.point_in_ellipse_frame(point)
point_in_frame_abs = self.__vector_cls__(abs(point_in_frame.x), abs(point_in_frame.y))
distance, point_in_ellipse = self._eberly_distance(point_in_frame_abs)
if is_inside:
# Fixme: right ???
return (
(point_in_frame_abs - self._center).magnitude_square
<=
(point_in_ellipse - self._center).magnitude_square
)
elif return_point:
point_in_ellipse = self.__vector_cls__(
sign(point_in_frame.x)*(point_in_ellipse.x),
sign(point_in_frame.y)*(point_in_ellipse.y),
)
point_in_ellipse = self.point_from_ellipse_frame(point_in_ellipse)
return distance, point_in_ellipse
else:
return distance
##############################################
def is_point_inside(self, point):
return self.distance_to_point(point, is_inside=True)
##############################################
def intersect_segment(self, segment):
# Fixme: to be checked
# Map segment in ellipse frame and scale y axis so as to transform the ellipse to a circle
y_scale = self._radius_x / self._radius_y
points = [self.point_in_ellipse_frame(point) for point in segment.points]
points = [self.__vector_cls__(point.x, point.y * y_scale) for point in points]
segment_in_frame = Segment2D(*points)
circle = Circle2D(self.__vector_cls__(0, 0), self._radius_x)
points = circle.intersect_segment(segment_in_frame)
points = [self.__vector_cls__(point.x, point.y / y_scale) for point in points]
points = [self.point_from_ellipse_frame(point) for point in points]
return points
##############################################
def intersect_conic(self, conic):
"""
Reference
* Intersection of Ellipses
* David Eberly, Geometric Tools, Redmond WA 98052
* June 23, 2015
* https://www.geometrictools.com/
* https://www.geometrictools.com/Documentation/IntersectionOfEllipses.pdf
"""
raise NotImplementedError
Patro-master-Patro/Patro/GeometryEngine/Interpolation.py 0000664 0000000 0000000 00000002520 13424433274 0023733 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to perform interpolation."""
####################################################################################################
__all__ = [
'interpolate_two_points',
]
####################################################################################################
def interpolate_two_points(p0, p1, t):
"""Return the linear interpolate of two points."""
return p0 * (1 - t) + p1 * t
Patro-master-Patro/Patro/GeometryEngine/Line.py 0000664 0000000 0000000 00000017027 13424433274 0022003 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement line.
"""
####################################################################################################
__all__ = ['Line2D']
####################################################################################################
from Patro.Common.IterTools import pairwise
from .Primitive import Primitive, Primitive2DMixin
from .Vector import Vector2D
####################################################################################################
class Line2D(Primitive2DMixin, Primitive):
"""Class to implement 2D Line."""
##############################################
@staticmethod
def from_two_points(p0, p1):
"""Construct a :class:`Line2D` from two points."""
return Line2D(p0, p1 - p0)
##############################################
def __init__(self, point, vector):
"""Construct a :class:`Line2D` from a point and a vector."""
self.p = point
self.v = vector
##############################################
def clone(self):
return self.__class__(self.p, self.v)
##############################################
def __str__(self):
str_format = '''Line
Point {0.p}
Vector {0.v}
magnitude {1}
'''
return str_format.format(self, self.v.magnitude)
##############################################
@property
def is_infinite(self):
return True
##############################################
def interpolate(self, s):
"""Return the Point corresponding to the curvilinear abscissa s"""
return self.p + (self.v * s)
point_at_s = interpolate
point_at_t = interpolate
##############################################
def compute_distance_between_abscissae(self, s0, s1):
"""Compute distance between two abscissae"""
return abs(s1 - s0) * self.v.magnitude()
##############################################
def compute_distance(self, s_list):
"""Compute distance between a set of abscissae"""
# Fixme: ?
# s_list_sorted = copy.deepcopy(s_list)
# s_list_sorted.sort()
return [self.compute_distance_between_abscissae(s0, s1) for s0, s1 in pairwise(s_list)]
##############################################
def get_y_from_x(self, x):
"""Return y corresponding to x"""
return self.v.tan * (x - self.p.x) + self.p.y
##############################################
def get_x_from_y(self, y):
"""Return x corresponding to y"""
return self.v.inverse_tan * (y - self.p.y) + self.p.x
##############################################
# Fixme: is_parallel_to
def is_parallel(self, other, return_cross=False):
"""Self is parallel to other"""
return self.v.is_parallel(other.v, return_cross)
##############################################
def is_orthogonal(self, other):
"""Self is orthogonal to other"""
return self.v.is_orthogonal(other.v)
##############################################
def shifted_parallel_line(self, shift):
"""Return the shifted parallel line"""
n = self.v.normal
n.normalise()
point = self.p + n*shift
return self.__class__(point, self.v)
##############################################
def orthogonal_line_at_abscissa(self, s):
"""Return the orthogonal line at abscissa s"""
point = self.interpolate(s)
vector = self.v.normal
return self.__class__(point, vector)
##############################################
def intersection_abscissae(l1, l2):
"""Return the intersection abscissae between l1 and l2"""
# l1 = p1 + s1*v1
# l2 = p2 + s2*v2
# at intersection l1 = l2
# p2 + s2*v2 = p1 + s1*v1
# delta = p2 - p1 = s1*v1 - s2*v2
# delta x v1 = - s2 * v2 x v1 = s2 * v1 x v2
# delta x v2 = s1 * v1 x v2
test, cross = l1.is_parallel(l2, return_cross=True)
if test:
return (None, None)
else:
denominator = 1. / cross
delta = l2.p - l1.p
s1 = delta.cross(l2.v) * denominator
s2 = delta.cross(l1.v) * denominator
return (s1, s2)
##############################################
def intersection(self, other):
"""Return the intersection Point between self and other"""
s1, s2 = self.intersection_abscissae(other)
if s1 is None:
return None
else:
return self.interpolate(s1)
##############################################
def projected_abscissa(self, point):
"""Return the abscissa corresponding to the perpendicular projection of a point to the line
"""
delta = point - self.p
s = delta.projection_on(self.v)
return s
##############################################
def distance_to_line(self, point):
"""Return the distance of a point to the line"""
# Reference: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
# Line equation: a*x + b*y + c = 0
# d = |a*x + b*y + c| / sqrt(a**2 + b**2)
# Vx*y - Vy*x + c = 0
# c = Vy*X0 - Vx*Y0
# d = (vx*(y - y0) - vy*(x - x0)) / |V|
# d = V x (P - P0) / |V|
# x0 = self.p.x
# y0 = self.p.y
# vx = self.v.x
# vy = self.v.y
# return (self.v.x*(point.y - self.p.y) - self.v.y*(point.x - self.p.x)) / self.v.magnitude
delta = point - self.p
d = delta.deviation_with(self.v)
return d
##############################################
def distance_and_abscissa_to_line(self, point):
"""Return the distance of a point to the line"""
delta = point - self.p
if delta.magnitude_square == 0:
return 0, 0
else:
d = delta.deviation_with(self.v)
s = delta.projection_on(self.v)
return d, s # distance to line, abscissa
##############################################
def get_x_y_from_bounding_box(self, interval):
"""Return the bounding box build on the intersection of the input bounding box with the line
"""
left, bottom, right, top = interval.bounding_box()
vb = Vector2D(interval.size())
if abs(self.v.tan) > vb.tan:
x_min, y_min = self.get_x_from_y(bottom), bottom
x_max, y_max = self.get_x_from_y(top), top
else:
x_min, y_min = left, self.get_y_from_x(left)
x_max, y_max = right, self.get_y_from_x(right)
return Vector2D(x_min, y_min), Vector2D(x_max, y_max)
Patro-master-Patro/Patro/GeometryEngine/Mixin.py 0000664 0000000 0000000 00000012324 13424433274 0022173 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement mixins.
"""
####################################################################################################
__all__ = [
'AngularDomain',
'AngularDomainMixin',
'CenterMixin',
]
####################################################################################################
import math
from math import radians, pi # , degrees
####################################################################################################
class AngularDomain:
"""Class to define an angular domain"""
##############################################
def __init__(self, start=0, stop=360, degrees=True):
if not degrees:
start = math.degrees(start)
stop = math.degrees(stop)
self.start = start
self.stop = stop
##############################################
def __clone__(self):
return self.__class__(self._start, self._stop)
##############################################
def __repr__(self):
return '{0}({1._start}, {1._stop})'.format(self.__class__.__name__, self)
##############################################
@property
def start(self):
return self._start
@start.setter
def start(self, value):
self._start = float(value)
@property
def stop(self):
return self._stop
@stop.setter
def stop(self, value):
self._stop = float(value)
@property
def start_radians(self):
return radians(self._start)
@property
def stop_radians(self):
return radians(self._stop)
##############################################
@property
def is_null(self):
return self._stop == self._start
@property
def is_closed(self):
return abs(self._stop - self._start) >= 360
@property
def is_over_closed(self):
return abs(self._stop - self._start) > 360
@property
def is_counterclockwise(self):
"""Return True if start <= stop, e.g. 10 <= 300"""
# Fixme: name ???
return self.start <= self.stop
@property
def is_clockwise(self):
"""Return True if stop < start, e.g. 300 < 10"""
return self.stop < self.start
##############################################
@property
def length(self):
"""Return the length for an unitary circle"""
if self.is_closed:
return 2*pi
else:
length = self.stop_radians - self.start_radians
if self.is_counterclockwise:
return length
else:
return 2*pi - length
##############################################
def is_inside(self, angle):
if self.is_counterclockwise:
return self._start <= angle <= self._stop
else:
# Fixme: check !!!
return not(self._stop < angle < self._start)
####################################################################################################
class AngularDomainMixin:
##############################################
@property
def domain(self):
return self._domain
@domain.setter
def domain(self, value):
if value is not None:
self._domain = value # Fixme: AngularDomain() ??
else:
self._domain = None
##############################################
@property
def is_closed(self):
return self._domain is None
##############################################
def start_stop_point(self, start=True):
if self._domain is not None:
angle = self.domain.start if start else self.domain.stop
return self.point_at_angle(angle)
else:
return None
##############################################
@property
def start_point(self):
return self.start_stop_point(start=True)
##############################################
@property
def stop_point(self):
return self.start_stop_point(start=False)
####################################################################################################
class CenterMixin:
@property
def center(self):
return self._center
@center.setter
def center(self, value):
self._center = self.__vector_cls__(value)
@property
def points(self):
return (self._center,)
Patro-master-Patro/Patro/GeometryEngine/Path.py 0000664 0000000 0000000 00000077610 13424433274 0022014 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement path.
For resources on path see :ref:`this section `.
"""
####################################################################################################
__all__ = [
'LinearSegment',
'QuadraticBezierSegment',
'CubicBezierSegment',
'Path2D',
]
####################################################################################################
import logging
import math
from Patro.Common.Math.Functions import sign
from .Primitive import Primitive1P, Primitive2DMixin
from .Bezier import QuadraticBezier2D, CubicBezier2D
from .Conic import AngularDomain, Circle2D, Ellipse2D
from .Segment import Segment2D
from .Vector import Vector2D
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class PathPart:
##############################################
def __init__(self, path, index):
self._path = path
self._index = index
##############################################
def _init_absolute(self, absolute):
self._absolute = bool(absolute)
##############################################
def clone(self, path):
raise NotImplementedError
##############################################
def __repr__(self):
return '{0}(@{1._index})'.format(self.__class__.__name__, self)
##############################################
@property
def path(self):
return self._path
@property
def index(self):
return self._index
@index.setter
def index(self, value):
self._index = int(value)
##############################################
@property
def prev_part(self):
return self._path[self._index -1]
@property
def next_part(self):
return self._path[self._index +1]
##############################################
@property
def start_point(self):
# Fixme: cache ???
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
##############################################
def to_absolute_point(self, point):
# Fixme: cache ???
if self._absolute:
return point
else:
return point + self.start_point
##############################################
@property
def geometry(self):
raise NotImplementedError
##############################################
@property
def bounding_box(self):
return self.geometry.bounding_box
####################################################################################################
class OnePointMixin:
##############################################
@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):
return self.to_absolute_point(self._point)
##############################################
def apply_transformation(self, transformation):
# Fixme: right for relative ???
self._point = transformation * self._point
####################################################################################################
class TwoPointMixin:
##############################################
@property
def point1(self):
return self.to_absolute_point(self._point1)
@point1.setter
def point1(self, value):
self._point1 = Vector2D(value) # self._path.__vector_cls__
##############################################
@property
def point2(self):
return self.to_absolute_point(self._point2)
@point2.setter
def point2(self, value):
self._point2 = Vector2D(value)
##############################################
def apply_transformation(self, transformation):
# Fixme: right for relative ???
self._point1 = transformation * self._point1
self._point2 = transformation * self._point2
####################################################################################################
class ThreePointMixin(TwoPointMixin):
##############################################
@property
def point3(self):
return self.to_absolute_point(self._point3)
@point3.setter
def point3(self, value):
self._point3 = Vector2D(value) # self._path.__vector_cls__
##############################################
def apply_transformation(self, transformation):
# Fixme: right for relative ???
TwoPointMixin.apply_transformation(self, transformation)
self._point3 = transformation * self._point3
####################################################################################################
class LinearSegment(PathPart):
"""Class to implement a linear segment.
"""
# Fixme:
#
# If two successive vertices share the same circle, then it should be merged to one.
#
_logger = _module_logger.getChild('LinearSegment')
##############################################
def __init__(self, path, index, radius, closing=False):
super().__init__(path, index)
self._bissector = None
self._direction = None
self._start_bulge = False
self._closing = bool(closing)
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._reset_cache()
##############################################
def _reset_cache(self):
self._bulge_angle = None
self._bulge_center = None
self._start_bulge_point = None
self._stop_bulge_point = None
##############################################
def close(self, radius):
"""Set the bulge radius at the closure"""
self.radius = radius
self._reset_cache()
self._start_bulge = True
##############################################
@property
def prev_part(self):
if self._start_bulge:
return self._path.stop_segment # or [-1] don't work
else:
# Fixme: super
return self._path[self._index -1]
@property
def next_part(self):
if self._closing:
return self._path.start_segment # or [0]
else:
return self._path[self._index +1]
##############################################
@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:
value = abs(float(value))
if value == 0:
radius = None
self._radius = value
##############################################
@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:
# Fixme: rad vs degree
angle = self.direction.angle_with(self.prev_part.direction)
if angle >= 0:
angle = 180 - angle
else:
angle = -(180 + angle)
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 self.bulge_angle_rad / 2
##############################################
@property
def bulge_center(self):
if self._bulge_center is None:
offset = self.bissector * self._radius / math.sin(abs(self.half_bulge_angle))
self._bulge_center = self.start_point + offset
# Note: -offset create external loop
return self._bulge_center
##############################################
@property
def bulge_start_point(self):
if self._start_bulge_point is None:
angle = self.half_bulge_angle
offset = self.prev_part.direction * self._radius / math.tan(angle)
self._start_bulge_point = self.start_point - sign(angle) *offset
# Note: -offset create internal loop
return self._start_bulge_point
@property
def bulge_stop_point(self):
if self._stop_bulge_point is None:
angle = self.half_bulge_angle
offset = self.direction * self._radius / math.tan(self.half_bulge_angle)
self._stop_bulge_point = self.start_point + sign(angle) * 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)
# self._dump_bulge(arc)
return arc
##############################################
def _dump_bulge(self, arc):
self._logger.info(
'Bulge @{}\n'.format(self._index) +
str(arc)
)
####################################################################################################
class PathSegment(OnePointMixin, LinearSegment):
##############################################
def __init__(self, path, index, point, radius=None, absolute=False, closing=False):
super().__init__(path, index, radius, closing)
self.point = point
self._init_absolute(absolute)
##############################################
def clone(self, path):
# Fixme: check
if obj._start_bulge:
radius = None
else:
radius = self._radius
obj = self.__class__(path, self._index, self._point, radius, self._absolute, self._closing)
if obj._start_bulge:
self.close(self._radius)
return obj
##############################################
def to_absolute(self):
self._point = self.stop_point
self._absolute = True
##############################################
def apply_transformation(self, transformation):
OnePointMixin.apply_transformation(self, transformation)
if self._radius is not None:
self._radius = transformation * self._radius
##############################################
@property
def geometry(self):
# Fixme: cache ???
return Segment2D(*self.points)
####################################################################################################
class DirectionalSegmentMixin(LinearSegment):
##############################################
def apply_transformation(self, transformation):
# Since a rotation will change the direction
# DirectionalSegment must be casted to PathSegment
raise NotImplementedError
##############################################
@property
def geometry(self):
# Fixme: cache ???
return Segment2D(self.start_point, self.stop_point)
####################################################################################################
class AbsoluteHVSegment(DirectionalSegmentMixin):
##############################################
def to_path_segment(self):
# Fixme: duplicted
if self._index == 0 and self._radius is not None:
radius = None
close = True
else:
radius = self._radius
close = False
path = PathSegment(self._path, self._index, self.stop_point, radius, absolute=True)
if close:
path.close(self._radius)
return path
####################################################################################################
class AbsoluteHorizontalSegment(AbsoluteHVSegment):
##############################################
def __init__(self, path, index, x, radius=None):
super().__init__(path, index, radius)
self.x = x
self._init_absolute(False) # Fixme: mix
##############################################
def __repr__(self):
return '{0}(@{1._index}, {1._x})'.format(self.__class__.__name__, self)
##############################################
def clone(self, path):
return self.__class__(path, self._index, self._x, self._radius)
##############################################
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = float(value)
##############################################
@property
def stop_point(self):
return Vector2D(self._x, self.start_point.y)
####################################################################################################
class AbsoluteVerticalSegment(AbsoluteHVSegment):
##############################################
def __init__(self, path, index, y, radius=None):
super().__init__(path, index, radius)
self.y = y
self._init_absolute(False) # Fixme: mix
##############################################
def __repr__(self):
return '{0}(@{1._index}, {1._y})'.format(self.__class__.__name__, self)
##############################################
def clone(self, path):
return self.__class__(path, self._index, self._y, self._radius)
##############################################
@property
def y(self):
return self._y
@y.setter
def y(self, value):
self._y = float(value)
##############################################
@property
def stop_point(self):
return Vector2D(self.start_point.x, self._y)
####################################################################################################
class DirectionalSegment(DirectionalSegmentMixin):
__angle__ = None
##############################################
def __init__(self, path, index, length, radius=None):
super().__init__(path, index, radius)
self.length = length
##############################################
def __repr__(self):
return '{0}(@{1._index}, {1.offset})'.format(self.__class__.__name__, self)
##############################################
def clone(self, path):
return self.__class__(path, self._index, self._length, self._radius)
##############################################
@property
def length(self):
return self._length
@length.setter
def length(self, value):
self._length = float(value)
##############################################
@property
def offset(self):
# Fixme: cache ???
return Vector2D.from_polar(self._length, self.__angle__)
@property
def stop_point(self):
# Fixme: cache ???
return self.start_point + self.offset
##############################################
def to_path_segment(self):
if self._index == 0 and self._radius is not None:
radius = None
close = True
else:
radius = self._radius
close = False
path = PathSegment(self._path, self._index, self.offset, radius, absolute=False)
if close:
path.close(self._radius)
return path
####################################################################################################
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 QuadraticBezierSegment(PathPart, TwoPointMixin):
# Fixme: abs / inc
##############################################
def __init__(self, path, index, point1, point2, absolute=False):
PathPart.__init__(self, path, index)
self._init_absolute(absolute)
self.point1 = point1
self.point2 = point2
##############################################
def clone(self, path):
return self.__class__(path, self._index, self._point1, self._point2, self._absolute)
##############################################
def to_absolute(self):
self._point1 = self.point1
self._point2 = self.point2
self._absolute = True
##############################################
@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.points)
####################################################################################################
class CubicBezierSegment(PathPart, ThreePointMixin):
##############################################
def __init__(self, path, index, point1, point2, point3, absolute=False):
PathPart.__init__(self, path, index)
self._init_absolute(absolute)
self.point1 = point1
self.point2 = point2
self.point3 = point3
##############################################
def clone(self, path):
return self.__class__(path, self._index, self._point1, self._point2, self._point3, absolute)
##############################################
def to_absolute(self):
self._point1 = self.point1
self._point2 = self.point2
self._point3 = self.point3
self._absolute = True
##############################################
@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.points)
####################################################################################################
class StringedQuadtraticBezierSegment(PathPart, TwoPointMixin):
##############################################
def __init__(self, path, index, point1, absolute=False):
PathPart.__init__(self, path, index)
self._init_absolute(absolute)
self.point1 = point1
##############################################
def clone(self, path):
return self.__class__(path, self._index, self._point1, absolute)
##############################################
@property
def geometry(self):
# Fixme: cache ???
# Fixme: !!!
return Segment2D(self.start_point, self._point2)
# return CubicBezier2D(self.start_point, self._point1, self._point2, self._point3)
####################################################################################################
class StringedCubicBezierSegment(PathPart, TwoPointMixin):
##############################################
def __init__(self, path, index, point1, point2, absolute=False):
PathPart.__init__(self, path, index)
self._init_absolute(absolute)
# self.point1 = point1
##############################################
def clone(self, path):
return self.__class__(path, self._index, self._point1, self._point2, absolute)
##############################################
@property
def geometry(self):
# Fixme: cache ???
# Fixme: !!!
return Segment2D(self.start_point, self._point2)
# return CubicBezier2D(self.start_point, self._point1, self._point2, self._point3)
####################################################################################################
class ArcSegment(OnePointMixin, PathPart):
##############################################
def __init__(self, path, index, point, radius_x, radius_y, angle, large_arc, sweep, absolute=False):
PathPart.__init__(self, path, index)
self._init_absolute(absolute)
self.point = point
self._large_arc = bool(large_arc)
self._sweep = bool(sweep)
self._radius_x = radius_x
self._radius_y = radius_y
self._angle = angle
##############################################
def clone(self, path):
return self.__class__(
path,
self._index,
self._point,
self._radius_x, self._radius_y,
self._angle,
self._large_arc, self._sweep,
self._absolute,
)
##############################################
def __repr__(self):
template = '{0}(@{1._index} {1._point} rx={1._radius_x} ry={1._radius_y} a={1._angle} la={1._large_arc} s={1._sweep})'
return template.format(self.__class__.__name__, self)
##############################################
def to_absolute(self):
self._point = self.stop_point
self._absolute = True
##############################################
@property
def points(self):
return self.start_point, self.stop_point
##############################################
@property
def geometry(self):
return Ellipse2D.svg_arc(
self.start_point, self.stop_point,
self._radius_x, self._radius_y,
self._angle,
self._large_arc, self._sweep,
)
####################################################################################################
class Path2D(Primitive2DMixin, Primitive1P):
"""Class to implements 2D Path."""
_logger = _module_logger.getChild('Path2D')
##############################################
def __init__(self, start_point):
Primitive1P.__init__(self, start_point)
self._parts = [] # Fixme: segment ???
self._is_closed = False
##############################################
def clone(self):
obj = self.__class__(self._p0)
# parts must be added sequentially to the path for bulge check
parts = obj._parts
for part in self._parts:
parts.append(part.clone(obj))
return obj
##############################################
def __len__(self):
return len(self._parts)
def __iter__(self):
return iter(self._parts)
def __getitem__(self, index):
# try:
# return self._parts[slice_]
# except IndexError:
# return None
index = int(index)
number_of_parts = len(self._parts)
if 0 <= index < number_of_parts:
return self._parts[index]
# elif self._is_closed:
# if index == -1:
# return self.start_segment
# elif index == number_of_parts:
# return self.stop_segment
return None
##############################################
@property
def start_segment(self):
# Fixme: start_part ???
return self._parts[0]
@property
def stop_segment(self):
return self._parts[-1]
##############################################
@property
def is_closed(self):
return self._is_closed
##############################################
def _add_part(self, part_cls, *args, **kwargs):
if not self._is_closed:
obj = part_cls(self, len(self._parts), *args, **kwargs)
self._parts.append(obj)
return obj
##############################################
def apply_transformation(self, transformation):
# self._logger.info(str(self) + '\n' + str(transformation.type) + '\n' + str(transformation) )
for part in self._parts:
# print(part)
if isinstance(part, PathSegment):
part._reset_cache()
if isinstance(part, DirectionalSegmentMixin):
# Since a rotation will change the direction
# DirectionalSegment must be casted to PathSegment
part = part.to_path_segment()
self._parts[part.index] = part
if part._absolute is False:
# print('incremental', part, part.points)
part.to_absolute()
# print('->', part.points)
# print()
self._p0 = transformation * self._p0
# print('p0', self._p0)
for part in self._parts:
# print(part)
part.apply_transformation(transformation)
# print('->', part.points)
##############################################
@property
def bounding_box(self):
# Fixme: cache
bounding_box = None
for item in self._parts:
interval = item.geometry.bounding_box
if bounding_box is None:
bounding_box = interval
else:
bounding_box |= interval
return bounding_box
##############################################
def move_to(self, point):
self.p0 = point
##############################################
def horizontal_to(self, length, radius=None, absolute=False):
if absolute:
return self._add_part(PathSegment, self.__vector_cls__(length, 0), radius,
absolute=True)
else:
return self._add_part(HorizontalSegment, length, radius)
##############################################
def vertical_to(self, length, radius=None, absolute=False):
if absolute:
return self._add_part(PathSegment, self.__vector_cls__(0, length), radius,
absolute=True)
else:
return self._add_part(VerticalSegment, length, radius)
##############################################
def absolute_horizontal_to(self, x, radius=None):
return self._add_part(AbsoluteHorizontalSegment, x, radius)
def absolute_vertical_to(self, y, radius=None):
return self._add_part(AbsoluteVerticalSegment, y, radius)
##############################################
def north_to(self, length, radius=None):
return self._add_part(NorthSegment, length, radius)
def south_to(self, length, radius=None):
return self._add_part(SouthSegment, length, radius)
def west_to(self, length, radius=None):
return self._add_part(WestSegment, length, radius)
def east_to(self, length, radius=None):
return self._add_part(EastSegment, length, radius)
def north_east_to(self, length, radius=None):
return self._add_part(NorthEastSegment, length, radius)
def south_east_to(self, length, radius=None):
return self._add_part(SouthEastSegment, length, radius)
def north_west_to(self, length, radius=None):
return self._add_part(NorthWestSegment, length, radius)
def south_west_to(self, length, radius=None):
return self._add_part(SouthWestSegment, length, radius)
##############################################
def line_to(self, point, radius=None, absolute=False):
return self._add_part(PathSegment, point, radius, absolute=absolute)
##############################################
def close(self, radius=None, close_radius=None):
# Fixme: identify as close for SVG export <-- meaning ???
closing = close_radius is not None
segment = self._add_part(PathSegment, self._p0, radius, absolute=True, closing=closing)
if closing:
self.start_segment.close(close_radius)
self._is_closed = True
return segment
##############################################
def quadratic_to(self, point1, point2, absolute=False):
return self._add_part(QuadraticBezierSegment, point1, point2, absolute=absolute)
##############################################
def cubic_to(self, point1, point2, point3, absolute=False):
return self._add_part(CubicBezierSegment, point1, point2, point3, absolute=absolute)
##############################################
def stringed_quadratic_to(self, point, absolute=False):
return self._add_part(StringedQuadraticBezierSegment, point, absolute=absolute)
##############################################
def stringed_cubic_to(self, point1, point2, absolute=False):
return self._add_part(StringedCubicBezierSegment, point1, point2, absolute=absolute)
##############################################
def arc_to(self, point, radius_x, radius_y, angle, large_arc, sweep, absolute=False):
return self._add_part(ArcSegment, point, radius_x, radius_y, angle, large_arc, sweep,
absolute=absolute)
##############################################
@classmethod
def rounded_rectangle(cls, point, width, height, radius=None):
path = cls(point)
path.horizontal_to(width)
path.vertical_to(height, radius=radius)
path.horizontal_to(-width, radius=radius)
path.close(radius=radius, close_radius=radius)
return path
##############################################
@classmethod
def circle(cls, point, radius):
diameter = 2*float(radius)
path = cls(point)
path.horizontal_to(diameter)
path.vertical_to(diameter, radius=radius)
path.horizontal_to(-diameter, radius=radius)
path.close(radius=radius, close_radius=radius)
return path
Patro-master-Patro/Patro/GeometryEngine/Polygon.py 0000664 0000000 0000000 00000036660 13424433274 0022547 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement polygon.
"""
####################################################################################################
__all__ = ['Polygon2D']
####################################################################################################
import math
import numpy as np
from .Primitive import PrimitiveNP, ClosedPrimitiveMixin, PathMixin, Primitive2DMixin
from .Segment import Segment2D
from .Triangle import Triangle2D
from Patro.Common.Math.Functions import sign
####################################################################################################
# Fixme: PrimitiveNP last ???
class Polygon2D(Primitive2DMixin, ClosedPrimitiveMixin, PathMixin, PrimitiveNP):
"""Class to implements 2D Polygon."""
##############################################
# def __new__(cls, *points):
# # remove consecutive duplicates
# no_duplicate = []
# for point in points:
# if no_duplicate and point == no_duplicate[-1]:
# continue
# no_duplicate.append(point)
# if len(no_duplicate) > 1 and no_duplicate[-1] == no_duplicate[0]:
# no_duplicate.pop() # last point was same as first
# # remove collinear points
# i = -3
# while i < len(no_duplicate) - 3 and len(no_duplicate) > 2:
# a, b, c = no_duplicate[i], no_duplicate[i + 1], no_duplicate[i + 2]
# if Point.is_collinear(a, b, c):
# no_duplicate.pop(i + 1)
# if a == c:
# no_duplicate.pop(i)
# else:
# i += 1
# if len(vertices) > 3:
# return GeometryEntity.__new__(cls, *vertices, **kwargs)
# elif len(vertices) == 3:
# return Triangle(*vertices, **kwargs)
# elif len(vertices) == 2:
# return Segment(*vertices, **kwargs)
# else:
# return Point(*vertices, **kwargs)
##############################################
def __init__(self, *points):
if len(points) < 3:
raise ValueError('Polygon require at least 3 vertexes')
PrimitiveNP.__init__(self, points)
self._edges = None
self._is_simple = None
self._is_convex = None
self._area = None
# self._cross = None
# self._barycenter = None
# self._major_axis_angle = None
self._major_axis = None
# self._minor_axis = None
# self._axis_ratio = None
##############################################
@property
def is_triangle(self):
return self.number_of_points == 3
def to_triangle(self):
if self.is_triangle:
return Triangle2D(*self.points)
else:
raise ValueError('Polygon is not a triangle')
##############################################
@property
def edges(self):
if self._edges is None:
edges = []
N = self.number_of_points
for i in range(N):
j = (i+1) % N
edge = Segment2D(self._points[i], self._points[j])
edges.append(edge)
self._edges = edges
return iter(self._edges)
##############################################
def _test_is_simple(self):
edges = list(self.edges)
# intersections = []
# Test for edge intersection
for edge1 in edges:
for edge2 in edges:
if edge1 != edge2:
# Fixme: recompute line for edge
intersection, intersect = edge1.intersection(edge2)
if intersect:
common_vertex = edge1.share_vertex_with(edge2)
if common_vertex is not None:
if common_vertex == intersection:
continue
else:
# degenerated case where a vertex lie on an edge
return False
else:
# two edge intersect
# intersections.append(intersection)
return False
return True
##############################################
def _test_is_convex(self):
# https://en.wikipedia.org/wiki/Convex_polygon
# http://mathworld.wolfram.com/ConvexPolygon.html
if not self.is_simple:
return False
edges = list(self.edges)
# a polygon is convex if all turns from one edge vector to the next have the same sense
# sign = edges[-1].perp_dot(edges[0])
sign0 = sign(edges[-1].cross(edges[0]))
for i in range(len(edges)):
if sign(edges[i].cross(edges[i+1])) != sign0:
return False
return True
##############################################
@property
def is_simple(self):
"""Test if the polygon is simple, i.e. if it doesn't self-intersect."""
if self._is_simple is None:
self._is_simple = self._test_is_simple()
return self._is_simple
##############################################
@property
def is_convex(self):
if self._is_convex is None:
self._is_convex = self._test_is_convex()
return self._is_convex
@property
def is_concave(self):
return not self.is_convex
##############################################
@property
def perimeter(self):
return sum([edge.length for edge in self.edges])
##############################################
@property
def point_barycenter(self):
center = self.start_point
for point in self.iter_from_second_point():
center += point
return center / self.number_of_points
##############################################
def _compute_area_barycenter(self):
r"""Compute polygon area and barycenter.
Polygon area is determined by
.. math::
\begin{align}
\mathbf{A} &= \frac{1}{2} \sum_{i=0}^{n-1} P_i \otimes P_{i+1} \\
&= \frac{1}{2} \sum_{i=0}^{n-1}
\begin{vmatrix}
x_i & x_{i+1} \\
y_i & y_{i+1}
\end{vmatrix} \\
&= \frac{1}{2} \sum_{i=0}^{n-1} x_i y_{i+1} - x_{i+1} y_i
\end{align}
where :math:`x_n = x_0`
Polygon barycenter is determined by
.. math::
\begin{align}
\mathbf{C} &= \frac{1}{6\mathbf{A}} \sum_{i=0}^{n-1}
(P_i + P_{i+1}) \times (P_i \otimes P_{i+1}) \\
&= \frac{1}{6\mathbf{A}} \sum_{i=0}^{n-1}
\begin{pmatrix}
(x_i + x_{i+1}) (x_i y_{i+1} - x_{i+1} y_i) \\
(y_i + y_{i+1}) (x_i y_{i+1} - x_{i+1} y_i)
\end{pmatrix}
\end{align}
References
* On the Calculation of Arbitrary Moments of Polygons,
Carsten Steger,
Technical Report FGBV–96–05,
October 1996
* http://mathworld.wolfram.com/PolygonArea.html
* https://en.wikipedia.org/wiki/Polygon#Area_and_centroid
"""
if not self.is_simple:
return None
# area = self._points[-1].cross(self._points[0])
# for i in range(self.number_of_points):
# area *= self._points[i].cross(self._points[i+1])
# P0, P1, Pn-1, P0
points = self.closed_point_array
# from 0 to n-1 : P0, ..., Pn-1
xi = points[0,:-1]
yi = points[1,:-1]
# from 1 to n : P1, ..., Pn-1, P0
xi1 = points[0,1:]
yi1 = points[1,1:]
# Fixme: np.cross ???
cross = xi * yi1 - xi1 * yi
self._cross = cross
area = .5 * np.sum(cross)
if area == 0:
# print('Null area')
self._area = 0
self._barycenter = self.start_point
else:
factor = 1 / (6*area)
x = factor * np.sum((xi + xi1) * cross)
y = factor * np.sum((yi + yi1) * cross)
# area of a convex polygon is defined to be positive if the points are arranged in a
# counterclockwise order, and negative if they are in clockwise order (Beyer 1987).
self._area = abs(area)
self._barycenter = self.__vector_cls__(x, y)
##############################################
def _compute_inertia_moment(self):
r"""Compute inertia moment on vertices.
.. warning:: untrusted formulae
.. math::
\begin{align}
I_x &= \frac{1}{12} \sum (y_i^2 + y_i y_{i+1} + y_{i+1}^2) (x_i y_{i+1} - x_{i+1} y_i) \\
I_y &= \frac{1}{12} \sum (x_i^2 + x_i x_{i+1} + x_{i+1}^2) (x_i y_{i+1} - x_{i+1} y_i) \\
I_{xy} &= \frac{1}{24} \sum (x_i y_{i+1} + 2 x_i y_i + 2 x_{i+1} y_{i+1} + x_{i+1} y_i) (x_i y_{i+1} - x_{i+1} y_i)
\end{align}
Reference
* https://en.wikipedia.org/wiki/Second_moment_of_area#Any_cross_section_defined_as_polygon
"""
# self.recenter()
# Fixme: duplicated code
# P0, P1, Pn-1, P0
points = self.closed_point_array
# from 0 to n-1 : P0, ..., Pn-1
xi = points[0,:-1]
yi = points[1,:-1]
# from 1 to n : P1, ..., Pn-1, P0
xi1 = points[0,1:]
yi1 = points[1,1:]
# computation on vertices
number_of_points = self.number_of_points
Ix = np.sum(yi**2) / number_of_points
Iy = np.sum(xi**2) / number_of_points
Ixy = - np.sum(xi*yi) / number_of_points
# cross = xi * yi1 - xi1 * yi
# cross = self._cross
# Ix = 1/(12*self._area) * np.sum((yi**2 + yi*yi1 + yi1**2) * cross)
# Iy = 1/(12*self._area) * np.sum((xi**2 + xi*xi1 + xi1**2) * cross)
# Ixy = 1/(24*self._area) * np.sum((xi*yi1 + 2*(xi*yi + xi1*yi1) + xi1*yi) * cross)
# cx, cy = self._barycenter
# Ix -= cy**2
# Iy -= cx**2
# Ixy -= cx*cy
# Ix = -Ix
# Iy = -Iy
# print(Ix, Iy, Ixy)
if Ixy == 0:
if Iy >= Ix:
self._major_axis_angle = 0
lambda1 = Iy
lambda2 = Ix
vx = 0
v1y = 1
v2y = 0
else:
self._major_axis_angle = 90
lambda1 = Ix
lambda2 = Iy
vx = 1
v1y = 0
v2y = 1
else:
Is = Iy + Ix
Id = Ix - Iy
sqrt0 = math.sqrt(Id*Id + 4*Ixy*Ixy)
lambda1 = (Is + sqrt0) / 2
lambda2 = (Is - sqrt0) / 2
vx = Ixy
v1y = (Id + sqrt0) / 2
v2y = (Id - sqrt0) / 2
if lambda1 < lambda2:
v1y, v2y = v2y, v1y
lambda1, lambda2 = lambda2, lambda1
self._major_axis_angle = - math.degrees(math.atan(v1y/vx))
self._major_axis = 4 * math.sqrt(math.fabs(lambda1))
self._minor_axis = 4 * math.sqrt(math.fabs(lambda2))
if self._minor_axis != 0:
self._axis_ratio = self._major_axis / self._minor_axis
else:
self._axis_ratio = 0
##############################################
def _check_area(self):
if self.is_simple and self._area is None:
self._compute_area_barycenter()
##############################################
@property
def area(self):
"""Return polygon area."""
self._check_area()
return self._area
##############################################
@property
def barycenter(self):
"""Return polygon barycenter."""
self._check_area()
return self._barycenter
##############################################
def recenter(self):
"""Recenter the polygon to the barycenter."""
# if self._centred:
# return
barycenter = self._barycenter
for point in self._points:
point -= barycenter
# self._centred = True
##############################################
def _check_moment(self):
if self.is_simple and self._major_axis is None:
self._compute_inertia_moment()
##############################################
@property
def major_axis_angle(self):
self._check_moment()
return self._major_axis_angle
@property
def major_axis(self):
self._check_moment()
return self._major_axis
@property
def minor_axis(self):
self._check_moment()
return self._minor_axis
@property
def axis_ratio(self):
self._check_moment()
return self._axis_ratio
##############################################
def _crossing_number_test(self, point):
"""Crossing number test for a point in a polygon."""
# Wm. Randolph Franklin, "PNPOLY - Point Inclusion in Polygon Test" Web Page (2000)
# https://www.ecse.rpi.edu/Homepages/wrf/research/geom/pnpoly.html
crossing_number = 0
x = point.x
y = point.y
for edge in self.edges:
if ((edge.p0.y <= y < edge.p1.y) or # upward crossing
(edge.p1.y <= y < edge.p0.y)): # downward crossing
xi = edge.p0.x + (y - edge.p0.y) / edge.vector.slope
if x < xi:
crossing_number += 1
# Fixme: even/odd func
return (crossing_number & 1) == 1 # odd => in
##############################################
def _winding_number_test(self, point):
"""Winding number test for a point in a polygon."""
# more accurate than crossing number test
# http://geomalgorithms.com/a03-_inclusion.html#wn_PnPoly()
winding_number = 0
y = point.y
for edge in self.edges:
if edge.p0.y <= y:
if edge.p1.y > y: # upward crossing
if edge.is_left(point):
winding_number += 1
else:
if edge.p1.y <= y: # downward crossing
if edge.is_right(point):
winding_number -= 1
return winding_number > 0
##############################################
def is_point_inside(self, point):
# http://geomalgorithms.com/a03-_inclusion.html
# http://paulbourke.net/geometry/polygonmesh/#insidepoly
# Fixme: bounding box test
return self._winding_number_test(point)
Patro-master-Patro/Patro/GeometryEngine/Polyline.py 0000664 0000000 0000000 00000005327 13424433274 0022707 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement polyline.
"""
####################################################################################################
__all__ = ['Polyline2D']
####################################################################################################
from .Path import Path2D
from .Primitive import PrimitiveNP, Primitive2DMixin
from .Segment import Segment2D
####################################################################################################
class Polyline2D(Primitive2DMixin, PrimitiveNP):
"""Class to implements 2D Polyline."""
##############################################
def __init__(self, *points):
if len(points) < 2:
raise ValueError('Polyline require at least 2 vertexes')
PrimitiveNP.__init__(self, points)
self._edges = None
##############################################
@property
def edges(self):
if self._edges is None:
self._edges = [Segment2D(self._points[i], self._points[i+1])
for i in range(self.number_of_points -1)]
return iter(self._edges)
##############################################
@property
def length(self):
return sum([edge.magnitude for edge in self.edges])
##############################################
def distance_to_point(self, point):
distance = None
for edge in self.edges:
edge_distance = edge.distance_to_point(point)
if distance is None or edge_distance < distance:
distance = edge_distance
return distance
##############################################
def to_path(self):
path = Path2D(self.start_point)
for point in self.iter_from_second_point():
path.line_to(point)
return path
Patro-master-Patro/Patro/GeometryEngine/Primitive.py 0000664 0000000 0000000 00000036404 13424433274 0023064 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement base classes for primitives.
"""
# Fixme:
# length, interpolate path
# area
####################################################################################################
__all__ = [
'Primitive',
'Primitive2DMixin',
'Primitive1P',
'Primitive2P',
'Primitive3P',
'Primitive4P',
]
####################################################################################################
from collections import abc as collections
import numpy as np
from .BoundingBox import bounding_box_from_points
# Fixme: circular import
# from .Transformation import Transformation2D
####################################################################################################
class Primitive:
"""Base class for geometric primitive"""
__vector_cls__ = None
##############################################
@property
def dimension(self):
"""Dimension in [2, 3] for 2D / 3D primitive"""
raise NotImplementedError
##############################################
@property
def is_infinite(self):
"""True if the primitive has infinite extend like a line"""
return False
##############################################
@property
def is_closed(self):
"""True if the primitive is a closed path."""
return False
##############################################
@property
def number_of_points(self):
"""Number of points which define the primitive."""
raise NotImplementedError
def __len__(self):
return self.number_of_points
##############################################
@property
def is_reversible(self):
"""True if the order of the points is reversible"""
# Fixme: True if number_of_points > 1 ???
return False
##############################################
# Fixme: part of the API imply points
# it is not true for Path2D
@property
def points(self):
raise NotImplementedError
##############################################
# @points.setter
# def points(self):
# raise NotImplementedError
def _set_points(self, points):
raise NotImplementedError
##############################################
def __repr__(self):
return self.__class__.__name__ + str([str(p) for p in self.points])
##############################################
def clone(self):
return self.__class__(*self.points)
##############################################
@property
def bounding_box(self):
"""Bounding box of the primitive.
Return None if primitive is infinite.
"""
# Fixme: cache
if self.is_infinite:
return None
else:
return bounding_box_from_points(self.points)
##############################################
def reverse(self):
return self
##############################################
def transform(self, transformation, clone=False):
"""Apply a transformation to the primitive.
If *clone* is set then the primitive is cloned.
"""
obj = self.clone() if clone else self
if not transformation.is_identity:
obj.apply_transformation(transformation)
return obj
##############################################
def apply_transformation(self, transformation):
"""Apply a transformation to the primitive.
If *clone* is set then the primitive is cloned.
"""
# for point in self.points:
# point *= transformation # don't work
self._set_points([transformation*p for p in self.points])
##############################################
def mirror(self, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.Parity(), clone)
def x_mirror(self, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.XReflection(), clone)
def y_mirror(self, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.YReflection(), clone)
def rotate(self, angle, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.Rotation(angle), clone)
def scale(self, x_factor, y_factor=None, clone=False):
from .Transformation import Transformation2D
return self.transform(Transformation2D.Scale(x_factor, y_factor), clone)
##############################################
@property
def point_array(self):
r"""Return the geometry matrix as a Numpy array.
.. math::
\mathrm{Geometry\ Matrix} =
\begin{bmatrix}
x_0 & x_1 & \ldots & x_{n-1} \\
y_0 & y_1 & \ldots & y_{n-1}
\end{bmatrix}
"""
# Fixme: geometry_matrix vs point_array
# Fixme: cache ??? but point set and init
# if self._point_array is None:
# self._point_array = np.array(list(self.points)).transpose()
# return self._point_array
return np.array(list(self.points)).transpose()
##############################################
def is_point_close(self, other):
# Fixme: verus is_closed
# is_similar
return np.allclose(self.point_array, other.point_array)
####################################################################################################
class Primitive2DMixin:
__vector_cls__ = None # Fixme: due to import, done in module's __init__.py
# __dimension__ = 2
@property
def dimension(self):
return 2
####################################################################################################
class ClosedPrimitiveMixin:
# Fixme: should be reversible
##############################################
@property
def is_closed(self):
"""True if the primitive is a closed path."""
return True
##############################################
@property
def closed_points(self):
points = list(self.points)
points.append(self.start_point)
return points
##############################################
@property
def closed_point_array(self):
r"""Return the geometry matrix as a Numpy array for a closed primitive.
.. math::
\mathrm{Geometry\ Matrix} =
\begin{bmatrix}
x_0 & x_1 & \ldots & x_{n-1} & x_0 \\
y_0 & y_1 & \ldots & y_{n-1} & y_0
\end{bmatrix}
"""
# Fixme: place, func for closed_point
# Fixme: cache ???
return np.array(self.closed_points).transpose()
####################################################################################################
class ReversiblePrimitiveMixin:
##############################################
@property
def is_reversible(self):
True
##############################################
@property
def reversed_points(self):
raise NotImplementedError
# return reversed(list(self.points))
##############################################
def reverse(self):
return self.__class__(*self.reversed_points)
##############################################
@property
def start_point(self):
raise NotImplementedError
@property
def end_point(self):
raise NotImplementedError
####################################################################################################
class Primitive1P(Primitive):
##############################################
def __init__(self, p0):
self.p0 = p0
##############################################
@property
def number_of_points(self):
return 1
@property
def p0(self):
return self._p0
@p0.setter
def p0(self, value):
self._p0 = self.__vector_cls__(value)
##############################################
@property
def points(self):
return iter(self._p0) # Fixme: efficiency ???
@property
def reversed_points(self):
return self.points
##############################################
def _set_points(self, points):
self._p0 = points
####################################################################################################
class Primitive2P(Primitive1P, ReversiblePrimitiveMixin):
##############################################
def __init__(self, p0, p1):
# We don't call super().__init__(p0) for speed
self.p0 = p0
self.p1 = p1
##############################################
# Redundant code ... until we don't use self._points = []
@property
def number_of_points(self):
return 2
@property
def p1(self):
return self._p1
@p1.setter
def p1(self, value):
self._p1 = self.__vector_cls__(value)
##############################################
@property
def start_point(self):
return self._p0
@property
def end_point(self):
return self._p1
##############################################
@property
def points(self):
return iter((self._p0, self._p1))
@property
def reversed_points(self):
return iter((self._p1, self._p0))
##############################################
def _set_points(self, points):
self._p0, self._p1 = points
##############################################
def interpolate(self, t):
"""Return the linear interpolate of two points."""
return self._p0 * (1 - t) + self._p1 * t
####################################################################################################
class Primitive3P(Primitive2P):
##############################################
def __init__(self, p0, p1, p2):
self.p0 = p0
self.p1 = p1
self.p2 = p2
##############################################
@property
def number_of_points(self):
return 3
@property
def p2(self):
return self._p2
@p2.setter
def p2(self, value):
self._p2 = self.__vector_cls__(value)
##############################################
@property
def end_point(self):
return self._p2
##############################################
@property
def points(self):
return iter((self._p0, self._p1, self._p2))
@property
def reversed_points(self):
# Fixme: share code ???
return iter((self._p2, self._p1, self._p0))
def iter_from_second_point(self):
# Fixme: share code ???
return iter(self._p1, self._p2)
##############################################
def _set_points(self, points):
self._p0, self._p1, self._p2 = points
####################################################################################################
class Primitive4P(Primitive3P):
##############################################
def __init__(self, p0, p1, p2, p3):
self.p0 = p0
self.p1 = p1
self.p2 = p2
self.p3 = p3
##############################################
@property
def number_of_points(self):
return 4
@property
def p3(self):
return self._p3
@p3.setter
def p3(self, value):
self._p3 = self.__vector_cls__(value)
##############################################
@property
def end_point(self):
return self._p3
##############################################
@property
def points(self):
return iter((self._p0, self._p1, self._p2, self._p3))
@property
def reversed_points(self):
return iter((self._p3, self._p2, self._p1, self._p0))
def iter_from_second_point(self):
return iter(self._p1, self._p2, self._p3)
##############################################
def _set_points(self, points):
self._p0, self._p1, self._p2, self._p3 = points
####################################################################################################
class PrimitiveNP(Primitive, ReversiblePrimitiveMixin):
##############################################
@staticmethod
def handle_points(points):
if len(points) == 1 and isinstance(points[0], collections.Iterable):
points = points[0]
return points
##############################################
def __init__(self, *points):
points = self.handle_points(points)
self._points = [self.__vector_cls__(p) for p in points]
self._point_array = None
##############################################
@property
def number_of_points(self):
return len(self._points)
##############################################
@property
def start_point(self):
return self._points[0]
@property
def end_point(self):
return self._points[-1]
##############################################
@property
def points(self):
return iter(self._points)
@property
def reversed_points(self):
return reversed(self._points)
def iter_from_second_point(self):
return iter(self._points[1:])
##############################################
def _set_points(self, points):
self._points = points
self._point_array = None
##############################################
def __getitem__(self, _slice):
return self._points[_slice]
##############################################
def iter_on_nuplets(self, size):
if size > self.number_of_points:
raise ValueError('size {} > number of points {}'.format(size, self.number_of_points))
for i in range(self.number_of_points - size +1):
yield self._points[i:i+size]
####################################################################################################
class PolygonMixin:
##############################################
def to_polygon(self):
from .Polygon import Polygon2D
return Polygon2D(self.points)
####################################################################################################
class PathMixin:
##############################################
def to_path(self):
from .Path import Path2D
path = Path2D(self.start_point)
for point in self.iter_from_second_point():
path.line_to(point)
if self.is_closed:
path.close()
return path
Patro-master-Patro/Patro/GeometryEngine/Rectangle.py 0000664 0000000 0000000 00000006633 13424433274 0023021 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement rectangle.
"""
####################################################################################################
__all__ = ['Rectangle2D']
####################################################################################################
import math
from .Path import Path2D
from .Primitive import Primitive2P, ClosedPrimitiveMixin, PathMixin, PolygonMixin, Primitive2DMixin
from .Segment import Segment2D
####################################################################################################
class Rectangle2D(Primitive2DMixin, ClosedPrimitiveMixin, PathMixin, PolygonMixin, Primitive2P):
"""Class to implements 2D Rectangle."""
##############################################
def __init__(self, p0, p1):
# if p1 == p0:
# raise ValueError('Rectangle reduced to a point')
Primitive2P.__init__(self, p0, p1)
##############################################
@classmethod
def from_point_and_offset(self, p0, v):
return cls(p0, p0+v)
@classmethod
def from_point_and_radius(self, p0, v):
return cls(p0-v, p0+v)
##############################################
@property
def is_closed(self):
return True
##############################################
@property
def p01(self):
return self.__vector_cls__(self._p0.x, self._p1.y)
@property
def p10(self):
return self.__vector_cls__(self._p1.x, self._p0.y)
@property
def edges(self):
p0 = self._p0
p1 = self.p01
p2 = self._p1
p3 = self.p10
return (
Segment2D(p0, p1),
Segment2D(p1, p2),
Segment2D(p2, p3),
Segment2D(p3, p0),
)
##############################################
@property
def diagonal(self):
return self._p1 - self._p0
##############################################
@property
def perimeter(self):
d = self.diagonal
return 2*(abs(d.x) + abs(d.y))
##############################################
@property
def area(self):
d = self.diagonal
return abs(d.x * d.y)
##############################################
def is_point_inside(self, point):
bounding_box = self.bounding_box
return (point.x in bounding_box.x and
point.y in bounding_box.y)
##############################################
def distance_to_point(self, point):
raise NotImplementedError
Patro-master-Patro/Patro/GeometryEngine/Segment.py 0000664 0000000 0000000 00000013122 13424433274 0022506 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement segment.
"""
####################################################################################################
__all__ = ['Segment2D']
####################################################################################################
# from .Interpolation import interpolate_two_points
from .Line import Line2D
from .Primitive import Primitive2P, Primitive2DMixin
from .Triangle import triangle_orientation
from .Vector import Vector2D
####################################################################################################
class Segment2D(Primitive2DMixin, Primitive2P):
"""Class to implement 2D Segment"""
# Fixme: _p0 versus p0
#######################################
def __init__(self, p0, p1):
"""Construct a :class:`Segment2D` between two points."""
Primitive2P.__init__(self, p0, p1)
##############################################
@property
def vector(self):
return self._p1 - self._p0
@property
def length(self):
return self.vector.magnitude
@property
def center(self):
# midpoint, barycenter
return (self._p0 + self._p1) / 2
##############################################
@property
def cross_product(self):
return self._p0.cross(self._p1)
##############################################
def to_line(self):
# Fixme: cache
return Line2D.from_two_points(self._p0, self._p1)
##############################################
point_at_t = Primitive2P.interpolate
##############################################
def intersect_with(self, segment2):
"""Checks if the line segments intersect.
return 1 if there is an intersection
0 otherwise
"""
segment1 = self
# triangle_orientation returns 0 if two points are identical, except from the situation
# when p0 and p1 are identical and different from p2
ccw11 = triangle_orientation(segment1.p0, segment1.p1, segment2.p0)
ccw12 = triangle_orientation(segment1.p0, segment1.p1, segment2.p1)
ccw21 = triangle_orientation(segment2.p0, segment2.p1, segment1.p0)
ccw22 = triangle_orientation(segment2.p0, segment2.p1, segment1.p1)
return (((ccw11 * ccw12 < 0) and (ccw21 * ccw22 < 0))
# one ccw value is zero to detect an intersection
or (ccw11 * ccw12 * ccw21 * ccw22 == 0))
##############################################
def intersection(self, segment2):
# P = (1-t) * Pa0 + t * Pa1 = Pa0 + t * (Pa1 - Pa0)
# = (1-u) * Pb0 + u * Pb1 = Pb0 + u * (Pb1 - Pb0)
line1 = self.to_line()
line2 = segment2.to_line()
s1, s2 = line1.intersection_abscissae(line2)
if s1 is None:
return None, None
else:
intersect = (0 <= s1 <= 1) and (0 <= s2 <= 1)
return self.interpolate(s1), intersect
##############################################
def share_vertex_with(self, segment2):
# return (
# self._p0 == segment2._p0 or
# self._p0 == segment2._p1 or
# self._p1 == segment2._p0 or
# self._p1 == segment2._p1
# )
if (self._p0 == segment2._p0 or
self._p0 == segment2._p1):
return self._p0
elif (self._p1 == segment2._p0 or
self._p1 == segment2._p1):
return self._p1
else:
return None
##############################################
def side_of(self, point):
"""Tests if a point is left/on/right of a line.
> 0 if point is left of the line
= 0 if point is on the line
< 0 if point is right of the line
"""
v1 = self.vector
v2 = point - self._p0
return v1.cross(v2)
##############################################
def left_of(self, point):
"""Tests if a point is left a line"""
return self.side_of(point) > 0
def right_of(self, point):
"""Tests if a point is right a line"""
return self.side_of(point) < 0
def is_collinear(self, point):
"""Tests if a point is on line"""
return self.side_of(point) == 0
##############################################
def distance_to_point(self, point):
line = self.to_line()
if line.v.magnitude_square == 0:
return (self._p0 - point).magnitude
d, s = line.distance_and_abscissa_to_line(point)
if 0 <= s <= self.length:
return abs(d)
else:
if s < 0:
p = self._p0
else:
p = self._p1
return (p - point).magnitude
Patro-master-Patro/Patro/GeometryEngine/Spline.py 0000664 0000000 0000000 00000033256 13424433274 0022350 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
r"""Module to implement Spline curve.
For resources on Spline curve see :ref:`this section `.
"""
####################################################################################################
#
# Notes: algorithm details are on spline.rst
#
####################################################################################################
####################################################################################################
__all__ = ['BSpline2D']
####################################################################################################
# from math import log, sqrt
import numpy as np
from .Bezier import QuadraticBezier2D, CubicBezier2D
from .Primitive import Primitive3P, Primitive4P, PrimitiveNP, Primitive2DMixin
####################################################################################################
class QuadraticUniformSpline2D(Primitive2DMixin, Primitive3P):
"""Class to implements 2D Quadratic Spline Curve."""
BASIS = np.array((
(1, -2, 1),
(1, 2, -2),
(0, 0, 1),
))
INVERSE_BASIS = np.array((
(-2, 1, -2),
(-2, -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)
##############################################
def to_bezier(self):
basis = np.dot(self.BASIS, QuadraticBezier2D.INVERSE_BASIS)
points = np.dot(self.point_array, basis).transpose()
return QuadraticBezier2D(*points)
##############################################
def point_at_t(self, t):
# Q(t) = (
# P0 * (1-t)**3 +
# P1 * ( 3*t**3 - 6*t**2 + 4 ) +
# P2 * ( -3*t**3 + 3*t**2 + 3*t + 1 ) +
# P3 * t**3
# ) / 6
#
# = P0*(1-t)**3/6 + P1*(3*t**3 - 6*t**2 + 4)/6 + P2*(-3*t**3 + 3*t**2 + 3*t + 1)/6 + P3*t**3/6
return (self._p0/6 + self._p1*2/3 + self._p2/6 +
(-self._p0/2 + self._p2/2)*t +
(self._p0/2 - self._p1 + self._p2/2)*t**2 +
(-self._p0/6 + self._p1/2 - self._p2/2 + self._p3/6)*t**3)
####################################################################################################
class CubicUniformSpline2D(Primitive2DMixin, Primitive4P):
"""Class to implements 2D Cubic Spline Curve."""
# T = (1 t t**2 t**3)
# P = (Pi Pi+2 Pi+2 Pi+3)
# Q(t) = T M Pt
# = P Mt Tt
# Basis = Mt
BASIS = np.array((
(1, -3, 3, -1),
(4, 0, -6, 3),
(1, 3, 3, -3),
(0, 0, 0, 1),
)) / 6
INVERSE_BASIS = np.array((
( 1, 1, 1, 1),
( -1, 0, 1, 2),
(2/3, -1/3, 2/3, 11/3),
( 0, 0, 0, 6),
))
#######################################
def __init__(self, p0, p1, p2, p3):
Primitive4P.__init__(self, p0, p1, p2, p3)
##############################################
def __repr__(self):
return self.__class__.__name__ + '({0._p0}, {0._p1}, {0._p2}, {0._p3})'.format(self)
##############################################
def to_bezier(self):
basis = np.dot(self.BASIS, CubicBezier2D.INVERSE_BASIS)
points = np.dot(self.point_array, basis).transpose()
if self._start:
# list(self.points)[:2]
points[:2] = self._p0, self._p1
elif self._stop:
# list(self.points)[-2:]
points[-2:] = self._p2, self._p3
return CubicBezier2D(*points)
##############################################
def point_at_t(self, t):
# Q(t) = (
# P0 * (1-t)**3 +
# P1 * ( 3*t**3 - 6*t**2 + 4 ) +
# P2 * ( -3*t**3 + 3*t**2 + 3*t + 1 ) +
# P3 * t**3
# ) / 6
#
# = P0*(1-t)**3/6 + P1*(3*t**3 - 6*t**2 + 4)/6 + P2*(-3*t**3 + 3*t**2 + 3*t + 1)/6 + P3*t**3/6
return (self._p0/6 + self._p1*2/3 + self._p2/6 +
(-self._p0/2 + self._p2/2)*t +
(self._p0/2 - self._p1 + self._p2/2)*t**2 +
(-self._p0/6 + self._p1/2 - self._p2/2 + self._p3/6)*t**3)
####################################################################################################
class BSpline2D(Primitive2DMixin, PrimitiveNP):
"""Class to implement a 2D B-Spline curve.
"""
##############################################
@staticmethod
def uniform_knots(degree, number_of_points):
order = degree + 1
if number_of_points < order:
raise ValueError('Inconsistent degree and number of points')
knots = list(range(number_of_points - degree +1))
return [0]*degree + knots + [knots[-1]]*degree
##############################################
@classmethod
def check_for_unifom_knots(cls, degree, number_of_points, knots):
return np.array_equal(cls.uniform_knots(degree, number_of_points), knots)
##############################################
def __init__(self, points, degree, closed=False, knots=None):
points = self.handle_points(points)
PrimitiveNP.__init__(self, points)
self._degree = int(degree)
self._closed = bool(closed) # Fixme: not implemented
if knots is not None:
self._knots = list(knots)
if not np.all(np.diff(self._knots) >= 0):
raise ValueError('Invalid knots {}'.format(knots))
# Fixme: check_for_unifom_knots
self._uniform = False # Fixme:
else:
self._knots = self.uniform_knots(self._degree, self.number_of_points)
self._uniform = True
# self._number_of_points = len(self._knots) - self._degree - 1
# assert (self.number_of_points >= self.order and
# (len(self._coefficients) >= self._number_of_points))
##############################################
@property
def degree(self):
return self._degree
@property
def order(self):
return self._degree +1
@property
def is_closed(self):
return self._closed
@property
def uniform(self):
return self._uniform
@property
def knots(self):
return self._knots
@property
def start_knot(self):
if self._uniform:
return 0
else:
return self._knots[0]
@property
def end_knot(self):
if self._uniform:
return self.number_of_points - self._degree
else:
return self._knots[-1]
@property
def knot_iter(self):
if self._uniform:
return range(self.end_knot)
else:
return iter(self._knots)
@property
def number_of_spans(self):
if self._uniform:
count = self.number_of_points - self._degree
if self._closed:
count += 1
return count
else:
# multiplicity
raise NotImplementedError
##############################################
def span(self, t):
if not(self.start_knot <= t <= self.end_knot):
raise ValueError('Invalid t {}'.format(t))
if self._uniform:
return int(t) + self._degree # start padding
else:
for i, t_span in enumerate(self._knots):
if t < t_span:
return i-1
##############################################
def knot_multiplicity(self, knot):
count = 0
for t in self._knots:
if t == knot:
count += 1
elif t > knot:
break
return count
##############################################
def basis_function(self, i, k, t):
"""De Boor-Cox recursion formula"""
if k == 0:
return 1 if self._knots[i] <= t < self._knots[i+1] else 0
ki = self._knots[i]
kik = self._knots[i+k]
if kik == ki:
c1 = 0
else:
c1 = (t - ki)/(kik - ki) * self.basis_function(i, k-1, t)
ki = self._knots[i+1]
kik = self._knots[i+k+1]
if kik == ki:
c2 = 0
else:
c2 = (kik - t)/(kik - ki) * self.basis_function(i+1, k-1, t)
return c1 + c2
##############################################
def _deboor(self, t):
"""Compute point at t using De Boor algorithm"""
# l_minus_degree = int(t) # span index
# l = l_minus_degree + self._degree # knot index
l = self.span(t)
l_minus_degree = l - self._degree
knots = self._knots
points = [self._points[j + l_minus_degree] for j in range(self.order)]
for r in range(1, self.order):
for j in range(self._degree, r-1, -1):
k = j + l_minus_degree
d = knots[j+1+l-r] - knots[k]
if d == 0:
alpha = 0
else:
alpha = (t - knots[k]) / d
if alpha == 0:
point = points[j-1]
elif alpha == 1:
point = points[j]
else:
point = points[j-1] * (1 - alpha) + points[j] * alpha
points[j] = point
return points[self._degree]
##############################################
def _naive_point_at_t(self, t):
"""Compute point at t using a naive algorithm"""
basis = np.array([self.basis_function(i, self._degree, t)
for i in range(self.number_of_points)])
points = self.point_array
return self.__vector_cls__(np.dot(basis, points))
##############################################
def point_at_t(self, t, naive=False):
# Spline curve as a Bézier span at start and end
if self._uniform:
if t == 0:
return self.start_point
elif t == self.end_knot:
# else computation fail
return self.end_point
if naive:
return self._naive_point_at_t(t)
else:
return self._deboor(t)
##############################################
def insert_knot(self, t):
# http://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/B-spline/single-insertion.html
# t lie in the [t_l, t_{l+1}[ span
# this span is only affected by the control points: P_l, ..., P_{l-degree}
# (number of points = degree + 1 = order)
degree = self._degree
points = self._points
knots = self._knots
l = self.span(t)
new_points = []
for i in range(self.number_of_points +1):
# compute w
if i <= l - degree:
w = 1 # same point
elif i > l:
w = 0 # previous point
else: # blend previous and current point
ti = knots[i]
d = knots[i + degree] - ti
if d > 0:
w = (t - ti) / d
else:
w = 0
if w == 0:
point = points[i-1]
elif w == 1:
point = points[i]
else:
point = points[i-1] * (1 - w) + points[i] * w
new_points.append(point)
# l += degree
knots = self._knots[:l+1] + [t] + self._knots[l+1:]
return self.__class__(new_points, self._degree, knots=knots)
##############################################
def to_bezier_form(self, degree=None):
if not self._uniform:
raise ValueError('Must be uniform')
if degree is None:
degree = self._degree
new_spline = self
for t in range(1, self.end_knot):
multiplicity = self.knot_multiplicity(t)
for i in range(degree - multiplicity +1):
new_spline = new_spline.insert_knot(t)
return new_spline
##############################################
def to_bezier(self):
from . import Bezier
if self._degree == 2:
cls = Bezier.QuadraticBezier2D
elif self._degree >= 3:
cls = Bezier.CubicBezier2D
else:
not NotImplementedError
# Fixme: degree > 3
spline = self.to_bezier_form()
bezier_curves = []
order = spline.order
for i in range(self.number_of_spans):
i_inf = i * order
i_sup = i_inf + order
points = spline._points[i_inf:i_sup]
bezier = cls(*points)
bezier_curves.append(bezier)
return bezier_curves
Patro-master-Patro/Patro/GeometryEngine/Transformation.py 0000664 0000000 0000000 00000031622 13424433274 0024117 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
r"""Module to implement transformations like scale, rotation and translation.
For resources on transformations see :ref:`this section `.
"""
####################################################################################################
__all__ = [
'TransformationType',
'Transformation',
'Transformation2D',
'AffineTransformation',
'AffineTransformation2D',
]
####################################################################################################
from enum import Enum, auto
from math import sin, cos, radians, degrees
import numpy as np
from .Vector import Vector2D, HomogeneousVector2D
####################################################################################################
class TransformationType(Enum):
Identity = auto()
Scale = auto() # same scale factor across axes
Shear = auto() # different scale factor
Parity = auto()
XParity = auto()
YParity = auto()
Rotation = auto()
Translation = auto()
Generic = auto()
####################################################################################################
class IncompatibleArrayDimension(ValueError):
pass
####################################################################################################
class Transformation:
__dimension__ = None
__size__ = None
##############################################
@classmethod
def Identity(cls):
return cls(np.identity(cls.__size__), TransformationType.Identity)
##############################################
def __init__(self, obj, transformation_type=TransformationType.Generic):
if isinstance(obj, Transformation):
if self.same_dimension(obj):
array = obj.array # *._m
else:
raise IncompatibleArrayDimension
elif isinstance(obj, np.ndarray):
if obj.shape == (self.__size__, self.__size__):
array = obj
else:
raise IncompatibleArrayDimension
elif isinstance(obj, (list, tuple)):
if len(obj) == self.__size__ **2:
array = np.array(obj)
array.shape = (self.__size__, self.__size__)
else:
raise IncompatibleArrayDimension
else:
array = np.array((self.__size__, self.__size__))
array[...] = obj
self._m = np.array(array)
if transformation_type == TransformationType.Generic:
transformation_type = self._check_type()
self._type = transformation_type
##############################################
@property
def dimension(self):
return self.__dimension__
@property
def size(self):
return self._size
@property
def array(self):
return self._m
@property
def type(self):
return self._type
@property
def is_identity(self):
return self._type == TransformationType.Identity
##############################################
def __repr__(self):
return self.__class__.__name__ + str(self._m)
##############################################
def to_list(self):
return list(self._m.flat)
##############################################
def same_dimension(self, other):
return self.__size__ == other.dimension
##############################################
def _check_type(self):
raise NotImplementedError
##############################################
def _mul_type(self, obj):
# Fixme: check matrix value ???
# usage identity/rotation, scale/parity test
# metric test ?
# if t in (parity, xparity, yparity) t*t = Id
# if t in (rotation, scale) t*t = t
if self._type == obj._type:
if self._type in (
TransformationType.Parity,
TransformationType.XParity,
TransformationType.YParity
):
return TransformationType.Identity
elif self._type not in (TransformationType.Rotation, TransformationType.Scale):
return TransformationType.Generic
else:
return self._type
else: # shear, generic
return TransformationType.Generic
#######################################
def __mul__(self, obj):
"""Return self * obj composition."""
if isinstance(obj, Transformation):
# T = T1 * T2
array = np.matmul(self._m, obj.array)
return self.__class__(array, self._mul_type(obj))
elif isinstance(obj, Vector2D):
array = np.matmul(self._m, np.transpose(obj.v))
return Vector2D(array)
elif isinstance(obj, (int, float)):
# Scalar can only be scaled if the frame is not sheared
if self._type in (TransformationType.Identity, TransformationType.Rotation):
return obj
elif self._type not in (TransformationType.Shear, TransformationType.Generic):
return abs(self._m[0,0]) * obj
else:
raise ValueError('Transformation is sheared')
else:
raise ValueError
#######################################
def __imul__(self, obj):
"""Set transformation to obj * self composition."""
# Fixme: order ???
if isinstance(obj, Transformation):
if obj.type != TransformationType.Identity:
# (T = T1) *= T2
# T = T2 * T1
# order is inverted !
self._m = np.matmul(obj.array, self._m)
self._type = self._mul_type(obj)
if self._type == TransformationType.Generic:
self._type = self._check_type()
else:
raise ValueError
return self
####################################################################################################
class Transformation2D(Transformation):
__dimension__ = 2
__size__ = 2
##############################################
@classmethod
def type_for_scale(cls, x_scale, y_scale):
if x_scale == y_scale:
if x_scale == 1:
transformation_type = TransformationType.Identity
elif x_scale == -1:
transformation_type = TransformationType.Parity
else:
transformation_type = TransformationType.Scale
else:
if x_scale == -1 and y_scale == 1:
transformation_type = TransformationType.XParity
elif x_scale == 1 and y_scale == -1:
transformation_type = TransformationType.YParity
else:
transformation_type = TransformationType.Shear
return transformation_type
##############################################
@classmethod
def check_matrix_type(self, matrix):
# Fixme: check
m00, m01, m10, m11 = matrix
if m01 == 0 and m10 == 0:
if m00 == 1:
if m11 == 1:
return TransformationType.Identity
elif m11 == -1:
return TransformationType.YParity
elif m00 == -1:
if m11 == 1:
return TransformationType.XParity
elif m11 == -1:
return TransformationType.Parity
elif m00 == m11:
return TransformationType.Scale
# Fixme: check for rotation
return TransformationType.Generic
##############################################
def _check_type(self):
return self._check_matrix_type(self.to_list())
##############################################
@classmethod
def Rotation(cls, angle):
angle = radians(angle)
c = cos(angle)
s = sin(angle)
return cls(
np.array((
(c, -s),
(s, c))),
TransformationType.Rotation,
)
##############################################
@classmethod
def Scale(cls, x_scale, y_scale=None):
if y_scale is None:
y_scale = x_scale
transformation_type = cls.type_for_scale(x_scale, y_scale)
return cls(np.array(((x_scale, 0), (0, y_scale))), transformation_type)
##############################################
@classmethod
def Parity(cls):
return cls.Scale(-1, -1)
##############################################
@classmethod
def XReflection(cls):
return cls.Scale(-1, 1)
##############################################
@classmethod
def YReflection(cls):
return cls.Scale(1, -1)
####################################################################################################
class AffineTransformation(Transformation):
##############################################
@classmethod
def Translation(cls, vector):
transformation = cls.Identity()
transformation.translation_part[...] = vector.v[...]
transformation._type = TransformationType.Translation
return transformation
##############################################
@classmethod
def RotationAt(cls, center, angle):
# return cls.Translation(center) * cls.Rotation(angle) * cls.Translation(-center)
transformation = cls.Translation(-center)
transformation *= cls.Rotation(angle)
transformation *= cls.Translation(center)
return transformation
##############################################
@property
def matrix_part(self):
return self._m[:self.__dimension__,:self.__dimension__]
@property
def translation_part(self):
return self._m[:self.__dimension__,-1]
####################################################################################################
class AffineTransformation2D(AffineTransformation):
__dimension__ = 2
__size__ = 3
##############################################
def _check_type(self):
matrix_type = Transformation2D.check_matrix_type(self.matrix_part.flat)
# Fixme: translation etc. !!!
##############################################
@classmethod
def Rotation(cls, angle):
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Rotation(angle).array
transformation._type = TransformationType.Rotation
return transformation
##############################################
@classmethod
def Scale(cls, x_scale, y_scale):
# Fixme: others, use *= ? (comment means ???)
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Scale(x_scale, y_scale).array
transformation._type = cls.type_for_scale(x_scale, y_scale)
return transformation
##############################################
@classmethod
def Screen(cls, y_height):
transformation = cls.Identity()
# Fixme: better ?
transformation.matrix_part[...] = Transformation2D.YReflection().array
transformation.translation_part[...] = Vector2D(0, y_height).v[...]
transformation._type = TransformationType.Generic
return transformation
#######################################
def __mul__(self, obj):
if isinstance(obj, HomogeneousVector2D):
array = np.matmul(self._m, obj.v)
return obj.__class__(array)
elif isinstance(obj, Vector2D):
array = np.matmul(self._m, HomogeneousVector2D(obj).v)
# return HomogeneousVector2D(array).to_vector()
return Vector2D(array[:2])
else:
return super(AffineTransformation, self).__mul__(obj)
####################################################################################################
# The matrix to rotate an angle θ about the axis defined by unit vector (l, m, n) is
# l*l*(1-c) + c , m*l*(1-c) - n*s , n*l*(1-c) + m*s
# l*m*(1-c) + n*s , m*m*(1-c) + c , n*m*(1-c) - l*s
# l*n*(1-c) - m*s , m*n*(1-c) + l*s , n*n*(1-c) + c
Patro-master-Patro/Patro/GeometryEngine/Triangle.py 0000664 0000000 0000000 00000020322 13424433274 0022651 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement triangle.
"""
####################################################################################################
__all__ = ['Triangle2D']
####################################################################################################
import math
from .Primitive import Primitive3P, ClosedPrimitiveMixin, PathMixin, PolygonMixin, Primitive2DMixin
from .Line import Line2D
####################################################################################################
def triangle_orientation(p0, p1, p2):
"""Return the triangle orientation defined by the three points."""
dx1 = p1.x - p0.x
dy1 = p1.y - p0.y
dx2 = p2.x - p0.x
dy2 = p2.y - p0.y
# second slope is greater than the first one --> counter-clockwise
if dx1 * dy2 > dx2 * dy1:
return 1
# first slope is greater than the second one --> clockwise
elif dx1 * dy2 < dx2 * dy1:
return -1
# both slopes are equal --> collinear line segments
else:
# p0 is between p1 and p2
if dx1 * dx2 < 0 or dy1 * dy2 < 0:
return -1
# p2 is between p0 and p1, as the length is compared
# square roots are avoided to increase performance
elif dx1 * dx1 + dy1 * dy1 >= dx2 * dx2 + dy2 * dy2:
return 0
# p1 is between p0 and p2
else:
return 1
####################################################################################################
def same_side(p1, p2, a, b):
"""Return True if the points p1 and p2 lie on the same side of the edge [a, b]."""
v = b - a
cross1 = v.cross(p1 - a)
cross2 = v.cross(p2 - a)
# return cross1.dot(cross2) >= 0
return cross1*cross2 >= 0
####################################################################################################
class Triangle2D(Primitive2DMixin, ClosedPrimitiveMixin, PathMixin, PolygonMixin, Primitive3P):
"""Class to implements 2D Triangle."""
##############################################
def __init__(self, p0, p1, p2):
if (p1 - p0).is_parallel((p2 - p0)):
raise ValueError('Flat triangle')
Primitive3P.__init__(self, p0, p1, p2)
# self._p10 = None
# self._p21 = None
# self._p02 = None
##############################################
@property
def is_closed(self):
return True
##############################################
@property
def edges(self):
# Fixme: circular import, Segment import triangle_orientation
from .Segment import Segment2D
p0 = self._p0
p1 = self._p1
p2 = self._p2
return (
Segment2D(p0, p1),
Segment2D(p1, p2),
Segment2D(p2, p0),
)
##############################################
@property
def bisector_vector0(self):
return (self._p1 - self._p0) + (self._p2 - self._p0)
@property
def bisector_vector1(self):
return (self._p0 - self._p1) + (self._p2 - self._p1)
@property
def bisector_vector2(self):
return (self._p1 - self._p2) + (self._p0 - self._p2)
##############################################
@property
def bisector_line0(self):
return Line2D(self._p0, self.bisector_vector0)
@property
def bisector_line1(self):
return Line2D(self._p1, self.bisector_vector1)
@property
def bisector_line2(self):
return Line2D(self._p2, self.bisector_vector2)
##############################################
def _cache_length(self):
if not hasattr(self._p10):
self._p10 = (self._p1 - self._p0).magnitude
self._p21 = (self._p2 - self._p1).magnitude
self._p02 = (self._p0 - self._p2).magnitude
##############################################
def _cache_angle(self):
if not hasattr(self._a10):
self._a10 = (self._p1 - self._p0).orientation
self._a21 = (self._p2 - self._p1).orientation
self._a02 = (self._p0 - self._p2).orientation
##############################################
@property
def perimeter(self):
self._cache_length()
return self._p10 + self._p21 + self._p02
##############################################
@property
def area(self):
# using height
# = base * height / 2
# using edge length
# = \frac{1}{4} \sqrt{(a+b+c)(-a+b+c)(a-b+c)(a+b-c)} = \sqrt{p(p-a)(p-b)(p-c)}
# using sinus law
# = \frac{1}{2} a b \sin\gamma
# using coordinate
# = \frac{1}{2} \left\|{ \overrightarrow{AB} \wedge \overrightarrow{AC}}
# = \dfrac{1}{2} \big| x_A y_C - x_A y_B + x_B y_A - x_B y_C + x_C y_B - x_C y_A \big|
return .5 * math.fabs((self._p1 - self._p0).cross(self._p2 - self._p0))
##############################################
@property
def is_equilateral(self):
self._cache_length()
# all sides have the same length and angle = 60
return (self._p10 == self._p21 and
self._p21 == self._p02)
##############################################
@property
def is_scalene(self):
self._cache_length()
# all sides have different lengths
return (self._p10 != self._p21 and
self._p21 != self._p02 and
self._p02 != self._p10)
##############################################
@property
def is_isosceles(self):
self._cache_length()
# two sides of equal length
return not(self.is_equilateral) and not(self.is_scalene)
##############################################
@property
def is_right(self):
self._cache_angle()
# one angle = 90
raise NotImplementedError
##############################################
@property
def is_obtuse(self):
self._cache_angle()
# one angle > 90
return max(self._a10, self._a21, self._a02) > 90
##############################################
@property
def is_acute(self):
self._cache_angle()
# all angle < 90
return max(self._a10, self._a21, self._a02) < 90
##############################################
@property
def is_oblique(self):
return not self.is_equilateral
##############################################
@property
def orthocenter(self):
# intersection of the altitudes
raise NotImplementedError
##############################################
@property
def centroid(self):
# intersection of the medians
raise NotImplementedError
##############################################
@property
def circumcenter(self):
# intersection of the perpendiculars at middle
raise NotImplementedError
##############################################
@property
def in_circle(self):
# intersection of the bisectors
raise NotImplementedError # return circle
##############################################
def is_point_inside(self, point):
# Reference:
# http://mathworld.wolfram.com/TriangleInterior.html
return (
same_side(point, self._p0, self._p1, self._p2) and
same_side(point, self._p1, self._p0, self._p2) and
same_side(point, self._p2, self._p0, self._p1)
)
Patro-master-Patro/Patro/GeometryEngine/Vector.py 0000664 0000000 0000000 00000042063 13424433274 0022354 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement vector.
Example of usage::
v = Vector2D(10, 20)
v = Vector2D((10, 20))
v = Vector2D([10, 20])
v = Vector2D(iterable)
v = Vector2D(v)
v.x
v.y
# array interface
v[0], v[1]
iter(v)
-v
v + v
v += v
v - v
v -= v
v * 2
2 * v
v *= 2
v / 2
v /= 2
"""
####################################################################################################
__all__ = [
'Vector2D',
'NormalisedVector2D',
'HomogeneousVector2D',
]
####################################################################################################
import math
import numpy as np
from IntervalArithmetic import Interval2D, IntervalInt2D
from Patro.Common.Math.Functions import sign, trignometric_clamp #, is_in_trignometric_range
from .Primitive import Primitive, Primitive2DMixin
####################################################################################################
class Vector2DBase(Primitive, Primitive2DMixin):
__data_type__ = None
##############################################
def __init__(self, *args):
array = self._check_arguments(args)
# call __getitem__ once
self._v = np.array(array[:2], dtype=self.__data_type__) # Numpy implementation
##############################################
def _check_arguments(self, args):
size = len(args)
if size == 1:
array = args[0]
elif size == 2:
array = args
else:
raise ValueError("More than 2 arguments where given")
if not (np.iterable(array) and len(array) == 2):
raise ValueError("Argument must be iterable and of length 2")
return array
##############################################
def clone(self):
return self.__class__(self)
##############################################
@property
def v(self):
return self._v
# @v.setter
# def v(self, value):
# self._v = value
@property
def x(self):
return self.__data_type__(self._v[0])
@property
def y(self):
return self.__data_type__(self._v[1])
@x.setter
def x(self, x):
self._v[0] = x
@y.setter
def y(self, y):
self._v[1] = y
##############################################
def clone(self):
""" Return a copy of self """
return self.__class__(self._v)
##############################################
def __repr__(self):
return self.__class__.__name__ + str(self.v)
##############################################
def __nonzero__(self):
return bool(self._v.any())
##############################################
def __len__(self):
return 2
##############################################
def __iter__(self):
return iter(self._v)
##############################################
def __getitem__(self, a_slice):
return self._v[a_slice]
##############################################
def __setitem__(self, index, value):
self._v[index] = value
##############################################
def __eq__(v1, v2):
""" self == other """
return np.array_equal(v1.v, v2.v)
##############################################
def __add__(self, other):
"""Return a new vector equal to the addition of self and other"""
return self.__class__(self._v + other.v)
##############################################
def __iadd__(self, other):
"""Add other to self"""
self._v += other.v
return self
##############################################
def __sub__(self, other):
"""Return a new vector"""
return self.__class__(self._v - other.v)
##############################################
def __isub__(self, other):
"""Return a new vector equal to the subtraction of self and other"""
self._v -= other.v
return self
##############################################
def __pos__(self):
""" Return a new vector equal to self """
return self.__class__(self._v)
##############################################
def __neg__(self):
"""Return a new vector equal to the negation of self"""
return self.__class__(-self._v)
##############################################
def __abs__(self):
"""Return a new vector equal to abs of self"""
return self.__class__(np.abs(self._v))
##############################################
def to_int_list(self):
return [int(x) for x in self._v]
####################################################################################################
class Vector2DInt(Vector2DBase):
__data_type__ = np.int
##############################################
@property
def bounding_box(self):
x, y = self.x, self.y
return IntervalInt2D((x, x) , (y, y))
####################################################################################################
class Vector2DFloatBase(Vector2DBase):
__data_type__ = np.float
##############################################
@property
def bounding_box(self):
x, y = self.x, self.y
return Interval2D((x, x) , (y, y))
##############################################
def almost_equal(v1, v2, rtol=1e-05, atol=1e-08, equal_nan=False):
"""self ~= other"""
return np.allclose(v1, v2, rtol, atol, equal_nan)
##############################################
@property
def magnitude_square(self):
"""Return the square of the magnitude of the vector"""
return np.dot(self._v, self._v)
##############################################
@property
def magnitude(self):
"""Return the magnitude of the vector"""
# Note: To avoid float overflow use
# abs(x) * sqrt(1 + (y/x)**2) if x > y
return math.sqrt(self.magnitude_square)
##############################################
@property
def orientation(self):
"""Return the orientation in degree"""
#
# 2 | 1
# - + -
# 4 | 3
#
# | 1 | 2 | 3 | 4 |
# x | + | - | + | - |
# y | + | + | - | - |
# tan | + | - | - | + |
# atan | + | - | - | + |
# theta | atan | atan + pi | atan | atan - pi |
#
if not bool(self):
raise NameError("Null Vector")
if self.x == 0:
return math.copysign(90, self.y)
elif self.y == 0:
return 0 if self.x >= 0 else 180
else:
orientation = math.degrees(math.atan(self.tan))
if self.x < 0:
if self.y > 0:
orientation += 180
else:
orientation -= 180
return orientation
##############################################
def rotate(self, angle, counter_clockwise=True):
"""Return a new vector equal to self rotated of angle degree in the counter clockwise direction
"""
radians = math.radians(angle)
if not counter_clockwise:
radians = -radians
c = math.cos(radians)
s = math.sin(radians)
# Fixme: np matrice
xp = c * self._v[0] -s * self._v[1]
yp = s * self._v[0] +c * self._v[1]
return self.__class__((xp, yp))
##############################################
@property
def normal(self):
"""Return a new vector equal to self rotated of 90 degree in the counter clockwise direction
"""
xp = -self._v[1]
yp = self._v[0]
return self.__class__((xp, yp))
##############################################
@property
def anti_normal(self):
"""Return a new vector equal to self rotated of 90 degree in the clockwise direction
"""
xp = self._v[1]
yp = -self._v[0]
return self.__class__((xp, yp))
##############################################
@property
def permute(self):
"""Return a new vector where x and y are permuted.
"""
xp = self._v[1]
yp = self._v[0]
return self.__class__((xp, yp))
##############################################
@property
def parity(self):
"""Return a new vector equal to self rotated of 180 degree
"""
# parity
xp = -self._v[0]
yp = -self._v[1]
return self.__class__((xp, yp))
##############################################
@property
def tan(self):
"""Return the tangent"""
# RuntimeWarning: divide by zero encountered in double_scalars
return self.y / self.x
##############################################
@property
def inverse_tan(self):
"""Return the inverse tangent"""
return self.x / self.y
##############################################
def dot(self, other):
"""Return the dot product of self with other"""
return float(np.dot(self._v, other.v))
##############################################
def cross(self, other):
"""Return the cross product of self with other"""
return float(np.cross(self._v, other.v))
##############################################
# perp dot product
# perp = (-y1, x1)
# perp dot = -y1*x2 + x1*y2 = x1*y2 - x2*y1
perp_dot = cross
##############################################
def is_parallel(self, other, return_cross=False):
"""Self is parallel with other"""
cross = self.cross(other)
test = round(cross, 7) == 0
if return_cross:
return test, cross
else:
return test
##############################################
def is_orthogonal(self, other):
"""Self is orthogonal with other"""
return round(self.dot(other), 7) == 0
##############################################
def cos_with(self, direction):
"""Return the cosinus of self with direction"""
cos = direction.dot(self) / (direction.magnitude * self.magnitude)
return trignometric_clamp(cos)
##############################################
def projection_on(self, direction):
"""Return the projection of self on direction"""
return direction.dot(self) / direction.magnitude
##############################################
def sin_with(self, direction):
"""Return the sinus of self with other"""
# turn from direction to self
sin = direction.cross(self) / (direction.magnitude * self.magnitude)
return trignometric_clamp(sin)
##############################################
def deviation_with(self, direction):
"""Return the deviation of self with other"""
return direction.cross(self) / direction.magnitude
##############################################
def angle_with(self, direction):
"""Return the angle of self on direction"""
angle = math.acos(self.cos_with(direction))
angle_sign = sign(self.sin_with(direction))
return angle_sign * math.degrees(angle)
orientation_with = angle_with
####################################################################################################
class Vector2D(Vector2DFloatBase):
"""2D Vector"""
##############################################
@staticmethod
def from_angle(angle):
"""Create the unitary vector (cos(angle), sin(angle)). *angle* is in degree."""
rad = math.radians(angle)
return Vector2D((math.cos(rad), math.sin(rad))) # Fixme: classmethod
##############################################
@staticmethod
def from_polar(radius, angle):
"""Create the polar vector (radius*cos(angle), radius*sin(angle)). *angle* is in degree."""
return Vector2D.from_angle(angle) * radius # Fixme: classmethod
##############################################
@staticmethod
def from_ellipse(radius_x, radius_y, angle):
"""Create the vector (radius_x*cos(angle), radius_y*sin(angle)). *angle* is in degree."""
angle = math.radians(angle)
x = radius_x * cos(angle)
y = radius_y * sin(angle)
return Vector2D(x, y) # Fixme: classmethod
##############################################
@staticmethod
def middle(p0, p1):
"""Return the middle point."""
return Vector2D(p0 + p1) * .5 # Fixme: classmethod
##############################################
def __mul__(self, scale):
"""Return a new vector equal to the self scaled by scale"""
return self.__class__(scale * self._v) # Fixme: Vector2D ?
##############################################
def __rmul__(self, scale):
"""Return a new vector equal to the self scaled by scale"""
return self.__mul__(scale)
##############################################
def __imul__(self, scale):
"""Scale self by scale"""
self._v *= scale
return self
##############################################
def __truediv__(self, scale):
"""Return a new vector equal to the self dvivided by scale"""
return self.__class__(self._v / scale)
##############################################
def __itruediv__(self, scale):
"""Scale self by 1/scale"""
self._v /= scale
return self
##############################################
def scale(self, scale_x, scale_y):
"""Scale self by scale"""
obj = self.clone()
obj._v *= np.array((scale_x, scale_y))
return obj
##############################################
def divide(self, scale_x, scale_y):
"""Scale self by 1/scale"""
obj = self.clone()
obj._v /= np.array((scale_x, scale_y))
return obj
##############################################
def normalise(self):
"""Normalise the vector"""
self._v /= self.magnitude
return self
##############################################
def to_normalised(self):
"""Return a normalised vector"""
return NormalisedVector2D(self._v / self.magnitude)
##############################################
def rint(self):
return Vector2DInt(np.rint(self._v))
####################################################################################################
class NormalisedVector2D(Vector2DFloatBase):
"""2D Normalised Vector"""
##############################################
def __init__(self, *args):
super(NormalisedVector2D, self).__init__(*args)
#! if self.magnitude != 1.:
#! raise ValueError("Magnitude != 1")
# if not (is_in_trignometric_range(self.x) and
# is_in_trignometric_range(self.y)):
# raise ValueError("Values must be in trignometric range")
##############################################
def __mul__(self, scale):
""" Return a new vector equal to the self scaled by scale """
return self.__class__(scale * self._v) # Fixme: Vector2D ?
##############################################
def __rmul__(self, scale):
""" Return a new vector equal to the self scaled by scale """
return self.__mul__(scale)
####################################################################################################
class HomogeneousVector2D(Vector2D):
"""2D Homogeneous Coordinate Vector"""
##############################################
def __init__(self, vector):
# self._v = np.ones((3), dtype=self.__data_type__)
# self._v[:2] = vector.v[:2]
self._v = np.array(vector[:2]) # to keep compatibility
self._w = 1
##############################################
@property
def v(self):
return np.array(((self.x), (self.y), (self._w)), dtype=self.__data_type__)
# @v.setter
# def v(self, value):
# self._v = value
@property
def w(self):
return self._w
@w.setter
def w(self, value):
self._w = value
##############################################
def to_vector(self):
return Vector2D(self._v)
Patro-master-Patro/Patro/GeometryEngine/__init__.py 0000664 0000000 0000000 00000005044 13424433274 0022647 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This module implements a 2D geometry engine which implement standard primitives like line, conic
and Bézier curve.
.. note:: This module is a candidate for a dedicated project.
Purpose
-------
The purpose of this module is to provide all the required algorithms in Python language for a 2D
geometry engine. In particular it must avoid the use of a third-party libraries which could be over
sized for our purpose and challenging to trust.
It is not designed to provide optimised algorithms for a large number of graphic entities. Such
optimisations could be provided in addition, in particular if the Python implementation has dramatic
performances.
Bibliographical References
--------------------------
All complex algorithms in this module should have strong references matching theses criteria by
preference order:
* a citation to an article from a well known peer reviewed journal,
* a citation to a reference book authored by a well known author,
* a well written article which can be easily trusted ( in this case an electronic copy of this
article should be added to the repository).
However a Wikipedia article will usually not fulfils the criteria due to the weakness of this
collaborative encyclopedia: article quality, review process, content modification over time.
"""
####################################################################################################
from .Primitive import Primitive2DMixin
from .Vector import Vector2D
####################################################################################################
# Fixme: to fix cyclic import issue
Primitive2DMixin.__vector_cls__ = Vector2D
Patro-master-Patro/Patro/GraphicEngine/ 0000775 0000000 0000000 00000000000 13424433274 0020315 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/GraphicEngine/GraphicScene/ 0000775 0000000 0000000 00000000000 13424433274 0022650 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/GraphicEngine/GraphicScene/GraphicItem.py 0000664 0000000 0000000 00000025021 13424433274 0025416 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement graphic scene items like text, image, line, circle and Bézier curve.
"""
# Fixme: get_geometry / as argument
# position versus point
####################################################################################################
import logging
from Patro.GeometryEngine.Bezier import CubicBezier2D, QuadraticBezier2D
from Patro.GeometryEngine.Conic import Circle2D, Ellipse2D, AngularDomain
from Patro.GeometryEngine.Polyline import Polyline2D
from Patro.GeometryEngine.Rectangle import Rectangle2D
from Patro.GeometryEngine.Segment import Segment2D
from .GraphicItemMixin import (
GraphicItem,
PathStyleItemMixin,
PositionMixin,
TwoPositionMixin,
ThreePositionMixin,
FourPositionMixin,
NPositionMixin,
StartStopAngleMixin,
)
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class CoordinateItem(PositionMixin):
##############################################
def __init__(self, name, position):
PositionMixin.__init__(self, position)
self._name = str(name)
##############################################
@property
def name(self):
return self._name
####################################################################################################
class TextItem(PositionMixin, GraphicItem):
##############################################
def __init__(self, scene, position, text, font, user_data):
GraphicItem.__init__(self, scene, user_data)
PositionMixin.__init__(self, position)
self._text = str(text)
self._font = font
##############################################
@property
def text(self):
return self._text
# @text.setter
# def text(self, value):
# self._text = value
@property
def font(self):
return self._font
# @font.setter
# def font(self, value):
# self._font = value
##############################################
def get_geometry(self):
position = self.casted_position
# Fixme: require metric !
# QFontMetrics(font).width(self._text)
return Rectangle2D(position, position)
####################################################################################################
class CircleItem(PositionMixin, StartStopAngleMixin, PathStyleItemMixin):
##############################################
def __init__(self, scene, position, radius, path_style, user_data,
start_angle=0, # Fixme: kwargs ?
stop_angle=360,
):
PathStyleItemMixin.__init__(self, scene, path_style, user_data)
PositionMixin.__init__(self, position)
StartStopAngleMixin.__init__(self, start_angle, stop_angle)
# Fixme: radius = 1pt !!!
if radius == '1pt':
radius = 10
self._radius = radius
##############################################
@property
def radius(self):
return self._radius
# @radius.setter
# def radius(self, value):
# self._radius = value
##############################################
def get_geometry(self):
position = self.casted_position
# Fixme: radius
domain = AngularDomain(self._start_angle, self._stop_angle)
return Circle2D(position, self._radius, domain=domain)
####################################################################################################
class EllipseItem(PositionMixin, StartStopAngleMixin, PathStyleItemMixin):
##############################################
def __init__(self, scene, position,
radius_x, radius_y,
angle,
path_style, user_data,
start_angle=0,
stop_angle=360,
):
PathStyleItemMixin.__init__(self, scene, path_style, user_data)
PositionMixin.__init__(self, position)
StartStopAngleMixin.__init__(self, start_angle, stop_angle)
self._radius_x = radius_x
self._radius_y = radius_y
self._angle = angle
##############################################
@property
def radius_x(self):
return self._radius_x
# @radius_x.setter
# def radius_x(self, value):
# self._radius_x = value
@property
def radius_y(self):
return self._radius_y
# @radius_y.setter
# def radius_y(self, value):
# self._radius_y = value
@property
def angle(self):
return self._angle
##############################################
def get_geometry(self):
position = self.casted_position
return Ellipse2D(position, self._radius_x, self._radius_y, self._angle)
####################################################################################################
class SegmentItem(TwoPositionMixin, PathStyleItemMixin):
##############################################
def __init__(self, scene, position1, position2, path_style, user_data):
PathStyleItemMixin.__init__(self, scene, path_style, user_data)
TwoPositionMixin.__init__(self, position1, position2)
##############################################
def get_geometry(self):
positions = self.casted_positions
return Segment2D(*positions)
####################################################################################################
class RectangleItem(TwoPositionMixin, PathStyleItemMixin):
##############################################
def __init__(self, scene, position1, position2, path_style, user_data):
# Fixme: position or W H
PathStyleItemMixin.__init__(self, scene, path_style, user_data)
TwoPositionMixin.__init__(self, position1, position2)
##############################################
def get_geometry(self):
positions = self.casted_positions
return Rectangle2D(*positions)
####################################################################################################
class PolylineItem(NPositionMixin, PathStyleItemMixin):
##############################################
def __init__(self, scene, positions, path_style, user_data):
PathStyleItemMixin.__init__(self, scene, path_style, user_data)
NPositionMixin.__init__(self, positions)
##############################################
def get_geometry(self):
positions = self.casted_positions
return Polyline2D(*positions)
####################################################################################################
class ImageItem(TwoPositionMixin, GraphicItem):
##############################################
def __init__(self, scene, position1, position2, image, user_data):
# Fixme: position or W H
GraphicItem.__init__(self, scene, user_data)
TwoPositionMixin.__init__(self, position1, position2)
self._image = image
##############################################
@property
def image(self):
return self._image
# @image.setter
# def image(self, value):
# self._image = value
##############################################
def get_geometry(self):
positions = self.casted_positions
return Rectangle2D(*positions)
####################################################################################################
class CubicBezierItem(FourPositionMixin, PathStyleItemMixin):
##############################################
def __init__(self,
scene,
position1, position2, position3, position4,
path_style,
user_data,
):
# Fixme: curve vs path
PathStyleItemMixin.__init__(self, scene, path_style, user_data)
FourPositionMixin.__init__(self, position1, position2, position3, position4)
# super(CubicBezierItem, self).__init__(path_style)
# self._curve = curve
##############################################
# @property
# def curve(self):
# return self._curve
# @curve.setter
# def curve(self, value):
# self._curve = value
##############################################
def get_geometry(self):
positions = self.casted_positions
return CubicBezier2D(*positions)
####################################################################################################
class QuadraticBezierItem(ThreePositionMixin, PathStyleItemMixin):
##############################################
def __init__(self,
scene,
position1, position2, position3,
path_style,
user_data,
):
# Fixme: curve vs path
PathStyleItemMixin.__init__(self, scene, path_style, user_data)
ThreePositionMixin.__init__(self, position1, position2, position3)
# super(CubicBezierItem, self).__init__(path_style)
# self._curve = curve
##############################################
# @property
# def curve(self):
# return self._curve
# @curve.setter
# def curve(self, value):
# self._curve = value
##############################################
def get_geometry(self):
positions = self.casted_positions
return QuadraticBezier2D(*positions)
##############################################
@property
def cubic_positions(self):
if not(hasattr(self, '_cubic_points')):
cubic = self.geometry.to_cubic()
self._cubic_points = list(cubic.points)
return self._cubic_points
Patro-master-Patro/Patro/GraphicEngine/GraphicScene/GraphicItemMixin.py 0000664 0000000 0000000 00000020531 13424433274 0026424 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement graphic scene items mixins.
"""
####################################################################################################
__all__ = [
'GraphicItem',
'PathStyleItemMixin',
'PositionMixin',
'TwoPositionMixin',
'FourPositionMixin',
'NPositionMixin',
'StartStopAngleMixin',
]
####################################################################################################
class GraphicItem:
# clipping
# opacity
__subclasses__ = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.__subclasses__.append(cls)
##############################################
def __init__(self, scene, user_data):
self._scene = scene
self._user_data = user_data
self._z_value = 0
self._visible = True
self._selected = False
self._dirty = True
self._geometry = None
self._bounding_box = None
##############################################
@property
def scene(self):
return self._scene
@property
def user_data(self):
return self._user_data
##############################################
def __hash__(self):
return hash(self._user_data)
##############################################
@property
def z_value(self):
return self._z_value
@z_value.setter
def z_value(self, value):
self._z_value = value
@property
def visible(self):
return self._visible
@visible.setter
def visible(self, value):
self._visible = value
@property
def selected(self):
return self._selected
@selected.setter
def selected(self, value):
self._selected = bool(value)
##############################################
@property
def positions(self):
raise NotImplementedError
##############################################
@property
def casted_positions(self):
cast = self._scene.cast_position
return [cast(position) for position in self.positions]
##############################################
@property
def dirty(self):
return self._dirty
@dirty.setter
def dirty(self, value):
if bool(value):
self._dirty = True
else:
self._dirty = True
self._geometry = None
self._bounding_box = None
##############################################
@property
def bounding_box(self):
if self._bounding_box is None:
self._bounding_box = self.get_bounding_box()
return self._bounding_box
@property
def geometry(self):
if self._geometry is None:
self._geometry = self.get_geometry()
self._dirty = False
return self._geometry
##############################################
def get_geometry(self):
raise NotImplementedError
##############################################
def get_bounding_box(self):
return self.geometry.bounding_box
##############################################
def distance_to_point(self, point):
return self.geometry.distance_to_point(point)
####################################################################################################
class PathStyleItemMixin(GraphicItem):
##############################################
def __init__(self, scene, path_style, user_data):
GraphicItem.__init__(self, scene, user_data)
self._path_style = path_style
##############################################
@property
def path_style(self):
return self._path_style
# @path_style.setter
# def path_style(self, value):
# self._path_style = value
####################################################################################################
class PositionMixin:
##############################################
def __init__(self, position):
# Fixme: could be Vector2D or name
self._position = position # Vector2D(position)
##############################################
@property
def position(self):
return self._position
# @position.setter
# def position(self, value):
# self._position = value
@property
def positions(self):
return (self._position)
@property
def casted_position(self):
return self._scene.cast_position(self._position)
####################################################################################################
class TwoPositionMixin:
##############################################
def __init__(self, position1, position2):
self._position1 = position1
self._position2 = position2
##############################################
@property
def position1(self):
return self._position1
@property
def position2(self):
return self._position2
@property
def positions(self):
return (self._position1, self._position2)
####################################################################################################
class ThreePositionMixin(TwoPositionMixin):
##############################################
def __init__(self, position1, position2, position3):
TwoPositionMixin.__init__(self, position1, position2)
self._position3 = position3
##############################################
@property
def position3(self):
return self._position3
@property
def positions(self):
return (self._position1, self._position2, self._position3)
####################################################################################################
class FourPositionMixin(ThreePositionMixin):
##############################################
def __init__(self, position1, position2, position3, position4):
ThreePositionMixin.__init__(self, position1, position2, position3)
self._position4 = position4
##############################################
@property
def position4(self):
return self._position4
@property
def positions(self):
return (self._position1, self._position2, self._position3, self._position4)
####################################################################################################
class NPositionMixin:
##############################################
def __init__(self, positions):
self._positions = list(positions)
##############################################
@property
def positions(self): # Fixme: versus points
return self._positions # Fixme: iter list ???
####################################################################################################
class StartStopAngleMixin:
##############################################
def __init__(self, start_angle=0, stop_angle=360):
self._start_angle = start_angle
self._stop_angle = stop_angle
##############################################
@property
def start_angle(self):
return self._start_angle
# @start_angle.setter
# def start_angle(self, value):
# self._start_angle = value
@property
def stop_angle(self):
return self._stop_angle
# @stop_angle.setter
# def stop_angle(self, value):
# self._stop_angle = value
##############################################
@property
def is_closed(self):
return abs(self._stop_angle - self.start_angle) >= 360
Patro-master-Patro/Patro/GraphicEngine/GraphicScene/GraphicStyle.py 0000664 0000000 0000000 00000014666 13424433274 0025635 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement graphic styles.
"""
# Fixme: get_geometry / as argument
####################################################################################################
__all__= ['GraphicPathStyle', 'GraphicBezierStyle', 'Font']
####################################################################################################
import logging
from Patro.GraphicStyle import Colors, StrokeStyle, CapStyle, JoinStyle
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class GraphicPathStyle:
"""Class to define path style"""
##############################################
def __init__(self, **kwargs):
"""*color* can be a defined color name, a '#rrggbb' string or a :class:`Color` instance.
"""
self.stroke_style = kwargs.get('stroke_style', StrokeStyle.SolidLine)
self.line_width = kwargs.get('line_width', 1.0)
self.stroke_color = kwargs.get('stroke_color', Colors.black)
self.stroke_alpha = kwargs.get('stroke_alpha', 1.0)
self.fill_color = kwargs.get('fill_color', None) # only for closed path
self.fill_alpa = kwargs.get('fill_alpha', 1.0)
# This is default Qt
self.cap_style = kwargs.get('cap_style', CapStyle.SquareCap)
self.join_style = kwargs.get('join_style', JoinStyle.BevelJoin)
##############################################
def _dict_keys(self):
return (
'stroke_style',
'line_width',
'stroke_color',
'fill_color',
)
##############################################
def _to_dict(self):
return {name:getattr(self, '_' + name) for name in self._dict_keys()}
##############################################
def clone(self):
return self.__class__(**self._to_dict())
##############################################
def __repr__(self):
return '{0}({1})'.format(self.__class__.__name__, self._to_dict())
##############################################
@property
def stroke_style(self):
return self._stroke_style
@stroke_style.setter
def stroke_style(self, value):
self._stroke_style = StrokeStyle(value)
##############################################
@property
def line_width(self):
return self._line_width
@line_width.setter
def line_width(self, value):
self._line_width = value # Fixme: float ???
@property
def line_width_as_float(self):
line_width = self._line_width
# Fixme: use scale ?
if isinstance(line_width, (int, float)):
return line_width
else:
line_width = line_width.replace('pt', '')
line_width = line_width.replace('px', '')
return float(line_width)
##############################################
@property
def stroke_color(self):
return self._stroke_color
@stroke_color.setter
def stroke_color(self, value):
self._stroke_color = Colors.ensure_color(value)
@property
def stroke_alpha(self):
return self._stroke_alpha
@stroke_alpha.setter
def stroke_alpha(self, value):
self._stroke_alpha = float(value) # Fixme: check < 1
##############################################
@property
def fill_color(self):
return self._fill_color
@fill_color.setter
def fill_color(self, value):
self._fill_color = Colors.ensure_color(value)
@property
def fill_alpha(self):
return self._fill_alpha
@fill_alpha.setter
def fill_alpha(self, value):
self._fill_alpha = float(value)
##############################################
@property
def cap_style(self):
return self._cap_style
@cap_style.setter
def cap_style(self, value):
self._cap_style = CapStyle(value)
@property
def join_style(self):
return self._join_style
@join_style.setter
def join_style(self, value):
self._join_style = JoinStyle(value)
####################################################################################################
class GraphicBezierStyle(GraphicPathStyle):
"""Class to define Bézier curve style"""
##############################################
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.show_control = kwargs.get('show_control', False)
self.control_color = kwargs.get('control_color', None)
##############################################
def _dict_keys(self):
return (
'show_control',
'control_color'
)
##############################################
@property
def show_control(self):
return self._show_control
@show_control.setter
def show_control(self, value):
self._show_control = bool(value)
##############################################
@property
def control_color(self):
return self._control_color
@control_color.setter
def control_color(self, value):
self._control_color = Colors.ensure_color(value)
####################################################################################################
class Font:
"""Class to define font style"""
##############################################
def __init__(self, family, point_size):
self.family = family
self.point_size = point_size
Patro-master-Patro/Patro/GraphicEngine/GraphicScene/Scene.py 0000664 0000000 0000000 00000036505 13424433274 0024270 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement a graphic scene.
"""
####################################################################################################
__all__ = [
'GraphicScene',
# sphinx
'GraphicSceneScope',
]
####################################################################################################
import logging
import rtree
from Patro.GeometryEngine import (
Bezier,
Conic,
Line,
Path,
Polygon,
Polyline,
Rectangle,
Segment,
Spline,
Triangle,
)
from Patro.GeometryEngine.Transformation import AffineTransformation2D
from Patro.GeometryEngine.Vector import Vector2D
from . import GraphicItem
from .GraphicItem import CoordinateItem
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class GraphicSceneScope:
"""Class to implement a graphic scene."""
__ITEM_CTOR__ = {
'circle': GraphicItem.CircleItem,
'cubic_bezier': GraphicItem.CubicBezierItem,
'ellipse': GraphicItem.EllipseItem,
'image': GraphicItem.ImageItem,
# 'path': GraphicItem.PathItem,
# 'polygon': GraphicItem.PolygonItem,
'rectangle': GraphicItem.RectangleItem,
'segment': GraphicItem.SegmentItem,
'polyline': GraphicItem.PolylineItem,
'quadratic_bezier': GraphicItem.QuadraticBezierItem,
'text': GraphicItem.TextItem,
}
_logger = _module_logger.getChild('GraphicSceneScope')
##############################################
def __init__(self, transformation=None):
if transformation is None:
transformation = AffineTransformation2D.Identity()
self._transformation = transformation
self._coordinates = {}
self._items = {} # id(item) -> item, e.g. for rtree query
self._user_data_map = {}
self._rtree = rtree.index.Index()
# item_id -> bounding_box, used to delete item in rtree (cf. rtree api)
self._item_bounding_box_cache = {}
##############################################
@property
def transformation(self):
return self._transformation
##############################################
def __len__(self):
return self.number_of_items
@property
def number_of_items(self):
return len(self._items)
##############################################
def __iter__(self):
# must be an ordered item list
return iter(self._items.values())
##############################################
def z_value_iter(self):
# Fixme: cache ???
# Group by z_value and keep inserting order
z_map = {}
for item in self._items.values():
if item.visible:
items = z_map.setdefault(item.z_value, [])
items.append(item)
for z_value in sorted(z_map.keys()):
for item in z_map[z_value]:
yield item
##############################################
@property
def selected_items(self):
# Fixme: cache ?
return [item for item in self._items.values() if item.selected]
##############################################
def unselect_items(self):
for item in self.selected_items:
item.selected = False
##############################################
def add_coordinate(self, name, position):
item = CoordinateItem(name, position)
self._coordinates[name] = item
return item
##############################################
def remove_coordinate(self, name):
del self._coordinates[name]
##############################################
def coordinate(self, name):
return self._coordinates[name]
##############################################
def cast_position(self, position):
"""Cast coordinate and apply scope transformation, *position* can be a coordinate name string of a
:class:`Patro.GeometryEngine.Vector.Vector2D`.
"""
# Fixme: cache ?
if isinstance(position, str):
vector = self._coordinates[position].position
elif isinstance(position, Vector2D):
vector = position
return self._transformation * vector
##############################################
def add_item(self, cls, *args, **kwargs):
item = cls(self, *args, **kwargs)
# print(item, item.user_data, hash(item))
# if item in self._items:
# print('Warning duplicate', item.user_data)
item_id = id(item) # Fixme: hash ???
self._items[item_id] = item
user_data = item.user_data
if user_data is not None:
user_data_id = id(user_data) # Fixme: hash ???
items = self._user_data_map.setdefault(user_data_id, [])
items.append(item)
return item
##############################################
def remove_item(self, item):
self.update_rtree(item, insert=False)
items = self.item_for_user_data(item.user_data)
if items:
items.remove(item)
del self._items[item]
##############################################
def item_for_user_data(self, user_data):
user_data_id = id(user_data)
return self._user_data_map.get(user_data_id, None)
##############################################
def update_rtree(self):
for item in self._items.values():
if item.dirty:
self.update_rtree_item(item)
##############################################
def update_rtree_item(self, item, insert=True):
item_id = id(item)
old_bounding_box = self._item_bounding_box_cache.pop(item_id, None)
if old_bounding_box is not None:
self._rtree.delete(item_id, old_bounding_box)
if insert:
# try:
bounding_box = item.bounding_box.bounding_box # Fixme: name
# print(item, bounding_box)
self._rtree.insert(item_id, bounding_box)
self._item_bounding_box_cache[item_id] = bounding_box
# except AttributeError:
# print('bounding_box not implemented for', item)
# pass # Fixme:
##############################################
def item_in_bounding_box(self, bounding_box):
# Fixme: Interval2D ok ?
# print('item_in_bounding_box', bounding_box)
item_ids = self._rtree.intersection(bounding_box)
if item_ids:
return [self._items[item_id] for item_id in item_ids]
else:
return None
##############################################
def item_at(self, position, radius):
x, y = list(position)
bounding_box = (
x - radius, y - radius,
x + radius, y + radius,
)
items = []
for item in self.item_in_bounding_box(bounding_box):
try: # Fixme
distance = item.distance_to_point(position)
# print('distance_to_point {:6.2f} {}'.format(distance, item))
# Fixme: distance is None
if distance is not None and distance <= radius:
items.append((distance, item))
except NotImplementedError:
pass
return sorted(items, key=lambda pair: pair[0])
##############################################
# Fixme: !!!
# def add_scope(self, *args, **kwargs):
# return self.add_item(GraphicSceneScope, self, *args, **kwargs)
##############################################
def add_geometry(self, item, path_style):
"""Add a geometry primitive"""
ctor = None
points = None
args = []
args_tail = [path_style]
kwargs = dict(user_data=item)
# Bezier
if isinstance(item, Bezier.QuadraticBezier2D):
ctor = self.quadratic_bezier
elif isinstance(item, Bezier.CubicBezier2D):
ctor = self.cubic_bezier
# Conic
elif isinstance(item, Conic.Circle2D):
ctor = self.circle
args = [item.radius]
if item.domain:
kwargs['start_angle'] = item.domain.start
kwargs['stop_angle'] = item.domain.stop
elif isinstance(item, Conic.Ellipse2D):
ctor = self.ellipse
args = [item.radius_x, item.radius_y, item.angle]
# Line
elif isinstance(item, Line.Line2D):
# Fixme: extent ???
raise NotImplementedError
# Path
elif isinstance(item, Path.Path2D):
self.add_path(item, path_style)
# Polygon
elif isinstance(item, Polygon.Polygon2D):
# Fixme: to path
raise NotImplementedError
# Polyline
elif isinstance(item, Polyline.Polyline2D):
ctor = self.polyline
# fixme: to path
# Rectangle
elif isinstance(item, Rectangle.Rectangle2D):
ctor = self.rectangle
# Fixme: to path
# Segment
elif isinstance(item, Segment.Segment2D):
ctor = self.segment
# Spline
elif isinstance(item, Spline.BSpline2D):
return self.add_spline(item, path_style)
# Triangle
elif isinstance(item, Triangle.Triangle2D):
# Fixme: to path
raise NotImplementedError
# Not implemented
else:
self._logger.warning('Not implemented item {}'.format(item))
raise NotImplementedError
if ctor is not None:
if points is None:
points = list(item.points)
return ctor(*points, *args, *args_tail, **kwargs)
##############################################
# def add_quadratic_bezier(self, curve, *args, **kwargs):
# # Fixme:
# cubic = curve.to_cubic()
# return self.cubic_bezier(*cubic.points, *args, **kwargs)
##############################################
def add_spline(self, spline, path_style):
return [
self.cubic_bezier(*bezier.points, path_style, user_data=spline)
for bezier in spline.to_bezier()
]
##############################################
def add_path(self, path, path_style):
items = []
def add_bulge(segment):
arc = segment.bulge_geometry
arc_item = self.circle(
arc.center, arc.radius,
path_style,
start_angle=arc.domain.start,
stop_angle=arc.domain.stop,
user_data=segment,
)
items.append(arc_item)
def add_by_method(method, segment):
item = method(
*segment.points,
path_style,
user_data=segment,
)
items.append(item)
def add_segment(segment):
add_by_method(self.segment, segment)
def add_ellipse(segment):
# add_segment(Segment.Segment2D(*segment.points))
ellipse = segment.geometry
# print(ellipse, ellipse.domain)
arc_item = self.ellipse(
ellipse.center,
ellipse.radius_x,
ellipse.radius_y,
ellipse.angle,
path_style,
start_angle=ellipse.domain.start,
stop_angle=ellipse.domain.stop,
user_data=segment,
)
items.append(arc_item)
def add_quadratic(segment):
add_by_method(self.quadratic_bezier, segment)
def add_cubic(segment):
add_by_method(self.cubic_bezier, segment)
for segment in path:
item = None
if isinstance(segment, Path.LinearSegment):
# if segment._start_radius is True:
# continue
if segment.radius is not None:
add_bulge(segment)
# if segment._closing is True:
# start_segment = path.start_segment
# add_bulge(start_segment)
# add_segment(start_segment)
add_segment(segment)
elif isinstance(segment, Path.QuadraticBezierSegment):
add_quadratic(segment)
elif isinstance(segment, Path.CubicBezierSegment):
add_cubic(segment)
elif isinstance(segment, Path.ArcSegment):
add_ellipse(segment)
elif isinstance(segment, Path.StringedQuadtraticBezierSegment):
pass
elif isinstance(segment, Path.StringedCubicBezierSegment):
pass
##############################################
# def quadratic_bezier(self, p0, p1, p2, *args, **kwargs):
# # Fixme:
# cubic = Bezier.QuadraticBezier2D(p0, p1, p2).to_cubic()
# return self.add_cubic(cubic, *args, **kwargs)
##############################################
def bezier_path(self, points, degree, *args, **kwargs):
"""Add a Bézier curve with the given control points and degree"""
if degree == 1:
method = self.segment
elif degree == 2:
# Fixme:
method = self.quadratic_bezier
elif degree == 3:
method = self.cubic_bezier
else:
raise ValueError('Unsupported degree for Bezier curve: {}'.format(degree))
# Fixme: generic code
number_of_points = len(points)
n = number_of_points -1
if n % degree:
raise ValueError('Wrong number of points for Bezier {} curve: {}'.format(degree, number_of_points))
items = []
for i in range(number_of_points // degree):
j = degree * i
k = j + degree
item = method(*points[j:k+1], *args, **kwargs)
items.append(item)
return items
##############################################
# Register a method in GraphicSceneScope class for each type of graphic item
def _make_add_item_wrapper(cls):
def wrapper(self, *args, **kwargs):
return self.add_item(cls, *args, **kwargs)
return wrapper
for name, cls in GraphicSceneScope.__ITEM_CTOR__.items():
setattr(GraphicSceneScope, name, _make_add_item_wrapper(cls))
####################################################################################################
class GraphicScene(GraphicSceneScope):
"""Class to implement a graphic scene."""
pass
Patro-master-Patro/Patro/GraphicEngine/GraphicScene/TypographyUnit.py 0000664 0000000 0000000 00000005560 13424433274 0026236 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement common typography units.
"""
# See https://en.wikipedia.org/wiki/Point_(typography)
####################################################################################################
__all__ = [
'InchUnit',
'MmUnit',
'PointUnit',
]
####################################################################################################
class TypographyUnit:
INCH_2_MM = 25.4 # 1 inch = 25.4 mm
POINT_2_INCH = 72 # 1 inch = 72 point PostScript, CSS pt and TeX bp
TEX_POINT_2_INCH = 72.27 # TeX pt
PICA_2_POINT = 12 # 1 pica = 12 point
POINT_2_MM = INCH_2_MM / POINT_2_INCH
MM_2_INCH = 1 / INCH_2_MM
INCH_2_POINT = 1 / POINT_2_INCH
MM_2_POINT = 1 / POINT_2_MM
##############################################
def __init__(self, value):
self._value = value
##############################################
def __float__(self):
return self._value
####################################################################################################
class MmUnit(TypographyUnit):
def to_mm(self):
return self
def to_inch(self):
return InchUnit(self._value * self.MM_2_Point)
def to_point(self):
return PointUnit(self._value * self.MM_2_POINT)
####################################################################################################
class InchUnit(TypographyUnit):
def to_inch(self):
return self
def to_mm(self):
return MmUnit(self._value * self.INCH_2_MM)
def to_point(self):
return PointUnit(self._value * self.INCH_2_POINT)
####################################################################################################
class PointUnit(TypographyUnit):
def to_point(self):
return self
def to_mm(self):
return MmUnit(self._value * self.POINT_2_MM)
def to_inch(self):
return InchUnit(self._value * self.POINT_2_INCH)
Patro-master-Patro/Patro/GraphicEngine/GraphicScene/__init__.py 0000664 0000000 0000000 00000002070 13424433274 0024760 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements a graphic scene which serve to describe a vector drawing to be
rendered by a painter implementation.
"""
Patro-master-Patro/Patro/GraphicEngine/Painter/ 0000775 0000000 0000000 00000000000 13424433274 0021717 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/GraphicEngine/Painter/DxfPainter.py 0000664 0000000 0000000 00000014434 13424433274 0024343 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
__all__ = [
'DxfPainter',
]
####################################################################################################
import logging
from Patro.GeometryEngine.Vector import Vector2D
from .Painter import Painter
try:
import ezdxf
except ImportError:
ezdxf = None
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class DxfPainterBase(Painter):
##############################################
def __init__(self, path, scene, paper):
super().__init__(scene)
self._path = path
self._paper = paper
self._coordinates = {}
##############################################
def _cast_position(self, position):
# Fixme: to base class
if isinstance(position, str):
return self._coordinates[position]
elif isinstance(position, Vector2D):
return position
##############################################
def paint_CoordinateItem(self, item):
# Fixme: to base class
self._coordinates[item.name] = item.position
####################################################################################################
class EzdxfPainter(DxfPainterBase):
# AC1009 AutoCAD R12
# AC1012 AutoCAD R13 -> R2000
# AC1014 AutoCAD R14 -> R2000
# AC1015 AutoCAD R2000
# AC1018 AutoCAD R2004
# AC1021 AutoCAD R2007
# AC1024 AutoCAD R2010
# AC1027 AutoCAD R2013
# AC1032 AutoCAD R2018
__STROKE_STYLE__ = {
None: None,
'dashDotLine': 'DASHDOT',
'dotLine': 'DOTTED',
'hair': 'CONTINUOUS',
'none': 'PHANTOM', # Fixme: ???
'solid': 'CONTINUOUS',
}
__COLOR__ = {
None : None,
'black': 0,
}
##############################################
def __init__(self, path, scene, paper, dxf_version='R2010'):
super().__init__(path, scene, paper)
self._dxf_version = dxf_version
self._drawing = ezdxf.new(dxf_version)
self._model_space= self._drawing.modelspace() # add new entities to the model space
# self._model_space.page_setup(
# size=(self._paper.width, self._paper.height),
# # margins=(top, right, bottom, left)
# units='mm',
# )
# print('Available line types:')
# for line_type in self._drawing.linetypes:
# print('{}: {}'.format(line_type.dxf.name, line_type.dxf.description))
self.paint()
self._drawing.saveas(path)
##############################################
def _cast_position(self, position):
position = super()._cast_position(position)
position = position.clone() * 10 # Fixme: cm -> mm
return position
##############################################
def _cast_positions(self, positions):
return [list(self._cast_position(position)) for position in positions]
##############################################
def _graphic_style(self, item):
# cf. https://ezdxf.readthedocs.io/en/latest/graphic_base_class.html#common-dxf-attributes-for-dxf-r13-or-later
path_style = item.path_style
if path_style.stroke_color is None:
return {'linetype': 'PHANTOM', 'color': 2} # Fixme:
color = self.__COLOR__[path_style.stroke_color] # see also true_color color_name (AutoCAD R2004)
line_type = self.__STROKE_STYLE__[path_style.stroke_style]
# https://ezdxf.readthedocs.io/en/latest/graphic_base_class.html#GraphicEntity.dxf.lineweight
# line_weight = float(path_syle.line_width.replace('pt', '')) / 3 # Fixme: pt ???
return {'linetype': line_type, 'color': color} # 'lineweight':line_weight
##############################################
def paint_TextItem(self, item):
position = self._cast_position(item.position)
# Fixme: anchor position
# https://ezdxf.readthedocs.io/en/latest/tutorials/text.html
self._model_space.add_text(item.text).set_pos(list(position), align='CENTER')
##############################################
def paint_CircleItem(self, item):
# in fact a graphic dot
pass # skipped for DXF
##############################################
def paint_SegmentItem(self, item):
positions = self._cast_positions(item.positions)
self._model_space.add_line(
*positions,
dxfattribs=self._graphic_style(item),
)
##############################################
def paint_CubicBezierItem(self, item):
positions = self._cast_positions(item.positions)
for position in positions:
position.append(0)
# https://ezdxf.readthedocs.io/en/latest/layouts.html#Layout.add_open_spline
self._model_space.add_open_spline(
positions,
degree=3,
dxfattribs=self._graphic_style(item),
)
####################################################################################################
_driver_to_cls = {
'ezdxf': EzdxfPainter,
}
def DxfPainter(*args, **kwargs):
"""Wrapper to driver classes"""
driver = kwargs.get('driver', 'ezdxf')
if 'driver' in kwargs:
del kwargs['driver']
return _driver_to_cls[driver](*args, **kwargs)
Patro-master-Patro/Patro/GraphicEngine/Painter/MplPainter.py 0000664 0000000 0000000 00000011157 13424433274 0024351 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import logging
from matplotlib import pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches
from Patro.GeometryEngine.Vector import Vector2D
from .Painter import Painter, Tiler
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class MplPainter(Painter):
__STROKE_STYLE__ = {
None: None,
'dashDotLine': 'dashdot', # '--'
'dotLine': 'dotted', # ':'
'hair': 'solid', # '-'
'none': None,
'solid': 'solid',
}
__COLOR__ = {
None : None,
'black': 'black',
}
##############################################
def __init__(self, scene, paper):
super().__init__(scene)
self._paper = paper
self._figure = plt.figure(
# figsize=(self._paper.width_in, self._paper.height_in),
# dpi=200,
)
self._axes = self._figure.add_subplot(111)
bounding_box = scene.bounding_box
factor = 10 / 100
x_margin = bounding_box.x.length * factor
y_margin = bounding_box.y.length * factor
margin = max(x_margin, y_margin)
bounding_box = bounding_box.clone().enlarge(margin)
self._axes.set_xlim(bounding_box.x.inf, bounding_box.x.sup)
self._axes.set_ylim(bounding_box.y.inf, bounding_box.y.sup)
self._axes.set_aspect('equal')
self._coordinates = {}
self.paint()
##############################################
def show(self):
plt.show()
##############################################
def _add_path(self, item, vertices, codes):
path = Path(vertices, codes)
path_syle = item.path_style
color = self.__COLOR__[path_syle.stroke_color]
line_style = self.__STROKE_STYLE__[path_syle.stroke_style]
line_width = float(path_syle.line_width.replace('pt', '')) / 3
patch = patches.PathPatch(path, edgecolor=color, facecolor='none', linewidth=line_width, linestyle=line_style)
self._axes.add_patch(patch)
##############################################
def paint_CoordinateItem(self, item):
self._coordinates[item.name] = item.position
##############################################
def _cast_position(self, position):
if isinstance(position, str):
return self._coordinates[position]
elif isinstance(position, Vector2D):
return position
##############################################
def paint_TextItem(self, item):
position = self._cast_position(item.position)
# Fixme: anchor position
self._axes.text(position.x, position.y, item.text)
##############################################
def paint_CircleItem(self, item):
center = list(self._cast_position(item.position))
circle = plt.Circle(center, .5, color='black')
self._axes.add_artist(circle)
##############################################
def paint_SegmentItem(self, item):
vertices = [list(self._cast_position(position)) for position in item.positions]
codes = [Path.MOVETO, Path.LINETO]
self._add_path(item, vertices, codes)
##############################################
def paint_CubicBezierItem(self, item):
vertices = [list(self._cast_position(position)) for position in item.positions]
codes = [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]
self._add_path(item, vertices, codes)
Patro-master-Patro/Patro/GraphicEngine/Painter/Painter.py 0000664 0000000 0000000 00000012152 13424433274 0023674 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import logging
from IntervalArithmetic import Interval2D
from Patro.Common.Math.Functions import ceil_int
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicEngine.GraphicScene.GraphicItem import GraphicItem
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class Tiler:
_logger = _module_logger.getChild('Tiler')
##############################################
def __init__(self, bounding_box, paper):
self._bounding_box = bounding_box
self._paper = paper
##############################################
def __iter__(self):
figure_margin = 2
paper_size = Vector2D(self._paper.width, self._paper.height) / 10 # mm
paper_margin = (self._paper.margin + figure_margin) / 10
area_vector = paper_size - Vector2D(paper_margin, paper_margin) * 2
number_of_columns = ceil_int(self._bounding_box.x.length / area_vector.x)
number_of_rows = ceil_int(self._bounding_box.y.length / area_vector.y)
self._logger.info('Bounding Box {}'.format(self._bounding_box))
self._logger.info('Area {}'.format(area_vector))
self._logger.info('Grid {}x{}'.format(number_of_rows, number_of_columns))
min_point = Vector2D((self._bounding_box.x.inf, self._bounding_box.y.inf))
for r in range(number_of_rows):
for c in range(number_of_columns):
local_min_point = min_point + area_vector * Vector2D(r, c)
local_max_point = local_min_point + area_vector
interval = Interval2D((local_min_point.x, local_max_point.x), (local_min_point.y, local_max_point.y))
yield interval
####################################################################################################
class Painter:
##############################################
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Register paint methods
paint_method = {}
for item_cls in GraphicItem.__subclasses__:
name = 'paint_' + item_cls.__name__
try:
paint_method[item_cls] = getattr(cls, name)
except AttributeError:
pass
cls.__paint_method__ = paint_method
##############################################
def __init__(self, scene):
self._scene = scene
##############################################
@property
def scene(self):
return self._scene
# @scene.setter
# def scene(self, value):
# self._scene = value
##############################################
def paint(self):
if self._scene is None:
return
# Fixme: GraphicItemScope
for item in self._scene.z_value_iter():
self.__paint_method__[item.__class__](self, item)
##############################################
def cast_position(self, position):
"""Call :meth:`GraphicSceneScope.cast_position`, cast coordinate and apply scope transformation,
*position* can be a coordinate name string of a:class:`Vector2D`.
"""
return self._scene.cast_position(position)
##############################################
def cast_item_positions(self, item):
return [self.cast_position(position) for position in item.positions]
##############################################
def paint_CoordinateItem(self, item):
raise NotImplementedError
##############################################
def paint_TextItem(self, item):
raise NotImplementedError
##############################################
def paint_CircleItem(self, item):
raise NotImplementedError
##############################################
def paint_SegmentItem(self, item):
raise NotImplementedError
##############################################
def paint_CubicBezierItem(self, item):
raise NotImplementedError
Patro-master-Patro/Patro/GraphicEngine/Painter/Paper.py 0000664 0000000 0000000 00000005334 13424433274 0023345 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
from math import sqrt
####################################################################################################
class PaperSize:
"""Class to implements ISO 216 / ISO 269 paper size."""
# A0 841 × 1189 -> 594 × 841
# B0 1000 × 1414 -> 707 × 1000
# C0 917 × 1297 -> 648 × 917
LETTER_TO_DIMENSION = {
'A': 1189,
'B': 1414,
'C': 1297
}
##############################################
def __init__(self, name, orientation, margin):
name = name.upper()
letter = name[0]
level = int(name[1:])
if letter not in ('A', 'B', 'C'):
raise ValueError
if level < 0 or level > 10:
raise ValueError
self._name = name
if orientation not in ('portrait', 'landscape'):
raise ValueError
self._orientation = orientation
self._margin = float(margin)
SQRT_2 = sqrt(2)
scale = SQRT_2**level
height = self.LETTER_TO_DIMENSION[letter] / scale
width = height / SQRT_2
if orientation == 'landscape':
height, width = width, height
self._height = height
self._width = width
##############################################
@property
def name(self):
return self._name
@property
def orientation(self):
return self._orientation
@property
def height(self):
return self._height
@property
def width(self):
return self._width
@property
def height_in(self):
return self._height / 25.4
@property
def width_in(self):
return self._width / 25.4
@property
def margin(self):
return self._margin
Patro-master-Patro/Patro/GraphicEngine/Painter/PdfPainter.py 0000664 0000000 0000000 00000013772 13424433274 0024337 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
__all__ = [
'PdfPainter',
]
####################################################################################################
import logging
from Patro.GeometryEngine.Vector import Vector2D
from .Painter import Painter, Tiler
try:
import reportlab
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm, cm
except ImportError:
reportlab = None
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class PdfPainterBase(Painter):
##############################################
def __init__(self, path, scene, paper):
super().__init__(scene)
self._path = path
self._paper = paper
self._coordinates = {}
##############################################
def _cast_position(self, position):
# Fixme: to base class
if isinstance(position, str):
return self._coordinates[position]
elif isinstance(position, Vector2D):
return position
##############################################
def paint_CoordinateItem(self, item):
# Fixme: to base class
self._coordinates[item.name] = item.position
##############################################
@staticmethod
def mm_to_pt(x):
return x * 72 / 25.4
##############################################
@property
def page_size(self):
return [self.mm_to_pt(x) for x in (self._paper.width, self._paper.height)]
####################################################################################################
class ReportlabPainter(PdfPainterBase):
__STROKE_STYLE__ = {
None: None,
'dashDotLine': (6, 3),
'dotLine': (1, 2),
'hair': (),
'none': (), # Fixme: ???
'solid': (),
}
__COLOR__ = {
None : None,
'black': 'black',
}
##############################################
def __init__(self, path, scene, paper):
super().__init__(path, scene, paper)
self._canvas = canvas.Canvas(
str(self._path),
pagesize=self.page_size,
# bottomup=1,
# pageCompression=0,
# encoding=rl_config.defaultEncoding,
# verbosity=0,
# encrypt=None,
)
# self._canvas.setAuthor()
# self._canvas.addOutlineEntry(title, key, level=0, closed=None)
# self._canvas.setTitle(title)
# self._canvas.setSubject(subject)
bounding_box = scene.bounding_box
# print(bounding_box, bounding_box.x.length, bounding_box.y.length)
self._canvas.translate(-bounding_box.x.inf*cm, -bounding_box.y.inf*cm)
self._canvas.translate(1*cm, 1*cm) # Fixme: margin
self._canvas.setFont('Times-Roman', 20)
self.paint()
self._canvas.showPage()
self._canvas.save()
##############################################
def _cast_position(self, position):
position = super()._cast_position(position)
return position.clone() * cm * .7 # Fixme:
##############################################
def _cast_positions(self, positions):
vertices = []
for position in positions:
vertices += list(self._cast_position(position))
return vertices
##############################################
def _set_graphic_style(self, item):
path_syle = item.path_style
color = self.__COLOR__[path_syle.stroke_color]
self._canvas.setStrokeColor(color)
line_style = self.__STROKE_STYLE__[path_syle.stroke_style]
self._canvas.setDash(*line_style)
line_width = float(path_syle.line_width.replace('pt', '')) / 3 # Fixme: pt ???
self._canvas.setLineWidth(line_width)
##############################################
def paint_TextItem(self, item):
position = self._cast_position(item.position)
# Fixme: anchor position
self._canvas.drawString(position.x, position.y, item.text)
##############################################
def paint_CircleItem(self, item):
position = self._cast_position(item.position)
self._canvas.saveState()
self._canvas.setFillColor('black')
self._canvas.circle(position.x, position.y, 2*mm, fill=1)
self._canvas.restoreState()
##############################################
def paint_SegmentItem(self, item):
self._canvas.line(*self._cast_positions(item.positions))
##############################################
def paint_CubicBezierItem(self, item):
self._canvas.bezier(*self._cast_positions(item.positions))
####################################################################################################
_driver_to_cls = {
'reportlab': ReportlabPainter,
}
def PdfPainter(*args, **kwargs):
"""Wrapper to driver classes"""
driver = kwargs.get('driver', 'reportlab')
del kwargs['driver']
return _driver_to_cls[driver](*args, **kwargs)
Patro-master-Patro/Patro/GraphicEngine/Painter/QtPainter.py 0000664 0000000 0000000 00000057661 13424433274 0024217 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement a Qt Painter.
"""
####################################################################################################
import logging
import math
import numpy as np
from IntervalArithmetic import Interval2D
from QtShim.QtCore import (
Property, Signal, Slot, QObject,
QRectF, QSize, QSizeF, QPointF, Qt,
)
from QtShim.QtGui import QColor, QFont, QFontMetrics, QImage, QPainter, QPainterPath, QBrush, QPen
# from QtShim.QtQml import qmlRegisterType
from QtShim.QtQuick import QQuickPaintedItem
from .Painter import Painter
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicEngine.GraphicScene.Scene import GraphicScene
from Patro.GraphicStyle import StrokeStyle, CapStyle, JoinStyle
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class QtScene(QObject, GraphicScene):
"""Class to add Qt Object features to GraphicScene ."""
_logger = _module_logger.getChild('QtScene')
##############################################
def __init__(self):
QObject.__init__(self)
GraphicScene.__init__(self)
####################################################################################################
class QtPainter(Painter):
"""Class to implement a Qt painter."""
__STROKE_STYLE__ = {
None: None, # Fixme: ???
StrokeStyle.NoPen: Qt.NoPen,
StrokeStyle.SolidLine: Qt.SolidLine,
StrokeStyle.DashLine: Qt.DashLine,
StrokeStyle.DotLine: Qt.DotLine,
StrokeStyle.DashDotLine: Qt.DashDotLine,
StrokeStyle.DashDotDotLine: Qt.DashDotDotLine,
}
__CAP_STYLE__ = {
CapStyle.FlatCap: Qt.FlatCap,
CapStyle.SquareCap: Qt.SquareCap,
CapStyle.RoundCap: Qt.RoundCap,
}
__JOIN_STYLE__ = {
JoinStyle.MiterJoin: Qt.MiterJoin,
JoinStyle.BevelJoin: Qt.BevelJoin,
JoinStyle.RoundJoin: Qt.RoundJoin,
JoinStyle.SvgMiterJoin: Qt.SvgMiterJoin,
}
_logger = _module_logger.getChild('QtPainter')
##############################################
def __init__(self, scene=None):
super().__init__(scene)
self._show_grid = True
# self._paper = paper
# self._translation = QPointF(0, 0)
# self._scale = 1
##############################################
# @property
# def translation(self):
# return self._translation
# @translation.setter
# def translation(self, value):
# self._translation = value
# @property
# def scale(self):
# return self._scale
# @scale.setter
# def scale(self, value):
# print('set scale', value)
# self._scale = value
##############################################
def to_svg(self, path, scale=10, dpi=100, title='', description=''):
"""Render the scene to SVG"""
from QtShim.QtSvg import QSvgGenerator
generator = QSvgGenerator()
generator.setFileName(str(path))
generator.setTitle(str(title))
generator.setDescription(str(description))
generator.setResolution(dpi)
# Fixme: scale
# Scale applied to (x,y) and radius but not line with
self._scale = scale
bounding_box = self._scene.bounding_box
size = QSize(*[x*self._scale for x in bounding_box.size])
view_box = QRectF(*[x*self._scale for x in bounding_box.rect])
generator.setSize(size)
generator.setViewBox(view_box)
painter = QPainter()
painter.begin(generator)
self.paint(painter)
painter.end()
self._scale = None
##############################################
def paint(self, painter):
self._logger.info('Start painting')
self._painter = painter
if self._show_grid:
self._paint_grid()
super().paint()
self._logger.info('Paint done')
##############################################
def length_scene_to_viewport(self, length):
return length * self._scale
@property
def scene_area(self):
return None
def scene_to_viewport(self, position):
return QPointF(position.x * self._scale, position.y * self._scale)
# Note: painter.scale apply to text as well
# point = QPointF(position.x, position.y)
# point += self._translation
# point *= self._scale
# point = QPointF(point.x(), -point.y())
# return point
##############################################
def cast_position(self, position):
"""Cast coordinate, apply scope transformation and convert scene to viewport, *position* can be a
coordinate name string of a:class:`Vector2D`.
"""
position = super().cast_position(position)
return self.scene_to_viewport(position)
##############################################
def _set_pen(self, item):
path_syle = item.path_style
# print('_set_pen', item, path_syle)
if item.selected:
color = QColor('red') # Fixme: style
else:
color = path_syle.stroke_color
if color is not None:
color = QColor(str(color))
color.setAlphaF(path_syle.stroke_alpha)
else:
color = None
line_style = self.__STROKE_STYLE__[path_syle.stroke_style]
line_width = path_syle.line_width_as_float
# Fixme: selection style
if item.selected:
line_width *= 4
fill_color = path_syle.fill_color
if fill_color is not None:
color = QColor(str(fill_color))
color.setAlphaF(path_syle.fill_alpha)
self._painter.setBrush(color)
# return None
else:
self._painter.setBrush(Qt.NoBrush)
# print(item, color, line_style)
if color is None or line_style is StrokeStyle.NoPen:
# invisible item
pen = QPen(Qt.NoPen)
# print('Warning Pen:', item, item.user_data, color, line_style)
return None
else:
pen = QPen(
QBrush(color),
line_width,
line_style,
self.__CAP_STYLE__[path_syle.cap_style],
self.__JOIN_STYLE__[path_syle.join_style],
)
self._painter.setPen(pen)
return pen
##############################################
def _paint_grid(self):
area = self.scene_area
# Fixme:
if area is None:
return
xinf, xsup = area.x.inf, area.x.sup
yinf, ysup = area.y.inf, area.y.sup
length = min(area.x.length, area.y.length)
color = QColor('black')
brush = QBrush(color)
pen = QPen(brush, .75)
self._painter.setPen(pen)
self._painter.setBrush(Qt.NoBrush)
step = max(10**int(math.log10(length)), 10)
small_step = step // 10
self._logger.info('Grid of {}/{} for {:.1f} mm'.format(step, small_step, length))
self._paint_axis_grid(xinf, xsup, yinf, ysup, True, step)
self._paint_axis_grid(yinf, ysup, xinf, xsup, False, step)
color = QColor('black')
brush = QBrush(color)
pen = QPen(brush, .25)
self._painter.setPen(pen)
self._painter.setBrush(Qt.NoBrush)
self._paint_axis_grid(xinf, xsup, yinf, ysup, True, small_step)
self._paint_axis_grid(yinf, ysup, xinf, xsup, False, small_step)
##############################################
def _paint_axis_grid(self, xinf, xsup, yinf, ysup, is_x, step):
for i in range(int(xinf // step), int(xsup // step) +1):
x = i*step
if xinf <= x <= xsup:
if is_x:
p0 = Vector2D(x, yinf)
p1 = Vector2D(x, ysup)
else:
p0 = Vector2D(yinf, x)
p1 = Vector2D(ysup, x)
p0 = self.cast_position(p0)
p1 = self.cast_position(p1)
self._painter.drawLine(p0, p1)
##############################################
def paint_CoordinateItem(self, item):
self._coordinates[item.name] = self.scene_to_viewport(item.position)
##############################################
def _paint_arc(self, item, center, radius_x, radius_y):
if item.is_closed:
self._painter.drawEllipse(center, radius, radius)
else:
# drawArc cannot be filled !
rectangle = QRectF(
center + QPointF(-radius_x, radius_y),
center + QPointF(radius_x, -radius_y),
)
start_angle, stop_angle = [int(angle*16) for angle in (item.start_angle, item.stop_angle)]
span_angle = stop_angle - start_angle
if span_angle < 0:
span_angle = 5760 + span_angle
self._painter.drawArc(rectangle, start_angle, span_angle)
# self._painter.drawArc(center.x, center.y, radius, radius, start_angle, stop_angle)
##############################################
def paint_CircleItem(self, item):
center = self.cast_position(item.position)
radius = self.length_scene_to_viewport(item.radius)
pen = self._set_pen(item)
self._paint_arc(item, center, radius, radius)
##############################################
def paint_EllipseItem(self, item):
center = self.cast_position(item.position)
radius_x = self.length_scene_to_viewport(item.radius_x)
radius_y = self.length_scene_to_viewport(item.radius_y)
pen = self._set_pen(item)
# Fixme: angle !!!
self._paint_arc(item, center, radius_x, radius_y)
##############################################
def _paint_cubic(self, item, vertices):
pen = self._set_pen(item)
path = QPainterPath()
path.moveTo(vertices[0])
path.cubicTo(*vertices[1:])
self._painter.drawPath(path)
path_style = item.path_style
# if path_style.show_control:
if getattr(path_style, 'show_control', False):
color = QColor(str(path_style.control_color))
brush = QBrush(color)
pen = QPen(brush, 1) # Fixme
self._painter.setPen(pen)
self._painter.setBrush(Qt.NoBrush)
path = QPainterPath()
path.moveTo(vertices[0])
for vertex in vertices[1:]:
path.lineTo(vertex)
self._painter.drawPath(path)
# Fixme:
radius = 3
self._painter.setBrush(brush)
for vertex in vertices:
self._painter.drawEllipse(vertex, radius, radius)
self._painter.setBrush(Qt.NoBrush) # Fixme:
##############################################
def paint_QuadraticBezierItem(self, item):
vertices = [self.cast_position(position) for position in item.cubic_positions]
self._paint_cubic(item, vertices)
##############################################
def paint_CubicBezierItem(self, item):
vertices = self.cast_item_positions(item)
self._paint_cubic(item, vertices)
##############################################
def paint_ImageItem(self, item):
vertices = self.cast_item_positions(item)
rec = QRectF(vertices[0], vertices[1])
image = item.image
height, width, bytes_per_line = image.shape
bytes_per_line *= width
qimage = QImage(image, width, height, bytes_per_line, QImage.Format_RGB888)
self._painter.drawImage(rec, qimage)
##############################################
def paint_SegmentItem(self, item):
self._set_pen(item)
vertices = self.cast_item_positions(item)
self._painter.drawLine(*vertices)
##############################################
def paint_PolylineItem(self, item):
self._set_pen(item)
vertices = self.cast_item_positions(item)
path = QPainterPath()
path.moveTo(vertices[0])
for vertex in vertices[1:]:
path.lineTo(vertex)
self._painter.drawPath(path)
##############################################
def paint_TextItem(self, item):
position = self.cast_position(item.position)
font = item.font
qfont = QFont(font.family, font.point_size) # weight, italic = False
# Fixme: anchor position
# font_metrics = QFontMetrics(qfont)
# height = font_metrics.height()
# width = font_metrics.width(item.text)
self._painter.setFont(qfont)
self._painter.drawText(position, item.text)
####################################################################################################
class ViewportArea:
"""Class to implement a viewport."""
_logger = _module_logger.getChild('ViewportArea')
##############################################
def __init__(self):
self._scene = None
# self._width = None
# self._height = None
self._viewport_size = None
self._scale = 1
self._center = None
self._area = None
##############################################
def __bool__(self):
return self._scene is not None
##############################################
@classmethod
def _to_np_array(cls, *args):
if len(args) == 1:
args = args[0]
return np.array(args, dtype=np.float)
##############################################
@classmethod
def _point_to_np(cls, point):
return cls._to_np_array(point.x(), point.y())
##############################################
@property
def viewport_size(self):
return self._viewport_size
# return (self._width, self._height)
@viewport_size.setter
def viewport_size(self, geometry):
# self._width = geometry.width()
# self._height = geometry.height()
self._viewport_size = self._to_np_array(geometry.width(), geometry.height())
if self:
self._update_viewport_area()
##############################################
@property
def scene(self):
return self._scene
@scene.setter
def scene(self, value):
if not isinstance(value, GraphicScene):
raise ValueError
self._scene = value
@property
def scene_area(self):
if self:
return self._scene.bounding_box
else:
return None
##############################################
# @property
# def scale(self):
# return self._scale # px / mm
@property
def scale_px_by_mm(self):
# Fixme: assume unit is mm
return self._scale
@property
def scale_mm_by_px(self):
return 1 / self._scale
@property
def center(self):
return self._center
# @property
# def center_as_point(self):
# return QPointF(self._center[0], self._center[1])
@property
def area(self):
return self._area
##############################################
def _update_viewport_area(self):
offset = self._viewport_size / 2 * self.scale_mm_by_px
x, y = self._center
dx, dy = offset
self._area = Interval2D(
(x - dx, x + dx),
(y - dy, y + dy),
)
# Fixme: QPointF ???
self._translation = - QPointF(self._area.x.inf, self._area.y.sup)
# self._logger.debug('_update_viewport_area', self._center, self.scale_mm_by_px, self._area)
##############################################
def _compute_scale_to_fit_scene(self, margin=None):
# width_scale = self._width / scene_area.x.length
# height_scale = self._height / scene_area.y.length
# scale = min(width_scale, height_scale)
# scale [px/mm]
# Add 2% to scene for margin
margin_scale = 1 + 2 / 100
axis_scale = self._viewport_size / (self._to_np_array(self.scene_area.size) * margin_scale)
axis = axis_scale.argmin()
scale = axis_scale[axis]
return scale, axis
##############################################
def zoom_at(self, center, scale):
self._center = center
self._scale = scale
self._update_viewport_area()
##############################################
def fit_scene(self):
# Fixme: AttributeError: 'NoneType' object has no attribute 'center'
if self:
center = self._to_np_array(self.scene_area.center)
scale, axis = self._compute_scale_to_fit_scene()
self.zoom_at(center, scale)
##############################################
def scene_to_viewport(self, position):
point = QPointF(position.x, position.y)
point += self._translation
point *= self._scale
point = QPointF(point.x(), -point.y())
return point
##############################################
def viewport_to_scene(self, position):
point = QPointF(position.x(), -position.y())
point /= self._scale
point -= self._translation
return self._point_to_np(point)
##############################################
def length_scene_to_viewport(self, length):
return length * self._scale
##############################################
def length_viewport_to_scene(self, length):
return length / self._scale
##############################################
def pan_delta_to_scene(self, position):
point = self._point_to_np(position)
point *= self.scale_mm_by_px
return point
####################################################################################################
class QtQuickPaintedSceneItem(QQuickPaintedItem, QtPainter):
"""Class to implement a painter as Qt Quick item"""
_logger = _module_logger.getChild('QtQuickPaintedSceneItem')
##############################################
def __init__(self, parent=None):
QQuickPaintedItem.__init__(self, parent)
QtPainter.__init__(self)
# Setup backend rendering
self.setAntialiasing(True)
# self.setRenderTarget(QQuickPaintedItem.Image) # high quality antialiasing
self.setRenderTarget(QQuickPaintedItem.FramebufferObject) # use OpenGL
self._viewport_area = ViewportArea()
##############################################
def geometryChanged(self, new_geometry, old_geometry):
# self._logger.info('geometryChanged', new_geometry, old_geometry)
self._viewport_area.viewport_size = new_geometry
# if self._scene:
# self._update_transformation()
QQuickPaintedItem.geometryChanged(self, new_geometry, old_geometry)
##############################################
# def _update_transformation(self):
# area = self._viewport_area.area
# self.translation = - QPointF(area.x.inf, area.y.sup)
# self.scale = self._viewport_area.scale_px_by_mm # QtPainter
##############################################
@property
def scene_area(self):
return self._viewport_area.area
##############################################
def scene_to_viewport(self, position):
return self._viewport_area.scene_to_viewport(position)
##############################################
def length_scene_to_viewport(self, length):
return self._viewport_area.length_scene_to_viewport(length)
##############################################
def length_viewport_to_scene(self, length):
return self._viewport_area.length_viewport_to_scene(length)
##############################################
sceneChanged = Signal()
@Property(QtScene, notify=sceneChanged)
def scene(self):
return self._scene
@scene.setter
def scene(self, scene):
if self._scene is not scene:
# self._logger.info('QtQuickPaintedSceneItem set scene', scene)
self._logger.info('set scene') # Fixme: don't print ???
self._scene = scene
self._viewport_area.scene = scene
self._viewport_area.fit_scene()
# self._update_transformation()
self.update()
self.sceneChanged.emit()
##############################################
# zoomChanged = Signal()
# @Property(float, notify=zoomChanged)
# def zoom(self):
# return self._zoom
# @zoom.setter
# def zoom(self, zoom):
# if self._zoom != zoom:
# print('QtQuickPaintedSceneItem zoom', zoom, self.width(), self.height())
# self._zoom = zoom
# self.set_transformation(zoom)
# self.update()
# self.zoomChanged.emit()
##############################################
@Property(float)
def zoom(self):
return self._viewport_area.scale_px_by_mm
##############################################
@Slot(QPointF, result=str)
def format_coordinate(self, position):
scene_position = self._viewport_area.viewport_to_scene(position)
return '{:.3f}, {:.3f}'.format(scene_position[0], scene_position[1])
##############################################
@Slot(float)
def zoom_at_center(self, zoom):
self._viewport_area.zoom_at(self._viewport_area.center, zoom)
self.update()
##############################################
@Slot(QPointF, float)
def zoom_at(self, position, zoom):
# print('zoom_at', position, zoom)
scene_position = self._viewport_area.viewport_to_scene(position)
self._viewport_area.zoom_at(scene_position, zoom)
self.update()
##############################################
@Slot()
def fit_scene(self):
self._viewport_area.fit_scene()
self.update()
##############################################
@Slot(QPointF)
def pan(self, dxy):
position = self._viewport_area.center + self._viewport_area.pan_delta_to_scene(dxy)
self._viewport_area.zoom_at(position, self._viewport_area.scale_px_by_mm)
self.update()
##############################################
@Slot(QPointF)
def item_at(self, position, radius_px=10):
self._scene.update_rtree()
self._scene.unselect_items()
scene_position = Vector2D(self._viewport_area.viewport_to_scene(position))
radius = self.length_viewport_to_scene(radius_px)
self._logger.info('Item selection at {} with radius {:1f} mm'.format(scene_position, radius))
items = self._scene.item_at(scene_position, radius)
if items:
distance, nearest_item = items[0]
# print('nearest item at {} #{:6.2f} {} {}'.format(scene_position, len(items), distance, nearest_item.user_data))
nearest_item.selected = True
# Fixme: z_value ???
for pair in items[1:]:
distance, item = pair
# print(' {:6.2f} {}'.format(distance, item.user_data))
self.update()
####################################################################################################
# qmlRegisterType(QtQuickPaintedSceneItem, 'Patro', 1, 0, 'PaintedSceneItem')
Patro-master-Patro/Patro/GraphicEngine/Painter/SvgPainter.py 0000664 0000000 0000000 00000012112 13424433274 0024350 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
__all__ = [
'SvgPainter',
]
# See also
# https://github.com/mozman/svgwrite
# https://svgwrite.readthedocs.io/en/master/
####################################################################################################
import logging
from Patro.FileFormat.Svg.SvgFile import SvgFile
from Patro.FileFormat.Svg import SvgFormat
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GeometryEngine.Transformation import AffineTransformation2D
from .Painter import Painter
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class SvgPainter(Painter):
__STROKE_STYLE__ = {
None: None,
'dashDotLine': [6, 3],
'dotLine': [1, 2],
'hair': None,
'none': None, # Fixme: ???
'solid': None,
}
__COLOR__ = {
None : None,
'black': 'black',
}
##############################################
def __init__(self, path, scene, paper):
super().__init__(scene)
self._path = path
self._paper = paper
self._coordinates = {}
bounding_box = scene.bounding_box
self._transformation = AffineTransformation2D.Scale(10, -10)
self._transformation *= AffineTransformation2D.Translation(-Vector2D(bounding_box.x.inf, bounding_box.y.sup))
self._tree = []
self._append(SvgFormat.Style(text='''
.normal { font: 12px sans-serif; }
'''))
self.paint()
self._svg_file = SvgFile()
self._svg_file.write(paper, self._tree, transformation=None, path=path)
##############################################
def _append(self, element):
self._tree.append(element)
##############################################
def _cast_position(self, position):
# Fixme: to base class
if isinstance(position, str):
position = self._coordinates[position]
return self._transformation * position
##############################################
def paint_CoordinateItem(self, item):
# Fixme: to base class
self._coordinates[item.name] = item.position
##############################################
def _cast_positions(self, positions):
vertices = []
for position in positions:
vertices += list(self._cast_position(position))
return vertices
##############################################
def _graphic_style(self, item):
path_syle = item.path_style
color = self.__COLOR__[path_syle.stroke_color]
line_style = self.__STROKE_STYLE__[path_syle.stroke_style]
line_width = str(float(path_syle.line_width.replace('pt', '')) / 3) # Fixme: pt ???
kwargs = dict(stroke=color, stroke_width=line_width)
if line_style:
kwargs['stroke_dasharray'] = line_style
return kwargs
##############################################
def paint_TextItem(self, item):
x, y = list(self._cast_position(item.position))
# Fixme: anchor position
text = SvgFormat.Text(x=x, y=y, text=item.text, fill='black')
self._append(text)
##############################################
def paint_CircleItem(self, item):
x, y = list(self._cast_position(item.position))
circle = SvgFormat.Text(cx=x, cy=y, r=2, fill='black', _class='normal')
self._append(circle)
##############################################
def paint_SegmentItem(self, item):
x1, y1, x2, y2 = self._cast_positions(item.positions)
line = SvgFormat.Line(
x1=x1,
y1=y1,
x2=x2,
y2=y2,
**self._graphic_style(item),
)
self._append(line)
##############################################
def paint_CubicBezierItem(self, item):
path = SvgFormat.Path(
path_data='M {} {} C {} {}, {} {}, {} {}'.format(*self._cast_positions(item.positions)),
fill='none',
**self._graphic_style(item),
)
self._append(path)
Patro-master-Patro/Patro/GraphicEngine/Painter/TexPainter.py 0000664 0000000 0000000 00000014107 13424433274 0024357 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import logging
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicEngine.TeX.Document import Document
from Patro.GraphicEngine.TeX.Environment import Center
from Patro.GraphicEngine.TeX.Tikz import TikzFigure
from .Painter import Painter, Tiler
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
_preambule = r'''
%** Package ****************************************************************************************
%**** Page settings ******************************
\usepackage[%
paper=«0.paper_name»,%
%landscape,
%includeheadfoot,%
margin=«0.margin»mm,%
headsep=0cm, footskip=0cm,%
dvips,%
]{geometry}
%**** Encoding ***********************************
\usepackage[utf8]{inputenc}
%*************************************************
\usepackage{tikz}
\usetikzlibrary{calc}
\usepgflibrary{arrows}
\usepackage{calc}
%***************************************************************************************************
'''
####################################################################################################
class TexPainter(Painter):
##############################################
def __init__(self, path, scene, paper):
super().__init__(scene)
self._document = Document(path, 'article', '12pt')
self._paper = paper
self._preambule.append(Document._format(_preambule, self))
##############################################
@property
def paper_name(self):
return self._paper.name.lower() + 'paper'
@property
def margin(self):
return self._paper.margin
@property
def _preambule(self):
return self._document.preambule
@property
def _content(self):
return self._document.content
##############################################
def _format_position(self, position):
if isinstance(position, str):
return '(' + position + ')'
elif isinstance(position, Vector2D):
return '({0.x:.2f},{0.y:.2f})'.format(position)
##############################################
def paint_CoordinateItem(self, item):
# Fixme: implement detail in TikzFigure ?
coordinate = self._format_position(item.position)
self._figure.append(r'\coordinate ({0}) at {1};'.format(item.name, coordinate) + '\n')
##############################################
def paint_TextItem(self, item):
coordinate = self._format_position(item.position)
self._figure.append(Document._format(r'\draw[] «0» node[anchor=north west] {«1»};', coordinate, item.text) + '\n')
##############################################
def paint_CircleItem(self, item):
coordinate = self._format_position(item.position)
self._figure.append(r'\fill [black] {0} circle (1pt);'.format(coordinate) + '\n')
##############################################
def paint_SegmentItem(self, item):
style = TikzFigure.format_path_style(item.path_style)
coordinates = [self._format_position(position) for position in item.positions]
self._figure.append(r'\draw[{0}] {1} -- {2};'.format(style, *coordinates) + '\n')
##############################################
def paint_CubicBezierItem(self, item):
style = TikzFigure.format_path_style(item.path_style)
coordinates = [self._format_position(position) for position in item.positions]
self._figure.append(r'\draw[{0}] {1} .. controls {2} and {3} .. {4};'.format(style, *coordinates) + '\n')
##############################################
def _add_pagestyle_empty(self):
# Fixme: implement in Document ?
self._content.append(r'\pagestyle{empty}' + '\n')
##############################################
def add_detail_figure(self):
# Fixme: split document / scene painter
# don't make sense to generate a0 and a4 content on the same file !
self._add_pagestyle_empty()
self._content.append(r'\fontsize{64}{72}\selectfont % \fontsize{size}{baselineskip}' + '\n')
options = 'x=8mm,y=8mm'
self._figure = TikzFigure(options)
self._content.append(Center().append(self._figure))
self.paint()
##############################################
def add_tiled_detail_figure(self):
self._add_pagestyle_empty()
tiler = Tiler(self._scene.bounding_box, self._paper)
for interval in tiler:
options = 'x=10mm,y=10mm'
self._figure = TikzFigure(options)
self._figure.append(r'\draw[clip] '
r'({0.x.inf:.2f},{0.y.inf:.2f}) -- '
r'({0.x.sup:.2f},{0.y.inf:.2f}) -- '
r'({0.x.sup:.2f},{0.y.sup:.2f}) -- '
r'({0.x.inf:.2f},{0.y.sup:.2f}) -- cycle;'.format(interval) + '\n')
self._content.append(Center().append(self._figure))
self.paint()
Patro-master-Patro/Patro/GraphicEngine/Painter/__init__.py 0000664 0000000 0000000 00000001761 13424433274 0024035 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements painters for a graphic scene.
"""
Patro-master-Patro/Patro/GraphicEngine/TeX/ 0000775 0000000 0000000 00000000000 13424433274 0021015 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/GraphicEngine/TeX/Buffer.py 0000664 0000000 0000000 00000004126 13424433274 0022603 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import copy
####################################################################################################
class Buffer:
##############################################
def __init__(self):
self._content = []
self.clear()
##############################################
def __str__(self):
source = ''
for item in self._content:
source += str(item)
return source
##############################################
def clear(self):
self._content.clear()
##############################################
def append(self, data, deepcopy=False):
if isinstance(data, list):
for item in data:
self._append(item, deepcopy)
else:
self._append(data, deepcopy)
return self
##############################################
def _append(self, data, deepcopy=False):
if deepcopy:
data = copy.deepcopy(data)
# if isinstance(data, str) or isinstance(data, Buffer):
self._content.append(data)
Patro-master-Patro/Patro/GraphicEngine/TeX/Document.py 0000664 0000000 0000000 00000007031 13424433274 0023146 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import os
import subprocess
####################################################################################################
from .Buffer import Buffer
from .Environment import Environment
####################################################################################################
LINE_BREAK = r'\\'
####################################################################################################
class Document:
##############################################
def __init__(self, filename, class_name, class_options=''):
# Fixme: Path, vs path
if (len(filename) - filename.rfind('.tex')) == 4:
filename = filename[:-4]
self._filename = filename
self._class_name = class_name
self._class_options = class_options
self._preambule = Buffer()
self._content = Environment(name='document')
##############################################
@property
def tex_filename(self):
return self._filename + '.tex'
@property
def output_directory(self):
return os.path.dirname(self._tex_filename())
@property
def pdf_filename(self):
return self._filename + '.pdf'
@property
def preambule(self):
return self._preambule
@property
def content(self):
return self._content
##############################################
@staticmethod
def _format(pattern, *args, **kwargs):
pattern = pattern.replace('{', '{{')
pattern = pattern.replace('}', '}}')
pattern = pattern.replace('«', '{')
pattern = pattern.replace('»', '}')
return pattern.format(*args, **kwargs)
##############################################
def __str__(self):
source = self._format(r'\documentclass[«0._class_options»]{«0._class_name»}', self) + '\n'
source += str(self._preambule)
source += str(self._content)
return source
##############################################
def newpage(self):
self._content.append(r'\newpage' + '\n')
##############################################
def write(self):
with open(self.tex_filename, 'w') as fd:
fd.write(str(self))
##############################################
def run_pdflatex(self):
pass
# command_template = 'pdflatex --interaction=batchmode --output-directory=%s %s'
# command = command_template % (self.output_directory(), self.tex_filename())
# subprocess.call([command], shell=True)
Patro-master-Patro/Patro/GraphicEngine/TeX/Environment.py 0000664 0000000 0000000 00000004165 13424433274 0023701 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
from .Buffer import Buffer
####################################################################################################
class Environment(Buffer):
##############################################
def __init__(self, name, options=''):
super(Environment, self).__init__()
self._name = name
# self._options = options
self._begin_buffer = Buffer()
self._end_buffer = Buffer()
if options:
_options = '[' + options + ']'
else:
_options = ''
self._begin_buffer.append(r'\begin{%s}%s' % (self._name, _options) + '\n')
self._end_buffer.append(r'\end{%s}' % (self._name) + '\n')
##############################################
def __str__(self):
source = str(self._begin_buffer)
source += super(Environment, self).__str__()
source += str(self._end_buffer)
return source
####################################################################################################
class Center(Environment):
def __init__(self):
Environment.__init__(self, 'center')
Patro-master-Patro/Patro/GraphicEngine/TeX/Tikz.py 0000664 0000000 0000000 00000004316 13424433274 0022314 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
from .Environment import Environment
####################################################################################################
class TikzFigure(Environment):
##############################################
__STROKE_STYLE__ = {
None: None,
'dashDotLine': 'dash pattern=on 5mm off 4mm on 2mm off 4mm', # 'loosely dashdotted',
'dotLine': 'dash pattern=on 2mm off 2mm', # 'dotted',
'hair': 'solid',
'none': None,
'solid': 'solid',
}
__COLOR__ = {
None : None,
'black': 'black',
}
@staticmethod
def format_path_style(path_syle):
stroke_style = TikzFigure.__STROKE_STYLE__[path_syle.stroke_style]
stroke_color = TikzFigure.__COLOR__[path_syle.stroke_color]
styles = []
styles.append('line width={}'.format(path_syle.line_width))
if stroke_style is not None:
styles.append(stroke_style)
if stroke_color is not None:
styles.append(stroke_color)
return ', '.join(styles)
##############################################
def __init__(self, options=''):
super(TikzFigure, self).__init__('tikzpicture', options)
Patro-master-Patro/Patro/GraphicEngine/TeX/__init__.py 0000664 0000000 0000000 00000001745 13424433274 0023135 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements LaTeX facilities.
"""
Patro-master-Patro/Patro/GraphicEngine/__init__.py 0000664 0000000 0000000 00000003176 13424433274 0022435 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This module implements a 2D graphic engine as a scene rendered by a painter class.
A scene contains graphic items like text, image line, circle and Bézier curve. A painter class is
responsible to render the scene on the screen or a graphic file format.
These painters are available for screen rendering:
* Matplotlib : :mod:`Patro.GraphicEngine.Painter.MplPainter`
* Qt : :mod:`Patro.GraphicEngine.Painter.QtPainter`
These painters are available for graphic file format:
* DXF : :mod:`Patro.GraphicEngine.Painter.DxfPainter`
* LaTeX : :mod:`Patro.GraphicEngine.Painter.TexPainter`
* PDF : :mod:`Patro.GraphicEngine.Painter.PdfPainter`
* SVG : :mod:`Patro.GraphicEngine.Painter.SvgPainter`
"""
Patro-master-Patro/Patro/GraphicStyle/ 0000775 0000000 0000000 00000000000 13424433274 0020210 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/GraphicStyle/Color/ 0000775 0000000 0000000 00000000000 13424433274 0021266 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/GraphicStyle/Color/ColorDataBase.py 0000664 0000000 0000000 00000016552 13424433274 0024314 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement a colour database.
"""
####################################################################################################
__all__ = ['Color', 'ColorDataBase']
####################################################################################################
import colorsys
####################################################################################################
class Color:
"""Class to define a colour
Usage::
color.red
color.blue
color.green
color.name
# to get a '#rrggbb' string
str(color)
color1 == color2
"""
__STR_FORMAT__ = '#' + '{:02x}'*3
##############################################
def __init__(self, *args, **kwargs):
# self._rgb = str(rgb)
number_of_args = len(args)
if number_of_args == 1:
color = args[0]
if isinstance(str, Color):
self._red = color.red_float
self._green = color.green_float
self._blue = color.blue_float
else:
rgb = str(color)
if not rgb.startswith('#'):
raise ValueError('Invalid color {}'.format(rgb))
rgb = rgb[1:]
red, green, blue = rgb[:2], rgb[2:4], rgb[-2:]
self._red, self._green, self._blue = [self._to_float(int(x, 16))
for x in (red, green, blue)]
elif number_of_args == 3:
self._red, self._green, self._blue = [self._check_value(arg) for arg in args]
else:
self._red, self._green, self._blue = 0
if 'hue' in kwargs:
if 'light' in kwargs:
hue, light, saturation = [kwargs[x] for x in ('hue', 'light', 'saturation')]
red, green, blue = colorsys.hls_to_rgb(hue, light, saturation)
elif 'value' in kwargs:
hue, value, saturation = [kwargs[x] for x in ('hue', 'value', 'saturation')]
red, green, blue = colorsys.hsv_to_rgb(hue, saturation, value)
else:
raise ValueError('Missing color parameter')
self._red, self._green, self._blue = [self._check_value(x) for x in (red, green, blue)]
# self._name = kwargs.get('name', None)
if 'name' in kwargs:
self._name = kwargs['name']
else:
self._name = None
##############################################
def clone(self):
return self.__class__(self._red, self._green, self._blue, name=self._name)
##############################################
def __str__(self):
return self.__STR_FORMAT__.format(self.red, self.green, self.blue)
##############################################
def __repr__(self):
return 'Color({})'.format(str(self))
##############################################
@staticmethod
def _to_int(x):
return int(x*255)
@staticmethod
def _to_float(x):
return x/255
##############################################
def _check_value(self, value):
if isinstance(value, int):
if 0 <= value <= 255:
return self._to_float(value)
if isinstance(value, float):
if 0 <= value <= 1:
# return int(value * 255)
return value # keep float
raise ValueError('Invalid colour {}'.format(value))
##############################################
@property
def red_float(self):
return self._red
@property
def red(self):
return self._to_int(self._red)
@red.setter
def red(self, value):
self._red = self._check_value(value)
@property
def green_float(self):
return self._green
@property
def green(self):
return self._to_int(self._green)
@green.setter
def green(self, value):
self._green = self._check_value(value)
@property
def blue_float(self):
return self._blue
@property
def blue(self):
return self._to_int(self._blue)
@blue.setter
def blue(self, value):
self._blue = self._check_value(value)
##############################################
# note hue and saturation is ambiguous
@property
def hls(self):
return colorsys.rgb_to_hls(self._red, self._green, self._blue)
@property
def hsv(self):
return colorsys.rgb_to_hsv(self._red, self._green, self._blue)
##############################################
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = str(value)
##############################################
def __eq__(self, other):
return str(self) == str(other)
####################################################################################################
class ColorDataBase:
"""Class to implement a colour database.
The class implements a dictionary API::
color_database['black']
'black' in 'color_database
for color in color_database:
pass
We can get a color directly using::
color_database.black
"""
##############################################
def __init__(self):
self._colors = {}
##############################################
def __len__(self):
return len(self._colors)
def __iter__(self):
return iter(self._colors.values())
def __contains__(self, name):
return name in self._colors
def __getitem__(self, name):
return self._colors[str(name)]
def __getattr__(self, name):
return self._colors[name]
##############################################
def iter_names(self):
return iter(self._colors.keys())
##############################################
def add(self, name, color):
"""Register a :class:`Color` instance"""
# Fixme: color.name ???
self._colors[str(name)] = color
##############################################
def ensure_color(self, color):
"""Ensure *color* is a :class:`Color` instance"""
if color is None:
return None
elif isinstance(color, Color):
return color
elif isinstance(color, str):
if color.startswith('#'):
return Color(color)
else:
return self[color]
raise ValueError('Invalid color {}'.format(color))
Patro-master-Patro/Patro/GraphicStyle/Color/__init__.py 0000664 0000000 0000000 00000004331 13424433274 0023400 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement a colour database.
"""
####################################################################################################
__all__ = ['Colors']
####################################################################################################
from . import color_data as _color_data
from .ColorDataBase import Color, ColorDataBase
####################################################################################################
#: Colour Database Singleton as an instance of :class:`ColorDataBase`
Colors = ColorDataBase()
def __init__():
# First name set color
for color_set in (
_color_data.BASE_COLORS,
_color_data.TABLEAU_COLORS,
_color_data.XKCD_COLORS,
_color_data.CSS4_COLORS,
_color_data.QML_COLORS,
_color_data.VALENTINA_COLORS ,
):
for name, value in color_set.items():
name = name.replace(' ', '_')
color = Color(value, name=name)
if name in Colors and color != Colors[name]:
pass
# print('# {:15} {} vs {}'.format(name, color, Colors[name]))
# print('Warning: color name clash: {} {} vs {}'.format(name, color, Colors[name]))
else:
Colors.add(name, color)
__init__()
Patro-master-Patro/Patro/GraphicStyle/Color/color_data.py 0000664 0000000 0000000 00000124176 13424433274 0023762 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to define colours from several sets.
"""
####################################################################################################
# from Matplotlib, Valentina, Qt
__all__ = [
'BASE_COLORS',
'TABLEAU_COLORS',
'XKCD_COLORS',
'CSS4_COLORS',
'QML_COLORS',
'VALENTINA_COLORS',
]
####################################################################################################
# aqua #00ffff vs #13eac9
# aquamarine #7fffd4 vs #04d8b2
# azure #f0ffff vs #069af3
# beige #f5f5dc vs #e6daa6
# blue #0343df vs #0000ff
# blue #1f77b4 vs #0000ff
# brown #653700 vs #8c564b
# brown #a52a2a vs #8c564b
# chartreuse #7fff00 vs #c1f80a
# chocolate #d2691e vs #3d1c02
# coral #ff7f50 vs #fc5a50
# crimson #dc143c vs #8c000f
# cyan #17becf vs #00ffff
# darkblue #00008b vs #030764
# darkgreen #006400 vs #054907
# fuchsia #ff00ff vs #ed0dd9
# goldenrod #daa520 vs #fac205
# gold #ffd700 vs #dbb40c
# gray #808080 vs #7f7f7f
# green #008000 vs #00ff00
# green #15b01a vs #00ff00
# green #2ca02c vs #00ff00
# grey #808080 vs #929591
# indigo #4b0082 vs #380282
# ivory #fffff0 vs #ffffcb
# khaki #f0e68c vs #aaa662
# lavender #e6e6fa vs #c79fef
# lightblue #add8e6 vs #7bc8f6
# lightgreen #90ee90 vs #76ff7b
# lime #00ff00 vs #aaff32
# magenta #c20078 vs #ff00ff
# maroon #800000 vs #650021
# navy #000080 vs #01153e
# olive #6e750e vs #bcbd22
# olive #808000 vs #bcbd22
# orange #f97306 vs #ff7f0e
# orange #ffa500 vs #ff7f0e
# orangered #ff4500 vs #fe420f
# orchid #da70d6 vs #c875c4
# pink #ff81c0 vs #e377c2
# pink #ffc0cb vs #e377c2
# plum #dda0dd vs #580f41
# purple #7e1e9c vs #9467bd
# purple #800080 vs #9467bd
# red #d62728 vs #ff0000
# red #e50000 vs #ff0000
# salmon #fa8072 vs #ff796c
# sienna #a0522d vs #a9561e
# silver #c0c0c0 vs #c5c9c7
# tan #d2b48c vs #d1b26f
# teal #008080 vs #029386
# tomato #ff6347 vs #ef4026
# turquoise #40e0d0 vs #06c2ac
# violet #ee82ee vs #9a0eea
# wheat #f5deb3 vs #fbdd7e
# yellow #ffff14 vs #ffff00
# yellowgreen #9acd32 vs #bbf90f
####################################################################################################
BASE_COLORS = {
'black': '#000000',
'blue': '#0000ff',
'green': '#00ff00',
'cyan': '#00ffff',
'red': '#ff0000',
'magenta': '#ff00ff',
'yellow': '#ffff00',
'white': '#ffffff',
}
####################################################################################################
# These colors are from Tableau
TABLEAU_COLORS = {
'blue': '#1f77b4',
'brown': '#8c564b',
'cyan': '#17becf',
'gray': '#7f7f7f',
'green': '#2ca02c',
'olive': '#bcbd22',
'orange': '#ff7f0e',
'pink': '#e377c2',
'purple': '#9467bd',
'red': '#d62728',
}
####################################################################################################
# This mapping of color names -> hex values is taken from
# a survey run by Randel Monroe see:
# http://blog.xkcd.com/2010/05/03/color-survey-results/
# for more details. The results are hosted at
# https://xkcd.com/color/rgb.txt
#
# License: http://creativecommons.org/publicdomain/zero/1.0/
XKCD_COLORS = {
"robin's egg blue": '#98eff9',
"robin's egg": '#6dedfd',
'acid green': '#8ffe09',
'adobe': '#bd6c48',
'algae green': '#21c36f',
'algae': '#54ac68',
'almost black': '#070d0d',
'amber': '#feb308',
'amethyst': '#9b5fc0',
'apple green': '#76cd26',
'apple': '#6ecb3c',
'apricot': '#ffb16d',
'aqua blue': '#02d8e9',
'aqua green': '#12e193',
'aqua marine': '#2ee8bb',
'aqua': '#13eac9',
'aquamarine': '#04d8b2',
'army green': '#4b5d16',
'asparagus': '#77ab56',
'aubergine': '#3d0734',
'auburn': '#9a3001',
'avocado green': '#87a922',
'avocado': '#90b134',
'azul': '#1d5dec',
'azure': '#069af3',
'baby blue': '#a2cffe',
'baby green': '#8cff9e',
'baby pink': '#ffb7ce',
'baby poo': '#ab9004',
'baby poop green': '#8f9805',
'baby poop': '#937c00',
'baby puke green': '#b6c406',
'baby purple': '#ca9bf7',
'baby shit brown': '#ad900d',
'baby shit green': '#889717',
'banana yellow': '#fafe4b',
'banana': '#ffff7e',
'barbie pink': '#fe46a5',
'barf green': '#94ac02',
'barney purple': '#a00498',
'barney': '#ac1db8',
'battleship grey': '#6b7c85',
'beige': '#e6daa6',
'berry': '#990f4b',
'bile': '#b5c306',
'black': '#000000',
'bland': '#afa88b',
'blood orange': '#fe4b03',
'blood red': '#980002',
'blood': '#770001',
'blue blue': '#2242c7',
'blue green': '#137e6d',
'blue grey': '#607c8e',
'blue purple': '#5729ce',
'blue violet': '#5d06e9',
'blue with a hint of purple': '#533cc6',
'blue': '#0343df',
'blue/green': '#0f9b8e',
'blue/grey': '#758da3',
'blue/purple': '#5a06ef',
'blueberry': '#464196',
'bluegreen': '#017a79',
'bluegrey': '#85a3b2',
'bluey green': '#2bb179',
'bluey grey': '#89a0b0',
'bluey purple': '#6241c7',
'bluish green': '#10a674',
'bluish grey': '#748b97',
'bluish purple': '#703be7',
'bluish': '#2976bb',
'blurple': '#5539cc',
'blush pink': '#fe828c',
'blush': '#f29e8e',
'booger green': '#96b403',
'booger': '#9bb53c',
'bordeaux': '#7b002c',
'boring green': '#63b365',
'bottle green': '#044a05',
'brick orange': '#c14a09',
'brick red': '#8f1402',
'brick': '#a03623',
'bright aqua': '#0bf9ea',
'bright blue': '#0165fc',
'bright cyan': '#41fdfe',
'bright green': '#01ff07',
'bright lavender': '#c760ff',
'bright light blue': '#26f7fd',
'bright light green': '#2dfe54',
'bright lilac': '#c95efb',
'bright lime green': '#65fe08',
'bright lime': '#87fd05',
'bright magenta': '#ff08e8',
'bright olive': '#9cbb04',
'bright orange': '#ff5b00',
'bright pink': '#fe01b1',
'bright purple': '#be03fd',
'bright red': '#ff000d',
'bright sea green': '#05ffa6',
'bright sky blue': '#02ccfe',
'bright teal': '#01f9c6',
'bright turquoise': '#0ffef9',
'bright violet': '#ad0afd',
'bright yellow green': '#9dff00',
'bright yellow': '#fffd01',
'british racing green': '#05480d',
'bronze': '#a87900',
'brown green': '#706c11',
'brown grey': '#8d8468',
'brown orange': '#b96902',
'brown red': '#922b05',
'brown yellow': '#b29705',
'brown': '#653700',
'brownish green': '#6a6e09',
'brownish grey': '#86775f',
'brownish orange': '#cb7723',
'brownish pink': '#c27e79',
'brownish purple': '#76424e',
'brownish red': '#9e3623',
'brownish yellow': '#c9b003',
'brownish': '#9c6d57',
'browny green': '#6f6c0a',
'browny orange': '#ca6b02',
'bruise': '#7e4071',
'bubble gum pink': '#ff69af',
'bubblegum pink': '#fe83cc',
'bubblegum': '#ff6cb5',
'buff': '#fef69e',
'burgundy': '#610023',
'burnt orange': '#c04e01',
'burnt red': '#9f2305',
'burnt siena': '#b75203',
'burnt sienna': '#b04e0f',
'burnt umber': '#a0450e',
'burnt yellow': '#d5ab09',
'burple': '#6832e3',
'butter yellow': '#fffd74',
'butter': '#ffff81',
'butterscotch': '#fdb147',
'cadet blue': '#4e7496',
'camel': '#c69f59',
'camo green': '#526525',
'camo': '#7f8f4e',
'camouflage green': '#4b6113',
'canary yellow': '#fffe40',
'canary': '#fdff63',
'candy pink': '#ff63e9',
'caramel': '#af6f09',
'carmine': '#9d0216',
'carnation pink': '#ff7fa7',
'carnation': '#fd798f',
'carolina blue': '#8ab8fe',
'celadon': '#befdb7',
'celery': '#c1fd95',
'cement': '#a5a391',
'cerise': '#de0c62',
'cerulean blue': '#056eee',
'cerulean': '#0485d1',
'charcoal grey': '#3c4142',
'charcoal': '#343837',
'chartreuse': '#c1f80a',
'cherry red': '#f7022a',
'cherry': '#cf0234',
'chestnut': '#742802',
'chocolate brown': '#411900',
'chocolate': '#3d1c02',
'cinnamon': '#ac4f06',
'claret': '#680018',
'clay brown': '#b2713d',
'clay': '#b66a50',
'clear blue': '#247afd',
'cloudy blue': '#acc2d9',
'cobalt blue': '#030aa7',
'cobalt': '#1e488f',
'cocoa': '#875f42',
'coffee': '#a6814c',
'cool blue': '#4984b8',
'cool green': '#33b864',
'cool grey': '#95a3a6',
'copper': '#b66325',
'coral pink': '#ff6163',
'coral': '#fc5a50',
'cornflower blue': '#5170d7',
'cornflower': '#6a79f7',
'cranberry': '#9e003a',
'cream': '#ffffc2',
'creme': '#ffffb6',
'crimson': '#8c000f',
'custard': '#fffd78',
'cyan': '#00ffff',
'dandelion': '#fedf08',
'dark aqua': '#05696b',
'dark aquamarine': '#017371',
'dark beige': '#ac9362',
'dark blue green': '#005249',
'dark blue grey': '#1f3b4d',
'dark blue': '#00035b',
'dark brown': '#341c02',
'dark coral': '#cf524e',
'dark cream': '#fff39a',
'dark cyan': '#0a888a',
'dark forest green': '#002d04',
'dark fuchsia': '#9d0759',
'dark gold': '#b59410',
'dark grass green': '#388004',
'dark green blue': '#1f6357',
'dark green': '#033500',
'dark grey blue': '#29465b',
'dark grey': '#363737',
'dark hot pink': '#d90166',
'dark indigo': '#1f0954',
'dark khaki': '#9b8f55',
'dark lavender': '#856798',
'dark lilac': '#9c6da5',
'dark lime green': '#7ebd01',
'dark lime': '#84b701',
'dark magenta': '#960056',
'dark maroon': '#3c0008',
'dark mauve': '#874c62',
'dark mint green': '#20c073',
'dark mint': '#48c072',
'dark mustard': '#a88905',
'dark navy blue': '#00022e',
'dark navy': '#000435',
'dark olive green': '#3c4d03',
'dark olive': '#373e02',
'dark orange': '#c65102',
'dark pastel green': '#56ae57',
'dark peach': '#de7e5d',
'dark periwinkle': '#665fd1',
'dark pink': '#cb416b',
'dark plum': '#3f012c',
'dark purple': '#35063e',
'dark red': '#840000',
'dark rose': '#b5485d',
'dark royal blue': '#02066f',
'dark sage': '#598556',
'dark salmon': '#c85a53',
'dark sand': '#a88f59',
'dark sea green': '#11875d',
'dark seafoam green': '#3eaf76',
'dark seafoam': '#1fb57a',
'dark sky blue': '#448ee4',
'dark slate blue': '#214761',
'dark tan': '#af884a',
'dark taupe': '#7f684e',
'dark teal': '#014d4e',
'dark turquoise': '#045c5a',
'dark violet': '#34013f',
'dark yellow green': '#728f02',
'dark yellow': '#d5b60a',
'dark': '#1b2431',
'darkblue': '#030764',
'darkgreen': '#054907',
'darkish blue': '#014182',
'darkish green': '#287c37',
'darkish pink': '#da467d',
'darkish purple': '#751973',
'darkish red': '#a90308',
'deep aqua': '#08787f',
'deep blue': '#040273',
'deep brown': '#410200',
'deep green': '#02590f',
'deep lavender': '#8d5eb7',
'deep lilac': '#966ebd',
'deep magenta': '#a0025c',
'deep orange': '#dc4d01',
'deep pink': '#cb0162',
'deep purple': '#36013f',
'deep red': '#9a0200',
'deep rose': '#c74767',
'deep sea blue': '#015482',
'deep sky blue': '#0d75f8',
'deep teal': '#00555a',
'deep turquoise': '#017374',
'deep violet': '#490648',
'denim blue': '#3b5b92',
'denim': '#3b638c',
'desert': '#ccad60',
'diarrhea': '#9f8303',
'dirt brown': '#836539',
'dirt': '#8a6e45',
'dirty blue': '#3f829d',
'dirty green': '#667e2c',
'dirty orange': '#c87606',
'dirty pink': '#ca7b80',
'dirty purple': '#734a65',
'dirty yellow': '#cdc50a',
'dodger blue': '#3e82fc',
'drab green': '#749551',
'drab': '#828344',
'dried blood': '#4b0101',
'duck egg blue': '#c3fbf4',
'dull blue': '#49759c',
'dull brown': '#876e4b',
'dull green': '#74a662',
'dull orange': '#d8863b',
'dull pink': '#d5869d',
'dull purple': '#84597e',
'dull red': '#bb3f3f',
'dull teal': '#5f9e8f',
'dull yellow': '#eedc5b',
'dusk blue': '#26538d',
'dusk': '#4e5481',
'dusky blue': '#475f94',
'dusky pink': '#cc7a8b',
'dusky purple': '#895b7b',
'dusky rose': '#ba6873',
'dust': '#b2996e',
'dusty blue': '#5a86ad',
'dusty green': '#76a973',
'dusty lavender': '#ac86a8',
'dusty orange': '#f0833a',
'dusty pink': '#d58a94',
'dusty purple': '#825f87',
'dusty red': '#b9484e',
'dusty rose': '#c0737a',
'dusty teal': '#4c9085',
'earth': '#a2653e',
'easter green': '#8cfd7e',
'easter purple': '#c071fe',
'ecru': '#feffca',
'egg shell': '#fffcc4',
'eggplant purple': '#430541',
'eggplant': '#380835',
'eggshell blue': '#c4fff7',
'eggshell': '#ffffd4',
'electric blue': '#0652ff',
'electric green': '#21fc0d',
'electric lime': '#a8ff04',
'electric pink': '#ff0490',
'electric purple': '#aa23ff',
'emerald green': '#028f1e',
'emerald': '#01a049',
'evergreen': '#05472a',
'faded blue': '#658cbb',
'faded green': '#7bb274',
'faded orange': '#f0944d',
'faded pink': '#de9dac',
'faded purple': '#916e99',
'faded red': '#d3494e',
'faded yellow': '#feff7f',
'fawn': '#cfaf7b',
'fern green': '#548d44',
'fern': '#63a950',
'fire engine red': '#fe0002',
'flat blue': '#3c73a8',
'flat green': '#699d4c',
'fluorescent green': '#08ff08',
'fluro green': '#0aff02',
'foam green': '#90fda9',
'forest green': '#06470c',
'forest': '#0b5509',
'forrest green': '#154406',
'french blue': '#436bad',
'fresh green': '#69d84f',
'frog green': '#58bc08',
'fuchsia': '#ed0dd9',
'gold': '#dbb40c',
'golden brown': '#b27a01',
'golden rod': '#f9bc08',
'golden yellow': '#fec615',
'golden': '#f5bf03',
'goldenrod': '#fac205',
'grape purple': '#5d1451',
'grape': '#6c3461',
'grapefruit': '#fd5956',
'grass green': '#3f9b0b',
'grass': '#5cac2d',
'grassy green': '#419c03',
'green apple': '#5edc1f',
'green blue': '#06b48b',
'green brown': '#544e03',
'green grey': '#77926f',
'green teal': '#0cb577',
'green yellow': '#c9ff27',
'green': '#15b01a',
'green/blue': '#01c08d',
'green/yellow': '#b5ce08',
'greenblue': '#23c48b',
'greenish beige': '#c9d179',
'greenish blue': '#0b8b87',
'greenish brown': '#696112',
'greenish cyan': '#2afeb7',
'greenish grey': '#96ae8d',
'greenish tan': '#bccb7a',
'greenish teal': '#32bf84',
'greenish turquoise': '#00fbb0',
'greenish yellow': '#cdfd02',
'greenish': '#40a368',
'greeny blue': '#42b395',
'greeny brown': '#696006',
'greeny grey': '#7ea07a',
'greeny yellow': '#c6f808',
'grey blue': '#6b8ba4',
'grey brown': '#7f7053',
'grey green': '#789b73',
'grey pink': '#c3909b',
'grey purple': '#826d8c',
'grey teal': '#5e9b8a',
'grey': '#929591',
'grey/blue': '#647d8e',
'grey/green': '#86a17d',
'greyblue': '#77a1b5',
'greyish blue': '#5e819d',
'greyish brown': '#7a6a4f',
'greyish green': '#82a67d',
'greyish pink': '#c88d94',
'greyish purple': '#887191',
'greyish teal': '#719f91',
'greyish': '#a8a495',
'gross green': '#a0bf16',
'gunmetal': '#536267',
'hazel': '#8e7618',
'heather': '#a484ac',
'heliotrope': '#d94ff5',
'highlighter green': '#1bfc06',
'hospital green': '#9be5aa',
'hot green': '#25ff29',
'hot magenta': '#f504c9',
'hot pink': '#ff028d',
'hot purple': '#cb00f5',
'hunter green': '#0b4008',
'ice blue': '#d7fffe',
'ice': '#d6fffa',
'icky green': '#8fae22',
'indian red': '#850e04',
'indigo blue': '#3a18b1',
'indigo': '#380282',
'iris': '#6258c4',
'irish green': '#019529',
'ivory': '#ffffcb',
'jade green': '#2baf6a',
'jade': '#1fa774',
'jungle green': '#048243',
'kelley green': '#009337',
'kelly green': '#02ab2e',
'kermit green': '#5cb200',
'key lime': '#aeff6e',
'khaki green': '#728639',
'khaki': '#aaa662',
'kiwi green': '#8ee53f',
'kiwi': '#9cef43',
'lavender blue': '#8b88f8',
'lavender pink': '#dd85d7',
'lavender': '#c79fef',
'lawn green': '#4da409',
'leaf green': '#5ca904',
'leaf': '#71aa34',
'leafy green': '#51b73b',
'leather': '#ac7434',
'lemon green': '#adf802',
'lemon lime': '#bffe28',
'lemon yellow': '#fdff38',
'lemon': '#fdff52',
'lichen': '#8fb67b',
'light aqua': '#8cffdb',
'light aquamarine': '#7bfdc7',
'light beige': '#fffeb6',
'light blue green': '#7efbb3',
'light blue grey': '#b7c9e2',
'light blue': '#95d0fc',
'light bluish green': '#76fda8',
'light bright green': '#53fe5c',
'light brown': '#ad8150',
'light burgundy': '#a8415b',
'light cyan': '#acfffc',
'light eggplant': '#894585',
'light forest green': '#4f9153',
'light gold': '#fddc5c',
'light grass green': '#9af764',
'light green blue': '#56fca2',
'light green': '#96f97b',
'light greenish blue': '#63f7b4',
'light grey blue': '#9dbcd4',
'light grey green': '#b7e1a1',
'light grey': '#d8dcd6',
'light indigo': '#6d5acf',
'light khaki': '#e6f2a2',
'light lavendar': '#efc0fe',
'light lavender': '#dfc5fe',
'light light blue': '#cafffb',
'light light green': '#c8ffb0',
'light lilac': '#edc8ff',
'light lime green': '#b9ff66',
'light lime': '#aefd6c',
'light magenta': '#fa5ff7',
'light maroon': '#a24857',
'light mauve': '#c292a1',
'light mint green': '#a6fbb2',
'light mint': '#b6ffbb',
'light moss green': '#a6c875',
'light mustard': '#f7d560',
'light navy blue': '#2e5a88',
'light navy': '#155084',
'light neon green': '#4efd54',
'light olive green': '#a4be5c',
'light olive': '#acbf69',
'light orange': '#fdaa48',
'light pastel green': '#b2fba5',
'light pea green': '#c4fe82',
'light peach': '#ffd8b1',
'light periwinkle': '#c1c6fc',
'light pink': '#ffd1df',
'light plum': '#9d5783',
'light purple': '#bf77f6',
'light red': '#ff474c',
'light rose': '#ffc5cb',
'light royal blue': '#3a2efe',
'light sage': '#bcecac',
'light salmon': '#fea993',
'light sea green': '#98f6b0',
'light seafoam green': '#a7ffb5',
'light seafoam': '#a0febf',
'light sky blue': '#c6fcff',
'light tan': '#fbeeac',
'light teal': '#90e4c1',
'light turquoise': '#7ef4cc',
'light urple': '#b36ff6',
'light violet': '#d6b4fc',
'light yellow green': '#ccfd7f',
'light yellow': '#fffe7a',
'light yellowish green': '#c2ff89',
'lightblue': '#7bc8f6',
'lighter green': '#75fd63',
'lighter purple': '#a55af4',
'lightgreen': '#76ff7b',
'lightish blue': '#3d7afd',
'lightish green': '#61e160',
'lightish purple': '#a552e6',
'lightish red': '#fe2f4a',
'lilac': '#cea2fd',
'liliac': '#c48efd',
'lime green': '#89fe05',
'lime yellow': '#d0fe1d',
'lime': '#aaff32',
'lipstick red': '#c0022f',
'lipstick': '#d5174e',
'macaroni and cheese': '#efb435',
'magenta': '#c20078',
'mahogany': '#4a0100',
'maize': '#f4d054',
'mango': '#ffa62b',
'manilla': '#fffa86',
'marigold': '#fcc006',
'marine blue': '#01386a',
'marine': '#042e60',
'maroon': '#650021',
'mauve': '#ae7181',
'medium blue': '#2c6fbb',
'medium brown': '#7f5112',
'medium green': '#39ad48',
'medium grey': '#7d7f7c',
'medium pink': '#f36196',
'medium purple': '#9e43a2',
'melon': '#ff7855',
'merlot': '#730039',
'metallic blue': '#4f738e',
'mid blue': '#276ab3',
'mid green': '#50a747',
'midnight blue': '#020035',
'midnight purple': '#280137',
'midnight': '#03012d',
'military green': '#667c3e',
'milk chocolate': '#7f4e1e',
'mint green': '#8fff9f',
'mint': '#9ffeb0',
'minty green': '#0bf77d',
'mocha': '#9d7651',
'moss green': '#658b38',
'moss': '#769958',
'mossy green': '#638b27',
'mud brown': '#60460f',
'mud green': '#606602',
'mud': '#735c12',
'muddy brown': '#886806',
'muddy green': '#657432',
'muddy yellow': '#bfac05',
'mulberry': '#920a4e',
'murky green': '#6c7a0e',
'mushroom': '#ba9e88',
'mustard brown': '#ac7e04',
'mustard green': '#a8b504',
'mustard yellow': '#d2bd0a',
'mustard': '#ceb301',
'muted blue': '#3b719f',
'muted green': '#5fa052',
'muted pink': '#d1768f',
'muted purple': '#805b87',
'nasty green': '#70b23f',
'navy blue': '#001146',
'navy green': '#35530a',
'navy': '#01153e',
'neon blue': '#04d9ff',
'neon green': '#0cff0c',
'neon pink': '#fe019a',
'neon purple': '#bc13fe',
'neon red': '#ff073a',
'neon yellow': '#cfff04',
'nice blue': '#107ab0',
'night blue': '#040348',
'ocean blue': '#03719c',
'ocean green': '#3d9973',
'ocean': '#017b92',
'ocher': '#bf9b0c',
'ochre': '#bf9005',
'ocre': '#c69c04',
'off blue': '#5684ae',
'off green': '#6ba353',
'off white': '#ffffe4',
'off yellow': '#f1f33f',
'old pink': '#c77986',
'old rose': '#c87f89',
'olive brown': '#645403',
'olive drab': '#6f7632',
'olive green': '#677a04',
'olive yellow': '#c2b709',
'olive': '#6e750e',
'orange brown': '#be6400',
'orange pink': '#ff6f52',
'orange red': '#fd411e',
'orange yellow': '#ffad01',
'orange': '#f97306',
'orangeish': '#fd8d49',
'orangered': '#fe420f',
'orangey brown': '#b16002',
'orangey red': '#fa4224',
'orangey yellow': '#fdb915',
'orangish brown': '#b25f03',
'orangish red': '#f43605',
'orangish': '#fc824a',
'orchid': '#c875c4',
'pale aqua': '#b8ffeb',
'pale blue': '#d0fefe',
'pale brown': '#b1916e',
'pale cyan': '#b7fffa',
'pale gold': '#fdde6c',
'pale green': '#c7fdb5',
'pale grey': '#fdfdfe',
'pale lavender': '#eecffe',
'pale light green': '#b1fc99',
'pale lilac': '#e4cbff',
'pale lime green': '#b1ff65',
'pale lime': '#befd73',
'pale magenta': '#d767ad',
'pale mauve': '#fed0fc',
'pale olive green': '#b1d27b',
'pale olive': '#b9cc81',
'pale orange': '#ffa756',
'pale peach': '#ffe5ad',
'pale pink': '#ffcfdc',
'pale purple': '#b790d4',
'pale red': '#d9544d',
'pale rose': '#fdc1c5',
'pale salmon': '#ffb19a',
'pale sky blue': '#bdf6fe',
'pale teal': '#82cbb2',
'pale turquoise': '#a5fbd5',
'pale violet': '#ceaefa',
'pale yellow': '#ffff84',
'pale': '#fff9d0',
'parchment': '#fefcaf',
'pastel blue': '#a2bffe',
'pastel green': '#b0ff9d',
'pastel orange': '#ff964f',
'pastel pink': '#ffbacd',
'pastel purple': '#caa0ff',
'pastel red': '#db5856',
'pastel yellow': '#fffe71',
'pea green': '#8eab12',
'pea soup green': '#94a617',
'pea soup': '#929901',
'pea': '#a4bf20',
'peach': '#ffb07c',
'peachy pink': '#ff9a8a',
'peacock blue': '#016795',
'pear': '#cbf85f',
'periwinkle blue': '#8f99fb',
'periwinkle': '#8e82fe',
'perrywinkle': '#8f8ce7',
'petrol': '#005f6a',
'pig pink': '#e78ea5',
'pine green': '#0a481e',
'pine': '#2b5d34',
'pink purple': '#db4bda',
'pink red': '#f5054f',
'pink': '#ff81c0',
'pink/purple': '#ef1de7',
'pinkish brown': '#b17261',
'pinkish grey': '#c8aca9',
'pinkish orange': '#ff724c',
'pinkish purple': '#d648d7',
'pinkish red': '#f10c45',
'pinkish tan': '#d99b82',
'pinkish': '#d46a7e',
'pinky purple': '#c94cbe',
'pinky red': '#fc2647',
'pinky': '#fc86aa',
'piss yellow': '#ddd618',
'pistachio': '#c0fa8b',
'plum purple': '#4e0550',
'plum': '#580f41',
'poison green': '#40fd14',
'poo brown': '#885f01',
'poo': '#8f7303',
'poop brown': '#7a5901',
'poop green': '#6f7c00',
'poop': '#7f5e00',
'powder blue': '#b1d1fc',
'powder pink': '#ffb2d0',
'primary blue': '#0804f9',
'prussian blue': '#004577',
'puce': '#a57e52',
'puke brown': '#947706',
'puke green': '#9aae07',
'puke yellow': '#c2be0e',
'puke': '#a5a502',
'pumpkin orange': '#fb7d07',
'pumpkin': '#e17701',
'pure blue': '#0203e2',
'purple blue': '#632de9',
'purple brown': '#673a3f',
'purple grey': '#866f85',
'purple pink': '#e03fd8',
'purple red': '#990147',
'purple': '#7e1e9c',
'purple/blue': '#5d21d0',
'purple/pink': '#d725de',
'purpleish blue': '#6140ef',
'purpleish pink': '#df4ec8',
'purpleish': '#98568d',
'purpley blue': '#5f34e7',
'purpley grey': '#947e94',
'purpley pink': '#c83cb9',
'purpley': '#8756e4',
'purplish blue': '#601ef9',
'purplish brown': '#6b4247',
'purplish grey': '#7a687f',
'purplish pink': '#ce5dae',
'purplish red': '#b0054b',
'purplish': '#94568c',
'purply blue': '#661aee',
'purply pink': '#f075e6',
'purply': '#983fb2',
'putty': '#beae8a',
'racing green': '#014600',
'radioactive green': '#2cfa1f',
'raspberry': '#b00149',
'raw sienna': '#9a6200',
'raw umber': '#a75e09',
'really light blue': '#d4ffff',
'red brown': '#8b2e16',
'red orange': '#fd3c06',
'red pink': '#fa2a55',
'red purple': '#820747',
'red violet': '#9e0168',
'red wine': '#8c0034',
'red': '#e50000',
'reddish brown': '#7f2b0a',
'reddish grey': '#997570',
'reddish orange': '#f8481c',
'reddish pink': '#fe2c54',
'reddish purple': '#910951',
'reddish': '#c44240',
'reddy brown': '#6e1005',
'rich blue': '#021bf9',
'rich purple': '#720058',
'robin egg blue': '#8af1fe',
'rosa': '#fe86a4',
'rose pink': '#f7879a',
'rose red': '#be013c',
'rose': '#cf6275',
'rosy pink': '#f6688e',
'rouge': '#ab1239',
'royal blue': '#0504aa',
'royal purple': '#4b006e',
'royal': '#0c1793',
'ruby': '#ca0147',
'russet': '#a13905',
'rust brown': '#8b3103',
'rust orange': '#c45508',
'rust red': '#aa2704',
'rust': '#a83c09',
'rusty orange': '#cd5909',
'rusty red': '#af2f0d',
'saffron': '#feb209',
'sage green': '#88b378',
'sage': '#87ae73',
'salmon pink': '#fe7b7c',
'salmon': '#ff796c',
'sand brown': '#cba560',
'sand yellow': '#fce166',
'sand': '#e2ca76',
'sandstone': '#c9ae74',
'sandy brown': '#c4a661',
'sandy yellow': '#fdee73',
'sandy': '#f1da7a',
'sap green': '#5c8b15',
'sapphire': '#2138ab',
'scarlet': '#be0119',
'sea blue': '#047495',
'sea green': '#53fca1',
'sea': '#3c9992',
'seafoam blue': '#78d1b6',
'seafoam green': '#7af9ab',
'seafoam': '#80f9ad',
'seaweed green': '#35ad6b',
'seaweed': '#18d17b',
'sepia': '#985e2b',
'shamrock green': '#02c14d',
'shamrock': '#01b44c',
'shit brown': '#7b5804',
'shit green': '#758000',
'shit': '#7f5f00',
'shocking pink': '#fe02a2',
'sick green': '#9db92c',
'sickly green': '#94b21c',
'sickly yellow': '#d0e429',
'sienna': '#a9561e',
'silver': '#c5c9c7',
'sky blue': '#75bbfd',
'sky': '#82cafc',
'slate blue': '#5b7c99',
'slate green': '#658d6d',
'slate grey': '#59656d',
'slate': '#516572',
'slime green': '#99cc04',
'snot green': '#9dc100',
'snot': '#acbb0d',
'soft blue': '#6488ea',
'soft green': '#6fc276',
'soft pink': '#fdb0c0',
'soft purple': '#a66fb5',
'spearmint': '#1ef876',
'spring green': '#a9f971',
'spruce': '#0a5f38',
'squash': '#f2ab15',
'steel blue': '#5a7d9a',
'steel grey': '#6f828a',
'steel': '#738595',
'stone': '#ada587',
'stormy blue': '#507b9c',
'straw': '#fcf679',
'strawberry': '#fb2943',
'strong blue': '#0c06f7',
'strong pink': '#ff0789',
'sun yellow': '#ffdf22',
'sunflower yellow': '#ffda03',
'sunflower': '#ffc512',
'sunny yellow': '#fff917',
'sunshine yellow': '#fffd37',
'swamp green': '#748500',
'swamp': '#698339',
'tan brown': '#ab7e4c',
'tan green': '#a9be70',
'tan': '#d1b26f',
'tangerine': '#ff9408',
'taupe': '#b9a281',
'tea green': '#bdf8a3',
'tea': '#65ab7c',
'teal blue': '#01889f',
'teal green': '#25a36f',
'teal': '#029386',
'tealish green': '#0cdc73',
'tealish': '#24bca8',
'terra cotta': '#c9643b',
'terracota': '#cb6843',
'terracotta': '#ca6641',
'tiffany blue': '#7bf2da',
'tomato red': '#ec2d01',
'tomato': '#ef4026',
'topaz': '#13bbaf',
'toupe': '#c7ac7d',
'toxic green': '#61de2a',
'tree green': '#2a7e19',
'true blue': '#010fcc',
'true green': '#089404',
'turquoise blue': '#06b1c4',
'turquoise green': '#04f489',
'turquoise': '#06c2ac',
'turtle green': '#75b84f',
'twilight blue': '#0a437a',
'twilight': '#4e518b',
'ugly blue': '#31668a',
'ugly brown': '#7d7103',
'ugly green': '#7a9703',
'ugly pink': '#cd7584',
'ugly purple': '#a442a0',
'ugly yellow': '#d0c101',
'ultramarine blue': '#1805db',
'ultramarine': '#2000b1',
'umber': '#b26400',
'velvet': '#750851',
'vermillion': '#f4320c',
'very dark blue': '#000133',
'very dark brown': '#1d0200',
'very dark green': '#062e03',
'very dark purple': '#2a0134',
'very light blue': '#d5ffff',
'very light brown': '#d3b683',
'very light green': '#d1ffbd',
'very light pink': '#fff4f2',
'very light purple': '#f6cefc',
'very pale blue': '#d6fffe',
'very pale green': '#cffdbc',
'vibrant blue': '#0339f8',
'vibrant green': '#0add08',
'vibrant purple': '#ad03de',
'violet blue': '#510ac9',
'violet pink': '#fb5ffc',
'violet red': '#a50055',
'violet': '#9a0eea',
'viridian': '#1e9167',
'vivid blue': '#152eff',
'vivid green': '#2fef10',
'vivid purple': '#9900fa',
'vomit green': '#89a203',
'vomit yellow': '#c7c10c',
'vomit': '#a2a415',
'warm blue': '#4b57db',
'warm brown': '#964e02',
'warm grey': '#978a84',
'warm pink': '#fb5581',
'warm purple': '#952e8f',
'washed out green': '#bcf5a6',
'water blue': '#0e87cc',
'watermelon': '#fd4659',
'weird green': '#3ae57f',
'wheat': '#fbdd7e',
'white': '#ffffff',
'windows blue': '#3778bf',
'wine red': '#7b0323',
'wine': '#80013f',
'wintergreen': '#20f986',
'wisteria': '#a87dc2',
'yellow brown': '#b79400',
'yellow green': '#c0fb2d',
'yellow ochre': '#cb9d06',
'yellow orange': '#fcb001',
'yellow tan': '#ffe36e',
'yellow': '#ffff14',
'yellow/green': '#c8fd3d',
'yellowgreen': '#bbf90f',
'yellowish brown': '#9b7a01',
'yellowish green': '#b0dd16',
'yellowish orange': '#ffab0f',
'yellowish tan': '#fcfc81',
'yellowish': '#faee66',
'yellowy brown': '#ae8b0c',
'yellowy green': '#bff128',
}
####################################################################################################
# https://drafts.csswg.org/css-color-4/#named-colors
CSS4_COLORS = {
'aliceblue': '#F0F8FF',
'antiquewhite': '#FAEBD7',
'aqua': '#00FFFF',
'aquamarine': '#7FFFD4',
'azure': '#F0FFFF',
'beige': '#F5F5DC',
'bisque': '#FFE4C4',
'black': '#000000',
'blanchedalmond': '#FFEBCD',
'blue': '#0000FF',
'blueviolet': '#8A2BE2',
'brown': '#A52A2A',
'burlywood': '#DEB887',
'cadetblue': '#5F9EA0',
'chartreuse': '#7FFF00',
'chocolate': '#D2691E',
'coral': '#FF7F50',
'cornflowerblue': '#6495ED',
'cornsilk': '#FFF8DC',
'crimson': '#DC143C',
'cyan': '#00FFFF',
'darkblue': '#00008B',
'darkcyan': '#008B8B',
'darkgoldenrod': '#B8860B',
'darkgray': '#A9A9A9',
'darkgreen': '#006400',
'darkgrey': '#A9A9A9',
'darkkhaki': '#BDB76B',
'darkmagenta': '#8B008B',
'darkolivegreen': '#556B2F',
'darkorange': '#FF8C00',
'darkorchid': '#9932CC',
'darkred': '#8B0000',
'darksalmon': '#E9967A',
'darkseagreen': '#8FBC8F',
'darkslateblue': '#483D8B',
'darkslategray': '#2F4F4F',
'darkslategrey': '#2F4F4F',
'darkturquoise': '#00CED1',
'darkviolet': '#9400D3',
'deeppink': '#FF1493',
'deepskyblue': '#00BFFF',
'dimgray': '#696969',
'dimgrey': '#696969',
'dodgerblue': '#1E90FF',
'firebrick': '#B22222',
'floralwhite': '#FFFAF0',
'forestgreen': '#228B22',
'fuchsia': '#FF00FF',
'gainsboro': '#DCDCDC',
'ghostwhite': '#F8F8FF',
'gold': '#FFD700',
'goldenrod': '#DAA520',
'gray': '#808080',
'green': '#008000',
'greenyellow': '#ADFF2F',
'grey': '#808080',
'honeydew': '#F0FFF0',
'hotpink': '#FF69B4',
'indianred': '#CD5C5C',
'indigo': '#4B0082',
'ivory': '#FFFFF0',
'khaki': '#F0E68C',
'lavender': '#E6E6FA',
'lavenderblush': '#FFF0F5',
'lawngreen': '#7CFC00',
'lemonchiffon': '#FFFACD',
'lightblue': '#ADD8E6',
'lightcoral': '#F08080',
'lightcyan': '#E0FFFF',
'lightgoldenrodyellow': '#FAFAD2',
'lightgray': '#D3D3D3',
'lightgreen': '#90EE90',
'lightgrey': '#D3D3D3',
'lightpink': '#FFB6C1',
'lightsalmon': '#FFA07A',
'lightseagreen': '#20B2AA',
'lightskyblue': '#87CEFA',
'lightslategray': '#778899',
'lightslategrey': '#778899',
'lightsteelblue': '#B0C4DE',
'lightyellow': '#FFFFE0',
'lime': '#00FF00',
'limegreen': '#32CD32',
'linen': '#FAF0E6',
'magenta': '#FF00FF',
'maroon': '#800000',
'mediumaquamarine': '#66CDAA',
'mediumblue': '#0000CD',
'mediumorchid': '#BA55D3',
'mediumpurple': '#9370DB',
'mediumseagreen': '#3CB371',
'mediumslateblue': '#7B68EE',
'mediumspringgreen': '#00FA9A',
'mediumturquoise': '#48D1CC',
'mediumvioletred': '#C71585',
'midnightblue': '#191970',
'mintcream': '#F5FFFA',
'mistyrose': '#FFE4E1',
'moccasin': '#FFE4B5',
'navajowhite': '#FFDEAD',
'navy': '#000080',
'oldlace': '#FDF5E6',
'olive': '#808000',
'olivedrab': '#6B8E23',
'orange': '#FFA500',
'orangered': '#FF4500',
'orchid': '#DA70D6',
'palegoldenrod': '#EEE8AA',
'palegreen': '#98FB98',
'paleturquoise': '#AFEEEE',
'palevioletred': '#DB7093',
'papayawhip': '#FFEFD5',
'peachpuff': '#FFDAB9',
'peru': '#CD853F',
'pink': '#FFC0CB',
'plum': '#DDA0DD',
'powderblue': '#B0E0E6',
'purple': '#800080',
'rebeccapurple': '#663399',
'red': '#FF0000',
'rosybrown': '#BC8F8F',
'royalblue': '#4169E1',
'saddlebrown': '#8B4513',
'salmon': '#FA8072',
'sandybrown': '#F4A460',
'seagreen': '#2E8B57',
'seashell': '#FFF5EE',
'sienna': '#A0522D',
'silver': '#C0C0C0',
'skyblue': '#87CEEB',
'slateblue': '#6A5ACD',
'slategray': '#708090',
'slategrey': '#708090',
'snow': '#FFFAFA',
'springgreen': '#00FF7F',
'steelblue': '#4682B4',
'tan': '#D2B48C',
'teal': '#008080',
'thistle': '#D8BFD8',
'tomato': '#FF6347',
'turquoise': '#40E0D0',
'violet': '#EE82EE',
'wheat': '#F5DEB3',
'white': '#FFFFFF',
'whitesmoke': '#F5F5F5',
'yellow': '#FFFF00',
'yellowgreen': '#9ACD32',
}
####################################################################################################
# Theses colors are from Qt5 QML http://doc.qt.io/qt-5/qml-color.html
QML_COLORS = {
'aliceblue': '#f0f8ff',
'antiquewhite': '#faebd7',
'aqua': '#00ffff',
'aquamarine': '#7fffd4',
'azure': '#f0ffff',
'beige': '#f5f5dc',
'bisque': '#ffe4c4',
'black': '#000000',
'blanchedalmond': '#ffebcd',
'blue': '#0000ff',
'blueviolet': '#8a2be2',
'brown': '#a52a2a',
'burlywood': '#deb887',
'cadetblue': '#5f9ea0',
'chartreuse': '#7fff00',
'chocolate': '#d2691e',
'coral': '#ff7f50',
'cornflowerblue': '#6495ed',
'cornsilk': '#fff8dc',
'crimson': '#dc143c',
'cyan': '#00ffff',
'darkblue': '#00008b',
'darkcyan': '#008b8b',
'darkgoldenrod': '#b8860b',
'darkgray': '#a9a9a9',
'darkgreen': '#006400',
'darkgrey': '#a9a9a9',
'darkkhaki': '#bdb76b',
'darkmagenta': '#8b008b',
'darkolivegreen': '#556b2f',
'darkorange': '#ff8c00',
'darkorchid': '#9932cc',
'darkred': '#8b0000',
'darksalmon': '#e9967a',
'darkseagreen': '#8fbc8f',
'darkslateblue': '#483d8b',
'darkslategray': '#2f4f4f',
'darkslategrey': '#2f4f4f',
'darkturquoise': '#00ced1',
'darkviolet': '#9400d3',
'deeppink': '#ff1493',
'deepskyblue': '#00bfff',
'dimgray': '#696969',
'dimgrey': '#696969',
'dodgerblue': '#1e90ff',
'firebrick': '#b22222',
'floralwhite': '#fffaf0',
'forestgreen': '#228b22',
'fuchsia': '#ff00ff',
'gainsboro': '#dcdcdc',
'ghostwhite': '#f8f8ff',
'gold': '#ffd700',
'goldenrod': '#daa520',
'gray': '#808080',
'green': '#008000',
'greenyellow': '#adff2f',
'grey': '#808080',
'honeydew': '#f0fff0',
'hotpink': '#ff69b4',
'indianred': '#cd5c5c',
'indigo': '#4b0082',
'ivory': '#fffff0',
'khaki': '#f0e68c',
'lavender': '#e6e6fa',
'lavenderblush': '#fff0f5',
'lawngreen': '#7cfc00',
'lemonchiffon': '#fffacd',
'lightblue': '#add8e6',
'lightcoral': '#f08080',
'lightcyan': '#e0ffff',
'lightgoldenrodyellow': '#fafad2',
'lightgray': '#d3d3d3',
'lightgreen': '#90ee90',
'lightgrey': '#d3d3d3',
'lightpink': '#ffb6c1',
'lightsalmon': '#ffa07a',
'lightseagreen': '#20b2aa',
'lightskyblue': '#87cefa',
'lightslategray': '#778899',
'lightslategrey': '#778899',
'lightsteelblue': '#b0c4de',
'lightyellow': '#ffffe0',
'lime': '#00ff00',
'limegreen': '#32cd32',
'linen': '#faf0e6',
'magenta': '#ff00ff',
'maroon': '#800000',
'mediumaquamarine': '#66cdaa',
'mediumblue': '#0000cd',
'mediumorchid': '#ba55d3',
'mediumpurple': '#9370db',
'mediumseagreen': '#3cb371',
'mediumslateblue': '#7b68ee',
'mediumspringgreen': '#00fa9a',
'mediumturquoise': '#48d1cc',
'mediumvioletred': '#c71585',
'midnightblue': '#191970',
'mintcream': '#f5fffa',
'mistyrose': '#ffe4e1',
'moccasin': '#ffe4b5',
'navajowhite': '#ffdead',
'navy': '#000080',
'oldlace': '#fdf5e6',
'olive': '#808000',
'olivedrab': '#6b8e23',
'orange': '#ffa500',
'orangered': '#ff4500',
'orchid': '#da70d6',
'palegoldenrod': '#eee8aa',
'palegreen': '#98fb98',
'paleturquoise': '#afeeee',
'palevioletred': '#db7093',
'papayawhip': '#ffefd5',
'peachpuff': '#ffdab9',
'peru': '#cd853f',
'pink': '#ffc0cb',
'plum': '#dda0dd',
'powderblue': '#b0e0e6',
'purple': '#800080',
'red': '#ff0000',
'rosybrown': '#bc8f8f',
'royalblue': '#4169e1',
'saddlebrown': '#8b4513',
'salmon': '#fa8072',
'sandybrown': '#f4a460',
'seagreen': '#2e8b57',
'seashell': '#fff5ee',
'sienna': '#a0522d',
'silver': '#c0c0c0',
'skyblue': '#87ceeb',
'slateblue': '#6a5acd',
'slategray': '#708090',
'slategrey': '#708090',
'snow': '#fffafa',
'springgreen': '#00ff7f',
'steelblue': '#4682b4',
'tan': '#d2b48c',
'teal': '#008080',
'thistle': '#d8bfd8',
'tomato': '#ff6347',
'turquoise': '#40e0d0',
'violet': '#ee82ee',
'wheat': '#f5deb3',
'white': '#ffffff',
'whitesmoke': '#f5f5f5',
'yellow': '#ffff00',
'yellowgreen': '#9acd32',
}
####################################################################################################
VALENTINA_COLORS = (
'black',
'blue',
'cornflowerblue',
'darkBlue',
'darkGreen',
'darkRed',
'darkviolet',
'deeppink',
'deepskyblue',
'goldenrod',
'green',
'lightsalmon',
'lime',
'mediumseagreen',
'orange',
'violet',
'yellow',
)
VALENTINA_COLORS = {name:QML_COLORS[name.lower()] for name in VALENTINA_COLORS}
Patro-master-Patro/Patro/GraphicStyle/__init__.py 0000664 0000000 0000000 00000005410 13424433274 0022321 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to define graphic styles like colours and stroke styles.
This module import :class:`Color.Colors`.
"""
####################################################################################################
__all__ = ['Colors', 'StrokeStyle', 'CapStyle', 'JoinStyle']
####################################################################################################
from enum import Enum, auto
#: Colour Database Singleton as an instance of :class:`ColorDataBase`
from .Color import Colors
####################################################################################################
class StrokeStyle(Enum):
"""Enum class to define stroke styles"""
NoPen = auto() # Inivisble ?
SolidLine = auto()
DashLine = auto()
DotLine = auto()
DashDotLine = auto()
DashDotDotLine = auto()
# Custom
####################################################################################################
class CapStyle(Enum):
"""Enum class to define cap styles"""
#: a square line end that does not cover the end point of the line
FlatCap = auto()
#: a square line end that covers the end point and extends beyond it by half the line width
SquareCap = auto()
#: a rounded line end.
RoundCap = auto()
####################################################################################################
class JoinStyle(Enum):
"""Enum class to define join styles"""
#! The outer edges of the lines are extended to meet at an angle, and this area is filled.
MiterJoin = auto()
#: The triangular notch between the two lines is filled.
BevelJoin = auto()
#: A circular arc between the two lines is filled.
RoundJoin = auto()
#: A miter join corresponding to the definition of a miter join in the SVG 1.2 Tiny specification.
SvgMiterJoin = auto()
Patro-master-Patro/Patro/Measurement/ 0000775 0000000 0000000 00000000000 13424433274 0020077 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Measurement/Measurement.py 0000664 0000000 0000000 00000014757 13424433274 0022754 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import logging
import sympy
import yaml
from .PersonalData import PersonalData
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class Measurement:
"""Class to define a measurement"""
##############################################
def __init__(self, measurements, name, value, full_name='', description=''):
name = str(name)
for c in name:
if not c.isalnum() and c != '_':
raise ValueError('Invalid measurement name "{}"'.format(name))
self._measurements = measurements
self._name = name
self._full_name = str(full_name) # for human
self._description = str(description) # describe the purpose of the measurement
self._expression = sympy.sympify(value)
self._evaluated_expression = None
self._value = None
##############################################
@property
def name(self):
return self._name
@property
def full_name(self):
return self._full_name
@property
def description(self):
return self._description
@property
def expression(self):
return self._expression
##############################################
@property
def evaluated_expression(self):
if self._evaluated_expression is None:
# variable order doesn't matter, sympy do the job
self._evaluated_expression = self._expression.subs(self._measurements._expressions)
return self._evaluated_expression
@property
def value(self):
if self._value is None:
self._value = float(self.evaluated_expression.evalf(3)) # ensure a float or raise
return self._value
def __float__(self):
return self.value
####################################################################################################
class Measurements: # Fixme: -> MeasurementSet as well as file
"""Class to store a set of measurements"""
__measurement_cls__ = Measurement
_logger = _module_logger.getChild('Measurements')
##############################################
def __init__(self):
self._unit = None
self._pattern_making_system = None # Fixme: purpose ???
self._personal = PersonalData()
self._measurements = {} # name -> Measurement
self._expressions = {} # name -> expression for sympy substitution
##############################################
@property
def personal(self):
return self._personal
##############################################
@property
def unit(self):
return self._unit
@unit.setter
def unit(self, value):
self._unit = value
##############################################
@property
def pattern_making_system(self):
return self._pattern_making_system
@pattern_making_system.setter
def pattern_making_system(self, value):
self._pattern_making_system = value
##############################################
def __iter__(self):
return iter(self._measurements.values())
##############################################
def sorted_iter(self):
for name in sorted(self._measurements.keys()):
yield self._measurements[name]
##############################################
def __getitem__(self, name):
return self._measurements[name]
##############################################
def add(self, *args, **kgwars):
# Fixme: name ?
measurement = self.__measurement_cls__(self, *args, **kgwars)
self._measurements[measurement.name] = measurement
self._expressions[measurement.name] = measurement.expression
return measurement
##############################################
def dump(self):
template = '''{0.name} = {0.expression}
= {0.evaluated_expression}
= {0.value}
'''
for measurement in self.sorted_iter():
print(template.format(measurement))
##############################################
def save_as_yaml(self, yaml_path):
measurements = {}
for measurement in self.sorted_iter():
# for measurement in self:
expression = measurement.expression
if isinstance(expression, sympy.Integer):
expression = int(expression)
elif isinstance(expression, sympy.Float):
expression = float(expression)
else:
expression = str(expression)
data = [expression]
if measurement.full_name or measurement.description:
data += [measurement.full_name, measurement.description]
measurements[measurement.name] = data
with open(yaml_path, 'w') as fh:
yaml_string = yaml.dump(measurements, default_flow_style=False, width=160)
fh.write(yaml_string)
##############################################
def load_yaml(self, yaml_path):
with open(yaml_path, 'r') as fh:
measurements = yaml.load(fh.read())
for name, data in measurements.items():
if len(data) > 1:
value, full_name, description = data
else:
value = data[0]
full_name = description = ''
self.add(name, value, full_name, description)
Patro-master-Patro/Patro/Measurement/PersonalData.py 0000664 0000000 0000000 00000010276 13424433274 0023034 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
from enum import Enum, auto
from Patro.Common.Datetime import ensure_date, ensure_datetime
####################################################################################################
class Gender(Enum):
UNKNOWN = auto() # information is not available
FEMALE = auto()
MALE = auto()
# Gender can be mixed or modified, body malformation or injury
PARTICULAR = auto() # Fixme: use case to be defined ...
####################################################################################################
class PersonalData:
"""Class to define personal data like name and gender."""
##############################################
def __init__(self, **kwargs):
self._first_name = None
self._last_name = None
self._birth_date = None # to get age
self._measurement_date = None # when the measurement was done
self._gender = None
self._email = None # contact
self._comment = None # any useful information to adapt garment to the person
for key, default in (
('first_name', ''), # Fixme: str_or_none ?
('last_name', ''),
('birth_date', None),
('measurement_date', None),
('gender', Gender.UNKNOWN),
('email', ''),
('comment', ''),
):
setattr(self, key, kwargs.get(key, default))
##############################################
@property
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
self._first_name = str(value)
##############################################
@property
def last_name(self):
return self._last_name
@last_name.setter
def last_name(self, value):
self._last_name = str(value)
##############################################
@property
def birth_date(self):
return self._birth_date
@birth_date.setter
def birth_date(self, value):
if value is not None:
self._birth_date = ensure_date(value)
else:
self._birth_date = None
##############################################
@property
def measurement_date(self):
return self._measurement_date
@measurement_date.setter
def measurement_date(self, value):
if value is not None:
self._measurement_date = ensure_datetime(value)
else:
self._measurement_date = None
##############################################
@property
def gender(self):
return self._gender
@gender.setter
def gender(self, value):
if isinstance(value, str):
gender = Gender[value.upper()]
else:
gender = Gender(value)
self._gender = gender
##############################################
@property
def email(self):
return self._email
@email.setter
def email(self, value):
self._email = str(value)
##############################################
@property
def comment(self):
return self._comment
@comment.setter
def comment(self, value):
self._comment = str(value)
Patro-master-Patro/Patro/Measurement/StandardMeasurement.py 0000664 0000000 0000000 00000006741 13424433274 0024427 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
from pathlib import Path
import yaml
####################################################################################################
class Measurement:
##############################################
def __init__(self, name, full_name, description, default_value=0):
self._name = name
self._full_name = full_name
self._description = description
self._default_value = default_value
##############################################
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
##############################################
@property
def full_name(self):
return self._full_name
@full_name.setter
def full_name(self, value):
self._full_name = value
##############################################
@property
def description(self):
return self._description
@description.setter
def description(self, value):
self._description = value
##############################################
@property
def default_value(self):
return self._default_value
@default_value.setter
def default_value(self, value):
self._default_value = value
####################################################################################################
class StandardMeasurement:
##############################################
def __init__(self):
self._names = {}
##############################################
def __len__(self):
return len(self._names)
##############################################
def __iter__(self):
return iter(self._names.values())
##############################################
def __getitem__(self, name):
return self._names[name]
##############################################
def __contains__(self, name):
return name in self._names
##############################################
def add(self, measurement):
if measurement.name not in self._names:
self._names[measurement.name] = measurement
else:
raise NameError('{} is already registered'.format(measurement.name))
##############################################
def dump(self):
template = '''{0.name}
- {0.full_name}
- {0.description}
- {0.default_value}
'''
for measurement in self:
print(template.format(measurement))
Patro-master-Patro/Patro/Measurement/ValentinaMeasurement.py 0000664 0000000 0000000 00000006561 13424433274 0024610 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import logging
from .Measurement import Measurement, Measurements
from .ValentinaStandardMeasurement import ValentinaStandardMeasurement
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
_valentina_standard_measurement = ValentinaStandardMeasurement()
####################################################################################################
class ValentinaMeasurement(Measurement):
"""Class to define a Valentina measurement"""
# CUSTOM_PREFIX = '__custom__'
CUSTOM_PREFIX = 'C_'
##############################################
@classmethod
def replace_custom_prefix(cls, string):
# Fixme: how to only replace when there is no clash ?
return string.replace('@', cls.CUSTOM_PREFIX)
##############################################
def __init__(self, measurements, name, value, full_name='', description=''):
# Valentina defines custom measurement with a @ prefix
self._valentina_name = str(name)
name = self.replace_custom_prefix(self._valentina_name)
# if self.is_custom():
# name = name[1:]
# if name in _valentina_standard_measurement:
# name = self.CUSTOM_PREFIX + name
value = self.replace_custom_prefix(value)
super().__init__(measurements, name, value, full_name, description)
##############################################
@property
def valentina_name(self):
return self._valentina_name
##############################################
def is_custom(self):
return self._valentina_name.startswith('@')
####################################################################################################
class ValentinaMeasurements(Measurements):
"""Class to store a set of Valentina measurements"""
__measurement_cls__ = ValentinaMeasurement
_logger = _module_logger.getChild('ValentinaMeasurements')
##############################################
def add(self, *args, **kgwars):
# Fixme: name ?
measurement = super().add(*args, **kgwars)
if measurement.is_custom():
self._measurements[measurement.valentina_name] = measurement
Patro-master-Patro/Patro/Measurement/ValentinaStandardMeasurement.py 0000664 0000000 0000000 00000004527 13424433274 0026271 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
from pathlib import Path
import yaml
from .StandardMeasurement import Measurement, StandardMeasurement
####################################################################################################
class ValentinaMeasurement(Measurement):
##############################################
def __init__(self, code, name, full_name, description, default_value):
super().__init__(name, full_name, description, default_value)
self._code = code
##############################################
@property
def code(self):
return self._code
@code.setter
def code(self, value):
self._code = value
####################################################################################################
class ValentinaStandardMeasurement(StandardMeasurement):
##############################################
def __init__(self):
super().__init__()
yaml_path = Path(__file__).parent.joinpath('data', 'valentina-standard-measurements.yaml')
with open(yaml_path, 'r') as fh:
data = yaml.load(fh.read())
for topic in data.values():
for code, measurement_data in topic['measurements'].items():
measurement = ValentinaMeasurement(code, *measurement_data)
self.add(measurement)
Patro-master-Patro/Patro/Measurement/__init__.py 0000664 0000000 0000000 00000000000 13424433274 0022176 0 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Measurement/data/ 0000775 0000000 0000000 00000000000 13424433274 0021010 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Measurement/data/valentina-standard-measurements.yaml 0000664 0000000 0000000 00000113551 13424433274 0030167 0 ustar 00root root 0000000 0000000 A:
description: Direct Height
measurements:
A01:
- height
- 'Height: Total'
- Vertical distance from crown of head to floor.
- 0
A02:
- height_neck_back
- 'Height: Neck Back'
- Vertical distance from the Neck Back (cervicale vertebra) to the floor.
- 0
A03:
- height_scapula
- 'Height: Scapula'
- Vertical distance from the Scapula (Blade point) to the floor.
- 0
A04:
- height_armpit
- 'Height: Armpit'
- Vertical distance from the Armpit to the floor.
- 0
A05:
- height_waist_side
- 'Height: Waist Side'
- Vertical distance from the Waist Side to the floor.
- 0
A06:
- height_hip
- 'Height: Hip'
- Vertical distance from the Hip level to the floor.
- 0
A07:
- height_gluteal_fold
- 'Height: Gluteal Fold'
- Vertical distance from the Gluteal fold, where the Gluteal muscle meets the top of the back thigh, to the floor.
- 0
A08:
- height_knee
- 'Height: Knee'
- Vertical distance from the fold at the back of the Knee to the floor.
- 0
A09:
- height_calf
- 'Height: Calf'
- Vertical distance from the widest point of the calf to the floor.
- 0
A10:
- height_ankle_high
- 'Height: Ankle High'
- Vertical distance from the deepest indentation of the back of the ankle to the floor.
- 0
A11:
- height_ankle
- 'Height: Ankle'
- Vertical distance from point where the front leg meets the foot to the floor.
- 0
A12:
- height_highhip
- 'Height: Highhip'
- Vertical distance from the Highhip level, where front abdomen is most prominent, to the floor.
- 0
A13:
- height_waist_front
- 'Height: Waist Front'
- Vertical distance from the Waist Front to the floor.
- 0
A14:
- height_bustpoint
- 'Height: Bustpoint'
- Vertical distance from Bustpoint to the floor.
- 0
A15:
- height_shoulder_tip
- 'Height: Shoulder Tip'
- Vertical distance from the Shoulder Tip to the floor.
- 0
A16:
- height_neck_front
- 'Height: Neck Front'
- Vertical distance from the Neck Front to the floor.
- 0
A17:
- height_neck_side
- 'Height: Neck Side'
- Vertical distance from the Neck Side to the floor.
- 0
A18:
- height_neck_back_to_knee
- 'Height: Neck Back to Knee'
- Vertical distance from the Neck Back (cervicale vertebra) to the fold at the back of the knee.
- height_neck_back - height_knee
A19:
- height_waist_side_to_knee
- 'Height: Waist Side to Knee'
- Vertical distance from the Waist Side to the fold at the back of the knee.
- height_waist_side - height_knee
A20:
- height_waist_side_to_hip
- 'Height: Waist Side to Hip'
- Vertical distance from the Waist Side to the Hip level.
- height_waist_side - height_hip
A21:
- height_knee_to_ankle
- 'Height: Knee to Ankle'
- Vertical distance from the fold at the back of the knee to the point where the front leg meets the top of the foot.
- height_knee - height_ankle
A22:
- height_neck_back_to_waist_side
- 'Height: Neck Back to Waist Side'
- 'Vertical distance from Neck Back to Waist Side. (''Height: Neck Back'' - ''Height: Waist Side'').'
- height_neck_back - height_waist_side
A23:
- height_waist_back
- 'Height: Waist Back'
- 'Vertical height from Waist Back to floor. (''Height: Waist Front'''' - ''Leg: Crotch to floor'''').'
- height_waist_front - leg_crotch_to_floor
B:
description: Direct Width
measurements:
B01:
- width_shoulder
- 'Width: Shoulder'
- Horizontal distance from Shoulder Tip to Shoulder Tip.
- 0
B02:
- width_bust
- 'Width: Bust'
- Horizontal distance from Bust Side to Bust Side.
- 0
B03:
- width_waist
- 'Width: Waist'
- Horizontal distance from Waist Side to Waist Side.
- 0
B04:
- width_hip
- 'Width: Hip'
- Horizontal distance from Hip Side to Hip Side.
- 0
B05:
- width_abdomen_to_hip
- 'Width: Abdomen to Hip'
- Horizontal distance from the greatest abdomen prominence to the greatest hip prominence.
- 0
C:
description: Indentation
measurements:
C01:
- indent_neck_back
- 'Indent: Neck Back'
- Horizontal distance from Scapula (Blade point) to the Neck Back.
- 0
C02:
- indent_waist_back
- 'Indent: Waist Back'
- Horizontal distance between a flat stick, placed to touch Hip and Scapula, and Waist Back.
- 0
C03:
- indent_ankle_high
- 'Indent: Ankle High'
- Horizontal Distance between a flat stick, placed perpendicular to Heel, and the greatest indentation of Ankle.
- 0
D:
description: Circumference and Arc
measurements:
D01:
- hand_palm_length
- 'Hand: Palm length'
- Length from Wrist line to base of middle finger.
- 0
D02:
- hand_length
- 'Hand: Length'
- Length from Wrist line to end of middle finger.
- 0
D03:
- hand_palm_width
- 'Hand: Palm width'
- Measure where Palm is widest.
- 0
D04:
- hand_palm_circ
- 'Hand: Palm circumference'
- Circumference where Palm is widest.
- 0
D05:
- hand_circ
- 'Hand: Circumference'
- Tuck thumb toward smallest finger, bring fingers close together. Measure circumference around widest part of hand.
- 0
E:
description: Vertical
measurements:
E01:
- foot_width
- 'Foot: Width'
- Measure at widest part of foot.
- 0
E02:
- foot_length
- 'Foot: Length'
- Measure from back of heel to end of longest toe.
- 0
E03:
- foot_circ
- 'Foot: Circumference'
- Measure circumference around widest part of foot.
- 0
E04:
- foot_instep_circ
- 'Foot: Instep circumference'
- Measure circumference at tallest part of instep.
- 0
F:
description: Horizontal
measurements:
F01:
- head_circ
- 'Head: Circumference'
- Measure circumference at largest level of head.
- 0
F02:
- head_length
- 'Head: Length'
- Vertical distance from Head Crown to bottom of jaw.
- 0
F03:
- head_depth
- 'Head: Depth'
- Horizontal distance from front of forehead to back of head.
- 0
F04:
- head_width
- 'Head: Width'
- Horizontal distance from Head Side to Head Side, where Head is widest.
- 0
F05:
- head_crown_to_neck_back
- 'Head: Crown to Neck Back'
- 'Vertical distance from Crown to Neck Back. (''Height: Total'' - ''Height: Neck Back'').'
- height - height_neck_back
F06:
- head_chin_to_neck_back
- 'Head: Chin to Neck Back'
- 'Vertical distance from Chin to Neck Back. (''Height'' - ''Height: Neck Back'' - ''Head: Length'')'
- height - height_neck_back - head_length
G:
description: Bust
measurements:
G01:
- neck_mid_circ
- Neck circumference, midsection
- Circumference of Neck midsection, about halfway between jaw and torso.
- 0
G02:
- neck_circ
- Neck circumference
- Neck circumference at base of Neck, touching Neck Back, Neck Sides, and Neck Front.
- 0
G03:
- highbust_circ
- Highbust circumference
- Circumference at Highbust, following shortest distance between Armfolds across chest, high under armpits.
- 0
G04:
- bust_circ
- Bust circumference
- Circumference around Bust, parallel to floor.
- 0
G05:
- lowbust_circ
- Lowbust circumference
- Circumference around LowBust under the breasts, parallel to floor.
- 0
G06:
- rib_circ
- Rib circumference
- Circumference around Ribs at level of the lowest rib at the side, parallel to floor.
- 0
G07:
- waist_circ
- Waist circumference
- Circumference around Waist, following natural contours. Waists are typically higher in back.
- 0
G08:
- highhip_circ
- Highhip circumference
- Circumference around Highhip, where Abdomen protrusion is greatest, parallel to floor.
- 0
G09:
- hip_circ
- Hip circumference
- Circumference around Hip where Hip protrusion is greatest, parallel to floor.
- 0
G10:
- neck_arc_f
- Neck arc, front
- From Neck Side to Neck Side through Neck Front.
- 0
G11:
- highbust_arc_f
- Highbust arc, front
- From Highbust Side (Armpit) to HIghbust Side (Armpit) across chest.
- 0
G12:
- size
- Size
- Same as bust_arc_f.
- 0
G13:
- lowbust_arc_f
- Lowbust arc, front
- From Lowbust Side to Lowbust Side across front.
- 0
G14:
- rib_arc_f
- Rib arc, front
- From Rib Side to Rib Side, across front.
- 0
G15:
- waist_arc_f
- Waist arc, front
- From Waist Side to Waist Side across front.
- 0
G16:
- highhip_arc_f
- Highhip arc, front
- From Highhip Side to Highhip Side across front.
- 0
G17:
- hip_arc_f
- Hip arc, front
- From Hip Side to Hip Side across Front.
- 0
G18:
- neck_arc_half_f
- Neck arc, front, half
- Half of 'Neck arc, front'. ('Neck arc, front' / 2).
- neck_arc_f/2
G19:
- highbust_arc_half_f
- Highbust arc, front, half
- Half of 'Highbust arc, front'. From Highbust Front to Highbust Side. ('Highbust arc, front' / 2).
- highbust_arc_f/2
G20:
- bust_arc_half_f
- Bust arc, front, half
- Half of 'Bust arc, front'. ('Bust arc, front'/2).
- bust_arc_f/2
G21:
- lowbust_arc_half_f
- Lowbust arc, front, half
- Half of 'Lowbust arc, front'. ('Lowbust Arc, front' / 2).
- lowbust_arc_f/2
G22:
- rib_arc_half_f
- Rib arc, front, half
- Half of 'Rib arc, front'. ('Rib Arc, front' / 2).
- rib_arc_f/2
G23:
- waist_arc_half_f
- Waist arc, front, half
- Half of 'Waist arc, front'. ('Waist arc, front' / 2).
- waist_arc_f/2
G24:
- highhip_arc_half_f
- Highhip arc, front, half
- Half of 'Highhip arc, front'. ('Highhip arc, front' / 2).
- highhip_arc_f/2
G25:
- hip_arc_half_f
- Hip arc, front, half
- Half of 'Hip arc, front'. ('Hip arc, front' / 2).
- hip_arc_f/2
G26:
- neck_arc_b
- Neck arc, back
- From Neck Side to Neck Side across back. ('Neck circumference' - 'Neck arc, front').
- neck_circ - neck_arc_f
G27:
- highbust_arc_b
- Highbust arc, back
- From Highbust Side to Highbust Side across back. ('Highbust circumference' - 'Highbust arc, front').
- highbust_circ - highbust_arc_f
G28:
- bust_arc_b
- Bust arc, back
- From Bust Side to Bust Side across back. ('Bust circumference' - 'Bust arc, front').
- bust_circ - bust_arc_f
G29:
- lowbust_arc_b
- Lowbust arc, back
- From Lowbust Side to Lowbust Side across back. ('Lowbust circumference' - 'Lowbust arc, front').
- lowbust_circ - lowbust_arc_f
G30:
- rib_arc_b
- Rib arc, back
- From Rib Side to Rib side across back. ('Rib circumference' - 'Rib arc, front').
- rib_circ - rib_arc_f
G31:
- waist_arc_b
- Waist arc, back
- From Waist Side to Waist Side across back. ('Waist circumference' - 'Waist arc, front').
- waist_circ - waist_arc_f
G32:
- highhip_arc_b
- Highhip arc, back
- From Highhip Side to Highhip Side across back. ('Highhip circumference' - 'Highhip arc, front').
- highhip_circ - highhip_arc_f
G33:
- hip_arc_b
- Hip arc, back
- From Hip Side to Hip Side across back. ('Hip circumference' - 'Hip arc, front').
- hip_circ - hip_arc_f
G34:
- neck_arc_half_b
- Neck arc, back, half
- Half of 'Neck arc, back'. ('Neck arc, back' / 2).
- neck_arc_b/2
G35:
- highbust_arc_half_b
- Highbust arc, back, half
- Half of 'Highbust arc, back'. From Highbust Back to Highbust Side. ('Highbust arc, back' / 2).
- highbust_arc_b/2
G36:
- bust_arc_half_b
- Bust arc, back, half
- Half of 'Bust arc, back'. ('Bust arc, back' / 2).
- bust_arc_b/2
G37:
- lowbust_arc_half_b
- Lowbust arc, back, half
- Half of 'Lowbust Arc, back'. ('Lowbust arc, back' / 2).
- lowbust_arc_b/2
G38:
- rib_arc_half_b
- Rib arc, back, half
- Half of 'Rib arc, back'. ('Rib arc, back' / 2).
- rib_arc_b/2
G39:
- waist_arc_half_b
- Waist arc, back, half
- Half of 'Waist arc, back'. ('Waist arc, back' / 2).
- waist_arc_b/2
G40:
- highhip_arc_half_b
- Highhip arc, back, half
- Half of 'Highhip arc, back'. From Highhip Back to Highbust Side. ('Highhip arc, back'/ 2).
- highhip_arc_b/2
G41:
- hip_arc_half_b
- Hip arc, back, half
- Half of 'Hip arc, back'. ('Hip arc, back' / 2).
- hip_arc_b/2
G42:
- hip_with_abdomen_arc_f
- Hip arc with Abdomen, front
- Curve stiff paper around front of abdomen, tape at sides. Measure from Hip Side to Hip Side overpaper across front.
- 0
G43:
- body_armfold_circ
- Body circumference at Armfold level
- Measure around arms and torso at Armfold level.
- 0
G44:
- body_bust_circ
- Body circumference at Bust level
- Measure around arms and torso at Bust level.
- 0
G45:
- body_torso_circ
- Body circumference of full torso
- Circumference around torso from mid-shoulder around crotch back up to mid-shoulder.
- 0
G46:
- hip_circ_with_abdomen
- Hip circumference, including Abdomen
- Measurement at Hip level, including the depth of the Abdomen. (Hip arc, back + Hip arc withabdomen, front).
- hip_arc_b + hip_with_abdomen_arc_f
H:
description: Balance
measurements:
H01:
- neck_front_to_waist_f
- Neck Front to Waist Front
- From Neck Front, over tape between Breastpoints, down to Waist Front.
- 0
H02:
- neck_front_to_waist_flat_f
- Neck Front to Waist Front flat
- From Neck Front down between breasts to Waist Front.
- 0
H03:
- armpit_to_waist_side
- Armpit to Waist Side
- From Armpit down to Waist Side.
- 0
H04:
- shoulder_tip_to_waist_side_f
- Shoulder Tip to Waist Side, front
- From Shoulder Tip, curving around Armscye Front, then down to Waist Side.
- 0
H05:
- neck_side_to_waist_f
- Neck Side to Waist level, front
- From Neck Side straight down front to Waist level.
- 0
H06:
- neck_side_to_waist_bustpoint_f
- Neck Side to Waist level, through Bustpoint
- From Neck Side over Bustpoint to Waist level, forming a straight line.
- 0
H07:
- neck_front_to_highbust_f
- Neck Front to Highbust Front
- Neck Front down to Highbust Front.
- 0
H08:
- highbust_to_waist_f
- Highbust Front to Waist Front
- From Highbust Front to Waist Front. Use tape to bridge gap between Bustpoints. ('Neck Front toWaist Front' - 'Neck Front to Highbust Front').
- neck_front_to_waist_f - neck_front_to_highbust_f
H09:
- neck_front_to_bust_f
- Neck Front to Bust Front
- From Neck Front down to Bust Front. Requires tape to cover gap between Bustpoints.
- 0
H10:
- bust_to_waist_f
- Bust Front to Waist Front
- From Bust Front down to Waist level. ('Neck Front to Waist Front' - 'Neck Front to Bust Front').
- neck_front_to_waist_f - neck_front_to_bust_f
H11:
- lowbust_to_waist_f
- Lowbust Front to Waist Front
- From Lowbust Front down to Waist Front.
- 0
H12:
- rib_to_waist_side
- Rib Side to Waist Side
- From lowest rib at side down to Waist Side.
- 0
H13:
- shoulder_tip_to_armfold_f
- Shoulder Tip to Armfold Front
- From Shoulder Tip around Armscye down to Armfold Front.
- 0
H14:
- neck_side_to_bust_f
- Neck Side to Bust level, front
- From Neck Side straight down front to Bust level.
- 0
H15:
- neck_side_to_highbust_f
- Neck Side to Highbust level, front
- From Neck Side straight down front to Highbust level.
- 0
H16:
- shoulder_center_to_highbust_f
- Shoulder center to Highbust level, front
- From mid-Shoulder down front to Highbust level, aimed at Bustpoint.
- 0
H17:
- shoulder_tip_to_waist_side_b
- Shoulder Tip to Waist Side, back
- From Shoulder Tip, curving around Armscye Back, then down to Waist Side.
- 0
H18:
- neck_side_to_waist_b
- Neck Side to Waist level, back
- From Neck Side straight down back to Waist level.
- 0
H19:
- neck_back_to_waist_b
- Neck Back to Waist Back
- From Neck Back down to Waist Back.
- 0
H20:
- neck_side_to_waist_scapula_b
- Neck Side to Waist level, through Scapula
- From Neck Side across Scapula down to Waist level, forming a straight line.
- 0
H21:
- neck_back_to_highbust_b
- Neck Back to Highbust Back
- From Neck Back down to Highbust Back.
- 0
H22:
- highbust_to_waist_b
- Highbust Back to Waist Back
- From Highbust Back down to Waist Back. ('Neck Back to Waist Back' - 'Neck Back to Highbust Back').
- neck_back_to_waist_b - neck_back_to_highbust_b
H23:
- neck_back_to_bust_b
- Neck Back to Bust Back
- From Neck Back down to Bust Back.
- 0
H24:
- bust_to_waist_b
- Bust Back to Waist Back
- From Bust Back down to Waist level. ('Neck Back to Waist Back' - 'Neck Back to Bust Back').
- neck_back_to_waist_b - neck_back_to_bust_b
H25:
- lowbust_to_waist_b
- Lowbust Back to Waist Back
- From Lowbust Back down to Waist Back.
- 0
H26:
- shoulder_tip_to_armfold_b
- Shoulder Tip to Armfold Back
- From Shoulder Tip around Armscye down to Armfold Back.
- 0
H27:
- neck_side_to_bust_b
- Neck Side to Bust level, back
- From Neck Side straight down back to Bust level.
- 0
H28:
- neck_side_to_highbust_b
- Neck Side to Highbust level, back
- From Neck Side straight down back to Highbust level.
- 0
H29:
- shoulder_center_to_highbust_b
- Shoulder center to Highbust level, back
- From mid-Shoulder down back to Highbust level, aimed through Scapula.
- 0
H30:
- waist_to_highhip_f
- Waist Front to Highhip Front
- From Waist Front to Highhip Front.
- 0
H31:
- waist_to_hip_f
- Waist Front to Hip Front
- From Waist Front to Hip Front.
- 0
H32:
- waist_to_highhip_side
- Waist Side to Highhip Side
- From Waist Side to Highhip Side.
- 0
H33:
- waist_to_highhip_b
- Waist Back to Highhip Back
- From Waist Back down to Highhip Back.
- 0
H34:
- waist_to_hip_b
- Waist Back to Hip Back
- From Waist Back down to Hip Back. Requires tape to cover the gap between buttocks.
- 0
H35:
- waist_to_hip_side
- Waist Side to Hip Side
- From Waist Side to Hip Side.
- 0
H36:
- shoulder_slope_neck_side_angle
- Shoulder Slope Angle from Neck Side
- Angle formed by line from Neck Side to Shoulder Tip and line from Neck Side parallel to floor.
- 0
H37:
- shoulder_slope_neck_side_length
- Shoulder Slope length from Neck Side
- Vertical distance between Neck Side and Shoulder Tip.
- 0
H38:
- shoulder_slope_neck_back_angle
- Shoulder Slope Angle from Neck Back
- Angle formed by line from Neck Back to Shoulder Tip and line from Neck Back parallel to floor.
- 0
H39:
- shoulder_slope_neck_back_height
- Shoulder Slope length from Neck Back
- Vertical distance between Neck Back and Shoulder Tip.
- 0
H40:
- shoulder_slope_shoulder_tip_angle
- Shoulder Slope Angle from Shoulder Tip
- Angle formed by line from Neck Side to Shoulder Tip and vertical line at Shoulder Tip.
- 0
H41:
- neck_back_to_across_back
- Neck Back to Across Back
- From neck back, down to level of Across Back measurement.
- 0
H42:
- across_back_to_waist_b
- Across Back to Waist back
- From middle of Across Back down to Waist back.
- neck_back_to_waist_b - neck_back_to_across_back
I:
description: Arm
measurements:
I01:
- shoulder_length
- Shoulder length
- From Neck Side to Shoulder Tip.
- 0
I02:
- shoulder_tip_to_shoulder_tip_f
- Shoulder Tip to Shoulder Tip, front
- From Shoulder Tip to Shoulder Tip, across front.
- 0
I03:
- across_chest_f
- Across Chest
- From Armscye to Armscye at narrowest width across chest.
- 0
I04:
- armfold_to_armfold_f
- Armfold to Armfold, front
- From Armfold to Armfold, shortest distance between Armfolds, not parallel to floor.
- 0
I05:
- shoulder_tip_to_shoulder_tip_half_f
- Shoulder Tip to Shoulder Tip, front, half
- Half of' Shoulder Tip to Shoulder tip, front'. ('Shoulder Tip to Shoulder Tip, front' / 2).
- shoulder_tip_to_shoulder_tip_f/2
I06:
- across_chest_half_f
- Across Chest, half
- Half of 'Across Chest'. ('Across Chest' / 2).
- across_chest_f/2
I07:
- shoulder_tip_to_shoulder_tip_b
- Shoulder Tip to Shoulder Tip, back
- From Shoulder Tip to Shoulder Tip, across the back.
- 0
I08:
- across_back_b
- Across Back
- From Armscye to Armscye at the narrowest width of the back.
- 0
I09:
- armfold_to_armfold_b
- Armfold to Armfold, back
- From Armfold to Armfold across the back.
- 0
I10:
- shoulder_tip_to_shoulder_tip_half_b
- Shoulder Tip to Shoulder Tip, back, half
- Half of 'Shoulder Tip to Shoulder Tip, back'. ('Shoulder Tip to Shoulder Tip, back' / 2).
- shoulder_tip_to_shoulder_tip_b/2
I11:
- across_back_half_b
- Across Back, half
- Half of 'Across Back'. ('Across Back' / 2).
- across_back_b/2
I12:
- neck_front_to_shoulder_tip_f
- Neck Front to Shoulder Tip
- From Neck Front to Shoulder Tip.
- 0
I13:
- neck_back_to_shoulder_tip_b
- Neck Back to Shoulder Tip
- From Neck Back to Shoulder Tip.
- 0
I14:
- neck_width
- Neck Width
- Measure between the 'legs' of an unclosed necklace or chain draped around the neck.
- 0
J:
description: Leg
measurements:
J01:
- bustpoint_to_bustpoint
- Bustpoint to Bustpoint
- From Bustpoint to Bustpoint.
- 0
J02:
- bustpoint_to_neck_side
- Bustpoint to Neck Side
- From Neck Side to Bustpoint.
- 0
J03:
- bustpoint_to_lowbust
- Bustpoint to Lowbust
- From Bustpoint down to Lowbust level, following curve of bust or chest.
- 0
J04:
- bustpoint_to_waist
- Bustpoint to Waist level
- From Bustpoint to straight down to Waist level, forming a straight line (not curving along thebody).
- 0
J05:
- bustpoint_to_bustpoint_half
- Bustpoint to Bustpoint, half
- Half of 'Bustpoint to Bustpoint'. ('Bustpoint to Bustpoint' / 2).
- bustpoint_to_bustpoint/2
J06:
- bustpoint_neck_side_to_waist
- Bustpoint, Neck Side to Waist level
- From Neck Side to Bustpoint, then straight down to Waist level. ('Neck Side to Bustpoint' +'Bustpoint to Waist level').
- bustpoint_to_neck_side + bustpoint_to_waist
J07:
- bustpoint_to_shoulder_tip
- Bustpoint to Shoulder Tip
- From Bustpoint to Shoulder tip.
- 0
J08:
- bustpoint_to_waist_front
- Bustpoint to Waist Front
- From Bustpoint to Waist Front, in a straight line, not following the curves of the body.
- 0
J09:
- bustpoint_to_bustpoint_halter
- Bustpoint to Bustpoint Halter
- From Bustpoint around Neck Back down to other Bustpoint.
- 0
J10:
- bustpoint_to_shoulder_center
- Bustpoint to Shoulder Center
- From center of Shoulder to Bustpoint.
- 0
K:
description: Crotch and Rise
measurements:
K01:
- shoulder_tip_to_waist_front
- Shoulder Tip to Waist Front
- From Shoulder Tip diagonal to Waist Front.
- 0
K02:
- neck_front_to_waist_side
- Neck Front to Waist Side
- From Neck Front diagonal to Waist Side.
- 0
K03:
- neck_side_to_waist_side_f
- Neck Side to Waist Side, front
- From Neck Side diagonal across front to Waist Side.
- 0
K04:
- shoulder_tip_to_waist_back
- Shoulder Tip to Waist Back
- From Shoulder Tip diagonal to Waist Back.
- 0
K05:
- shoulder_tip_to_waist_b_1in_offset
- Shoulder Tip to Waist Back, with 1in (2.54cm) offset
- Mark 1in (2.54cm) outward from Waist Back along Waist level. Measure from Shoulder Tip diagonal to mark.
- 0
K06:
- neck_back_to_waist_side
- Neck Back to Waist Side
- From Neck Back diagonal across back to Waist Side.
- 0
K07:
- neck_side_to_waist_side_b
- Neck Side to Waist Side, back
- From Neck Side diagonal across back to Waist Side.
- 0
K08:
- neck_side_to_armfold_f
- Neck Side to Armfold Front
- From Neck Side diagonal to Armfold Front.
- 0
K09:
- neck_side_to_armpit_f
- Neck Side to Highbust Side, front
- From Neck Side diagonal across front to Highbust Side (Armpit).
- 0
K10:
- neck_side_to_bust_side_f
- Neck Side to Bust Side, front
- Neck Side diagonal across front to Bust Side.
- 0
K11:
- neck_side_to_armfold_b
- Neck Side to Armfold Back
- From Neck Side diagonal to Armfold Back.
- 0
K12:
- neck_side_to_armpit_b
- Neck Side to Highbust Side, back
- From Neck Side diagonal across back to Highbust Side (Armpit).
- 0
K13:
- neck_side_to_bust_side_b
- Neck Side to Bust Side, back
- Neck Side diagonal across back to Bust Side.
- 0
L:
description: Hand
measurements:
L01:
- arm_shoulder_tip_to_wrist_bent
- 'Arm: Shoulder Tip to Wrist, bent'
- Bend Arm, measure from Shoulder Tip around Elbow to radial Wrist bone.
- 0
L02:
- arm_shoulder_tip_to_elbow_bent
- 'Arm: Shoulder Tip to Elbow, bent'
- Bend Arm, measure from Shoulder Tip to Elbow Tip.
- 0
L03:
- arm_elbow_to_wrist_bent
- 'Arm: Elbow to Wrist, bent'
- 'Elbow tip to wrist. (''Arm: Shoulder Tip to Wrist, bent'' - ''Arm: Shoulder Tip to Elbow, bent'').'
- arm_shoulder_tip_to_wrist_bent - arm_shoulder_tip_to_elbow_bent
L04:
- arm_elbow_circ_bent
- 'Arm: Elbow circumference, bent'
- Elbow circumference, arm is bent.
- 0
L05:
- arm_shoulder_tip_to_wrist
- 'Arm: Shoulder Tip to Wrist'
- From Shoulder Tip to Wrist bone, arm straight.
- 0
L06:
- arm_shoulder_tip_to_elbow
- 'Arm: Shoulder Tip to Elbow'
- From Shoulder tip to Elbow Tip, arm straight.
- 0
L07:
- arm_elbow_to_wrist
- 'Arm: Elbow to Wrist'
- 'From Elbow to Wrist, arm straight. (''Arm: Shoulder Tip to Wrist'' - ''Arm: Shoulder Tip to Elbow'').'
- arm_shoulder_tip_to_wrist - arm_shoulder_tip_to_elbow
L08:
- arm_armpit_to_wrist
- 'Arm: Armpit to Wrist, inside'
- From Armpit to ulna Wrist bone, arm straight.
- 0
L09:
- arm_armpit_to_elbow
- 'Arm: Armpit to Elbow, inside'
- From Armpit to inner Elbow, arm straight.
- 0
L10:
- arm_elbow_to_wrist_inside
- 'Arm: Elbow to Wrist, inside'
- 'From inside Elbow to Wrist. (''Arm: Armpit to Wrist, inside'' - ''Arm: Armpit to Elbow, inside'').'
- arm_armpit_to_wrist - arm_armpit_to_elbow
L11:
- arm_upper_circ
- 'Arm: Upper Arm circumference'
- Arm circumference at Armpit level.
- 0
L12:
- arm_above_elbow_circ
- 'Arm: Above Elbow circumference'
- Arm circumference at Bicep level.
- 0
L13:
- arm_elbow_circ
- 'Arm: Elbow circumference'
- Elbow circumference, arm straight.
- 0
L14:
- arm_lower_circ
- 'Arm: Lower Arm circumference'
- Arm circumference where lower arm is widest.
- 0
L15:
- arm_wrist_circ
- 'Arm: Wrist circumference'
- Wrist circumference.
- 0
L16:
- arm_shoulder_tip_to_armfold_line
- 'Arm: Shoulder Tip to Armfold line'
- From Shoulder Tip down to Armpit level.
- 0
L17:
- arm_neck_side_to_wrist
- 'Arm: Neck Side to Wrist'
- 'From Neck Side to Wrist. (''Shoulder Length'' + ''Arm: Shoulder Tip to Wrist'').'
- shoulder_length + arm_shoulder_tip_to_wrist
L18:
- arm_neck_side_to_finger_tip
- 'Arm: Neck Side to Finger Tip'
- 'From Neck Side down arm to tip of middle finger. (''Shoulder Length'' + ''Arm: Shoulder Tip toWrist'' + ''Hand: Length'').'
- shoulder_length + arm_shoulder_tip_to_wrist + hand_length
L19:
- armscye_circ
- 'Armscye: Circumference'
- Let arm hang at side. Measure Armscye circumference through Shoulder Tip and Armpit.
- 0
L20:
- armscye_length
- 'Armscye: Length'
- Vertical distance from Shoulder Tip to Armpit.
- 0
L21:
- armscye_width
- 'Armscye: Width'
- Horizontal distance between Armscye Front and Armscye Back.
- 0
L22:
- arm_neck_side_to_outer_elbow
- 'Arm: Neck side to Elbow'
- 'From Neck Side over Shoulder Tip down to Elbow. (Shoulder length + Arm: Shoulder Tip to Elbow).'
- shoulder_length + arm_shoulder_tip_to_elbow
M:
description: Foot
measurements:
M01:
- leg_crotch_to_floor
- 'Leg: Crotch to floor'
- Stand feet close together. Measure from crotch level (touching body, no extra space) down to floor.
- 0
M02:
- leg_waist_side_to_floor
- 'Leg: Waist Side to floor'
- From Waist Side along curve to Hip level then straight down to floor.
- 0
M03:
- leg_thigh_upper_circ
- 'Leg: Thigh Upper circumference'
- Thigh circumference at the fullest part of the upper Thigh near the Crotch.
- 0
M04:
- leg_thigh_mid_circ
- 'Leg: Thigh Middle circumference'
- Thigh circumference about halfway between Crotch and Knee.
- 0
M05:
- leg_knee_circ
- 'Leg: Knee circumference'
- Knee circumference with straight leg.
- 0
M06:
- leg_knee_small_circ
- 'Leg: Knee Small circumference'
- Leg circumference just below the knee.
- 0
M07:
- leg_calf_circ
- 'Leg: Calf circumference'
- Calf circumference at the largest part of lower leg.
- 0
M08:
- leg_ankle_high_circ
- 'Leg: Ankle High circumference'
- Ankle circumference where the indentation at the back of the ankle is the deepest.
- 0
M09:
- leg_ankle_circ
- 'Leg: Ankle circumference'
- Ankle circumference where front of leg meets the top of the foot.
- 0
M10:
- leg_knee_circ_bent
- 'Leg: Knee circumference, bent'
- Knee circumference with leg bent.
- 0
M11:
- leg_ankle_diag_circ
- 'Leg: Ankle diagonal circumference'
- Ankle circumference diagonal from top of foot to bottom of heel.
- 0
M12:
- leg_crotch_to_ankle
- 'Leg: Crotch to Ankle'
- 'From Crotch to Ankle. (''Leg: Crotch to Floor'' - ''Height: Ankle'').'
- leg_crotch_to_floor - height_ankle
M13:
- leg_waist_side_to_ankle
- 'Leg: Waist Side to Ankle'
- 'From Waist Side to Ankle. (''Leg: Waist Side to Floor'' - ''Height: Ankle'').'
- leg_waist_side_to_floor - height_ankle
M14:
- leg_waist_side_to_knee
- 'Leg: Waist Side to Knee'
- 'From Waist Side along curve to Hip level then straight down to Knee level. (''Leg: Waist Side toFloor'' - ''Height Knee'').'
- leg_waist_side_to_floor - height_knee
N:
description: Head
measurements:
N01:
- crotch_length
- Crotch length
- Put tape across gap between buttocks at Hip level. Measure from Waist Front down betwen legs and up to Waist Back.
- 0
N02:
- crotch_length_b
- Crotch length, back
- Put tape across gap between buttocks at Hip level. Measure from Waist Back to mid-Crotch, either at the vagina or between testicles and anus).
- 0
N03:
- crotch_length_f
- Crotch length, front
- From Waist Front to start of vagina or end of testicles. ('Crotch length' - 'Crotch length, back').
- crotch_length - crotch_length_b
N04:
- rise_length_side_sitting
- ''
- From Waist Side around hip curve down to surface, while seated on hard surface.
- 0
N05:
- rise_length_diag
- Rise length, diagonal
- Measure from Waist Side diagonally to a string tied at the top of the leg, seated on a hard surface.
- 0
N06:
- rise_length_b
- Rise length, back
- 'Vertical distance from Waist Back to Crotch level. (''Height: Waist Back'' - ''Leg: Crotch to Floor'')'
- height_waist_back - leg_crotch_to_floor
N07:
- rise_length_f
- Rise length, front
- 'Vertical Distance from Waist Front to Crotch level. (''Height: Waist Front'' - ''Leg: Crotch to Floor'')'
- height_waist_front - leg_crotch_to_floor
N08:
- rise_length_side
- Rise length, side
- 'Vertical distance from Waist side down to Crotch level. Use formula (Height: Waist side - Leg: Crotch to floor).'
- 0
O:
description: Men & Tailoring
measurements:
O01:
- neck_back_to_waist_front
- Neck Back to Waist Front
- From Neck Back around Neck Side down to Waist Front.
- 0
O02:
- waist_to_waist_halter
- Waist to Waist Halter, around Neck Back
- From Waist level around Neck Back to Waist level.
- 0
O03:
- waist_natural_circ
- Natural Waist circumference
- Torso circumference at men's natural side Abdominal Obliques indentation, if Oblique indentation isn't found then just below the Navel level.
- 0
O04:
- waist_natural_arc_f
- Natural Waist arc, front
- From Side to Side at the Natural Waist level, across the front.
- 0
O05:
- waist_natural_arc_b
- Natural Waist arc, back
- From Side to Side at Natural Waist level, across the back. Calculate as ( Natural Waist circumference - Natural Waist arc (front) ).
- waist_natural_circ - waist_natural_arc_f
O06:
- waist_to_natural_waist_f
- Waist Front to Natural Waist Front
- Length from Waist Front to Natural Waist Front.
- 0
O07:
- waist_to_natural_waist_b
- Waist Back to Natural Waist Back
- Length from Waist Back to Natural Waist Back.
- 0
O08:
- arm_neck_back_to_elbow_bent
- 'Arm: Neck Back to Elbow, high bend'
- Bend Arm with Elbow out, hand in front. Measure from Neck Back to Elbow Tip.
- 0
O09:
- arm_neck_back_to_wrist_bent
- 'Arm: Neck Back to Wrist, high bend'
- Bend Arm with Elbow out, hand in front. Measure from Neck Back to Elbow Tip to Wrist bone.
- 0
O10:
- arm_neck_side_to_elbow_bent
- 'Arm: Neck Side to Elbow, high bend'
- Bend Arm with Elbow out, hand in front. Measure from Neck Side to Elbow Tip.
- 0
O11:
- arm_neck_side_to_wrist_bent
- 'Arm: Neck Side to Wrist, high bend'
- Bend Arm with Elbow out, hand in front. Measure from Neck Side to Elbow Tip to Wrist bone.
- 0
O12:
- arm_across_back_center_to_elbow_bent
- 'Arm: Across Back Center to Elbow, high bend'
- Bend Arm with Elbow out, hand in front. Measure from Middle of Back to Elbow Tip.
- 0
O13:
- arm_across_back_center_to_wrist_bent
- 'Arm: Across Back Center to Wrist, high bend'
- Bend Arm with Elbow out, hand in front. Measure from Middle of Back to Elbow Tip to Wrist bone.
- 0
O14:
- arm_armscye_back_center_to_wrist_bent
- 'Arm: Armscye Back Center to Wrist, high bend'
- Bend Arm with Elbow out, hand in front. Measure from Armscye Back to Elbow Tip.
- 0
P:
description: Historical & Specialty
measurements:
P01:
- neck_back_to_bust_front
- Neck Back to Bust Front
- From Neck Back, over Shoulder, to Bust Front.
- 0
P02:
- neck_back_to_armfold_front
- Neck Back to Armfold Front
- From Neck Back over Shoulder to Armfold Front.
- 0
P03:
- neck_back_to_armfold_front_to_waist_side
- Neck Back, over Shoulder, to Waist Side
- From Neck Back, over Shoulder, down chest to Waist Side.
- 0
P04:
- highbust_back_over_shoulder_to_armfold_front
- Highbust Back, over Shoulder, to Armfold Front
- From Highbust Back over Shoulder to Armfold Front.
- 0
P05:
- highbust_back_over_shoulder_to_waist_front
- Highbust Back, over Shoulder, to Waist Front
- From Highbust Back, over Shoulder touching Neck Side, to Waist Front.
- 0
P06:
- neck_back_to_armfold_front_to_neck_back
- Neck Back, to Armfold Front, to Neck Back
- From Neck Back, over Shoulder to Armfold Front, under arm and return to start.
- 0
P07:
- across_back_center_to_armfold_front_to_across_back_center
- Across Back Center, circled around Shoulder
- From center of Across Back, over Shoulder, under Arm, and return to start.
- 0
P08:
- neck_back_to_armfold_front_to_highbust_back
- Neck Back, to Armfold Front, to Highbust Back
- From Neck Back over Shoulder to Armfold Front, under arm to Highbust Back.
- 0
P09:
- armfold_to_armfold_bust
- Armfold to Armfold, front, curved through Bust Front
- Measure in a curve from Armfold Left Front through Bust Front curved back up to Armfold Right Front.
- 0
P10:
- armfold_to_bust_front
- Armfold to Bust Front
- Measure from Armfold Front to Bust Front, shortest distance between the two, as straight as possible.
- 0
P11:
- highbust_b_over_shoulder_to_highbust_f
- Highbust Back, over Shoulder, to Highbust level
- From Highbust Back, over Shoulder, then aim at Bustpoint, stopping measurement at Highbust level.
- 0
P12:
- armscye_arc
- 'Armscye: Arc'
- From Armscye at Across Chest over ShoulderTip to Armscye at Across Back.
- 0
Q:
description: Patternmaking measurements
measurements:
Q01:
- dart_width_shoulder
- 'Dart Width: Shoulder'
- This information is pulled from pattern charts in some patternmaking systems, e.g. Winifred P.Aldrich's "Metric Pattern Cutting".
- 0
Q02:
- dart_width_bust
- 'Dart Width: Bust'
- This information is pulled from pattern charts in some patternmaking systems, e.g. Winifred P.Aldrich's "Metric Pattern Cutting".
- 0
Q03:
- dart_width_waist
- 'Dart Width: Waist'
- This information is pulled from pattern charts in some patternmaking systems, e.g. Winifred P.Aldrich's "Metric Pattern Cutting".
- 0
Patro-master-Patro/Patro/Pattern/ 0000775 0000000 0000000 00000000000 13424433274 0017227 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/Pattern/Calculator.py 0000664 0000000 0000000 00000027567 13424433274 0021713 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
####################################################################################################
import ast
import logging
import astunparse
# import astor
from Patro.Common.Graph.DirectedAcyclicGraph import DirectedAcyclicGraph
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class NodeVisitor(ast.NodeVisitor):
"""Class to implement a AST node visitor to register dependencies."""
##############################################
def __init__(self, calculator):
super(NodeVisitor).__init__()
self._calculator = calculator
self._dependencies = []
##############################################
@property
def dependencies(self):
return self._dependencies
##############################################
# def visit(self, node):
# print(node)
# super(NodeVisitor, self).visit(node)
##############################################
def visit_Call(self, node):
# See Green Tree Snakes - the missing Python AST docs
# https://greentreesnakes.readthedocs.io/en/latest/index.html
function = node.func
if isinstance(function, ast.Attribute) and function.value.id == '__calculator__':
dag = self._calculator.dag
if function.attr == '_function_Line': # catched _function_Line.__calculator__
for arg in node.args:
dependence = self._calculator._name_to_point(arg.s)
self._dependencies.append(dependence)
self.generic_visit(node)
####################################################################################################
class Calculator:
"""Class to implement a calculator for expressions"""
_logger = _module_logger.getChild('Calculator')
##############################################
def __init__(self, measurements):
self._measurements = measurements
self._dag = DirectedAcyclicGraph()
self._cache = {'__calculator__': self} # used to eval expressions
self._points = {}
self._current_operation = None # Fixme: ???
self._current_segment = None
if measurements is not None:
for measurement in measurements:
self._cache[measurement.name] = float(measurement)
##############################################
@property
def measurements(self):
return self._measurements
@property
def dag(self):
return self._dag
@property
def cache(self):
return self._cache
##############################################
def set_current_segment(self, vector):
self._current_segment = vector
def unset_current_segment(self):
self._current_segment = None
# self._logger.debug('Unset current segment')
##############################################
def add_point(self, point):
self._points[point.name] = point
##############################################
def _name_to_point(self, name):
return self._points[name]
##############################################
def _name_to_vector_point(self, name):
return self._points[name].vector
##############################################
def _names_to_vector_points(self, *names):
return [self._name_to_vector_point(name) for name in names]
##############################################
#
# Define special functions
#
# See in Valentina source
# libs/ifc/ifcdef.cpp
# Fixme: special functions
# increments ?
# Length of curve: Spl_A_B
# Angle of line: AngleLine_A_B
# radius of arcs
# Angles of curves: Angle1Spl_A_B Angle2Spl_A_B
# Length of control points: C1LengthSpl_A_B C2LengthSpl_A_B
# functions
def _function_Angle1Spl(self, point_name1, point_name2):
point1, point2 = self._names_to_vector_points(point_name1, point_name2)
return 0 # Fixme:
def _function_Angle2Spl(self, point_name1, point_name2):
point1, point2 = self._names_to_vector_points(point_name1, point_name2)
return 0 # Fixme:
def _function_AngleLine(self, point_name1, point_name2):
point1, point2 = self._names_to_vector_points(point_name1, point_name2)
return (point2 - point1).orientation
def _function_CurrentLength(self):
return self._current_segment.magnitude
def _function_C1LengthSpl(self, point_name1, point_name2):
point1, point2 = self._names_to_vector_points(point_name1, point_name2)
return 0 # Fixme:
def _function_C2LengthSpl(self, point_name1, point_name2):
point1, point2 = self._names_to_vector_points(point_name1, point_name2)
return 0 # Fixme:
def _function_Line(self, point_name1, point_name2):
point1, point2 = self._names_to_vector_points(point_name1, point_name2)
return (point2 - point1).magnitude
def _function_Spl(self, point_name1, point_name2):
point1, point2 = self._names_to_vector_points(point_name1, point_name2)
return 0 # Fixme:
####################################################################################################
class Expression:
"""Class to define an expression."""
_logger = _module_logger.getChild('Expression')
##############################################
def __init__(self, expression, calculator=None):
self._expression = str(expression)
self._calculator = calculator
self._ast = None
self._dependencies = None
self._code = None
self._value = None
self._value_error = False
##############################################
@property
def expression(self):
return self._expression
@property
def dependencies(self):
return self._dependencies
##############################################
def __str__(self):
return self._expression
##############################################
def is_float(self):
try:
float(self._expression)
return True
except ValueError:
return False
##############################################
def _find_identifier(self, prefix, start=0):
"""Find an identifier in a Python expression"""
expression = self._expression
start = expression.find(prefix, start)
if start is -1:
return None, None
index = start + 1
while index < len(expression):
c = expression[index]
if 'a' <= c <= 'z' or 'A' <= c <= 'Z' or '0' <= c <= '9' or c in '_':
index += 1
else:
break
return expression[start:index], index + 1
##############################################
def _compile(self):
expression = self._expression
self._logger.debug("expression '{}'".format(expression))
# Python don't accept identifier starting with @
# Replace @foo by __custom__foo
# https://docs.python.org/3.5/reference/lexical_analysis.html#identifiers
if '@' in expression:
custom_measurements = []
start = 0
while True:
name, start = self._find_identifier('@', start)
if name is None:
break
else:
custom_measurements.append(name)
for measurement in custom_measurements:
py_name = self._calculator.measurements[measurement].name
expression = self.expression.replace(measurement, py_name)
# Replace special function like Line_A1_A2 to __calculator__._function_Line('A1', 'A2')
functions = []
for function in (
'Angle1Spl_',
'Angle2Spl_',
'AngleLine_',
'CurrentLength',
'C1LengthSpl_',
'C2LengthSpl_',
'Line_',
'Spl_',
):
start = 0
while True:
name, start = self._find_identifier(function, start)
if name is None:
break
else:
functions.append(name)
# self._logger.debug('Functions ' + str(functions))
for function_call in functions:
parts = function_call.split('_')
function = parts[0]
args = parts[1:]
pythonised_function = '__calculator__._function_' + function + '(' + ', '.join(["'{}'".format(x) for x in args]) + ')'
# self._logger.debug('Function {} {} -> {}'.format(function, args, pythonised_function))
expression = expression.replace(function_call, pythonised_function)
self._logger.debug("Pythonised expression '{}'".format(expression))
# Fixme: What is the (supported) grammar ?
# http://beltoforion.de/article.php?a=muparser
# http://beltoforion.de/article.php?a=muparserx
# Get the AST of the expression
self._ast = ast.parse(expression, mode='eval')
# Get the dependencies
node_visitor = NodeVisitor(self._calculator)
node_visitor.generic_visit(self._ast)
self._dependencies = node_visitor.dependencies
# Compile to Python bytecode
self._code = compile(self._ast, '', mode='eval')
# print('AST', astunparse.dump(self._ast))
# print('AST -> Python', astunparse.unparse(self._ast))
## print('AST -> Python', astor.to_source(self._ast.body))
##############################################
def eval(self):
if self._code is None:
self._compile()
try:
self._value = eval(self._code, self._calculator.cache)
self._value_error = False
except NameError:
self._value = None
self._value_error = True
# except AttributeError as e:
# self._logger.warning(e)
# self._value = None
self._logger.debug('Eval {} = {}'.format(self._expression, self._value))
##############################################
def set_dirty(self):
self._value = None
self._value_error = False
##############################################
@property
def value(self):
"""Eval on the fly and return the value"""
if self._value is None and self._value_error is False:
self.eval()
return self._value
####################################################################################################
class NamedExpression(Expression):
"""Class for Expression with a name"""
##############################################
def __init__(self, name, expression, calculator=None):
Expression.__init__(self, expression, calculator)
self._name = name
##############################################
@property
def name(self):
return self._name
Patro-master-Patro/Patro/Pattern/Pattern.py 0000664 0000000 0000000 00000007014 13424433274 0021220 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""
"""
####################################################################################################
import logging
from Patro.Common.Object import ObjectNameMixin
from .Sketch import Sketch
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class Pattern:
"""Class to implement the root of a pattern"""
_logger = _module_logger.getChild('Pattern')
##############################################
def __init__(self, measurements, unit):
self._measurements = measurements
self._unit = unit
self._scopes = [] # not a dict so as to don't manage renaming
##############################################
@property
def measurements(self):
return self._measurements
@property
def unit(self):
return self._unit
##############################################
@property
def scopes(self):
return iter(self._scopes)
##############################################
def scope_names(self):
return [scope.name for scope in self._scopes]
##############################################
def add_scope(self, name):
scope = PatternScope(self, name)
self._scopes.append(scope)
return scope
##############################################
def scope(self, id):
# Fixem: try ? for slice
if isinstance(id, int):
return self._scopes[id]
else:
for scope in self._scopes:
if scope.name == id:
return scope
return None
##############################################
def remove_scope(self, name):
scope = self.scope(name)
if scope is not None:
self._scopes.remove(scope)
####################################################################################################
class PatternScope(ObjectNameMixin):
"""Class to implement a pattern scope"""
_logger = _module_logger.getChild('Pattern')
##############################################
def __init__(self, pattern, name):
super().__init__(name)
self._pattern = pattern
self._sketch = Sketch(self)
##############################################
@property
def measurements(self):
return self._pattern.measurements
@property
def unit(self):
return self._pattern._unit
@property
def sketch(self):
return self._sketch
Patro-master-Patro/Patro/Pattern/Sketch.py 0000664 0000000 0000000 00000020756 13424433274 0021034 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""
"""
####################################################################################################
import logging
from Patro.GeometryEngine.Vector import Vector2D
from Patro.GraphicEngine.GraphicScene.GraphicStyle import GraphicPathStyle, Font
from Patro.GraphicEngine.GraphicScene.Scene import GraphicScene
from . import SketchOperation
from .Calculator import Calculator
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class Sketch:
# Fixme:
# do we want to have several sketches
# apply a transformation
# share points
_logger = _module_logger.getChild('Sketch')
##############################################
def __init__(self, pattern):
self._pattern = pattern
self._calculator = Calculator(self.measurements)
self._operations = []
self._operation_dict = {}
##############################################
@property
def pattern(self):
return self._pattern
@property
def measurements(self):
return self._pattern.measurements
@property
def calculator(self):
return self._calculator
@property
def unit(self):
return self._pattern.unit
##############################################
@property
def operations(self):
return self._operations
##############################################
def _add_operation(self, operation):
# Works as a post init
self._operations.append(operation)
# Fixme: operation id, only for valentina ?
self._operation_dict[operation.id] = operation
if hasattr(operation, 'name'):
self._operation_dict[operation.name] = operation
##############################################
def has_operation_id(self, id):
return id in self._operation_dict
##############################################
def get_operation(self, id):
return self._operation_dict[id]
##############################################
def eval(self):
self._logger.info('Eval all operations')
for operation in self._operations:
if isinstance(operation, SketchOperation.Point):
self._calculator.add_point(operation)
operation.eval()
elif isinstance(operation, SketchOperation.SimpleInteractiveSpline):
operation.eval() # for control points
else:
pass
operation.connect_ancestor_for_expressions()
##############################################
def dump(self):
print("\nDump operations:")
for operation in self._operations:
if isinstance(operation, SketchOperation.Point):
print(operation, operation.vector)
else:
print(operation)
for dependency in operation.dependencies:
print(' ->', dependency)
##############################################
@property
def bounding_box(self):
"""Compute the bounding box of the pattern."""
# Fixme: to function
# cache ???
bounding_box = None
for operation in self._operations:
interval = operation.geometry().bounding_box
if bounding_box is None:
bounding_box = interval
else:
bounding_box |= interval
return bounding_box
##############################################
def _operation_to_path_style(self, operation, **kwargs):
"""Generate a :class:`GraphicPathStyle` instance for a operation"""
return GraphicPathStyle(
stroke_style=operation.line_style,
stroke_color=operation.line_color,
**kwargs
)
##############################################
def detail_scene(self, scene_cls=GraphicScene):
"""Generate a graphic scene for the detail mode
Scene class can be customised using the *scene_cls* parameter.
"""
scene = scene_cls()
# Fixme: scene bounding box
scene.bounding_box = self.bounding_box
# Fixme: implement a transformer class to prevent if ... ?
font = Font('', 16)
for operation in self._operations:
if isinstance(operation, SketchOperation.Point):
# Register coordinate
scene.add_coordinate(operation.name, operation.vector)
# Draw point and label
scene.circle(operation.name, '1pt',
GraphicPathStyle(fill_color='black'),
user_data=operation,
)
label_offset = operation.label_offset
offset = Vector2D(label_offset.x, -label_offset.y) # Fixme: ???
label_position = operation.vector + offset
if offset:
# arrow must point to the label center and be clipped
scene.segment(operation.vector, label_position,
GraphicPathStyle(line_width='.5pt'),
user_data=operation,
)
scene.text(label_position, operation.name, font, user_data=operation)
if isinstance(operation, SketchOperation.LinePropertiesMixin):
path_style = self._operation_to_path_style(operation, line_width='2pt')
if isinstance(operation, SketchOperation.AlongLinePoint):
scene.segment(operation.first_point.name, operation.name,
path_style,
user_data=operation,
)
elif isinstance(operation, SketchOperation.EndLinePoint):
scene.segment(operation.base_point.name, operation.name,
path_style,
user_data=operation,
)
# elif isinstance(operation, LineIntersectPoint):
# scene.segment(operation.point1_line1.name, operation.name, path_style)
# source += r'\draw[{0}] ({1.point1_line1.name}) -- ({1.name});'.format(style, operation) + '\n'
elif isinstance(operation, SketchOperation.NormalPoint):
scene.segment(operation.first_point.name, operation.name,
path_style,
user_data=operation,
)
# Draw path item like segments and Bézier curves
elif isinstance(operation, SketchOperation.Line):
path_style = self._operation_to_path_style(operation, line_width='4pt')
scene.segment(operation.first_point.name, operation.second_point.name,
path_style,
user_data=operation,
)
elif isinstance(operation, SketchOperation.SimpleInteractiveSpline):
path_style = self._operation_to_path_style(operation, line_width='4pt')
scene.cubic_bezier(operation.first_point.name,
operation.control_point1, operation.control_point2,
operation.second_point.name,
path_style,
user_data=operation,
)
return scene
Patro-master-Patro/Patro/Pattern/SketchOperation.py 0000664 0000000 0000000 00000055260 13424433274 0022713 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This module defines all the sketch operations supported by the pattern engine, e.g. the
intersection between two lines.
A sketch operation must be build from the corresponding method of the Pattern class.
"""
####################################################################################################
import logging
from Patro.Common.Object import ObjectGlobalIdMixin
from Patro.GeometryEngine.Bezier import CubicBezier2D
from Patro.GeometryEngine.Line import Line2D
from Patro.GeometryEngine.Segment import Segment2D
from Patro.GeometryEngine.Vector import Vector2D
from .Calculator import Expression
# Rename id buitin
pyid = id
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
def quote(x):
return "'{}'".format(x)
####################################################################################################
### class SketchOperationMetaClass:
###
### _logger = _module_logger.getChild('SketchOperationMetaClass')
###
### ##############################################
###
### def __init__(cls, class_name, super_classes, class_attribute_dict):
### # cls._logger.debug(str((cls, class_name, super_classes, class_attribute_dict)))
### type.__init__(cls, class_name, super_classes, class_attribute_dict)
####################################################################################################
# metaclass = SketchOperationMetaClass
class SketchOperation(ObjectGlobalIdMixin):
"""Baseclass for sketch operation"""
_logger = _module_logger.getChild('SketchOperation')
##############################################
def __init__(self, sketch, id=None):
self._sketch = sketch
# Valentina set an incremental integer id for each calculation (entity)
# id is used to identify operation, see _get_operation
# A calculation which generate a point has also a name
try:
super().__init__(id)
except ValueError:
raise NameError("id {} is already attributed".format(id))
self._dag_node = self._dag.add_node(pyid(self), data=self)
self._dependencies = set()
##############################################
@property
def sketch(self):
return self._sketch
@property
def _dag(self):
return self._sketch.calculator.dag
@property
def dependencies(self):
return self._dependencies
##############################################
def _get_operation(self, operation):
"""Return the corresponding :obj:`SketchOperation` object where *operation* can be
:obj:`SketchOperation` instance, an id or a name.
"""
if isinstance(operation, SketchOperation):
return operation
# elif isinstance(operation, (int, str)):
else: # must be id or string
return self._sketch.get_operation(operation)
##############################################
def eval(self):
self._logger.debug('Eval {}'.format(self))
self.eval_internal()
##############################################
def eval_internal(self):
"""Code to evaluate the operation in subclasses, i.e. to compute internal states like points."""
pass
##############################################
def _init_args(self):
# cf. to_python
args = self.__init__.__code__.co_varnames
args = args[2:-1] # remove self, sketch, id
return args
##############################################
def to_python(self):
"""Return the Python code for the operation"""
args = self._init_args()
values = []
for arg in args:
value = getattr(self, arg)
# if arg == 'sketch':
# value_str = 'sketch'
# if isinstance(value, Sketch):
# value_str = 'sketch'
if value is None:
value_str = 'None'
elif isinstance(value, (int, float)):
value_str = str(value)
elif isinstance(value, str):
value_str = quote(value)
# elif isinstance(value, SketchOperation):
# value_str = str(value.id)
elif isinstance(value, Point):
value_str = quote(value.name)
elif isinstance(value, Expression):
if value.is_float():
value_str = str(value)
else:
value_str = quote(value)
elif isinstance(value, Vector2D):
value_str = 'Vector2D({0.x}, {0.y})'.format(value)
else:
value_str = ''
values.append(value_str)
kwargs = ', '.join(['sketch'] + [key + '=' + value for key, value in zip(args, values)])
return self.__class__.__name__ + '(' + kwargs + ')'
##############################################
def _connect_ancestor(self, *points):
"""Connect point dependencies in the DAG."""
dag = self._dag
for point in points:
self._dependencies.add(point)
self._dag_node.connect_ancestor(dag[pyid(point)])
##############################################
# def _connect_ancestor_for_expressions(self, *expressions):
# for expression in expressions:
# self._connect_ancestor(*expression.dependencies)
def _iter_on_expressions(self):
"""Lookup for :obj:`Expression` in the object and yield them."""
for attribute in self.__dict__.values():
if isinstance(attribute, Expression):
yield attribute
def connect_ancestor_for_expressions(self):
"""Connect dependencies from expressions in the DAG."""
# Expression's dependencies are only known after compilation
for expression in self._iter_on_expressions():
self._connect_ancestor(*expression.dependencies)
##############################################
# @property
def geometry(self):
"""Return the geometric object"""
raise NotImplementedError('Geometry is not implemented for {}'.format(self))
####################################################################################################
class LinePropertiesMixin:
##############################################
def __init__(self, line_style, line_color):
self._line_color = line_color
self._line_style = line_style
##############################################
@property
def line_color(self):
return self._line_color
@line_color.setter
def line_color(self, value):
self._line_color = value
@property
def line_style(self):
return self._line_style
@line_style.setter
def line_style(self, value):
self._line_style = value
####################################################################################################
class FirstSecondPointMixin:
##############################################
def __init__(self, first_point, second_point):
self._first_point = self._get_operation(first_point)
self._second_point = self._get_operation(second_point)
self._connect_ancestor(self._first_point, self._second_point)
##############################################
@property
def first_point(self):
return self._first_point
@property
def second_point(self):
return self._second_point
####################################################################################################
class BasePointMixin:
##############################################
def __init__(self, base_point):
self._base_point = self._get_operation(base_point)
self._connect_ancestor(self._base_point)
##############################################
@property
def base_point(self):
return self._base_point
####################################################################################################
class LengthMixin:
##############################################
def __init__(self, length):
self._length = Expression(length, self._sketch.calculator)
# self._connect_ancestor_for_expressions(self._length)
##############################################
@property
def length(self):
return self._length
####################################################################################################
class AngleMixin:
##############################################
def __init__(self, angle):
self._angle = Expression(angle, self._sketch.calculator)
# self._connect_ancestor_for_expressions(self._angle)
##############################################
@property
def angle(self):
return self._angle
####################################################################################################
class LengthAngleMixin(LengthMixin, AngleMixin):
##############################################
def __init__(self, length, angle):
LengthMixin.__init__(self, length)
AngleMixin.__init__(self, angle)
####################################################################################################
class Point(SketchOperation):
"""Base class for point."""
##############################################
def __init__(self, sketch, name, label_offset, id=None):
SketchOperation.__init__(self, sketch, id)
self._name = name
self._label_offset = label_offset
self._vector = None
##############################################
@property
def name(self):
return self._name
@property
def label_offset(self):
return self._label_offset
@property
def vector(self):
return self._vector
##############################################
def _post_eval_internal(self):
self._logger.debug('{0._name} {0._vector}'.format(self))
##############################################
def geometry(self):
return self._vector.clone()
####################################################################################################
class SinglePoint(Point):
"""Construct a point from coordinate"""
##############################################
def __init__(self, sketch, name,
x, y,
label_offset,
id=None
):
Point.__init__(self, sketch, name, label_offset, id)
self._x = Expression(x, sketch.calculator)
self._y = Expression(y, sketch.calculator)
# self._connect_ancestor_for_expressions(self._x, self._y)
##############################################
@property
def x(self):
return self._x
@property
def y(self):
return self._y
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._name} = ({0._x}, {0._y})'.format(self)
##############################################
def eval_internal(self):
self._vector = Vector2D(self._x.value, self._y.value)
self._post_eval_internal()
####################################################################################################
class AlongLinePoint(Point, LinePropertiesMixin, FirstSecondPointMixin, LengthMixin):
"""Construct a point from two points defining a direction and a length"""
##############################################
def __init__(self, sketch, name,
first_point, second_point, length,
label_offset,
line_style=None, line_color=None,
id=None,
):
Point.__init__(self, sketch, name, label_offset, id)
LinePropertiesMixin.__init__(self, line_style, line_color)
FirstSecondPointMixin.__init__(self, first_point, second_point)
LengthMixin.__init__(self, length)
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._name} = ({0._first_point.name}, {0._second_point.name}, {0._length})'.format(self)
##############################################
def eval_internal(self):
vector = self._second_point.vector - self._first_point.vector
self._sketch.calculator.set_current_segment(vector)
self._vector = self._first_point.vector + vector.to_normalised()*self._length.value
self._sketch.calculator.unset_current_segment()
self._post_eval_internal()
####################################################################################################
class EndLinePoint(Point, LinePropertiesMixin, BasePointMixin, LengthAngleMixin):
"""Construct a point from a base point and a vector defined by an angle and a length"""
##############################################
def __init__(self, sketch, name,
base_point, angle, length,
label_offset,
line_style=None, line_color=None,
id=None,
):
Point.__init__(self, sketch, name, label_offset, id)
LinePropertiesMixin.__init__(self, line_style, line_color)
BasePointMixin.__init__(self, base_point)
LengthAngleMixin.__init__(self, length, angle)
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._name} = ({0._base_point.name}, {0._angle}, {0._length})'.format(self)
##############################################
def eval_internal(self):
self._vector = self._base_point._vector + Vector2D.from_angle(self._angle.value)*self._length.value
self._post_eval_internal()
####################################################################################################
class LineIntersectPoint(Point):
"""Construct a point from the intersection of two segments defined by four points"""
##############################################
def __init__(self, sketch, name,
point1_line1, point2_line1, point1_line2, point2_line2,
label_offset,
id=None,
):
Point.__init__(self, sketch, name, label_offset, id)
self._point1_line1 = self._get_operation(point1_line1)
self._point2_line1 = self._get_operation(point2_line1)
self._point1_line2 = self._get_operation(point1_line2)
self._point2_line2 = self._get_operation(point2_line2)
self._connect_ancestor(self._point1_line1, self._point2_line1,
self._point1_line2, self._point2_line2)
##############################################
@property
def point1_line1(self):
return self._point1_line1
@property
def point2_line1(self):
return self._point2_line1
@property
def point1_line2(self):
return self._point1_line2
@property
def point2_line2(self):
return self._point2_line2
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._name} = ({0._point1_line1.name}, {0._point2_line1.name}, {0._point1_line2.name}, {0._point2_line2.name})'.format(self)
##############################################
def eval_internal(self):
line1 = Line2D.from_two_points(self._point1_line1.vector, self._point2_line1.vector)
line2 = Line2D.from_two_points(self._point1_line2.vector, self._point2_line2.vector)
self._vector = line1.intersection(line2)
self._post_eval_internal()
####################################################################################################
class NormalPoint(Point, LinePropertiesMixin, FirstSecondPointMixin, LengthAngleMixin):
"""Construct a point at a distance of the first point on the rotated normal of a line defined by two points"""
##############################################
def __init__(self, sketch, name,
first_point, second_point, angle, length,
label_offset,
line_style=None, line_color=None,
id=None,
):
Point.__init__(self, sketch, name, label_offset, id)
LinePropertiesMixin.__init__(self, line_style, line_color)
FirstSecondPointMixin.__init__(self, first_point, second_point)
LengthAngleMixin.__init__(self, length, angle)
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._name} = ({0._first_point.name}, {0._second_point.name}, {0._angle}, {0._length})'.format(self)
##############################################
def eval_internal(self):
vector = self._second_point.vector - self._first_point.vector
self._sketch.calculator.set_current_segment(vector)
direction = vector.to_normalised()
direction = direction.normal
angle = self._angle.value
if angle:
direction = direction.rotate(angle)
self._vector = self._first_point.vector + direction*self._length.value
self._sketch.calculator.unset_current_segment()
self._post_eval_internal()
####################################################################################################
class PointOfIntersection(Point, FirstSecondPointMixin):
"""Construct a point from the x coordinate of a fist point and the y coordinate of a second point"""
##############################################
def __init__(self, sketch, name,
first_point, second_point,
label_offset,
id=None,
):
Point.__init__(self, sketch, name, label_offset, id)
FirstSecondPointMixin.__init__(self, first_point, second_point)
##############################################
def __repr__(self):
return self.__class__.__name__ + ' {0._name} = ({0._first_point.name}, {0._second_point.name})'.format(self)
##############################################
def eval_internal(self):
self._vector = Vector2D(self._first_point.vector.x, self._second_point.vector.y)
self._post_eval_internal()
####################################################################################################
class Line(SketchOperation, LinePropertiesMixin, FirstSecondPointMixin):
"""Construct a line defined by two points"""
##############################################
def __init__(self, sketch,
first_point, second_point,
line_style='solid', line_color='black',
id=None,
):
SketchOperation.__init__(self, sketch, id)
LinePropertiesMixin.__init__(self, line_style, line_color)
FirstSecondPointMixin.__init__(self, first_point, second_point)
##############################################
def __repr__(self):
return self.__class__.__name__ + ' ({0._first_point.name}, {0._second_point.name})'.format(self)
##############################################
def eval_internal(self):
pass
##############################################
def geometry(self):
return Segment2D(self._first_point.vector, self._second_point.vector)
####################################################################################################
class SimpleInteractiveSpline(SketchOperation, LinePropertiesMixin, FirstSecondPointMixin):
""""Construct a quadratic Bezier curve from two extremity points and two control points"""
##############################################
def __init__(self, sketch,
first_point, second_point,
angle1, length1,
angle2, length2,
line_style='solid', line_color='black',
id=None,
):
SketchOperation.__init__(self, sketch, id)
LinePropertiesMixin.__init__(self, line_style, line_color)
FirstSecondPointMixin.__init__(self, first_point, second_point)
self._angle1 = Expression(angle1, sketch.calculator)
self._length1 = Expression(length1, sketch.calculator)
self._angle2 = Expression(angle2, sketch.calculator)
self._length2 = Expression(length2, sketch.calculator)
# self._connect_ancestor_for_expressions(self._angle1, self._length1, self._angle2, self._length2)
self._control_point1 = None # Fixme: not yet computed
self._control_point2 = None
##############################################
@property
def angle1(self):
return self._angle1
@property
def length1(self):
return self._length1
@property
def angle2(self):
return self._angle2
@property
def length2(self):
return self._length2
@property
def control_point1(self):
return self._control_point1
@property
def control_point2(self):
return self._control_point2
##############################################
def __repr__(self):
return self.__class__.__name__ + ' ({0._first_point.name}, {0._second_point.name}, {0._angle1}, {0._length1}, {0._angle2}, {0._length2})'.format(self)
##############################################
def eval_internal(self):
control_point1_offset = Vector2D.from_angle(self._angle1.value)*self._length1.value
control_point2_offset = Vector2D.from_angle(self._angle2.value)*self._length2.value
self._control_point1 = self.first_point.vector + control_point1_offset
self._control_point2 = self.second_point.vector + control_point2_offset
# self._logger.debug("Control points : {} {}".format(self._control_point1, self._control_point2))
##############################################
def geometry(self):
if self._control_point1 is None:
raise NameError("eval before to get geometry")
return CubicBezier2D(self._first_point.vector, self._control_point1,
self._control_point2, self._second_point.vector)
Patro-master-Patro/Patro/Pattern/__init__.py 0000664 0000000 0000000 00000004016 13424433274 0021341 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""This subpackage implements the core engine for pattern.
"""
####################################################################################################
import inspect
from . import SketchOperation
from .Sketch import Sketch
####################################################################################################
def _get_sketch_operations(module):
return [item
for item in module.__dict__.values()
if inspect.isclass(item) and issubclass(item, SketchOperation.SketchOperation)]
####################################################################################################
_sketch_operations = _get_sketch_operations(SketchOperation)
for operation_cls in _sketch_operations:
def _make_function(operation_cls):
def function(self, *args, **kwargs):
sketch_operation = operation_cls(self, *args, **kwargs)
self._add_operation(sketch_operation)
return sketch_operation
return function
function_name = operation_cls.__name__
setattr(Sketch, function_name, _make_function(operation_cls))
Patro-master-Patro/Patro/QtApplication/ 0000775 0000000 0000000 00000000000 13424433274 0020362 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/QtApplication/QmlApplication.py 0000664 0000000 0000000 00000024136 13424433274 0023657 0 ustar 00root root 0000000 0000000 ####################################################################################################
#
# 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 .
#
####################################################################################################
"""Module to implement a Qt Application.
"""
####################################################################################################
__all__ = [
'QmlApplication',
]
####################################################################################################
import argparse
import datetime
import logging
import sys
import traceback
from pathlib import Path
# Fixme:
from PyQt5 import QtCore
from QtShim.QtCore import (
Property, Signal, QObject,
Qt, QTimer, QUrl
)
from QtShim.QtGui import QGuiApplication, QIcon
from QtShim.QtQml import qmlRegisterType, QQmlApplicationEngine
# Fixme: PYSIDE-574 qmlRegisterSingletonType and qmlRegisterUncreatableType missing in QtQml
from QtShim.QtQml import qmlRegisterUncreatableType
from QtShim.QtQuick import QQuickPaintedItem, QQuickView
# from QtShim.QtQuickControls2 import QQuickStyle
from Patro.Common.Platform import QtPlatform
from Patro.Common.ArgparseAction import PathAction
from Patro.GraphicEngine.Painter.QtPainter import QtScene, QtQuickPaintedSceneItem
from .rcc import PatroRessource
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class QmlApplication(QObject):
"""Class to implement a Qt QML Application."""
_logger = _module_logger.getChild('QmlApplication')
##############################################
def __init__(self, application):
super().__init__()
self._application = application
self._scene = None
##############################################
sceneChanged = Signal()
@Property(QtScene, notify=sceneChanged)
def scene(self):
return self._scene
@scene.setter
def scene(self, scene):
if self._scene is not scene:
self._logger.info('set scene {}'.format(scene))
self._scene = scene
self.sceneChanged.emit()
####################################################################################################
class Application(QObject):
"""Class to implement a Qt Application."""
instance = None
_logger = _module_logger.getChild('Application')
##############################################
# Fixme: Singleton
@classmethod
def create(cls, *args, **kwargs):
if cls.instance is not None:
raise NameError('Instance exists')
cls.instance = cls(*args, **kwargs)
return cls.instance
##############################################
def __init__(self):
super().__init__()
QtCore.qInstallMessageHandler(self._message_handler)
self._parse_arguments()
self._appplication = QGuiApplication(sys.argv)
self._engine = QQmlApplicationEngine()
self._qml_application = QmlApplication(self)
logo_path = ':/icons/logo-256.png'
self._appplication.setWindowIcon(QIcon(logo_path))
self._platform = QtPlatform()
# self._logger.info('\n' + str(self._platform))
self._scene = None
# self._load_translation()
self._register_qml_types()
self._set_context_properties()
self._load_qml_main()
# self._run_before_event_loop()
QTimer.singleShot(0, self._post_init)
# self._view = QQuickView()
# self._view.setResizeMode(QQuickView.SizeRootObjectToView)
# self._view.setSource(qml_url)
##############################################
@property
def args(self):
return self._args
@property
def qml_application(self):
return self._qml_application
@property
def platform(self):
return self._platform
##############################################
def _print_critical_message(self, message):
# print('\nCritical Error on {}'.format(datetime.datetime.now()))
# print('-'*80)
# print(message)
self._logger.critical(message)
##############################################
def _message_handler(self, msg_type, context, msg):
if msg_type == QtCore.QtDebugMsg:
method = self._logger.debug
elif msg_type == QtCore.QtInfoMsg:
method = self._logger.info
elif msg_type == QtCore.QtWarningMsg:
method = self._logger.warning
elif msg_type in (QtCore.QtCriticalMsg, QtCore.QtFatalMsg):
method = self._logger.critical
# method = None
# local_msg = msg.toLocal8Bit()
# localMsg.constData()
context_file = context.file
if context_file is not None:
file_path = Path(context_file).name
else:
file_path = ''
message = '{1} {3} — {0}'.format(msg, file_path, context.line, context.function)
if method is not None:
method(message)
else:
self._print_critical_message(message)
##############################################
def _on_critical_exception(self, exception):
message = str(exception) + '\n' + traceback.format_exc()
self._print_critical_message(message)
sys.exit(1)
##############################################
@classmethod
def setup_gui_application(cls):
# QGuiApplication.setApplicationName(APPLICATION_NAME)
# QGuiApplication.setOrganizationName(ORGANISATION_NAME)
QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
# QQuickStyle.setStyle('Material')
##############################################
def _parse_arguments(self):
parser = argparse.ArgumentParser(
description='Patro',
)
# parser.add_argument(
# '--version',
# action='store_true', default=False,
# help="show version and exit",
# )
parser.add_argument(
'--user-script',
action=PathAction,
default=None,
help='user script to execute',
)
parser.add_argument(
'--user-script-args',
default='',
help="user script args (don't forget to quote)",
)
self._args = parser.parse_args()
##############################################
# def _load_translationt(self):
# locale = QLocale()
# if m_translator.load(locale, '...', '.', ':/translations', '.qm'):
# m_application.installTranslator(m_translator)
# else:
# raise "No translator for locale" locale.name()
##############################################
def _register_qml_types(self):
qmlRegisterUncreatableType(QmlApplication, 'Patro', 1, 0, 'QmlApplication', 'Cannot create QmlApplication')
qmlRegisterUncreatableType(QtScene, 'Patro', 1, 0, 'QtScene', 'Cannot create QtScene')
# qmlRegisterType(QmlApplication, 'Patro', 1, 0, 'QmlApplication')
# qmlRegisterType(QtScene, 'Patro', 1, 0, 'QtScene')
qmlRegisterType(QtQuickPaintedSceneItem, 'Patro', 1, 0, 'PaintedSceneItem')
##############################################
def _set_context_properties(self):
context = self._engine.rootContext()
context.setContextProperty('application', self._qml_application)
##############################################
def _load_qml_main(self):
# self._engine.addImportPath('qrc:///qml')
qml_path = Path(__file__).parent.joinpath('qml', 'main.qml')
self._qml_url = QUrl.fromLocalFile(str(qml_path))
# QUrl('qrc:/qml/main.qml')
self._engine.objectCreated.connect(self._check_qml_is_loaded)
self._engine.load(self._qml_url)
##############################################
def _check_qml_is_loaded(self, obj, url):
# See https://bugreports.qt.io/browse/QTBUG-39469
if (obj is None and url == self._qml_url):
sys.exit(-1)
##############################################
def exec_(self):
# self._view.show()
sys.exit(self._appplication.exec_())
##############################################
def _post_init(self):
# Fixme: ui refresh ???
self._logger.info('post init')
if self._args.user_script is not None:
self.execute_user_script(self._args.user_script)
##############################################
def execute_user_script(self, script_path):
"""Execute an user script provided by file *script_path* in a context where is defined a
variable *application* that is a reference to the application instance.
"""
script_path = Path(script_path).absolute()
self._logger.info('Execute user script:\n {}'.format(script_path))
try:
source = open(script_path).read()
except FileNotFoundError:
self._logger.info('File {} not found'.format(script_path))
sys.exit(1)
try:
bytecode = compile(source, script_path, 'exec')
except SyntaxError as exception:
self._on_critical_exception(exception)
try:
exec(bytecode, {'application':self})
except Exception as exception:
self._on_critical_exception(exception)
self._logger.info('User script done')
Patro-master-Patro/Patro/QtApplication/__init__.py 0000664 0000000 0000000 00000000000 13424433274 0022461 0 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/QtApplication/qml/ 0000775 0000000 0000000 00000000000 13424433274 0021153 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/QtApplication/qml/main.qml 0000664 0000000 0000000 00000011335 13424433274 0022615 0 ustar 00root root 0000000 0000000 /***************************************************************************************************
*
* 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 .
*
***************************************************************************************************/
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
import Patro 1.0
ApplicationWindow {
id: application_window
title: 'Patro'
visible: true
width: 1000
height: 500
property int zoom_step: 10
Component.onCompleted: {
console.info('ApplicationWindow.onCompleted')
// application_window.showMaximized()
}
menuBar: MenuBar {
Menu {
title: qsTr('&File')
Action { text: qsTr('&Open') }
MenuSeparator { }
Action {
text: qsTr('&Quit')
onTriggered: application_window.close()
}
}
Menu {
title: qsTr('&Help')
Action { text: qsTr('&About') }
}
}
header: ToolBar {
RowLayout {
anchors.fill: parent
ToolButton {
icon.source: 'qrc:/icons/36x36/settings-overscan-black.png'
onClicked: scene_view.fit_scene()
}
ToolButton {
icon.source: 'qrc:/icons/36x36/zoom-in-black.png'
onClicked: {
var zoom_factor = 1 + application_window.zoom_step/100
scene_view.zoom_at_center(scene_view.zoom*zoom_factor)
}
}
ToolButton {
icon.source: 'qrc:/icons/36x36/zoom-out-black.png'
onClicked: {
var zoom_factor = 1 - application_window.zoom_step/100
scene_view.zoom_at_center(scene_view.zoom*zoom_factor)
}
}
Item {
Layout.fillWidth: true
}
}
}
footer: ToolBar {
RowLayout {
anchors.fill: parent
Text {
id: position_label
text: ''
}
}
}
PaintedSceneItem {
id: scene_view
anchors.fill: parent
scene: application.scene
focus: true
property real pan_speed: 1.5 // scale to speedup the mouse paning
property int pan_step: 10 // px
Keys.onLeftPressed: scene_view.pan_x_y(-pan_step, 0)
Keys.onRightPressed: scene_view.pan_x_y(pan_step, 0)
Keys.onDownPressed: scene_view.pan_x_y(0, -pan_step)
Keys.onUpPressed: scene_view.pan_x_y(0, pan_step)
function pan_x_y(dx, dy) {
var dxy = Qt.point(dx, dy)
// console.info('pan', dxy)
scene_view.pan(dxy)
}
MouseArea {
id: scene_mouse_area
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
// var point ???
property var mouse_start
Component.onCompleted: {
mouse_start = null
}
// onClicked: {
// if (mouse.button == Qt.LeftButton) {
// console.info('Mouse left', mouse.x, mouse.y)
// }
// }
onPressed: {
scene_view.focus = true
if (mouse.button == Qt.LeftButton) {
// console.info('Mouse left', mouse.x, mouse.y)
var position = Qt.point(mouse.x, mouse.y)
scene_view.item_at(position)
}
else if (mouse.button == Qt.MiddleButton) {
// console.info('Mouse left', mouse.x, mouse.y)
mouse_start = Qt.point(mouse.x, mouse.y)
}
}
onReleased: {
mouse_start = null
}
onPositionChanged: {
// console.info('onPositionChanged', mouse.button, mouse_start)
if (mouse_start !== null) {
var dx = (mouse.x - mouse_start.x)
var dy = (mouse_start.y - mouse.y)
dx *= scene_view.pan_speed
dy *= scene_view.pan_speed
// if (dx^2 + dy^2 > 100)
scene_view.pan_x_y(dx, dy)
mouse_start = Qt.point(mouse.x, mouse.y)
} else {
position_label.text = scene_view.format_coordinate(Qt.point(mouse.x, mouse.y))
}
}
onWheel: {
var direction = wheel.angleDelta.y > 0
// console.info('Mouse wheel', wheel.x, wheel.y, direction)
var zoom = scene_view.zoom
if (direction)
zoom *= 2
else
zoom /= 2
scene_view.zoom_at(Qt.point(wheel.x, wheel.y), zoom)
}
}
}
}
Patro-master-Patro/Patro/QtApplication/rcc/ 0000775 0000000 0000000 00000000000 13424433274 0021131 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/QtApplication/rcc/Makefile 0000664 0000000 0000000 00000000727 13424433274 0022577 0 ustar 00root root 0000000 0000000 # -*- Makefile -*-
####################################################################################################
all: patro.rcc PatroRessource.py
####################################################################################################
%.rcc : %.qrc
rcc-qt5 -binary $< -o $@
PatroRessource.py : patro.qrc
pyrcc5 -o $@ $<
####################################################################################################
clean:
rm *.py *.rcc
Patro-master-Patro/Patro/QtApplication/rcc/icons/ 0000775 0000000 0000000 00000000000 13424433274 0022244 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/QtApplication/rcc/icons/36x36/ 0000775 0000000 0000000 00000000000 13424433274 0023035 5 ustar 00root root 0000000 0000000 Patro-master-Patro/Patro/QtApplication/rcc/icons/36x36/settings-overscan-black.png 0000664 0000000 0000000 00000000367 13424433274 0030301 0 ustar 00root root 0000000 0000000 PNG
IHDR $ $ K P IDATx 0@QA+5~%T%%NA|\dRtT̄(T(m̭t LߛV2x5`ty9BG~=da|
YBBB5+VCGK?|V1F
6Ũa~5,q+Pg$^ IENDB` Patro-master-Patro/Patro/QtApplication/rcc/icons/36x36/zoom-fit-width.png 0000664 0000000 0000000 00000000544 13424433274 0026427 0 ustar 00root root 0000000 0000000 PNG
IHDR $ $ sBIT|d pHYs + IDATX?JAf9E+:kXx\"EU
z $Z8g50>7#{x F= +Ŗpq
(SAhsbU =s˞Hh=6Y, `"lMh;
CO{jZsL=(iB(o2"Bv#u6:+qS27ЃUp4JOG}