-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pcb): add modified version of svg2mod
Includes Python 3 fixes and some of the changes from: mtl/svg2mod#26
- Loading branch information
Showing
9 changed files
with
2,886 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__pycache__ |
Empty file.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
SVG parser library | ||
================== | ||
|
||
This is a SVG parser library written in Python. | ||
([see here](https://github.com/cjlano/svg])) | ||
|
||
Capabilities: | ||
- Parse SVG XML | ||
- apply any transformation (svg transform) | ||
- Explode SVG Path into basic elements (Line, Bezier, ...) | ||
- Interpolate SVG Path as a series of segments | ||
- Able to simplify segments given a precision using Ramer-Douglas-Peucker algorithm | ||
|
||
Not (yet) supported: | ||
- SVG Path Arc ('A') | ||
- Non-linear transformation drawing (SkewX, ...) | ||
|
||
License: GPLv2+ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .svg import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
#__all__ = ['geometry', 'svg'] | ||
|
||
from .svg import * | ||
|
||
def parse(filename): | ||
f = svg.Svg(filename) | ||
return f | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]] |
Oops, something went wrong.