Skip to content
Commits on Source (28)
......@@ -21,3 +21,13 @@ old-todo.txt
todo.txt
tools/upload-www
trash/
Patro/GraphicEngine/TeX/__MERGE_MUSICA__
examples/output-2017/
examples/patterns/veravenus-little-bias-dress.pattern-a0.pdf
examples/patterns/veravenus-little-bias-dress.pattern-a0.svg
notes.txt
open-doc.sh
outdated.txt
ressources
src/
This diff is collapsed.
......@@ -42,13 +42,20 @@ def middle(a, b):
def cmp(a, b):
return (a > b) - (a < b)
# Fixme: sign_of ?
####################################################################################################
def sign(x):
return cmp(x, 0)
# 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.:
......
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
"""This module implements root finding for second and third degree equation.
"""
####################################################################################################
__all__ = [
'quadratic_root',
'cubic_root',
]
####################################################################################################
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 cubic_root_sympy(a, b, c, d):
x = sympy.Symbol('x')
E = a*x**3 + b*x**2 + c*x + d
return [i.n() for i in sympy.real_roots(E, x)]
####################################################################################################
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
......@@ -87,12 +87,20 @@ class Attribute:
##############################################
def from_xml(self, value):
@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`"""
......@@ -122,7 +130,8 @@ class BoolAttribute(Attribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
if value == "true" or value == "1":
return True
elif value == "false" or value == "0":
......@@ -130,13 +139,20 @@ class BoolAttribute(Attribute):
else:
raise ValueError("Incorrect boolean value {}".format(value))
##############################################
@classmethod
def to_xml(cls, value):
return 'true' if value else 'false'
####################################################################################################
class IntAttribute(Attribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
return int(value)
####################################################################################################
......@@ -145,7 +161,8 @@ class FloatAttribute(Attribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
return float(value)
####################################################################################################
......@@ -154,10 +171,13 @@ class FloatListAttribute(Attribute):
##############################################
@classmethod
def from_xml(self, value):
if value == 'none':
if value == 'none' or value is None:
return None
elif isinstance(value, (tuple, list)): # Python value
return value
else:
if ' ' in value:
separator = ' '
......@@ -167,13 +187,20 @@ class FloatListAttribute(Attribute):
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):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
return str(value)
####################################################################################################
......@@ -218,7 +245,7 @@ class XmlObjectAdaptatorMetaClass(type):
####################################################################################################
class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass):
class XmlObjectAdaptator(metaclass=XmlObjectAdaptatorMetaClass):
"""Class to implement an object oriented adaptor for XML elements."""
......@@ -288,9 +315,18 @@ class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass):
##############################################
def to_xml(self, **kwargs):
"""Return an etree element"""
attributes = {attribute.xml_attribute:str(attribute.get_attribute(self)) for attribute in self.__attributes__}
# 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)
##############################################
......@@ -303,3 +339,32 @@ class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass):
# 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
......@@ -32,7 +32,6 @@ Import Algorithm:
####################################################################################################
import logging
from pathlib import Path
from lxml import etree
......@@ -45,6 +44,24 @@ _module_logger = logging.getLogger(__name__)
####################################################################################################
class RenderState:
##############################################
def __init__(self):
self._transformations = []
##############################################
def push_transformation(self, transformation):
self._transformations.append(transformation)
def pop_transformation(self):
self._transformations.pop()
####################################################################################################
class SvgDispatcher:
"""Class to dispatch XML to Python class."""
......@@ -94,6 +111,8 @@ class SvgDispatcher:
def __init__(self, root):
self._state = RenderState()
self.on_root(root)
##############################################
......@@ -149,6 +168,11 @@ class SvgFile(XmlFileMixin):
_logger = _module_logger.getChild('SvgFile')
SVG_DOCTYPE = '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
SVG_xmlns = 'http://www.w3.org/2000/svg'
SVG_xmlns_xlink = 'http://www.w3.org/1999/xlink'
SVG_version = '1.1'
##############################################
def __init__(self, path=None):
......@@ -156,7 +180,9 @@ class SvgFile(XmlFileMixin):
# Fixme: path
if path is None:
path = ''
XmlFileMixin.__init__(self, path)
# Fixme:
# if path is not None:
if path != '':
......@@ -180,16 +206,52 @@ class SvgFile(XmlFileMixin):
##############################################
def write(self, path=None):
root = etree.Element('pattern')
@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('Pattern created with Patro (https://github.com/FabriceSalvaire/Patro)'))
# Fixme: ...
return root
##############################################
def write(self, paper, root_tree, transformation=None, path=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())
if path is None:
path = self.path
with open(str(path), 'wb') as f:
# ElementTree.write() ?
f.write(etree.tostring(root, pretty_print=True))
tree = etree.ElementTree(root)
tree.write(str(path),
pretty_print=True,
xml_declaration=True,
encoding='utf-8',
standalone=False,
doctype=self.SVG_DOCTYPE,
)
......@@ -72,10 +72,12 @@ from Patro.Common.Xml.Objectivity import (
IntAttribute, FloatAttribute,
FloatListAttribute,
StringAttribute,
XmlObjectAdaptator
XmlObjectAdaptator,
TextXmlObjectAdaptator,
)
# from Patro.GeometryEngine.Vector import Vector2D
from Patro.GeometryEngine.Transformation import AffineTransformation2D
####################################################################################################
......@@ -190,10 +192,101 @@ class IdMixin:
#
####################################################################################################
####################################################################################################
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 red #FFFFFF
StringAttribute('fill'), # none inherit red #ffbb00
StringAttribute('stroke'),
)
......@@ -204,8 +297,8 @@ class StrokeMixin:
__attributes__ = (
StringAttribute('stroke_line_cap', 'stroke-linecap'),
StringAttribute('stroke_line_join', 'stroke-linejoin'),
FloatAttribute('stroke_miter_limit', 'stroke-miterlimit'),
FloatAttribute('stroke_width', 'stroke-width'),
NumberAttribute('stroke_miter_limit', 'stroke-miterlimit'),
PercentLengthAttribute('stroke_width', 'stroke-width'),
FloatListAttribute('stroke_dasharray', 'stroke-dasharray') # stroke-dasharray="20,10,5,5,5,10"
)
......@@ -329,19 +422,30 @@ class TransformAttribute(StringAttribute):
##############################################
def from_xml(self, value):
@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
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))
##############################################
return transforms
@classmethod
def to_xml(cls, value):
return 'matrix({})'.format(' '.join([str(x) for x in value.to_list()])) # Fixme: to func
####################################################################################################
......@@ -789,30 +893,58 @@ class PathDataAttribute(StringAttribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
# Replace comma separator by space
value = value.replace(',', ' ')
# Add space after letter
data_path = ''
for c in value:
data_path += c
if c.isalpha:
data_path += ' '
# Convert float
parts = []
for part in split_space_list(value):
if not(len(part) == 1 and part.isalpha):
part = float(part)
parts.append(part)
parts = split_space_list(value)
commands = []
command = None # last command
number_of_args = None
i = 0
while i < len(parts):
part = parts[i]
command = part
command_lower = command.lower()
if command_lower in self.COMMANDS:
number_of_args = self.NUMBER_OF_ARGS[command_lower]
if number_of_args % 2:
raise ValueError
next_i = i+number_of_args+1
values = [float(x) for x in parts[i+1:next_i]]
#! points = [Vector2D(values[2*i], values[2*i+1]) for i in range(number_of_args / 2)]
points = values
commands.append((command, points))
i = next_i
else:
raise ValueError
if isinstance(part, str):
command = part
command_lower = command.lower()
if command_lower not in cls.COMMANDS:
raise ValueError('Invalid path instruction')
number_of_args = cls.NUMBER_OF_ARGS[command_lower]
# else repeated instruction
next_i = i + number_of_args + 1
values = parts[i+1:next_i]
#! points = [Vector2D(values[2*i], values[2*i+1]) for i in range(number_of_args / 2)]
points = values
commands.append((command, points))
i = next_i
return 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
####################################################################################################
class Path(PathMixin, SvgElementMixin, XmlObjectAdaptator):
......@@ -910,7 +1042,15 @@ class Stop(XmlObjectAdaptator):
####################################################################################################
class Text(DeltaMixin, FontMixin, ColorMixin, SvgElementMixin, XmlObjectAdaptator):
class Style(TextXmlObjectAdaptator):
"""Defines style"""
__tag__ = 'style'
####################################################################################################
class Text(PositionMixin, DeltaMixin, FontMixin, ColorMixin, SvgElementMixin, TextXmlObjectAdaptator):
"""Defines a text"""
......@@ -924,6 +1064,12 @@ class Text(DeltaMixin, FontMixin, ColorMixin, SvgElementMixin, XmlObjectAdaptato
# 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):
......
......@@ -24,8 +24,8 @@ import logging
from Patro.Common.Xml.Objectivity import StringAttribute, XmlObjectAdaptator
from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.Pattern.Measurement import Measurements
from Patro.Pattern.PersonalData import Gender
from Patro.Measurement.ValentinaMeasurement import ValentinaMeasurements
from Patro.Measurement.PersonalData import Gender
####################################################################################################
......@@ -76,7 +76,7 @@ class VitFile(XmlFileMixin):
def __init__(self, path):
XmlFileMixin.__init__(self, path)
self._measurements = Measurements()
self._measurements = ValentinaMeasurements()
self._read()
##############################################
......@@ -116,5 +116,3 @@ class VitFile(XmlFileMixin):
measurements.add(**xml_measurement.to_dict())
else:
raise NotImplementedError
measurements.eval()
......@@ -54,6 +54,145 @@ from Patro.GeometryEngine.Vector import Vector2D
####################################################################################################
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__ = (
......@@ -675,81 +814,3 @@ class DetailNode(XmlObjectAdaptator):
StringAttribute('type'),
BoolAttribute('reverse'),
)
####################################################################################################
# angle
# angle1
# angle2
# arc
# axisP1
# axisP2
# axisType
# baseLineP1
# baseLineP2
# basePoint
# c1Center
# c1Radius
# c2Center
# c2Radius
# cCenter
# center
# color
# cRadius
# crossPoint
# curve
# curve1
# curve2
# dartP1
# dartP2
# dartP3
# duplicate
# firstArc
# firstPoint
# hCrossPoint
# id
# idObject
# length
# length1
# length2
# lineColor
# mx
# mx1
# mx2
# my
# my1
# my2
# name
# name1
# name2
# object (group)
# p1Line
# p1Line1
# p1Line2
# p2Line
# p2Line1
# p2Line2
# point1
# point2
# point3
# point4
# pShoulder
# pSpline
# radius
# radius1
# radius2
# rotationAngle
# secondArc
# secondPoint
# spline
# splinePath
# suffix
# tangent
# thirdPoint
# tool
# type
# typeLine
# vCrossPoint
# visible (group)
# x
# y
This diff is collapsed.
......@@ -18,11 +18,83 @@
#
####################################################################################################
"""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 = points[0].bounding_box()
for point in points[1:]:
bounding_box |= point.bounding_box()
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."""
# Reference: Graham Scan Algorithm
# https://en.wikipedia.org/wiki/Graham_scan
# convex_hull is a stack of points beginning with the leftmost point.
convex_hull = []
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
......@@ -20,28 +20,108 @@
####################################################################################################
from math import sqrt, radians, cos, sin
from math import sqrt, radians, cos, sin, fabs, pi
import numpy as np
from IntervalArithmetic import Interval, Interval2D
from IntervalArithmetic import Interval
from .Primitive import Primitive2D
from Patro.Common.Math.Functions import sign
from .Line import Line2D
from .Primitive import Primitive, Primitive2DMixin
from .Segment import Segment2D
from .Vector import Vector2D
####################################################################################################
class Circle2D(Primitive2D):
class DomainMixin:
##############################################
@property
def domain(self):
return self._domain
@domain.setter
def domain(self, interval):
if interval is not None and interval.length < 360:
self._domain = Interval(interval)
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.inf if start else self.domain.sup
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 Circle2D(Primitive2DMixin, DomainMixin, 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
##############################################
# Fixme: tangent constructs ...
##############################################
def __init__(self, center, radius, domain=None, diameter=False):
"""Construct a 2D circle from a center point and a radius.
def __init__(self, center, radius, domain):
If the circle is not closed, *domain* is an interval in degrees.
"""
if diameter:
radius /= 2
self._radius = radius
self._center = Vector2D(center)
self._domain = Interval(domain)
self.center = center
self.domain = domain # Fixme: name ???
##############################################
......@@ -51,7 +131,7 @@ class Circle2D(Primitive2D):
@center.setter
def center(self, value):
self._center = value
self._center = Vector2D(value)
@property
def radius(self):
......@@ -62,38 +142,202 @@ class Circle2D(Primitive2D):
self._radius = value
@property
def domain(self):
return self._domain
def diameter(self):
return self._radius * 2
@domain.setter
def domain(self, value):
self._domain = value
##############################################
@property
def eccentricity(self):
return 1
@property
def perimeter(self):
return 2 * pi * self._radius
@property
def area(self):
return pi * self._radius**2
##############################################
def point_at_angle(self, angle):
return Vector2D.from_polar(self._radius, angle) + self._center
##############################################
def tangent_at_angle(self, angle):
point = Vector2D.from_polar(self._radius, angle) + self._center
tangent = (point - self._center).normal
return Line2D(point, tangent)
##############################################
@property
def bounding_box(self):
return self._center.bounding_box.enlarge(self._radius)
##############################################
def is_point_inside(self, point):
return (point - self._center).magnitude_square <= self._radius**2
return self._center.bounding_box().enlarge(self._radius)
##############################################
def intersect_segment(self, segment):
# Fixme: check domain !!!
# http://mathworld.wolfram.com/Circle-LineIntersection.html
# Reference: 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.
# Definitions
# dx = x1 - x0
# dy = y1 - y0
# D = x0 * y1 - x1 * y0
# Equations
# x**2 + y**2 = r**2
# dx * y = dy * x - D
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)
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 Conic2D(Primitive2D):
class Conic2D(Primitive2DMixin, DomainMixin, Primitive):
"""Class to implements 2D Conic."""
#######################################
def __init__(self, x_radius, y_radius, center, angle, domain):
def __init__(self, center, x_radius, y_radius, angle, domain=None):
self.center = center
self._x_radius = x_radius
self._y_radius = y_radius
self._center = Vector2D(center)
self._angle = angle
self._domain = Interval(domain)
self.domain = Interval(domain)
##############################################
......@@ -103,7 +347,7 @@ class Conic2D(Primitive2D):
@center.setter
def center(self, value):
self._center = value
self._center = Vector2D(value)
@property
def x_radius(self):
......@@ -129,14 +373,6 @@ class Conic2D(Primitive2D):
def angle(self, value):
self._angle = value
@property
def domain(self):
return self._domain
@domain.setter
def domain(self, value):
self._domain = value
##############################################
@property
......@@ -179,3 +415,29 @@ class Conic2D(Primitive2D):
(B/2, C, E/2),
(D/2, E/2, F),
))
##############################################
def point_at_angle(self, angle):
raise NotImplementedError
##############################################
@property
def bounding_box(self):
raise NotImplementedError
##############################################
def is_point_inside(self, point):
raise NotImplementedError
##############################################
def intersect_segment(self, segment):
raise NotImplementedError
##############################################
def intersect_conic(self, conic):
raise NotImplementedError
......@@ -22,12 +22,12 @@
from Patro.Common.IterTools import pairwise
from .Primitive import Primitive2D
from .Primitive import Primitive, Primitive2DMixin
from .Vector import Vector2D
####################################################################################################
class Line2D(Primitive2D):
class Line2D(Primitive2DMixin, Primitive):
"""Class to implement 2D Line."""
......@@ -68,10 +68,19 @@ class Line2D(Primitive2D):
##############################################
def point_at_s(self, s):
@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):
......@@ -106,9 +115,9 @@ class Line2D(Primitive2D):
# Fixme: is_parallel_to
def is_parallel(self, other):
def is_parallel(self, other, cross=False):
"""Self is parallel to other"""
return self.v.is_parallel(other.v)
return self.v.is_parallel(other.v, cross)
##############################################
......@@ -134,7 +143,7 @@ class Line2D(Primitive2D):
"""Return the orthogonal line at abscissa s"""
point = self.point_at_s(s)
point = self.interpolate(s)
vector = self.v.normal()
return self.__class__(point, vector)
......@@ -147,17 +156,20 @@ class Line2D(Primitive2D):
# l1 = p1 + s1*v1
# l2 = p2 + s2*v2
# delta = p2 - p1 = s2*v2 - s1*v1
# delta x v1 = s2*v2 x v1 = s2 * - v1 x v2
# delta x v2 = s1*v1 x v2 = s1 * v1 x v2
if l1.is_parallel(l2):
# 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, cross=True)
if test:
return (None, None)
else:
denominator = 1. / l1.v.cross(l2.v)
denominator = 1. / cross
delta = l2.p - l1.p
s1 = delta.cross(l2.v) * denominator
s2 = delta.cross(l1.v) * -denominator
s2 = delta.cross(l1.v) * denominator
return (s1, s2)
##############################################
......@@ -166,11 +178,11 @@ class Line2D(Primitive2D):
"""Return the intersection Point between self and other"""
s0, s1 = self.intersection_abscissae(other)
if s0 is None:
s1, s2 = self.intersection_abscissae(other)
if s1 is None:
return None
else:
return self.point_at_s(s0)
return self.interpolate(s1)
##############################################
......@@ -191,9 +203,24 @@ class Line2D(Primitive2D):
"""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
##############################################
......
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
####################################################################################################
import math
from Patro.Common.Math.Functions import sign
from .Primitive import PrimitiveNP, Primitive2DMixin
from .Segment import Segment2D
from .Triangle import Triangle2D
####################################################################################################
class Polygon2D(Primitive2DMixin, 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
##############################################
@property
def is_closed(self):
return True
##############################################
@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')
##############################################
# barycenter
# momentum
##############################################
@property
def edges(self):
if self._edges is None:
N = self.number_of_points
for i in range(N):
j = (i+1) % N
edge = Segment2D(self._points[i], self._points[j])
self._edges.append(edge)
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
##############################################
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.magnitude for edge in self.edges])
##############################################
@property
def area(self):
if not self.is_simple:
return None
# http://mathworld.wolfram.com/PolygonArea.html
# A = 1/2 (x1*y2 - x2*y1 + x2*y3 - x3*y2 + ... + x(n-1)*yn - xn*y(n-1) + xn*y1 - x1*yn)
# determinant
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])
# 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).
return abs(area) / 2
##############################################
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)
......@@ -18,55 +18,164 @@
#
####################################################################################################
__all__ = [
'Primitive',
'Primitive2DMixin',
'Primitive1P',
'Primitive2P',
'Primitive3P',
'Primitive4P',
]
####################################################################################################
import collections
from .BoundingBox import bounding_box_from_points
####################################################################################################
# Fixme:
# length, interpolate path
# area
####################################################################################################
class Primitive:
"""Base class for geometric primitive"""
__dimension__ = None # in [2, 3] for 2D / 3D primitive
# __dimension__ = None # in [2, 3] for 2D / 3D primitive
__vector_cls__ = None
##############################################
def clone(self):
@property
def dimension(self):
"""Dimension in [2, 3] for 2D / 3D primitive"""
raise NotImplementedError
##############################################
def bounding_box(self):
# Fixme: infinite primitive
@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_reversable(self):
def is_reversible(self):
"""True if the order of the points is reversible"""
# Fixme: True if number_of_points > 1 ???
return False
##############################################
@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):
# for point in self.points:
# point *= transformation # don't work
self._set_points([transformation*p for p in self.points])
####################################################################################################
class Primitive2D:
__dimension__ = 2
class Primitive2DMixin:
# __dimension__ = 2
__vector_cls__ = None # Fixme: due to import, done in module's __init__.py
@property
def dimension(self):
return 2
####################################################################################################
class ReversablePrimitiveMixin:
class ReversiblePrimitiveMixin:
##############################################
@property
def is_reversable(self):
return True
def is_reversible(self):
True
##############################################
def reverse(self):
@property
def reversed_points(self):
raise NotImplementedError
# return reversed(list(self.points))
##############################################
def reverse(self):
return self.__class__(*self.reversed_points)
##############################################
......@@ -78,8 +187,244 @@ class ReversablePrimitiveMixin:
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):
return iter((self._p2, self._p1, self._p0))
##############################################
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 _set_points(self, points):
self._p0, self._p1, self._p2, self._p3 = points
####################################################################################################
class PrimitiveNP(Primitive, ReversiblePrimitiveMixin):
##############################################
def __init__(self, *points):
if len(points) == 1 and isinstance(points[0], collections.Iterable):
points = points[0]
self._points = [self.__vector_cls__(p) for p in points]
##############################################
@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 _set_points(self, points):
self._points = points
##############################################
def iter_on_points(self):
for point in self.start_point, self.start_point:
yield point
def __getitem__(self, _slice):
return self._points[_slice]
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
####################################################################################################
import math
from .Primitive import Primitive2P, Primitive2DMixin
from .Segment import Segment2D
####################################################################################################
class Rectangle2D(Primitive2DMixin, 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)
......@@ -21,17 +21,18 @@
####################################################################################################
# from .Interpolation import interpolate_two_points
from .BoundingBox import bounding_box_from_points
from .Line import Line2D
from .Primitive import Primitive2D, ReversablePrimitiveMixin
from .Primitive import Primitive2P, Primitive2DMixin
from .Triangle import triangle_orientation
from .Vector import Vector2D
####################################################################################################
class Segment2D(Primitive2D, ReversablePrimitiveMixin):
class Segment2D(Primitive2DMixin, Primitive2P):
"""2D Segment"""
"""Class to implement 2D Segment"""
# Fixme: _p0 versus p0
#######################################
......@@ -39,49 +40,7 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
"""Construct a :class:`Segment2D` between two points."""
self._p0 = Vector2D(p0)
self._p1 = Vector2D(p1)
##############################################
def clone(self):
return self.__class__(self._p0, self._p1)
##############################################
def bounding_box(self):
return bounding_box_from_points((self._p0, self._p1))
##############################################
def reverse(self):
return self.__class__(self._p1, self._p0)
##############################################
@property
def p0(self):
return self._p0
@p0.setter
def p0(self, value):
self._p0 = value
@property
def p1(self):
return self._p1
@p1.setter
def p1(self, value):
self._p1 = value
@property
def start_point(self):
return self._p0
@property
def end_point(self):
return self._p1
Primitive2P.__init__(self, p0, p1)
##############################################
......@@ -100,19 +59,23 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
##############################################
@property
def cross_product(self):
return self._p0.cross(self._p1)
##############################################
def to_line(self):
return Line2D.from_two_points(self._p1, self._p0)
##############################################
def point_at_t(self, t):
# return interpolate_two_points(self._p0, self._p1)
return self._p0 * (1 - t) + self._p1 * t
point_at_t = Primitive2P.interpolate
##############################################
def intersect(self, segment2):
def intersect_with(self, segment2):
"""Checks if the line segments intersect.
return 1 if there is an intersection
......@@ -131,3 +94,69 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
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
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
......@@ -80,6 +80,11 @@ class Transformation:
##############################################
def to_list(self):
return list(self._m.flat)
##############################################
def same_dimension(self, other):
return self.__size__ == other.dimension
......@@ -129,7 +134,7 @@ class Transformation2D(Transformation):
@classmethod
def Scale(cls, x_scale, y_scale):
return cls(np.array((x_scale, 0), (0, y_scale)))
return cls(np.array(((x_scale, 0), (0, y_scale))))
##############################################
......@@ -164,15 +169,6 @@ class AffineTransformation(Transformation):
##############################################
@classmethod
def Rotation(cls, angle):
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Rotation(angle).array
return transformation
##############################################
@classmethod
def RotationAt(cls, center, angle):
......@@ -191,6 +187,33 @@ class AffineTransformation(Transformation):
def translation_part(self):
return self._m[:self.__dimension__,-1]
####################################################################################################
class AffineTransformation2D(AffineTransformation):
__dimension__ = 2
__size__ = 3
##############################################
@classmethod
def Rotation(cls, angle):
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Rotation(angle).array
return transformation
##############################################
@classmethod
def Scale(cls, x_scale, y_scale):
# Fixme: others, use *= ?
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Scale(x_scale, y_scale).array
return transformation
#######################################
def __mul__(self, obj):
......@@ -206,12 +229,6 @@ class AffineTransformation(Transformation):
####################################################################################################
class AffineTransformation2D(AffineTransformation):
__dimension__ = 2
__size__ = 3
####################################################################################################
# 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
......
......@@ -20,6 +20,13 @@
####################################################################################################
import math
from .Primitive import Primitive3P, Primitive2DMixin
from .Line import Line2D
####################################################################################################
def triangle_orientation(p0, p1, p2):
"""Return the triangle orientation defined by the three points."""
......@@ -47,3 +54,235 @@ def triangle_orientation(p0, p1, p2):
# 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, 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)
)