Added guilloche creations

This commit is contained in:
Mario Voigt 2021-10-11 12:07:18 +02:00
parent f8cbca6a5c
commit 4d8d350d89
4 changed files with 1749 additions and 0 deletions

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Guilloche Contour</name>
<id>fablabchemnitz.de.guilloche_contour</id>
<param name="contourFunction" type="optiongroup" appearance="combo" gui-text="Function">
<option value="line">Line</option>
<option value="sin">Sin</option>
<option value="cos">Cos</option>
<option value="env1">Env1</option>
<option value="env2">Env2</option>
<option value="env3">Env3</option>
<option value="env4">Env4</option>
<option value="env5">Env5</option>
<option value="env6">Env6</option>
<option value="env7">Env7</option>
<option value="env8">Env8</option>
<option value="env9">Env9</option>
<option value="env10">Env10</option>
<option value="env11">Env11</option>
<option value="env12">Env12</option>
</param>
<param name="tab" type="notebook">
<page name="contour" gui-text="Contour">
<param name="frequency" type="int" min="1" max="100" gui-text="Frequency:">10</param>
<param name="amplitude" type="int" min="-15" max="15" gui-text="Amplitude:">1</param>
<param name="phaseOffset" type="int" min="-100" max="100" gui-text="Phase offset:">0</param>
<param name="offset" type="int" min="-100" max="100" gui-text="Offset:">0</param>
<param name="nodes" type="int" min="2" max="1000" gui-text="Number of nodes:">20</param>
<param name="remove" type="bool" gui-text="Remove control object">false</param>
<param name="strokeColor" type="color" gui-text="Stroke color"></param>
</page>
<page name="function" gui-text="Function">
<param name="amplitude1" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 1:">0.0</param>
<param name="phase1" type="int" min="-100" max="100" gui-text="Phase offset 1:">0</param>
<param name="amplitude2" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 2:">0.0</param>
<param name="phase2" type="int" min="-100" max="100" gui-text="Phase offset 2:">0</param>
<param name="amplitude3" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 3:">0.0</param>
<param name="phase3" type="int" min="-100" max="100" gui-text="Phase offset 3:">0</param>
<param name="amplitude4" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 4:">0.0</param>
<param name="phase4" type="int" min="-100" max="100" gui-text="Phase offset 4:">0</param>
<param name="amplitude5" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 5:">0.0</param>
<param name="phase5" type="int" min="-100" max="100" gui-text="Phase offset 5:">0</param>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">guilloche_contour.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,663 @@
#! /usr/bin/env python3
'''
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.
Quick description:
'''
# standard library
from math import *
from copy import deepcopy
# local library
import inkex
import pathmodifier
import cubicsuperpath
from inkex import paths
from lxml import etree
def getColorAndOpacity(longColor):
'''
Convert the long into a #rrggbb color value
Conversion back is A + B*256^1 + G*256^2 + R*256^3
'''
longColor = int(longColor)
if longColor < 0:
longColor = longColor & 0xFFFFFFFF
hexColor = hex(longColor)
lhc = len(hexColor)
hexOpacity = hexColor[lhc-2 : ]
hexColor = '#' + hexColor[2:-2].rjust(6, '0')
return (hexColor, hexOpacity)
def setColorAndOpacity(style, color, opacity):
declarations = style.split(';')
strokeOpacityInStyle = False
newOpacity = round((int(opacity, 16) / 255.0), 8)
for i,decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if (prop == 'stroke' and val != color):
declarations[i] = prop + ':' + color
if prop == 'stroke-opacity':
if val != newOpacity:
declarations[i] = prop + ':' + str(newOpacity)
strokeOpacityInStyle = True
if not strokeOpacityInStyle:
declarations.append('stroke-opacity' + ':' + str(newOpacity))
return ";".join(declarations)
def getSkeletonPath(d, offs):
'''
Recieves a current skeleton path and offset specified by the user if it's line.
Calculates new skeleton path to use for creating contour with given offset.
'''
if offs != 0:
comps = d.split()
if ((comps[2] == 'h' or comps[2] == 'H') and len(comps) == 4):
startPt = comps[1].split(',')
startX = float(startPt[0])
startY = float(startPt[1])
finalX = float(comps[3]) if comps[2] == 'H' else startX + float(comps[3])
if startX < finalX:
startY -= offs
else:
startY += offs
comps[1] = startPt[0] + ',' + str(startY)
elif ((comps[2] == 'v' or comps[2] == 'V') and len(comps) == 4):
startPt = comps[1].split(',')
startX = float(startPt[0])
startY = float(startPt[1])
finalY = float(comps[3]) if comps[2] == 'V' else startY + float(comps[3])
if startY < finalY:
startX += offs
else:
startX -= offs
comps[1] = str(startX) + ',' + startPt[1]
elif (comps[0] == 'M' and len(comps) == 3):
startPt = comps[1].split(',')
startX = float(startPt[0])
startY = float(startPt[1])
finalPt = comps[2].split(',')
finalX = float(finalPt[0])
finalY = float(finalPt[1])
if startX < finalX:
if (startY > finalY):
startX -= offs
finalX -= offs
else:
startX += offs
finalX += offs
startY -= offs
finalY -= offs
else:
if startY > finalY:
startX -= offs
finalX -= offs
else:
startX += offs
finalX += offs
startY += offs
finalY += offs
comps[1] = str(startX) + ',' + str(startY)
comps[2] = str(finalX) + ',' + str(finalY)
elif (comps[0] == 'm' and len(comps) == 3):
startPt = comps[1].split(',')
startX = float(startPt[0])
startY = float(startPt[1])
finalPt = comps[2].split(',')
dx = float(finalPt[0])
dy = float(finalPt[1])
finalX = startX + dx
finalY = startY + dy
if startX < finalX:
if startY > finalY:
startX -= offs
else:
startX += offs
startY -= offs
else:
if startY > finalY:
startX -= offs
else:
startX += offs
startY += offs
comps[1] = str(startX) + ',' + str(startY)
comps[2] = str(dx) + ',' + str(dy)
return paths.CubicSuperPath(paths.Path(' '.join(comps)))
return paths.CubicSuperPath(paths.Path(d))
def modifySkeletonPath(skelPath):
resPath = []
l = len(skelPath)
resPath += skelPath[0]
if l > 1:
for i in range(1, l):
if skelPath[i][0][1] == resPath[-1][1]:
skelPath[i][0][0] = resPath[-1][0]
del resPath[-1]
resPath += skelPath[i]
return resPath
def linearize(p, tolerance=0.001):
'''
This function receives a component of a 'cubicsuperpath' and returns two things:
The path subdivided in many straight segments, and an array containing the length of each segment.
'''
zero = 0.000001
i = 0
d = 0
lengths=[]
while i < len(p) - 1:
box = inkex.bezier.pointdistance(p[i][1], p[i][2])
box += inkex.bezier.pointdistance(p[i][2], p[i+1][0])
box += inkex.bezier.pointdistance(p[i+1][0], p[i+1][1])
chord = inkex.bezier.pointdistance(p[i][1], p[i+1][1])
if (box - chord) > tolerance:
b1, b2 = inkex.bezier.beziersplitatt([p[i][1], p[i][2], p[i + 1][0], p[i + 1][1]], 0.5)
p[i][2][0], p[i][2][1] = b1[1]
p[i + 1][0][0], p[i + 1][0][1] = b2[2]
p.insert(i + 1, [[b1[2][0], b1[2][1]], [b1[3][0], b1[3][1]], [b2[1][0], b2[1][1]]])
else:
d = (box + chord) / 2
lengths.append(d)
i += 1
new = [p[i][1] for i in range(0, len(p) - 1) if lengths[i] > zero]
new.append(p[-1][1])
lengths = [l for l in lengths if l > zero]
return (new, lengths)
def isSkeletonClosed(sklCmp):
requiredPrecision = 0.005
sctest1 = abs(sklCmp[0][0] - sklCmp[-1][0]) > requiredPrecision
sctest2 = abs(sklCmp[0][1] - sklCmp[-1][1]) > requiredPrecision
if sctest1 or sctest2:
return False
return True
def getPolygonCentroid(polygon):
x = 0
y = 0
n = len(polygon)
for vert in polygon:
x += vert[0]
y += vert[1]
x = x / n
y = y / n
return [x, y]
def getPoint(p1, p2, x, y):
x1 = p1[0]
y1 = p1[1]
x2 = p2[0]
y2 = p2[1]
a = (y1 - y2) / (x1 - x2)
b = y1 - a * x1
if x == None:
x = (y - b) / a
else:
y = a * x + b
return [x, y]
def getPtOnSeg(p1, p2, segLen, l):
if p1[0] == p2[0]:
return [p2[0], p2[1] - l] if p2[1] < p1[1] else [p2[0], p2[1] + l]
if p1[1] == p2[1]:
return [p2[0] - l, p2[1]] if p2[0] < p1[0] else [p2[0] + l, p2[1]]
dy = abs(p1[1] - p2[1])
angle = asin(dy / segLen)
dx = l * cos(angle)
x = p1[0] - dx if p1[0] > p2[0] else p1[0] + dx
return getPoint(p1, p2, x, None)
def drawfunction(nodes, width, fx):
# x-bounds of the plane
xstart = 0.0
xend = 2 * pi
# y-bounds of the plane
ybottom = -1.0
ytop = 1.0
# size and location of the plane on the canvas
height = 2
left = 15
bottom = 15 + height
# function specified by the user
try:
if fx != "":
f = eval('lambda x: ' + fx.strip('"'))
except SyntaxError:
return []
scalex = width / (xend - xstart)
xoff = left
# conver x-value to coordinate
coordx = lambda x: (x - xstart) * scalex + xoff
scaley = height / (ytop - ybottom)
yoff = bottom
# conver y-value to coordinate
coordy = lambda y: (ybottom - y) * scaley + yoff
# step is the distance between nodes on x
step = (xend - xstart) / (nodes - 1)
third = step / 3.0
# step used in calculating derivatives
ds = step * 0.001
# initialize function and derivative for 0;
# they are carried over from one iteration to the next, to avoid extra function calculations.
x0 = xstart
y0 = f(xstart)
# numerical derivative, using 0.001*step as the small differential
x1 = xstart + ds # Second point AFTER first point (Good for first point)
y1 = f(x1)
dx0 = (x1 - x0) / ds
dy0 = (y1 - y0) / ds
# path array
a = []
# Start curve
#a.append(['M ', [coordx(x0), coordy(y0)]])
a.append(['M', [coordx(x0), coordy(y0)]])
for i in range(int(nodes - 1)):
x1 = (i + 1) * step + xstart
x2 = x1 - ds # Second point BEFORE first point (Good for last point)
y1 = f(x1)
y2 = f(x2)
# numerical derivative
dx1 = (x1 - x2) / ds
dy1 = (y1 - y2) / ds
# create curve
a.append(['C', [coordx(x0 + (dx0 * third)), coordy(y0 + (dy0 * third)),
coordx(x1 - (dx1 * third)), coordy(y1 - (dy1 * third)),
coordx(x1), coordy(y1)]])
# Next segment's start is this segment's end
x0 = x1
y0 = y1
# Assume the function is smooth everywhere, so carry over the derivative too
dx0 = dx1
dy0 = dy1
return a
def offset(pathComp, dx, dy):
for ctl in pathComp:
for pt in ctl:
pt[0] += dx
pt[1] += dy
def stretch(pathComp, xscale, yscale, org):
for ctl in pathComp:
for pt in ctl:
pt[0] = org[0] + (pt[0] - org[0]) * xscale
pt[1] = org[1] + (pt[1] - org[1]) * yscale
class GuillocheContour(pathmodifier.PathModifier):
def add_arguments(self, pars):
pars.add_argument("--contourFunction", default="sin", help="Function defining the contour")
pars.add_argument("--tab")
pars.add_argument("--frequency", type=int, default=10, help="Frequency of the function")
pars.add_argument("--amplitude", type=int, default=1, help="Amplitude of the function")
pars.add_argument("--phaseOffset", type=int, default=0, help="Phase offset of the function")
pars.add_argument("--offset", type=int, default=0, help="Offset of the function")
pars.add_argument("--nodes", type=int, default=20, help="Count of nodes")
pars.add_argument("--remove", type=inkex.Boolean, default=False, help="If Ttrue, control object will be removed")
pars.add_argument("--strokeColor", type=inkex.Color)
pars.add_argument("--amplitude1", type=float, default=0.0, help="Amplitude of first harmonic")
pars.add_argument("--phase1", type=int, default=0, help="Phase offset of first harmonic")
pars.add_argument("--amplitude2", type=float, default=0.0, help="Amplitude of second harmonic")
pars.add_argument("--phase2", type=int, default=0, help="Phase offset of second harmonic")
pars.add_argument("--amplitude3", type=float, default=0.0, help="Amplitude of third harmonic")
pars.add_argument("--phase3", type=int, default=0, help="Phase offset of third harmonic")
pars.add_argument("--amplitude4", type=float, default=0.0, help="Amplitude of fourth harmonic")
pars.add_argument("--phase4", type=int, default=0, help="Phase offset of fourth harmonic")
pars.add_argument("--amplitude5", type=float, default=0.0, help="Amplitude of fifth harmonic")
pars.add_argument("--phase5", type=int, default=0, help="Phase offset of fifth harmonic")
def prepareSelectionList(self):
self.skeletons = self.svg.selected
pathmodifier.PathModifier.expand_clones(self, self.skeletons, True, False)
pathmodifier.PathModifier.objects_to_paths(self, self.skeletons, True)
def linearizePath(self, skelPath, offs):
comps, lengths = linearize(skelPath)
self.skelCompIsClosed = isSkeletonClosed(comps)
if (self.skelCompIsClosed and offs != 0):
centroid = getPolygonCentroid(comps)
for i in range(len(comps)):
pt1 = comps[i]
dist = inkex.bezier.pointdistance(centroid, pt1)
comps[i] = getPtOnSeg(centroid, pt1, dist, dist + offs)
if i > 0:
lengths[i - 1] = inkex.bezier.pointdistance(comps[i - 1], comps[i])
return (comps, lengths)
def getFunction(self, func):
res = ''
presetAmp1 = presetAmp2 = presetAmp3 = presetAmp4 = presetAmp5 = 0.0
presetPhOf1 = presetPhOf2 = presetPhOf3 = presetPhOf4 = presetPhOf5 = presetOffs = 0
if (func == 'sin' or func == 'cos'):
return '(' + str(self.options.amplitude) + ') * ' + func + '(x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + '))'
if func == 'env1':
presetAmp1 = presetAmp3 = 0.495
elif func == 'env2':
presetAmp1 = presetAmp3 = 0.65
presetPhOf1 = presetPhOf3 = 25
elif func == 'env3':
presetAmp1 = 0.75
presetPhOf1 = 25
presetAmp3 = 0.24
presetPhOf3 = -25
elif func == 'env4':
presetAmp1 = 1.105
presetAmp3 = 0.27625
presetPhOf3 = 50
elif func == 'env5':
presetAmp1 = 0.37464375
presetPhOf1 = 25
presetAmp2 = 0.5655
presetAmp3 = 0.37464375
presetPhOf3 = -25
elif func == 'env6':
presetAmp1 = 0.413725
presetPhOf1 = 25
presetAmp2 = 0.45695
presetPhOf2 = 50
presetAmp3 = 0.494
presetPhOf3 = -25
elif func == 'env7':
presetAmp1 = 0.624
presetPhOf1 = 25
presetAmp2 = 0.312
presetAmp3 = 0.624
presetPhOf3 = 25
elif func == 'env8':
presetAmp1 = 0.65
presetPhOf1 = 50
presetAmp2 = 0.585
presetAmp3 = 0.13
elif func == 'env9':
presetAmp1 = 0.07605
presetPhOf1 = 25
presetAmp2 = 0.33345
presetPhOf2 = 50
presetAmp3 = 0.468
presetPhOf3 = -25
presetAmp4 = 0.32175
elif func == 'env10':
presetAmp1 = 0.3575
presetPhOf1 = -25
presetAmp2 = 0.3575
presetAmp3 = 0.3575
presetPhOf3 = 25
presetAmp4 = 0.3575
presetPhOf4 = 50
elif func == 'env11':
presetAmp1 = 0.65
presetPhOf1 = 25
presetAmp2 = 0.13
presetPhOf2 = 50
presetAmp3 = 0.26
presetPhOf3 = 25
presetAmp4 = 0.39
elif func == 'env12':
presetAmp1 = 0.5525
presetPhOf1 = -25
presetAmp2 = 0.0414375
presetPhOf2 = 50
presetAmp3 = 0.15884375
presetPhOf3 = 25
presetAmp4 = 0.0966875
presetAmp5 = 0.28315625
presetPhOf5 = -25
harm1 = '(' + str(self.options.amplitude * (presetAmp1 + self.options.amplitude1)) + ') * cos(1 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf1 + self.options.phase1) / 100.0 * 2 * pi) + '))'
harm2 = '(' + str(self.options.amplitude * (presetAmp2 + self.options.amplitude2)) + ') * cos(2 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf2 + self.options.phase2) / 100.0 * 2 * pi) + '))'
harm3 = '(' + str(self.options.amplitude * (presetAmp3 + self.options.amplitude3)) + ') * cos(3 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf3 + self.options.phase3) / 100.0 * 2 * pi) + '))'
harm4 = '(' + str(self.options.amplitude * (presetAmp4 + self.options.amplitude4)) + ') * cos(4 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf4 + self.options.phase4) / 100.0 * 2 * pi) + '))'
harm5 = '(' + str(self.options.amplitude * (presetAmp5 + self.options.amplitude5)) + ') * cos(5 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf5 + self.options.phase5) / 100.0 * 2 * pi) + '))'
res = harm1 + ' + ' + harm2 + ' + ' + harm3 + ' + ' + harm4 + ' + ' + harm5
return res
def lengthToTime(self, l):
'''
Recieves an arc length l, and returns the index of the segment in self.skelComp
containing the corresponding point, together with the position of the point on this segment.
If the deformer is closed, do computations modulo the total length.
'''
if self.skelCompIsClosed:
l = l % sum(self.lengths)
if l <= 0:
return 0, l / self.lengths[0]
i = 0
while (i < len(self.lengths)) and (self.lengths[i] <= l):
l -= self.lengths[i]
i += 1
t = l / self.lengths[min(i, len(self.lengths) - 1)]
return (i, t)
def applyDiffeo(self, bpt, vects=()):
'''
The kernel of this stuff:
bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
'''
s = bpt[0] - self.skelComp[0][0]
i, t = self.lengthToTime(s)
if i == len(self.skelComp) - 1:
x, y = inkex.bezier.tpoint(self.skelComp[i - 1], self.skelComp[i], t + 1)
dx = (self.skelComp[i][0] - self.skelComp[i - 1][0]) / self.lengths[-1]
dy = (self.skelComp[i][1] - self.skelComp[i - 1][1]) / self.lengths[-1]
else:
x, y = inkex.bezier.tpoint(self.skelComp[i], self.skelComp[i + 1], t)
dx = (self.skelComp[i + 1][0] - self.skelComp[i][0]) / self.lengths[i]
dy = (self.skelComp[i + 1][1] - self.skelComp[i][1]) / self.lengths[i]
vx = 0
vy = bpt[1] - self.skelComp[0][1]
bpt[0] = x + vx * dx - vy * dy
bpt[1] = y + vx * dy + vy * dx
for v in vects:
vx = v[0] - self.skelComp[0][0] - s
vy = v[1] - self.skelComp[0][1]
v[0] = x + vx * dx - vy * dy
v[1] = y + vx * dy + vy * dx
def effect(self):
if len(self.options.ids) < 1:
inkex.errormsg(_("This extension requires one selected path."))
return
self.prepareSelectionList()
for skeleton in self.skeletons.__iter__():
resPath = []
pattern = etree.Element(inkex.addNS('path','svg'))
self.options.strokeHexColor, self.strokeOpacity = getColorAndOpacity(self.options.strokeColor)
# Copy style of skeleton with setting color and opacity
s = skeleton.get('style')
# Get any path transform for the contour output
xfm = skeleton.get('transform')
firstSkel = skeleton.get('d')
if s:
pattern.set('style', setColorAndOpacity(s, self.options.strokeHexColor, self.strokeOpacity))
if xfm:
pattern.set('transform', xfm)
skeletonPath = modifySkeletonPath(getSkeletonPath(skeleton.get('d'), self.options.offset))
self.skelComp, self.lengths = self.linearizePath(skeletonPath, self.options.offset)
length = sum(self.lengths)
patternWidth = length / self.options.frequency
selectedFunction = self.getFunction(self.options.contourFunction)
pattern.set('d', str(paths.Path(drawfunction(self.options.nodes, patternWidth, selectedFunction))))
# Add path into SVG structure
skeleton.getparent().append(pattern)
if self.options.remove:
skeleton.getparent().remove(skeleton)
# Compute bounding box
#pattPath = inkex.paths.Path(skeleton.get('d'))
patternCubicPath = paths.CubicSuperPath(paths.Path(pattern.get('d')))
patternPath = inkex.paths.Path(patternCubicPath)
bbox = patternPath.bounding_box()
width = bbox.maximum[0] - bbox.minimum[0]
dx = width
if dx < 0.01:
exit(_("The total length of the pattern is too small."))
curPath = deepcopy(patternCubicPath)
xoffset = self.skelComp[0][0] - bbox.minimum[0]
yoffset = self.skelComp[0][1] - (bbox.bottom + bbox.top) / 2
patternCopies = max(1, int(round(length / dx)))
width = dx * patternCopies
newPath = []
# Repeat pattern to cover whole skeleton
for subPath in curPath:
for i in range(0, patternCopies, 1):
newPath.append(deepcopy(subPath))
offset(subPath, dx, 0)
curPath = newPath
# Offset pattern to the first node of the skeleton
for subPath in curPath:
offset(subPath, xoffset, yoffset)
# Stretch pattern to whole skeleton
for subPath in curPath:
stretch(subPath, length / width, 1, self.skelComp[0])
for subPath in curPath:
for ctlpt in subPath:
self.applyDiffeo(ctlpt[1], (ctlpt[0], ctlpt[2]))
# Check if there is a need to close path manually
if self.skelCompIsClosed:
firstPtX = round(curPath[0][0][1][0], 8)
firstPtY = round(curPath[0][0][1][1], 8)
finalPtX = round(curPath[-1][-1][1][0], 8)
finalPtY = round(curPath[-1][-1][1][1], 8)
if (firstPtX != finalPtX or firstPtY != finalPtY):
curPath[-1].append(curPath[0][0])
resPath += curPath
# This final step takes the newly constructed contour from a multilevel list to
# a formal svg path
step1rep = paths.CubicSuperPath(resPath).to_path().to_arrays()
step2rep = str(paths.Path(step1rep))
pattern.set('d', step2rep)
if __name__ == '__main__':
GuillocheContour().run()

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Guilloche Pattern</name>
<id>fablabchemnitz.de.guilloche_pattern</id>
<param name="patternFunction" type="optiongroup" appearance="combo" gui-text="Function">
<option value="line">Line</option>
<option value="sin">Sin</option>
<option value="cos">Cos</option>
<option value="env1">Env1</option>
<option value="env2">Env2</option>
<option value="env3">Env3</option>
<option value="env4">Env4</option>
<option value="env5">Env5</option>
<option value="env6">Env6</option>
<option value="env7">Env7</option>
<option value="env8">Env8</option>
<option value="env9">Env9</option>
<option value="env10">Env10</option>
<option value="env11">Env11</option>
<option value="env12">Env12</option>
</param>
<param name="tab" type="notebook">
<page name="pattern" gui-text="Pattern">
<param name="frequency" type="int" min="1" max="100" gui-text="Frequency:">10</param>
<param name="amplitude" type="int" min="-300" max="300" gui-text="Amplitude:">100</param>
<param name="phaseOffset" type="int" min="-100" max="100" gui-text="Phase offset:">0</param>
<param name="offset" type="int" min="-100" max="100" gui-text="Offset:">0</param>
<param name="phaseCoverage" type="int" min="-100" max="100" gui-text="Phase Coverage">100</param>
<param name="series" type="int" min="1" max="50" gui-text="Series">1</param>
<param name="nodes" type="int" min="2" max="1000" gui-text="Number of nodes:">20</param>
<param name="remove" type="bool" gui-text="Remove control objects">false</param>
<param name="strokeColor" type="color" gui-text="Stroke color"></param>
</page>
<page name="function" gui-text="Function">
<param name="amplitude1" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 1:">0.0</param>
<param name="phase1" type="int" min="-100" max="100" gui-text="Phase offset 1:">0</param>
<param name="amplitude2" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 2:">0.0</param>
<param name="phase2" type="int" min="-100" max="100" gui-text="Phase offset 2:">0</param>
<param name="amplitude3" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 3:">0.0</param>
<param name="phase3" type="int" min="-100" max="100" gui-text="Phase offset 3:">0</param>
<param name="amplitude4" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 4:">0.0</param>
<param name="phase4" type="int" min="-100" max="100" gui-text="Phase offset 4:">0</param>
<param name="amplitude5" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 5:">0.0</param>
<param name="phase5" type="int" min="-100" max="100" gui-text="Phase offset 5:">0</param>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">guilloche_pattern.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,972 @@
#! /usr/bin/env python3
'''
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.
Quick description:
This extension is an update to the guilloche_pattern extension that works under inkscape v0.92
'''
# standard library
from math import asin, atan2, pi, sin, cos
from copy import deepcopy
# local library
import inkex
import pathmodifier
import cubicsuperpath
from inkex import paths
from lxml import etree
def modifySkeletonPath(skelPath):
resPath = []
l = len(skelPath)
resPath += skelPath[0]
if l > 1:
for i in range(1, l):
if skelPath[i][0][1] == resPath[-1][1]:
skelPath[i][0][0] = resPath[-1][0]
del resPath[-1]
resPath += skelPath[i]
return resPath
def linearize(p, tolerance=0.001):
'''
This function receives a component of a 'cubicsuperpath' and returns two things:
The path subdivided in many straight segments, and an array containing the length
of each segment.
'''
zero = 0.000001
i = 0
d = 0
lengths=[]
while i < len(p) - 1:
box = inkex.bezier.pointdistance(p[i][1], p[i][2])
box += inkex.bezier.pointdistance(p[i][2], p[i+1][0])
box += inkex.bezier.pointdistance(p[i+1][0], p[i+1][1])
chord = inkex.bezier.pointdistance(p[i][1], p[i+1][1])
if (box - chord) > tolerance:
b1, b2 = inkex.bezier.beziersplitatt([p[i][1], p[i][2], p[i + 1][0], p[i + 1][1]], 0.5)
p[i][2][0], p[i][2][1] = b1[1]
p[i + 1][0][0], p[i + 1][0][1] = b2[2]
p.insert(i + 1, [[b1[2][0], b1[2][1]], [b1[3][0], b1[3][1]], [b2[1][0], b2[1][1]]])
else:
d = (box + chord) / 2
lengths.append(d)
i += 1
new = [p[i][1] for i in range(0, len(p) - 1) if lengths[i] > zero]
new.append(p[-1][1])
lengths = [l for l in lengths if l > zero]
return (new, lengths)
def isSkeletonClosed(sklCmp):
requiredPrecision = 0.005
sctest1 = abs(sklCmp[0][0] - sklCmp[-1][0]) > requiredPrecision
sctest2 = abs(sklCmp[0][1] - sklCmp[-1][1]) > requiredPrecision
if sctest1 or sctest2:
return False
return True
def checkCompatibility(bbox1, bbox2, comps1, comps2):
cl1 = isSkeletonClosed(comps1)
cl2 = isSkeletonClosed(comps2)
if (cl1 and cl2):
if ((bbox1.left >= bbox2.left) and (bbox1.right <= bbox2.right) and (bbox1.top >= bbox2.top) and (bbox1.bottom <= bbox2.bottom)):
return (True, False)
elif ((bbox1.left <= bbox2.left) and (bbox1.right >= bbox2.right) and (bbox1.top <= bbox2.top) and (bbox1.bottom >= bbox2.bottom)):
return (True, True)
elif (not cl1 and not cl2):
if (comps1[0][0] == comps2[0][0] and comps1[-1][0] == comps2[-1][0]):
if ((comps1[0][0] < comps1[-1][0] and comps1[0][1] >= comps2[0][1]) or (comps1[0][0] > comps1[-1][0] and comps1[0][1] <= comps2[0][1])):
return (True, False)
else:
return (True, True)
elif (comps1[0][1] == comps2[0][1] and comps1[-1][1] == comps2[-1][1]):
if ((comps1[0][1] < comps1[-1][1] and comps1[0][0] <= comps2[0][0]) or (comps1[0][1] > comps1[-1][1] and comps1[0][0] >= comps2[0][0])):
return (True, False)
else:
return (True, True)
return (False, False)
def linearizeEnvelopes(envs):
env0Path = paths.Path(envs[0].get('d'))
env1Path = paths.Path(envs[1].get('d'))
env0CubicPath = paths.CubicSuperPath(env0Path)
env1CubicPath = paths.CubicSuperPath(env1Path)
env0ReformedPath = paths.Path(env0CubicPath)
env1ReformedPath = paths.Path(env1CubicPath)
bbox1 = env0ReformedPath.bounding_box()
bbox2 = env1ReformedPath.bounding_box()
comps1, lengths1 = linearize(modifySkeletonPath(env0CubicPath))
comps2, lengths2 = linearize(modifySkeletonPath(env1CubicPath))
correctness, shouldSwap = checkCompatibility(bbox1, bbox2, comps1, comps2)
if not shouldSwap:
return (comps1, lengths1, comps2, lengths2, bbox1, bbox2, correctness)
else:
return (comps2, lengths2, comps1, lengths1, bbox2, bbox1, correctness)
def getMidPoint(p1, p2):
return [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2]
def getPoint(p1, p2, x, y):
x1 = p1[0]
y1 = p1[1]
x2 = p2[0]
y2 = p2[1]
a = (y1 - y2) / (x1 - x2)
b = y1 - a * x1
if x == None:
x = (y - b) / a
else:
y = a * x + b
return [x, y]
def getPtOnSeg(p1, p2, segLen, l):
if p1[0] == p2[0]:
return [p1[0], p1[1] - l] if p1[1] > p2[1] else [p1[0], p1[1] + l]
if p1[1] == p2[1]:
return [p1[0] - l, p1[1]] if p1[0] > p2[0] else [p1[0] + l, p1[1]]
dy = abs(p1[1] - p2[1])
angle = asin(dy / segLen)
dx = l * cos(angle)
x = p1[0] - dx if p1[0] > p2[0] else p1[0] + dx
return getPoint(p1, p2, x, None)
def getPtsByX(pt, comps, isClosed):
res = []
for i in range(1, len(comps)):
if ((comps[i - 1][0] <= pt[0] and pt[0] <= comps[i][0]) or (comps[i - 1][0] >= pt[0] and pt[0] >= comps[i][0])):
if comps[i - 1][0] == comps[i][0]:
d1 = inkex.bezier.pointdistance(pt, comps[i - 1])
d2 = inkex.bezier.pointdistance(pt, comps[i])
if d1 < d2:
res.append(comps[i - 1])
else:
res.append(comps[i])
elif comps[i - 1][1] == comps[i][1]:
res.append([pt[0], comps[i - 1][1]])
else:
res.append(getPoint(comps[i - 1], comps[i], pt[0], None))
if not isClosed:
return res[0]
return res
def getPtsByY(pt, comps, isClosed):
res = []
for i in range(1, len(comps)):
if ((comps[i - 1][1] <= pt[1] and pt[1] <= comps[i][1]) or (comps[i - 1][1] >= pt[1] and pt[1] >= comps[i][1])):
if comps[i - 1][1] == comps[i][1]:
d1 = inkex.bezier.pointdistance(pt, comps[i - 1])
d2 = inkex.bezier.pointdistance(pt, comps[i])
if d1 < d2:
res.append(comps[i - 1])
else:
res.append(comps[i])
elif comps[i - 1][0] == comps[i][0]:
res.append([comps[i - 1][0], pt[1]])
else:
res.append(getPoint(comps[i - 1], comps[i], None, pt[1]))
if not isClosed:
return res[0]
return res
def getIntersectionPt(cntr, p, comps):
# Find the intersection of the infinitely extended line from cntr to p with comps
# This algorithm assumes that each comps segment is a straight line and that the comps segment subtends less than
# pi angle at cntr
twopi = 2.0 * pi
a = atan2((cntr[1] - p[1]), (cntr[0] - p[0])) # a is a four-quadrant angle of the cntr->p line, -pi <= a <= pi
obtPts = []
for i in range(1, len(comps)): # Check every interval in comps for an intersection
a2 = atan2((cntr[1] - comps[i][1]) , (cntr[0] - comps[i][0]))
a1 = atan2((cntr[1] - comps[i - 1][1]) , (cntr[0] - comps[i - 1][0]))
da1a = a - a1 # the angle from a1 to a
da12 = a2 - a1 # the angle interval covered by comps[i-1]->comps[i]
#inkex.errormsg("raw da1a = " + str(da1a))
#inkex.errormsg("raw da12 = " + str(da12))
if da1a > pi: # make the angle fit -pi->pi
da1a -= twopi
elif da1a < -pi:
da1a += twopi
if da12 > pi: # make the angle fit -pi->pi
da12 -= twopi
elif da12 < -pi:
da12 += twopi
frac = da1a / da12
if frac < 0.0 or frac > 1.0: # if the line does not cross the comps interval, move on
continue
x = frac * comps[i][0] + (1.0 - frac) * comps[i-1][0]
y = frac * comps[i][1] + (1.0 - frac) * comps[i-1][1]
obtPts.append([x,y])
if len(obtPts) < 1:
inkex.errormsg("No intersection pt found")
exit()
else:
return obtPts[0]
def getPolygonCentroid(polygon):
x = 0
y = 0
n = len(polygon)
for vert in polygon:
x += vert[0]
y += vert[1]
x = x / n
y = y / n
return [x, y]
def getDistBetweenFirstPts(comps1, comps2, bbox1, bbox2, nest):
pt1 = comps1[0]
pt2 = None
if (bbox1[0] == bbox2[0] and bbox1[1] == bbox2[1]):
pt2 = getPtsByX(pt1, comps2, False)
elif (bbox1[2] == bbox2[2] and bbox1[3] == bbox2[3]):
pt2 = getPtsByY(pt1, comps2, False)
elif nest:
centroid = getPolygonCentroid(comps1)
pt2 = getIntersectionPt(centroid, pt1, comps2)
dist = inkex.bezier.pointdistance(pt1, pt2)
return dist
def getCirclePath(startPt, rx):
curX = startPt[0]
curY = startPt[1]
signRX = signRY = 1
res = 'M ' + str(curX) + ',' + str(curY) + ' A '
for i in range(4):
res += str(rx) + ',' + str(rx) + ' 0 0 1 '
if i % 2 == 0:
signRX = -signRX
else:
signRY = -signRY
curX += signRX * rx
curY += signRY * rx
res += str(curX) + ',' + str(curY) + ' '
res += 'Z'
return res
def getClosedLinearizedSkeletonPath(comps1, comps2, offs, isLine):
path = []
lengths = []
centroid = getPolygonCentroid(comps1)
if isLine:
for pt2 in comps2:
pt1 = getIntersectionPt(centroid, pt2, comps1)
midPt = getMidPoint(pt1, pt2)
if offs > 0:
dist = inkex.bezier.pointdistance(pt1, pt2)
path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
else:
path.append(midPt)
if len(path) > 1:
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
else:
pt1 = comps1[0]
pt2 = getIntersectionPt(centroid, pt1, comps2)
midPt = getMidPoint(pt1, pt2)
rx = inkex.bezier.pointdistance(centroid, midPt)
svgPath = getCirclePath(midPt, rx)
inkexPath = paths.Path(svgPath)
cubicPath = paths.CubicSuperPath(inkexPath)
path, lengths = linearize(modifySkeletonPath(cubicPath))
return (path, lengths)
def getHorizontalLinearizedSkeletonPath(comps1, comps2, offs, isLine):
path = []
lengths = []
if isLine:
for pt1 in comps1:
pt2 = getPtsByX(pt1, comps2, False)
midPt = getMidPoint(pt1, pt2)
if offs > 0:
dist = inkex.bezier.pointdistance(pt1, pt2)
path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
else:
path.append(midPt)
if len(path) > 1:
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
else:
pt1 = comps1[0]
pt2 = comps2[0]
firstPt = getMidPoint(pt1, pt2)
path.append(firstPt)
lastPt = [comps1[-1][0], firstPt[1]]
path.append(lastPt)
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
return (path, lengths)
def getVerticalLinearizedSkeletonPath(comps1, comps2, offs, isLine):
path = []
lengths = []
if isLine:
for pt1 in comps1:
pt2 = getPtsByY(pt1, comps2, False)
midPt = getMidPoint(pt1, pt2)
if offs > 0:
dist = inkex.bezier.pointdistance(pt1, pt2)
path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
else:
path.append(midPt)
if len(path) > 1:
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
else:
pt1 = comps1[0]
pt2 = comps2[0]
firstPt = getMidPoint(pt1, pt2)
path.append(firstPt)
lastPt = [firstPt[0], comps1[-1][1]]
path.append(lastPt)
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
return (path, lengths)
def getColorAndOpacity(longColor):
'''
Convert the long into a #rrggbb color value
Conversion back is A + B*256^1 + G*256^2 + R*256^3
'''
longColor = int(longColor)
if longColor < 0:
longColor = longColor & 0xFFFFFFFF
hexColor = hex(longColor)
lhc = len(hexColor)
hexOpacity = hexColor[lhc-2 : ]
hexColor = '#' + hexColor[2:-2].rjust(6, '0')
return (hexColor, hexOpacity)
def setColorAndOpacity(style, color, opacity):
declarations = style.split(';')
strokeOpacityInStyle = False
newOpacity = round((int(opacity, 16) / 255.0), 8)
for i,decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if (prop == 'stroke' and val != color):
declarations[i] = prop + ':' + color
if prop == 'stroke-opacity':
if val != newOpacity:
declarations[i] = prop + ':' + str(newOpacity)
strokeOpacityInStyle = True
if not strokeOpacityInStyle:
declarations.append('stroke-opacity' + ':' + str(newOpacity))
return ";".join(declarations)
def drawfunction(nodes, width, fx):
# x-bounds of the plane
xstart = 0.0
xend = 2 * pi
# y-bounds of the plane
ybottom = -1.0
ytop = 1.0
# size and location of the plane on the canvas
height = 2
left = 15
bottom = 15 + height
# function specified by the user
try:
if fx != "":
f = eval('lambda x: ' + fx.strip('"'))
except SyntaxError:
return []
scalex = width / (xend - xstart)
xoff = left
# conver x-value to coordinate
coordx = lambda x: (x - xstart) * scalex + xoff
scaley = height / (ytop - ybottom)
yoff = bottom
# conver y-value to coordinate
coordy = lambda y: (ybottom - y) * scaley + yoff
# step is the distance between nodes on x
step = (xend - xstart) / (nodes - 1)
third = step / 3.0
# step used in calculating derivatives
ds = step * 0.001
# initialize function and derivative for 0;
# they are carried over from one iteration to the next, to avoid extra function calculations.
x0 = xstart
y0 = f(xstart)
# numerical derivative, using 0.001*step as the small differential
x1 = xstart + ds # Second point AFTER first point (Good for first point)
y1 = f(x1)
dx0 = (x1 - x0) / ds
dy0 = (y1 - y0) / ds
# path array
a = []
# Start curve
a.append(['M', [coordx(x0), coordy(y0)]])
for i in range(int(nodes - 1)):
x1 = (i + 1) * step + xstart
x2 = x1 - ds # Second point BEFORE first point (Good for last point)
y1 = f(x1)
y2 = f(x2)
# numerical derivative
dx1 = (x1 - x2) / ds
dy1 = (y1 - y2) / ds
# create curve
a.append(['C', [coordx(x0 + (dx0 * third)), coordy(y0 + (dy0 * third)),
coordx(x1 - (dx1 * third)), coordy(y1 - (dy1 * third)),
coordx(x1), coordy(y1)]])
# Next segment's start is this segment's end
x0 = x1
y0 = y1
# Assume the function is smooth everywhere, so carry over the derivative too
dx0 = dx1
dy0 = dy1
return a
def offset(pathComp, dx, dy):
for ctl in pathComp:
for pt in ctl:
pt[0] += dx
pt[1] += dy
def compsToSVGd(p):
f = p[0]
p = p[1:]
svgd = 'M %.9f,%.9f ' % (f[0], f[1])
for x in p:
svgd += 'L %.9f,%.9f ' % (x[0], x[1])
return svgd
scRepCounter = 0
def stretchComps(skelComps, patComps, comps1, comps2, bbox1, bbox2, nest, halfHeight, ampl, offs):
res = []
repCounter = 0
if nest:
newPt = None
centroid = getPolygonCentroid(comps1)
for pt in patComps:
skelPt = getIntersectionPt(centroid, pt, skelComps)
pt1 = getIntersectionPt(centroid, pt, comps1)
pt2 = getIntersectionPt(centroid, pt, comps2)
midPt = getMidPoint(pt1, pt2)
dist1 = inkex.bezier.pointdistance(skelPt, pt)
dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
dist3 = dist2 * dist1 / halfHeight
if (skelPt[0] >= centroid[0] and skelPt[1] >= centroid[1]):
if (pt[0] >= skelPt[0] and pt[1] >= skelPt[1]):
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
else:
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
elif (skelPt[0] <= centroid[0] and skelPt[1] <= centroid[1]):
if (pt[0] <= skelPt[0] and pt[1] <= skelPt[1]):
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
else:
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
elif (skelPt[0] < centroid[0] and skelPt[1] > centroid[1]):
if (pt[0] <= skelPt[0] and pt[1] >= skelPt[1]):
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
else:
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
else:
if (pt[0] >= skelPt[0] and pt[1] <= skelPt[1]):
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
else:
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
res.append(newPt)
elif (bbox1.left == bbox2.left and bbox1.right == bbox2.right):
midY = skelComps[0][1]
newPt = None
if (patComps[-1][0] != comps1[-1][0] and round(patComps[-1][0], 10) == comps1[-1][0]):
patComps[-1][0] = comps1[-1][0]
for pt in patComps:
pt1 = getPtsByX(pt, comps1, False)
pt2 = getPtsByX(pt, comps2, False)
midPt = getMidPoint(pt1, pt2)
dist1 = abs(pt[1] - midY)
dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
dist3 = dist2 * dist1 / halfHeight
if bbox1.left < bbox1.right:
if pt[1] > midY:
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
else:
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
else:
if pt[1] < midY:
newPt = getPtOnSeg(midPt, pt1, bezmisc.pointdistance(midPt, pt1), dist3 - offs)
else:
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
res.append(newPt)
elif (bbox1.top == bbox2.top and bbox1.bottom == bbox2.bottom):
midX = skelComps[0][0]
newPt = None
if (patComps[-1][1] != comps1[-1][1] and round(patComps[-1][1], 10) == comps1[-1][1]):
patComps[-1][1] = comps1[-1][1]
for pt in patComps:
pt1 = getPtsByY(pt, comps1, False)
pt2 = getPtsByY(pt, comps2, False)
midPt = getMidPoint(pt1, pt2)
dist1 = abs(pt[0] - midX)
dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
dist3 = dist2 * dist1 / halfHeight
if bbox1[2] < bbox1[3]:
if pt[0] < midX:
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
else:
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
else:
if pt[1] > midX:
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
else:
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
res.append(newPt)
return res
def stretch(pathComp, xscale, yscale, org):
for ctl in pathComp:
for pt in ctl:
pt[0] = org[0] + (pt[0] - org[0]) * xscale
pt[1] = org[1] + (pt[1] - org[1]) * yscale
class GuillochePattern(pathmodifier.PathModifier):
def add_arguments(self, pars):
pars.add_argument("--tab")
pars.add_argument("--patternFunction", default="sin", help="Function of the pattern")
pars.add_argument("--frequency", type=int, default=10, help="Frequency of the function")
pars.add_argument("--amplitude", type=int, default=100, help="Amplitude of the function")
pars.add_argument("--phaseOffset", type=int, default=0, help="Phase offset of the function")
pars.add_argument("--offset", type=int, default=0, help="Offset of the function")
pars.add_argument("--phaseCoverage", type=int, default=100, help="Phase coverage of the function")
pars.add_argument("--series", type=int, default=1, help="Series of the function")
pars.add_argument("--nodes", type=int, default=20, help="Count of nodes")
pars.add_argument("--remove", type=inkex.Boolean, default=False, help="If True, control objects will be removed")
pars.add_argument("--strokeColor", type=inkex.Color, default=000, help="The line's color")
pars.add_argument("--amplitude1", type=float, default=0.0, help="Amplitude of first harmonic")
pars.add_argument("--phase1", type=int, default=0, help="Phase offset of first harmonic")
pars.add_argument("--amplitude2", type=float, default=0.0, help="Amplitude of second harmonic")
pars.add_argument("--phase2", type=int, default=0, help="Phase offset of second harmonic")
pars.add_argument("--amplitude3", type=float, default=0.0, help="Amplitude of third harmonic")
pars.add_argument("--phase3", type=int, default=0, help="Phase offset of third harmonic")
pars.add_argument("--amplitude4", type=float, default=0.0, help="Amplitude of fourth harmonic")
pars.add_argument("--phase4", type=int, default=0, help="Phase offset of fourth harmonic")
pars.add_argument("--amplitude5", type=float, default=0.0, help="Amplitude of fifth harmonic")
pars.add_argument("--phase5", type=int, default=0, help="Phase offset of fifth harmonic")
def prepareSelectionList(self):
self.envelopes = self.svg.selected
pathmodifier.PathModifier.expand_clones(self, self.envelopes, True, False)
pathmodifier.PathModifier.objects_to_paths(self, self.envelopes, True)
def getFunction(self, func, funcOffs):
res = ''
presetAmp1 = presetAmp2 = presetAmp3 = presetAmp4 = presetAmp5 = 0.0
presetPhOf1 = presetPhOf2 = presetPhOf3 = presetPhOf4 = presetPhOf5 = presetOffs = 0
funcOffs *= self.options.phaseCoverage / 100.0
if (func == 'sin' or func == 'cos'):
return func + '(x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + '))'
if func == 'env1':
presetAmp1 = presetAmp3 = 0.495
elif func == 'env2':
presetAmp1 = presetAmp3 = 0.65
presetPhOf1 = presetPhOf3 = 25
elif func == 'env3':
presetAmp1 = 0.75
presetPhOf1 = 25
presetAmp3 = 0.24
presetPhOf3 = -25
elif func == 'env4':
presetAmp1 = 1.105
presetAmp3 = 0.27625
presetPhOf3 = 50
elif func == 'env5':
presetAmp1 = 0.37464375
presetPhOf1 = 25
presetAmp2 = 0.5655
presetAmp3 = 0.37464375
presetPhOf3 = -25
elif func == 'env6':
presetAmp1 = 0.413725
presetPhOf1 = 25
presetAmp2 = 0.45695
presetPhOf2 = 50
presetAmp3 = 0.494
presetPhOf3 = -25
elif func == 'env7':
presetAmp1 = 0.624
presetPhOf1 = 25
presetAmp2 = 0.312
presetAmp3 = 0.624
presetPhOf3 = 25
elif func == 'env8':
presetAmp1 = 0.65
presetPhOf1 = 50
presetAmp2 = 0.585
presetAmp3 = 0.13
elif func == 'env9':
presetAmp1 = 0.07605
presetPhOf1 = 25
presetAmp2 = 0.33345
presetPhOf2 = 50
presetAmp3 = 0.468
presetPhOf3 = -25
presetAmp4 = 0.32175
elif func == 'env10':
presetAmp1 = 0.3575
presetPhOf1 = -25
presetAmp2 = 0.3575
presetAmp3 = 0.3575
presetPhOf3 = 25
presetAmp4 = 0.3575
presetPhOf4 = 50
elif func == 'env11':
presetAmp1 = 0.65
presetPhOf1 = 25
presetAmp2 = 0.13
presetPhOf2 = 50
presetAmp3 = 0.26
presetPhOf3 = 25
presetAmp4 = 0.39
elif func == 'env12':
presetAmp1 = 0.5525
presetPhOf1 = -25
presetAmp2 = 0.0414375
presetPhOf2 = 50
presetAmp3 = 0.15884375
presetPhOf3 = 25
presetAmp4 = 0.0966875
presetAmp5 = 0.28315625
presetPhOf5 = -25
harm1 = '(' + str(presetAmp1 + self.options.amplitude1) + ') * cos(1 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf1 + self.options.phase1) / 100.0 * 2 * pi) + '))'
harm2 = '(' + str(presetAmp2 + self.options.amplitude2) + ') * cos(2 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf2 + self.options.phase2) / 100.0 * 2 * pi) + '))'
harm3 = '(' + str(presetAmp3 + self.options.amplitude3) + ') * cos(3 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf3 + self.options.phase3) / 100.0 * 2 * pi) + '))'
harm4 = '(' + str(presetAmp4 + self.options.amplitude4) + ') * cos(4 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf4 + self.options.phase4) / 100.0 * 2 * pi) + '))'
harm5 = '(' + str(presetAmp5 + self.options.amplitude5) + ') * cos(5 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf5 + self.options.phase5) / 100.0 * 2 * pi) + '))'
res = harm1 + ' + ' + harm2 + ' + ' + harm3 + ' + ' + harm4 + ' + ' + harm5
return res
def lengthToTime(self, l):
'''
Recieves an arc length l, and returns the index of the segment in self.skelComp
containing the corresponding point, together with the position of the point on this segment.
If the deformer is closed, do computations modulo the total length.
'''
if self.skelCompIsClosed:
l = l % sum(self.lengths)
if l <= 0:
return 0, l / self.lengths[0]
i = 0
while (i < len(self.lengths)) and (self.lengths[i] <= l):
l -= self.lengths[i]
i += 1
t = l / self.lengths[min(i, len(self.lengths) - 1)]
return (i, t)
def applyDiffeo(self, bpt, vects=()):
'''
The kernel of this stuff:
bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
'''
s = bpt[0] - self.skelComp[0][0]
i, t = self.lengthToTime(s)
if i == len(self.skelComp) - 1:
x, y = inkex.bezier.tpoint(self.skelComp[i - 1], self.skelComp[i], t + 1)
dx = (self.skelComp[i][0] - self.skelComp[i - 1][0]) / self.lengths[-1]
dy = (self.skelComp[i][1] - self.skelComp[i - 1][1]) / self.lengths[-1]
else:
x, y = inkex.bezier.tpoint(self.skelComp[i], self.skelComp[i + 1], t)
dx = (self.skelComp[i + 1][0] - self.skelComp[i][0]) / self.lengths[i]
dy = (self.skelComp[i + 1][1] - self.skelComp[i][1]) / self.lengths[i]
vx = 0
vy = bpt[1] - self.skelComp[0][1]
bpt[0] = x + vx * dx - vy * dy
bpt[1] = y + vx * dy + vy * dx
for v in vects:
vx = v[0] - self.skelComp[0][0] - s
vy = v[1] - self.skelComp[0][1]
v[0] = x + vx * dx - vy * dy
v[1] = y + vx * dy + vy * dx
def effect(self):
if len(self.options.ids) != 2:
inkex.errormsg(_("This extension requires two selected paths."))
return
self.prepareSelectionList()
envs = list(self.envelopes.values())
s = envs[0].get('style')
parent = envs[0].getparent()
# Get any path transform for the contour output
xfm = envs[0].get('transform')
fstEnvComps, fstEnvLengths, sndEnvComps, sndEnvLengths, fstEnvBbox, sndEnvBbox, isCorrect = linearizeEnvelopes(envs)
if not isCorrect:
inkex.errormsg(_("Selected paths are not compatible."))
return
areNested = isSkeletonClosed(fstEnvComps) and isSkeletonClosed(sndEnvComps)
self.skelComp = None
self.lengths = None
countOfSkelPaths = 1
distBetweenFirstPts = 1
funcSeries = self.options.series
isLine = True if self.options.patternFunction == 'line' else False
if (isLine and self.options.offset > 0):
distBetweenFirstPts = getDistBetweenFirstPts(fstEnvComps, sndEnvComps, fstEnvBbox, sndEnvBbox, areNested)
countOfSkelPaths = int(distBetweenFirstPts / self.options.offset)
funcSeries = 1
for cnt in range(0, countOfSkelPaths):
curOffset = (cnt + 1) * self.options.offset / distBetweenFirstPts
if areNested:
self.skelComp, self.lengths = getClosedLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
elif (fstEnvBbox.left == sndEnvBbox.left and fstEnvBbox.right == sndEnvBbox.right):
self.skelComp, self.lengths = getHorizontalLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
elif (fstEnvBbox.top == sndEnvBbox.top and fstEnvBbox.bottom == sndEnvBbox.bottom):
self.skelComp, self.lengths = getVerticalLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
self.skelCompIsClosed = isSkeletonClosed(self.skelComp)
length = sum(self.lengths)
patternWidth = length / self.options.frequency
funcOffsetStep = 100 / funcSeries
resPath = ''
pattern = etree.Element(inkex.addNS('path','svg'))
self.options.strokeHexColor, self.strokeOpacity = getColorAndOpacity(self.options.strokeColor)
if s:
pattern.set('style', setColorAndOpacity(s, self.options.strokeHexColor, self.strokeOpacity))
if xfm:
pattern.set('transform', xfm)
for j in range(funcSeries):
selectedFunction = self.getFunction(self.options.patternFunction, j * funcOffsetStep)
pattern.set('d', str(paths.Path(drawfunction(self.options.nodes, patternWidth, selectedFunction))))
# Add path into SVG structure
parent.append(pattern)
patternCubicPath = paths.CubicSuperPath(paths.Path(pattern.get('d')))
patternPath = inkex.paths.Path(patternCubicPath)
# Compute bounding box
bbox = patternPath.bounding_box()
width = bbox.maximum[0] - bbox.minimum[0]
height = bbox.maximum[1] - bbox.minimum[1]
dx = width
if dx < 0.01:
exit(_("The total length of the pattern is too small."))
curPath = deepcopy(patternCubicPath)
xoffset = self.skelComp[0][0] - bbox.minimum[0]
yoffset = self.skelComp[0][1] - (bbox.maximum[1] + bbox.minimum[1]) / 2
patternCopies = max(1, int(round(length / dx)))
width = dx * patternCopies
newPath = []
# Repeat pattern to cover whole skeleton
for subPath in curPath:
for i in range(0, patternCopies, 1):
newPath.append(deepcopy(subPath))
offset(subPath, dx, 0)
# Offset pattern to the first node of the skeleton
for subPath in newPath:
offset(subPath, xoffset, yoffset)
curPath = deepcopy(newPath)
# Stretch pattern to whole skeleton
for subPath in curPath:
stretch(subPath, length / width, 1, self.skelComp[0])
for subPath in curPath:
for ctlpt in subPath:
self.applyDiffeo(ctlpt[1], (ctlpt[0], ctlpt[2]))
# Check if there is a need to close path manually
if self.skelCompIsClosed:
firstPtX = round(curPath[0][0][1][0], 8)
firstPtY = round(curPath[0][0][1][1], 8)
finalPtX = round(curPath[-1][-1][1][0], 8)
finalPtY = round(curPath[-1][-1][1][1], 8)
if (firstPtX != finalPtX or firstPtY != finalPtY):
curPath[-1].append(curPath[0][0])
curPathComps, curPathLengths = linearize(modifySkeletonPath(curPath))
if not isLine:
curPathComps = stretchComps(self.skelComp, curPathComps, fstEnvComps, sndEnvComps, fstEnvBbox, sndEnvBbox, areNested, height / 2, self.options.amplitude / 100.0, self.options.offset)
resPath += compsToSVGd(curPathComps)
pattern.set('d', resPath)
if self.options.remove:
parent.remove(envs[0])
parent.remove(envs[1])
if __name__ == '__main__':
GuillochePattern().run()