480 lines
16 KiB
Python
480 lines
16 KiB
Python
#!/usr/bin/env python
|
|
from __future__ import division
|
|
import inkex
|
|
import simplestyle
|
|
|
|
|
|
from math import *
|
|
from collections import namedtuple
|
|
|
|
|
|
#Note: keep in mind that SVG coordinates start in the top-left corner i.e. with an inverted y-axis
|
|
|
|
errormsg = inkex.errormsg
|
|
debug = inkex.debug
|
|
|
|
default_style = simplestyle.formatStyle(
|
|
{'stroke': '#000000',
|
|
'stroke-width': '1',
|
|
'fill': 'none'
|
|
})
|
|
|
|
groove_style = simplestyle.formatStyle(
|
|
{'stroke': '#0000FF',
|
|
'stroke-width': '1',
|
|
'fill': 'none'
|
|
})
|
|
|
|
mark_style = simplestyle.formatStyle(
|
|
{'stroke': '#00FF00',
|
|
'stroke-width': '1',
|
|
'fill': 'none'
|
|
})
|
|
|
|
def draw_rectangle(parent, w, h, x, y, rx=0, ry=0, style=default_style):
|
|
attribs = {
|
|
'style': style,
|
|
'height': str(h),
|
|
'width': str(w),
|
|
'x': str(x),
|
|
'y': str(y)
|
|
}
|
|
if rx != 0 and ry != 0:
|
|
attribs['rx'] = str(rx)
|
|
attribs['ry'] = str(ry)
|
|
inkex.etree.SubElement(parent, inkex.addNS('rect', 'svg'), attribs)
|
|
|
|
def draw_ellipse(parent, rx, ry, center, start_end=(0, 2*pi), style=default_style, transform=''):
|
|
ell_attribs = {'style': style,
|
|
inkex.addNS('cx', 'sodipodi'): str(center.x),
|
|
inkex.addNS('cy', 'sodipodi'): str(center.y),
|
|
inkex.addNS('rx', 'sodipodi'): str(rx),
|
|
inkex.addNS('ry', 'sodipodi'): str(ry),
|
|
inkex.addNS('start', 'sodipodi'): str(start_end[0]),
|
|
inkex.addNS('end', 'sodipodi'): str(start_end[1]),
|
|
inkex.addNS('open', 'sodipodi'): 'true', #all ellipse sectors we will draw are open
|
|
inkex.addNS('type', 'sodipodi'): 'arc',
|
|
'transform': transform
|
|
}
|
|
inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), ell_attribs)
|
|
|
|
|
|
def draw_arc(parent, rx, ry, x_axis_rot, style=default_style):
|
|
arc_attribs = {'style': style,
|
|
'rx': str(rx),
|
|
'ry': str(ry),
|
|
'x-axis-rotation': str(x_axis_rot),
|
|
'large-arc': '',
|
|
'sweep': '',
|
|
'x': '',
|
|
'y': ''
|
|
}
|
|
#name='part'
|
|
style = {'stroke': '#000000', 'fill': 'none'}
|
|
drw = {'style':simplestyle.formatStyle(style),inkex.addNS('label','inkscape'):name,'d':XYstring}
|
|
inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), drw)
|
|
inkex.addNS('', 'svg')
|
|
|
|
def draw_text(parent, coordinate, txt, style=default_style):
|
|
text = inkex.etree.Element(inkex.addNS('text', 'svg'))
|
|
text.text = txt
|
|
text.set('x', str(coordinate.x))
|
|
text.set('y', str(coordinate.y))
|
|
style = {'text-align': 'center', 'text-anchor': 'middle'}
|
|
text.set('style', simplestyle.formatStyle(style))
|
|
parent.append(text)
|
|
|
|
#draw an SVG line segment between the given (raw) points
|
|
def draw_line(parent, start, end, style = default_style):
|
|
line_attribs = {'style': style,
|
|
'd': 'M '+str(start.x)+','+str(start.y)+' L '+str(end.x)+','+str(end.y)}
|
|
|
|
inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), line_attribs)
|
|
|
|
def layer(parent, layer_name):
|
|
layer = inkex.etree.SubElement(parent, 'g')
|
|
layer.set(inkex.addNS('label', 'inkscape'), layer_name)
|
|
layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
|
|
return layer
|
|
|
|
def group(parent):
|
|
return inkex.etree.SubElement(parent, 'g')
|
|
|
|
|
|
class IntersectionError(ValueError):
|
|
"""Raised when two lines do not intersect."""
|
|
|
|
def on_segment(pt, start, end):
|
|
"""Check if pt is between start and end. The three points are presumed to be collinear."""
|
|
pt -= start
|
|
end -= start
|
|
ex, ey = end.x, end.y
|
|
px, py = pt.x, pt.y
|
|
px *= cmp(ex, 0)
|
|
py *= cmp(ey, 0)
|
|
return px >= 0 and px <= abs(ex) and py >= 0 and py <= abs(ey)
|
|
|
|
def intersection (s1, e1, s2, e2, on_segments = True):
|
|
D = (s1.x - e1.x) * (s2.y - e2.y) - (s1.y - e1.y) * (s2.x - e2.x)
|
|
if D == 0:
|
|
raise IntersectionError("Lines from {s1} to {e1} and {s2} to {e2} are parallel")
|
|
N1 = s1.x * e1.y - s1.y * e1.x
|
|
N2 = s2.x * e2.y - s2.y * e2.x
|
|
I = ((s2 - e2) * N1 - (s1 - e1) * N2) / D
|
|
|
|
if on_segments and not (on_segment(I, s1, e1) and on_segment(I, s2, e2)):
|
|
raise IntersectionError("Intersection {0} is not on line segments [{1} -> {2}] [{3} -> {4}]".format(I, s1, e1, s2, e2))
|
|
return I
|
|
|
|
|
|
def inner_product(a, b):
|
|
return a.x * b.x + a.y * b.y
|
|
|
|
class Coordinate:
|
|
def __init__(self, x, y):
|
|
self.x = float(x)
|
|
self.y = float(y)
|
|
|
|
@property
|
|
def t(self):
|
|
return atan2(self.y, self.x)
|
|
|
|
#@t.setter
|
|
#def t(self, value):
|
|
|
|
@property
|
|
def r(self):
|
|
return hypot(self.x, self.y)
|
|
|
|
#@r.setter
|
|
#def r(self, value):
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
def __str__(self):
|
|
return "(%f, %f)" % (self.x, self.y)
|
|
|
|
def __eq__(self, other):
|
|
return self.x == other.x and self.y == other.y
|
|
|
|
def __add__(self, other):
|
|
return Coordinate(self.x + other.x, self.y + other.y)
|
|
|
|
def __sub__(self, other):
|
|
return Coordinate(self.x - other.x, self.y - other.y)
|
|
|
|
def __mul__(self, factor):
|
|
return Coordinate(self.x * factor, self.y * factor)
|
|
|
|
def __rmul__(self, other):
|
|
return self * other
|
|
|
|
def __div__(self, quotient):
|
|
return Coordinate(self.x / quotient, self.y / quotient)
|
|
|
|
def __truediv__(self, quotient):
|
|
return self.__div__(quotient)
|
|
|
|
|
|
|
|
class Effect(inkex.Effect):
|
|
"""
|
|
|
|
"""
|
|
def __init__(self, options=None):
|
|
inkex.Effect.__init__(self)
|
|
self.knownUnits = ['in', 'pt', 'px', 'mm', 'cm', 'm', 'km', 'pc', 'yd', 'ft']
|
|
|
|
if options != None:
|
|
for opt in options:
|
|
if len(opt) == 2:
|
|
self.OptionParser.add_option('--' + opt[0], type = opt[1], dest = opt[0])
|
|
else:
|
|
self.OptionParser.add_option('--' + opt[0], type = opt[1], dest = opt[0],default = opt[2], help = opt[3])
|
|
|
|
try:
|
|
inkex.Effect.unittouu # unitouu has moved since Inkscape 0.91
|
|
except AttributeError:
|
|
try:
|
|
def unittouu(self, unit):
|
|
return inkex.unittouu(unit)
|
|
except AttributeError:
|
|
pass
|
|
|
|
def effect(self):
|
|
"""
|
|
|
|
"""
|
|
pass
|
|
|
|
|
|
def _format_1st(command, is_absolute):
|
|
return command.upper() if is_absolute else command.lower()
|
|
|
|
class Path:
|
|
def __init__(self):
|
|
self.nodes = []
|
|
|
|
def move_to(self, coord, absolute=False):
|
|
self.nodes.append("{0} {1} {2}".format(_format_1st('m', absolute), coord.x, coord.y))
|
|
|
|
def line_to(self, coord, absolute=False):
|
|
self.nodes.append("{0} {1} {2}".format(_format_1st('l', absolute), coord.x, coord.y))
|
|
|
|
def h_line_to(self, dist, absolute=False):
|
|
self.nodes.append("{0} {1}".format(_format_1st('h', absolute), dist))
|
|
|
|
def v_line_to(self, dist, absolute=False):
|
|
self.nodes.append("{0} {1}".format(_format_1st('v', absolute), dist))
|
|
|
|
def arc_to(self, rx, ry, x, y, rotation=0, pos_sweep=True, large_arc=False, absolute=False):
|
|
self.nodes.append("{0} {1} {2} {3} {4} {5} {6} {7}".format(_format_1st('a', absolute), rx, ry, rotation, 1 if large_arc else 0, 1 if pos_sweep else 0, x, y))
|
|
|
|
def close(self):
|
|
self.nodes.append('z')
|
|
|
|
def path(self, parent, style):
|
|
attribs = {'style': style,
|
|
'd': ' '.join(self.nodes)}
|
|
inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), attribs)
|
|
|
|
def curve(parent, segments, style, closed=True):
|
|
#pathStr = 'M '+ segments[0]
|
|
pathStr = ' '.join(segments)
|
|
if closed:
|
|
pathStr += ' z'
|
|
attributes = {
|
|
'style': style,
|
|
'd': pathStr}
|
|
inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), attributes)
|
|
|
|
def remove_last(self):
|
|
self.nodes.pop()
|
|
|
|
|
|
PathPoint = namedtuple('PathPoint', 't coord tangent curvature c_dist')
|
|
|
|
class PathSegment():
|
|
|
|
def __init__(self):
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def lenth(self):
|
|
raise NotImplementedError
|
|
|
|
def subdivide(self, part_length):
|
|
raise NotImplementedError
|
|
# also need:
|
|
|
|
# find a way do do curvature dependent spacing
|
|
# - based on deviation from a standard radius?
|
|
# - or ratio between thickness and curvature?
|
|
#def point_at_distance(d):
|
|
# pass
|
|
|
|
|
|
class Line(PathSegment):
|
|
|
|
def __init__(self, start, end):
|
|
self.start = start
|
|
self.end = end
|
|
|
|
@property
|
|
def length(self):
|
|
return (self.end - self.start).r
|
|
|
|
def subdivide(self, part_length, start_offset=0): # note: start_offset should be smaller than part_length
|
|
nr_parts = int((self.length - start_offset) // part_length)
|
|
k_o = start_offset / self.length
|
|
k2t = lambda k : k_o + k * part_length / self.length
|
|
pp = lambda t : PathPoint(t, self.start + t * (self.end - self.start), self.end - self.start, 0, t * self.length)
|
|
points = [pp(k2t(k)) for k in range(nr_parts + 1)]
|
|
return(points, self.length - points[-1].c_dist)
|
|
|
|
|
|
|
|
class BezierCurve(PathSegment):
|
|
nr_points = 10
|
|
def __init__(self, P): # number of points is limited to 3 or 4
|
|
|
|
if len(P) == 3: # quadratic
|
|
self.B = lambda t : (1 - t)**2 * P[0] + 2 * (1 - t) * t * P[1] + t**2 * P[2]
|
|
Bd = lambda t : 2 * (1 - t) * (P[1] - P[0]) + 2 * t * (P[2] - P[1])
|
|
Bdd = lambda t : 2 * (P[2] - 2 * P[1] + P[0])
|
|
elif len(P) == 4: #cubic
|
|
self.B = lambda t : (1 - t)**3 * P[0] + 3 * (1 - t)**2 * t * P[1] + 3 * (1 - t) * t**2 * P[2] + t**3 * P[3]
|
|
Bd = lambda t : 3 * (1 - t)**2 * (P[1] - P[0]) + 6 * (1 - t) * t * (P[2] - P[1]) + 3 * t**2 * (P[3] - P[2])
|
|
Bdd = lambda t : 6 * (1 - t) * (P[2] - 2 * P[1] + P[0]) + 6 * t * (P[3] - 2 * P[2] + P[1])
|
|
|
|
self.tangent = lambda t : Bd(t)
|
|
self.curvature = lambda t : (Bd(t).x * Bdd(t).y - Bd(t).y * Bdd(t).x) / hypot(Bd(t).x, Bd(t).y)**3
|
|
|
|
self.distances = [0] # cumulative distances for each 't'
|
|
prev_pt = self.B(0)
|
|
for i in range(self.nr_points):
|
|
t = (i + 1) / self.nr_points
|
|
pt = self.B(t)
|
|
self.distances.append(self.distances[-1] + hypot(prev_pt.x - pt.x, prev_pt.y - pt.y))
|
|
prev_pt = pt
|
|
self.length = self.distances[-1]
|
|
|
|
@classmethod
|
|
def quadratic(cls, start, c, end):
|
|
bezier = cls()
|
|
|
|
@classmethod
|
|
def cubic(cls, start, c1, c2, end):
|
|
bezier = cls()
|
|
|
|
def __make_eq__(self):
|
|
pass
|
|
|
|
@property
|
|
def length(self):
|
|
return self.length
|
|
|
|
def subdivide(self, part_length, start_offset=0):
|
|
nr_parts = int((self.length - start_offset) / part_length + 10E-10)
|
|
print "NR PARTS:", nr_parts, self.length, start_offset, part_length, int(self.length / part_length), self.length - 2 * part_length
|
|
k_o = start_offset / self.length
|
|
k2t = lambda k : k_o + k * part_length / self.length
|
|
points = [self.pathpoint_at_t(k2t(k)) for k in range(nr_parts + 1)]
|
|
return(points, self.length - points[-1].c_dist)
|
|
|
|
|
|
def pathpoint_at_t(self, t):
|
|
"""pathpoint on the curve from t=0 to point at t."""
|
|
step = 1 / self.nr_points
|
|
pt_idx = int(t / step)
|
|
#print "index", pt_idx, self.distances[pt_idx]
|
|
length = self.distances[pt_idx]
|
|
ip_fact = (t - pt_idx * step) / step
|
|
|
|
if ip_fact > 0 and t < 1: # not a perfect match, need to interpolate
|
|
length += ip_fact * (self.distances[pt_idx + 1] - self.distances[pt_idx])
|
|
|
|
return PathPoint(t, self.B(t), self.tangent(t), self.curvature(t), length)
|
|
|
|
|
|
def t_at_length(self, length):
|
|
"""interpolated t where the curve is at the given length"""
|
|
if length == self.length:
|
|
return 1
|
|
i_small = 0
|
|
i_big = self.nr_points + 1
|
|
|
|
while i_big - i_small > 1: # binary search
|
|
i_half = i_small + (i_big - i_small) // 2
|
|
if self.distances[i_half] <= length:
|
|
i_small = i_half
|
|
else:
|
|
i_big = i_half
|
|
|
|
small_dist = self.distances[i_small]
|
|
return i_small / self.nr_points + (length - small_dist) * (self.distances[i_big] - small_dist) # interpolated length
|
|
|
|
class Ellipse():
|
|
nrPoints = 1000 #used for piecewise linear circumference calculation (ellipse circumference is tricky to calculate)
|
|
# approximate circumfere: c = pi * (3 * (a + b) - sqrt(10 * a * b + 3 * (a ** 2 + b ** 2)))
|
|
|
|
def __init__(self, w, h):
|
|
self.h = h
|
|
self.w = w
|
|
EllipsePoint = namedtuple('EllipsePoint', 'angle coord cDist')
|
|
self.ellData = [EllipsePoint(0, Coordinate(w/2, 0), 0)] # (angle, x, y, cumulative distance from angle = 0)
|
|
angle = 0
|
|
self.angleStep = 2 * pi / self.nrPoints
|
|
#note: the render angle (ra) corresponds to the angle from the ellipse center (ca) according to:
|
|
# ca = atan(w/h * tan(ra))
|
|
for i in range(self.nrPoints):
|
|
angle += self.angleStep
|
|
prev = self.ellData[-1]
|
|
x, y = w / 2 * cos(angle), h / 2 * sin(angle)
|
|
self.ellData.append(EllipsePoint(angle, Coordinate(x, y), prev.cDist + hypot(prev.coord.x - x, prev.coord.y - y)))
|
|
self.circumference = self.ellData[-1].cDist
|
|
#inkex.debug("circ: %d" % self.circumference)
|
|
|
|
def rAngle(self, a):
|
|
"""Convert an angle measured from ellipse center to the angle used to generate ellData (used for lookups)"""
|
|
cf = 0
|
|
if a > pi / 2:
|
|
cf = pi
|
|
if a > 3 * pi / 2:
|
|
cf = 2 * pi
|
|
return atan(self.w / self.h * tan(a)) + cf
|
|
|
|
def coordinateFromAngle(self, angle):
|
|
"""Coordinate of the point at angle."""
|
|
return Coordinate(self.w / 2 * cos(angle), self.h / 2 * sin(angle))
|
|
|
|
def notchCoordinate(self, angle, notchHeight):
|
|
"""Coordinate for a notch at the given angle. The notch is perpendicular to the ellipse."""
|
|
angle %= (2 * pi)
|
|
#some special cases to avoid divide by zero:
|
|
if angle == 0:
|
|
return (0, Coordinate(self.w / 2 + notchHeight, 0))
|
|
elif angle == pi:
|
|
return (pi, Coordinate(-self.w / 2 - notchHeight, 0))
|
|
elif angle == pi / 2:
|
|
return(pi / 2, doc.Coordinate(0, self.h / 2 + notchHeight))
|
|
elif angle == 3 * pi / 2:
|
|
return(3 * pi / 2, Coordinate(0, -self.h / 2 - notchHeight))
|
|
|
|
x = self.w / 2 * cos(angle)
|
|
derivative = self.h / self.w * -x / sqrt((self.w / 2) ** 2 - x ** 2)
|
|
if angle > pi:
|
|
derivative = -derivative
|
|
|
|
normal = -1 / derivative
|
|
nAngle = atan(normal)
|
|
if angle > pi / 2 and angle < 3 * pi / 2:
|
|
nAngle += pi
|
|
|
|
nCoordinate = self.coordinateFromAngle(angle) + Coordinate(cos(nAngle), sin(nAngle)) * notchHeight
|
|
return nCoordinate
|
|
|
|
|
|
def distFromAngles(self, a1, a2):
|
|
"""Distance accross the surface from point at angle a2 to point at angle a2. Measured in CCW sense."""
|
|
i1 = int(self.rAngle(a1) / self.angleStep)
|
|
p1 = self.rAngle(a1) % self.angleStep
|
|
l1 = self.ellData[i1 + 1].cDist - self.ellData[i1].cDist
|
|
i2 = int(self.rAngle(a2) / self.angleStep)
|
|
p2 = self.rAngle(a2) % self.angleStep
|
|
l2 = self.ellData[i2 + 1].cDist - self.ellData[i2].cDist
|
|
if a1 <= a2:
|
|
len = self.ellData[i2].cDist - self.ellData[i1].cDist + l2 * p2 - l1 * p1
|
|
else:
|
|
len = self.circumference + self.ellData[i2].cDist - self.ellData[i1].cDist
|
|
return len
|
|
|
|
def angleFromDist(self, startAngle, relDist):
|
|
"""Returns the angle that you get when starting at startAngle and moving a distance (dist) in CCW direction"""
|
|
si = int(self.rAngle(startAngle) / self.angleStep)
|
|
p = self.rAngle(startAngle) % self.angleStep
|
|
|
|
l = self.ellData[si + 1].cDist - self.ellData[si].cDist
|
|
|
|
startDist = self.ellData[si].cDist + p * l
|
|
|
|
absDist = relDist + startDist
|
|
|
|
if absDist > self.ellData[-1].cDist: # wrap around zero angle
|
|
absDist -= self.ellData[-1].cDist
|
|
|
|
iMin = 0
|
|
iMax = self.nrPoints
|
|
count = 0
|
|
while iMax - iMin > 1: # binary search
|
|
count += 1
|
|
iHalf = iMin + (iMax - iMin) // 2
|
|
if self.ellData[iHalf].cDist < absDist:
|
|
iMin = iHalf
|
|
else:
|
|
iMax = iHalf
|
|
|
|
stepDist = self.ellData[iMax].cDist - self.ellData[iMin].cDist
|
|
return self.ellData[iMin].angle + self.angleStep * (absDist - self.ellData[iMin].cDist)/stepDist
|