huge update for paperfold

This commit is contained in:
leyghisbb 2021-05-10 04:34:04 +02:00
parent 8c8614e4f8
commit 9d6d01f845
3 changed files with 680 additions and 600 deletions

View File

@ -75,7 +75,7 @@ class ConvexHull(inkex.EffectExtension):
line_attribs['transform'] = cloneTransform line_attribs['transform'] = cloneTransform
etree.SubElement(g, inkex.addNS('path', 'svg' ), line_attribs) etree.SubElement(g, inkex.addNS('path', 'svg' ), line_attribs)
def getControlPoints(self, element, n_array = None): #this does the same as "CTRL + SHIFT + K" def getControlPoints(self, element, n_array = None):
if n_array == None: if n_array == None:
n_array = [] n_array = []
if element.tag == inkex.addNS('path','svg'): if element.tag == inkex.addNS('path','svg'):

View File

@ -8,6 +8,7 @@
<param name="inputfile" type="path" gui-text="Input File" filetypes="obj,off,ply,stl" mode="file" gui-description="The model to unfold. You can use obj files provided in extensions dir of Inkscape \Poly3DObjects\*.obj to play around">/your/beautiful/3dmodel/file</param> <param name="inputfile" type="path" gui-text="Input File" filetypes="obj,off,ply,stl" mode="file" gui-description="The model to unfold. You can use obj files provided in extensions dir of Inkscape \Poly3DObjects\*.obj to play around">/your/beautiful/3dmodel/file</param>
<param name="maxNumFaces" type="int" min="1" max="99999" gui-text="Maximum allowed faces" gui-description="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.">200</param> <param name="maxNumFaces" type="int" min="1" max="99999" gui-text="Maximum allowed faces" gui-description="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.">200</param>
<param name="scalefactor" type="float" precision="3" min="0.0001" max="10000.0" gui-text="Manual scale factor" gui-description="default is 1.0">1.0</param> <param name="scalefactor" type="float" precision="3" min="0.0001" max="10000.0" gui-text="Manual scale factor" gui-description="default is 1.0">1.0</param>
<param name="roundingDigits" type="int" min="0" max="16" gui-text="Digits for rounding" gui-description="Controls how (nearly) coplanar lines are handled.">3</param>
<separator/> <separator/>
<hbox> <hbox>
<vbox> <vbox>
@ -27,6 +28,8 @@
<option value="pt">pt</option> <option value="pt">pt</option>
<option value="px">px</option> <option value="px">px</option>
</param> </param>
<param name="writeTwoDSTL" type="bool" gui-text="Write 2D STL unfoldings">false</param>
<param name="TwoDSTLdir" type="path" mode="folder" gui-text="Location to save exported 2D STL">./inkscape_export/</param>
</vbox> </vbox>
<separator/> <separator/>
<vbox> <vbox>
@ -35,7 +38,7 @@
<param name="flipLabels" type="bool" gui-text="Flip labels">false</param> <param name="flipLabels" type="bool" gui-text="Flip labels">false</param>
<param name="dashes" type="bool" gui-text="Dashes for cut/coplanar lines">true</param> <param name="dashes" type="bool" gui-text="Dashes for cut/coplanar lines">true</param>
<param name="saturationsForAngles" type="bool" gui-text="Adjust color saturation for folding edges" gui-description="The larger the angle the darker the color">false</param> <param name="saturationsForAngles" type="bool" gui-text="Adjust color saturation for folding edges" gui-description="The larger the angle the darker the color">false</param>
<param name="separateGluePairsByColor" type="bool" gui-text="Separate glue pairs by color">false</param> <param name="separateGluePairsByColor" type="bool" gui-text="Separate glue tab pairs by color" gui-description="Generates random color sets for glue tab pairs">false</param>
<param name="colorValleyCut" type="color" appearance="colorbutton" gui-text="Valley cut edges">255</param> <param name="colorValleyCut" type="color" appearance="colorbutton" gui-text="Valley cut edges">255</param>
<param name="colorMountainCut" type="color" appearance="colorbutton" gui-text="Mountain cut edges">1968208895</param> <param name="colorMountainCut" type="color" appearance="colorbutton" gui-text="Mountain cut edges">1968208895</param>
<param name="colorCoplanarEdges" type="color" appearance="colorbutton" gui-text="Coplanar edges">1943148287</param> <param name="colorCoplanarEdges" type="color" appearance="colorbutton" gui-text="Coplanar edges">1943148287</param>
@ -43,9 +46,17 @@
<param name="colorMountainPerforates" type="color" appearance="colorbutton" gui-text="Mountain perforation edges">879076607</param> <param name="colorMountainPerforates" type="color" appearance="colorbutton" gui-text="Mountain perforation edges">879076607</param>
</vbox> </vbox>
</hbox> </hbox>
<separator/> </page>
<label appearance="header">Post Processing</label> <page name="tab_postprocessing" gui-text="Post Processing">
<label appearance="header">Joinery</label>
<label>Joinery only works on ungrouped paths.</label>
<param name="joineryMode" type="bool" gui-text="Enable joinery mode" gui-description="Makes flat file instead creating groups. Guarantees compability for joinery.">false</param>
<label appearance="url">https://clementzheng.github.io/joinery</label> <label appearance="url">https://clementzheng.github.io/joinery</label>
<label appearance="url">https://www.instructables.com/Joinery-Joints-for-Laser-Cut-Assemblies</label>
<label appearance="header">Origami Simulator</label>
<label appearance="url">https://origamisimulator.org</label>
<label appearance="url">https://github.com/amandaghassaei/OrigamiSimulator</label>
<param name="origamiSimulatorMode" type="bool" gui-text="Enable origami simulator mode" gui-description="Overwrites styles to be compatible to origami simulator.">false</param>
</page> </page>
<page name="tab_about" gui-text="About"> <page name="tab_about" gui-text="About">
<label appearance="header">Paperfold for Inkscape</label> <label appearance="header">Paperfold for Inkscape</label>

