2021-05-17 16:19:20 +02:00
#!/usr/bin/env python3
"""
2021-05-18 12:54:40 +02:00
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
2021-05-17 16:19:20 +02:00
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
2021-05-18 12:54:40 +02:00
path segments . Has options to extrude as a band ( adds height ; adds vertical lines and another horizontal path as bottom enclosure )
2021-05-17 16:19:20 +02:00
2021-05-18 12:54:40 +02:00
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
2021-05-17 16:19:20 +02:00
"""
2021-05-18 12:54:40 +02:00
import copy
2021-05-17 16:19:20 +02:00
import inkex
2021-05-20 13:31:03 +02:00
from inkex import Color , bezier , Path , CubicSuperPath
2021-05-17 16:19:20 +02:00
from lxml import etree
import math
2021-05-18 12:54:40 +02:00
import random
2021-05-17 16:19:20 +02:00
class UnwindPaths ( inkex . EffectExtension ) :
#draw an SVG line segment between the given (raw) points
2021-05-18 12:54:40 +02:00
def drawline ( self , pathData , name , parent , line_style ) :
line_attribs = { ' style ' : str ( inkex . Style ( line_style ) ) , inkex . addNS ( ' label ' , ' inkscape ' ) : name , ' d ' : pathData }
2021-05-17 16:19:20 +02:00
line = etree . SubElement ( parent , inkex . addNS ( ' path ' , ' svg ' ) , line_attribs )
def add_arguments ( self , pars ) :
2021-05-18 12:54:40 +02:00
pars . add_argument ( ' --tab ' )
pars . add_argument ( ' --keep_original ' , type = inkex . Boolean , default = False , help = " If selected, the original paths get deleted " )
2021-05-19 02:14:15 +02:00
pars . add_argument ( ' --break_apart ' , type = inkex . Boolean , default = False , help = " Split each path into single curve segments " )
2021-05-20 13:31:03 +02:00
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 " )
2021-05-17 16:19:20 +02:00
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 " )
2021-05-18 19:38:58 +02:00
pars . add_argument ( ' --render_vertical_dividers ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --render_with_dashes ' , type = inkex . Boolean , default = False )
2021-05-18 12:54:40 +02:00
#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 ' )
replacedelement . set ( ' d ' , CubicSuperPath ( subpath ) )
replacedelement . set ( ' id ' , oldId + str ( idSuffix ) . zfill ( 5 ) )
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
2021-05-17 16:19:20 +02:00
2021-05-20 13:31:03 +02:00
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
2021-05-17 16:19:20 +02:00
def effect ( self ) :
shifting = self . svg . unittouu ( str ( self . options . extrude_height ) + self . options . unit )
2021-05-18 12:54:40 +02:00
2021-05-18 19:38:58 +02:00
#some mode handling
if self . options . colorize is True :
self . options . break_apart = True #required to make it work
2021-05-18 12:54:40 +02:00
if len ( self . svg . selected ) > 0 :
#we break apart combined paths to get distinct contours
2021-05-19 02:14:15 +02:00
breakApartPaths = [ ]
2021-05-18 12:54:40 +02:00
for element in self . svg . selection . filter ( inkex . PathElement ) . values ( ) :
2021-05-19 02:14:15 +02:00
breakApartPaths . append ( self . breakContours ( element ) )
2021-05-18 12:54:40 +02:00
2021-05-19 02:14:15 +02:00
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 )
2021-05-17 16:19:20 +02:00
2021-05-19 02:14:15 +02:00
#generate random colors; used to identify glue tab pairs
if self . options . colorize is True :
2021-05-20 13:31:03 +02:00
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 ) ) )
2021-05-19 02:14:15 +02:00
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 = [ ]
2021-05-18 12:54:40 +02:00
if self . options . break_apart is True :
2021-05-19 02:14:15 +02:00
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 )
2021-05-20 13:31:03 +02:00
newOriginalPathGroup = self . svg . get_current_layer ( ) . add ( inkex . Group ( id = " new-original- " + element . get ( ' id ' ) ) )
2021-05-19 02:14:15 +02:00
self . svg . get_current_layer ( ) . append ( newOriginalPathGroup ) #we want this to be one level above unwound stuff
2021-05-18 12:54:40 +02:00
if self . options . extrude is True :
2021-05-19 02:14:15 +02:00
vlinesGroup = self . svg . get_current_layer ( ) . add ( inkex . Group ( id = " vlines- " + element . get ( ' id ' ) ) )
elemGroup . append ( vlinesGroup )
2021-05-20 13:31:03 +02:00
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 )
2021-05-19 02:14:15 +02:00
if self . options . extrude is True :
2021-05-20 13:31:03 +02:00
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 ' }
2021-05-19 02:14:15 +02:00
if self . options . extrude is True :
2021-05-20 13:31:03 +02:00
#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 )
2021-05-19 02:14:15 +02:00
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 :
2021-05-20 13:31:03 +02:00
stroke_color = colorSet [ i - 1 ]
2021-05-19 02:14:15 +02:00
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 :
2021-05-20 13:31:03 +02:00
element . delete ( )
2021-05-18 12:54:40 +02:00
else :
self . msg ( ' Please select some paths first. ' )
return
2021-05-17 16:19:20 +02:00
if __name__ == ' __main__ ' :
UnwindPaths ( ) . run ( )