.
This commit is contained in:
67
extensions/fablabchemnitz/unwind_paths/unwind_paths.inx
Normal file
67
extensions/fablabchemnitz/unwind_paths/unwind_paths.inx
Normal file
@ -0,0 +1,67 @@
|
||||
<?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 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 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>
|
||||
<label appearance="header">Extrude Options</label>
|
||||
<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="unit" gui-text="Unit" type="optiongroup" appearance="combo">
|
||||
<option value="mm">mm</option>
|
||||
<option value="cm">cm</option>
|
||||
<option value="m">m</option>
|
||||
<option value="in">in</option>
|
||||
<option value="pt">pt</option>
|
||||
<option value="px">px</option>
|
||||
</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.</label>
|
||||
<label>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/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>
|
238
extensions/fablabchemnitz/unwind_paths/unwind_paths.py
Normal file
238
extensions/fablabchemnitz/unwind_paths/unwind_paths.py
Normal file
@ -0,0 +1,238 @@
|
||||
#!/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
|
||||
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 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('--extrude', type=inkex.Boolean, default=False)
|
||||
pars.add_argument('--extrude_height', type=float, default=10.000)
|
||||
pars.add_argument('--unit', default="mm")
|
||||
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)
|
||||
replacedelement.set('id', oldId + str(idSuffix))
|
||||
parent.insert(idx, replacedelement)
|
||||
idSuffix += 1
|
||||
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)
|
||||
|
||||
#some mode handling
|
||||
if self.options.colorize is True:
|
||||
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)))
|
||||
|
||||
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':'1px','fill':'none'}
|
||||
|
||||
length = bezier.cspseglength(new[-1][-1], sub[i]) #sub path length
|
||||
segment = "h {:0.6f} ".format(length)
|
||||
topPathData += segment
|
||||
bottomPathData += segment
|
||||
new[-1].append(sub[i]) #important line!
|
||||
if self.options.break_apart is True:
|
||||
self.drawline("m {:0.6f},{:0.0f} ".format(xmin + sum([length for length in lengths]), ymax) + segment,
|
||||
"segmented-top-{}-{}".format(element.get('id'), i), topLineGroup, horizontal_line_style)
|
||||
if self.options.extrude is True:
|
||||
self.drawline("m {:0.6f},{:0.0f} ".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':'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':'1px','fill':'none'}
|
||||
if self.options.render_with_dashes is True:
|
||||
vertical_mid_lines_style = {'stroke':'#000000','stroke-width':'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':'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()
|
Reference in New Issue
Block a user