View File

@ -19,7 +19,7 @@ Paperfold is another flattener for triangle mesh files, heavily based on paperfo
Author: Mario Voigt / FabLab Chemnitz Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org Mail: mario.voigt@stadtfabrikanten.org
Date: 13.09.2020 Date: 13.09.2020
Last patch: 08.05.2021 Last patch: 10.05.2021
License: GNU GPL v3 License: GNU GPL v3
To run this you need to install OpenMesh with python pip. To run this you need to install OpenMesh with python pip.
@ -38,17 +38,18 @@ possible import file types -> https://www.graphics.rwth-aachen.de/media/openmesh
todo: todo:
- debug coplanar color for edges for some cases - debug coplanar color for edges for some cases
- remove empty groups (text)
- abort if 0 faces
- give hints for joinery preparations (apply transform, ungroup, ...)
- update documentation accordingly
- make angleRange global for complete unfolding (to match glue pairs between multiple unfoldings) - make angleRange global for complete unfolding (to match glue pairs between multiple unfoldings)
- add angleRange to stats - 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
- origami simulator docu + add support for opacities
- fix line: dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight) # #might fail without throwing any error ...
""" """
class Unfold(inkex.EffectExtension):
# Compute the third point of a triangle when two points and all edge lengths are given # Compute the third point of a triangle when two points and all edge lengths are given
def getThirdPoint(v0, v1, l01, l12, l20): def getThirdPoint(self, v0, v1, l01, l12, l20):
v2rotx = (l01 ** 2 + l20 ** 2 - l12 ** 2) / (2 * l01) 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) v2roty0 = np.sqrt((l01 + l20 + l12) * (l01 + l20 - l12) * (l01 - l20 + l12) * (-l01 + l20 + l12)) / (2 * l01)
@ -64,7 +65,7 @@ def getThirdPoint(v0, v1, l01, l12, l20):
# Check if two lines intersect # Check if two lines intersect
def lineIntersection(v1, v2, v3, v4, epsilon): 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]) 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]) 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]) v = (v2[0] - v1[0]) * (v1[1] - v3[1]) - (v2[1] - v1[1]) * (v1[0] - v3[0])
@ -73,7 +74,7 @@ def lineIntersection(v1, v2, v3, v4, epsilon):
return ((0 + epsilon) <= u <= (d - epsilon)) and ((0 + epsilon) <= v <= (d - epsilon)) return ((0 + epsilon) <= u <= (d - epsilon)) and ((0 + epsilon) <= v <= (d - epsilon))
# Check if a point lies inside a triangle # Check if a point lies inside a triangle
def pointInTriangle(A, B, C, P, epsilon): def pointInTriangle(self, A, B, C, P, epsilon):
v0 = [C[0] - A[0], C[1] - A[1]] v0 = [C[0] - A[0], C[1] - A[1]]
v1 = [B[0] - A[0], B[1] - A[1]] v1 = [B[0] - A[0], B[1] - A[1]]
v2 = [P[0] - A[0], P[1] - A[1]] v2 = [P[0] - A[0], P[1] - A[1]]
@ -87,38 +88,39 @@ def pointInTriangle(A, B, C, P, epsilon):
# Check if two triangles intersect # Check if two triangles intersect
def triangleIntersection(t1, t2, epsilon): def triangleIntersection(self, t1, t2, epsilon):
if lineIntersection(t1[0], t1[1], t2[0], t2[1], epsilon): return True if self.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 self.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 self.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 self.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 self.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 self.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 self.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 self.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 if self.lineIntersection(t1[1], t1[2], t2[1], t2[2], epsilon): return True
inTri = True inTri = True
inTri = inTri and pointInTriangle(t1[0], t1[1], t1[2], t2[0], epsilon) inTri = inTri and self.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 self.pointInTriangle(t1[0], t1[1], t1[2], t2[1], epsilon)
inTri = inTri and pointInTriangle(t1[0], t1[1], t1[2], t2[2], epsilon) inTri = inTri and self.pointInTriangle(t1[0], t1[1], t1[2], t2[2], epsilon)
if inTri == True: return True if inTri == True: return True
inTri = True inTri = True
inTri = inTri and pointInTriangle(t2[0], t2[1], t2[2], t1[0], epsilon) inTri = inTri and self.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 self.pointInTriangle(t2[0], t2[1], t2[2], t1[1], epsilon)
inTri = inTri and pointInTriangle(t2[0], t2[1], t2[2], t1[2], epsilon) inTri = inTri and self.pointInTriangle(t2[0], t2[1], t2[2], t1[2], epsilon)
if inTri == True: return True if inTri == True: return True
return False return False
# Functions for visualisation and output # Functions for visualisation and output
def addVisualisationData(mesh, unfoldedMesh, originalHalfedges, unfoldedHalfedges, glueNumber, foldingDirection): def addVisualisationData(self, mesh, unfoldedMesh, originalHalfedges, unfoldedHalfedges, glueNumber, foldingDirection):
for i in range(3): for i in range(3):
foldingDirection[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = round(math.degrees(mesh.calc_dihedral_angle(originalHalfedges[i])), 3) foldingDirection[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = round(math.degrees(mesh.calc_dihedral_angle(originalHalfedges[i])), self.options.roundingDigits)
# Information, which edges belong together # Information, which edges belong together
glueNumber[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = mesh.edge_handle(originalHalfedges[i]).idx() glueNumber[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = mesh.edge_handle(originalHalfedges[i]).idx()
# Function that unwinds a spanning tree # Function that unwinds a spanning tree
def unfoldSpanningTree(mesh, spanningTree): def unfoldSpanningTree(self, mesh, spanningTree):
try:
unfoldedMesh = om.TriMesh() # the unfolded mesh unfoldedMesh = om.TriMesh() # the unfolded mesh
numFaces = mesh.n_faces() numFaces = mesh.n_faces()
@ -152,7 +154,7 @@ def unfoldSpanningTree(mesh, spanningTree):
secondUnfoldedPoint = np.array([edgelengths[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 # We calculate the third point of the triangle from the first two. There are two possibilities
[thirdUnfolded0, thirdUnfolded1] = getThirdPoint(firstUnfoldedPoint, secondUnfoldedPoint, edgelengths[0], [thirdUnfolded0, thirdUnfolded1] = self.getThirdPoint(firstUnfoldedPoint, secondUnfoldedPoint, edgelengths[0],
edgelengths[1], edgelengths[1],
edgelengths[2]) edgelengths[2])
if thirdUnfolded0[1] > 0: if thirdUnfolded0[1] > 0:
@ -183,7 +185,7 @@ def unfoldSpanningTree(mesh, spanningTree):
# Associated triangle in 3D mesh # Associated triangle in 3D mesh
connections[unfoldedFace.idx()] = startingTriangle.idx() connections[unfoldedFace.idx()] = startingTriangle.idx()
# Folding direction and adhesive number # Folding direction and adhesive number
addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, foldingDirection) self.addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, foldingDirection)
halfEdgeConnections = {firstHalfEdge.idx(): firstUnfoldedHalfEdge.idx(), halfEdgeConnections = {firstHalfEdge.idx(): firstUnfoldedHalfEdge.idx(),
secondHalfEdge.idx(): secondUnfoldedHalfEdge.idx(), secondHalfEdge.idx(): secondUnfoldedHalfEdge.idx(),
@ -221,7 +223,7 @@ def unfoldSpanningTree(mesh, spanningTree):
mesh.calc_edge_length(thirdHalfEdgeInFace)] mesh.calc_edge_length(thirdHalfEdgeInFace)]
# We calculate the two possibilities for the third point in the triangle # We calculate the two possibilities for the third point in the triangle
[newUnfoldedVertex0, newUnfoldedVertex1] = getThirdPoint(unfoldedMesh.point(unfoldedFromVertex), [newUnfoldedVertex0, newUnfoldedVertex1] = self.getThirdPoint(unfoldedMesh.point(unfoldedFromVertex),
unfoldedMesh.point(unfoldedToVertex), edgelengths[0], unfoldedMesh.point(unfoldedToVertex), edgelengths[0],
edgelengths[1], edgelengths[2]) edgelengths[1], edgelengths[2])
@ -236,12 +238,12 @@ def unfoldSpanningTree(mesh, spanningTree):
unfoldedHalfEdges = [unfoldedLastHalfEdge, secondUnfoldedHalfEdge, thirdUnfoldedHalfEdge] unfoldedHalfEdges = [unfoldedLastHalfEdge, secondUnfoldedHalfEdge, thirdUnfoldedHalfEdge]
# Saving the information about edges and page # Saving the information about edges and page
# Dotted line in the output # Dotted one's in the output
unfoldedLastEdge = unfoldedMesh.edge_handle(unfoldedLastHalfEdge) unfoldedLastEdge = unfoldedMesh.edge_handle(unfoldedLastHalfEdge)
isFoldingEdge[unfoldedLastEdge.idx()] = True isFoldingEdge[unfoldedLastEdge.idx()] = True
# Gluing number and folding direction # Gluing number and folding direction
addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, foldingDirection) self.addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, foldingDirection)
# Related page # Related page
connections[newface.idx()] = dualEdge[1] connections[newface.idx()] = dualEdge[1]
@ -251,25 +253,34 @@ def unfoldSpanningTree(mesh, spanningTree):
halfEdgeConnections[originalHalfEdges[i].idx()] = unfoldedHalfEdges[i].idx() halfEdgeConnections[originalHalfEdges[i].idx()] = unfoldedHalfEdges[i].idx()
return [unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection] return [unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection]
except Exception as e:
inkex.utils.debug(str(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")
exit(1)
def unfold(mesh, maxNumFaces, printStats):
def unfold(self, mesh):
# Calculate the number of surfaces, edges and corners, as well as the length of the longest shortest edge # Calculate the number of surfaces, edges and corners, as well as the length of the longest shortest edge
numEdges = mesh.n_edges() numEdges = mesh.n_edges()
numVertices = mesh.n_vertices() numVertices = mesh.n_vertices()
numFaces = mesh.n_faces() numFaces = mesh.n_faces()
if numFaces > maxNumFaces: if numFaces > self.options.maxNumFaces:
inkex.utils.debug("Aborted. Target STL file has " + str(numFaces) + " faces, but " + str(maxNumFaces) + " are allowed.") inkex.utils.debug("Aborted. Target STL file has " + str(numFaces) + " faces, but only " + str(maxNumFaces) + " are allowed.")
exit(1) exit(1)
if printStats is True: if self.options.printStats is True:
inkex.utils.debug("Input STL mesh stats:") inkex.utils.debug("Input STL mesh stats:")
inkex.utils.debug("* Number of edges: " + str(numEdges)) inkex.utils.debug("* Number of edges: " + str(numEdges))
inkex.utils.debug("* Number of vertices: " + str(numVertices)) inkex.utils.debug("* Number of vertices: " + str(numVertices))
inkex.utils.debug("* Number of faces: " + str(numFaces)) inkex.utils.debug("* Number of faces: " + str(numFaces))
inkex.utils.debug("-----------------------------------------------------------") inkex.utils.debug("-----------------------------------------------------------")
# Generate the dual graph of the mesh and calculate the weights # Generate the dual graph of the mesh and calculate the weights
dualGraph = nx.Graph() dualGraph = nx.Graph()
@ -285,12 +296,15 @@ def unfold(mesh, maxNumFaces, printStats):
# All edges in the net # All edges in the net
for edge in mesh.edges(): for edge in mesh.edges():
#inkex.utils.debug("edge.idx = " + str(edge.idx()))
# The two sides adjacent to the edge # The two sides adjacent to the edge
face1 = mesh.face_handle(mesh.halfedge_handle(edge, 0)) face1 = mesh.face_handle(mesh.halfedge_handle(edge, 0))
face2 = mesh.face_handle(mesh.halfedge_handle(edge, 1)) face2 = mesh.face_handle(mesh.halfedge_handle(edge, 1))
# The weight # The weight
edgeweight = 1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength) edgeweight = 1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength)
#inkex.utils.debug("edgeweight = " + str(edgeweight))
# Calculate the centres of the pages (only necessary for visualisation) # Calculate the centres of the pages (only necessary for visualisation)
center1 = (0, 0) center1 = (0, 0)
@ -303,13 +317,14 @@ def unfold(mesh, maxNumFaces, printStats):
# Add the new nodes and edge to the dual graph # Add the new nodes and edge to the dual graph
dualGraph.add_node(face1.idx(), pos=center1) dualGraph.add_node(face1.idx(), pos=center1)
dualGraph.add_node(face2.idx(), pos=center2) dualGraph.add_node(face2.idx(), pos=center2)
dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight) dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight) # #might fail without throwing any error ...
# Calculate the minimum spanning tree # Calculate the minimum spanning tree
spanningTree = nx.minimum_spanning_tree(dualGraph) spanningTree = nx.minimum_spanning_tree(dualGraph)
# Unfold the tree # Unfold the tree
fullUnfolding = unfoldSpanningTree(mesh, spanningTree) fullUnfolding = self.unfoldSpanningTree(mesh, spanningTree)
[unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection] = fullUnfolding [unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection] = fullUnfolding
@ -327,7 +342,7 @@ def unfold(mesh, maxNumFaces, printStats):
triangle1.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge))) triangle1.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge)))
for halfedge in unfoldedMesh.fh(face2): for halfedge in unfoldedMesh.fh(face2):
triangle2.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge))) triangle2.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge)))
if triangleIntersection(triangle1, triangle2, epsilon): if self.triangleIntersection(triangle1, triangle2, epsilon):
faceIntersections.append([connections[face1.idx()], connections[face2.idx()]]) faceIntersections.append([connections[face1.idx()], connections[face2.idx()]])
# Find the paths # Find the paths
@ -397,12 +412,13 @@ def unfold(mesh, maxNumFaces, printStats):
# Unfolding of the components # Unfolding of the components
unfoldings = [] unfoldings = []
for component in connectedComponentList: for component in connectedComponentList:
unfoldings.append(unfoldSpanningTree(mesh, spanningTree.subgraph(component))) unfoldings.append(self.unfoldSpanningTree(mesh, spanningTree.subgraph(component)))
return fullUnfolding, unfoldings return fullUnfolding, unfoldings
def findBoundingBox(mesh): def findBoundingBox(self, mesh):
firstpoint = mesh.point(mesh.vertex_handle(0)) firstpoint = mesh.point(mesh.vertex_handle(0))
xmin = firstpoint[0] xmin = firstpoint[0]
xmax = firstpoint[0] xmax = firstpoint[0]
@ -433,12 +449,12 @@ def writeSVG(self, unfolding, size, randomColorSet):
gluePairs = 0 gluePairs = 0
mountainCuts = 0 mountainCuts = 0
valleyCuts = 0 valleyCuts = 0
coplanarLines = 0 coplanarEdges = 0
mountainPerforations = 0 mountainPerforations = 0
valleyPerforations = 0 valleyPerforations = 0
# Calculate the bounding box # Calculate the bounding box
[xmin, ymin, boxSize] = findBoundingBox(unfolding[0]) [xmin, ymin, boxSize] = self.findBoundingBox(unfolding[0])
if size > 0: if size > 0:
boxSize = size boxSize = size
@ -472,6 +488,15 @@ def writeSVG(self, unfolding, size, randomColorSet):
textGroup.add(textFacesGroup) textGroup.add(textFacesGroup)
textGroup.add(textEdgesGroup) 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)
if self.options.printTriangleNumbers is True: if self.options.printTriangleNumbers is True:
for face in mesh.faces(): for face in mesh.faces():
@ -524,7 +549,7 @@ def writeSVG(self, unfolding, size, randomColorSet):
line.set("id", uniqueMainId + "-coplanar-edge-" + str(edge.idx())) line.set("id", uniqueMainId + "-coplanar-edge-" + str(edge.idx()))
#if self.options.importCoplanarEdges is False: #if self.options.importCoplanarEdges is False:
# line.delete() # line.delete()
coplanarLines += 1 coplanarEdges += 1
lineStyle.update({"stroke-width":str(strokewidth)}) lineStyle.update({"stroke-width":str(strokewidth)})
lineStyle.update({"stroke-linecap":"butt"}) lineStyle.update({"stroke-linecap":"butt"})
@ -614,30 +639,32 @@ def writeSVG(self, unfolding, size, randomColorSet):
tspanText.append("{:0.2f} {}".format(self.options.scalefactor * math.hypot(vertex1[0] - vertex0[0], vertex1[1] - vertex0[1]), unitToPrint)) tspanText.append("{:0.2f} {}".format(self.options.scalefactor * math.hypot(vertex1[0] - vertex0[0], vertex1[1] - vertex0[1]), unitToPrint))
tspan.text = " | ".join(tspanText) 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: if tspan.text == "": #if no text we remove again to clean up
text.delete() text.delete()
tspan.delete() tspan.delete()
#delete unrequired groups if no text labels if len(textFacesGroup) == 0:
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: textFacesGroup.delete() #delete if empty set
textGroup.delete()
textFacesGroup.delete() if len(textFacesGroup) == 0:
textEdgesGroup.delete() textEdgesGroup.delete() #delete if empty set
if len(textGroup) == 0:
textGroup.delete() #delete if empty set
if self.options.printStats is True: if self.options.printStats is True:
inkex.utils.debug("Folding edges stats:")
inkex.utils.debug(" * Number of mountain cuts: " + str(mountainCuts)) inkex.utils.debug(" * Number of mountain cuts: " + str(mountainCuts))
inkex.utils.debug(" * Number of valley cuts: " + str(valleyCuts)) inkex.utils.debug(" * Number of valley cuts: " + str(valleyCuts))
inkex.utils.debug(" * Number of coplanar lines: " + str(coplanarLines)) inkex.utils.debug(" * Number of coplanar edges: " + str(coplanarEdges))
inkex.utils.debug(" * Number of mountain perforations: " + str(mountainPerforations)) inkex.utils.debug(" * Number of mountain perforations: " + str(mountainPerforations))
inkex.utils.debug(" * Number of valley perforations: " + str(valleyPerforations)) inkex.utils.debug(" * Number of valley perforations: " + str(valleyPerforations))
inkex.utils.debug("-----------------------------------------------------------") inkex.utils.debug(" * Number of glue pairs: {:0.0f}".format(gluePairs / 2))
inkex.utils.debug("Number of glue pairs: {:0.0f}".format(gluePairs / 2)) inkex.utils.debug(" * min angle: {:0.2f}".format(minAngle))
inkex.utils.debug(" * max angle: {:0.2f}".format(maxAngle))
inkex.utils.debug(" * Edge angle range: {:0.2f}".format(angleRange))
return paperfoldPageGroup return paperfoldPageGroup
class Unfold(inkex.EffectExtension):
def add_arguments(self, pars): def add_arguments(self, pars):
pars.add_argument("--tab") pars.add_argument("--tab")
@ -645,6 +672,7 @@ class Unfold(inkex.EffectExtension):
pars.add_argument("--inputfile") pars.add_argument("--inputfile")
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.") 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.")
pars.add_argument("--scalefactor", type=float, default=1.0, help="Manual scale factor") pars.add_argument("--scalefactor", type=float, default=1.0, help="Manual scale factor")
pars.add_argument("--roundingDigits", type=int, default=3, help="Digits for rounding")
#Output #Output
pars.add_argument("--printGluePairNumbers", type=inkex.Boolean, default=False, help="Print glue pair numbers on cut edges") pars.add_argument("--printGluePairNumbers", type=inkex.Boolean, default=False, help="Print glue pair numbers on cut edges")
@ -652,10 +680,12 @@ class Unfold(inkex.EffectExtension):
pars.add_argument("--printLengths", type=inkex.Boolean, default=False, help="Print lengths on edges") 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") pars.add_argument("--printTriangleNumbers", type=inkex.Boolean, default=False, help="Print triangle numbers on faces")
pars.add_argument("--importCoplanarEdges", type=inkex.Boolean, default=False, help="Import coplanar edges") 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") pars.add_argument("--printStats", type=inkex.Boolean, default=False, help="Show some unfold statistics")
pars.add_argument("--resizetoimport", type=inkex.Boolean, default=True, help="Resize the canvas to the imported drawing's bounding box") 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) pars.add_argument("--extraborder", type=float, default=0.0)
pars.add_argument("--extraborderUnits") 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")
#Style #Style
pars.add_argument("--fontSize", type=int, default=15, help="Label font size (%)") pars.add_argument("--fontSize", type=int, default=15, help="Label font size (%)")
@ -669,6 +699,10 @@ class Unfold(inkex.EffectExtension):
pars.add_argument("--colorValleyPerforates", type=Color, default='3422552319', help="Valley perforation 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") pars.add_argument("--colorMountainPerforates", type=Color, default='879076607', help="Mountain perforation edges")
#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")
def effect(self): def effect(self):
if not os.path.exists(self.options.inputfile): 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.") inkex.utils.debug("The input file does not exist. Please select a proper file and try again.")
@ -676,12 +710,21 @@ class Unfold(inkex.EffectExtension):
mesh = om.read_trimesh(self.options.inputfile) mesh = om.read_trimesh(self.options.inputfile)
#mesh = om.read_polymesh(self.options.inputfile) #we must work with triangles instead of polygons because the algorithm works with that #mesh = om.read_polymesh(self.options.inputfile) #we must work with triangles instead of polygons because the algorithm works with that
fullUnfolded, unfoldedComponents = unfold(mesh, self.options.maxNumFaces, self.options.printStats) fullUnfolded, unfoldedComponents = self.unfold(mesh)
#if len(unfoldedComponents) == 0:
# inkex.utils.debug("Error: no components were unfolded.")
# exit(1)
if self.options.printStats is True:
inkex.utils.debug("Unfolding components: {:0.0f}".format(len(unfoldedComponents)))
# Compute maxSize of the components # Compute maxSize of the components
# All components must be scaled to the same size as the largest component # All components must be scaled to the same size as the largest component
maxSize = 0 maxSize = 0
for unfolding in unfoldedComponents: for unfolding in unfoldedComponents:
[xmin, ymin, boxSize] = findBoundingBox(unfolding[0]) [xmin, ymin, boxSize] = self.findBoundingBox(unfolding[0])
if boxSize > maxSize: if boxSize > maxSize:
maxSize = boxSize maxSize = boxSize
@ -694,10 +737,26 @@ class Unfold(inkex.EffectExtension):
if newColor not in randomColorSet: if newColor not in randomColorSet:
randomColorSet.append(newColor) randomColorSet.append(newColor)
#some mode configs:
if self.options.joineryMode is True:
self.options.separateGluePairsByColor = True #we need random colors in this mode
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
self.options.colorMountainCut = "#000000" #black
self.options.colorValleyCut = "#000000" #black
self.options.colorCoplanarEdges = "#000000" #black
self.options.colorMountainPerforates = "#ff0000" #red
self.options.colorValleyPerforates = "#0000ff" #blue
# Create a new container group to attach all paperfolds # 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 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)): for i in range(len(unfoldedComponents)):
paperfoldPageGroup = writeSVG(self, unfoldedComponents[i], maxSize, randomColorSet) 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)
#translate the groups next to each other to remove overlappings #translate the groups next to each other to remove overlappings
if i != 0: if i != 0:
previous_bbox = paperfoldMainGroup[i-1].bounding_box() previous_bbox = paperfoldMainGroup[i-1].bounding_box()
@ -715,12 +774,22 @@ class Unfold(inkex.EffectExtension):
if self.options.resizetoimport: if self.options.resizetoimport:
bbox = paperfoldMainGroup.bounding_box() bbox = paperfoldMainGroup.bounding_box()
namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi')) namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi'))
doc_units = namedView.get(inkex.addNS('document-units', 'inkscape'))
root = self.svg.getElement('//svg:svg'); root = self.svg.getElement('//svg:svg');
offset = self.svg.unittouu(str(self.options.extraborder) + self.options.extraborderUnits) offset = self.svg.unittouu(str(self.options.extraborder) + self.options.extraborderUnits)
root.set('viewBox', '%f %f %f %f' % (bbox.left - offset, bbox.top - offset, bbox.width + 2 * offset, bbox.height + 2 * offset)) 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('width', str(bbox.width + 2 * offset) + self.svg.unit)
root.set('height', str(bbox.height + 2 * offset) + doc_units) root.set('height', str(bbox.height + 2 * offset) + self.svg.unit)
#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
if __name__ == '__main__': if __name__ == '__main__':
Unfold().run() Unfold().run()