2020-09-13 03:20:36 +02:00
#!/usr/bin/env python3
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-10 04:34:04 +02:00
Last patch : 10.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
2021-05-08 22:22:29 +02:00
todo :
2021-05-10 04:34:04 +02:00
- option to render all triangles in a detached way ( overlapping lines / independent ) + merge coplanar adjacent triangles to polygons
- write tab and slot generator ( like joinery / polyhedra extension )
- fstl preview
2021-05-10 13:54:23 +02:00
- fix line : dualGraph . add_edge ( face1 . idx ( ) , face2 . idx ( ) , idx = edge . idx ( ) , weight = edgeweight ) # #might fail without throwing any error (silent aborts) ...
2021-05-11 23:46:05 +02:00
- option to set fill color per face
2021-05-12 01:22:17 +02:00
- add some way to merge coplanar triangles ( tri - faces ) to polygons and keep those polygons ( facets ) intact . At the moment facets are getting destroyed . Not good for some papercrafts
2020-09-13 03:20:36 +02:00
"""
2021-06-02 23:30:37 +02:00
class Paperfold ( inkex . EffectExtension ) :
2020-09-13 03:20:36 +02:00
2021-05-10 13:54:23 +02:00
angleRangeCalculated = False #set to true after first calculation iteration (needed globally)
minAngle = 0
minAngle = 0
angleRange = 0
2021-05-10 04:34:04 +02:00
# Compute the third point of a triangle when two points and all edge lengths are given
def getThirdPoint ( self , 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 ( self , 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 ( self , 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 ( self , t1 , t2 , epsilon ) :
if self . lineIntersection ( t1 [ 0 ] , t1 [ 1 ] , t2 [ 0 ] , t2 [ 1 ] , epsilon ) : return True
if self . lineIntersection ( t1 [ 0 ] , t1 [ 1 ] , t2 [ 0 ] , t2 [ 2 ] , epsilon ) : return True
if self . lineIntersection ( t1 [ 0 ] , t1 [ 1 ] , t2 [ 1 ] , t2 [ 2 ] , epsilon ) : return True
if self . lineIntersection ( t1 [ 0 ] , t1 [ 2 ] , t2 [ 0 ] , t2 [ 1 ] , epsilon ) : return True
if self . lineIntersection ( t1 [ 0 ] , t1 [ 2 ] , t2 [ 0 ] , t2 [ 2 ] , epsilon ) : return True
if self . lineIntersection ( t1 [ 0 ] , t1 [ 2 ] , t2 [ 1 ] , t2 [ 2 ] , epsilon ) : return True
if self . lineIntersection ( t1 [ 1 ] , t1 [ 2 ] , t2 [ 0 ] , t2 [ 1 ] , epsilon ) : return True
if self . lineIntersection ( t1 [ 1 ] , t1 [ 2 ] , t2 [ 0 ] , t2 [ 2 ] , epsilon ) : return True
if self . lineIntersection ( t1 [ 1 ] , t1 [ 2 ] , t2 [ 1 ] , t2 [ 2 ] , epsilon ) : return True
inTri = True
inTri = inTri and self . pointInTriangle ( t1 [ 0 ] , t1 [ 1 ] , t1 [ 2 ] , t2 [ 0 ] , epsilon )
inTri = inTri and self . pointInTriangle ( t1 [ 0 ] , t1 [ 1 ] , t1 [ 2 ] , t2 [ 1 ] , epsilon )
inTri = inTri and self . pointInTriangle ( t1 [ 0 ] , t1 [ 1 ] , t1 [ 2 ] , t2 [ 2 ] , epsilon )
if inTri == True : return True
inTri = True
inTri = inTri and self . pointInTriangle ( t2 [ 0 ] , t2 [ 1 ] , t2 [ 2 ] , t1 [ 0 ] , epsilon )
inTri = inTri and self . pointInTriangle ( t2 [ 0 ] , t2 [ 1 ] , t2 [ 2 ] , t1 [ 1 ] , epsilon )
inTri = inTri and self . pointInTriangle ( t2 [ 0 ] , t2 [ 1 ] , t2 [ 2 ] , t1 [ 2 ] , epsilon )
if inTri == True : return True
return False
# Functions for visualisation and output
2021-05-10 13:54:23 +02:00
def addVisualisationData ( self , mesh , unfoldedMesh , originalHalfedges , unfoldedHalfedges , glueNumber , dihedralAngles ) :
2020-09-13 03:20:36 +02:00
for i in range ( 3 ) :
2021-05-10 13:54:23 +02:00
dihedralAngles [ unfoldedMesh . edge_handle ( unfoldedHalfedges [ i ] ) . idx ( ) ] = round ( math . degrees ( mesh . calc_dihedral_angle ( originalHalfedges [ i ] ) ) , self . options . roundingDigits )
2021-05-10 04:34:04 +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 ( self , mesh , spanningTree ) :
try :
unfoldedMesh = om . TriMesh ( ) # the unfolded mesh
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-10 13:54:23 +02:00
dihedralAngles = np . empty ( numUnfoldedEdges , dtype = float ) # Valley folding or mountain folding
2021-05-10 04:34:04 +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 ] = self . getThirdPoint ( firstUnfoldedPoint , secondUnfoldedPoint , edgelengths [ 0 ] ,
edgelengths [ 1 ] ,
edgelengths [ 2 ] )
if thirdUnfolded0 [ 1 ] > 0 :
thirdUnfoldedPoint = thirdUnfolded0
2020-09-13 03:20:36 +02:00
else :
2021-05-10 04:34:04 +02:00
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
2021-05-10 13:54:23 +02:00
self . addVisualisationData ( mesh , unfoldedMesh , originalHalfEdges , unfoldedHalfEdges , glueNumber , dihedralAngles )
if self . angleRangeCalculated is False :
self . minAngle = min ( dihedralAngles )
self . maxAngle = max ( dihedralAngles )
#sometimes weird large value are returned, like -34345645435464565453356788761029782
if self . minAngle < - 180.0 :
self . minAngle = - 180.0
if self . maxAngle > 180.0 :
self . maxAngle = 180.0
self . angleRange = self . maxAngle - self . minAngle
#self.msg(minAngle)
#self.msg(maxAngle)
#self.msg(angleRange)
self . angleRangeCalculated = True
2021-05-10 04:34:04 +02:00
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 ] = self . 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 one's in the output
unfoldedLastEdge = unfoldedMesh . edge_handle ( unfoldedLastHalfEdge )
isFoldingEdge [ unfoldedLastEdge . idx ( ) ] = True
# Gluing number and folding direction
2021-05-10 13:54:23 +02:00
self . addVisualisationData ( mesh , unfoldedMesh , originalHalfEdges , unfoldedHalfEdges , glueNumber , dihedralAngles )
2021-05-10 04:34:04 +02:00
# Related page
connections [ newface . idx ( ) ] = dualEdge [ 1 ]
# Identify the half edges
for i in range ( 3 ) :
halfEdgeConnections [ originalHalfEdges [ i ] . idx ( ) ] = unfoldedHalfEdges [ i ] . idx ( )
2021-05-10 13:54:23 +02:00
return [ unfoldedMesh , isFoldingEdge , connections , glueNumber , dihedralAngles ]
2021-05-10 04:34:04 +02:00
except Exception as e :
inkex . utils . debug ( " Error: model could not be unfolded. Check for: " )
inkex . utils . debug ( " - watertight model / intact hull " )
inkex . utils . debug ( " - duplicated edges or faces " )
inkex . utils . debug ( " - detached faces or holes " )
inkex . utils . debug ( " - missing units " )
inkex . utils . debug ( " - missing coordinate system " )
2021-05-10 13:54:23 +02:00
inkex . utils . debug ( " - multiple bodies in one file " )
inkex . utils . debug ( " error was: " + str ( e ) )
2021-05-10 04:34:04 +02:00
exit ( 1 )
def unfold ( self , mesh ) :
# 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-08 19:29:02 +02:00
2021-05-10 04:34:04 +02:00
if numFaces > self . options . maxNumFaces :
2021-05-10 13:54:23 +02:00
inkex . utils . debug ( " Aborted. Target STL file has " + str ( numFaces ) + " faces, but only " + str ( self . options . maxNumFaces ) + " are allowed. " )
2021-05-10 04:34:04 +02:00
exit ( 1 )
2021-05-08 19:29:02 +02:00
2021-05-10 04:34:04 +02:00
if self . options . 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 ( " ----------------------------------------------------------- " )
2021-05-08 19:29:02 +02:00
2021-05-10 04:34:04 +02:00
# Generate the dual graph of the mesh and calculate the weights
dualGraph = nx . Graph ( )
2021-05-08 19:29:02 +02:00
2021-05-10 04:34:04 +02:00
# 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 ( ) :
#inkex.utils.debug("edge.idx = " + str(edge.idx()))
2021-05-08 19:29:02 +02:00
2021-05-10 04:34:04 +02:00
# 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 ) )
2021-05-12 01:22:17 +02:00
2021-05-10 04:34:04 +02:00
# The weight
edgeweight = 1.0 - ( mesh . calc_edge_length ( edge ) - minLength ) / ( maxLength - minLength )
2021-05-12 01:22:17 +02:00
if self . options . experimentalWeights is True :
if round ( math . degrees ( mesh . calc_dihedral_angle ( edge ) ) , self . options . roundingDigits ) > 0 :
edgeweight = 1.0 - ( mesh . calc_edge_length ( edge ) - minLength ) / ( maxLength - minLength )
if round ( math . degrees ( mesh . calc_dihedral_angle ( edge ) ) , self . options . roundingDigits ) < 0 :
edgeweight = - ( 1.0 - ( mesh . calc_edge_length ( edge ) - minLength ) / ( maxLength - minLength ) )
if round ( math . degrees ( mesh . calc_dihedral_angle ( edge ) ) , self . options . roundingDigits ) == 0 :
edgeweight = 0.0
2021-05-10 04:34:04 +02:00
#inkex.utils.debug("edgeweight = " + str(edgeweight))
# 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 ) # #might fail without throwing any error ...
# Calculate the minimum spanning tree
spanningTree = nx . minimum_spanning_tree ( dualGraph )
# Unfold the tree
fullUnfolding = self . unfoldSpanningTree ( mesh , spanningTree )
2021-05-10 13:54:23 +02:00
[ unfoldedMesh , isFoldingEdge , connections , glueNumber , dihedralAngles ] = fullUnfolding
2021-05-10 04:34:04 +02:00
# 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 self . 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 ( self . unfoldSpanningTree ( mesh , spanningTree . subgraph ( component ) ) )
return fullUnfolding , unfoldings
def findBoundingBox ( self , 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 ]
def writeSVG ( self , unfolding , size , randomColorSet ) :
mesh = unfolding [ 0 ]
isFoldingEdge = unfolding [ 1 ]
glueNumber = unfolding [ 3 ]
2021-05-10 13:54:23 +02:00
dihedralAngles = unfolding [ 4 ]
2021-05-10 04:34:04 +02:00
#statistic values
gluePairs = 0
2021-05-10 13:54:23 +02:00
cuts = 0
2021-05-10 04:34:04 +02:00
coplanarEdges = 0
2021-05-12 01:22:17 +02:00
mountainFolds = 0
valleyFolds = 0
2021-05-10 04:34:04 +02:00
# Calculate the bounding box
[ xmin , ymin , boxSize ] = self . findBoundingBox ( unfolding [ 0 ] )
if size > 0 :
boxSize = size
strokewidth = boxSize * self . options . fontSize / 8000
dashLength = boxSize * self . options . fontSize / 2000
spaceLength = boxSize * self . options . fontSize / 800
textDistance = boxSize * self . options . fontSize / 800
2021-05-10 13:54:23 +02:00
textStrokeWidth = boxSize * self . options . fontSize / 3000
2021-05-10 04:34:04 +02:00
fontsize = boxSize * self . options . fontSize / 1000
# 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 )
#we could write the unfolded mesh as a 2D stl file to disk if we like:
if self . options . writeTwoDSTL is True :
if not os . path . exists ( self . options . TwoDSTLdir ) :
inkex . utils . debug ( " Export location for 2D STL unfoldings does not exist. Please select a another dir and try again. " )
exit ( 1 )
else :
om . write_mesh ( os . path . join ( self . options . TwoDSTLdir , uniqueMainId + " -paperfold-page.stl " ) , mesh )
2021-05-10 13:54:23 +02:00
#########################################################
# Nmbering triangle faces with circle around
#########################################################
2021-05-10 04:34:04 +02:00
if self . options . printTriangleNumbers is True :
for face in mesh . faces ( ) :
2021-05-10 13:54:23 +02:00
centroid = mesh . calc_face_centroid ( face )
2021-05-10 04:34:04 +02:00
textFaceGroup = inkex . Group ( id = uniqueMainId + " -textFace- " + str ( face . idx ( ) ) )
2021-05-11 12:35:26 +02:00
circle = textFaceGroup . add ( Circle ( cx = " {:0.6f} " . format ( centroid [ 0 ] ) , cy = " {:0.6f} " . format ( centroid [ 1 ] ) , r = " {:0.6f} " . format ( fontsize ) ) )
2021-05-10 04:34:04 +02:00
circle . set ( ' id ' , uniqueMainId + " -textFaceCricle- " + str ( face . idx ( ) ) )
2021-05-11 12:35:26 +02:00
circle . set ( " style " , " stroke:#000000;stroke-width: {:0.6f} " . format ( strokewidth / 2 ) + " ;fill:none " )
2021-05-10 04:34:04 +02:00
text = textFaceGroup . add ( TextElement ( id = uniqueMainId + " -textFaceNumber- " + str ( face . idx ( ) ) ) )
2021-05-11 12:35:26 +02:00
text . set ( " x " , " {:0.6f} " . format ( centroid [ 0 ] ) )
text . set ( " y " , " {:0.6f} " . format ( centroid [ 1 ] + fontsize / 3 ) )
text . set ( " font-size " , " {:0.6f} " . format ( fontsize ) )
2021-05-10 13:54:23 +02:00
text . set ( " style " , " stroke-width {:0.6f} " . format ( textStrokeWidth ) + " ;text-anchor:middle;text-align:center " )
2021-05-10 04:34:04 +02:00
tspan = text . add ( Tspan ( id = uniqueMainId + " -textFaceNumberTspan- " + str ( face . idx ( ) ) ) )
2021-05-11 12:35:26 +02:00
tspan . set ( " x " , " {:0.6f} " . format ( centroid [ 0 ] ) )
tspan . set ( " y " , " {:0.6f} " . format ( centroid [ 1 ] + fontsize / 3 ) )
2021-05-10 13:54:23 +02:00
tspan . set ( " style " , " stroke-width {:0.6f} " . format ( textStrokeWidth ) + " ;text-anchor:middle;text-align:center " )
2021-05-10 04:34:04 +02:00
tspan . text = str ( face . idx ( ) )
textFacesGroup . append ( textFaceGroup )
2021-05-10 13:54:23 +02:00
#########################################################
# Nmbering triangle edges and style them according to their type
#########################################################
2021-05-10 04:34:04 +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
line = edgesGroup . add ( inkex . PathElement ( ) )
2021-05-11 12:35:26 +02:00
line . set ( ' d ' , " M {:0.6f} , {:0.6f} {:0.6f} , {:0.6f} " . format ( vertex0 [ 0 ] , vertex0 [ 1 ] , vertex1 [ 0 ] , vertex1 [ 1 ] ) )
2021-05-10 04:34:04 +02:00
# Colour depending on folding direction
lineStyle = { " fill " : " none " }
2021-05-10 13:54:23 +02:00
lineStyle . update ( { " stroke " : self . options . colorCutEdges } )
line . set ( " id " , uniqueMainId + " -cut-edge- " + str ( edge . idx ( ) ) )
2021-05-10 04:34:04 +02:00
2021-05-10 13:54:23 +02:00
lineStyle . update ( { " stroke-width " : " {:0.6f} " . format ( strokewidth ) } )
2021-05-10 04:34:04 +02:00
lineStyle . update ( { " stroke-linecap " : " butt " } )
lineStyle . update ( { " stroke-linejoin " : " miter " } )
lineStyle . update ( { " stroke-miterlimit " : " 4 " } )
2021-05-10 13:54:23 +02:00
dihedralAngle = dihedralAngles [ edge . idx ( ) ]
2021-05-10 04:34:04 +02:00
# Dotted lines for folding edges
if isFoldingEdge [ edge . idx ( ) ] :
if self . options . dashes is True :
2021-05-11 12:35:26 +02:00
lineStyle . update ( { " stroke-dasharray " : " {:0.6f} , {:0.6f} " . format ( dashLength , spaceLength ) } )
2021-05-10 04:34:04 +02:00
if dihedralAngle > 0 :
2021-05-10 13:54:23 +02:00
lineStyle . update ( { " stroke " : self . options . colorMountainFolds } )
line . set ( " id " , uniqueMainId + " -mountain-fold- " + str ( edge . idx ( ) ) )
2021-05-12 01:22:17 +02:00
mountainFolds + = 1
2021-05-10 04:34:04 +02:00
if dihedralAngle < 0 :
2021-05-10 13:54:23 +02:00
lineStyle . update ( { " stroke " : self . options . colorValleyFolds } )
line . set ( " id " , uniqueMainId + " -valley-fold- " + str ( edge . idx ( ) ) )
2021-05-12 01:22:17 +02:00
valleyFolds + = 1
2021-05-10 04:34:04 +02:00
if dihedralAngle == 0 :
lineStyle . update ( { " stroke " : self . options . colorCoplanarEdges } )
line . set ( " id " , uniqueMainId + " -coplanar-edge- " + str ( edge . idx ( ) ) )
if self . options . importCoplanarEdges is False :
line . delete ( )
2021-05-10 13:54:23 +02:00
coplanarEdges + = 1
2021-05-10 04:34:04 +02:00
else :
lineStyle . update ( { " stroke-dasharray " : " none " } )
# 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
2021-05-10 13:54:23 +02:00
lineStyle . update ( { " stroke-dashoffset " : " 0.0 " } )
lineStyle . update ( { " stroke-opacity " : " 1.0 " } )
2021-05-10 04:34:04 +02:00
2021-05-10 13:54:23 +02:00
if self . options . edgeStyle == " saturationsForAngles " :
2021-05-10 04:34:04 +02:00
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 ( )
2021-05-10 13:54:23 +02:00
newSaturation = abs ( dihedralAngle / self . angleRange ) * 100 #percentage values
2021-05-10 04:34:04 +02:00
hslColor . saturation = newSaturation
2021-05-10 13:54:23 +02:00
lineStyle . update ( { " stroke " : hslColor . to_rgb ( ) } )
elif self . options . edgeStyle == " opacitiesForAngles " :
if dihedralAngle != 0 : #we dont want to apply opacity adjustments for zero angle lines because they would be invisible then
opacity = abs ( dihedralAngle / 180 )
lineStyle . update ( { " stroke-opacity " : " {:0.6f} " . format ( opacity ) } )
2021-05-10 04:34:04 +02:00
line . style = lineStyle
2021-05-10 13:54:23 +02:00
#########################################################
2021-05-10 04:34:04 +02:00
# Textual things
2021-05-10 13:54:23 +02:00
#########################################################
2021-05-10 04:34:04 +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 )
vector = vector / np . linalg . norm ( vector ) # normalize
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
text = textEdgesGroup . add ( TextElement ( id = uniqueMainId + " -edgeNumber- " + str ( edge . idx ( ) ) ) )
2021-05-11 12:35:26 +02:00
text . set ( " x " , " {:0.6f} " . format ( position [ 0 ] ) )
text . set ( " y " , " {:0.6f} " . format ( position [ 1 ] ) )
text . set ( " font-size " , " {:0.6f} " . format ( fontsize ) )
2021-05-10 13:54:23 +02:00
text . set ( " style " , " stroke-width {:0.6f} " . format ( textStrokeWidth ) + " ;text-anchor:middle;text-align:center " )
2021-05-11 12:35:26 +02:00
text . set ( " transform " , " rotate( {:0.6f} {:0.6f} {:0.6f} ) " . format ( rotation , position [ 0 ] , position [ 1 ] ) )
2021-05-08 16:08:07 +02:00
2021-05-10 04:34:04 +02:00
tspan = text . add ( Tspan ( ) )
2021-05-11 12:35:26 +02:00
tspan . set ( " x " , " {:0.6f} " . format ( position [ 0 ] ) )
tspan . set ( " y " , " {:0.6f} " . format ( position [ 1 ] ) )
2021-05-10 13:54:23 +02:00
tspan . set ( " style " , " stroke-width {:0.6f} " . format ( textStrokeWidth ) + " ;text-anchor:middle;text-align:center " )
2021-05-10 04:34:04 +02:00
tspanText = [ ]
if self . options . printGluePairNumbers is True and not isFoldingEdge [ edge . idx ( ) ] :
tspanText . append ( str ( glueNumber [ edge . idx ( ) ] ) )
2021-05-11 20:47:59 +02:00
if self . options . printAngles is True and dihedralAngle != 0.0 :
2021-05-10 04:34:04 +02:00
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 tspan . text == " " : #if no text we remove again to clean up
text . delete ( )
tspan . delete ( )
if len ( textFacesGroup ) == 0 :
textFacesGroup . delete ( ) #delete if empty set
2020-09-13 03:20:36 +02:00
2021-05-11 12:35:26 +02:00
if len ( textEdgesGroup ) == 0 :
2021-05-10 04:34:04 +02:00
textEdgesGroup . delete ( ) #delete if empty set
2021-05-08 15:07:57 +02:00
2021-05-10 04:34:04 +02:00
if len ( textGroup ) == 0 :
textGroup . delete ( ) #delete if empty set
if self . options . printStats is True :
2021-05-10 13:54:23 +02:00
inkex . utils . debug ( " * Number of cuts: " + str ( cuts ) )
2021-05-10 04:34:04 +02:00
inkex . utils . debug ( " * Number of coplanar edges: " + str ( coplanarEdges ) )
2021-05-12 01:22:17 +02:00
inkex . utils . debug ( " * Number of mountain folds: " + str ( mountainFolds ) )
inkex . utils . debug ( " * Number of valley folds: " + str ( valleyFolds ) )
2021-05-10 04:34:04 +02:00
inkex . utils . debug ( " * Number of glue pairs: {:0.0f} " . format ( gluePairs / 2 ) )
2021-05-10 13:54:23 +02:00
inkex . utils . debug ( " * min angle: {:0.2f} " . format ( self . minAngle ) )
inkex . utils . debug ( " * max angle: {:0.2f} " . format ( self . maxAngle ) )
inkex . utils . debug ( " * Edge angle range: {:0.2f} " . format ( self . angleRange ) )
2021-05-10 04:34:04 +02:00
return paperfoldPageGroup
2020-09-13 03:20:36 +02:00
2021-04-15 17:03:47 +02:00
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-10 04:34:04 +02:00
pars . add_argument ( " --roundingDigits " , type = int , default = 3 , help = " Digits for rounding " )
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 " )
2021-05-12 01:22:17 +02:00
pars . add_argument ( " --experimentalWeights " , type = inkex . Boolean , default = False , help = " Mess around with algorithm " )
2021-05-10 04:34:04 +02:00
pars . add_argument ( " --printStats " , type = inkex . Boolean , default = False , 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-10 04:34:04 +02:00
pars . add_argument ( " --extraborderUnits " )
pars . add_argument ( " --writeTwoDSTL " , type = inkex . Boolean , default = False , help = " Write 2D STL unfoldings " )
pars . add_argument ( " --TwoDSTLdir " , default = " ./inkscape_export/ " , help = " Location to save exported 2D STL " )
2021-05-08 15:07:57 +02:00
#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 " )
2021-05-10 13:54:23 +02:00
pars . add_argument ( " --edgeStyle " , help = " Adjust color saturation or opacity 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 " )
2021-05-10 13:54:23 +02:00
pars . add_argument ( " --colorCutEdges " , type = Color , default = ' 255 ' , help = " Cut edges " )
2021-05-08 15:07:57 +02:00
pars . add_argument ( " --colorCoplanarEdges " , type = Color , default = ' 1943148287 ' , help = " Coplanar edges " )
2021-05-10 13:54:23 +02:00
pars . add_argument ( " --colorValleyFolds " , type = Color , default = ' 3422552319 ' , help = " Valley fold edges " )
pars . add_argument ( " --colorMountainFolds " , type = Color , default = ' 879076607 ' , help = " Mountain fold edges " )
2021-05-10 04:34:04 +02:00
#Post Processing
pars . add_argument ( " --joineryMode " , type = inkex . Boolean , default = False , help = " Enable joinery mode " )
pars . add_argument ( " --origamiSimulatorMode " , type = inkex . Boolean , default = False , help = " Enable origami simulator mode " )
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-12 01:22:17 +02:00
#mesh = om.read_polymesh(self.options.inputfile) #we must work with triangles instead of polygons because the algorithm works with that ONLY
2021-05-06 22:44:14 +02:00
2021-05-10 04:34:04 +02:00
fullUnfolded , unfoldedComponents = self . unfold ( mesh )
2021-05-11 23:46:05 +02:00
unfoldComponentCount = len ( unfoldedComponents )
2021-05-10 04:34:04 +02:00
#if len(unfoldedComponents) == 0:
# inkex.utils.debug("Error: no components were unfolded.")
# exit(1)
if self . options . printStats is True :
2021-05-11 23:46:05 +02:00
inkex . utils . debug ( " Unfolding components: {:0.0f} " . format ( unfoldComponentCount ) )
2021-05-10 04:34:04 +02:00
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 :
2021-05-10 04:34:04 +02:00
[ xmin , ymin , boxSize ] = self . findBoundingBox ( unfolding [ 0 ] )
2020-09-13 03:20:36 +02:00
if boxSize > maxSize :
maxSize = boxSize
2021-05-11 23:46:05 +02:00
xSpacing = maxSize / unfoldComponentCount * 0.1 # 10% spacing between each component; calculated by max box size
2020-09-13 03:20:36 +02:00
2021-05-10 13:54:23 +02:00
#########################################################
# mode config for joinery:
#########################################################
2021-05-10 04:34:04 +02:00
if self . options . joineryMode is True :
self . options . separateGluePairsByColor = True #we need random colors in this mode
2021-05-10 13:54:23 +02:00
#########################################################
# mode config for origami simulator:
#########################################################
'''
required style for Origami Simulator :
colors :
- #ff0000 (red) - mountain folds
- #0000ff (blue) - valley folds
- #000000 (black) - boundary cuts (for both the outline of the pattern and any internal holes)
- #ffff00 (yellow) - coplonar triangle edges ("facet creases") (no support for polygons > 3 edges)
- #00ff00 (green) - thin slits
- #ff00ff (magenta) - undriven creases (swing freely)
opacity :
- final fold angle of a mountain or valley fold is set by its opacity . Any fold angle between 0 ° and 180 ° may be used . For example :
- 1.0 = 180 ° ( fully folded )
- 0.5 = 90 °
- 0 = 0 ° ( flat )
'''
2021-05-10 04:34:04 +02:00
if self . options . origamiSimulatorMode is True :
self . options . joineryMode = True #we set to true even if false because we need the same flat structure for origami simulator
self . options . separateGluePairsByColor = False #we need to have no weird random colors in this mode
2021-05-10 13:54:23 +02:00
self . options . edgeStyle = " opacitiesForAngles " #highly important for simulation
2021-05-11 23:46:05 +02:00
self . options . dashes = False
self . options . printGluePairNumbers = False
self . options . printAngles = False
self . options . printLengths = False
2021-05-10 13:54:23 +02:00
self . options . importCoplanarEdges = True
self . options . colorCutEdges = " #000000 " #black
self . options . colorCoplanarEdges = " #ffff00 " #yellow
self . options . colorMountainFolds = " #ff0000 " #red
self . options . colorValleyFolds = " #0000ff " #blue
2021-05-11 12:35:26 +02:00
#generate random colors; used to identify glue tab pairs
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-10 04:34:04 +02:00
if self . options . printStats is True :
inkex . utils . debug ( " ----------------------------------------------------------- " )
inkex . utils . debug ( " Unfolding component nr.: {:0.0f} " . format ( i ) )
paperfoldPageGroup = self . writeSVG ( 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 ( )
2021-05-11 23:46:05 +02:00
paperfoldPageGroup . set ( " transform " , " translate( {:0.6f} , 0.0) " . format ( previous_bbox . left + previous_bbox . width - this_bbox . left + xSpacing ) )
2020-09-13 03:20:36 +02:00
paperfoldMainGroup . append ( paperfoldPageGroup )
#apply scale factor
2021-05-11 23:46:05 +02:00
translation_matrix = [ [ self . options . scalefactor , 0.0 , 0.0 ] , [ 0.0 , self . options . scalefactor , 0.0 ] ]
2020-09-13 03:20:36 +02:00
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 ' ) )
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 ) )
2021-05-11 12:35:26 +02:00
root . set ( ' width ' , " {:0.6f} {} " . format ( bbox . width + 2 * offset , self . svg . unit ) )
root . set ( ' height ' , " {:0.6f} {} " . format ( bbox . height + 2 * offset , self . svg . unit ) )
2021-05-10 04:34:04 +02:00
#if set, we move all edges (path elements) to the top level
if self . options . joineryMode is True :
for paperfoldPage in paperfoldMainGroup . getchildren ( ) :
for child in paperfoldPage :
if " -edges " in child . get ( ' id ' ) :
for edge in child :
edgeTransform = edge . composed_transform ( )
self . document . getroot ( ) . append ( edge )
edge . transform = edgeTransform
2020-09-13 03:20:36 +02:00
if __name__ == ' __main__ ' :
2021-06-02 23:30:37 +02:00
Paperfold ( ) . run ( )