mightyscape-1.2/extensions/fablabchemnitz/fillet_and_chamfer/fillet_and_chamfer.py

293 lines
12 KiB
Python
Raw Normal View History

2022-09-02 18:19:57 +02:00
#!/usr/bin/env python3
'''
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
import math
import svgpathtools
from lxml import etree
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 FilletAndChamfer(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("-t", "--fillet_type", default="fillet", help="Selects whether using fillet or chamfer")
pars.add_argument("-R", "--radius", type=float, default=60.0, help="The radius")
pars.add_argument('--unit', default='px', help='units of measurement')
pars.add_argument("--remove", type=inkex.Boolean, default=False, help="If True, control object will be removed")
def addEle(self, ele, parent, props):
# https://inkscape.org/~pacogarcia/%E2%98%85new-version-of-shapes-extension
elem = 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.svg.unittouu(str(self.options.radius) + self.options.unit)
if self.options.radius == 0:
return
for id, node in self.svg.selected.items():
_shape = 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(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 self.options.remove:
node.delete()
if __name__ == '__main__':
FilletAndChamfer().run()