several fixes and more extensions from 1.1 back again

This commit is contained in:
Mario Voigt 2022-09-02 03:15:14 +02:00
parent 1f3e8b9cb5
commit a38a160484
74 changed files with 11200 additions and 3 deletions

View File

@ -17,7 +17,7 @@
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Archimedes Spiral</label>
<label>2020 - 2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<label>2020 - 2022 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/archimedesspiral</label>

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Buxtronix Living Hinges</name>
<id>fablabchemnitz.de.buxtronix_living_hinges</id>
<param name="tab" type="notebook">
<page name="straight_lattice" gui-text="Straight lattice">
<image>images/straight-lattice.png</image>
<param name="sl_length" type="int" min="1" max="200" gui-text="Length of elements" appearance="full">20</param>
<param name="sl_spacing" type="float" min="0.1" max="10.0" precision="1" gui-text="Vertical element spacing" appearance="full">2.0</param>
<param name="sl_gap" type="float" min="0.0" max="10.0" precision="1" gui-text="Element height" appearance="full">0.0</param>
<label xml:space="preserve">
</label>
</page>
<page name="diamond_lattice" gui-text="Diamond lattice">
<image>images/diamond-lattice.png</image>
<param name="dl_length" type="int" min="1" max="200" gui-text="Length of elements" appearance="full">20</param>
<param name="dl_spacing" type="float" min="0.1" max="10.0" precision="1" gui-text="Vertical element spacing" appearance="full">4.0</param>
<param name="dl_curve" type="float" min="-1.0" max="1.0" precision="1" gui-text="Curve of each element" appearance="full">0.5</param>
<label xml:space="preserve">
</label>
</page>
<page name="cross_lattice" gui-text="Cross lattice">
<image>images/cross-lattice.png</image>
<param name="cl_length" type="int" min="1" max="200" gui-text="Length of elements" appearance="full">20</param>
<param name="cl_spacing" type="float" min="0.1" max="10.0" precision="1" gui-text="Vertical element spacing" appearance="full">6.0</param>
<label xml:space="preserve">
</label>
</page>
<page name="wavy_lattice" gui-text="Wavy lattice">
<image>images/wavy-lattice.png</image>
<param name="wl_length" type="int" min="1" max="200" gui-text="Length of elements" appearance="full">29</param>
<param name="wl_spacing" type="float" min="0.1" max="10.0" precision="1" gui-text="Vertical element spacing" appearance="full">4.0</param>
<label xml:space="preserve">
</label>
</page>
<page name="about" gui-text="About/Help">
<label xml:space="preserve">
This extension renders Living Hinges, aka lattice hinges aka laser kerf bending.
These patterns allow otherwise rigid materials (eg wood, acrylic) to have bends formed in them.
</label>
<image>images/about.png</image>
<label appearance="url">https://github.com/buxtronix/living-hinge</label>
</page>
</param>
<param name="width" type="float" min="0" max="10000" gui-text="Width">100</param>
<param name="height" type="float" min="0" max="10000" gui-text="Height">100</param>
<param name="unit" gui-text="Unit for width/height" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
</param>
<label>If any object(s) are currently selected, their bounding boxes will be filled with the pattern
instead of using the above Width/Height. Slider dimensions are all millimetres.</label>
<separator />
<param name="swatch" type="bool" gui-text="Draw swatch card">false</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">buxtronix_living_hinges.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,448 @@
#!/usr/bin/env python3
import inkex
cut_colour = '#ff0000'
engrave_colour = '#0000ff'
class Generator(object):
"""A generic generator, subclassed for each different lattice style."""
def __init__(self, x, y, width, height, stroke_width, svg, e_length, p_spacing):
self.x = x
self.y = y
self.width = width
self.height = height
self.stroke_width = stroke_width
self.svg = svg
self.canvas = self.svg.get_current_layer()
self.e_length = e_length
self.e_height = 0 # Provided by sub-classes.
self.p_spacing = p_spacing
self.fixed_commands = ""
def draw_one(self, x, y):
return "M %f,%f %s" % (x, y, self.fixed_commands)
def parameter_text(self):
return "length: %.f spacing: %.1f" % (
self.e_length,
self.p_spacing,
)
def draw_swatch(self):
border = self.canvas.add(inkex.PathElement())
# Curve radius
cr = self.svg.unittouu('10mm')
# Swatch padding
sp = self.svg.unittouu('30mm')
# Handle length
hl = cr/2
path_command = (
'm %f,%f l %f,%f c %f,%f %f,%f %f,%f'
'l %f,%f c %f,%f %f,%f %f,%f '
'l %f,%f c %f,%f %f,%f %f,%f '
'l %f,%f c %f,%f %f,%f %f,%f ') % (
cr, 0,
self.width - 2*cr, 0,
hl, 0,
cr, cr-hl,
cr, cr,
0, self.height - 2*cr + 1.5*sp,
0, cr/2,
0-cr+hl, cr,
0-cr, cr,
0-self.width + 2*cr, 0,
0-hl, 0,
0-cr, 0-cr+hl,
0-cr, 0-cr,
0, 0-self.height - 1.5*sp + 2*cr,
0, 0-hl,
cr-hl, 0-cr,
cr, 0-cr
)
style = {
"stroke": cut_colour,
"stroke-width": str(self.stroke_width),
"fill": "none",
}
border.update(**{"style": style, "inkscape:label": "lattice_border", "d": path_command})
c = self.canvas.add(inkex.Circle(
style=str(inkex.Style(style)),
cx=str(cr),
cy=str(cr),
r=str(self.svg.unittouu('4mm'))))
self.y += sp
text_style = {
'fill': engrave_colour,
'font-size': '9px',
'font-family': 'sans-serif',
'text-anchor': 'middle',
'text-align': 'center',
}
text = self.canvas.add(
inkex.TextElement(
style=str(inkex.Style(text_style)),
x=str(self.x + self.width/2),
y=str(self.y - sp/2)))
text.text = "Style: %s" % self.name
text_style['font-size'] = "3px"
text = self.canvas.add(
inkex.TextElement(
style=str(inkex.Style(text_style)),
x=str(self.x + self.width/2),
y=str(self.y - sp/4)))
text.text = self.parameter_text()
text = self.canvas.add(
inkex.TextElement(
style=str(inkex.Style(text_style)),
x=str(self.x + self.width/2),
y=str(self.y +self.height + sp/4)))
text.text = "https://github.com/buxtronix/living-hinge"
def generate(self, swatch):
if swatch:
self.draw_swatch()
# Round width/height to integer number of patterns.
self.e_length = self.width / max(round(self.width / self.e_length), 1.0)
self.e_height = self.height / max(round(self.height / self.e_height), 1.0)
self.prerender()
style = {
"stroke": cut_colour,
"stroke-width": str(self.stroke_width),
"fill": "none",
}
path_command = ""
y = self.y
while y < self.y + self.height:
x = self.x
while x < self.x + self.width:
path_command = "%s %s " % (path_command, self.draw_one(x, y))
x += self.e_length
y += self.e_height
link = self.canvas.add(inkex.PathElement())
link.update(**{"style": style, "inkscape:label": "lattice", "d": path_command})
link.insert(0, inkex.Desc("%s hinge %s" % (self.name, self.parameter_text())))
class StraightLatticeGenerator(Generator):
def __init__(self, *args, **kwargs):
super(StraightLatticeGenerator, self).__init__(*args)
self.link_gap = kwargs['link_gap']
self.e_height = 2 * self.p_spacing
self.name = "straight"
def prerender(self):
self.e_height = 2 * self.p_spacing
w = self.e_length
lg = self.link_gap
if lg < 0.1:
# Single line for 0 height gap.
self.fixed_commands = " m %f,%f h %f m %f,%f h %f m %f,%f h %f" % (
0, self.e_height / 2,
w * 2 / 5,
0 - w / 5, 0 - self.e_height / 2,
w * 3 / 5,
0 - w / 5, self.e_height / 2,
w * 2 / 5,
)
else:
self.fixed_commands = (
" m %f,%f h %f v %f h %f"
" m %f,%f h %f v %f h %f v %f"
" m %f,%f h %f v %f h %f "
) % (
0,
self.e_height / 2,
w * 2 / 5,
lg,
0 - w * 2 / 5,
w / 8,
0 - lg - self.e_height / 2,
w * 3 / 4,
lg,
0 - w * 3 / 4,
0 - lg,
w * 7 / 8,
lg + self.e_height / 2,
0 - w * 2 / 5,
0 - lg,
w * 2 / 5,
)
def parameter_text(self):
text = super(StraightLatticeGenerator, self).parameter_text()
return "%s element_height: %.1f" % (text, self.link_gap)
class DiamondLatticeGenerator(Generator):
def __init__(self, *args, **kwargs):
super(DiamondLatticeGenerator, self).__init__(*args)
self.e_height = self.p_spacing
self.diamond_curve = kwargs['diamond_curve']
self.name = "diamond"
def prerender(self):
h = self.e_height
w = self.e_length
# Diamond curve
dc = 0-self.diamond_curve
# Horiz handle length.
hhl = abs(dc * w * 0.2)
# Endpoint horiz handle length
ehhl = hhl if dc > 0 else 0
# Vert handle length
vhl = abs(dc * h / 8) if dc < 0 else 0
# Left
self.fixed_commands = " m %f,%f c %f,%f %f,%f %f,%f c %f,%f %f,%f %f,%f " % (
0, h / 4,
hhl, 0,
w * 0.4 - ehhl, h / 4 - vhl,
w * 0.4, h / 4,
0 - ehhl, vhl,
0 - (w * 0.4 - hhl), h / 4,
0 - w * 0.4, h / 4,
)
# Bottom
self.fixed_commands = "%s m %f,%f c %f,%f %f,%f %f,%f s %f,%f %f,%f " % (
self.fixed_commands,
w * 0.1, h / 4,
ehhl, 0 - vhl,
w * 0.4 - hhl, 0 - h / 4,
w * 0.4, 0 - h / 4,
w * 0.4 - ehhl, h / 4 - vhl,
w * 0.4, h / 4,
)
# Top
self.fixed_commands = "%s m %f,%f c %f,%f %f,%f %f,%f s %f,%f %f,%f " % (
self.fixed_commands,
0 - w * 0.8, 0 - h,
ehhl, vhl,
w * 0.4 - hhl, h / 4,
w * 0.4, h / 4,
w * 0.4 - ehhl, 0 - h / 4 + vhl,
w * 0.4, 0 - h / 4,
)
# Right
self.fixed_commands = "%s m %f,%f c %f,%f %f,%f %f,%f c %f,%f %f,%f %f,%f " % (
self.fixed_commands,
w * 0.1, h *0.75,
0 - hhl, 0,
(0 - w * 0.4) + ehhl, 0 - h / 4 + vhl,
0 - w * 0.4, 0 - h / 4,
ehhl, 0 - vhl,
w * 0.4 - hhl, 0 - h / 4,
w * 0.4, 0 - h / 4,
)
def draw_one(self, x, y):
return "M %f,%f %s" % (x, y, self.fixed_commands)
def parameter_text(self):
text = super(DiamondLatticeGenerator, self).parameter_text()
return "%s curve: %.1f" % (text, self.diamond_curve)
class CrossLatticeGenerator(Generator):
def __init__(self, *args):
super(CrossLatticeGenerator, self).__init__(*args)
self.e_height = self.p_spacing
self.name = "cross"
def prerender(self):
l = self.e_length
h = self.e_height
self.fixed_commands = (
"m %f,%f l %f,%f l %f,%f m %f,%f l %f,%f"
"m %f,%f l %f,%f l %f,%f l %f,%f "
"m %f,%f l %f,%f l %f,%f l %f,%f "
"m %f,%f l %f,%f l %f,%f m %f,%f l %f,%f"
) % (
# Left
0, h * 0.5,
l * 0.2, 0,
l * 0.2, 0 - h * 0.3,
0 - l * 0.2, h * 0.3,
l * 0.2, h * 0.3,
# Top
0 - l * 0.3, 0 - h * 0.5,
l * 0.2, 0 - h * 0.3,
l * 0.4, 0,
l * 0.2, h * 0.3,
# Bottom
0, h * 0.4,
0 - l * 0.2, h * 0.3,
0 - l * 0.4, 0,
0 - l * 0.2, 0 - h * 0.3,
# Right
l * 0.5, 0 - h * 0.5,
l * 0.2, h * 0.3,
0 - l * 0.2, h * 0.3,
l * 0.2, 0 - h * 0.3,
l * 0.2, 0,
)
class WavyLatticeGenerator(Generator):
def __init__(self, *args, **kwargs):
super(WavyLatticeGenerator, self).__init__(*args)
self.e_height = self.p_spacing
self.name = "wavy"
def prerender(self):
h = self.e_height
w = self.e_length
self.fixed_commands = (
" m %f,%f h %f c %f,%f %f,%f %f,%f h %f "
"m %f,%f h %f c %f,%f %f,%f %f,%f h %f "
) % (
0, h, # Start of element (left)
w * 0.1, # Short horiz line.
w * 0.1, 0, # Control 1
w * 3 / 40, 0 - h / 2, # Control 2
w * 0.2, 0 - h / 2, # Curve top.
w * 0.175, # Top horiz line.
0 - w * 0.1, 0 - h / 2, # Move to higher line.
w * 0.3, # Long higher horiz line.
w / 5, 0, # Control 1
w / 10, h, # Control 2
w * 0.25, h, # Curve down.
w * 0.075, # End horiz line.
)
class BuxtronixLivingHinges(inkex.EffectExtension):
"""
Extension to create laser cut bend lattices.
"""
def add_arguments(self, pars):
pars.add_argument("--tab", help="Bend pattern to generate")
pars.add_argument("--unit", help="Units for dimensions")
pars.add_argument("--swatch", type=inkex.Boolean, help="Draw as a swatch card")
pars.add_argument("--width", type=float, default=300, help="Width of pattern")
pars.add_argument("--height", type=float, default=100, help="Height of pattern")
pars.add_argument("--sl_length", type=int, default=20, help="Length of links")
pars.add_argument("--sl_gap", type=float, default=0.5, help="Gap between links")
pars.add_argument("--sl_spacing", type=float, default=20, help="Spacing of links")
pars.add_argument("--dl_curve", type=float, default=0.5, help="Curve of diamonds")
pars.add_argument("--dl_length", type=float, default=24, help="Length of diamonds")
pars.add_argument("--dl_spacing", type=float, default=4, help="Spacing of diamonds")
pars.add_argument("--cl_length", type=float, default=24, help="Length of combs")
pars.add_argument("--cl_spacing", type=float, default=6, help="Spacing of combs")
pars.add_argument("--wl_length", type=int, default=20, help="Length of links")
pars.add_argument("--wl_interval", type=int, default=30, help="Interval between links")
pars.add_argument("--wl_spacing", type=float, default=0.5, help="Spacing between links")
def convert(self, value):
return self.svg.unittouu(str(value) + self.options.unit)
def convertmm(self, value):
return self.svg.unittouu('%fmm' % value)
def effect(self):
stroke_width = self.svg.unittouu("0.2mm")
self.options.width = self.convert(self.options.width)
self.options.height = self.convert(self.options.height)
def draw_one(x, y):
if self.options.tab == "straight_lattice":
generator = StraightLatticeGenerator(
x,
y,
self.options.width,
self.options.height,
stroke_width,
self.svg,
self.convertmm(self.options.sl_length),
self.convertmm(self.options.sl_spacing),
link_gap=self.convertmm(self.options.sl_gap),
)
elif self.options.tab == "diamond_lattice":
generator = DiamondLatticeGenerator(
x,
y,
self.options.width,
self.options.height,
stroke_width,
self.svg,
self.convertmm(self.options.dl_length),
self.convertmm(self.options.dl_spacing),
diamond_curve=self.options.dl_curve,
)
elif self.options.tab == "cross_lattice":
generator = CrossLatticeGenerator(
x,
y,
self.options.width,
self.options.height,
stroke_width,
self.svg,
self.convertmm(self.options.cl_length),
self.convertmm(self.options.cl_spacing),
)
elif self.options.tab == "wavy_lattice":
generator = WavyLatticeGenerator(
x,
y,
self.options.width,
self.options.height,
stroke_width,
self.svg,
self.convertmm(self.options.wl_length),
self.convertmm(self.options.wl_spacing),
)
else:
inkex.errormsg("Select a valid pattern tab before rendering.")
return
generator.generate(self.options.swatch)
if self.options.swatch or not self.svg.selected:
draw_one(0, 0)
else:
for elem in self.svg.selected.values():
bbox = elem.bounding_box()
self.options.width = self.svg.unittouu(bbox.width)
self.options.height = self.svg.unittouu(bbox.height)
x = self.svg.unittouu(bbox.x.minimum)
y = self.svg.unittouu(bbox.y.minimum)
draw_one(x, y)
BuxtronixLivingHinges().run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="60mm"
height="14mm"
viewBox="0 0 60 14"
version="1.1"
id="svg961"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
sodipodi:docname="diamond-lattice.svg">
<defs
id="defs955" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.8484056"
inkscape:cx="155.04988"
inkscape:cy="77.608175"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1006"
inkscape:window-x="0"
inkscape:window-y="40"
inkscape:window-maximized="1" />
<metadata
id="metadata958">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 46.666285,12.224966 c 1.729044,0 4.034437,0.720435 5.76348,0.720435 1.729044,0 4.034437,-0.720435 5.763481,-0.720435"
id="path5440" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 32.257584,12.224966 c 1.729044,0 4.034437,0.720435 5.76348,0.720435 1.729044,0 4.034437,-0.720435 5.763481,-0.720435"
id="path5430" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 46.666285,9.3432239 c 1.729044,0 4.034437,0.7204361 5.76348,0.7204361 1.729044,0 4.034437,-0.7204361 5.763481,-0.7204361"
id="path5390" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 46.666285,12.224966 c 1.729044,0 4.034437,-0.720435 5.76348,-0.720435 1.729044,0 4.034437,0.720435 5.763481,0.720435"
id="path5388" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 59.634116,10.06366 c -1.729044,0 -4.034437,0.720435 -5.763481,0.720435 1.729044,0 4.034437,0.720435 5.763481,0.720435"
id="path5386" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 45.225415,10.06366 c 1.729044,0 4.034437,0.720435 5.763481,0.720435 -1.729044,0 -4.034437,0.720435 -5.763481,0.720435"
id="path5384" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="M 45.225415,9.3432239"
id="path5382" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 32.257584,9.3432239 c 1.729044,0 4.034437,0.7204361 5.76348,0.7204361 1.729044,0 4.034437,-0.7204361 5.763481,-0.7204361"
id="path5380" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 32.257584,12.224966 c 1.729044,0 4.034437,-0.720435 5.76348,-0.720435 1.729044,0 4.034437,0.720435 5.763481,0.720435"
id="path5378" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 45.225415,10.06366 c -1.729044,0 -4.034437,0.720435 -5.763481,0.720435 1.729044,0 4.034437,0.720435 5.763481,0.720435"
id="path5376" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 30.816714,10.06366 c 1.729044,0 4.034437,0.720435 5.763481,0.720435 -1.729044,0 -4.034437,0.720435 -5.763481,0.720435"
id="path5374" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="M 30.816714,9.3432239"
id="path5372" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 46.666285,6.4614845 c 1.729044,0 4.034437,0.720435 5.76348,0.720435 1.729044,0 4.034437,-0.720435 5.763481,-0.720435"
id="path5340" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 46.666285,9.3432239 c 1.729044,0 4.034437,-0.7204345 5.76348,-0.7204345 1.729044,0 4.034437,0.7204345 5.763481,0.7204345"
id="path5338" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 59.634116,7.1819195 c -1.729044,0 -4.034437,0.7204349 -5.763481,0.7204349 1.729044,0 4.034437,0.720435 5.763481,0.720435"
id="path5336" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 45.225415,7.1819195 c 1.729044,0 4.034437,0.7204349 5.763481,0.7204349 -1.729044,0 -4.034437,0.720435 -5.763481,0.720435"
id="path5334" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="M 45.225415,6.4614845"
id="path5332" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 32.257584,6.4614845 c 1.729044,0 4.034437,0.720435 5.76348,0.720435 1.729044,0 4.034437,-0.720435 5.763481,-0.720435"
id="path5330" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 32.257584,9.3432239 c 1.729044,0 4.034437,-0.7204345 5.76348,-0.7204345 1.729044,0 4.034437,0.7204345 5.763481,0.7204345"
id="path5328" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 45.225415,7.1819195 c -1.729044,0 -4.034437,0.7204349 -5.763481,0.7204349 1.729044,0 4.034437,0.720435 5.763481,0.720435"
id="path5326" />
<path
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="m 30.816714,7.1819195 c 1.729044,0 4.034437,0.7204349 5.763481,0.7204349 -1.729044,0 -4.034437,0.720435 -5.763481,0.720435"
id="path5324" />
<path
id="path892"
style="fill:none;stroke:#ff0000;stroke-width:0.144087"
d="M 30.333099,5.2546335 M 0.54402462,0.32757335 m 0,0.68989875 c 0,0 5.51918898,0.6898986 5.51918898,0.6898986 0,0 -5.51918898,0.6898986 -5.51918898,0.6898986 M 14.341997,1.0174721 c 0,0 -5.5191888,0.6898986 -5.5191888,0.6898986 0,0 5.5191888,0.6898986 5.5191888,0.6898986 M 1.9238218,3.087168 c 0,0 5.5191892,-0.6898987 5.5191892,-0.6898987 0,0 5.519189,0.6898987 5.519189,0.6898987 M 1.9238218,0.32757335 c 0,0 5.5191892,0.68989875 5.5191892,0.68989875 0,0 5.519189,-0.68989875 5.519189,-0.68989875 m 1.379797,0 m 0,0.68989875 c 0,0 5.519189,0.6898986 5.519189,0.6898986 0,0 -5.519189,0.6898986 -5.519189,0.6898986 M 28.13997,1.0174721 c 0,0 -5.519189,0.6898986 -5.519189,0.6898986 0,0 5.519189,0.6898986 5.519189,0.6898986 M 15.721795,3.087168 c 0,0 5.519189,-0.6898987 5.519189,-0.6898987 0,0 5.519189,0.6898987 5.519189,0.6898987 M 15.721795,0.32757335 c 0,0 5.519189,0.68989875 5.519189,0.68989875 0,0 5.519189,-0.68989875 5.519189,-0.68989875 m 1.379797,0 m 0,0.68989875 c 0,0 5.519189,0.6898986 5.519189,0.6898986 0,0 -5.519189,0.6898986 -5.519189,0.6898986 M 0.54402462,3.087168 m 0,0.6898986 c 0,0 5.51918898,0.6898986 5.51918898,0.6898986 0,0 -5.51918898,0.6898987 -5.51918898,0.6898987 M 14.341997,3.7770666 c 0,0 -5.5191888,0.6898986 -5.5191888,0.6898986 0,0 5.5191888,0.6898987 5.5191888,0.6898987 M 1.9238218,5.8467625 c 0,0 5.5191892,-0.6898986 5.5191892,-0.6898986 0,0 5.519189,0.6898986 5.519189,0.6898986 M 1.9238218,3.087168 c 0,0 5.5191892,0.6898986 5.5191892,0.6898986 0,0 5.519189,-0.6898986 5.519189,-0.6898986 m 1.379797,0 m 0,0.6898986 c 0,0 5.519189,0.6898986 5.519189,0.6898986 0,0 -5.519189,0.6898987 -5.519189,0.6898987 M 28.13997,3.7770666 c 0,0 -5.519189,0.6898986 -5.519189,0.6898986 0,0 5.519189,0.6898987 5.519189,0.6898987 M 15.721795,5.8467625 c 0,0 5.519189,-0.6898986 5.519189,-0.6898986 0,0 5.519189,0.6898986 5.519189,0.6898986 M 15.721795,3.087168 c 0,0 5.519189,0.6898986 5.519189,0.6898986 0,0 5.519189,-0.6898986 5.519189,-0.6898986 m 1.379797,0 m 0,0.6898986 c 0,0 5.519189,0.6898986 5.519189,0.6898986 0,0 -5.519189,0.6898987 -5.519189,0.6898987" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,21 @@
[
{
"name": "Buxtronix Living Hinges",
"id": "fablabchemnitz.de.buxtronix_living_hinges",
"path": "buxtronix_living_hinges",
"dependent_extensions": null,
"original_name": "Living Hinges",
"original_id": "net.buxtronix.living_hinge",
"license": "GNU GPL v3",
"license_url": "https://github.com/buxtronix/living-hinge/blob/master/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/buxtronix_living_hinges",
"fork_url": "https://github.com/buxtronix/living-hinge",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Buxtronix+Living+Hinges",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/buxtronix",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Chip Scratches</name>
<id>fablabchemnitz.de.chip_scratches</id>
<param type="notebook" name="Nmain">
<page name="Overall" gui-text="Overall">
<!-- Dummy line for index. Nmain Current index tab "page" -->
<param name="pgsizep" type="bool" gui-text="Default rectangle to page size?">true</param>
<param name="rx" type="int" max="10000" gui-text="Width">1000</param>
<param name="ry" type="int" max="10000" gui-text="Height">1000</param>
<param name="mainSize" type="float" max="100.0" gui-text="Size of objects">1.0</param>
<param name="mainNum" type="int" max="5000" gui-text="Number of objects">200</param>
</page>
<page name="Scratches" gui-text="Scratches">
<param name="honp" type="bool" gui-text="Enable scratches">true</param>
<param name="hsize" type="float" max="100.0" min="-100.0" gui-text="Size of scratches">2.0</param>
<param name="hgrow" type="float" max="100.0" min="-100.0" gui-text="Grow scratches with distance">0.0</param>
<param name="hnum" type="float" max="100.0" gui-text="Number of scratches">0.2</param>
<param name="hrad" type="bool" gui-text="Angle scratches toward center">false</param>
<param name="hang" type="float" max="180.0" min="-180.0" gui-text="Angle from radius">90.</param>
<param name="hcurve" type="float" max="100.0" min="-100.0" gui-text="Change angle with distance">0.0</param>
<param name="hgrad" type="bool" gui-text="Use density gradient">false</param>
</page>
<page name="Chips" gui-text="Chips">
<param name="conp" type="bool" gui-text="Enable chips">true</param>
<param name="csize" type="float" max="100.0" min="-100.0" gui-text="Size of chips">1.0</param>
<param name="cgrow" type="float" max="100.0" min="-100.0" gui-text="Grow chips with distance">0.0</param>
<param name="cnum" type="float" max="100.0" gui-text="Number of chips">1.0</param>
<param name="cgrad" type="bool" gui-text="Use density gradient">false</param>
</page>
<page name="Specks" gui-text="Specks">
<param name="sonp" type="bool" gui-text="Enable specks">true</param>
<param name="ssize" type="float" max="100.0" min="-100.0" gui-text="Size of specks">1.0</param>
<param name="sgrow" type="float" max="100.0" min="-100.0" gui-text="Grow specks with distance">0.0</param>
<param name="snum" type="float" max="100.0" gui-text="Number of specks">10.0</param>
<param name="sgrad" type="bool" gui-text="Use density gradient">false</param>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Shape Generators">
<submenu name="Streaks And Blobs" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">chip_scratches.py</command>
</script>
</inkscape-extension>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
[
{
"name": "Chip Scratches",
"id": "fablabchemnitz.de.chip_scratches",
"path": "chip_scratches",
"dependent_extensions": null,
"original_name": "ChipScratches",
"original_id": "ca.sfu.AT.kurn.ChipScratches",
"license": "GNU GPL v2",
"license_url": "https://inkscape.org/~kurn/%E2%98%85chipscratches",
"comment": "ported to Inkscape v1 by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/chip_scratches",
"fork_url": "https://inkscape.org/~kurn/%E2%98%85chipscratches",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Chip+Scratches",
"inkscape_gallery_url": null,
"main_authors": [
"inkscape.org/kurn",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Ellipse By Five Points (Replaced by LPE)</name>
<id>fablabchemnitz.de.ellipse_by_five_points</id>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">ellipse_by_five_points.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,230 @@
#!/usr/bin/env python3
# Copyright (c) 2012 Stuart Pernsteiner
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import sys
import inkex
from inkex.paths import Path
import gettext
_ = gettext.gettext
from copy import deepcopy
import math
from math import sqrt
class EllipseByFivePoints(inkex.EffectExtension):
def effect(self):
if len(self.svg.selected) == 0:
sys.exit(_("Error: You must select at least one path"))
for pathId in self.svg.selected:
path = self.svg.selected[pathId]
pathdata = Path(path.get('d')).to_arrays()
if len(pathdata) < 5:
sys.exit(_("Error: The selected path has %d points, " +
"but 5 are needed.") % len(pathdata))
points = []
for i in range(5):
# pathdata[i] is the i'th segment of the path
# pathdata[i][1] is the list of coordinates for the segment
# pathdata[i][1][-2] is the x-coordinate of the last x,y pair
# in the segment definition
segpoints = pathdata[i][1]
x = segpoints[-2]
y = segpoints[-1]
points.append((x,y))
conic = solve_conic(points)
[a,b,c,d,e,f] = conic
if bareiss_determinant([[a,b/2,d/2],[b/2,c,e/2],[d/2,e/2,f]]) == 0 or a*c - b*b/4 <= 0:
sys.exit(_("Error: Could not find an ellipse that passes " +
"through the provided points"))
center = ellipse_center(conic)
[ad1, ad2] = ellipse_axes(conic)
al1 = ellipse_axislen(conic, center, ad1)
al2 = ellipse_axislen(conic, center, ad2)
# Create an <svg:ellipse> object with the appropriate cx,cy and
# with the major axis in the x direction. Then add a transform to
# rotate it to the correct angle.
if al1 > al2:
major_dir = ad1
major_len = al1
minor_len = al2
else:
major_dir = ad2
major_len = al2
minor_len = al1
# add sodipodi magic to turn the path into an ellipse
def sodi(x):
return inkex.addNS(x, 'sodipodi')
path.set(sodi('cx'), str(center[0]))
path.set(sodi('cy'), str(center[1]))
path.set(sodi('rx'), str(major_len))
path.set(sodi('ry'), str(minor_len))
path.set(sodi('type'), 'arc')
#inkex.utils.debug(str(center[0]))
#inkex.utils.debug(str(center[1]))
angle = math.atan2(major_dir[1], major_dir[0])
if angle > math.pi / 2:
angle -= math.pi
if angle < -math.pi / 2:
angle += math.pi
path.apply_transform()
path.set('transform', 'rotate(%f %f %f)' % (angle * 180 / math.pi, center[0], center[1]))
# NASTY BUGFIX: We do apply_transform and path.set twice because otherwise the ellipse will not be rendered correctly. Reason unknown!
path.apply_transform()
path.set('transform', 'rotate(%f %f %f)' % (angle * 180 / math.pi, center[0], center[1]))
def solve_conic(pts):
# Find the equation of the conic section passing through the five given
# points.
#
# This technique is from
# http://math.fullerton.edu/mathews/n2003/conicfit/ConicFitMod/Links/ConicFitMod_lnk_9.html
# (retrieved 31 Jan 2012)
rowmajor_matrix = []
for i in range(5):
(x,y) = pts[i]
row = [x*x, x*y, y*y, x, y, 1]
rowmajor_matrix.append(row)
full_matrix = []
for i in range(6):
col = []
for j in range(5):
col.append(rowmajor_matrix[j][i])
full_matrix.append(col);
coeffs = []
sign = 1
for i in range(6):
mat = []
for j in range(6):
if j == i:
continue
mat.append(full_matrix[j])
coeffs.append(bareiss_determinant(mat) * sign)
sign = -sign
return coeffs
def bareiss_determinant(mat_orig):
# Compute the determinant of the matrix using Bareiss's algorithm. It
# doesn't matter whether 'mat' is in row-major or column-major layout,
# because det(A) = det(A^T)
# Algorithm from:
# Yap, Chee, "Linear Systems", Fundamental Problems of Algorithmic Algebra
# Lecture X, Section 2
# http://cs.nyu.edu/~yap/book/alge/ftpSite/l10.ps.gz
mat = deepcopy(mat_orig);
size = len(mat)
last_akk = 1
for k in range(size-1):
if last_akk == 0:
return 0
for i in range(k+1, size):
for j in range(k+1, size):
mat[i][j] = (mat[i][j] * mat[k][k] - mat[i][k] * mat[k][j]) / last_akk
last_akk = mat[k][k]
return mat[size-1][size-1]
def ellipse_center(conic):
# From
# http://en.wikipedia.org/wiki/Matrix_representation_of_conic_sections#Center
[a,b,c,d,e,f] = conic
x = (b*e - 2*c*d) / (4*a*c - b*b);
y = (d*b - 2*a*e) / (4*a*c - b*b);
return (x,y)
def ellipse_axes(conic):
# Compute the axis directions of the ellipse.
# This technique is from
# http://en.wikipedia.org/wiki/Matrix_representation_of_conic_sections#Axes
[a,b,c,d,e,f] = conic
# Compute the eigenvalues of
# / a b/2 \
# \ b/2 c /
# This algorithm is from
# http://www.math.harvard.edu/archive/21b_fall_04/exhibits/2dmatrices/index.html
# (retrieved 31 Jan 2012)
ma = a
mb = b/2
mc = b/2
md = c
mdet = ma*md - mb*mc
mtrace = ma + md
(l1,l2) = solve_quadratic(1, -mtrace, mdet);
# Eigenvalues (\lambda_1, \lambda_2)
#l1 = mtrace / 2 + sqrt(mtrace*mtrace/4 - mdet)
#l2 = mtrace / 2 - sqrt(mtrace*mtrace/4 - mdet)
if mb == 0:
return [(0,1), (1,0)]
else:
return [(mb, l1-ma), (mb, l2-ma)]
def ellipse_axislen(conic, center, direction):
# Compute the axis length as a multiple of the magnitude of 'direction'
[a,b,c,d,e,f] = conic
(cx,cy) = center
(dx,dy) = direction
dlen = sqrt(dx*dx + dy*dy)
dx /= dlen
dy /= dlen
# Solve for t:
# a*x^2 + b*x*y + c*y^2 + d*x + e*y + f = 0
# x = cx + t * dx
# y = cy + t * dy
# by substituting, we get qa*t^2 + qb*t + qc = 0, where:
qa = a*dx*dx + b*dx*dy + c*dy*dy
qb = a*2*cx*dx + b*(cx*dy + cy*dx) + c*2*cy*dy + d*dx + e*dy
qc = a*cx*cx + b*cx*cy + c*cy*cy + d*cx + e*cy + f
(t1,t2) = solve_quadratic(qa,qb,qc)
return max(t1,t2)
def solve_quadratic(a,b,c):
disc = b*b - 4*a*c
disc_root = sqrt(b*b - 4*a*c)
x1 = (-b + disc_root) / (2*a)
x2 = (-b - disc_root) / (2*a)
return (x1,x2)
if __name__ == '__main__':
EllipseByFivePoints().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "Ellipse By Five Points (Replaced by LPE)",
"id": "fablabchemnitz.de.ellipse_by_five_points",
"path": "ellipse_by_five_points",
"dependent_extensions": null,
"original_name": "Ellipse by 5 Points",
"original_id": "org.pernsteiner.inkscape.ellipse_5pts",
"license": "BSD-2-Clause License",
"license_url": "http://www.pernsteiner.org/inkscape/ellipse_5pts/inkscape-ellipse_5pts-0.1.1.zip",
"comment": "bugfixed and ported to Inkscape v1 manually by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/ellipse_by_five_points",
"fork_url": "http://www.pernsteiner.org/inkscape/ellipse_5pts/",
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55019659",
"inkscape_gallery_url": null,
"main_authors": [
"pernsteiner.org/Stuart Pernsteiner",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Fun Shapes</name>
<id>fablabchemnitz.de.fun_shapes</id>
<effect implements-custom-gui="true">
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Shape Generators">
<submenu name="Streaks And Blobs" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">fun_shapes.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,199 @@
#!/usr/bin/env python3
# coding=utf-8
#
# Copyright (C) [2021] [Matt Cottam], [mpcottam@raincloud.co.uk]
#
# 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.
#
"""
A simple Extension to test if a Gtk3 Gui will work on most Inkscape 1.1+ Systems.
Inkscape 1.1 is the minimum verion.
Draws a simple set of randomly coloured circles and outputs them to the canvas.
"""
import inkex
from inkex.elements import Group, PathElement
from inkex import transforms, styles
import random
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GdkPixbuf, Gdk
from lxml import etree
# Could not find simplestyle, found this instead in extensions repo
def formatStyle(a):
"""Format an inline style attribute from a dictionary"""
return ";".join([att + ":" + str(val) for att, val in a.items()])
def random_rgb(self):
random_red = random.randrange(0, 255)
random_green = random.randrange(0, 255)
random_blue = random.randrange(0, 255)
return f'rgb({random_red}, {random_green}, {random_blue})'
# Generate a circle or circle path
def draw_svg_circle(self, r, cx, cy, parent, is_path):
style = {'stroke': 'black',
# 'stroke-opacity': (r/200),
'stroke-width': '0.1',
# 'fill': 'none'
'fill': random_rgb(self)
}
attribs = {
'style': formatStyle(style),
'r': str(r),
'cx': str(cx),
'cy': str(cy)
}
my_circle = etree.SubElement(Funshapes.output_group, inkex.addNS('circle', 'svg'), attribs)
def draw_svg_circle_series(self, r, cx, cy, parent, is_path):
my_list = range(1, 100)
for item in reversed(my_list):
draw_svg_circle(self, item, cx, cy, parent, is_path)
gtk3_add_svg_image(self, Funshapes.output_group)
def draw_svg_circle_series_scale(self, r, cx, cy, parent, is_path):
Funshapes.output_group.clear()
upper_range = int(Funshapes.main_window.h_scale.get_value())
my_list = range(1, upper_range)
for item in reversed(my_list):
draw_svg_circle(self, item, cx, cy, parent, is_path)
gtk3_add_svg_image(self, Funshapes.output_group)
def group_wrapper(self, my_objects, to_layer):
group_id = 'g' + str(random.randrange(100000, 1000000))
new_group = self.svg.add(Group.new('#' + group_id))
# inkex set, takes account of NS attribs etc
new_group.set('inkscape:groupmode', 'layer')
new_group.set('inkscape:label', 'My_Layer_' + group_id)
# When plucking an object from an svg, will only have its own transforms
# Composed transforms must be applied instead.
for my_object in my_objects:
my_object_transforms = my_object.composed_transform()
my_object.attrib['transform'] = ''
my_object.transform.add_matrix(my_object_transforms)
new_group.append(my_object)
new_group.attrib['id'] = group_id
return new_group
def object_to_xml_tag(self):
my_tag = str(self.tostring().decode("utf-8"))
return my_tag
##########################
# GTK3 GUI SECTION BELOW
##########################
def gtk3_gui(self):
Funshapes.main_window = Gtk.Window()
Funshapes.main_window.root_grid = Gtk.Grid()
Funshapes.main_window.add(Funshapes.main_window.root_grid)
root_grid = Funshapes.main_window.root_grid
main_window = Funshapes.main_window
gtk3_scales(root_grid, Funshapes.main_window)
main_window.connect("destroy", Gtk.main_quit)
main_window.show_all()
draw_svg_circle_series(self, 200, 100, 100, Funshapes.self.svg, 'yes')
Gtk.main()
return main_window
def gtk3_scales(grid, inkex_self):
ad1 = Gtk.Adjustment(value=20, lower=0, upper=100, step_increment=5, page_increment=10, page_size=0)
inkex_self.h_scale = Gtk.Scale(
orientation=Gtk.Orientation.HORIZONTAL, adjustment=ad1)
inkex_self.h_scale.set_digits(0)
inkex_self.h_scale.set_valign(Gtk.Align.START)
inkex_self.h_scale.connect("value-changed", draw_svg_circle_series_scale, 50, 100, 100, None, None)
grid.attach(inkex_self.h_scale, 1, 0, 1, 1)
def gtk3_add_svg_image(self, svg_object):
Funshapes.base_image = Gtk.Image()
Funshapes.base_image.set_from_file('white.png')
random_number = random.randrange(10000, 1000000, 1)
# convert svg object to svg xml
svg = object_to_xml_tag(svg_object)
svg = f'<svg width="200" viewBox="0 0 200 ' \
f'200" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">{svg}</svg> '
loader = GdkPixbuf.PixbufLoader()
loader.write(svg.encode())
loader.close()
pixbuf = loader.get_pixbuf()
Funshapes.preview_image = Gtk.Image.new_from_pixbuf(pixbuf)
Funshapes.preview_image.show_all()
if Funshapes.main_window.root_grid.get_child_at(1, 1) is None:
Funshapes.main_window.root_grid.attach(Funshapes.preview_image, 1, 1, 1, 1)
else:
Funshapes.main_window.root_grid.get_child_at(1, 1).destroy()
Funshapes.main_window.root_grid.attach(Funshapes.preview_image, 1, 1, 1, 1)
class Funshapes(inkex.EffectExtension):
def effect(self):
Funshapes.self = self
output_group_id = f'Funshapes_{random.randrange(10000, 1000000, 1)}'
Funshapes.output_group = Group.new('#' + output_group_id)
# create new gtk3 window and attach to effect self
self.win = gtk3_gui(self)
# When GTK is exited
Funshapes.self.svg.append(Funshapes.output_group)
if __name__ == '__main__':
Funshapes().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "Fun Shapes",
"id": "fablabchemnitz.de.fun_shapes",
"path": "fun_shapes",
"dependent_extensions": null,
"original_name": "Fun Shapes",
"original_id": "org.inkscape.funshapes",
"license": "GNU GPL v3",
"license_url": "https://inkscape.org/~inklinea/%E2%98%85funshapes",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/fun_shapes",
"fork_url": "https://inkscape.org/~inklinea/%E2%98%85funshapes",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Fun+Shapes",
"inkscape_gallery_url": null,
"main_authors": [
"inkscape.org/inklinea",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Hatches And Grains</name>
<id>fablabchemnitz.de.hatches_and_grains</id>
<param name="tab" type="notebook">
<page name="splash" gui-text="Hatches">
<label>Create standardized hatches to differentiate or prioritize cartographic objects.</label>
<label appearance="header">Hatches settings</label>
<param name="type" type="optiongroup" appearance="combo" gui-text="Sort of hatches:">
<option value="h">Continuous</option>
<option value="d">Discontinuous</option>
</param>
<param name="angle" type="optiongroup" appearance="combo" gui-text="Orientation:">
<option value="0">―</option>
<option value="135"></option>
<option value="90">|</option>
<option value="45"></option>
</param>
<param name="thickness" type="optiongroup" appearance="combo" gui-text="Thickness:">
<option value="1">1px</option>
<option value="3">3px</option>
<option value="5">5px</option>
</param>
<param name="spacing" type="optiongroup" appearance="combo" gui-text="Spacing:">
<option value="8">8px</option>
<option value="10">10px</option>
<option value="12">12px</option>
<option value="15">15px</option>
<option value="20">20px</option>
<option value="25">25px</option>
<option value="30">30px</option>
<option value="40">40px</option>
<option value="50">50px</option>
</param>
<label appearance="header">Hatches color</label>
<param name="hcolor" type="color" gui-text="Hatches color" />
</page>
<page name="grains_page" gui-text="Grains">
<label>Create standardized grains to differentiate or prioritize cartographic objects.</label>
<label appearance="header">Grains settings</label>
<param name="type_grain" type="optiongroup" appearance="combo" gui-text="Grains:">
<option value="grain_v">V</option>
<option value="grain_m">ʌ</option>
<option value="grain_p">+</option>
<option value="grain_x">X</option>
<option value="grain_c">○</option>
<option value="grain_r">●</option>
</param>
<param name="size" type="optiongroup" appearance="combo" gui-text="Size:">
<option value="1">Very small</option>
<option value="2">Small</option>
<option value="3">Medium</option>
<option value="4">Large</option>
</param>
<label appearance="header">Grain color</label>
<param name="gcolor" type="color" gui-text="Grain color" />
</page>
<page name="info" gui-text="Information">
<label xml:space="preserve">
This mapping module is intended for the community of geographers and cartographers.
It makes it possible to create visual variables like "hatch" or "grain" on surface or point implantation, in order to differentiate or prioritize the cartographic information.
Laurent Porcheret
Géographe - cartographe / Geographer - cartographer
Sorbonne université - INSPE de Paris
V.15.01.2020</label>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Object(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">hatches_and_grains.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,144 @@
#!/bin/env python3
#
# Copyright (C) 2020 Marc Jeanmougin, Laurent Porcheret
#
# 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
#
# Thanks to Bénédicte Cuperly for reminding me to do this and for her feedback
""" Geography patterns """
import math
import inkex
from inkex import Pattern, PathElement, Circle, Rectangle, AbortExtension
GRAIN_V="m 0 0 10 20 10 -20"
GRAIN_VI="m 0 20 10 -20 10 20"
GRAIN_PLUS="M 10 0 10 20 M 0 10 20 10"
GRAIN_X="M 0 0 20 20 M 0 20 20 0"
class Geography(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('--tab', default='grains_page', help='grains_page or hatches_page')
pars.add_argument('--type', default='h', help='h|d')
pars.add_argument('--angle', default='0', help='angle')
pars.add_argument('--thickness', default='1', help='width')
pars.add_argument('--spacing', default='8', help='spacing between hatches')
pars.add_argument('--type_grain', default='grain_v', help='Shape of patterns (v^+xoO)')
pars.add_argument('--size', default='1', help='size of grain')
pars.add_argument('--hcolor', default=inkex.Color(255), type=inkex.Color, help='color of hatches')
pars.add_argument('--gcolor', default=inkex.Color(255), type=inkex.Color, help='color of grains')
def effect(self):
#step 0: find desired id
i=self.get_id()
#step 1: add stuff in defs if not present
if self.svg.getElementById(i) is None:
#construct it
newNode = self.svg.defs.add(Pattern())
self.construct(newNode)
#step 2: assign
for node in self.svg.selection.values():
node.style['fill'] = "url(#"+i+")"
node.set('fill', "url(#"+i+")")
def construct(self, node):
if(self.options.tab == "grains_page"):
self.construct_grain(node)
else:
self.construct_hatch(node)
node.set('patternUnits', "userSpaceOnUse")
node.set('id', self.get_id())
def get_id(self):
if(self.options.tab=="grains_page"):
return "inkgeo_"+self.options.type_grain+"_"+self.options.size+"_"+str(int(self.options.gcolor))
else:
return "inkgeo_"+self.options.type+"_"+self.options.angle+"_"+self.options.thickness+"_"+self.options.spacing+"_"+str(int(self.options.hcolor))
def construct_grain(self, node):
size=str(math.sqrt(float(self.options.size)))
node.set('width', "100")
node.set('height', "100")
n1=0
n2=0
node.style="stroke-width:4;"
if self.options.type_grain == "grain_c" or self.options.type_grain == "grain_r":
n1 = node.add(Circle())
n2 = node.add(Circle())
n1.set('cx',10)
n1.set('cy',10)
n1.set('r',10)
n2.set('cx',10)
n2.set('cy',10)
n2.set('r',10)
if self.options.type_grain == "grain_c":
node.style.set_color(self.options.gcolor, 'stroke')
node.set('fill', "none")
else:
node.style.set_color(self.options.gcolor, 'stroke')
node.style.set_color(self.options.gcolor, 'fill')
else:
node.style.set_color(self.options.gcolor, 'stroke')
node.set('fill', "none")
#paths
n1 = node.add(PathElement())
n2 = node.add(PathElement())
if self.options.type_grain == "grain_v":
n1.set('d', GRAIN_V)
n2.set('d', GRAIN_V)
elif self.options.type_grain == "grain_m":
n1.set('d', GRAIN_VI)
n2.set('d', GRAIN_VI)
elif self.options.type_grain == "grain_p":
n1.set('d', GRAIN_PLUS)
n2.set('d', GRAIN_PLUS)
elif self.options.type_grain == "grain_x":
n1.set('d', GRAIN_X)
n2.set('d', GRAIN_X)
n1.set('transform', "translate(5,5)scale("+size+")")
n2.set('transform', "translate(55,55)scale("+size+")")
node.set('patternTransform', "scale(0.1)")
def construct_hatch(self,node):
h = int(self.options.spacing)+int(self.options.thickness)
node.set('width', str(h))
node.set('patternTransform', "rotate("+self.options.angle+")")
r = node.add(Rectangle())
r.set('x', "0")
r.set('y', "0")
r.set('height', self.options.thickness)
r.style.set_color(self.options.hcolor, 'fill')
if self.options.type=="h":
node.set('height',str(h))
r.set('width', str(h))
else:
node.set('height',str(2*h))
r.set('width', str(h/2))
r2 = node.add(Rectangle())
r2.set('x', str(h/2))
r2.set('y', str(h))
r2.set('width', str(h/2))
r2.set('height', self.options.thickness)
r2.style.set_color(self.options.hcolor, 'fill')
if __name__ == '__main__':
Geography().run()

View File

@ -0,0 +1,128 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: hatches_grains\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-28 11:27+0200\n"
"PO-Revision-Date: 2020-09-28 11:32+0200\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.3\n"
#: hatches_grains.inx:3
msgid "Hatches and grains"
msgstr "Hachures et grains"
#: hatches_grains.inx:11
msgid "Hatches"
msgstr "Hachures"
#: hatches_grains.inx:12
msgid ""
"\n"
"Create standardized hatches to differentiate or prioritize cartographic "
"objects. \n"
" "
msgstr ""
"\n"
"Crée des hachures standardisées pour différencier ou prioriser des objets "
"cartographiques.\n"
" "
#: hatches_grains.inx:16
msgid "Hatches settings"
msgstr "Paramètres de hachures"
#: hatches_grains.inx:18
msgid "Sort of hatches:"
msgstr "Type de hachures :"
#: hatches_grains.inx:23
msgid "Orientation:"
msgstr "Orientation :"
#: hatches_grains.inx:30
msgid "Thickness:"
msgstr "Épaisseur :"
#: hatches_grains.inx:37
msgid "Spacing:"
msgstr "Espacement :"
#: hatches_grains.inx:49 hatches_grains.inx:50
msgid "Hatches color"
msgstr "Couleur de hachures"
#: hatches_grains.inx:55
msgid "Grains"
msgstr "Grains"
#: hatches_grains.inx:57
msgid ""
"\n"
"Create standardized grains to differentiate or prioritize cartographic "
"objects.\n"
" "
msgstr ""
"\n"
"Crée des grains standardisées pour différencier ou prioriser des objets "
"cartographiques.\n"
" "
#: hatches_grains.inx:61
msgid "Grains settings"
msgstr "Paramètres de grains"
#: hatches_grains.inx:63
msgid "Grains:"
msgstr "Grains :"
#: hatches_grains.inx:72
msgid "Size:"
msgstr "Taille :"
#: hatches_grains.inx:79 hatches_grains.inx:80
msgid "Grain color"
msgstr "Couleur de grains"
#: hatches_grains.inx:86
msgid "Information"
msgstr "Information"
#: hatches_grains.inx:87
msgid ""
"\n"
"This mapping module is intended for the community of geographers and "
"cartographers.\n"
"\n"
"It makes it possible to create visual variables like \"hatch\" or \"grain\" "
"on surface or point implantation, in order to differentiate or prioritize "
"the cartographic information.\n"
"\n"
"Laurent Porcheret \n"
"Géographe - cartographe / Geographer - cartographer\n"
"Sorbonne université - INSPE de Paris\n"
"V.15.01.2020"
msgstr ""
"\n"
"Ce module est destiné à la communauté des géographes et des cartographes.\n"
"Il permet de créer des variables visuelles telles que des hachures ou des "
"grains sur des surfaces ou des ponctuels, pour différencier ou hiérarchiser "
"des informations cartographiques.\n"
"\n"
"Laurent Porcheret \n"
"Géographe - cartographe / Geographer - cartographer\n"
"Sorbonne université - INSPE de Paris"
#: hatches_grains.inx:102
msgid "Cartography"
msgstr "Cartographie"

View File

@ -0,0 +1,110 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-28 11:33+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: hatches_grains.inx:3
msgid "Hatches and grains"
msgstr ""
#: hatches_grains.inx:11
msgid "Hatches"
msgstr ""
#: hatches_grains.inx:12
msgid ""
"\n"
"Create standardized hatches to differentiate or prioritize cartographic "
"objects. \n"
" "
msgstr ""
#: hatches_grains.inx:16
msgid "Hatches settings"
msgstr ""
#: hatches_grains.inx:18
msgid "Sort of hatches:"
msgstr ""
#: hatches_grains.inx:23
msgid "Orientation:"
msgstr ""
#: hatches_grains.inx:30
msgid "Thickness:"
msgstr ""
#: hatches_grains.inx:37
msgid "Spacing:"
msgstr ""
#: hatches_grains.inx:49 hatches_grains.inx:50
msgid "Hatches color"
msgstr ""
#: hatches_grains.inx:55
msgid "Grains"
msgstr ""
#: hatches_grains.inx:57
msgid ""
"\n"
"Create standardized grains to differentiate or prioritize cartographic "
"objects.\n"
" "
msgstr ""
#: hatches_grains.inx:61
msgid "Grains settings"
msgstr ""
#: hatches_grains.inx:63
msgid "Grains:"
msgstr ""
#: hatches_grains.inx:72
msgid "Size:"
msgstr ""
#: hatches_grains.inx:79 hatches_grains.inx:80
msgid "Grain color"
msgstr ""
#: hatches_grains.inx:86
msgid "Information"
msgstr ""
#: hatches_grains.inx:87
msgid ""
"\n"
"This mapping module is intended for the community of geographers and "
"cartographers.\n"
"\n"
"It makes it possible to create visual variables like \"hatch\" or \"grain\" "
"on surface or point implantation, in order to differentiate or prioritize "
"the cartographic information.\n"
"\n"
"Laurent Porcheret \n"
"Géographe - cartographe / Geographer - cartographer\n"
"Sorbonne université - INSPE de Paris\n"
"V.15.01.2020"
msgstr ""
#: hatches_grains.inx:102
msgid "Cartography"
msgstr ""

View File

@ -0,0 +1,21 @@
[
{
"name": "Hatches And Grains",
"id": "fablabchemnitz.de.hatches_and_grains",
"path": "hatches_and_grains",
"dependent_extensions": null,
"original_name": "Hatches and grains",
"original_id": "carto",
"license": "GNU GPL v3",
"license_url": "https://gitlab.com/marcjeanmougin/inkscape-carto-ext/-/blob/master/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/hatches_and_grains",
"fork_url": "https://gitlab.com/marcjeanmougin/inkscape-carto-ext",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Hatches+And+Grains",
"inkscape_gallery_url": null,
"main_authors": [
"gitlab.com/marcjeanmougin",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Ids To Text</name>
<id>fablabchemnitz.de.ids_to_text</id>
<param name="path_attribute" appearance="combo" gui-text="Path attribute to show:" type="optiongroup">
<option value="id">Id</option>
<option value="label">Label</option>
<option value="fill">Fill color</option>
<option value="stroke">Stroke color</option>
<option value="width">Width</option>
<option value="height">Height</option>
</param>
<param name="fontsize" type="int" min="1" max="1000" gui-text="Font size (px):">10</param>
<param name="color" type="color" appearance="colorbutton" gui-text="Text color:">255</param>
<param name="font" type="string" gui-text="Font:">Roboto</param>
<param name="fontweight" appearance="combo" gui-text="Font weight:" type="optiongroup">
<option value="normal">Normal</option>
<option value="bold">Bold</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="300">300</option>
<option value="400">400</option>
<option value="500">500</option>
<option value="600">600</option>
<option value="700">700</option>
<option value="800">800</option>
<option value="900">900</option>
</param>
<param name="replaced" type="string" gui-text="Text to replace:" />
<param name="replacewith" type="string" gui-text="Replace with:" />
<param name="matchre" type="string" gui-text="Match regular expression:" />
<param name="angle" type="float" min="-360" max="360" gui-text="Angle (°):">0</param>
<param name="capitals" type="bool" gui-text="Capitalize all text">false</param>
<param name="group" type="bool" gui-text="Group paths with the generated text elements">false</param>
<label appearance="header">Help</label>
<label>Lets you extract the ids (or other attributes) from all selected paths and show them as text elements inside the paths.
Examples and more info:</label>
<label appearance="url">https://github.com/whiplashoo/ids_to_text_inkscape</label>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Text"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">ids_to_text.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,103 @@
#!/usr/bin/env python3
import re
import inkex
from inkex import TextElement, TextPath, Tspan
from inkex.bezier import csparea, cspcofm, csplength
from inkex.colors import Color
class IdsToText(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('--fontsize', type = int, default = '10', help = 'Font Size')
pars.add_argument('--color', type=Color, default = 255, help = 'Color')
pars.add_argument('--font', default = 'Roboto', help = 'Font Family')
pars.add_argument('--fontweight', default = 'bold', help = 'Font Weight')
pars.add_argument('--replaced', default = '', help = 'Text to replace')
pars.add_argument('--replacewith', default = '', help = 'Replace with this text')
pars.add_argument('--angle', type = float, dest = 'angle', default = 0, help = 'Rotation angle')
pars.add_argument('--capitals', type = inkex.Boolean, default = False, help = 'Capitalize')
pars.add_argument('--path_attribute', default='id', help='Path attribute to show')
pars.add_argument('--matchre', default='', help='Match regular expression')
pars.add_argument('--group', type=inkex.Boolean, default=False, help='Group paths with generated text elements')
def extract_path_attribute(self, attr, node):
ret = ''
if attr == 'id':
ret = node.get(attr)
elif attr == 'label':
value = node.get(attr)
ret = str(value) if value else (node.get("inkscape:label") or "")
elif attr == 'width':
ret = format(node.bounding_box().width, '.2f')
elif attr == 'height':
ret = format(node.bounding_box().height, '.2f')
elif attr == 'fill' or attr == 'stroke':
if 'style' in node.attrib:
style = node.attrib.get('style')
style = dict(inkex.styles.Style.parse_str(style))
if attr in style:
ret = style.get(attr)
elif attr in node.attrib:
ret = node.attrib.get(attr)
return ret
def effect(self):
if len(self.svg.selection.filter(inkex.PathElement)) == 0:
inkex.errormsg("Please select some paths first.")
exit()
path_attribute = self.options.path_attribute
is_text_attribute = path_attribute in ['id', 'label']
for id, node in self.svg.selection.filter(inkex.PathElement).items():
to_show = self.extract_path_attribute(path_attribute, node)
node.path.transform(node.composed_transform()).to_superpath()
bbox = node.bounding_box()
tx, ty = bbox.center
if self.options.group:
group_element = node.getparent().add(inkex.Group())
group_element.add(node)
group_element.set('id', node.get('id') + "_group")
text_element = group_element.add(inkex.TextElement())
else:
text_element = node.getparent().add(inkex.TextElement())
tspan_element = text_element.add(inkex.Tspan())
tspan_element.set('sodipodi:role', 'line')
styles = {'text-align': 'center',
'vertical-align': 'bottom',
'text-anchor': 'middle',
'font-size': str(self.options.fontsize) + 'px',
'font-weight': self.options.fontweight,
'font-style': 'normal',
'font-family': self.options.font,
'fill': str(self.options.color)
}
tspan_element.set('style', str(inkex.Style(styles)))
tspan_element.set('dy', '0')
if is_text_attribute:
if self.options.capitals:
to_show = to_show.upper()
if self.options.matchre != '':
matches = re.findall(self.options.matchre, to_show)
if len(matches) > 0:
to_show = matches[0]
if self.options.replaced != '':
to_show = to_show.replace(
self.options.replaced, self.options.replacewith)
tspan_element.text = to_show
tspan_element.set('id', node.get('id') + "_tspan")
text_element.set('id', node.get('id') + "_text")
text_element.set('x', str(tx))
text_element.set('y', str(ty))
text_element.set('transform', 'rotate(%s, %s, %s)' %
(-int(self.options.angle), tx, ty))
if __name__ == '__main__':
IdsToText().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "Ids To Text",
"id": "fablabchemnitz.de.ids_to_text",
"path": "ids_to_text",
"dependent_extensions": null,
"original_name": "Ids To Text",
"original_id": "org.inkscape.render.ids_to_text",
"license": "GNU GPL v3",
"license_url": "https://github.com/whiplashoo/ids_to_text_inkscape/blob/master/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/ids_to_text",
"fork_url": "https://github.com/whiplashoo/ids_to_text_inkscape",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Ids+To+Text",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/whiplashoo",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>IFS Fractals</name>
<id>fablabchemnitz.de.ifs_fractals</id>
<param name="tab" type="notebook">
<page name="Options" gui-text="Options">
<param name="iter" type="int" min="0" max="100" gui-text="Number of iterations:">3</param>
<label appearance="header">Transform Matrices</label>
<param name="tab" type="notebook">
<page name="0" gui-text="1">
<param name="xform0" type="bool" gui-text="Enabled:">true</param>
<param name="A0" type="float" min="-10000" max="10000" precision="3" gui-text="A:">0.5</param>
<param name="B0" type="float" min="-10000" max="10000" precision="3" gui-text="B:">0</param>
<param name="C0" type="float" min="-10000" max="10000" precision="3" gui-text="C:">0</param>
<param name="D0" type="float" min="-10000" max="10000" precision="3" gui-text="D:">0.5</param>
<param name="E0" type="float" min="-10000" max="10000" precision="3" gui-text="E:">0</param>
<param name="F0" type="float" min="-10000" max="10000" precision="3" gui-text="F:">0</param>
</page>
<page name="1" gui-text="2">
<param name="xform1" type="bool" gui-text="Enabled:">false</param>
<param name="A1" type="float" min="-10000" max="10000" precision="3" gui-text="A:">0.5</param>
<param name="B1" type="float" min="-10000" max="10000" precision="3" gui-text="B:">0</param>
<param name="C1" type="float" min="-10000" max="10000" precision="3" gui-text="C:">0</param>
<param name="D1" type="float" min="-10000" max="10000" precision="3" gui-text="D:">0.5</param>
<param name="E1" type="float" min="-10000" max="10000" precision="3" gui-text="E:">1</param>
<param name="F1" type="float" min="-10000" max="10000" precision="3" gui-text="F:">0</param>
</page>
<page name="2" gui-text="3">
<param name="xform2" type="bool" gui-text="Enabled:">false</param>
<param name="A2" type="float" min="-10000" max="10000" precision="3" gui-text="A:">0.5</param>
<param name="B2" type="float" min="-10000" max="10000" precision="3" gui-text="B:">0</param>
<param name="C2" type="float" min="-10000" max="10000" precision="3" gui-text="C:">0</param>
<param name="D2" type="float" min="-10000" max="10000" precision="3" gui-text="D:">0.5</param>
<param name="E2" type="float" min="-10000" max="10000" precision="3" gui-text="E:">0.5</param>
<param name="F2" type="float" min="-10000" max="10000" precision="3" gui-text="F:">1</param>
</page>
<page name="3" gui-text="4">
<param name="xform3" type="bool" gui-text="Enabled:">false</param>
<param name="A3" type="float" min="-10000" max="10000" precision="3" gui-text="A:">0.5</param>
<param name="B3" type="float" min="-10000" max="10000" precision="3" gui-text="B:">0</param>
<param name="C3" type="float" min="-10000" max="10000" precision="3" gui-text="C:">0</param>
<param name="D3" type="float" min="-10000" max="10000" precision="3" gui-text="D:">0.5</param>
<param name="E3" type="float" min="-10000" max="10000" precision="3" gui-text="E:">0</param>
<param name="F3" type="float" min="-10000" max="10000" precision="3" gui-text="F:">0</param>
</page>
<page name="4" gui-text="5">
<param name="xform4" type="bool" gui-text="Enabled:">false</param>
<param name="A4" type="float" min="-10000" max="10000" precision="3" gui-text="A:">0.5</param>
<param name="B4" type="float" min="-10000" max="10000" precision="3" gui-text="B:">0</param>
<param name="C4" type="float" min="-10000" max="10000" precision="3" gui-text="C:">0</param>
<param name="D4" type="float" min="-10000" max="10000" precision="3" gui-text="D:">0.5</param>
<param name="E4" type="float" min="-10000" max="10000" precision="3" gui-text="E:">0</param>
<param name="F4" type="float" min="-10000" max="10000" precision="3" gui-text="F:">0</param>
</page>
</param>
</page>
<page name="Help" gui-text="Help">
<label>
This performs an Iterated Function System by repeating one or more duplicate-and-transform operations on the selected objects.
</label>
<label>
The transformations are specified using matrices, in the same form as the transformation dialog, each of which should be contractive (i.e., shrinking).
</label>
<label>
For example, if you set N transforms, it will make N duplicates and transform each in the first iteration, and then N^2 duplicates of those, and so on, for a total of (N^(I+1)-1)/(N-1) duplicates.
</label>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Object(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">ifs_fractals.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
# coding=utf-8
#
# Copyright (C) 2020 Dylan Simon, dylan@dylex.net
#
# 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.
#
"""
Perform fixed-depth IFS repeated duplicate-and-transform.
"""
import inkex
class IFSFractals(inkex.EffectExtension):
NXFORM = 5
XFORM_PARAMS = list("ABCDEF")
def add_arguments(self, pars):
pars.add_argument("--tab")
pars.add_argument("--iter", type=int, default=3, help="number of iterations")
for i in range(self.NXFORM):
pars.add_argument("--xform%d"%i, type=inkex.Boolean, default=False, help="enable transformation %d"%i)
for p in self.XFORM_PARAMS:
pars.add_argument("--%s%d"%(p,i), type=float, help="transformation matrix %d %s"%(i,p))
def effect(self):
xforms = []
for i in range(self.NXFORM):
if getattr(self.options,'xform%d'%i):
t = [getattr(self.options,"%s%d"%(p,i)) for p in self.XFORM_PARAMS]
xforms.append(inkex.Transform(t))
if not xforms:
inkex.errormsg(_('There are no transforms to apply'))
return False
if not self.svg.selected:
inkex.errormsg(_('There is no selection to duplicate'))
return False
nodes = self.svg.selected.values()
grp = inkex.Group('IFS')
layer = self.svg.get_current_layer().add(grp)
for i in range(self.options.iter):
n = []
for node in nodes:
for x in xforms:
d = node.copy()
d.transform = x @ d.transform
n.append(d)
g = inkex.Group('IFS iter %d'%i)
g.add(*n)
grp.add(g)
nodes = n
return True
if __name__ == '__main__':
IFSFractals().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "IFS Fractals",
"id": "fablabchemnitz.de.ifs_fractals",
"path": "ifs_fractals",
"dependent_extensions": null,
"original_name": "IFS",
"original_id": "org.inkscape.filter.ifs",
"license": "GNU GPL v2",
"license_url": "https://github.com/dylex/inkscape-ifs/blob/master/ifs.py",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/ifs_fractals",
"fork_url": "https://github.com/dylex/inkscape-ifs",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/IFS+Fractals",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/dylex",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Insert Paper Template</name>
<id>fablabchemnitz.de.insert_paper_template</id>
<param name="papertype" type="optiongroup" appearance="combo" gui-text="Box type">
<option value="A5">A5</option>
<option value="A4">A4</option>
<option value="A3">A3</option>
<option value="A2">A2</option>
<option value="A1">A1</option>
<option value="A0">A0</option>
<option value="POKER">Poker Card</option>
<option value="BRIDGE">Bridge Card</option>
<option value="MINI_US">Mini American Card</option>
<option value="MINI_EU">Mini European Card</option>
<option value="TAROT">Tarot Card</option>
</param>
<param name="show_type" type="bool" gui-text="Include Label">true</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Grids/Guides"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">insert_paper_template.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
# Copyright 2016 Luke Phillips (lukerazor@hotmail.com)
# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
# Extension dirs
# linux:~/.config/inkscape/extensions
# windows: D:\Program Files\Inkscape\share\extensions
from lxml import etree
import inkex
inkex.NSS[u'cs'] = u'http://www.razorfoss.org/tuckboxextension/'
class InsertPaperTemplate(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('-p', '--papertype')
pars.add_argument('-s', '--show_type')
### colour ###
self.StrokeWidthMM = 0.25
self.FontSize = "11px"
def effect(self):
self.Group = etree.SubElement(self.svg.get_current_layer(), inkex.addNS('g','svg'), {} )
papertypes = {}
papertypes["A5"] = (148, 210, "#ffeeaa") # yellow
papertypes["A4"] = (210, 297, "#ffccaa") # orange
papertypes["A3"] = (297, 420, "#afdde9") # blue
papertypes["A2"] = (420, 594, "#ccaaff") # purple
papertypes["A1"] = (594, 841, "#afe9c6") # green
papertypes["A0"] = (841, 1189, "#ffd5d5") # red
papertypes["POKER"] = (63.5, 88, "#ffffff") # white
papertypes["BRIDGE"] = (56, 88, "#ffffff") # white
papertypes["MINI_US"] = (41, 63, "#ffffff") # white
papertypes["MINI_EU"] = (44, 68, "#ffffff") # white
papertypes["TAROT"] = (70, 120, "#ffffff") # white
if self.options.papertype in papertypes:
self.CreateTemplate(self.options.papertype, *(papertypes[self.options.papertype]))
else:
raise Exception("Paper type '{0}' is undefined".format(self.options.papertype))
def CreateTemplate(self, label, width, height, colour):
x = 0
y = 0
self._CreateRectangleInMillimetres(width-self.StrokeWidthMM, height-self.StrokeWidthMM, x, y, colour)
if self.options.show_type == "true":
self._CreateText(label, x + width/2 , y + height/2)
def _CreateText(self, labelText, x, y):
style = {'stroke': '#000000',
'stroke-width': self.MMtoUU(self.StrokeWidthMM),
'fill' : '#000000',
'font-size' : self.FontSize,
'text-align' : 'center',
'text-anchor' : 'middle'
}
attribs = {'style': str(inkex.Style(style)), 'x': self.MMtoUU(x), 'y': self.MMtoUU(y)}
text = etree.Element(inkex.addNS('text','svg'), attribs)
text.text = labelText
self.Group.append(text)
def _CreateRectangleInMillimetres(self, width, height, x, y, color):
style = {'stroke': '#000000', 'stroke-width': self.MMtoUU(self.StrokeWidthMM), 'fill' : color}
attribs = {'style': str(inkex.Style(style)), 'height': self.MMtoUU(height), 'width': self.MMtoUU(width), 'x': self.MMtoUU(x), 'y': self.MMtoUU(y)}
etree.SubElement(self.Group, inkex.addNS('rect','svg'), attribs )
def MMtoUU(self, mmval):
if hasattr(self.svg, "unittouu"):
return str(self.svg.unittouu("{0}mm".format(mmval)))
else:
MM_TO_PIXELS = 1
return str(MM_TO_PIXELS * mmval)
if __name__ == '__main__':
InsertPaperTemplate().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "Insert Paper Template",
"id": "fablabchemnitz.de.insert_paper_template",
"path": "insert_paper_template",
"dependent_extensions": null,
"original_name": "Insert Paper Template",
"original_id": "phillips.effect.papertemplate",
"license": "GNU GPL v2",
"license_url": "https://sourceforge.net/p/razorfoss/svn/HEAD/tree/trunk/Inkscape/InsertPaperTemplate/InsertPaperTemplate.py",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/insert_paper_template",
"fork_url": "https://sourceforge.net/p/razorfoss/svn/HEAD/tree/trunk/Inkscape/InsertPaperTemplate/",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Insert+Paper+Template",
"inkscape_gallery_url": null,
"main_authors": [
"Luke Phillips:lukerazor@hotmail.com",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Join Paths / Create Tabs And Dimples</name>
<id>fablabchemnitz.de.join_paths</id>
<param name="tab" type="notebook">
<page name="subdividePath" gui-text="Join Paths / Create Tabs And Dimples">
<hbox>
<vbox>
<label appearance="header">Join Paths</label>
<param name="optimized" type="bool" gui-text="Optimized joining">true</param>
<param name="reverse" type="bool" gui-text="Reverse">true</param>
<param name="margin" type="float" min="0.0001" max="99999.0000" precision="4" gui-text="Merge margin">0.0100</param>
<label appearance="header">Tabs And Dimples</label>
<label>Enable to insert dimples (pressfit noses) into</label>
<label>the gaps (instead regular straight lines)</label>
<param name="add_dimples" type="bool" gui-text="Create dimples">false</param>
<param name="dimples_to_group" type="bool" gui-text="Unify into single group">false</param>
<param name="dimple_type" type="optiongroup" appearance="combo" gui-text="Dimple type">
<option value="lines">lines</option>
<option value="peaks">peaks</option>
<option value="arcs">arcs</option>
<option value="tabs">tabs</option>
<option value="sheetmetal">sheetmetal</option>
</param>
<param name="draw_dimple_centers" type="bool" gui-text="Draw dimple centers">false</param>
<param name="dimple_invert" type="bool" gui-text="Invert dimple sides">false</param>
<param name="draw_both_sides" type="bool" gui-text="Draw both sides">false</param>
<param name="draw_arcs_as_paths" type="bool" gui-text="Draw arcs as svg:path" gui-description="If disabled, we get svg:ellipse elements. Only applies for dimple type 'arcs'">true</param>
</vbox>
<separator/>
<vbox>
<label appearance="header">Dimple/Tab Dimensions</label>
<param name="dimple_height_mode" type="optiongroup" appearance="combo" gui-text="Basic height by ...">
<option value="by_height">by height</option>
<option value="by_angle">by angle</option>
</param>
<param name="dimple_angle" type="float" min="0.000" max="360.000" precision="3" gui-text="... angle">45.000</param>
<param name="dimple_height" type="float" min="0.001" max="99999.000" precision="3" gui-text="... height">4.000</param>
<param name="dimple_tab_angle" type="float" min="0.000" max="360.000" precision="3" gui-text="Tab: angle" gui-description="Only applies for dimple type 'tabs'">45.000</param>
<param name="dimple_sheetmetal_depth" type="float" min="0.000" max="99999.000" precision="3" gui-text="Sheetmetal: depth" gui-description="Only applies for dimple type 'sheetmetal'">4.000</param>
<param name="dimple_height_units" gui-text="Height units" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
</param>
<label appearance="header">Gap filter</label>
<label>Prevents filling all gaps with tabs/dimples.</label>
<param name="dimple_gap_filter" type="bool" gui-text="Apply min/max filter">false</param>
<param name="dimple_min_gap" type="float" min="0.000" max="99999.000" precision="3" gui-text="Min">1</param>
<param name="dimple_max_gap" type="float" min="0.000" max="99999.000" precision="3" gui-text="Max">40</param>
<param name="dimple_gap_filter_units" gui-text="Filter units" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
</param>
</vbox>
</hbox>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Join Paths / Create Tabs And Dimples</label>
<label>This effect joins the Bezier curves with straight line segments.
If the end nodes are close enough, they are merged into a single one.
With the optimized option selected, the new curve starts from the top
most curve from the selection. The curves are then joined based on the
distance of their closest end point to the previous curve.
Additionally it allows to create different tabs / dimples for lasercutting.
This extension is originally based on 'Join Paths Optimized' by Shriinivas.</label>
<label>2021 - 2022 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/joinpaths</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Paths - Join/Order" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">join_paths.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,429 @@
#!/usr/bin/env python3
'''
Inkscape extension to join the selected paths. With the optimized option selected,
the next path to be joined is the one, which has one of its end nodes closest to the ending
node of the earlier path.
Copyright (C) 2019 Shrinivas Kulkarni
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
from inkex.paths import CubicSuperPath
import sys
import copy
import math
def floatCmpWithMargin(float1, float2, margin):
return abs(float1 - float2) < margin
def vectCmpWithMargin(vect1, vect2, margin):
return all(floatCmpWithMargin(vect2[i], vect1[i], margin) for i in range(0, len(vect1)))
def getPartsFromCubicSuper(cspath):
parts = []
for subpath in cspath:
part = []
prevBezPt = None
for i, bezierPt in enumerate(subpath):
if(prevBezPt != None):
seg = [prevBezPt[1], prevBezPt[2], bezierPt[0], bezierPt[1]]
part.append(seg)
prevBezPt = bezierPt
parts.append(part)
return parts
def getCubicSuperFromParts(parts):
cbsuper = []
for part in parts:
subpath = []
lastPt = None
pt = None
for seg in part:
if(pt == None):
ptLeft = seg[0]
pt = seg[0]
ptRight = seg[1]
subpath.append([ptLeft, pt, ptRight])
ptLeft = seg[2]
pt = seg[3]
subpath.append([ptLeft, pt, pt])
cbsuper.append(subpath)
return cbsuper
def getArrangedIds(pathMap, startPathId):
nextPathId = startPathId
orderPathIds = [nextPathId]
#Arrange in order
while(len(orderPathIds) < len(pathMap)):
minDist = 9e+100 #A large float
closestId = None
np = pathMap[nextPathId]
if np[-1] == []:
inkex.utils.debug("Warning. Selection seems to contain invalid paths, e.g. pointy paths like M 54,54 Z. Please check and try again!")
exit(1)
npPts = [np[-1][-1][-1]]
if(len(orderPathIds) == 1):#compare both the ends for the first path
npPts.append(np[0][0][0])
for key in pathMap:
if(key in orderPathIds):
continue
parts = pathMap[key]
start = parts[0][0][0]
end = parts[-1][-1][-1]
for i, npPt in enumerate(npPts):
dist = abs(start[0] - npPt[0]) + abs(start[1] - npPt[1])
if(dist < minDist):
minDist = dist
closestId = key
dist = abs(end[0] - npPt[0]) + abs(end[1] - npPt[1])
if(dist < minDist):
minDist = dist
pathMap[key] = [[[pts for pts in reversed(seg)] for seg in \
reversed(part)] for part in reversed(parts)]
closestId = key
#If start point of the first path is closer reverse its direction
if(i > 0 and closestId == key):
pathMap[nextPathId] = [[[pts for pts in reversed(seg)] for seg in \
reversed(part)] for part in reversed(np)]
orderPathIds.append(closestId)
nextPathId = closestId
return orderPathIds
def rotate(origin, point, angle):
"""
Rotate a point counterclockwise by a given angle around a given origin.
The angle should be given in radians.
"""
ox, oy = origin
px, py = point
qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy)
qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy)
return qx, qy
class JoinPaths(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--optimized", type=inkex.Boolean, default=True)
pars.add_argument("--reverse", type=inkex.Boolean, default=False)
pars.add_argument("--margin", type=float, default=0.0100)
pars.add_argument("--add_dimples", type=inkex.Boolean, default=False)
pars.add_argument("--draw_dimple_centers", type=inkex.Boolean, default=False)
pars.add_argument("--draw_arcs_as_paths", type=inkex.Boolean, default=False)
pars.add_argument("--dimple_invert", type=inkex.Boolean, default=False)
pars.add_argument("--dimple_type", default="lines")
pars.add_argument("--dimples_to_group", type=inkex.Boolean, default=False)
pars.add_argument("--draw_both_sides", type=inkex.Boolean, default=False)
pars.add_argument("--dimple_height_mode", default="by_height")
pars.add_argument("--dimple_height", type=float, default=4)
pars.add_argument("--dimple_angle", type=float, default=45)
pars.add_argument("--dimple_tab_angle", type=float, default=45)
pars.add_argument("--dimple_sheetmetal_depth", type=float, default=4)
pars.add_argument("--dimple_gap_filter", type=inkex.Boolean, default=False)
pars.add_argument("--dimple_min_gap", type=float, default=1)
pars.add_argument("--dimple_max_gap", type=float, default=40)
pars.add_argument("--dimple_gap_filter_units", default="mm")
pars.add_argument("--dimple_height_units", default="mm")
pars.add_argument("--tab", default="sampling", help="Tab")
def effect(self):
pathNodes = self.document.xpath('//svg:path',namespaces=inkex.NSS)
if self.options.reverse is True: #helps debugging some strange Z orders (try out)
pathNodes = pathNodes[::-1]
#pathNodes[0].path = pathNodes[0].path.reverse()
#pathNodes[0].path = pathNodes[-1].path.reverse()
paths = {p.get('id'): getPartsFromCubicSuper(CubicSuperPath(p.get('d'))) for p in pathNodes }
#paths.keys() Order disturbed
pathIds = [p.get('id') for p in pathNodes]
if self.options.dimples_to_group is True:
dimpleUnifyGroup = self.svg.get_current_layer().add(inkex.Group(id=self.svg.get_unique_id("dimplesCollection")))
if(len(paths) > 1):
if(self.options.optimized):
startPathId = pathIds[0]
pathIds = getArrangedIds(paths, startPathId)
newParts = []
firstElem = None
for key in pathIds:
parts = paths[key]
# ~ parts = getPartsFromCubicSuper(cspath)
start = parts[0][0][0]
try:
elem = self.svg.selected[key]
if(len(newParts) == 0):
newParts += parts[:]
firstElem = elem
else:
if(vectCmpWithMargin(start, newParts[-1][-1][-1], margin = self.options.margin)) and self.options.add_dimples is False:
newParts[-1] += parts[0]
else:
if self.options.add_dimples is True:
if self.options.dimples_to_group is True:
dimpleGroup = dimpleUnifyGroup.add(inkex.Group(id="dimpleGroup-{}".format(elem.attrib["id"])))
else:
dimpleGroup = elem.getparent().add(inkex.Group(id="dimpleGroup-{}".format(elem.attrib["id"])))
p1 = newParts[-1][-1][-1]
p2 = start
midPoint = [(p1[0] + p2[0])/2, (p1[1] + p2[1])/2]
newParts[-1].append([newParts[-1][-1][-1], newParts[-1][-1][-1], midPoint, midPoint])
newParts[-1].append([newParts[-1][-1][-1], newParts[-1][-1][-1], p2, p2])
newParts[-1] += parts[0]
#get slope, distance and norm slope
dx = midPoint[0]-p1[0]
dy = midPoint[1]-p1[1]
dist = math.sqrt(dx*dx + dy*dy)
dx /= dist
dy /= dist
dx2 = p2[0]-p1[0]
dy2 = p2[1]-p1[1]
dist2 = math.sqrt(dx2*dx2 + dy2*dy2)
if dx2 == 0:
slope=sys.float_info.max #vertical
else:
slope=(p2[1] - p1[1]) / dx2
slope_angle = 90 + math.degrees(math.atan(slope))
if (self.options.dimple_gap_filter is True \
and dist2 >= self.svg.unittouu(str(self.options.dimple_min_gap) + self.options.dimple_gap_filter_units) \
and dist2 < self.svg.unittouu(str(self.options.dimple_max_gap) + self.options.dimple_gap_filter_units)
) \
or self.options.dimple_gap_filter is False:
if self.options.dimple_height_mode == "by_height":
dimple_height = self.svg.unittouu(str(self.options.dimple_height) + self.options.dimple_height_units)
else:
dimple_height = dist * math.sin(math.radians(self.options.dimple_angle))
x3 = midPoint[0] + (dimple_height)*dy
y3 = midPoint[1] - (dimple_height)*dx
x4 = midPoint[0] - (dimple_height)*dy
y4 = midPoint[1] + (dimple_height)*dx
dimple_center_style = {'stroke': '#00FFFF', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px'))}
dimple_style = {'stroke': '#0000FF', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px'))}
if self.options.draw_dimple_centers is True:
#add a new dimple center cross (4 segments)
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_center_perp1')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(midPoint[0], midPoint[1], x3, y3))
line.style = dimple_center_style
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_center_perp2')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(midPoint[0], midPoint[1], x4, y4))
line.style = dimple_center_style
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_center_join1')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(p1[0], p1[1], midPoint[0], midPoint[1]))
line.style = dimple_center_style
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_center_join1')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(midPoint[0], midPoint[1], p2[0], p2[1]))
line.style = dimple_center_style
##########
### LINES
##########
if self.options.dimple_type == "lines":
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_line')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(p1[0], p1[1], p2[0], p2[1]))
line.style = dimple_style
##########
### PEAKS
##########
elif self.options.dimple_type == "peaks":
if self.options.dimple_invert is True:
x5 = x3
y5 = y3
x3 = x4
y3 = y4
x4 = x5
y4 = y5
#add a new dimple center
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_peak')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(p1[0], p1[1], x3, y3, p2[0], p2[1]))
line.style = dimple_style
if self.options.draw_both_sides is True:
#add a new opposite dimple center
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_peak')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(p1[0], p1[1], x4, y4, p2[0], p2[1]))
line.style = dimple_style
##########
### ARCS
##########
elif self.options.dimple_type == "arcs":
if self.options.draw_arcs_as_paths is False:
ellipse = dimpleGroup.add(inkex.Ellipse(id=self.svg.get_unique_id('dimple_arc')))
ellipse.set('transform', "rotate({:0.6f} {:0.6f} {:0.6f})".format(slope_angle, midPoint[0], midPoint[1]))
ellipse.set('sodipodi:arc-type', "arc")
ellipse.set('sodipodi:type', "arc")
ellipse.set('sodipodi:cx', "{:0.6f}".format(midPoint[0]))
ellipse.set('sodipodi:cy', "{:0.6f}".format(midPoint[1]))
ellipse.set('sodipodi:rx', "{:0.6f}".format(dimple_height))
ellipse.set('sodipodi:ry', "{:0.6f}".format(dist2 / 2))
if self.options.dimple_invert is True:
ellipse.set('sodipodi:start', "{:0.6f}".format(math.radians(90.0)))
ellipse.set('sodipodi:end', "{:0.6f}".format(math.radians(270.0)))
else:
ellipse.set('sodipodi:start', "{:0.6f}".format(math.radians(270.0)))
ellipse.set('sodipodi:end', "{:0.6f}".format(math.radians(90.0)))
ellipse.style = dimple_style
if self.options.draw_both_sides is True:
ellipse = dimpleGroup.add(inkex.Ellipse(id=self.svg.get_unique_id('dimple_arc')))
ellipse.set('transform', "rotate({:0.6f} {:0.6f} {:0.6f})".format(slope_angle, midPoint[0], midPoint[1]))
ellipse.set('sodipodi:arc-type', "arc")
ellipse.set('sodipodi:type', "arc")
ellipse.set('sodipodi:cx', "{:0.6f}".format(midPoint[0]))
ellipse.set('sodipodi:cy', "{:0.6f}".format(midPoint[1]))
ellipse.set('sodipodi:rx', "{:0.6f}".format(dimple_height))
ellipse.set('sodipodi:ry', "{:0.6f}".format(dist2 / 2))
if self.options.dimple_invert is True:
ellipse.set('sodipodi:start', "{:0.6f}".format(math.radians(270.0)))
ellipse.set('sodipodi:end', "{:0.6f}".format(math.radians(90.0)))
else:
ellipse.set('sodipodi:start', "{:0.6f}".format(math.radians(90.0)))
ellipse.set('sodipodi:end', "{:0.6f}".format(math.radians(270.0)))
ellipse.style = dimple_style
else: #if draw_arcs_as_paths is True
# +--- x-end point
# |
# counterclockwise ---+ | +--- y-end point
# | | |
#<path d="M 85 350 A 150 180 0 0 0 280 79" stroke="red" fill="none"/>
# | | | |
# 1 Radius x-Axis ---+ | | +--- 4 short / long way
# | |
# 2 Radius y-Axis ---+ +--- 3 Rotation x
if self.options.dimple_invert is True:
b1 = 1
b2 = 0
else:
b1 = 0
b2 = 1
ellipse = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_arc')))
ellipse.set('d', "M {:0.6f} {:0.6f} A {:0.6f} {:0.6f} {:0.6f} 0 {} {:0.6f} {:0.6f}".format(p1[0], p1[1], dimple_height, dist2 / 2, slope_angle, b1, p2[0], p2[1]))
ellipse.style = dimple_style
if self.options.draw_both_sides is True:
ellipse = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_arc')))
ellipse.set('d', "M {:0.6f} {:0.6f} A {:0.6f} {:0.6f} {:0.6f} 0 {} {:0.6f} {:0.6f}".format(p1[0], p1[1], dimple_height, dist2 / 2, slope_angle, b2, p2[0], p2[1]))
ellipse.style = dimple_style
##########
### TABS
##########
elif self.options.dimple_type == "tabs":
l_hypo = dimple_height / (math.cos(math.radians(90.0 - self.options.dimple_tab_angle)))
pbottom1 = rotate(p1, [p1[0] + l_hypo * dx, p1[1] + l_hypo * dy], math.radians(-self.options.dimple_tab_angle))
pbottom2 = rotate(p2, [p2[0] - l_hypo * dx, p2[1] - l_hypo * dy], math.radians(-360.0 + self.options.dimple_tab_angle))
ptop1 = rotate(p1, [p1[0] + l_hypo * dx, p1[1] + l_hypo * dy], math.radians(self.options.dimple_tab_angle))
ptop2 = rotate(p2, [p2[0] - l_hypo * dx, p2[1] - l_hypo * dy], math.radians(360.0 - self.options.dimple_tab_angle))
if self.options.dimple_invert is True:
ptemp1 = pbottom1
ptemp2 = pbottom2
pbottom1 = ptop1
pbottom2 = ptop2
ptop1 = ptemp1
ptop2 = ptemp2
#add a new tab
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_tab')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f} L{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(
p1[0], p1[1], pbottom1[0], pbottom1[1], pbottom2[0], pbottom2[1], p2[0], p2[1]))
line.style = dimple_style
if self.options.draw_both_sides is True:
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_tab')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f} L{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(
p1[0], p1[1], ptop1[0], ptop1[1], ptop2[0], ptop2[1], p2[0], p2[1]))
line.style = dimple_style
##########
### sHEETMETAL
##########
elif self.options.dimple_type == "sheetmetal":
if self.options.dimple_invert is True:
self.options.dimple_tab_angle = 360.0 - self.options.dimple_tab_angle
pbottom1 = rotate(p1, [p1[0] + dimple_height * dx, p1[1] + dimple_height * dy], math.radians(-self.options.dimple_tab_angle))
pbottom2 = rotate(p2, [p2[0] - dimple_height * dx, p2[1] - dimple_height * dy], math.radians(-360.0 + self.options.dimple_tab_angle))
depth = self.svg.unittouu(str(self.options.dimple_sheetmetal_depth) + self.options.dimple_height_units)
poff1 = [p1[0] + (depth)*dx, p1[1] + (depth)*dy]
poff2 = [p2[0] - (depth)*dx, p2[1] - (depth)*dy]
poff1_start = rotate(poff1, [poff1[0] + dimple_height * dx, poff1[1] + dimple_height * dy], math.radians(180.0 - self.options.dimple_tab_angle))
poff2_end = rotate(poff2, [poff2[0] + dimple_height * dx, poff2[1] + dimple_height * dy], math.radians(self.options.dimple_tab_angle))
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_sheetmetal_start')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(
p1[0], p1[1], pbottom1[0], pbottom1[1]))
line.style = dimple_style
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_sheetmetal_middle')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f} L{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(
poff1_start[0], poff1_start[1], poff1[0], poff1[1], poff2[0], poff2[1], poff2_end[0], poff2_end[1]))
line.style = dimple_style
line = dimpleGroup.add(inkex.PathElement(id=self.svg.get_unique_id('dimple_sheetmetal_end')))
line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(
p2[0], p2[1], pbottom2[0], pbottom2[1]))
line.style = dimple_style
#cleanup groups
if len(dimpleGroup) == 1: ##move up child if group has only one child
for child in dimpleGroup:
dimpleGroup.getparent().insert(elem.getparent().index(elem), child)
dimpleGroup.delete() #delete the empty group now
else:
newParts[-1].append([newParts[-1][-1][-1], newParts[-1][-1][-1], start, start])
newParts[-1] += parts[0]
if(len(parts) > 1):
newParts += parts[1:]
parent = elem.getparent()
idx = parent.index(elem)
if self.options.add_dimples is False:
parent.remove(elem)
except:
pass #elem might come from group item - in this case we need to ignore it
if firstElem is None:
self.msg('Please select some paths first. Check if you selected a group or an object instead.')
exit()
newElem = copy.copy(firstElem)
oldId = firstElem.get('id')
newElem.set('d', CubicSuperPath(getCubicSuperFromParts(newParts)))
newElem.set('id', oldId + '_joined')
if self.options.add_dimples is False:
parent.insert(idx, newElem)
if __name__ == '__main__':
JoinPaths().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "Join Paths / Create Tabs And Dimples",
"id": "fablabchemnitz.de.join_paths",
"path": "join_paths",
"dependent_extensions": null,
"original_name": "Join Paths Optimized",
"original_id": "khema.optim.join.paths",
"license": "GNU GPL v2",
"license_url": "https://github.com/Shriinivas/inkscapejoinpaths/blob/master/LICENSE",
"comment": "ported to Inkscape v1 by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/join_paths",
"fork_url": "https://github.com/Shriinivas/inkscapejoinpaths",
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55019193",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/Shriinivas",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Living Hinge (Fit To Rectangle)</name>
<id>fablabchemnitz.de.living_hinge</id>
<param name="direction" gui-text="Direction" type="optiongroup" appearance="combo">
<option value="y">vertical cuts</option>
<option value="x">horizontal cuts</option>
</param>
<param name="unit" gui-text="Unit" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
</param>
<param name="cut_length" type="float" precision="2" gui-text="cut length (y)" min="1" max="1000">19.0</param>
<param name="gap_length" type="float" precision="2" gui-text="gap length (y)" min="1" max="1000">3.0</param>
<param name="sep_distance" type="float" precision="2" gui-text="separation distance (x)" min="1" max="1000">1.5</param>
<label xml:space="preserve">This extension renders the lines to make the cuts for a living hinge. The cuts run in the choosen direction.
- direction: choose the direction of the cuts.
- cut length: the length of each cut.
- gap length: the separation between cut lines.
- separation distance: the separation distance between adjacent sets of cut lines.
The entered value for cut length will be adjusted so that an integer number of whole cut lines lies in the rectangle. The entered value for separation distance will be adjusted so that in integer number of cut lines lies in rectangle.</label>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Object(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">living_hinge.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""
hinge_cuts.py
A module for creating lines to laser cut living hinges
Copyright (C) 2013 Mark Endicott; drphonon@gmail.com
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.
For a copy of the GNU General Public License
write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
"""
Change in version 0.2.
Changed self.unittouu to self.svg.unittouu
and self.uutounit to self.uutounit
to make it work with Inkscape 0.91
Thanks to Pete Prodoehl for pointing this out.
"""
"""
Change in version 0.3
Add a direction option so the cuts can be done in the X or Y direction.
Modification by Sylvain GARNAVAULT; garnav@wanadoo.fr
"""
"""
Change in version 0.4.
Python3 / inkscape 1.0 migration
"""
__version__ = "0.4"
import inkex
import gettext
_ = gettext.gettext
from lxml import etree
class LivingHinge(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('--direction',default='y',help='cuts direction')
pars.add_argument('--unit',default='mm',help='units of measurement')
pars.add_argument('--cut_length',type=float, default=0,help='length of the cuts for the hinge.')
pars.add_argument('--gap_length',type=float, default=0,help='separation distance between successive hinge cuts.')
pars.add_argument('--sep_distance',type=float, default=0,help='distance between successive lines of hinge cuts.')
def effect(self):
unit=self.options.unit
# which direction are we cutting
dir = self.options.direction
# starting cut length. Will be adjusted for get an integer number of cuts in the y-direction.
l = self.svg.unittouu(str(self.options.cut_length) + unit)
# cut separation in the y-direction
d = self.svg.unittouu(str(self.options.gap_length) + unit)
# starting separation between lines of cuts in the x-direction. Will be adjusted to get an integer
# number of cut lines in the x-direction.
dd = self.svg.unittouu(str(self.options.sep_distance) + unit)
# get selected nodes
if self.svg.selected:
# put lines on the current layer
parent = self.svg.get_current_layer()
for id, node in self.svg.selected.items():
# inkex.utils.debug("id:" + id)
# for key in node.attrib.keys():
# inkex.utils.debug(key + ": " + node.get(key))
bbox = node.bounding_box()
# calculate the cut lines for the hinge
if (dir=="y"):
lines, l_actual, d_actual, dd_actual = self.calcYCutLines(bbox.left, bbox.top, bbox.width, bbox.height, l, d, dd)
else:
lines, l_actual, d_actual, dd_actual = self.calcXCutLines(bbox.left, bbox.top, bbox.width, bbox.height, l, d, dd)
s = ''
for line in lines:
s = s + "M %s, %s L %s, %s " % (line['x1'], line['y1'], line['x2'], line['y2'])
style = { 'stroke': '#000000', 'fill': 'none', 'stroke-width': self.svg.unittouu("0.1 mm")}
drw = {'style':str(inkex.Style(style)), 'd': s}
hinge = etree.SubElement(parent, inkex.addNS('path', 'svg'), drw)
desc = etree.SubElement(hinge, inkex.addNS('desc', 'svg'))
desc.text = "Hinge cut parameters: actual(requested)\n" \
"cut length: %.2f %s (%.2f %s)\n" \
"gap length: %.2f %s (%.2f %s)\n" \
"separation distance: %.2f %s (%.2f %s)" % (self.svg.uutounit(l_actual, unit), unit, self.svg.uutounit(l, unit), unit,
self.svg.uutounit(d_actual, unit), unit, self.svg.uutounit(d, unit), unit,
self.svg.uutounit(dd_actual, unit), unit, self.svg.uutounit(dd, unit), unit)
else:
inkex.utils.debug("No rectangle(s) have been selected.")
def calcYCutLines(self, x, y, dx, dy, l, d, dd):
"""
Return a list of cut lines as dicts. Each dict contains the end points for one cut line.
[{x1, y1, x2, y2}, ... ]
Parameters
x, y: the coordinates of the lower left corner of the bounding rect
dx, dy: width and height of the bounding rect
l: the nominal length of a cut line
d: the separation between cut lines in the y-direction
dd: the nominal separation between cut lines in the x-direction
l will be adjusted so that there is an integral number of cuts in the y-direction.
dd will be adjusted so that there is an integral number of cuts in the x-direction.
"""
ret = []
# use l as a starting guess. Adjust it so that we get an integer number of cuts in the y-direction
# First compute the number of cuts in the y-direction using l. This will not in general be an integer.
p = (dy-d)/(d+l)
#round p to the nearest integer
p = round(p)
#compute the new l that will result in p cuts in the y-direction.
if p == 0:
p = 1 #avoid divison by zero
l = (dy-d)/p - d
# use dd as a starting guess. Adjust it so that we get an even integer number of cut lines in the x-direction.
p = dx/dd
p = round(p)
if p % 2 == 1:
p = p + 1
dd = dx/p
#
# Column A cuts
#
currx = 0
donex = False
while not donex:
doney = False
starty = 0
endy = (l + d)/2.0
while not doney:
if endy >= dy:
endy = dy
# Add the end points of the line
ret.append({'x1' : x + currx, 'y1' : y + starty, 'x2': x + currx, 'y2': y + endy})
starty = endy + d
endy = starty + l
if starty >= dy:
doney = True
currx = currx + dd * 2.0
if currx - dx > dd:
donex = True
# inkex.utils.debug("lastx: " + str(lastx) + "; currx: " + str(currx))
#
#Column B cuts
#
currx = dd
donex = False
while not donex:
doney = False
starty = d
endy = starty + l
while not doney:
if endy >= dy:
endy = dy
# create a line
ret.append({'x1' : x + currx, 'y1' : y + starty, 'x2': x + currx, 'y2': y + endy})
starty = endy + d
endy = starty + l
if starty >= dy:
doney = True
currx = currx + dd*2.0
if currx > dx:
donex = True
return (ret, l, d, dd)
def calcXCutLines(self, x, y, dx, dy, l, d, dd):
"""
Return a list of cut lines as dicts. Each dict contains the end points for one cut line.
[{x1, y1, x2, y2}, ... ]
Parameters
x, y: the coordinates of the lower left corner of the bounding rect
dx, dy: width and height of the bounding rect
l: the nominal length of a cut line
d: the separation between cut lines in the x-direction
dd: the nominal separation between cut lines in the y-direction
l will be adjusted so that there is an integral number of cuts in the x-direction.
dd will be adjusted so that there is an integral number of cuts in the y-direction.
"""
ret = []
# use l as a starting guess. Adjust it so that we get an integer number of cuts in the y-direction
# First compute the number of cuts in the x-direction using l. This will not in general be an integer.
p = (dx-d)/(d+l)
#round p to the nearest integer
p = round(p)
#compute the new l that will result in p cuts in the x-direction.
l = (dx-d)/p - d
# use dd as a starting guess. Adjust it so that we get an even integer number of cut lines in the y-direction.
p = dy/dd
p = round(p)
if p % 2 == 1:
p = p + 1
dd = dy/p
#
# Rows A cuts
#
curry = 0
doney = False
while not doney:
donex = False
startx = 0
endx = (l + d)/2.0
while not donex:
if endx >= dx:
endx = dx
# Add the end points of the line
ret.append({'x1' : x + startx, 'y1' : y + curry, 'x2': x + endx, 'y2': y + curry})
startx = endx + d
endx = startx + l
if startx >= dx:
donex = True
curry = curry + dd * 2.0
if curry - dy > dd:
doney = True
#
# Rows B cuts
#
curry = dd
doney = False
while not doney:
donex = False
startx = d
endx = startx + l
while not donex:
if endx >= dx:
endx = dx
# create a line
ret.append({'x1' : x + startx, 'y1' : y + curry, 'x2': x + endx, 'y2': y + curry})
startx = endx + d
endx = startx + l
if startx >= dx:
donex = True
curry = curry + dd*2.0
if curry > dy:
doney = True
return (ret, l, d, dd)
if __name__ == '__main__':
LivingHinge().run()

View File

@ -0,0 +1,22 @@
[
{
"name": "Living Hinge (Fit To Rectangle)",
"id": "fablabchemnitz.de.living_hinge",
"path": "living_hinge",
"dependent_extensions": null,
"original_name": "Living Hinge",
"original_id": "org.lvl1.living_hinge",
"license": "GNU GPL v3",
"license_url": "https://github.com/siteswapjuggler/Inkscape_LivingHinge/blob/master/LICENSE.md",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/living_hinge",
"fork_url": "https://github.com/siteswapjuggler/Inkscape_LivingHinge",
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55018530",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/drphonon",
"github.com/siteswapjuggler",
"github.com/eridur-de"
]
}
]

View File

@ -17,7 +17,7 @@
<page name="tab_about" gui-text="About">
<label appearance="header">Move Path Node</label>
<label>Extension to change starting / end node of a path and visualize it by dots and numbers. You can also use this extension as a trimmer for open paths.</label>
<label>2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<label>2021 - 2022 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer />
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/movepathnode</label>

View File

@ -0,0 +1,20 @@
[
{
"name": "Normalize Drawing Scale",
"id": "fablabchemnitz.de.normalize_drawing_scale",
"path": "laser_check",
"dependent_extensions": null,
"original_name": "Normalize Drawing Scale",
"original_id": "fablabchemnitz.de.normalize_drawing_scale",
"license": "GNU GPL v3",
"license_url": "",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/normalize_drawing_scale",
"fork_url": null,
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Normale+Drawing+Scale",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Normalize Drawing Scale</name>
<id>fablabchemnitz.de.normalize_drawing_scale</id>
<param name="remove_viewbox" type="bool" gui-text="Remove viewBox and set document units to px" gui-description="Removes viewBox attribute from svg:svg. Warning: this disabled the feature to change the document units to anything else than px unit.">false</param>
<param name="target_scale" type="float" min="0.001" max="9999.000" precision="3" gui-text="Target scale (%)" gui-description="Default is 100%">100.0</param>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Transformations"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">normalize_drawing_scale.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,100 @@
#!/usr/bin/env python3
import inkex
from inkex import Transform
from lxml import etree
class NormalizeDrawingScale(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('--remove_viewbox', type=inkex.Boolean, default=True)
pars.add_argument('--target_scale', type=float, default=100.0)
def effect(self):
format_units = inkex.units.parse_unit(self.svg.get('width'))[1] #get the "Format:" unit at "Display tab"
namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi'))
display_units = namedView.get(inkex.addNS('document-units', 'inkscape')) #means the "Display units:" at "Display tab"
docScale = self.svg.scale
inkscapeScale = self.svg.inkscape_scale #this is the "Scale:" value at "Display tab"
docWidth = self.svg.get('width')
docHeight = self.svg.get('height')
docWidth_fin = inkex.units.parse_unit(docWidth)[0]
docHeight_fin = inkex.units.parse_unit(docHeight)[0]
vxMin, vyMin, vxMax, vyMax = self.svg.get_viewbox()
vxTotal = vxMax - vxMin
targetScale = self.options.target_scale / 100
visualScaleX = self.svg.unittouu(str(vxTotal / self.svg.viewport_width) + display_units)
formatScaleX = self.svg.unittouu(str(vxTotal / self.svg.viewport_width) + format_units)
docWidth_new = docWidth_fin * visualScaleX * inkscapeScale
docHeight_new = docHeight_fin * visualScaleX * inkscapeScale
docWidth_new = docWidth_fin * targetScale / inkscapeScale
docHeight_new = docHeight_fin * targetScale / inkscapeScale
#inkex.errormsg("format_units: " + str(format_units))
#inkex.errormsg("display_units: " + str(display_units))
#inkex.errormsg("docScale: {:0.6f}".format(docScale))
#inkex.errormsg("inkscapeScale: {:0.6f}".format(inkscapeScale))
#inkex.errormsg("docWidth_fin: {:0.3f}{}".format(docWidth_fin, format_units))
#inkex.errormsg("docHeight_fin: {:0.3f}{}".format(docHeight_fin, format_units))
#inkex.errormsg("vxTotal: " + str(vxTotal))
#inkex.errormsg("docWidth_new: {:0.3f}{} ({:0.3f}px)".format(docWidth_new, format_units, self.svg.unittouu(str(docWidth_new) + format_units)))
#inkex.errormsg("docHeight_new: {:0.3f}{} ({:0.3f}px)".format(docHeight_new, format_units, self.svg.unittouu(str(docHeight_new) + format_units)))
#inkex.errormsg("targetScale: {:0.6f}".format(targetScale))
#inkex.errormsg("visualScaleX: {:0.6f}".format(visualScaleX))
#inkex.errormsg("formatScaleX: {:0.6f}".format(formatScaleX))
if inkscapeScale == targetScale: #strange rule. might break sth.
inkex.utils.debug("Nothing to do. Scale is already 100%")
return
if visualScaleX == 0.0: #seems there is no viewBox attribute, then ...
#inkex.errormsg("viewBox attribute is missing in svg:svg. Applying new one ...")
visualScaleX = 1.0 #this is the case we deal with px as display unit and we removed the viewBox
self.svg.set('viewBox', '0 0 {} {}'.format(targetScale * docWidth_fin, targetScale * docHeight_fin))
if round(visualScaleX, 5) != targetScale or self.options.remove_viewbox is True:
#set scale to 100% (we adjust viewBox)
sc = (1 / (targetScale / inkscapeScale))
if self.options.remove_viewbox is False:
viewBoxNew = '0 0 {} {}'.format(docWidth_fin / targetScale, docHeight_fin / targetScale)
#inkex.errormsg("viewBox modifying to: {}".format(viewBoxNew))
#inkex.errormsg("width modifying to: {}{}".format(docWidth_fin, format_units))
#inkex.errormsg("height modifying to: {}{}".format(docHeight_fin, format_units))
self.svg.set('viewBox', viewBoxNew)
self.svg.set('width', "{}{}".format(docWidth_fin, format_units))
self.svg.set('height', "{}{}".format(docHeight_fin, format_units))
else:
#inkex.errormsg("viewBox popping; setting back to px ...")
self.svg.pop('viewBox')
self.document.getroot().set('inkscape:document-units', 'px')
self.svg.set('width', docWidth_fin)
self.svg.set('height', docHeight_fin)
namedView.attrib[inkex.addNS('document-units', 'inkscape')] = 'px'
translation_matrix = [[sc, 0.0, 0.0], [0.0, sc, 0.0]]
#select each top layer and apply the transformation to scale
processed = []
for element in self.document.getroot().iter(tag=etree.Element):
if element != self.document.getroot():
if element.tag == inkex.addNS('g','svg'):
parent = element.getparent()
if parent.get('inkscape:groupmode') != 'layer' and element.get('inkscape:groupmode') == 'layer':
element.transform = Transform(translation_matrix) @ element.composed_transform()
processed.append(element)
#do the same for all elements which lay on first level and which are not a layer
for element in self.document.getroot().getchildren():
if isinstance(element, inkex.ShapeElement) and element not in processed:
element.transform = Transform(translation_matrix) @ element.composed_transform()
else:
inkex.utils.debug("Nothing to do. Scale is already 100%")
return
if __name__ == '__main__':
NormalizeDrawingScale().run()

129
extensions/fablabchemnitz/ocr/.gitignore vendored Normal file
View File

@ -0,0 +1,129 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

View File

@ -0,0 +1,20 @@
[
{
"name": "OCR (Image To Text)",
"id": "fablabchemnitz.de.ocr",
"path": "ocr",
"dependent_extensions": null,
"original_name": "Image To Text",
"original_id": "org.inkscape.ocr",
"license": "Apache-2.0 License",
"license_url": "https://github.com/amal-san/inkscape-ocr/blob/main/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/ocr",
"fork_url": "https://github.com/amal-san/inkscape-ocr",
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=114524275",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>OCR (Image To Text)</name>
<id>fablabchemnitz.de.ocr</id>
<label>OCR might fail on different languages. Ensure you have installed the proper tesseract language data packages.</label>
<param name="lang" gui-text="Language" type="optiongroup" appearance="combo">
<option value="afr">afr (Afrikaans)</option>
<option value="amh">amh (Amharic)</option>
<option value="ara">ara (Arabic)</option>
<option value="asm">asm (Assamese)</option>
<option value="aze">aze (Azerbaijani)</option>
<option value="aze_cyrl">aze_cyrl (Azerbaijani - Cyrilic)</option>
<option value="bel">bel (Belarusian)</option>
<option value="ben">ben (Bengali)</option>
<option value="bod">bod (Tibetan)</option>
<option value="bos">bos (Bosnian)</option>
<option value="bre">bre (Breton)</option>
<option value="bul">bul (Bulgarian)</option>
<option value="cat">cat (Catalan; Valencian)</option>
<option value="ceb">ceb (Cebuano)</option>
<option value="ces">ces (Czech)</option>
<option value="chi_sim">chi_sim (Chinese - Simplified)</option>
<option value="chi_tra">chi_tra (Chinese - Traditional)</option>
<option value="chr">chr (Cherokee)</option>
<option value="cos">cos (Corsican)</option>
<option value="cym">cym (Welsh)</option>
<option value="dan">dan (Danish)</option>
<option value="dan_frak">dan_frak (Danish - Fraktur (contrib))</option>
<option value="deu">deu (German)</option>
<option value="deu_frak">deu_frak (German - Fraktur (contrib))</option>
<option value="dzo">dzo (Dzongkha)</option>
<option value="ell">ell (Greek, Modern (1453-))</option>
<option value="eng">eng (English)</option>
<option value="enm">enm (English, Middle (1100-1500))</option>
<option value="epo">epo (Esperanto)</option>
<option value="equ">equ (Math / equation detection module)</option>
<option value="est">est (Estonian)</option>
<option value="eus">eus (Basque)</option>
<option value="fao">fao (Faroese)</option>
<option value="fas">fas (Persian)</option>
<option value="fil">fil (Filipino (old - Tagalog))</option>
<option value="fin">fin (Finnish)</option>
<option value="fra">fra (French)</option>
<option value="frk">frk (German - Fraktur)</option>
<option value="frm">frm (French, Middle (ca.1400-1600))</option>
<option value="fry">fry (Western Frisian)</option>
<option value="gla">gla (Scottish Gaelic)</option>
<option value="gle">gle (Irish)</option>
<option value="glg">glg (Galician)</option>
<option value="grc">grc (Greek, Ancient (to 1453) (contrib))</option>
<option value="guj">guj (Gujarati)</option>
<option value="hat">hat (Haitian; Haitian Creole)</option>
<option value="heb">heb (Hebrew)</option>
<option value="hin">hin (Hindi)</option>
<option value="hrv">hrv (Croatian)</option>
<option value="hun">hun (Hungarian)</option>
<option value="hye">hye (Armenian)</option>
<option value="iku">iku (Inuktitut)</option>
<option value="ind">ind (Indonesian)</option>
<option value="isl">isl (Icelandic)</option>
<option value="ita">ita (Italian)</option>
<option value="ita_old">ita_old (Italian - Old)</option>
<option value="jav">jav (Javanese)</option>
<option value="jpn">jpn (Japanese)</option>
<option value="kan">kan (Kannada)</option>
<option value="kat">kat (Georgian)</option>
<option value="kat_old">kat_old (Georgian - Old)</option>
<option value="kaz">kaz (Kazakh)</option>
<option value="khm">khm (Central Khmer)</option>
<option value="kir">kir (Kirghiz; Kyrgyz)</option>
<option value="kmr">kmr (Kurmanji (Kurdish - Latin Script))</option>
<option value="kor">kor (Korean)</option>
<option value="kor_vert">kor_vert (Korean (vertical))</option>
<option value="kur">kur (Kurdish (Arabic Script))</option>
<option value="lao">lao (Lao)</option>
<option value="lat">lat (Latin)</option>
<option value="lav">lav (Latvian)</option>
<option value="lit">lit (Lithuanian)</option>
<option value="ltz">ltz (Luxembourgish)</option>
<option value="mal">mal (Malayalam)</option>
<option value="mar">mar (Marathi)</option>
<option value="mkd">mkd (Macedonian)</option>
<option value="mlt">mlt (Maltese)</option>
<option value="mon">mon (Mongolian)</option>
<option value="mri">mri (Maori)</option>
<option value="msa">msa (Malay)</option>
<option value="mya">mya (Burmese)</option>
<option value="nep">nep (Nepali)</option>
<option value="nld">nld (Dutch; Flemish)</option>
<option value="nor">nor (Norwegian)</option>
<option value="oci">oci (Occitan (post 1500))</option>
<option value="ori">ori (Oriya)</option>
<option value="osd">osd (Orientation and script detection module)</option>
<option value="pan">pan (Panjabi; Punjabi)</option>
<option value="pol">pol (Polish)</option>
<option value="por">por (Portuguese)</option>
<option value="pus">pus (Pushto; Pashto)</option>
<option value="que">que (Quechua)</option>
<option value="ron">ron (Romanian; Moldavian; Moldovan)</option>
<option value="rus">rus (Russian)</option>
<option value="san">san (Sanskrit)</option>
<option value="sin">sin (Sinhala; Sinhalese)</option>
<option value="slk">slk (Slovak)</option>
<option value="slk_frak">slk_frak (Slovak - Fraktur (contrib))</option>
<option value="slv">slv (Slovenian)</option>
<option value="snd">snd (Sindhi)</option>
<option value="spa">spa (Spanish; Castilian)</option>
<option value="spa_old">spa_old (Spanish; Castilian - Old)</option>
<option value="sqi">sqi (Albanian)</option>
<option value="srp">srp (Serbian)</option>
<option value="srp_latn">srp_latn (Serbian - Latin)</option>
<option value="sun">sun (Sundanese)</option>
<option value="swa">swa (Swahili)</option>
<option value="swe">swe (Swedish)</option>
<option value="syr">syr (Syriac)</option>
<option value="tam">tam (Tamil)</option>
<option value="tat">tat (Tatar)</option>
<option value="tel">tel (Telugu)</option>
<option value="tgk">tgk (Tajik)</option>
<option value="tgl">tgl (Tagalog (new - Filipino))</option>
<option value="tha">tha (Thai)</option>
<option value="tir">tir (Tigrinya)</option>
<option value="ton">ton (Tonga)</option>
<option value="tur">tur (Turkish)</option>
<option value="uig">uig (Uighur; Uyghur)</option>
<option value="ukr">ukr (Ukrainian)</option>
<option value="urd">urd (Urdu)</option>
<option value="uzb">uzb (Uzbek)</option>
<option value="uzb_cyrl">uzb_cyrl (Uzbek - Cyrilic)</option>
<option value="vie">vie (Vietnamese)</option>
<option value="yid">yid (Yiddish)</option>
<option value="yor">yor (Yoruba)</option>
</param>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Text" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">ocr.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python3
# Copyright (C) 2021 Amal Santhosh , amalsanp@gmail.com
# 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 pytesseract
from PIL import Image
import cairosvg
import os
class OcrOutputExtension(inkex.OutputExtension):
def add_arguments(self, pars):
pars.add_argument('--lang',default='eng',help='Language')
def effect(self):
try:
img = 'read.png'
cairosvg.svg2png(url = self.file_io.name, write_to = img)
text = pytesseract.image_to_string(Image.open(img), lang = self.options.lang)
self.msg(text.rstrip())
os.remove('read.png')
except Exception as e:
self.msg(e)
self.msg("Image reading failed!")
return
def save(self, stream):
pass
if __name__ == '__main__':
OcrOutputExtension().run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,20 @@
[
{
"name": "Offset Paths",
"id": "fablabchemnitz.de.offset_paths",
"path": "offset_paths",
"dependent_extensions": null,
"original_name": "Offset Paths",
"original_id": "fablabchemnitz.de.offset_paths",
"license": "GNU GPL v3",
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE",
"comment": "based on https://github.com/TimeTravel-0/ofsplot",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/offset_paths",
"fork_url": null,
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Offset+Paths",
"inkscape_gallery_url": "https://inkscape.org/de/~MarioVoigt/%E2%98%85offset-paths",
"main_authors": [
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Offset Paths</name>
<id>fablabchemnitz.de.offset_paths</id>
<param name="tab" type="notebook">
<page name="tab_settings" gui-text="Settings">
<param name="unit" type="optiongroup" appearance="combo" gui-text="Unit">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
<option value="pc">pc</option>
</param>
<param name="offset_count" type="int" min="1" max="100000" gui-text="Number of offset paths">1</param>
<param name="init_offset" type="float" precision="4" min="-1000" max="+1000" gui-text="Initial offset from original path">1.0000</param>
<param name="offset" type="float" precision="4" min="-1000" max="+1000" gui-text="Offset between two paths">1.0000</param>
<param name="offset_increase" type="float" precision="4" min="-1000" max="+1000" gui-text="Offset increase per iteration">0.0000</param>
<param name="miterlimit" type="float" min="0.0" max="1000" gui-text="Miter limit">3.0</param>
<param name="clipperscale" type="int" min="2" max="65536" gui-text="Scaling factor" gui-description="Should be a multiplicator of 2, like 2^4=16 or 2^10=1024. The higher the scale factor the higher the quality.">1024</param>
<param name="jointype" appearance="combo" type="optiongroup" gui-text="Join type">
<option value="0">Square</option>
<option value="1">Round</option>
<option value="2">Miter</option>
</param>
<param name="endtype" appearance="combo" type="optiongroup" gui-text="End type">
<option value="0">Closed Polygon</option>
<option value="1">Closed Line</option>
<option value="2">Open Butt</option>
<option value="3">Open Square</option>
<option value="4">Open Round</option>
</param>
<param name="copy_org" type="bool" gui-text="Keep original path" gui-description="If enabled, keeps original path as a copy">false</param>
<param name="individual" type="bool" gui-text="Separate into individual paths" gui-description="If enabled, each offset curve will be an individual svg element">false</param>
<param name="group" type="bool" gui-text="Put all offset paths into group">true</param>
<param name="path_types" type="optiongroup" appearance="combo" gui-text="Path types to apply" gui-description="Process open, closed or all paths!">
<option value="both">all paths</option>
<option value="open_paths">open paths</option>
<option value="closed_paths">closed paths</option>
</param>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Offset Paths</label>
<label>Create offset for open or closed paths. Python library 'pyclipper' needs to be installed. Use 'Flatten Bezier' extension in advance of this plugin.</label>
<label>2020 - 2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/offsetpaths</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">offset_paths.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""
Based on
- https://github.com/TimeTravel-0/ofsplot
ToDo's
- break apart combined paths
- option to handle groups
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Last Patch: 10.06.2021
License: GNU GPL v3
"""
import inkex
import math
from inkex.paths import CubicSuperPath
import re
import copy
import pyclipper
class OffsetPaths(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('--tab')
pars.add_argument('--unit')
pars.add_argument("--offset_count", type=int, default=1, help="Number of offset paths")
pars.add_argument("--offset", type=float, default=1.000, help="Offset amount")
pars.add_argument("--init_offset", type=float, default=0.000, help="Initial Offset Amount")
pars.add_argument("--offset_increase", type=float, default=0.000, help="Offset increase between iterations")
pars.add_argument("--jointype", default="2", help="Join type")
pars.add_argument("--endtype", default="3", help="End type")
pars.add_argument("--miterlimit", type=float, default=3.0, help="Miter limit")
pars.add_argument("--clipperscale", type=int, default=1024, help="Scaling factor. Should be a multiplicator of 2, like 2^4=16 or 2^10=1024. The higher the scale factor the higher the quality.")
pars.add_argument("--copy_org", type=inkex.Boolean, default=True, help="copy original path")
pars.add_argument("--individual", type=inkex.Boolean, default=True, help="Separate into individual paths")
pars.add_argument("--group", type=inkex.Boolean, default=True, help="Put all offset paths into group")
pars.add_argument("--path_types", default="both", help="Process open, closed or all paths!")
def effect(self):
unit_factor = 1.0 / self.svg.uutounit(1.0, self.options.unit)
pathElements = self.svg.selection.filter(inkex.PathElement).values()
count = sum(1 for pathElement in pathElements)
pathElements = self.svg.selection.filter(inkex.PathElement).values() #we need to call this twice because the sum function consumes the generator
if count == 0:
inkex.errormsg("No paths selected.")
exit()
for pathElement in pathElements:
csp = CubicSuperPath(pathElement.get('d'))
'''
check for closed or open paths
'''
isClosed = False
raw = pathElement.path.to_arrays()
if raw[-1][0] == 'Z' or \
(raw[-1][0] == 'L' and raw[0][1] == raw[-1][1]) or \
(raw[-1][0] == 'C' and raw[0][1] == [raw[-1][1][-2], raw[-1][1][-1]]) \
: #if first is last point the path is also closed. The "Z" command is not required
isClosed = True
if self.options.path_types == "open_paths" and isClosed is True:
continue #skip this loop iteration
elif self.options.path_types == "closed_paths" and isClosed is False:
continue #skip this loop iteration
scale_factor = self.options.clipperscale # 2 ** 32 = 1024 - see also https://github.com/fonttools/pyclipper/wiki/Deprecating-SCALING_FACTOR
pco = pyclipper.PyclipperOffset(self.options.miterlimit)
JT = None #join types
if self.options.jointype == "0":
JT = pyclipper.JT_SQUARE
elif self.options.jointype == "1":
JT = pyclipper.JT_ROUND
elif self.options.jointype == "2":
JT = pyclipper.JT_MITER
ET = None #end types
if self.options.endtype == "0":
ET = pyclipper.ET_CLOSEDPOLYGON
elif self.options.endtype == "1":
ET = pyclipper.ET_CLOSEDLINE
elif self.options.endtype == "2":
ET = pyclipper.ET_OPENBUTT
elif self.options.endtype == "3":
ET = pyclipper.ET_OPENSQUARE
elif self.options.endtype == "4":
ET = pyclipper.ET_OPENROUND
newPaths = []
# load in initial paths
for subPath in csp:
sub_simple = []
for item in subPath:
itemx = [float(z) * scale_factor for z in item[1]]
sub_simple.append(itemx)
pco.AddPath(sub_simple, JT, ET)
# calculate offset paths for different offset amounts
offset_list = []
offset_list.append(self.options.init_offset * unit_factor)
for i in range(0, self.options.offset_count):
ofs_increase = +math.pow(float(i) * self.options.offset_increase * unit_factor, 2)
if self.options.offset_increase < 0:
ofs_increase = -ofs_increase
offset_list.append(offset_list[0] + float(i) * self.options.offset * unit_factor + ofs_increase * unit_factor)
solutions = []
for offset in offset_list:
solution = pco.Execute(offset * scale_factor)
solutions.append(solution)
if len(solution) <= 0:
continue # no more loops to go, will provide no results.
# re-arrange solutions to fit expected format & add to array
for solution in solutions:
for sol in solution:
solx = [[float(s[0]) / scale_factor, float(s[1]) / scale_factor] for s in sol]
sol_p = [[a, a, a] for a in solx]
sol_p.append(sol_p[0][:])
if sol_p not in newPaths:
newPaths.append(sol_p)
if self.options.individual is True:
parent = pathElement.getparent()
if self.options.group is True: parentGroup = parent.add(inkex.Group(id="g-offset-{}".format(pathElement.attrib["id"])))
idx = parent.index(pathElement) + 1
idSuffix = 0
for newPath in newPaths:
copyElement = copy.copy(pathElement)
elementId = copyElement.get('id')
copyElement.path = CubicSuperPath(newPath)
copyElement.set('id', elementId + str(idSuffix))
if self.options.group is True:
parentGroup.append(copyElement)
else:
parent.append(copyElement)
idSuffix += 1
if self.options.group is True: parent.insert(idx, parentGroup)
if self.options.copy_org is False:
pathElement.delete()
else:
if self.options.copy_org is True:
for subPath in csp:
newPaths.append(subPath)
pathElement.set('d', CubicSuperPath(newPaths))
if __name__ == '__main__':
OffsetPaths().run()

View File

@ -0,0 +1,20 @@
[
{
"name": "Open Extension Directory",
"id": "fablabchemnitz.de.open_dir",
"path": "open_dir",
"dependent_extensions": null,
"original_name": "Open Extension Directory",
"original_id": "fablabchemnitz.de.open_dir",
"license": "GNU GPL v3",
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE",
"comment": "Written by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/open_dir",
"fork_url": null,
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Open+Extension+Directory",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Open Extension Directory</name>
<id>fablabchemnitz.de.open_dir</id>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz"/>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">open_dir.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
import subprocess
import os
import sys
import warnings
import inkex
DETACHED_PROCESS = 0x00000008
class OpenExtensionDirectory(inkex.EffectExtension):
def spawnIndependentProcess(self, args):
warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback"
if os.name == 'nt':
subprocess.Popen(args, close_fds=True, creationflags=DETACHED_PROCESS)
else:
subprocess.Popen(args, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
warnings.simplefilter("default", ResourceWarning)
def effect(self):
extension_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', '..')
if os.name == 'nt':
explorer = "explorer"
else:
explorer = "xdg-open"
args = [explorer, extension_dir]
try:
self.spawnIndependentProcess(args)
except FileNotFoundError as e:
inkex.utils.debug(e)
exit(1)
if __name__ == '__main__':
OpenExtensionDirectory().run()

View File

@ -0,0 +1,20 @@
[
{
"name": "Paths To Lowlevel Strokes",
"id": "fablabchemnitz.de.paths_to_lowlevel_strokes",
"path": "paths_to_lowlevel_strokes",
"dependent_extensions": null,
"original_name": "Paths To Lowlevel Strokes",
"original_id": "fablabchemnitz.de.paths_to_lowlevel_strokes",
"license": "GNU GPL v3",
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE",
"comment": "Created by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/paths_to_strokes",
"fork_url": null,
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Paths+To+Lowlevel+Strokes",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Paths To Lowlevel Strokes</name>
<id>fablabchemnitz.de.paths_to_lowlevel_strokes</id>
<param name="flattenbezier" type="bool" gui-text="Quantization (flatten bezier curves to polylines)">true</param>
<param name="flatness" type="float" min="0.001" max="99999.000" precision="3" gui-text="Flatness (tolerance)" gui-description="Minimum flatness = 0.001. The smaller the value the more fine segments you will get.">0.100</param>
<param name="decimals" type="int" min="0" max="16" gui-text="Decimals" gui-description="Accuracy">3</param>
<param name="keep_style" type="bool" gui-text="Keep style">true</param>
<effect needs-live-preview="true">
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">paths_to_lowlevel_strokes.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,118 @@
#!/usr/bin/env python3
from lxml import etree
import inkex
from inkex import bezier, PathElement
from inkex.paths import CubicSuperPath, Path
import copy
class PathsToStrokes(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--flattenbezier", type=inkex.Boolean, default=False, help="Flatten bezier curves to polylines")
pars.add_argument("--flatness", type=float, default=0.1, help="Minimum flatness = 0.1. The smaller the value the more fine segments you will get (quantization).")
pars.add_argument("--decimals", type=int, default=3)
pars.add_argument("--keep_style", type=inkex.Boolean, default=False)
def effect(self):
def flatten(node):
path = node.path.transform(node.composed_transform()).to_superpath()
bezier.cspsubdiv(path, self.options.flatness)
newpath = []
for subpath in path:
first = True
for csp in subpath:
cmd = 'L'
if first:
cmd = 'M'
first = False
newpath.append([cmd, [csp[1][0], csp[1][1]]])
node.path = newpath
def break_contours(element, breakelements = None):
if breakelements == None:
breakelements = []
if element.tag == inkex.addNS('path','svg'):
if self.options.flattenbezier is True:
flatten(element)
parent = element.getparent()
idx = parent.index(element)
idSuffix = 0
raw = element.path.to_arrays()
subPaths = []
prev = 0
for i in range(len(raw)): # Breaks compound paths into simple paths
if raw[i][0] == 'M' and i != 0:
subPath = raw[prev:i]
subPaths.append(Path(subPath))
prev = i
subPaths.append(Path(raw[prev:])) #finally add the last path
for subPath in subPaths:
replacedelement = copy.copy(element)
oldId = replacedelement.get('id')
csp = CubicSuperPath(subPath)
if len(subPath) > 1 and csp[0][0] != csp[0][1]: #avoids pointy paths like M "31.4794 57.6024 Z"
replacedelement.path = subPath
if len(subPaths) == 1:
replacedelement.set('id', oldId)
else:
replacedelement.set('id', oldId + str(idSuffix))
idSuffix += 1
parent.insert(idx, replacedelement)
breakelements.append(replacedelement)
element.delete()
for child in element.getchildren():
break_contours(child, breakelements)
return breakelements
if len(self.svg.selected) == 0:
elementsToWork = break_contours(self.document.getroot())
else:
elementsToWork = None
for element in self.svg.selected.values():
elementsToWork = break_contours(element, elementsToWork)
for element in elementsToWork:
oldId = element.get('id')
oldStyle = element.style
path = element.path.to_absolute().to_arrays() #to_arrays() is deprecated. How to make more modern?
pathIsClosed = False
if path[-1][0] == 'Z' or \
(path[-1][0] == 'L' and path[0][1] == path[-1][1]) or \
(path[-1][0] == 'C' and path[0][1] == [path[-1][1][-2], path[-1][1][-1]]) \
: #if first is last point the path is also closed. The "Z" command is not required
pathIsClosed = True
parent = element.getparent()
idx = parent.index(element)
element.delete()
if len(path) == 2 and pathIsClosed is False:
ll = inkex.Line(id=oldId)
ll.set('x1', '{:0.{dec}f}'.format(path[0][1][0], dec=self.options.decimals))
ll.set('y1', '{:0.{dec}f}'.format(path[0][1][1], dec=self.options.decimals))
ll.set('x2', '{:0.{dec}f}'.format(path[1][1][0], dec=self.options.decimals))
ll.set('y2', '{:0.{dec}f}'.format(path[1][1][1], dec=self.options.decimals))
if len(path) > 2 and pathIsClosed is False:
ll = inkex.Polyline(id=oldId)
points = ""
for i in range(0, len(path)):
points += '{:0.{dec}f},{:0.{dec}f} '.format(path[i][1][0], path[i][1][1], dec=self.options.decimals)
ll.set('points', points)
if len(path) > 2 and pathIsClosed is True:
ll = inkex.Polygon(id=oldId)
points = ""
for i in range(0, len(path) - 1):
points += '{:0.{dec}f},{:0.{dec}f} '.format(path[i][1][0], path[i][1][1], dec=self.options.decimals)
ll.set('points', points)
if self.options.keep_style is True:
ll.style = oldStyle
else:
ll.style = "fill:none;stroke:#0000FF;stroke-width:" + str(self.svg.unittouu("1px"))
parent.insert(idx, ll)
if __name__ == '__main__':
PathsToStrokes().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "Polygon Side",
"id": "fablabchemnitz.de.polygon_side",
"path": "polygon_side",
"dependent_extensions": null,
"original_name": "Polygon Side",
"original_id": "org.inkscape.polygon_side",
"license": "GNU GPL v3",
"license_url": "https://inkscape.org/de/~inklinea/%E2%98%85polygon-side",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/polygon_side",
"fork_url": "https://inkscape.org/de/~inklinea/%E2%98%85polygon-side",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Polygon+Side",
"inkscape_gallery_url": null,
"main_authors": [
"inkscape.org/inklinea",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Polygon Side</name>
<id>fablabchemnitz.de.polygon_side</id>
<param name="number_of_sides" type="int" min="1" max="100" gui-text="Number of Sides">3</param>
<param name="length_of_sides" type="float" min="0.01" max="9999" gui-text="Length of Sides">25</param>
<param name="unit_choice" type="optiongroup" appearance="radio" gui-text="Units">
<option value="1">User</option>
<option value="2">Pixels</option>
</param>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">polygon_side.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,118 @@
#!/usr/bin/env python3
#
# Copyright (C) [2021] [Matt Cottam], [mpcottam@raincloud.co.uk]
#
# 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.
#
#
# Create a Polygon given a number of sides and side length.
#
import math
import inkex
from inkex import PathElement
from inkex import units
import re
# Formula to find the circumradius ( centre to apex ) required to create
# for a given number of sectors to return desired side length
def radius_from_side_and_sectors(side_length, sectors, found_units, unit_choice):
conversions = {
'in': 96.0,
'pt': 1.3333333333333333,
'px': 1.0,
'mm': 3.779527559055118,
'cm': 37.79527559055118,
'm': 3779.527559055118,
'km': 3779527.559055118,
'Q': 0.94488188976378,
'pc': 16.0,
'yd': 3456.0,
'ft': 1152.0,
'': 1.0, # Default px
}
# Try to convert from detected units into pixels
if unit_choice == 2:
try:
pixel_conversion_factor = conversions[found_units]
except:
pixel_conversion_factor = 1
else:
pixel_conversion_factor = 1
radius = (side_length / (2 * (math.sin(math.pi / sectors)))) / pixel_conversion_factor
path = svg_poly(0, 0, radius, sectors)
return path
# All points of a regular polygon lie on a circle
# Calculate points on circle for given number of sides
def svg_poly(cx, cy, radius, sectors):
x_start = cx
y_start = cy - radius
angle = 0
y_start = cy / 2 + (radius * (math.sin(angle)))
x_start = cx / 2 + (radius * (math.cos(angle)))
path = f'M {x_start} {y_start}'
for sector in range(1, sectors + 1):
angle = (sector * math.pi) / (sectors / 2)
y = cy / 2 + (radius * (math.sin(angle)))
x = cx / 2 + (radius * (math.cos(angle)))
path = path + f' L {x} {y} '
x_start = x
y_start = y
return path + ' z'
class makepoly(inkex.GenerateExtension):
def add_arguments(self, pars):
pars.add_argument("--number_of_sides", type=int, dest="sectors", default=6)
pars.add_argument("--length_of_sides", type=float, dest="side_length", default=25)
pars.add_argument("--unit_choice", type=int, dest="unit_choice", default=25)
container_label = 'Side Poly'
def generate(self):
found_units = self.svg.unit
path = radius_from_side_and_sectors(self.options.side_length, self.options.sectors, found_units,
self.options.unit_choice)
style = {'stroke': '#000000', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px'))}
poly_path = PathElement()
poly_path.style = style
poly_path.path = path
yield poly_path
if __name__ == '__main__':
makepoly().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "Slic3r STL Input",
"id": "fablabchemnitz.de.slic3r_stl_input",
"path": "slic3r_stl_input",
"dependent_extensions": null,
"original_name": "STL Input",
"original_id": "com.github.jnweiger.inkscape.input.stl",
"license": "GNU GPL v2",
"license_url": "https://github.com/jnweiger/inkscape-input-stl/blob/master/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/slic3r_stl_input",
"fork_url": "https://github.com/jnweiger/inkscape-input-stl",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Slic3r+STL+Input",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/jnweiger",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Sine And Lace</name>
<id>fablabchemnitz.de.sine_and_lace</id>
<param name="tab" type="notebook">
<page name="splash" gui-text="Sine and Lace">
<param name="nWidth" type="int" min="1" max="10000" gui-text="Width (pixels)">3200</param>
<param name="nHeight" type="int" min="1" max="10000" gui-text="Height (pixels)">100</param>
<param name="fCycles" type="float" min="0.0001" max="10000" precision="5" gui-text="Number of cycles (periods)">10</param>
<param name="nrN" type="int" min="-100" max="100" gui-text="Start angle at 2 pi ( n / m ); n = ">0</param>
<param name="nrM" type="int" min="-100" max="100" gui-text="Start angle at 2 pi ( n / m ); m = ">0</param>
<param name="fRecess" type="float" min="0" max="100" precision="5" gui-text="Recede from envelope by percentage">2</param>
<param name="nSamples" type="int" min="2" max="100000" gui-text="Number of sample points">1000</param>
<param name="nOffsetX" type="int" min="-10000" max="10000" gui-text="Starting x coordinate (pixels)">0</param>
<param name="nOffsetY" type="int" min="-10000" max="10000" gui-text="Starting y coordinate (pixels)">500</param>
<param name="bLace" type="bool" gui-text="Lace">true</param>
<param name="bSpline" type="bool" gui-text="Spline">false</param>
</page>
<page name="info" gui-text="About...">
<label xml:space="preserve">This extension renders sinusoidal and "lace"
patterns whose period is a specified multiple
of the document width or any specified width.
By selecting two previously drawn patterns,
a third pattern may be inscribed within them.
Patterns may not be inscribed within an inscribed
pattern, however.
This extension may be found at Thingiverse as
Thing #24594.
Sine and Lace v0.9
Dan Newman (dan newman @ mtbaldy us)
12 June 2012</label>
</page>
</param>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">sine_and_lace.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,332 @@
#!/usr/bin/env python3
# eggbot_sineandlace.py
#
# Generate sinusoidal and "lace" curves. The curves are described in SVG
# along with the data necessary to regenerate them. The same data can be
# used to generate new curves which are bounded by a pair of previously
# generated curves.
# Written by Daniel C. Newman for the Eggbot Project
# dan newman @ mtbaldy us
# Last updated 28 November 2010
# 15 October 2010
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from math import pi, cos, sin
from lxml import etree
import inkex
from inkex.paths import Path
VERSION = 1
def parseDesc(str):
"""
Create a dictionary from string description
"""
if str is None:
return {}
else:
return dict([tok.split(':') for tok in str.split(';') if len(tok)])
def formatDesc(d):
"""
Format an inline name1:value1;name2:value2;... style attribute value
from a dictionary
"""
return ';'.join([atr + ':' + str(val) for atr, val in d.iteritems()])
def drawSine(cycles=8, rn=0, rm=0, nPoints=50, offset=None,
height=200, width=3200, rescale=0.98, bound1='', bound2='', fun='sine', spline=True):
"""
cycles
Number of periods to plot within the rectangle of width 'width'
rn, rm
Start the function (on the left edge) at the value x = 2 * pi * rn / rm.
When rm = 0, function is started at x = 0.
nPoints
The number of points to sample the function at. Since the function is
approximated using Bezier cubic splines, this isn't the number of points
to plot.
offset
(x, y) coordinate of the lower left corner of the bounding rectangle
in which to plot the function.
height, width
The height and width of the rectangle in which to plot the function.
Ignored when bounding functions, bound1 and bound2, are supplied.
rescale
Multiplicative Y-scaling factor by which to rescale the plotted function
so that it does not fully reach the vertical limits of its bounds. This
aids in Eggbot plots by preventing lines from touching and overlapping.
bound1, bound2
Descriptions of upper and lower bounding functions by which to limit the
vertical range of the function to be plotted.
fun
May be either 'sine' or 'lace'.
"""
"""
A complicated way of plotting y = sin(x)
Complicated because we wish to generate the sine wave using a
parametric representation. For plotting a single sine wave in
Cartesian coordinates, this is overkill. However, it's useful
for when we want to compress and expand the amplitude of the
sine wave in accord with upper and lower boundaries which themselves
are functions. By parameterizing everything in sight with the
same parameter s and restricting s to the range [0, 1], our life
is made much easier.
"""
if offset is None:
offset = [0, 0]
bounded = False
if bound1 and bound2:
func = parseDesc(bound1)
if len(func) == 0:
return None, None
m1 = int(func['rm'])
if m1 == 0:
x_min1 = 0.0
else:
x_min1 = 2 * pi * float(func['rn']) / float(m1)
x_max1 = x_min1 + 2 * pi * float(func['cycles'])
y_min1 = -1.0
y_max1 = 1.0
y_scale1 = float(func['height']) / (y_max1 - y_min1)
y_offset1 = float(func['y'])
Y1s = lambda s: y_offset1 - y_scale1 * sin(x_min1 + (x_max1 - x_min1) * s)
func = parseDesc(bound2)
if len(func) == 0:
return None, None
m2 = int(func['rm'])
if m2 == 0:
x_min2 = 0.0
else:
x_min2 = 2 * pi * float(func['rn']) / float(m2)
x_max2 = x_min2 + 2 * pi * float(func['cycles'])
y_min2 = -1.0
y_max2 = 1.0
y_scale2 = float(func['height']) / (y_max2 - y_min2)
y_offset2 = float(func['y'])
Y2s = lambda s: y_offset2 - y_scale2 * sin(x_min2 + (x_max2 - x_min2) * s)
bounded = True
rescale = float(rescale)
x_offset = float(offset[0])
y_offset = float(offset[1])
# Each cycle is 2pi
r = 2 * pi * float(cycles)
if (int(rm) == 0) or (int(rn) == 0):
x_min = 0.0
else:
x_min = 2 * pi * float(rn) / float(rm)
x_max = x_min + r
x_scale = float(width) / r # width / ( x_max - x_min )
y_min = -1.0
y_max = 1.0
y_scale = float(height) / (y_max - y_min)
# Our parametric equations which map the results to our drawing window
# Note the "-y_scale" that's because in SVG, the y-axis runs "backwards"
if not fun:
fun = 'sine'
fun = fun.lower()
if fun == 'sine':
Xs = lambda s: x_offset + x_scale * (x_max - x_min) * s
Ys = lambda s: y_offset - y_scale * sin(x_min + (x_max - x_min) * s)
dYdXs = lambda s: -y_scale * cos(x_min + (x_max - x_min) * s) / x_scale
elif fun == 'lace':
Xs = lambda s: x_offset + x_scale * ((x_max - x_min) * s + 2 * sin(2 * s * (x_max - x_min) + pi))
dXs = lambda s: x_scale * (x_max - x_min) * (1.0 + 4.0 * cos(2 * s * (x_max - x_min) + pi))
Ys = lambda s: y_offset - y_scale * sin(x_min + (x_max - x_min) * s)
dYs = lambda s: -y_scale * cos(x_min + (x_max - x_min) * s) * (x_max - x_min)
dYdXs = lambda s: dYs(s) / dXs(s)
else:
inkex.errormsg('Unknown function {0} specified'.format(fun))
return
# Derivatives: remember the chain rule....
# dXs = lambda s: x_scale * ( x_max - x_min )
# dYs = lambda s: -y_scale * cos( x_min + ( x_max - x_min ) * s ) * ( x_max - x_min )
# x_third is 1/3 the step size
nPoints = int(nPoints)
# x_third is 1/3 the step size; note that Xs(1) - Xs(0) = x_scale * ( x_max - x_min )
x_third = (Xs(1.0) - Xs(0.0)) / float(3 * (nPoints - 1))
if bounded:
y_upper = Y2s(0.0)
y_lower = Y1s(0.0)
y_offset = 0.5 * (y_upper + y_lower)
y_upper = y_offset + rescale * (y_upper - y_offset)
y_lower = y_offset + rescale * (y_lower - y_offset)
y_scale = (y_upper - y_lower) / (y_max - y_min)
x1 = Xs(0.0)
y1 = Ys(0.0)
dx1 = 1.0
dy1 = dYdXs(0.0)
# Starting point in the path is ( x, sin(x) )
path_data = []
path_data.append(['M', [x1, y1]])
for i in range(1, nPoints):
s = float(i) / float(nPoints - 1)
if bounded:
y_upper = Y2s(s)
y_lower = Y1s(s)
y_offset = 0.5 * (y_upper + y_lower)
y_upper = y_offset + rescale * (y_upper - y_offset)
y_lower = y_offset + rescale * (y_lower - y_offset)
y_scale = (y_upper - y_lower) / (y_max - y_min)
x2 = Xs(s)
y2 = Ys(s)
dx2 = 1.0
dy2 = dYdXs(s)
if dy2 > 10.0:
dy2 = 10.0
elif dy2 < -10.0:
dy2 = -10.0
# Add another segment to the plot
if spline:
path_data.append(['C',
[x1 + (dx1 * x_third),
y1 + (dy1 * x_third),
x2 - (dx2 * x_third),
y2 - (dy2 * x_third),
x2, y2]])
else:
path_data.append(['L', [x1, y1]])
path_data.append(['L', [x2, y2]])
x1 = x2
y1 = y2
dx1 = dx2
dy1 = dy2
path_desc = \
'version:{0:d};style:linear;function:sin(x);'.format(VERSION) + \
'cycles:{0:f};rn:{1:d};rm:{2:d};points:{3:d};'.format(cycles, rn, rm, nPoints) + \
'width:{0:d};height:{1:d};x:{2:d};y:{3:d}'.format(width, height, offset[0], offset[1])
return path_data, path_desc
class SineAndLace(inkex.EffectExtension):
nsURI = 'http://sample.com/ns'
nsPrefix = 'doof'
def add_arguments(self, pars):
pars.add_argument("--tab", help="The active tab when Apply was pressed")
pars.add_argument('--fCycles', type=float, default=10.0, help='Number of cycles (periods)')
pars.add_argument('--nrN', type=int, default=0, help='Start x at 2 * pi * n / m')
pars.add_argument('--nrM', type=int, default=0, help='Start x at 2 * pi * n / m')
pars.add_argument('--fRecess', type=float, default=2.0, help='Recede from envelope by factor')
pars.add_argument("--nSamples", type=int, default=50.0, help="Number of points to sample")
pars.add_argument("--nWidth", type=int, default=3200, help="Width in pixels")
pars.add_argument("--nHeight", type=int, default=100, help="Height in pixels")
pars.add_argument("--nOffsetX", type=int, default=0, help="Starting x coordinate (pixels)")
pars.add_argument("--nOffsetY", type=int, default=400, help="Starting y coordinate (pixels)")
pars.add_argument('--bLace', type=inkex.Boolean, default=False, help='Lace')
pars.add_argument('--bSpline', type=inkex.Boolean, default=True, help='Spline')
self.recess = 0.95
def effect(self):
inkex.NSS[self.nsPrefix] = self.nsURI
if self.options.bLace:
func = 'lace'
else:
func = 'sine'
f_recess = 1.0
if self.options.fRecess > 0.0:
f_recess = 1.0 - self.options.fRecess / 100.0
if f_recess <= 0.0:
f_recess = 0.0
if self.options.ids:
if len(self.options.ids) == 2:
desc1 = self.selected[self.options.ids[0]].get(inkex.addNS('desc', self.nsPrefix))
desc2 = self.selected[self.options.ids[1]].get(inkex.addNS('desc', self.nsPrefix))
if (not desc1) or (not desc2):
inkex.errormsg('Selected objects do not smell right')
return
path_data, path_desc = drawSine(self.options.fCycles,
self.options.nrN,
self.options.nrM,
self.options.nSamples,
[self.options.nOffsetX, self.options.nOffsetY],
self.options.nHeight,
self.options.nWidth,
f_recess,
desc1, desc2, func, self.options.bSpline)
else:
inkex.errormsg('Exactly two objects must be selected')
return
else:
self.document.getroot().set(inkex.addNS(self.nsPrefix, 'xmlns'), self.nsURI)
path_data, path_desc = drawSine(self.options.fCycles,
self.options.nrN,
self.options.nrM,
self.options.nSamples,
[self.options.nOffsetX, self.options.nOffsetY],
self.options.nHeight,
self.options.nWidth,
f_recess,
'',
'',
func,
self.options.bSpline)
style = {'stroke': 'black', 'stroke-width': '1', 'fill': 'none'}
path_attrs = {
'style': str(inkex.Style(style)),
'd': str(Path(path_data)),
inkex.addNS('desc', self.nsPrefix): path_desc}
newpath = etree.SubElement(self.document.getroot(),
inkex.addNS('path', 'svg'), path_attrs, nsmap=inkex.NSS)
if __name__ == '__main__':
SineAndLace().run()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
[
{
"name": "SpiroGraph",
"id": "fablabchemnitz.de.spirograph",
"path": "spirograph",
"dependent_extensions": null,
"original_name": "SpiroGraph",
"original_id": "fsmMLK.spiroGraph",
"license": "GNU GPL v3",
"license_url": "https://github.com/fsmMLK/inkscapeSpirograph/blob/main/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/spirograph",
"fork_url": "https://github.com/fsmMLK/inkscapeSpirograph",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/SpiroGraph",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/fsmMLK",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>SpiroGraph</name>
<id>fablabchemnitz.de.spirograph</id>
<param name="curveType" type="optiongroup" appearance="combo" gui-text="Curve type:">
<option value="Epitrochoid">Epitrochoid</option>
<option value="Hypotrochoid">Hypotrochoid</option>
</param>
<label appearance="header">Curve parameters</label>
<param name="radius_R" type="int" min="0" max="1000" gui-text="Fixed circle radius (R):">10</param>
<param name="radius_r" type="int" min="-1000" max="1000" gui-text="Rolling circle radius (r):">5</param>
<param name="pencil_distance" type="int" min="-1000" max="1000" gui-text="Pencil distance¹ (d):">2</param>
<label>¹ use d=r for Epi/Hypocycloid.</label>
<label appearance="header">Plot parameters</label>
<param name="detailLevel" type="int" min="1" max="10" gui-text="Detail level:">1</param>
<param name="drawBaseCircles" type="bool" gui-text="Draw base circles">false</param>
<param name="animate" type="bool" gui-text="Animate">false</param>
<param type="path" name="directory" gui-text="Animation directory:" mode="folder"/>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">spirograph.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,248 @@
#!/usr/bin/python
import math
import os
import numpy as np
import scipy.signal as scipySignal
import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase
import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw
import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot
# least common multiplier
def myLcm(x, y):
return x * y / math.gcd(int(x), int(y))
# ---------------------------------------------
# noinspection PyAttributeOutsideInit
class Spirograph(inkBase.inkscapeMadeEasy):
def __init__(self):
inkBase.inkscapeMadeEasy.__init__(self)
self.arg_parser.add_argument("--curveType", type=str, dest="curveType", default='resistor')
self.arg_parser.add_argument("--radius_R", type=float, dest="radius_R", default=10.0)
self.arg_parser.add_argument("--radius_r", type=float, dest="radius_r", default=5.0)
self.arg_parser.add_argument("--detailLevel", type=float, dest="detailLevel", default=1.0)
self.arg_parser.add_argument("--adaptiveTheta", type=self.bool, dest="adaptiveTheta", default=False)
self.arg_parser.add_argument("--pencil_distance", type=float, dest="pencil_distance", default=1.0)
self.arg_parser.add_argument("--drawBaseCircles", type=self.bool, dest="drawBaseCircles", default=False)
self.arg_parser.add_argument("--animate", type=self.bool, dest="animate", default=False)
self.arg_parser.add_argument("--directory", type=str, dest="directory", default='./')
def effect(self):
so = self.options
# sets the position to the viewport center, round to next 10.
position = [self.svg.namedview.center[0], self.svg.namedview.center[1]]
position[0] = int(math.ceil(position[0] / 10.0)) * 10
position[1] = int(math.ceil(position[1] / 10.0)) * 10
root_layer = self.document.getroot()
group = self.createGroup(root_layer, 'Spiro')
# curve parameters
R = so.radius_R
r = so.radius_r
d = so.pencil_distance
finalTheta = 2 * np.pi * myLcm(abs(r), R) / R
if 'hypo' in so.curveType.lower():
typeCurve = 'hypo'
if 'epi' in so.curveType.lower():
typeCurve = 'epi'
# markers and linestyles
Lgray = inkDraw.color.gray(0.8)
Dgray = inkDraw.color.gray(0.3)
# wheel
markerCenterDisk = inkDraw.marker.createDotMarker(self, nameID='diskCenter', scale=0.3, RenameMode=1, strokeColor=Dgray,
fillColor=inkDraw.color.defined('white'))
markerPen = inkDraw.marker.createDotMarker(self, nameID='diskPen', scale=0.3, RenameMode=1, strokeColor=Dgray,
fillColor=inkDraw.color.defined('white'))
[startArrowMarker, endArrowMarker] = inkDraw.marker.createArrow1Marker(self, nameID='arrowRot', RenameMode=1, scale=0.3, strokeColor=Dgray,
fillColor=Dgray)
if typeCurve == 'hypo':
self.lineStyleArrow = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=startArrowMarker, markerEnd=None)
else:
self.lineStyleArrow = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=None, markerEnd=endArrowMarker)
self.lineStyleARM = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=markerCenterDisk, markerEnd=markerPen)
self.lineStyleDisk = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=None, fillColor=Lgray)
# curve
self.lineStyleCurve = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('red'), markerStart=None, markerEnd=None,
markerMid=None)
self.lineStyleCurve2 = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('Dgreen'), markerStart=None, markerEnd=None,
markerMid=None)
self.lineStyleCurve3 = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('blue'), markerStart=None, markerEnd=None,
markerMid=None)
self.lineStylePre = inkDraw.lineStyle.set(lineWidth=1, lineColor=inkDraw.color.defined('red'))
self.constructionLine = inkDraw.lineStyle.set(lineWidth=0.5, lineColor=Dgray)
# draft Points
if so.adaptiveTheta:
nPrePoints = 10 * so.detailLevel # number of pre points per turn
thetasDraft = np.linspace(0, finalTheta, int(nPrePoints * finalTheta / (2 * np.pi)))
[pointsDraft, _, curvatureDraft] = self.calcCurve__trochoid(typeCurve, R, r, d, thetasDraft)
# find sampling points based on local curvature
nSamples = np.ones(curvatureDraft.shape)*min(2,so.detailLevel)
detailFactor=5
# treshold normalized curvatures
nSamples[curvatureDraft>0.8] *=detailFactor
detailFactor = 2.5
# check if vector changed direction abuptly
for i,p in enumerate(pointsDraft):
if i==0:
v1=pointsDraft[i+1]-pointsDraft[i]
v2=pointsDraft[i]-pointsDraft[-1]
elif i < len(pointsDraft)-1:
v1=pointsDraft[i+1]-pointsDraft[i]
v2=pointsDraft[i]-pointsDraft[i-1]
else:
v1=pointsDraft[0]-pointsDraft[i]
v2=pointsDraft[i]-pointsDraft[i-1]
v1=v1/np.linalg.norm(v1)
v2=v2/np.linalg.norm(v2)
if np.dot(v1,v2)<0.5:
nSamples[i] *=detailFactor
thetasFinal = np.array([])
for i in range(len(nSamples) - 1):
thetasFinal = np.append(thetasFinal, np.linspace(thetasDraft[i], thetasDraft[i + 1], int(nSamples[i]), endpoint=False))
thetasFinal = np.append(thetasFinal, finalTheta)
# filter the sampled angles to have a smooth transition.
Ntaps = 5
gaussWindow = scipySignal.gaussian(Ntaps, std=5)
gaussWindow = gaussWindow / np.sum(gaussWindow)
# inkPlot.plot.cartesian(self, root_layer, np.arange(thetasFinal.shape[0]), thetasFinal * 180 / np.pi, position, xTicks=False, yTicks=True, xTickStep=thetasFinal.shape[0]/10, yTickStep=120.0, xScale=10, yScale=10,xGrid=True, yGrid=True, forceXlim=None, forceYlim=None)
thetasFinal = scipySignal.filtfilt(gaussWindow, np.array([1]), thetasFinal)
# inkPlot.plot.cartesian(self, root_layer, np.arange(thetasFinal.shape[0]), thetasFinal * 180 / np.pi, position, xTicks=False, yTicks=True,xTickStep=thetasFinal.shape[0]/10, yTickStep=120.0, xScale=10, yScale=10, xGrid=True, yGrid=True, forceXlim=None, forceYlim=None,drawAxis=False)
else:
nPrePoints = 20 * so.detailLevel # number of pre points per turn
thetasFinal = np.linspace(0, finalTheta, int(nPrePoints * finalTheta / (2 * np.pi)))
# final shape
[PointsFinal, CentersFinal, curvatureFinal] = self.calcCurve__trochoid(typeCurve, R, r, d, thetasFinal)
[PointsFinal2, CentersFinal2, curvatureFinal2] = self.calcCurve__trochoid(typeCurve, R, r, -d, thetasFinal)
[PointsFinal3, CentersFinal3, curvatureFinal3] = self.calcCurve__trochoid(typeCurve, R, r, r, thetasFinal)
if so.animate:
animGroup = self.createGroup(group, 'Anim')
circle_R = inkDraw.circle.centerRadius(parent=animGroup, centerPoint=[0, 0], radius=R, offset=position, lineStyle=self.constructionLine)
# draw planetary wheel
wheelGroup = self.createGroup(animGroup, 'Anim')
circle_r = inkDraw.circle.centerRadius(wheelGroup, centerPoint=CentersFinal[0], radius=r, offset=position, lineStyle=self.lineStyleDisk)
arms1 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal[0], PointsFinal[0]], offset=position, lineStyle=self.lineStyleARM)
arms2 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal2[0], PointsFinal2[0]], offset=position, lineStyle=self.lineStyleARM)
arms3 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal3[0], PointsFinal3[0]], offset=position, lineStyle=self.lineStyleARM)
arc1 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=40, angEnd=80, offset=position,
lineStyle=self.lineStyleArrow)
arc2 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=160, angEnd=200,
offset=position, lineStyle=self.lineStyleArrow)
arc3 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=280, angEnd=320,
offset=position, lineStyle=self.lineStyleArrow)
self.exportSVG(animGroup, os.path.join(so.directory,'outSVG_%1.5d.svg' % 0))
for i in range(1, len(thetasFinal)):
self.moveElement(wheelGroup, [CentersFinal[i][0] - CentersFinal[i - 1][0], CentersFinal[i][1] - CentersFinal[i - 1][1]])
if typeCurve == 'hypo':
self.rotateElement(wheelGroup, [position[0] + CentersFinal[i][0], position[1] + CentersFinal[i][1]],
(thetasFinal[i] - thetasFinal[i - 1]) * (R - r) / r * 180 / np.pi)
else:
self.rotateElement(wheelGroup, [position[0] + CentersFinal[i][0], position[1] + CentersFinal[i][1]],
- (thetasFinal[i] - thetasFinal[i - 1]) * (R + r) / r * 180 / np.pi)
curve1 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal[:i + 1], offset=position, lineStyle=self.lineStyleCurve,
closePath=False)
curve2 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal2[:i + 1], offset=position, lineStyle=self.lineStyleCurve2,
closePath=False)
curve3 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal3[:i + 1], offset=position, lineStyle=self.lineStyleCurve3,
closePath=False)
self.exportSVG(animGroup, os.path.join(so.directory , 'outSVG_%1.5d.svg' % i))
self.removeElement(curve1)
self.removeElement(curve2)
self.removeElement(curve3)
self.removeElement(animGroup)
else:
if so.drawBaseCircles:
inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=R, offset=[0, 0], lineStyle=self.constructionLine)
if typeCurve == 'hypo':
inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=r, offset=[R - r, 0], lineStyle=self.constructionLine)
if typeCurve == 'epi':
inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=r, offset=[R + r, 0], lineStyle=self.constructionLine)
inkDraw.line.absCoords(group, PointsFinal, position, 'spiro', self.lineStyleCurve, closePath=True)
# plot curvatures
if False:
inkPlot.plot.polar(self, group, curvatureFinal, thetasFinal * 180 / np.pi, [position[0] + 3 * R, position[1]], rTicks=False,
tTicks=False, rTickStep=0.2, tTickStep=45.0, rScale=20, rGrid=True, tGrid=True, lineStylePlot=self.lineStyleCurve,
forceRlim=[0.0, 1.0])
return
# typeCurve: 'hypo', 'epi'
def calcCurve__trochoid(self, typeCurve, R, r, d, thetas):
j = complex(0, 1)
if typeCurve.lower() == 'hypo':
# https://www.mathcurve.com/courbes2d.gb/hypotrochoid/hypotrochoid.shtml
P_complex = (R - r) * np.exp(j * thetas) + d * np.exp(-j * thetas * (R - r) / r)
dP_complex = (R - r) * j * np.exp(j * thetas) + d * (-j) * (R - r) / r * np.exp(-j * thetas * (R - r) / r)
ddP_complex = (R - r) * (-1) * np.exp(j * thetas) + d * (-1) * ((R - r) / r) ** 2 * np.exp(-j * thetas * (R - r) / r)
centerGear = (R - r) * np.exp(j * thetas)
if typeCurve.lower() == 'epi':
# https://www.mathcurve.com/courbes2d.gb/epitrochoid/epitrochoid.shtml
P_complex = (R + r) * np.exp(j * thetas) - d * np.exp(j * thetas * (R + r) / r)
dP_complex = (R + r) * j * np.exp(j * thetas) - d * j * (R + r) / r * np.exp(j * thetas * (R + r) / r)
ddP_complex = (R + r) * (-1) * np.exp(j * thetas) - d * (-1) * ((R + r) / r) ** 2 * np.exp(j * thetas * (R + r) / r)
centerGear = (R + r) * np.exp(j * thetas)
with np.errstate(divide='ignore', invalid='ignore'):
curvature = np.divide(abs(dP_complex.real * ddP_complex.imag - dP_complex.imag * ddP_complex.real),
(dP_complex.real ** 2 + dP_complex.imag ** 2) ** (2 / 3))
# remove Nan=0/0
np.nan_to_num(curvature, copy=False)
# remove values too large
curvature[curvature > 10 * np.mean(curvature)] = 0.0
# self.Dump(curvature, '/home/fernando/lixo.txt', 'w')
# normalize curvature
curvature = self._normalizeCurvatures(curvature, 0.0, 1.0)
Points = np.column_stack((P_complex.real, P_complex.imag))
Centers = np.column_stack((centerGear.real, centerGear.imag))
return [Points, Centers, curvature]
def _normalizeCurvatures(self, curvatures, normMin=0.0, normMax=1.0):
y1 = normMin
y2 = normMax
x1 = np.min(curvatures)
x2 = np.max(curvatures)
alpha = (y2 - y1) / (x2 - x1)
return alpha * (curvatures - x1) + y1
if __name__ == '__main__':
sp = Spirograph()
sp.run()

View File

@ -0,0 +1,20 @@
[
{
"name": "Unwind Paths",
"id": "fablabchemnitz.de.unwind_paths",
"path": "unwind_paths",
"dependent_extensions": null,
"original_name": "Unwind Paths",
"original_id": "fablabchemnitz.de.unwind_paths",
"license": "GNU GPL v3",
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE",
"comment": "Created by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/unwind_paths",
"fork_url": null,
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Unwind+Paths",
"inkscape_gallery_url": "https://inkscape.org/de/~MarioVoigt/%E2%98%85unwind-paths",
"main_authors": [
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Unwind Paths</name>
<id>fablabchemnitz.de.unwind_paths</id>
<param name="tab" type="notebook">
<page name="tab_settings" gui-text="Unwind Paths">
<label appearance="header">General Settings</label>
<param name="keep_original" type="bool" gui-text="Keep original paths" gui-description="If not selected, the original paths get deleted">false</param>
<param name="break_apart" type="bool" gui-text="Break apart paths" gui-description="Split each path into single curve segments">false</param>
<param name="break_only" type="bool" gui-text="Break apart paths only" gui-description="No unwinding at all">false</param>
<label appearance="header">Color And Style</label>
<param name="colorize" type="bool" gui-text="Colorize" gui-description="Colorize original paths and glue pairs">false</param>
<param name="color_increment" type="int" min="1" max="255" gui-text="Color increment" gui-description="For each segment we count up n colors. Does not apply if 'Randomize colors' is enabled.">10000</param>
<param name="randomize_colors" type="bool" gui-text="Randomize colors">false</param>
<param name="number" type="bool" gui-text="Number segments">false</param>
<label appearance="header">Offset / Extrude Options</label>
<param name="unit" gui-text="Unit" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
</param>
<param name="thickness_offset" type="float" min="-99999.000" max="99999.000" precision="3" gui-text="Thickness offset +/-" gui-description="Allows to add/subtract extra offset length for each curve segment.">0.000</param>
<param name="extrude" type="bool" gui-text="Extrude">false</param>
<param name="extrude_height" type="float" min="0.000" max="99999.000" precision="3" gui-text="Extrude height">10.000</param>
<param name="render_vertical_dividers" type="bool" gui-text="Render vertical dividers">true</param>
<param name="render_with_dashes" type="bool" gui-text="Use dash style for dividers">true</param>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Unwind Paths</label>
<label>An extension to wrap off paths to receive horizontal lines or extruded bands. Can be used for paper crafting, analysis and other works. You can also just use it to colorize path segments. Tip: use "Offset Paths" extension to create offset curves which help to create unwindings with correct material thickness.</label>
<label>2021 - 2022 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/unwindpaths</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">unwind_paths.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,272 @@
#!/usr/bin/env python3
"""
Extension for InkScape 1.0+
Paperfold is another flattener for triangle mesh files, heavily based on paperfoldmodels by Felix Scholz aka felixfeliz.
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Date: 17.05.2021
Last patch: 18.05.2021
License: GNU GPL v3
For each selected path element, this extension creates an additional path element
consisting of horizontal line segments which are the same size as the original
path segments. Has options to extrude as a band (adds height; adds vertical lines and another horizontal path as bottom enclosure)
ToDos:
- option to render separate rectangle shapes
- option to duplicate vertical lines and then to group each 4 lines into one rect-shape like group
- option to colorize vertical line start + end
- option to add glue tabs/flaps
- option to add length text to each segment
- option to add segment/surface numbers
"""
import copy
import inkex
from inkex import Color, bezier, Path, CubicSuperPath, TextElement, Tspan
from inkex.bezier import csplength
from lxml import etree
import math
import random
class UnwindPaths(inkex.EffectExtension):
#draw an SVG line segment between the given (raw) points
def drawline(self, pathData, name, parent, line_style):
line_attribs = {'style' : str(inkex.Style(line_style)), inkex.addNS('label','inkscape') : name, 'd' : pathData}
line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
def add_arguments(self, pars):
pars.add_argument('--tab')
pars.add_argument('--keep_original', type=inkex.Boolean, default=False, help="If not selected, the original paths get deleted")
pars.add_argument('--break_apart', type=inkex.Boolean, default=False, help="Split each path into single curve segments")
pars.add_argument('--break_only', type=inkex.Boolean, default=False, help="Only splits root paths into segments (no unwinding)")
pars.add_argument('--colorize', type=inkex.Boolean, default=False, help="Colorize original paths and glue pairs")
pars.add_argument('--color_increment', type=int, default=10000, help="For each segment we count up n colors. Does not apply if 'Randomize colors' is enabled.")
pars.add_argument('--randomize_colors', type=inkex.Boolean, default=False, help="Randomize colors")
pars.add_argument('--number', type=inkex.Boolean, default=False, help="Number segments")
pars.add_argument('--unit', default="mm")
pars.add_argument('--thickness_offset', type=float, default=0.000, help="Allows to add/subtract extra offset length for each curve segment.")
pars.add_argument('--extrude', type=inkex.Boolean, default=False)
pars.add_argument('--extrude_height', type=float, default=10.000)
pars.add_argument('--render_vertical_dividers', type=inkex.Boolean, default=False)
pars.add_argument('--render_with_dashes', type=inkex.Boolean, default=False)
#if multiple curves are inside the path we split (break apart)
def breakContours(self, element, breakelements = None): #this does the same as "CTRL + SHIFT + K"
if breakelements == None:
breakelements = []
if element.tag == inkex.addNS('path','svg'):
parent = element.getparent()
idx = parent.index(element)
idSuffix = 0
raw = element.path.to_arrays()
subPaths, prev = [], 0
for i in range(len(raw)): # Breaks compound paths into simple paths
if raw[i][0] == 'M' and i != 0:
subPaths.append(raw[prev:i])
prev = i
subPaths.append(raw[prev:])
if len(subPaths) > 1:
for subpath in subPaths:
replacedelement = copy.copy(element)
oldId = replacedelement.get('id')
csp = CubicSuperPath(subpath)
if len(subpath) > 1 and csp[0][0] != csp[0][1]: #avoids pointy paths like M "31.4794 57.6024 Z"
replacedelement.set('d', csp)
if len(subPaths) == 1:
replacedelement.set('id', oldId)
else:
replacedelement.set('id', oldId + str(idSuffix))
idSuffix += 1
parent.insert(idx, replacedelement)
breakelements.append(replacedelement)
parent.remove(element)
else:
breakelements.append(element)
for child in element.getchildren():
self.breakContours(child, breakelements)
return breakelements
def rgb(self, minimum, maximum, value):
minimum, maximum = float(minimum), float(maximum)
ratio = 2 * (value-minimum) / (maximum - minimum)
b = int(max(0, 255 * (1 - ratio)))
r = int(max(0, 255 * (ratio - 1)))
g = 255 - b - r
return r, g, b
def effect(self):
shifting = self.svg.unittouu(str(self.options.extrude_height) + self.options.unit)
to = self.svg.unittouu(str(self.options.thickness_offset) + self.options.unit)
#some mode handling
if self.options.colorize is True or self.options.number:
self.options.break_apart = True #required to make it work
if len(self.svg.selected) > 0:
#we break apart combined paths to get distinct contours
breakApartPaths = []
for element in self.svg.selection.filter(inkex.PathElement).values():
breakApartPaths.append(self.breakContours(element))
for breakApartPath in breakApartPaths:
for element in breakApartPath:
elemGroup = self.svg.get_current_layer().add(inkex.Group(id="unwinding-" + element.get('id')))
#beginning point of the unwind band:
bbox = element.bounding_box() #shift the element to the bottom of the element
xmin = bbox.left
ymax = bbox.bottom + bbox.height * 0.1 #10% additional spacing
csp = element.path.to_superpath()
subCount = len(element.path)
#generate random colors; used to identify glue tab pairs
if self.options.colorize is True:
colorSet = []
if self.options.randomize_colors is True:
while len(colorSet) < subCount - 1:
r = lambda: random.randint(0,255)
newColor = '#%02X%02X%02X' % (r(),r(),r())
if newColor not in colorSet:
colorSet.append(newColor)
else:
for i in range(subCount):
colorSet.append(Color(self.rgb(0, i+self.options.color_increment, 1*i)))
slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit
#self.msg(stotal) #total length of the path
for sub in csp:
#generate new horizontal line data by measuring each segment
new = []
new.append([sub[0]])
i = 1
topPathData = "m {:0.6f},{:0.6f} ".format(xmin, ymax)
bottomPathData = "m {:0.6f},{:0.6f} ".format(xmin, ymax + shifting)
lengths = []
if self.options.break_apart is True:
topLineGroup = self.svg.get_current_layer().add(inkex.Group(id="hline-top-" + element.get('id')))
bottomLineGroup = self.svg.get_current_layer().add(inkex.Group(id="hline-bottom-" + element.get('id')))
elemGroup.append(topLineGroup)
elemGroup.append(bottomLineGroup)
newOriginalPathGroup = self.svg.get_current_layer().add(inkex.Group(id="new-original-" + element.get('id')))
self.svg.get_current_layer().append(newOriginalPathGroup) #we want this to be one level above unwound stuff
if self.options.extrude is True:
vlinesGroup = self.svg.get_current_layer().add(inkex.Group(id="vlines-" + element.get('id')))
elemGroup.append(vlinesGroup)
if self.options.break_only is False:
while i <= len(sub) - 1:
stroke_color = '#000000'
if self.options.colorize is True and self.options.break_apart is True:
stroke_color =colorSet[i-1]
horizontal_line_style = {'stroke':stroke_color,'stroke-width':self.svg.unittouu('1px'),'fill':'none'}
length = bezier.cspseglength(new[-1][-1], sub[i]) + to #sub path length
#if length <= 0:
# inkex.utils.debug("Warning: path id={}, segment={} might overlap with previous and/or next segment. Maybe check for negative thickness offset.".format(element.get('id'), i))
segment = "h {:0.6f} ".format(length)
topPathData += segment
bottomPathData += segment
new[-1].append(sub[i]) #important line!
mid_coord_x = xmin + sum([length for length in lengths]) + length/2
font_size = 5
font_y_offset = font_size + 1
if self.options.number is True:
text = topLineGroup.add(TextElement(id=element.get('id') + "_TextNr{}".format(i)))
text.set("x", "{:0.6f}".format(mid_coord_x))
text.set("y", "{:0.6f}".format(ymax - font_y_offset))
text.set("font-size", "{:0.6f}".format(font_size))
text.set("style", "text-anchor:middle;text-align:center;fill:{}".format(stroke_color))
tspan = text.add(Tspan(id=element.get('id') + "_TSpanNr{}".format(i)))
tspan.set("x", "{:0.6f}".format(mid_coord_x))
if length <= 0:
tspan.set("y", "{:0.6f}".format(ymax - font_y_offset - i))
else:
tspan.set("y", "{:0.6f}".format(ymax - font_y_offset))
tspan.text = str(i)
if self.options.break_apart is True:
self.drawline("m {:0.6f},{:0.6f} ".format(xmin + sum([length for length in lengths]), ymax) + segment,
"segmented-top-{}-{}".format(element.get('id'), i), topLineGroup, horizontal_line_style)
if length <= 0:
self.drawline("m {:0.6f},{:0.6f} ".format(mid_coord_x, ymax) + "v {} ".format(-5-i),
"segmented-top-overlap-{}-{}".format(element.get('id'), i), topLineGroup, horizontal_line_style)
if self.options.extrude is True:
self.drawline("m {:0.6f},{:0.6f} ".format(xmin + sum([length for length in lengths]), ymax + shifting) + segment,
"segmented-bottom-{}-{}".format(element.get('id'), i), bottomLineGroup, horizontal_line_style)
lengths.append(length)
i += 1
if self.options.break_apart is False:
self.drawline(topPathData, "combined-top-{0}".format(element.get('id')), elemGroup, horizontal_line_style)
if self.options.extrude is True:
self.drawline(bottomPathData, "combined-bottom-{0}".format(element.get('id')), elemGroup, horizontal_line_style)
#draw as much vertical lines as segments in bezier + start + end vertical line
vertical_end_lines_style = {'stroke':'#000000','stroke-width':self.svg.unittouu('1px'),'fill':'none'}
if self.options.extrude is True:
#render start line
self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin, ymax, shifting),"vline-{}-start".format(element.get('id')), vlinesGroup, vertical_end_lines_style)
#render divider lines
if self.options.render_vertical_dividers is True:
vertical_mid_lines_style = {'stroke':'#000000','stroke-width':self.svg.unittouu('1px'),'fill':'none'}
if self.options.render_with_dashes is True:
vertical_mid_lines_style = {'stroke':'#000000','stroke-width':self.svg.unittouu('1px'),"stroke-dasharray":"2 2", 'fill':'none'}
x = 0
for n in range(0, i-2):
x += lengths[n]
self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin + x, ymax, shifting),"vline-{}-{}".format(element.get('id'), n + 1), vlinesGroup, vertical_mid_lines_style)
#render end line
self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin + sum([length for length in lengths]), ymax, shifting),"vline-{}-end".format(element.get('id')), vlinesGroup, vertical_end_lines_style)
if self.options.break_apart is True:
# Split (already broken apart) paths into detached segments
raw = Path(element.get("d")).to_arrays() #returns Uppercase Command Letters; does not include H, V
for i in range(len(raw)):
if i > 0:
if raw[i-1][0] in ("M", "L"):
startPoint = "M {},{}".format(raw[i-1][1][0], raw[i-1][1][1])
elif raw[i-1][0] == 'C':
startPoint = "M {},{}".format(raw[i-1][1][-2], raw[i-1][1][-1])
else:
inkex.utils.debug("Start point error. Unknown command!")
if raw[i][0] in ("M", "L"):
segment = " {},{}".format(raw[i][1][0], raw[i][1][1])
elif raw[i][0] == 'C':
segment = "{} {}".format(raw[i][0], ''.join(str(raw[i][1]))[1:-1])
elif raw[i][0] == 'Z':
segment = "{},{}".format(raw[0][1][0], raw[0][1][1])
else:
inkex.utils.debug("Segment error. Unknown command!")
d = str(Path("{} {}".format(startPoint, segment)))
stroke_color = '#000000'
if self.options.colorize is True:
stroke_color =colorSet[i-1]
new_original_line_style = {'stroke':stroke_color,'stroke-width':self.svg.unittouu('1px'),'fill':'none'}
self.drawline(d, "segmented-" + element.get('id'), newOriginalPathGroup, new_original_line_style)
if self.options.keep_original is False:
element.delete()
else:
self.msg('Please select some paths first.')
return
if __name__ == '__main__':
UnwindPaths().run()

View File

@ -26,7 +26,7 @@
<page name="tab_about" gui-text="About">
<label appearance="header">Vektorkollektor</label>
<label>This extension generates SVG data from given plt data.</label>
<label>2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<label>2021 - 2022 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/vektorkollektor</label>