several fixes and more extensions from 1.1 back again
@ -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>
|
||||
|
@ -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>
|
448
extensions/fablabchemnitz/buxtronix_living_hinges/buxtronix_living_hinges.py
Executable 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()
|
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 3.7 KiB |
@ -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 |
After Width: | Height: | Size: 852 B |
After Width: | Height: | Size: 2.1 KiB |
21
extensions/fablabchemnitz/buxtronix_living_hinges/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
50
extensions/fablabchemnitz/chip_scratches/chip_scratches.inx
Normal 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>
|
733
extensions/fablabchemnitz/chip_scratches/chip_scratches.py
Normal file
21
extensions/fablabchemnitz/chip_scratches/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
@ -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>
|
@ -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()
|
21
extensions/fablabchemnitz/ellipse_by_five_points/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
16
extensions/fablabchemnitz/fun_shapes/fun_shapes.inx
Executable 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>
|
199
extensions/fablabchemnitz/fun_shapes/fun_shapes.py
Normal 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()
|
21
extensions/fablabchemnitz/fun_shapes/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
@ -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>
|
@ -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()
|
@ -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"
|
@ -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 ""
|
21
extensions/fablabchemnitz/hatches_and_grains/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
50
extensions/fablabchemnitz/ids_to_text/ids_to_text.inx
Normal 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>
|
103
extensions/fablabchemnitz/ids_to_text/ids_to_text.py
Normal 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()
|
21
extensions/fablabchemnitz/ids_to_text/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
80
extensions/fablabchemnitz/ifs_fractals/ifs_fractals.inx
Normal 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>
|
72
extensions/fablabchemnitz/ifs_fractals/ifs_fractals.py
Normal 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()
|
21
extensions/fablabchemnitz/ifs_fractals/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
@ -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>
|
@ -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()
|
21
extensions/fablabchemnitz/insert_paper_template/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
109
extensions/fablabchemnitz/join_paths/join_paths.inx
Normal 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>
|
429
extensions/fablabchemnitz/join_paths/join_paths.py
Normal 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()
|
21
extensions/fablabchemnitz/join_paths/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
37
extensions/fablabchemnitz/living_hinge/living_hinge.inx
Normal 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>
|
264
extensions/fablabchemnitz/living_hinge/living_hinge.py
Normal 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()
|
22
extensions/fablabchemnitz/living_hinge/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
@ -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>
|
||||
|
20
extensions/fablabchemnitz/normalize_drawing_scale/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
@ -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>
|
@ -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
@ -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/
|
20
extensions/fablabchemnitz/ocr/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
145
extensions/fablabchemnitz/ocr/ocr.inx
Normal 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>
|
47
extensions/fablabchemnitz/ocr/ocr.py
Executable 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()
|
BIN
extensions/fablabchemnitz/ocr/read.png
Normal file
After Width: | Height: | Size: 40 KiB |
20
extensions/fablabchemnitz/offset_paths/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
80
extensions/fablabchemnitz/offset_paths/offset_paths.inx
Normal 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>
|
153
extensions/fablabchemnitz/offset_paths/offset_paths.py
Normal 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()
|
20
extensions/fablabchemnitz/open_dir/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
14
extensions/fablabchemnitz/open_dir/open_dir.inx
Normal 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>
|
36
extensions/fablabchemnitz/open_dir/open_dir.py
Normal 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()
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
@ -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>
|
@ -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()
|
21
extensions/fablabchemnitz/polygon_side/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
22
extensions/fablabchemnitz/polygon_side/polygon_side.inx
Executable 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>
|
118
extensions/fablabchemnitz/polygon_side/polygon_side.py
Normal 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()
|
21
extensions/fablabchemnitz/sine_and_lace/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
47
extensions/fablabchemnitz/sine_and_lace/sine_and_lace.inx
Normal 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>
|
332
extensions/fablabchemnitz/sine_and_lace/sine_and_lace.py
Normal 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()
|
21
extensions/fablabchemnitz/spirograph/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
30
extensions/fablabchemnitz/spirograph/spirograph.inx
Normal 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>
|
248
extensions/fablabchemnitz/spirograph/spirograph.py
Normal 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()
|
20
extensions/fablabchemnitz/unwind_paths/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
68
extensions/fablabchemnitz/unwind_paths/unwind_paths.inx
Normal 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>
|
272
extensions/fablabchemnitz/unwind_paths/unwind_paths.py
Normal 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()
|
@ -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>
|
||||
|