This commit is contained in:
parent
bdfa92b2f3
commit
6bde5301c2
23
extensions/fablabchemnitz_filletchamfer.inx
Normal file
23
extensions/fablabchemnitz_filletchamfer.inx
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<_name>Fillet And Chamfer</_name>
|
||||
<id>fablabchemnitz.de.filletchamfer</id>
|
||||
<dependency type="executable" location="extensions">fablabchemnitz_filletchamfer.py</dependency>
|
||||
<dependency type="executable" location="extensions">inkex.py</dependency>
|
||||
<param name="type" type="enum" _gui-text="Fillet or Chamfer:">
|
||||
<_item value="fillet">Fillet</_item>
|
||||
<_item value="chamfer">Chamfer</_item>
|
||||
</param>
|
||||
<param name="radius" type="float" min="0.0" max="1000.0" _gui-text="Radius (px):">20.0</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 reldir="extensions" interpreter="python">fablabchemnitz_filletchamfer.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
294
extensions/fablabchemnitz_filletchamfer.py
Normal file
294
extensions/fablabchemnitz_filletchamfer.py
Normal file
@ -0,0 +1,294 @@
|
||||
#! /usr/bin/python2
|
||||
'''
|
||||
Copyright (C) 2018 Tao Wei taowei@buffalo.edu
|
||||
|
||||
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.
|
||||
'''
|
||||
import inkex, simplestyle, math
|
||||
from simpletransform import computePointInNode
|
||||
import svgpathtools
|
||||
|
||||
KAPPA = 4/3. * (math.sqrt(2)-1)
|
||||
|
||||
def cround(cnumber, ndigits):
|
||||
return round(cnumber.real, ndigits) + round(cnumber.imag, ndigits)*1j
|
||||
|
||||
def round_seg(seg, ndigits):
|
||||
seg.start = cround(seg.start, ndigits)
|
||||
seg.end = cround(seg.end, ndigits)
|
||||
return seg
|
||||
|
||||
def round_path(p, ndigits=6):
|
||||
"""fix for precision issue"""
|
||||
for seg in p:
|
||||
round_seg(seg, ndigits)
|
||||
return p
|
||||
|
||||
def remove_zero_length_segments(p, eps=1e-6):
|
||||
"z will add a zero length line segment"
|
||||
return svgpathtools.Path(*filter(lambda seg: seg.length() > eps, p))
|
||||
|
||||
def iscontinuous(p):
|
||||
for seg1, seg2 in zip(p[:-1], p[1:]):
|
||||
if abs(seg1.end-seg2.start) >= 1e-6:
|
||||
return False
|
||||
return True
|
||||
|
||||
def isclosedac(p):
|
||||
return abs(p.start-p.end) < 1e-6
|
||||
|
||||
def isclosed(p):
|
||||
assert iscontinuous(p)
|
||||
return isclosedac(p)
|
||||
|
||||
from svgpathtools.path import Line, CubicBezier, QuadraticBezier, Arc
|
||||
|
||||
def d_str(self, useSandT=False, use_closed_attrib=False, rel=False):
|
||||
"""Returns a path d-string for the path object.
|
||||
For an explanation of useSandT and use_closed_attrib, see the
|
||||
compatibility notes in the README."""
|
||||
|
||||
if use_closed_attrib:
|
||||
self_closed = self.iscontinuous() and self.isclosed()
|
||||
if self_closed:
|
||||
segments = self[:-1]
|
||||
else:
|
||||
segments = self[:]
|
||||
else:
|
||||
self_closed = False
|
||||
segments = self[:]
|
||||
|
||||
current_pos = None
|
||||
parts = []
|
||||
previous_segment = None
|
||||
end = self[-1].end
|
||||
|
||||
for segment in segments:
|
||||
seg_start = segment.start
|
||||
# If the start of this segment does not coincide with the end of
|
||||
# the last segment or if this segment is actually the close point
|
||||
# of a closed path, then we should start a new subpath here.
|
||||
if current_pos != seg_start or \
|
||||
(self_closed and seg_start == end and use_closed_attrib):
|
||||
if rel:
|
||||
_seg_start = seg_start - current_pos if current_pos is not None else seg_start
|
||||
else:
|
||||
_seg_start = seg_start
|
||||
parts.append('M {},{}'.format(_seg_start.real, _seg_start.imag))
|
||||
|
||||
if isinstance(segment, Line):
|
||||
if rel:
|
||||
_seg_end = segment.end - seg_start
|
||||
else:
|
||||
_seg_end = segment.end
|
||||
parts.append('L {},{}'.format(_seg_end.real, _seg_end.imag))
|
||||
elif isinstance(segment, CubicBezier):
|
||||
if useSandT and segment.is_smooth_from(previous_segment,
|
||||
warning_on=False):
|
||||
if rel:
|
||||
_seg_control2 = segment.control2 - seg_start
|
||||
_seg_end = segment.end - seg_start
|
||||
else:
|
||||
_seg_control2 = segment.control2
|
||||
_seg_end = segment.end
|
||||
args = (_seg_control2.real, _seg_control2.imag,
|
||||
_seg_end.real, _seg_end.imag)
|
||||
parts.append('S {},{} {},{}'.format(*args))
|
||||
else:
|
||||
if rel:
|
||||
_seg_control1 = segment.control1 - seg_start
|
||||
_seg_control2 = segment.control2 - seg_start
|
||||
_seg_end = segment.end - seg_start
|
||||
else:
|
||||
_seg_control1 = segment.control1
|
||||
_seg_control2 = segment.control2
|
||||
_seg_end = segment.end
|
||||
args = (_seg_control1.real, _seg_control1.imag,
|
||||
_seg_control2.real, _seg_control2.imag,
|
||||
_seg_end.real, _seg_end.imag)
|
||||
parts.append('C {},{} {},{} {},{}'.format(*args))
|
||||
elif isinstance(segment, QuadraticBezier):
|
||||
if useSandT and segment.is_smooth_from(previous_segment,
|
||||
warning_on=False):
|
||||
if rel:
|
||||
_seg_end = segment.end - seg_start
|
||||
else:
|
||||
_seg_end = segment.end
|
||||
args = _seg_end.real, _seg_end.imag
|
||||
parts.append('T {},{}'.format(*args))
|
||||
else:
|
||||
if rel:
|
||||
_seg_control = segment.control - seg_start
|
||||
_seg_end = segment.end - seg_start
|
||||
else:
|
||||
_seg_control = segment.control
|
||||
_seg_end = segment.end
|
||||
args = (_seg_control.real, _seg_control.imag,
|
||||
_seg_end.real, _seg_end.imag)
|
||||
parts.append('Q {},{} {},{}'.format(*args))
|
||||
|
||||
elif isinstance(segment, Arc):
|
||||
if rel:
|
||||
_seg_end = segment.end - seg_start
|
||||
else:
|
||||
_seg_end = segment.end
|
||||
args = (segment.radius.real, segment.radius.imag,
|
||||
segment.rotation,int(segment.large_arc),
|
||||
int(segment.sweep),_seg_end.real, _seg_end.imag)
|
||||
parts.append('A {},{} {} {:d},{:d} {},{}'.format(*args))
|
||||
current_pos = segment.end
|
||||
previous_segment = segment
|
||||
|
||||
if self_closed:
|
||||
parts.append('Z')
|
||||
|
||||
s = ' '.join(parts)
|
||||
return s if not rel else s.lower()
|
||||
|
||||
class FilletChamfer(inkex.Effect):
|
||||
def __init__(self):
|
||||
inkex.Effect.__init__(self)
|
||||
self.OptionParser.add_option("-t", "--type",
|
||||
action="store", type="string",
|
||||
dest="fillet_type", default="fillet",
|
||||
help="Selects whether using fillet or chamfer")
|
||||
self.OptionParser.add_option("-R", "--radius",
|
||||
action="store", type="float",
|
||||
dest="radius", default=60.0,
|
||||
help="The radius")
|
||||
|
||||
def addEle(self, ele, parent, props):
|
||||
# https://inkscape.org/~pacogarcia/%E2%98%85new-version-of-shapes-extension
|
||||
elem = inkex.etree.SubElement(parent, ele)
|
||||
for n in props: elem.set(n,props[n])
|
||||
return elem
|
||||
|
||||
def circle(self, c, r):
|
||||
return svgpathtools.parse_path("m %f,%f a %f,%f 0 0 1 -%f,%f %f,%f 0 0 1 -%f,-%f %f,%f 0 0 1 %f,-%f %f,%f 0 0 1 %f,%f z" % tuple((c.real+r, c.imag) + (r,)*16))
|
||||
|
||||
def _calc_fillet_for_joint(self, p, i):
|
||||
seg1 = p[(i) % len(p)]
|
||||
seg2 = p[(i+1) % len(p)]
|
||||
|
||||
ori_p = svgpathtools.Path(seg1, seg2)
|
||||
new_p = svgpathtools.Path()
|
||||
|
||||
# ignore the node if G1 continuity
|
||||
tg1 = seg1.unit_tangent(1.0)
|
||||
tg2 = seg2.unit_tangent(0.0)
|
||||
cosA = abs(tg1.real * tg2.real + tg1.imag * tg2.imag)
|
||||
if abs(cosA - 1.0) < 1e-6:
|
||||
new_p.append(seg1.cropped(self._prev_t, 1.0))
|
||||
self._prev_t = 0.0
|
||||
if self._very_first_t is None:
|
||||
self._very_first_t = 1.0
|
||||
if not isclosedac(p) and i == len(p) - 2:
|
||||
new_p.append(seg2.cropped(0.0, 1.0)) # add last segment if not closed
|
||||
else:
|
||||
cir = self.circle(seg1.end, self.options.radius)
|
||||
# new_p.extend(cir)
|
||||
|
||||
intersects = ori_p.intersect(cir)
|
||||
if len(intersects) != 2:
|
||||
inkex.errormsg("Some fillet or chamfer may not be drawn: %d intersections!" % len(intersects))
|
||||
new_p.append(seg1.cropped(self._prev_t, 1.0))
|
||||
self._prev_t = 0.0
|
||||
if self._very_first_t is None:
|
||||
self._very_first_t = 1.0
|
||||
if not isclosedac(p) and i == len(p) - 2:
|
||||
new_p.append(seg2.cropped(0.0, 1.0)) # add last segment if not closed
|
||||
else:
|
||||
cb = []; segs = []; ts = []
|
||||
for (T1, seg1, t1), (T2, seg2, t2) in intersects:
|
||||
c1 = seg1.point(t1)
|
||||
tg1 = seg1.unit_tangent(t1) * (self.options.radius * KAPPA)
|
||||
cb.extend([c1, tg1])
|
||||
segs.append(seg1); ts.append(t1)
|
||||
|
||||
# cir1 = self.circle(c1, self.options.radius * KAPPA)
|
||||
# new_p.extend(cir1)
|
||||
# new_p.append(svgpathtools.Line(c1, c1+tg1))
|
||||
|
||||
assert len(cb) == 4
|
||||
new_p.append(segs[0].cropped(self._prev_t, ts[0]))
|
||||
if self.options.fillet_type == 'fillet':
|
||||
fillet = svgpathtools.CubicBezier(cb[0], cb[0]+cb[1], cb[2]-cb[3], cb[2])
|
||||
else:
|
||||
fillet = svgpathtools.Line(cb[0], cb[2])
|
||||
new_p.append(fillet)
|
||||
self._prev_t = ts[1]
|
||||
if self._very_first_t is None:
|
||||
self._very_first_t = ts[0]
|
||||
|
||||
if isclosedac(p) and i == len(p) - 1:
|
||||
new_p.append(segs[1].cropped(ts[1], self._very_first_t)) # update first segment if closed
|
||||
elif not isclosedac(p) and i == len(p) - 2:
|
||||
new_p.append(segs[1].cropped(ts[1], 1.0)) # add last segment if not closed
|
||||
|
||||
# # fix for the first segment
|
||||
# if p.isclosed():
|
||||
# new_p[0] = p[0].cropped(ts[1], self._very_first_t)
|
||||
|
||||
# new_p.append(segs[0].cropped(ts[0], 1.0))
|
||||
# new_p.append(segs[1].cropped(0.0, ts[1]))
|
||||
# if self.options.fillet_type == 'fillet':
|
||||
# fillet = svgpathtools.CubicBezier(cb[0], cb[0]+cb[1], cb[2]-cb[3], cb[2])
|
||||
# else:
|
||||
# fillet = svgpathtools.Line(cb[0], cb[2])
|
||||
# new_p.append(fillet.reversed())
|
||||
|
||||
return new_p
|
||||
|
||||
def add_fillet_to_path(self, d):
|
||||
p = svgpathtools.parse_path(d)
|
||||
p = remove_zero_length_segments(p) # for z, a zero length line segment is possibly added
|
||||
|
||||
if len(p) <= 1:
|
||||
return d
|
||||
|
||||
new_p = svgpathtools.Path()
|
||||
self._prev_t = 0 # used as cache
|
||||
self._very_first_t = None # update first segment if closed
|
||||
if isclosedac(p):
|
||||
for i in range(len(p)):
|
||||
new_p.extend(self._calc_fillet_for_joint(p, i))
|
||||
if not isclosedac(new_p):
|
||||
del new_p[0] # remove first segment if closed
|
||||
else:
|
||||
for i in range(len(p)-1):
|
||||
new_p.extend(self._calc_fillet_for_joint(p, i))
|
||||
new_p = round_path(new_p, 6)
|
||||
# inkex.errormsg(d_str(new_p, use_closed_attrib=True, rel=True))
|
||||
return d_str(new_p, use_closed_attrib=True, rel=True)
|
||||
|
||||
def effect(self):
|
||||
self.options.radius = self.unittouu(str(self.options.radius) + 'px')
|
||||
|
||||
if self.options.radius == 0:
|
||||
return
|
||||
|
||||
for id, node in self.selected.iteritems():
|
||||
_shape = inkex.etree.QName(node.tag).localname
|
||||
if _shape != "path":
|
||||
inkex.errormsg("Fillet and chamfer only operates on path: %s is %s" % (id, _shape))
|
||||
else:
|
||||
# inkex.errormsg(inkex.etree.tostring(node))
|
||||
attrib = {k:v for k,v in node.attrib.items()}
|
||||
attrib['d'] = self.add_fillet_to_path(attrib['d'])
|
||||
self.addEle(inkex.addNS('path','svg'), node.getparent(), attrib)
|
||||
|
||||
if __name__ == '__main__':
|
||||
e = FilletChamfer()
|
||||
e.affect()
|
Reference in New Issue
Block a user