svg2gcode/svg/geometry.py

changeset 2
660ce16822a9
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svg2gcode/svg/geometry.py	Sat Nov 07 13:33:12 2015 +0100
@@ -0,0 +1,343 @@
+# Copyright (C) 2013 -- CJlano < cjlano @ free.fr >
+
+# 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 2 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+'''
+This module contains all the geometric classes and functions not directly
+related to SVG parsing. It can be reused outside the scope of SVG.
+'''
+
+import math
+import numbers
+import operator
+
+class Point:
+    def __init__(self, x=None, y=None):
+        '''A Point is defined either by a tuple/list of length 2 or
+           by 2 coordinates
+        >>> Point(1,2)
+        (1.000,2.000)
+        >>> Point((1,2))
+        (1.000,2.000)
+        >>> Point([1,2])
+        (1.000,2.000)
+        >>> Point('1', '2')
+        (1.000,2.000)
+        >>> Point(('1', None))
+        (1.000,0.000)
+        '''
+        if (isinstance(x, tuple) or isinstance(x, list)) and len(x) == 2:
+            x,y = x
+
+        # Handle empty parameter(s) which should be interpreted as 0
+        if x is None: x = 0
+        if y is None: y = 0
+
+        try:
+            self.x = float(x)
+            self.y = float(y)
+        except:
+            raise TypeError("A Point is defined by 2 numbers or a tuple")
+
+    def __add__(self, other):
+        '''Add 2 points by adding coordinates.
+        Try to convert other to Point if necessary
+        >>> Point(1,2) + Point(3,2)
+        (4.000,4.000)
+        >>> Point(1,2) + (3,2)
+        (4.000,4.000)'''
+        if not isinstance(other, Point):
+            try: other = Point(other)
+            except: return NotImplemented
+        return Point(self.x + other.x, self.y + other.y)
+
+    def __sub__(self, other):
+        '''Substract two Points.
+        >>> Point(1,2) - Point(3,2)
+        (-2.000,0.000)
+        '''
+        if not isinstance(other, Point):
+            try: other = Point(other)
+            except: return NotImplemented
+        return Point(self.x - other.x, self.y - other.y)
+
+    def __mul__(self, other):
+        '''Multiply a Point with a constant.
+        >>> 2 * Point(1,2)
+        (2.000,4.000)
+        >>> Point(1,2) * Point(1,2) #doctest:+IGNORE_EXCEPTION_DETAIL
+        Traceback (most recent call last):
+            ...
+        TypeError:
+        '''
+        if not isinstance(other, numbers.Real):
+            return NotImplemented
+        return Point(self.x * other, self.y * other)
+    def __rmul__(self, other):
+        return self.__mul__(other)
+
+    def __eq__(self, other):
+        '''Test equality
+        >>> Point(1,2) == (1,2)
+        True
+        >>> Point(1,2) == Point(2,1)
+        False
+        '''
+        if not isinstance(other, Point):
+            try: other = Point(other)
+            except: return NotImplemented
+        return (self.x == other.x) and (self.y == other.y)
+
+    def __repr__(self):
+        return '(' + format(self.x,'.3f') + ',' + format( self.y,'.3f') + ')'
+
+    def __str__(self):
+        return self.__repr__();
+
+    def coord(self):
+        '''Return the point tuple (x,y)'''
+        return (self.x, self.y)
+
+    def length(self):
+        '''Vector length, Pythagoras theorem'''
+        return math.sqrt(self.x ** 2 + self.y ** 2)
+
+    def rot(self, angle):
+        '''Rotate vector [Origin,self] '''
+        if not isinstance(angle, Angle):
+            try: angle = Angle(angle)
+            except: return NotImplemented
+        x = self.x * angle.cos - self.y * angle.sin
+        y = self.x * angle.sin + self.y * angle.cos
+        return Point(x,y)
+
+
+class Angle:
+    '''Define a trigonometric angle [of a vector] '''
+    def __init__(self, arg):
+        if isinstance(arg, numbers.Real):
+        # We precompute sin and cos for rotations
+            self.angle = arg
+            self.cos = math.cos(self.angle)
+            self.sin = math.sin(self.angle)
+        elif isinstance(arg, Point):
+        # Point angle is the trigonometric angle of the vector [origin, Point]
+            pt = arg
+            try:
+                self.cos = pt.x/pt.length()
+                self.sin = pt.y/pt.length()
+            except ZeroDivisionError:
+                self.cos = 1
+                self.sin = 0
+
+            self.angle = math.acos(self.cos)
+            if self.sin < 0:
+                self.angle = -self.angle
+        else:
+            raise TypeError("Angle is defined by a number or a Point")
+
+    def __neg__(self):
+        return Angle(Point(self.cos, -self.sin))
+
+class Segment:
+    '''A segment is an object defined by 2 points'''
+    def __init__(self, start, end):
+        self.start = start
+        self.end = end
+
+    def __str__(self):
+        return 'Segment from ' + str(self.start) + ' to ' + str(self.end)
+
+    def segments(self, precision=0):
+        ''' Segments is simply the segment start -> end'''
+        return [self.start, self.end]
+
+    def length(self):
+        '''Segment length, Pythagoras theorem'''
+        s = self.end - self.start
+        return math.sqrt(s.x ** 2 + s.y ** 2)
+
+    def pdistance(self, p):
+        '''Perpendicular distance between this Segment and a given Point p'''
+        if not isinstance(p, Point):
+            return NotImplemented
+
+        if self.start == self.end:
+        # Distance from a Point to another Point is length of a segment
+            return Segment(self.start, p).length()
+
+        s = self.end - self.start
+        if s.x == 0:
+        # Vertical Segment => pdistance is the difference of abscissa
+            return abs(self.start.x - p.x)
+        else:
+        # That's 2-D perpendicular distance formulae (ref: Wikipedia)
+            slope = s.y/s.x
+            # intercept: Crossing with ordinate y-axis
+            intercept = self.start.y - (slope * self.start.x)
+            return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1)
+
+
+    def bbox(self):
+        xmin = min(self.start.x, self.end.x)
+        xmax = max(self.start.x, self.end.x)
+        ymin = min(self.start.y, self.end.y)
+        ymax = max(self.start.y, self.end.y)
+
+        return (Point(xmin,ymin),Point(xmax,ymax))
+
+    def transform(self, matrix):
+        self.start = matrix * self.start
+        self.end = matrix * self.end
+
+    def scale(self, ratio):
+        self.start *= ratio
+        self.end *= ratio
+    def translate(self, offset):
+        self.start += offset
+        self.end += offset
+    def rotate(self, angle):
+        self.start = self.start.rot(angle)
+        self.end = self.end.rot(angle)
+
+class Bezier:
+    '''Bezier curve class
+       A Bezier curve is defined by its control points
+       Its dimension is equal to the number of control points
+       Note that SVG only support dimension 3 and 4 Bezier curve, respectively
+       Quadratic and Cubic Bezier curve'''
+    def __init__(self, pts):
+        self.pts = list(pts)
+        self.dimension = len(pts)
+
+    def __str__(self):
+        return 'Bezier' + str(self.dimension) + \
+                ' : ' + ", ".join([str(x) for x in self.pts])
+
+    def control_point(self, n):
+        if n >= self.dimension:
+            raise LookupError('Index is larger than Bezier curve dimension')
+        else:
+            return self.pts[n]
+
+    def rlength(self):
+        '''Rough Bezier length: length of control point segments'''
+        pts = list(self.pts)
+        l = 0.0
+        p1 = pts.pop()
+        while pts:
+            p2 = pts.pop()
+            l += Segment(p1, p2).length()
+            p1 = p2
+        return l
+
+    def bbox(self):
+        return self.rbbox()
+
+    def rbbox(self):
+        '''Rough bounding box: return the bounding box (P1,P2) of the Bezier
+        _control_ points'''
+        xmin = min([p.x for p in self.pts])
+        xmax = max([p.x for p in self.pts])
+        ymin = min([p.y for p in self.pts])
+        ymax = max([p.y for p in self.pts])
+
+        return (Point(xmin,ymin), Point(xmax,ymax))
+
+    def segments(self, precision=0):
+        '''Return a polyline approximation ("segments") of the Bezier curve
+           precision is the minimum significative length of a segment'''
+        segments = []
+        # n is the number of Bezier points to draw according to precision
+        if precision != 0:
+            n = int(self.rlength() / precision) + 1
+        else:
+            n = 1000
+        if n < 10: n = 10
+        if n > 1000 : n = 1000
+
+        for t in range(0, n+1):
+            segments.append(self._bezierN(float(t)/n))
+        return segments
+
+    def _bezier1(self, p0, p1, t):
+        '''Bezier curve, one dimension
+        Compute the Point corresponding to a linear Bezier curve between
+        p0 and p1 at "time" t '''
+        pt = p0 + t * (p1 - p0)
+        return pt
+
+    def _bezierN(self, t):
+        '''Bezier curve, Nth dimension
+        Compute the point of the Nth dimension Bezier curve at "time" t'''
+        # We reduce the N Bezier control points by computing the linear Bezier
+        # point of each control point segment, creating N-1 control points
+        # until we reach one single point
+        res = list(self.pts)
+        # We store the resulting Bezier points in res[], recursively
+        for n in range(self.dimension, 1, -1):
+            # For each control point of nth dimension,
+            # compute linear Bezier point a t
+            for i in range(0,n-1):
+                res[i] = self._bezier1(res[i], res[i+1], t)
+        return res[0]
+
+    def transform(self, matrix):
+        self.pts = [matrix * x for x in self.pts]
+
+    def scale(self, ratio):
+        self.pts = [x * ratio for x in self.pts]
+    def translate(self, offset):
+        self.pts = [x + offset for x in self.pts]
+    def rotate(self, angle):
+        self.pts = [x.rot(angle) for x in self.pts]
+
+class MoveTo:
+    def __init__(self, dest):
+        self.dest = dest
+
+    def bbox(self):
+        return (self.dest, self.dest)
+
+    def transform(self, matrix):
+        self.dest = matrix * self.dest
+
+    def scale(self, ratio):
+        self.dest *= ratio
+    def translate(self, offset):
+        self.dest += offset
+    def rotate(self, angle):
+        self.dest = self.dest.rot(angle)
+
+
+def simplify_segment(segment, epsilon):
+    '''Ramer-Douglas-Peucker algorithm'''
+    if len(segment) < 3 or epsilon <= 0:
+        return segment[:]
+
+    l = Segment(segment[0], segment[-1]) # Longest segment
+
+    # Find the furthest point from the segment
+    index, maxDist = max([(i, l.pdistance(p)) for i,p in enumerate(segment)],
+            key=operator.itemgetter(1))
+
+    if maxDist > epsilon:
+        # Recursively call with segment splited in 2 on its furthest point
+        r1 = simplify_segment(segment[:index+1], epsilon)
+        r2 = simplify_segment(segment[index:], epsilon)
+        # Remove redundant 'middle' Point
+        return r1[:-1] + r2
+    else:
+        return [segment[0], segment[-1]]

mercurial