2020-09-13 03:20:36 +02:00
#!/usr/bin/env python3
import openmesh as om
2021-05-06 22:44:14 +02:00
import math
2020-09-13 03:20:36 +02:00
import inkex
import tempfile
import os
2021-05-08 15:07:57 +02:00
import random
2020-09-13 03:20:36 +02:00
import numpy as np
import openmesh as om
import networkx as nx
from lxml import etree
2021-05-08 16:08:07 +02:00
from inkex import Transform , TextElement , Tspan , Color , Circle
2020-09-13 03:20:36 +02:00
"""
Extension for InkScape 1.0
2020-09-13 22:44:17 +02:00
Paperfold is another flattener for triangle mesh files , heavily based on paperfoldmodels by Felix Scholz aka felixfeliz .
2020-09-13 03:20:36 +02:00
Author : Mario Voigt / FabLab Chemnitz
Mail : mario . voigt @stadtfabrikanten.org
Date : 13.09 .2020
2021-05-08 15:07:57 +02:00
Last patch : 08.05 .2021
2020-09-13 03:20:36 +02:00
License : GNU GPL v3
2020-09-13 22:44:17 +02:00
To run this you need to install OpenMesh with python pip .
The algorithm of paperfoldmodels consists of three steps :
- Find a minimum spanning tree of the dual graph of the mesh .
- Unfold the dual graph .
- Remove self - intersections by adding additional cuts along edges .
Reference : The code is mostly based on the algorithm presented in a by Straub and Prautzsch ( https : / / geom . ivd . kit . edu / downloads / proj - paper - models_cut_out_sheets . pdf ) .
2020-09-13 03:20:36 +02:00
Module licenses
- paperfoldmodels ( https : / / github . com / felixfeliz / paperfoldmodels ) - MIT License
possible import file types - > https : / / www . graphics . rwth - aachen . de / media / openmesh_static / Documentations / OpenMesh - 8.0 - Documentation / a04096 . html
"""
# Compute the third point of a triangle when two points and all edge lengths are given
def getThirdPoint ( v0 , v1 , l01 , l12 , l20 ) :
v2rotx = ( l01 * * 2 + l20 * * 2 - l12 * * 2 ) / ( 2 * l01 )
v2roty0 = np . sqrt ( ( l01 + l20 + l12 ) * ( l01 + l20 - l12 ) * ( l01 - l20 + l12 ) * ( - l01 + l20 + l12 ) ) / ( 2 * l01 )
v2roty1 = - v2roty0
theta = np . arctan2 ( v1 [ 1 ] - v0 [ 1 ] , v1 [ 0 ] - v0 [ 0 ] )
v2trans0 = np . array (
[ v2rotx * np . cos ( theta ) - v2roty0 * np . sin ( theta ) , v2rotx * np . sin ( theta ) + v2roty0 * np . cos ( theta ) , 0 ] )
v2trans1 = np . array (
[ v2rotx * np . cos ( theta ) - v2roty1 * np . sin ( theta ) , v2rotx * np . sin ( theta ) + v2roty1 * np . cos ( theta ) , 0 ] )
return [ v2trans0 + v0 , v2trans1 + v0 ]
# Check if two lines intersect
def lineIntersection ( v1 , v2 , v3 , v4 , epsilon ) :
d = ( v4 [ 1 ] - v3 [ 1 ] ) * ( v2 [ 0 ] - v1 [ 0 ] ) - ( v4 [ 0 ] - v3 [ 0 ] ) * ( v2 [ 1 ] - v1 [ 1 ] )
u = ( v4 [ 0 ] - v3 [ 0 ] ) * ( v1 [ 1 ] - v3 [ 1 ] ) - ( v4 [ 1 ] - v3 [ 1 ] ) * ( v1 [ 0 ] - v3 [ 0 ] )
v = ( v2 [ 0 ] - v1 [ 0 ] ) * ( v1 [ 1 ] - v3 [ 1 ] ) - ( v2 [ 1 ] - v1 [ 1 ] ) * ( v1 [ 0 ] - v3 [ 0 ] )
if d < 0 :
u , v , d = - u , - v , - d
return ( ( 0 + epsilon ) < = u < = ( d - epsilon ) ) and ( ( 0 + epsilon ) < = v < = ( d - epsilon ) )
# Check if a point lies inside a triangle
def pointInTriangle ( A , B , C , P , epsilon ) :
v0 = [ C [ 0 ] - A [ 0 ] , C [ 1 ] - A [ 1 ] ]
v1 = [ B [ 0 ] - A [ 0 ] , B [ 1 ] - A [ 1 ] ]
v2 = [ P [ 0 ] - A [ 0 ] , P [ 1 ] - A [ 1 ] ]
cross = lambda u , v : u [ 0 ] * v [ 1 ] - u [ 1 ] * v [ 0 ]
u = cross ( v2 , v0 )
v = cross ( v1 , v2 )
d = cross ( v1 , v0 )
if d < 0 :
u , v , d = - u , - v , - d
return u > = ( 0 + epsilon ) and v > = ( 0 + epsilon ) and ( u + v ) < = ( d - epsilon )
# Check if two triangles intersect
def triangleIntersection ( t1 , t2 , epsilon ) :
if lineIntersection ( t1 [ 0 ] , t1 [ 1 ] , t2 [ 0 ] , t2 [ 1 ] , epsilon ) : return True
if lineIntersection ( t1 [ 0 ] , t1 [ 1 ] , t2 [ 0 ] , t2 [ 2 ] , epsilon ) : return True
if lineIntersection ( t1 [ 0 ] , t1 [ 1 ] , t2 [ 1 ] , t2 [ 2 ] , epsilon ) : return True
if lineIntersection ( t1 [ 0 ] , t1 [ 2 ] , t2 [ 0 ] , t2 [ 1 ] , epsilon ) : return True
if lineIntersection ( t1 [ 0 ] , t1 [ 2 ] , t2 [ 0 ] , t2 [ 2 ] , epsilon ) : return True
if lineIntersection ( t1 [ 0 ] , t1 [ 2 ] , t2 [ 1 ] , t2 [ 2 ] , epsilon ) : return True
if lineIntersection ( t1 [ 1 ] , t1 [ 2 ] , t2 [ 0 ] , t2 [ 1 ] , epsilon ) : return True
if lineIntersection ( t1 [ 1 ] , t1 [ 2 ] , t2 [ 0 ] , t2 [ 2 ] , epsilon ) : return True
if lineIntersection ( t1 [ 1 ] , t1 [ 2 ] , t2 [ 1 ] , t2 [ 2 ] , epsilon ) : return True
inTri = True
inTri = inTri and pointInTriangle ( t1 [ 0 ] , t1 [ 1 ] , t1 [ 2 ] , t2 [ 0 ] , epsilon )
inTri = inTri and pointInTriangle ( t1 [ 0 ] , t1 [ 1 ] , t1 [ 2 ] , t2 [ 1 ] , epsilon )
inTri = inTri and pointInTriangle ( t1 [ 0 ] , t1 [ 1 ] , t1 [ 2 ] , t2 [ 2 ] , epsilon )
if inTri == True : return True
inTri = True
inTri = inTri and pointInTriangle ( t2 [ 0 ] , t2 [ 1 ] , t2 [ 2 ] , t1 [ 0 ] , epsilon )
inTri = inTri and pointInTriangle ( t2 [ 0 ] , t2 [ 1 ] , t2 [ 2 ] , t1 [ 1 ] , epsilon )
inTri = inTri and pointInTriangle ( t2 [ 0 ] , t2 [ 1 ] , t2 [ 2 ] , t1 [ 2 ] , epsilon )
if inTri == True : return True
return False
# Functions for visualisation and output
def addVisualisationData ( mesh , unfoldedMesh , originalHalfedges , unfoldedHalfedges , glueNumber , foldingDirection ) :
for i in range ( 3 ) :
2021-05-08 00:19:56 +02:00
foldingDirection [ unfoldedMesh . edge_handle ( unfoldedHalfedges [ i ] ) . idx ( ) ] = round ( math . degrees ( mesh . calc_dihedral_angle ( originalHalfedges [ i ] ) ) , 3 )
2020-09-13 03:20:36 +02:00
# Information, which edges belong together
glueNumber [ unfoldedMesh . edge_handle ( unfoldedHalfedges [ i ] ) . idx ( ) ] = mesh . edge_handle ( originalHalfedges [ i ] ) . idx ( )
# Function that unwinds a spanning tree
def unfoldSpanningTree ( mesh , spanningTree ) :
2021-05-06 22:44:14 +02:00
unfoldedMesh = om . TriMesh ( ) # the unfolded mesh
2020-09-13 03:20:36 +02:00
numFaces = mesh . n_faces ( )
sizeTree = spanningTree . number_of_edges ( )
numUnfoldedEdges = 3 * numFaces - sizeTree
isFoldingEdge = np . zeros ( numUnfoldedEdges , dtype = bool ) # Indicates whether an edge is folded or cut
glueNumber = np . empty ( numUnfoldedEdges , dtype = int ) # Saves with which edge is glued together
2021-05-08 00:19:56 +02:00
foldingDirection = np . empty ( numUnfoldedEdges , dtype = float ) # Valley folding or mountain folding
2020-09-13 03:20:36 +02:00
connections = np . empty ( numFaces , dtype = int ) # Saves which original triangle belongs to the unrolled one
# Select the first triangle as desired
startingNode = list ( spanningTree . nodes ( ) ) [ 0 ]
startingTriangle = mesh . face_handle ( startingNode )
# We unwind the first triangle
# All half edges of the first triangle
firstHalfEdge = mesh . halfedge_handle ( startingTriangle )
secondHalfEdge = mesh . next_halfedge_handle ( firstHalfEdge )
thirdHalfEdge = mesh . next_halfedge_handle ( secondHalfEdge )
originalHalfEdges = [ firstHalfEdge , secondHalfEdge , thirdHalfEdge ]
# Calculate the lengths of the edges, this will determine the shape of the triangle (congruence)
edgelengths = [ mesh . calc_edge_length ( firstHalfEdge ) , mesh . calc_edge_length ( secondHalfEdge ) ,
mesh . calc_edge_length ( thirdHalfEdge ) ]
# The first two points
firstUnfoldedPoint = np . array ( [ 0 , 0 , 0 ] )
secondUnfoldedPoint = np . array ( [ edgelengths [ 0 ] , 0 , 0 ] )
# We calculate the third point of the triangle from the first two. There are two possibilities
[ thirdUnfolded0 , thirdUnfolded1 ] = getThirdPoint ( firstUnfoldedPoint , secondUnfoldedPoint , edgelengths [ 0 ] ,
edgelengths [ 1 ] ,
edgelengths [ 2 ] )
if thirdUnfolded0 [ 1 ] > 0 :
thirdUnfoldedPoint = thirdUnfolded0
else :
thirdUnfoldePoint = thirdUnfolded1
# Add the new corners to the unwound net
firstUnfoldedVertex = unfoldedMesh . add_vertex ( secondUnfoldedPoint )
secondUnfoldedVertex = unfoldedMesh . add_vertex ( thirdUnfoldedPoint )
thirdUnfoldedVertex = unfoldedMesh . add_vertex ( firstUnfoldedPoint )
#firstUnfoldedVertex = unfoldedMesh.add_vertex(firstUnfoldedPoint)
#secondUnfoldedVertex = unfoldedMesh.add_vertex(secondUnfoldedPoint)
#thirdUnfoldedVertex = unfoldedMesh.add_vertex(thirdUnfoldedPoint)
# Create the page
unfoldedFace = unfoldedMesh . add_face ( firstUnfoldedVertex , secondUnfoldedVertex , thirdUnfoldedVertex )
# Memory properties of the surface and edges
# The half edges in unrolled mesh
firstUnfoldedHalfEdge = unfoldedMesh . opposite_halfedge_handle ( unfoldedMesh . halfedge_handle ( firstUnfoldedVertex ) )
secondUnfoldedHalfEdge = unfoldedMesh . next_halfedge_handle ( firstUnfoldedHalfEdge )
thirdUnfoldedHalfEdge = unfoldedMesh . next_halfedge_handle ( secondUnfoldedHalfEdge )
unfoldedHalfEdges = [ firstUnfoldedHalfEdge , secondUnfoldedHalfEdge , thirdUnfoldedHalfEdge ]
# Associated triangle in 3D mesh
connections [ unfoldedFace . idx ( ) ] = startingTriangle . idx ( )
# Folding direction and adhesive number
addVisualisationData ( mesh , unfoldedMesh , originalHalfEdges , unfoldedHalfEdges , glueNumber , foldingDirection )
halfEdgeConnections = { firstHalfEdge . idx ( ) : firstUnfoldedHalfEdge . idx ( ) ,
secondHalfEdge . idx ( ) : secondUnfoldedHalfEdge . idx ( ) ,
thirdHalfEdge . idx ( ) : thirdUnfoldedHalfEdge . idx ( ) }
# We walk through the tree
for dualEdge in nx . dfs_edges ( spanningTree , source = startingNode ) :
foldingEdge = mesh . edge_handle ( spanningTree [ dualEdge [ 0 ] ] [ dualEdge [ 1 ] ] [ ' idx ' ] )
# Find the corresponding half edge in the output triangle
foldingHalfEdge = mesh . halfedge_handle ( foldingEdge , 0 )
if not ( mesh . face_handle ( foldingHalfEdge ) . idx ( ) == dualEdge [ 0 ] ) :
foldingHalfEdge = mesh . halfedge_handle ( foldingEdge , 1 )
# Find the corresponding unwound half edge
unfoldedLastHalfEdge = unfoldedMesh . halfedge_handle ( halfEdgeConnections [ foldingHalfEdge . idx ( ) ] )
# Find the point in the unrolled triangle that is not on the folding edge
oppositeUnfoldedVertex = unfoldedMesh . to_vertex_handle ( unfoldedMesh . next_halfedge_handle ( unfoldedLastHalfEdge ) )
# We turn the half edges over to lie in the new triangle
foldingHalfEdge = mesh . opposite_halfedge_handle ( foldingHalfEdge )
unfoldedLastHalfEdge = unfoldedMesh . opposite_halfedge_handle ( unfoldedLastHalfEdge )
# The two corners of the folding edge
unfoldedFromVertex = unfoldedMesh . from_vertex_handle ( unfoldedLastHalfEdge )
unfoldedToVertex = unfoldedMesh . to_vertex_handle ( unfoldedLastHalfEdge )
# Calculate the edge lengths in the new triangle
secondHalfEdgeInFace = mesh . next_halfedge_handle ( foldingHalfEdge )
thirdHalfEdgeInFace = mesh . next_halfedge_handle ( secondHalfEdgeInFace )
originalHalfEdges = [ foldingHalfEdge , secondHalfEdgeInFace , thirdHalfEdgeInFace ]
edgelengths = [ mesh . calc_edge_length ( foldingHalfEdge ) , mesh . calc_edge_length ( secondHalfEdgeInFace ) ,
mesh . calc_edge_length ( thirdHalfEdgeInFace ) ]
# We calculate the two possibilities for the third point in the triangle
[ newUnfoldedVertex0 , newUnfoldedVertex1 ] = getThirdPoint ( unfoldedMesh . point ( unfoldedFromVertex ) ,
unfoldedMesh . point ( unfoldedToVertex ) , edgelengths [ 0 ] ,
edgelengths [ 1 ] , edgelengths [ 2 ] )
newUnfoldedVertex = unfoldedMesh . add_vertex ( newUnfoldedVertex0 )
# Make the face
newface = unfoldedMesh . add_face ( unfoldedFromVertex , unfoldedToVertex , newUnfoldedVertex )
secondUnfoldedHalfEdge = unfoldedMesh . next_halfedge_handle ( unfoldedLastHalfEdge )
thirdUnfoldedHalfEdge = unfoldedMesh . next_halfedge_handle ( secondUnfoldedHalfEdge )
unfoldedHalfEdges = [ unfoldedLastHalfEdge , secondUnfoldedHalfEdge , thirdUnfoldedHalfEdge ]
# Saving the information about edges and page
# Dotted line in the output
unfoldedLastEdge = unfoldedMesh . edge_handle ( unfoldedLastHalfEdge )
isFoldingEdge [ unfoldedLastEdge . idx ( ) ] = True
# Gluing number and folding direction
addVisualisationData ( mesh , unfoldedMesh , originalHalfEdges , unfoldedHalfEdges , glueNumber , foldingDirection )
# Related page
connections [ newface . idx ( ) ] = dualEdge [ 1 ]
# Identify the half edges
for i in range ( 3 ) :
halfEdgeConnections [ originalHalfEdges [ i ] . idx ( ) ] = unfoldedHalfEdges [ i ] . idx ( )
return [ unfoldedMesh , isFoldingEdge , connections , glueNumber , foldingDirection ]
2021-05-06 15:21:10 +02:00
def unfold ( mesh , maxNumFaces , printStats ) :
2020-09-13 03:20:36 +02:00
# Calculate the number of surfaces, edges and corners, as well as the length of the longest shortest edge
numEdges = mesh . n_edges ( )
numVertices = mesh . n_vertices ( )
numFaces = mesh . n_faces ( )
2021-05-06 15:21:10 +02:00
if numFaces > maxNumFaces :
inkex . utils . debug ( " Aborted. Target STL file has " + str ( numFaces ) + " faces, but " + str ( maxNumFaces ) + " are allowed. " )
exit ( 1 )
if printStats is True :
inkex . utils . debug ( " Input STL mesh stats: " )
inkex . utils . debug ( " * Number of edges: " + str ( numEdges ) )
inkex . utils . debug ( " * Number of vertices: " + str ( numVertices ) )
inkex . utils . debug ( " * Number of faces: " + str ( numFaces ) )
inkex . utils . debug ( " ----------------------------------------------------------- " )
2020-09-13 03:20:36 +02:00
# Generate the dual graph of the mesh and calculate the weights
dualGraph = nx . Graph ( )
# For the weights: calculate the longest and shortest edge of the triangle
minLength = 1000
maxLength = 0
for edge in mesh . edges ( ) :
edgelength = mesh . calc_edge_length ( edge )
if edgelength < minLength :
minLength = edgelength
if edgelength > maxLength :
maxLength = edgelength
# All edges in the net
for edge in mesh . edges ( ) :
# The two sides adjacent to the edge
face1 = mesh . face_handle ( mesh . halfedge_handle ( edge , 0 ) )
face2 = mesh . face_handle ( mesh . halfedge_handle ( edge , 1 ) )
# The weight
edgeweight = 1.0 - ( mesh . calc_edge_length ( edge ) - minLength ) / ( maxLength - minLength )
# Calculate the centres of the pages (only necessary for visualisation)
center1 = ( 0 , 0 )
for vertex in mesh . fv ( face1 ) :
center1 = center1 + 0.3333333333333333 * np . array ( [ mesh . point ( vertex ) [ 0 ] , mesh . point ( vertex ) [ 2 ] ] )
center2 = ( 0 , 0 )
for vertex in mesh . fv ( face2 ) :
center2 = center2 + 0.3333333333333333 * np . array ( [ mesh . point ( vertex ) [ 0 ] , mesh . point ( vertex ) [ 2 ] ] )
# Add the new nodes and edge to the dual graph
dualGraph . add_node ( face1 . idx ( ) , pos = center1 )
dualGraph . add_node ( face2 . idx ( ) , pos = center2 )
dualGraph . add_edge ( face1 . idx ( ) , face2 . idx ( ) , idx = edge . idx ( ) , weight = edgeweight )
# Calculate the minimum spanning tree
spanningTree = nx . minimum_spanning_tree ( dualGraph )
2020-09-14 00:04:58 +02:00
# Unfold the tree
2020-09-13 03:20:36 +02:00
fullUnfolding = unfoldSpanningTree ( mesh , spanningTree )
[ unfoldedMesh , isFoldingEdge , connections , glueNumber , foldingDirection ] = fullUnfolding
# Resolve the intersections
# Find all intersections
epsilon = 1E-12 # Accuracy
faceIntersections = [ ]
for face1 in unfoldedMesh . faces ( ) :
for face2 in unfoldedMesh . faces ( ) :
if face2 . idx ( ) < face1 . idx ( ) : # so that we do not double check the couples
# Get the triangle faces
triangle1 = [ ]
triangle2 = [ ]
for halfedge in unfoldedMesh . fh ( face1 ) :
triangle1 . append ( unfoldedMesh . point ( unfoldedMesh . from_vertex_handle ( halfedge ) ) )
for halfedge in unfoldedMesh . fh ( face2 ) :
triangle2 . append ( unfoldedMesh . point ( unfoldedMesh . from_vertex_handle ( halfedge ) ) )
if triangleIntersection ( triangle1 , triangle2 , epsilon ) :
faceIntersections . append ( [ connections [ face1 . idx ( ) ] , connections [ face2 . idx ( ) ] ] )
# Find the paths
# We find the minimum number of cuts to resolve any self-intersection
# Search all paths between overlapping triangles
paths = [ ]
for intersection in faceIntersections :
paths . append (
nx . algorithms . shortest_paths . shortest_path ( spanningTree , source = intersection [ 0 ] , target = intersection [ 1 ] ) )
# Find all edges in all threads
edgepaths = [ ]
for path in paths :
edgepath = [ ]
for i in range ( len ( path ) - 1 ) :
edgepath . append ( ( path [ i ] , path [ i + 1 ] ) )
edgepaths . append ( edgepath )
# List of all edges in all paths
allEdgesInPaths = list ( set ( ) . union ( * edgepaths ) )
# Count how often each edge occurs
numEdgesInPaths = [ ]
for edge in allEdgesInPaths :
num = 0
for path in edgepaths :
if edge in path :
num = num + 1
numEdgesInPaths . append ( num )
S = [ ]
C = [ ]
while len ( C ) != len ( paths ) :
# Calculate the weights to decide which edge to cut
cutWeights = np . empty ( len ( allEdgesInPaths ) )
for i in range ( len ( allEdgesInPaths ) ) :
currentEdge = allEdgesInPaths [ i ]
# Count how many of the paths in which the edge occurs have already been cut
numInC = 0
for path in C :
if currentEdge in path :
numInC = numInC + 1
# Determine the weight
if ( numEdgesInPaths [ i ] - numInC ) > 0 :
cutWeights [ i ] = 1 / ( numEdgesInPaths [ i ] - numInC )
else :
cutWeights [ i ] = 1000 # 1000 = infinite
# Find the edge with the least weight
minimalIndex = np . argmin ( cutWeights )
S . append ( allEdgesInPaths [ minimalIndex ] )
# Find all paths where the edge occurs and add them to C
for path in edgepaths :
if allEdgesInPaths [ minimalIndex ] in path and not path in C :
C . append ( path )
# Now we remove the cut edges from the minimum spanning tree
spanningTree . remove_edges_from ( S )
# Find the cohesive components
connectedComponents = nx . algorithms . components . connected_components ( spanningTree )
connectedComponentList = list ( connectedComponents )
# Unfolding of the components
unfoldings = [ ]
for component in connectedComponentList :
unfoldings . append ( unfoldSpanningTree ( mesh , spanningTree . subgraph ( component ) ) )
return fullUnfolding , unfoldings
def findBoundingBox ( mesh ) :
firstpoint = mesh . point ( mesh . vertex_handle ( 0 ) )
xmin = firstpoint [ 0 ]
xmax = firstpoint [ 0 ]
ymin = firstpoint [ 1 ]
ymax = firstpoint [ 1 ]
for vertex in mesh . vertices ( ) :
coordinates = mesh . point ( vertex )
if ( coordinates [ 0 ] < xmin ) :
xmin = coordinates [ 0 ]
if ( coordinates [ 0 ] > xmax ) :
xmax = coordinates [ 0 ]
if ( coordinates [ 1 ] < ymin ) :
ymin = coordinates [ 1 ]
if ( coordinates [ 1 ] > ymax ) :
ymax = coordinates [ 1 ]
boxSize = np . maximum ( np . abs ( xmax - xmin ) , np . abs ( ymax - ymin ) )
return [ xmin , ymin , boxSize ]
2021-05-08 16:08:07 +02:00
def writeSVG ( self , unfolding , size , randomColorSet ) :
2020-09-13 03:20:36 +02:00
mesh = unfolding [ 0 ]
isFoldingEdge = unfolding [ 1 ]
glueNumber = unfolding [ 3 ]
foldingDirection = unfolding [ 4 ]
2021-05-06 15:21:10 +02:00
#statistic values
gluePairs = 0
mountainCuts = 0
valleyCuts = 0
2021-05-06 22:44:14 +02:00
coplanarLines = 0
2021-05-06 15:21:10 +02:00
mountainPerforations = 0
valleyPerforations = 0
2020-09-13 03:20:36 +02:00
# Calculate the bounding box
[ xmin , ymin , boxSize ] = findBoundingBox ( unfolding [ 0 ] )
if size > 0 :
boxSize = size
2021-05-08 16:08:07 +02:00
strokewidth = boxSize * self . options . fontSize / 8000
dashLength = boxSize * self . options . fontSize / 2000
spaceLength = boxSize * self . options . fontSize / 800
2021-05-08 00:19:56 +02:00
textDistance = boxSize * self . options . fontSize / 800
2021-05-08 16:08:07 +02:00
textStrokewidth = boxSize * self . options . fontSize / 3000
2021-05-08 00:19:56 +02:00
fontsize = boxSize * self . options . fontSize / 1000
2020-09-13 03:20:36 +02:00
2021-05-08 19:29:02 +02:00
minAngle = min ( foldingDirection )
maxAngle = max ( foldingDirection )
angleRange = maxAngle - minAngle
#self.msg(minAngle)
#self.msg(maxAngle)
#self.msg(angleRange)
# Grouping
uniqueMainId = self . svg . get_unique_id ( " " )
paperfoldPageGroup = self . document . getroot ( ) . add ( inkex . Group ( id = uniqueMainId + " -paperfold-page " ) )
textGroup = inkex . Group ( id = uniqueMainId + " -text " )
edgesGroup = inkex . Group ( id = uniqueMainId + " -edges " )
paperfoldPageGroup . add ( textGroup )
paperfoldPageGroup . add ( edgesGroup )
textFacesGroup = inkex . Group ( id = uniqueMainId + " -textFaces " )
textEdgesGroup = inkex . Group ( id = uniqueMainId + " -textEdges " )
textGroup . add ( textFacesGroup )
textGroup . add ( textEdgesGroup )
2021-05-08 16:08:07 +02:00
if self . options . printTriangleNumbers is True :
2021-05-08 19:29:02 +02:00
2021-05-08 16:08:07 +02:00
for face in mesh . faces ( ) :
centroid = mesh . calc_face_centroid ( face )
2021-05-08 19:29:02 +02:00
textFaceGroup = inkex . Group ( id = uniqueMainId + " -textFace- " + str ( face . idx ( ) ) )
circle = textFaceGroup . add ( Circle ( cx = str ( centroid [ 0 ] ) , cy = str ( centroid [ 1 ] ) , r = str ( fontsize ) ) )
circle . set ( ' id ' , uniqueMainId + " -textFaceCricle- " + str ( face . idx ( ) ) )
2021-05-08 16:08:07 +02:00
circle . set ( " style " , " stroke:#000000;stroke-width: " + str ( strokewidth / 2 ) + " ;fill:none " )
2021-05-08 19:29:02 +02:00
text = textFaceGroup . add ( TextElement ( id = uniqueMainId + " -textFaceNumber- " + str ( face . idx ( ) ) ) )
2021-05-08 16:08:07 +02:00
text . set ( " x " , str ( centroid [ 0 ] ) )
text . set ( " y " , str ( centroid [ 1 ] + fontsize / 3 ) )
text . set ( " font-size " , str ( fontsize ) )
text . set ( " style " , " stroke-width: " + str ( textStrokewidth ) + " ;text-anchor:middle;text-align:center " )
2021-05-08 19:29:02 +02:00
tspan = text . add ( Tspan ( id = uniqueMainId + " -textFaceNumberTspan- " + str ( face . idx ( ) ) ) )
2021-05-08 16:08:07 +02:00
tspan . set ( " x " , str ( centroid [ 0 ] ) )
tspan . set ( " y " , str ( centroid [ 1 ] + fontsize / 3 ) )
tspan . set ( " style " , " stroke-width: " + str ( textStrokewidth ) + " ;text-anchor:middle;text-align:center " )
2021-05-08 19:29:02 +02:00
tspan . text = str ( face . idx ( ) )
textFacesGroup . append ( textFaceGroup )
2020-09-13 03:20:36 +02:00
# Go over all edges of the grid
for edge in mesh . edges ( ) :
# The two endpoints
he = mesh . halfedge_handle ( edge , 0 )
vertex0 = mesh . point ( mesh . from_vertex_handle ( he ) )
vertex1 = mesh . point ( mesh . to_vertex_handle ( he ) )
# Write a straight line between the two corners
2021-05-08 19:29:02 +02:00
line = edgesGroup . add ( inkex . PathElement ( ) )
2020-09-13 03:20:36 +02:00
line . set ( ' d ' , " M " + str ( vertex0 [ 0 ] ) + " , " + str ( vertex0 [ 1 ] ) + " " + str ( vertex1 [ 0 ] ) + " , " + str ( vertex1 [ 1 ] ) )
# Colour depending on folding direction
lineStyle = { " fill " : " none " }
2021-05-08 00:19:56 +02:00
dihedralAngle = foldingDirection [ edge . idx ( ) ]
if dihedralAngle > 0 :
2021-05-08 15:07:57 +02:00
lineStyle . update ( { " stroke " : self . options . colorMountainCut } )
2021-05-08 19:29:02 +02:00
line . set ( " id " , uniqueMainId + " -mountain-cut- " + str ( edge . idx ( ) ) )
2021-05-06 15:21:10 +02:00
mountainCuts + = 1
2021-05-08 00:19:56 +02:00
elif dihedralAngle < 0 :
2021-05-08 15:07:57 +02:00
lineStyle . update ( { " stroke " : self . options . colorValleyCut } )
2021-05-08 19:29:02 +02:00
line . set ( " id " , uniqueMainId + " -valley-cut- " + str ( edge . idx ( ) ) )
2021-05-06 15:21:10 +02:00
valleyCuts + = 1
2021-05-08 00:19:56 +02:00
elif dihedralAngle == 0 :
2021-05-08 15:07:57 +02:00
lineStyle . update ( { " stroke " : self . options . colorCoplanarEdges } )
2021-05-08 19:29:02 +02:00
line . set ( " id " , uniqueMainId + " -coplanar-edge- " + str ( edge . idx ( ) ) )
2021-05-08 16:08:07 +02:00
#if self.options.importCoplanarEdges is False:
# line.delete()
2021-05-06 22:44:14 +02:00
coplanarLines + = 1
2021-05-06 15:21:10 +02:00
2020-09-13 03:20:36 +02:00
lineStyle . update ( { " stroke-width " : str ( strokewidth ) } )
lineStyle . update ( { " stroke-linecap " : " butt " } )
lineStyle . update ( { " stroke-linejoin " : " miter " } )
lineStyle . update ( { " stroke-miterlimit " : " 4 " } )
# Dotted lines for folding edges
2021-05-08 00:19:56 +02:00
if isFoldingEdge [ edge . idx ( ) ] :
if self . options . dashes is True :
lineStyle . update ( { " stroke-dasharray " : ( str ( dashLength ) + " , " + str ( spaceLength ) ) } )
if dihedralAngle > 0 :
2021-05-08 15:07:57 +02:00
lineStyle . update ( { " stroke " : self . options . colorMountainPerforates } )
2021-05-08 19:29:02 +02:00
line . set ( " id " , uniqueMainId + " -mountain-perforate- " + str ( edge . idx ( ) ) )
2021-05-06 15:21:10 +02:00
mountainPerforations + = 1
2021-05-08 00:19:56 +02:00
if dihedralAngle < 0 :
2021-05-08 15:07:57 +02:00
lineStyle . update ( { " stroke " : self . options . colorValleyPerforates } )
2021-05-08 19:29:02 +02:00
line . set ( " id " , uniqueMainId + " -valley-perforate- " + str ( edge . idx ( ) ) )
2021-05-08 16:08:07 +02:00
valleyPerforations + = 1
if dihedralAngle == 0 :
lineStyle . update ( { " stroke " : self . options . colorCoplanarEdges } )
2021-05-08 19:29:02 +02:00
line . set ( " id " , uniqueMainId + " -coplanar-edge- " + str ( edge . idx ( ) ) )
2021-05-08 16:08:07 +02:00
if self . options . importCoplanarEdges is False :
line . delete ( )
valleyPerforations + = 1
2020-09-13 03:20:36 +02:00
else :
lineStyle . update ( { " stroke-dasharray " : " none " } )
2021-05-08 15:07:57 +02:00
# The number of the edge to be glued
if not isFoldingEdge [ edge . idx ( ) ] :
if self . options . separateGluePairsByColor is True :
lineStyle . update ( { " stroke " : randomColorSet [ glueNumber [ edge . idx ( ) ] ] } )
gluePairs + = 1
2020-09-13 03:20:36 +02:00
lineStyle . update ( { " stroke-dashoffset " : " 0 " } )
lineStyle . update ( { " stroke-opacity " : " 1 " } )
2021-05-08 19:29:02 +02:00
if self . options . saturationsForAngles is True :
if dihedralAngle != 0 : #we dont want to apply HSL adjustments for zero angle lines because they would be invisible then
hslColor = inkex . Color ( lineStyle . get ( ' stroke ' ) ) . to_hsl ( )
newSaturation = abs ( dihedralAngle / angleRange ) * 100 #percentage values
hslColor . saturation = newSaturation
lineStyle . update ( { " stroke " : hslColor . to_rgb ( ) } )
2020-09-13 03:20:36 +02:00
line . style = lineStyle
2021-05-08 16:08:07 +02:00
2021-05-08 19:29:02 +02:00
########################################################
2021-05-08 15:07:57 +02:00
# Textual things
2021-05-08 19:29:02 +02:00
########################################################
2021-05-08 15:07:57 +02:00
halfEdge = mesh . halfedge_handle ( edge , 0 ) # Find halfedge in the face
if mesh . face_handle ( halfEdge ) . idx ( ) == - 1 :
halfEdge = mesh . opposite_halfedge_handle ( halfEdge )
vector = mesh . calc_edge_vector ( halfEdge )
2021-05-08 16:08:07 +02:00
vector = vector / np . linalg . norm ( vector ) # normalize
2021-05-08 15:07:57 +02:00
midPoint = 0.5 * (
mesh . point ( mesh . from_vertex_handle ( halfEdge ) ) + mesh . point ( mesh . to_vertex_handle ( halfEdge ) ) )
rotatedVector = np . array ( [ - vector [ 1 ] , vector [ 0 ] , 0 ] )
angle = np . arctan2 ( vector [ 1 ] , vector [ 0 ] )
position = midPoint + textDistance * rotatedVector
if self . options . flipLabels is True :
position = midPoint - textDistance * rotatedVector
rotation = 180 / np . pi * angle
if self . options . flipLabels is True :
rotation + = 180
2021-05-08 19:29:02 +02:00
text = textEdgesGroup . add ( TextElement ( id = uniqueMainId + " -edgeNumber- " + str ( edge . idx ( ) ) ) )
2021-05-08 15:07:57 +02:00
text . set ( " x " , str ( position [ 0 ] ) )
text . set ( " y " , str ( position [ 1 ] ) )
text . set ( " font-size " , str ( fontsize ) )
text . set ( " style " , " stroke-width: " + str ( textStrokewidth ) + " ;text-anchor:middle;text-align:center " )
text . set ( " transform " , " rotate( " + str ( rotation ) + " , " + str ( position [ 0 ] ) + " , " + str ( position [ 1 ] ) + " ) " )
tspan = text . add ( Tspan ( ) )
tspan . set ( " x " , str ( position [ 0 ] ) )
tspan . set ( " y " , str ( position [ 1 ] ) )
tspan . set ( " style " , " stroke-width: " + str ( textStrokewidth ) + " ;text-anchor:middle;text-align:center " )
tspanText = [ ]
if self . options . printGluePairNumbers is True and not isFoldingEdge [ edge . idx ( ) ] :
tspanText . append ( str ( glueNumber [ edge . idx ( ) ] ) )
if self . options . printAngles is True :
tspanText . append ( " {:0.2f} ° " . format ( dihedralAngle ) )
if self . options . printLengths is True :
printUnit = True
if printUnit is False :
unitToPrint = self . svg . unit
else :
unitToPrint = " "
tspanText . append ( " {:0.2f} {} " . format ( self . options . scalefactor * math . hypot ( vertex1 [ 0 ] - vertex0 [ 0 ] , vertex1 [ 1 ] - vertex0 [ 1 ] ) , unitToPrint ) )
tspan . text = " | " . join ( tspanText )
if ( self . options . printGluePairNumbers is False and self . options . printAngles is False and self . options . printLengths is False ) or self . options . importCoplanarEdges is False and dihedralAngle == 0 :
text . delete ( )
tspan . delete ( )
2021-05-09 22:36:59 +02:00
#delete unrequired groups if no text labels
if ( self . options . printGluePairNumbers is False and self . options . printAngles is False and self . options . printLengths is False ) or self . options . importCoplanarEdges is False and dihedralAngle == 0 :
textGroup . delete ( )
textFacesGroup . delete ( )
textEdgesGroup . delete ( )
2021-05-08 15:07:57 +02:00
2021-05-06 15:21:10 +02:00
if self . options . printStats is True :
2021-05-08 00:19:56 +02:00
inkex . utils . debug ( " Folding edges stats: " )
2021-05-06 15:21:10 +02:00
inkex . utils . debug ( " * Number of mountain cuts: " + str ( mountainCuts ) )
inkex . utils . debug ( " * Number of valley cuts: " + str ( valleyCuts ) )
2021-05-06 22:44:14 +02:00
inkex . utils . debug ( " * Number of coplanar lines: " + str ( coplanarLines ) )
2021-05-06 15:21:10 +02:00
inkex . utils . debug ( " * Number of mountain perforations: " + str ( mountainPerforations ) )
inkex . utils . debug ( " * Number of valley perforations: " + str ( valleyPerforations ) )
inkex . utils . debug ( " ----------------------------------------------------------- " )
2021-05-08 15:07:57 +02:00
inkex . utils . debug ( " Number of glue pairs: {:0.0f} " . format ( gluePairs / 2 ) )
2021-05-06 15:21:10 +02:00
2020-09-13 03:20:36 +02:00
return paperfoldPageGroup
2021-04-15 17:03:47 +02:00
class Unfold ( inkex . EffectExtension ) :
def add_arguments ( self , pars ) :
2021-04-19 20:54:38 +02:00
pars . add_argument ( " --tab " )
2021-05-08 15:07:57 +02:00
#Input
2021-04-15 17:03:47 +02:00
pars . add_argument ( " --inputfile " )
2021-05-06 15:21:10 +02:00
pars . add_argument ( " --maxNumFaces " , type = int , default = 200 , help = " If the STL file has too much detail it contains a large number of faces. This will make unfolding extremely slow. So we can limit it. " )
2021-04-15 17:03:47 +02:00
pars . add_argument ( " --scalefactor " , type = float , default = 1.0 , help = " Manual scale factor " )
2021-05-08 15:07:57 +02:00
#Output
pars . add_argument ( " --printGluePairNumbers " , type = inkex . Boolean , default = False , help = " Print glue pair numbers on cut edges " )
pars . add_argument ( " --printAngles " , type = inkex . Boolean , default = False , help = " Print folding angles on edges " )
2021-05-08 16:08:07 +02:00
pars . add_argument ( " --printLengths " , type = inkex . Boolean , default = False , help = " Print lengths on edges " )
pars . add_argument ( " --printTriangleNumbers " , type = inkex . Boolean , default = False , help = " Print triangle numbers on faces " )
2021-05-08 15:07:57 +02:00
pars . add_argument ( " --importCoplanarEdges " , type = inkex . Boolean , default = False , help = " Import coplanar edges " )
pars . add_argument ( " --printStats " , type = inkex . Boolean , default = True , help = " Show some unfold statistics " )
2021-04-15 17:03:47 +02:00
pars . add_argument ( " --resizetoimport " , type = inkex . Boolean , default = True , help = " Resize the canvas to the imported drawing ' s bounding box " )
pars . add_argument ( " --extraborder " , type = float , default = 0.0 )
2021-05-08 15:07:57 +02:00
pars . add_argument ( " --extraborderUnits " )
#Style
pars . add_argument ( " --fontSize " , type = int , default = 15 , help = " Label font size ( % ) " )
pars . add_argument ( " --flipLabels " , type = inkex . Boolean , default = False , help = " Flip labels " )
2021-05-08 19:29:02 +02:00
pars . add_argument ( " --dashes " , type = inkex . Boolean , default = True , help = " Dashes for cut/coplanar edges " )
pars . add_argument ( " --saturationsForAngles " , type = inkex . Boolean , help = " Adjust color saturation for folding edges. The larger the angle the darker the color " )
2021-05-08 15:07:57 +02:00
pars . add_argument ( " --separateGluePairsByColor " , type = inkex . Boolean , default = False , help = " Separate glue pairs by color " )
pars . add_argument ( " --colorValleyCut " , type = Color , default = ' 255 ' , help = " Valley cut edges " )
pars . add_argument ( " --colorMountainCut " , type = Color , default = ' 1968208895 ' , help = " Mountain cut edges " )
pars . add_argument ( " --colorCoplanarEdges " , type = Color , default = ' 1943148287 ' , help = " Coplanar edges " )
pars . add_argument ( " --colorValleyPerforates " , type = Color , default = ' 3422552319 ' , help = " Valley perforation edges " )
pars . add_argument ( " --colorMountainPerforates " , type = Color , default = ' 879076607 ' , help = " Mountain perforation edges " )
2021-05-06 15:21:10 +02:00
2020-09-13 03:20:36 +02:00
def effect ( self ) :
2021-05-06 15:21:10 +02:00
if not os . path . exists ( self . options . inputfile ) :
inkex . utils . debug ( " The input file does not exist. Please select a proper file and try again. " )
exit ( 1 )
2020-09-13 03:20:36 +02:00
mesh = om . read_trimesh ( self . options . inputfile )
2021-05-06 22:44:14 +02:00
#mesh = om.read_polymesh(self.options.inputfile) #we must work with triangles instead of polygons because the algorithm works with that
2021-05-06 15:21:10 +02:00
fullUnfolded , unfoldedComponents = unfold ( mesh , self . options . maxNumFaces , self . options . printStats )
2020-09-13 03:20:36 +02:00
# Compute maxSize of the components
# All components must be scaled to the same size as the largest component
maxSize = 0
for unfolding in unfoldedComponents :
[ xmin , ymin , boxSize ] = findBoundingBox ( unfolding [ 0 ] )
if boxSize > maxSize :
maxSize = boxSize
2021-05-09 22:36:59 +02:00
#generate random colors; used to identify glue tab pairs
2021-05-08 16:08:07 +02:00
randomColorSet = [ ]
if self . options . separateGluePairsByColor :
while len ( randomColorSet ) < len ( mesh . edges ( ) ) :
r = lambda : random . randint ( 0 , 255 )
newColor = ' # %02X %02X %02X ' % ( r ( ) , r ( ) , r ( ) )
if newColor not in randomColorSet :
randomColorSet . append ( newColor )
2021-05-08 19:29:02 +02:00
2020-09-13 03:20:36 +02:00
# Create a new container group to attach all paperfolds
paperfoldMainGroup = self . document . getroot ( ) . add ( inkex . Group ( id = self . svg . get_unique_id ( " paperfold- " ) ) ) #make a new group at root level
for i in range ( len ( unfoldedComponents ) ) :
2021-05-08 16:08:07 +02:00
paperfoldPageGroup = writeSVG ( self , unfoldedComponents [ i ] , maxSize , randomColorSet )
2020-09-13 03:20:36 +02:00
#translate the groups next to each other to remove overlappings
if i != 0 :
previous_bbox = paperfoldMainGroup [ i - 1 ] . bounding_box ( )
this_bbox = paperfoldPageGroup . bounding_box ( )
paperfoldPageGroup . set ( " transform " , " translate( " + str ( previous_bbox . left + previous_bbox . width - this_bbox . left ) + " , 0.0) " )
paperfoldMainGroup . append ( paperfoldPageGroup )
#apply scale factor
translation_matrix = [ [ self . options . scalefactor , 0.0 , 0.0 ] , [ 0.0 , self . options . scalefactor , 0.0 ] ]
paperfoldMainGroup . transform = Transform ( translation_matrix ) * paperfoldMainGroup . transform
#paperfoldMainGroup.set('transform', 'scale(%f,%f)' % (self.options.scalefactor, self.options.scalefactor))
#adjust canvas to the inserted unfolding
if self . options . resizetoimport :
bbox = paperfoldMainGroup . bounding_box ( )
namedView = self . document . getroot ( ) . find ( inkex . addNS ( ' namedview ' , ' sodipodi ' ) )
doc_units = namedView . get ( inkex . addNS ( ' document-units ' , ' inkscape ' ) )
root = self . svg . getElement ( ' //svg:svg ' ) ;
2021-05-08 15:07:57 +02:00
offset = self . svg . unittouu ( str ( self . options . extraborder ) + self . options . extraborderUnits )
2020-09-13 03:20:36 +02:00
root . set ( ' viewBox ' , ' %f %f %f %f ' % ( bbox . left - offset , bbox . top - offset , bbox . width + 2 * offset , bbox . height + 2 * offset ) )
root . set ( ' width ' , str ( bbox . width + 2 * offset ) + doc_units )
root . set ( ' height ' , str ( bbox . height + 2 * offset ) + doc_units )
if __name__ == ' __main__ ' :
Unfold ( ) . run ( )