Skip to content
Commits on Source (28)
...@@ -21,3 +21,13 @@ old-todo.txt ...@@ -21,3 +21,13 @@ old-todo.txt
todo.txt todo.txt
tools/upload-www tools/upload-www
trash/ 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): ...@@ -42,13 +42,20 @@ def middle(a, b):
def cmp(a, b): def cmp(a, b):
return (a > b) - (a < b) return (a > b) - (a < b)
# Fixme: sign_of ? ####################################################################################################
def sign(x): 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): def epsilon_float(a, b, epsilon = 1e-3):
return abs(a-b) <= epsilon return abs(a-b) <= epsilon
####################################################################################################
def trignometric_clamp(x): def trignometric_clamp(x):
"""Clamp *x* in the range [-1.,1].""" """Clamp *x* in the range [-1.,1]."""
if x > 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: ...@@ -87,12 +87,20 @@ class Attribute:
############################################## ##############################################
def from_xml(self, value): @classmethod
def from_xml(cls, value):
"""Convert a value from XML to Python""" """Convert a value from XML to Python"""
raise NotImplementedError raise NotImplementedError
############################################## ##############################################
@classmethod
def to_xml(cls, value):
"""Convert a value from Python to XML"""
return str(value)
##############################################
def set_property(self, cls): def set_property(self, cls):
"""Define a property for this attribute in :class:`XmlObjectAdaptator`""" """Define a property for this attribute in :class:`XmlObjectAdaptator`"""
...@@ -122,7 +130,8 @@ class BoolAttribute(Attribute): ...@@ -122,7 +130,8 @@ class BoolAttribute(Attribute):
############################################## ##############################################
def from_xml(self, value): @classmethod
def from_xml(cls, value):
if value == "true" or value == "1": if value == "true" or value == "1":
return True return True
elif value == "false" or value == "0": elif value == "false" or value == "0":
...@@ -130,13 +139,20 @@ class BoolAttribute(Attribute): ...@@ -130,13 +139,20 @@ class BoolAttribute(Attribute):
else: else:
raise ValueError("Incorrect boolean value {}".format(value)) raise ValueError("Incorrect boolean value {}".format(value))
##############################################
@classmethod
def to_xml(cls, value):
return 'true' if value else 'false'
#################################################################################################### ####################################################################################################
class IntAttribute(Attribute): class IntAttribute(Attribute):
############################################## ##############################################
def from_xml(self, value): @classmethod
def from_xml(cls, value):
return int(value) return int(value)
#################################################################################################### ####################################################################################################
...@@ -145,7 +161,8 @@ class FloatAttribute(Attribute): ...@@ -145,7 +161,8 @@ class FloatAttribute(Attribute):
############################################## ##############################################
def from_xml(self, value): @classmethod
def from_xml(cls, value):
return float(value) return float(value)
#################################################################################################### ####################################################################################################
...@@ -154,10 +171,13 @@ class FloatListAttribute(Attribute): ...@@ -154,10 +171,13 @@ class FloatListAttribute(Attribute):
############################################## ##############################################
@classmethod
def from_xml(self, value): def from_xml(self, value):
if value == 'none': if value == 'none' or value is None:
return None return None
elif isinstance(value, (tuple, list)): # Python value
return value
else: else:
if ' ' in value: if ' ' in value:
separator = ' ' separator = ' '
...@@ -167,13 +187,20 @@ class FloatListAttribute(Attribute): ...@@ -167,13 +187,20 @@ class FloatListAttribute(Attribute):
return [float(value)] return [float(value)]
return [float(x) for x in value.split(separator)] 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): class StringAttribute(Attribute):
############################################## ##############################################
def from_xml(self, value): @classmethod
def from_xml(cls, value):
return str(value) return str(value)
#################################################################################################### ####################################################################################################
...@@ -218,7 +245,7 @@ class XmlObjectAdaptatorMetaClass(type): ...@@ -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.""" """Class to implement an object oriented adaptor for XML elements."""
...@@ -288,9 +315,18 @@ class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass): ...@@ -288,9 +315,18 @@ class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass):
############################################## ##############################################
def to_xml(self, **kwargs): def to_xml(self, **kwargs):
"""Return an etree element""" """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) attributes.update(kwargs)
return etree.Element(self.__tag__, **attributes) return etree.Element(self.__tag__, **attributes)
############################################## ##############################################
...@@ -303,3 +339,32 @@ class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass): ...@@ -303,3 +339,32 @@ class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass):
# def __getattribute__(self, name): # def __getattribute__(self, name):
# object.__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: ...@@ -32,7 +32,6 @@ Import Algorithm:
#################################################################################################### ####################################################################################################
import logging import logging
from pathlib import Path
from lxml import etree from lxml import etree
...@@ -45,6 +44,24 @@ _module_logger = logging.getLogger(__name__) ...@@ -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 SvgDispatcher:
"""Class to dispatch XML to Python class.""" """Class to dispatch XML to Python class."""
...@@ -94,6 +111,8 @@ class SvgDispatcher: ...@@ -94,6 +111,8 @@ class SvgDispatcher:
def __init__(self, root): def __init__(self, root):
self._state = RenderState()
self.on_root(root) self.on_root(root)
############################################## ##############################################
...@@ -149,6 +168,11 @@ class SvgFile(XmlFileMixin): ...@@ -149,6 +168,11 @@ class SvgFile(XmlFileMixin):
_logger = _module_logger.getChild('SvgFile') _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): def __init__(self, path=None):
...@@ -156,7 +180,9 @@ class SvgFile(XmlFileMixin): ...@@ -156,7 +180,9 @@ class SvgFile(XmlFileMixin):
# Fixme: path # Fixme: path
if path is None: if path is None:
path = '' path = ''
XmlFileMixin.__init__(self, path) XmlFileMixin.__init__(self, path)
# Fixme: # Fixme:
# if path is not None: # if path is not None:
if path != '': if path != '':
...@@ -180,16 +206,52 @@ class SvgFile(XmlFileMixin): ...@@ -180,16 +206,52 @@ class SvgFile(XmlFileMixin):
############################################## ##############################################
def write(self, path=None): @classmethod
def new_root(cls, paper):
root = etree.Element('pattern')
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)')) 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: if path is None:
path = self.path 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 ( ...@@ -72,10 +72,12 @@ from Patro.Common.Xml.Objectivity import (
IntAttribute, FloatAttribute, IntAttribute, FloatAttribute,
FloatListAttribute, FloatListAttribute,
StringAttribute, StringAttribute,
XmlObjectAdaptator XmlObjectAdaptator,
TextXmlObjectAdaptator,
) )
# from Patro.GeometryEngine.Vector import Vector2D # from Patro.GeometryEngine.Vector import Vector2D
from Patro.GeometryEngine.Transformation import AffineTransformation2D
#################################################################################################### ####################################################################################################
...@@ -190,10 +192,101 @@ class IdMixin: ...@@ -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: class ColorMixin:
__attributes__ = ( __attributes__ = (
StringAttribute('fill'), # none red #FFFFFF StringAttribute('fill'), # none inherit red #ffbb00
StringAttribute('stroke'), StringAttribute('stroke'),
) )
...@@ -204,8 +297,8 @@ class StrokeMixin: ...@@ -204,8 +297,8 @@ class StrokeMixin:
__attributes__ = ( __attributes__ = (
StringAttribute('stroke_line_cap', 'stroke-linecap'), StringAttribute('stroke_line_cap', 'stroke-linecap'),
StringAttribute('stroke_line_join', 'stroke-linejoin'), StringAttribute('stroke_line_join', 'stroke-linejoin'),
FloatAttribute('stroke_miter_limit', 'stroke-miterlimit'), NumberAttribute('stroke_miter_limit', 'stroke-miterlimit'),
FloatAttribute('stroke_width', 'stroke-width'), PercentLengthAttribute('stroke_width', 'stroke-width'),
FloatListAttribute('stroke_dasharray', 'stroke-dasharray') # stroke-dasharray="20,10,5,5,5,10" FloatListAttribute('stroke_dasharray', 'stroke-dasharray') # stroke-dasharray="20,10,5,5,5,10"
) )
...@@ -329,19 +422,30 @@ class TransformAttribute(StringAttribute): ...@@ -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): ...@@ -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 = [] commands = []
command = None # last command
number_of_args = None
i = 0 i = 0
while i < len(parts): while i < len(parts):
part = parts[i] part = parts[i]
command = part if isinstance(part, str):
command_lower = command.lower() command = part
if command_lower in self.COMMANDS: command_lower = command.lower()
number_of_args = self.NUMBER_OF_ARGS[command_lower] if command_lower not in cls.COMMANDS:
if number_of_args % 2: raise ValueError('Invalid path instruction')
raise ValueError number_of_args = cls.NUMBER_OF_ARGS[command_lower]
next_i = i+number_of_args+1 # else repeated instruction
values = [float(x) for x in parts[i+1:next_i]] next_i = i + number_of_args + 1
#! points = [Vector2D(values[2*i], values[2*i+1]) for i in range(number_of_args / 2)] values = parts[i+1:next_i]
points = values #! points = [Vector2D(values[2*i], values[2*i+1]) for i in range(number_of_args / 2)]
commands.append((command, points)) points = values
i = next_i commands.append((command, points))
else: i = next_i
raise ValueError
return commands 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): class Path(PathMixin, SvgElementMixin, XmlObjectAdaptator):
...@@ -910,7 +1042,15 @@ class Stop(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""" """Defines a text"""
...@@ -924,6 +1064,12 @@ class Text(DeltaMixin, FontMixin, ColorMixin, SvgElementMixin, XmlObjectAdaptato ...@@ -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)" # 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'" # 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): class TextRef(XmlObjectAdaptator):
......
...@@ -24,8 +24,8 @@ import logging ...@@ -24,8 +24,8 @@ import logging
from Patro.Common.Xml.Objectivity import StringAttribute, XmlObjectAdaptator from Patro.Common.Xml.Objectivity import StringAttribute, XmlObjectAdaptator
from Patro.Common.Xml.XmlFile import XmlFileMixin from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.Pattern.Measurement import Measurements from Patro.Measurement.ValentinaMeasurement import ValentinaMeasurements
from Patro.Pattern.PersonalData import Gender from Patro.Measurement.PersonalData import Gender
#################################################################################################### ####################################################################################################
...@@ -76,7 +76,7 @@ class VitFile(XmlFileMixin): ...@@ -76,7 +76,7 @@ class VitFile(XmlFileMixin):
def __init__(self, path): def __init__(self, path):
XmlFileMixin.__init__(self, path) XmlFileMixin.__init__(self, path)
self._measurements = Measurements() self._measurements = ValentinaMeasurements()
self._read() self._read()
############################################## ##############################################
...@@ -116,5 +116,3 @@ class VitFile(XmlFileMixin): ...@@ -116,5 +116,3 @@ class VitFile(XmlFileMixin):
measurements.add(**xml_measurement.to_dict()) measurements.add(**xml_measurement.to_dict())
else: else:
raise NotImplementedError raise NotImplementedError
measurements.eval()
...@@ -54,6 +54,145 @@ from Patro.GeometryEngine.Vector import Vector2D ...@@ -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: class MxMyMixin:
__attributes__ = ( __attributes__ = (
...@@ -675,81 +814,3 @@ class DetailNode(XmlObjectAdaptator): ...@@ -675,81 +814,3 @@ class DetailNode(XmlObjectAdaptator):
StringAttribute('type'), StringAttribute('type'),
BoolAttribute('reverse'), 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 @@ ...@@ -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): def bounding_box_from_points(points):
"""Return the bounding box of the list of points.""" """Return the bounding box of the list of points."""
bounding_box = points[0].bounding_box()
for point in points[1:]: bounding_box = None
bounding_box |= point.bounding_box() for point in points:
if bounding_box is None:
bounding_box = point.bounding_box
else:
bounding_box |= point.bounding_box
return 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 @@ ...@@ -20,28 +20,108 @@
#################################################################################################### ####################################################################################################
from math import sqrt, radians, cos, sin from math import sqrt, radians, cos, sin, fabs, pi
import numpy as np 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 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.""" """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._radius = radius
self._center = Vector2D(center) self.center = center
self._domain = Interval(domain) self.domain = domain # Fixme: name ???
############################################## ##############################################
...@@ -51,7 +131,7 @@ class Circle2D(Primitive2D): ...@@ -51,7 +131,7 @@ class Circle2D(Primitive2D):
@center.setter @center.setter
def center(self, value): def center(self, value):
self._center = value self._center = Vector2D(value)
@property @property
def radius(self): def radius(self):
...@@ -62,38 +142,202 @@ class Circle2D(Primitive2D): ...@@ -62,38 +142,202 @@ class Circle2D(Primitive2D):
self._radius = value self._radius = value
@property @property
def domain(self): def diameter(self):
return self._domain return self._radius * 2
@domain.setter ##############################################
def domain(self, value):
self._domain = value
@property @property
def eccentricity(self): def eccentricity(self):
return 1 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): 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.""" """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._x_radius = x_radius
self._y_radius = y_radius self._y_radius = y_radius
self._center = Vector2D(center)
self._angle = angle self._angle = angle
self._domain = Interval(domain) self.domain = Interval(domain)
############################################## ##############################################
...@@ -103,7 +347,7 @@ class Conic2D(Primitive2D): ...@@ -103,7 +347,7 @@ class Conic2D(Primitive2D):
@center.setter @center.setter
def center(self, value): def center(self, value):
self._center = value self._center = Vector2D(value)
@property @property
def x_radius(self): def x_radius(self):
...@@ -129,14 +373,6 @@ class Conic2D(Primitive2D): ...@@ -129,14 +373,6 @@ class Conic2D(Primitive2D):
def angle(self, value): def angle(self, value):
self._angle = value self._angle = value
@property
def domain(self):
return self._domain
@domain.setter
def domain(self, value):
self._domain = value
############################################## ##############################################
@property @property
...@@ -179,3 +415,29 @@ class Conic2D(Primitive2D): ...@@ -179,3 +415,29 @@ class Conic2D(Primitive2D):
(B/2, C, E/2), (B/2, C, E/2),
(D/2, E/2, F), (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 @@ ...@@ -22,12 +22,12 @@
from Patro.Common.IterTools import pairwise from Patro.Common.IterTools import pairwise
from .Primitive import Primitive2D from .Primitive import Primitive, Primitive2DMixin
from .Vector import Vector2D from .Vector import Vector2D
#################################################################################################### ####################################################################################################
class Line2D(Primitive2D): class Line2D(Primitive2DMixin, Primitive):
"""Class to implement 2D Line.""" """Class to implement 2D Line."""
...@@ -68,10 +68,19 @@ class Line2D(Primitive2D): ...@@ -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 the Point corresponding to the curvilinear abscissa s"""
return self.p + (self.v * s) return self.p + (self.v * s)
point_at_s = interpolate
point_at_t = interpolate
############################################## ##############################################
def compute_distance_between_abscissae(self, s0, s1): def compute_distance_between_abscissae(self, s0, s1):
...@@ -106,9 +115,9 @@ class Line2D(Primitive2D): ...@@ -106,9 +115,9 @@ class Line2D(Primitive2D):
# Fixme: is_parallel_to # Fixme: is_parallel_to
def is_parallel(self, other): def is_parallel(self, other, cross=False):
"""Self is parallel to other""" """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): ...@@ -134,7 +143,7 @@ class Line2D(Primitive2D):
"""Return the orthogonal line at abscissa s""" """Return the orthogonal line at abscissa s"""
point = self.point_at_s(s) point = self.interpolate(s)
vector = self.v.normal() vector = self.v.normal()
return self.__class__(point, vector) return self.__class__(point, vector)
...@@ -147,17 +156,20 @@ class Line2D(Primitive2D): ...@@ -147,17 +156,20 @@ class Line2D(Primitive2D):
# l1 = p1 + s1*v1 # l1 = p1 + s1*v1
# l2 = p2 + s2*v2 # l2 = p2 + s2*v2
# delta = p2 - p1 = s2*v2 - s1*v1 # at intersection l1 = l2
# delta x v1 = s2*v2 x v1 = s2 * - v1 x v2 # p2 + s2*v2 = p1 + s1*v1
# delta x v2 = s1*v1 x v2 = s1 * v1 x v2 # delta = p2 - p1 = s1*v1 - s2*v2
# delta x v1 = - s2 * v2 x v1 = s2 * v1 x v2
if l1.is_parallel(l2): # delta x v2 = s1 * v1 x v2
test, cross = l1.is_parallel(l2, cross=True)
if test:
return (None, None) return (None, None)
else: else:
denominator = 1. / l1.v.cross(l2.v) denominator = 1. / cross
delta = l2.p - l1.p delta = l2.p - l1.p
s1 = delta.cross(l2.v) * denominator s1 = delta.cross(l2.v) * denominator
s2 = delta.cross(l1.v) * -denominator s2 = delta.cross(l1.v) * denominator
return (s1, s2) return (s1, s2)
############################################## ##############################################
...@@ -166,11 +178,11 @@ class Line2D(Primitive2D): ...@@ -166,11 +178,11 @@ class Line2D(Primitive2D):
"""Return the intersection Point between self and other""" """Return the intersection Point between self and other"""
s0, s1 = self.intersection_abscissae(other) s1, s2 = self.intersection_abscissae(other)
if s0 is None: if s1 is None:
return None return None
else: else:
return self.point_at_s(s0) return self.interpolate(s1)
############################################## ##############################################
...@@ -191,9 +203,24 @@ class Line2D(Primitive2D): ...@@ -191,9 +203,24 @@ class Line2D(Primitive2D):
"""Return the distance of a point to the line""" """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 delta = point - self.p
d = delta.deviation_with(self.v) d = delta.deviation_with(self.v)
return d 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 @@ ...@@ -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: class Primitive:
"""Base class for geometric 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 raise NotImplementedError
############################################## ##############################################
def bounding_box(self): @property
# Fixme: infinite primitive 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 raise NotImplementedError
def __len__(self):
return self.number_of_points
############################################## ##############################################
@property @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 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): def reverse(self):
return 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: class Primitive2DMixin:
__dimension__ = 2
# __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 @property
def is_reversable(self): def is_reversible(self):
return True True
############################################## ##############################################
def reverse(self): @property
def reversed_points(self):
raise NotImplementedError raise NotImplementedError
# return reversed(list(self.points))
##############################################
def reverse(self):
return self.__class__(*self.reversed_points)
############################################## ##############################################
...@@ -78,8 +187,244 @@ class ReversablePrimitiveMixin: ...@@ -78,8 +187,244 @@ class ReversablePrimitiveMixin:
def end_point(self): def end_point(self):
raise NotImplementedError 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): def __getitem__(self, _slice):
for point in self.start_point, self.start_point: return self._points[_slice]
yield point
####################################################################################################
#
# 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 @@ ...@@ -21,17 +21,18 @@
#################################################################################################### ####################################################################################################
# from .Interpolation import interpolate_two_points # from .Interpolation import interpolate_two_points
from .BoundingBox import bounding_box_from_points
from .Line import Line2D from .Line import Line2D
from .Primitive import Primitive2D, ReversablePrimitiveMixin from .Primitive import Primitive2P, Primitive2DMixin
from .Triangle import triangle_orientation from .Triangle import triangle_orientation
from .Vector import Vector2D 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): ...@@ -39,49 +40,7 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
"""Construct a :class:`Segment2D` between two points.""" """Construct a :class:`Segment2D` between two points."""
self._p0 = Vector2D(p0) Primitive2P.__init__(self, p0, p1)
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
############################################## ##############################################
...@@ -100,19 +59,23 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin): ...@@ -100,19 +59,23 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
############################################## ##############################################
@property
def cross_product(self):
return self._p0.cross(self._p1)
##############################################
def to_line(self): def to_line(self):
return Line2D.from_two_points(self._p1, self._p0) return Line2D.from_two_points(self._p1, self._p0)
############################################## ##############################################
def point_at_t(self, t): point_at_t = Primitive2P.interpolate
# return interpolate_two_points(self._p0, self._p1)
return self._p0 * (1 - t) + self._p1 * t
############################################## ##############################################
def intersect(self, segment2): def intersect_with(self, segment2):
"""Checks if the line segments intersect. """Checks if the line segments intersect.
return 1 if there is an intersection return 1 if there is an intersection
...@@ -131,3 +94,69 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin): ...@@ -131,3 +94,69 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
return (((ccw11 * ccw12 < 0) and (ccw21 * ccw22 < 0)) return (((ccw11 * ccw12 < 0) and (ccw21 * ccw22 < 0))
# one ccw value is zero to detect an intersection # one ccw value is zero to detect an intersection
or (ccw11 * ccw12 * ccw21 * ccw22 == 0)) 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: ...@@ -80,6 +80,11 @@ class Transformation:
############################################## ##############################################
def to_list(self):
return list(self._m.flat)
##############################################
def same_dimension(self, other): def same_dimension(self, other):
return self.__size__ == other.dimension return self.__size__ == other.dimension
...@@ -129,7 +134,7 @@ class Transformation2D(Transformation): ...@@ -129,7 +134,7 @@ class Transformation2D(Transformation):
@classmethod @classmethod
def Scale(cls, x_scale, y_scale): 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): ...@@ -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 @classmethod
def RotationAt(cls, center, angle): def RotationAt(cls, center, angle):
...@@ -191,6 +187,33 @@ class AffineTransformation(Transformation): ...@@ -191,6 +187,33 @@ class AffineTransformation(Transformation):
def translation_part(self): def translation_part(self):
return self._m[:self.__dimension__,-1] 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): def __mul__(self, obj):
...@@ -206,12 +229,6 @@ class AffineTransformation(Transformation): ...@@ -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 # 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*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*m*(1-c) + n*s , m*m*(1-c) + c , n*m*(1-c) - l*s
......
...@@ -20,6 +20,13 @@ ...@@ -20,6 +20,13 @@
#################################################################################################### ####################################################################################################
import math
from .Primitive import Primitive3P, Primitive2DMixin
from .Line import Line2D
####################################################################################################
def triangle_orientation(p0, p1, p2): def triangle_orientation(p0, p1, p2):
"""Return the triangle orientation defined by the three points.""" """Return the triangle orientation defined by the three points."""
...@@ -47,3 +54,235 @@ def triangle_orientation(p0, p1, p2): ...@@ -47,3 +54,235 @@ def triangle_orientation(p0, p1, p2):
# p1 is between p0 and p2 # p1 is between p0 and p2
else: else:
return 1 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)
)