From 9d6d01f8452e240ff252b3a627f935c5eca8c35b Mon Sep 17 00:00:00 2001 From: leyghisbb Date: Mon, 10 May 2021 04:34:04 +0200 Subject: [PATCH] huge update for paperfold --- extensions/fablabchemnitz/convexhull.py | 2 +- extensions/fablabchemnitz/paperfold.inx | 17 +- extensions/fablabchemnitz/paperfold.py | 1261 ++++++++++++----------- 3 files changed, 680 insertions(+), 600 deletions(-) diff --git a/extensions/fablabchemnitz/convexhull.py b/extensions/fablabchemnitz/convexhull.py index 7c694b55..77dd98db 100644 --- a/extensions/fablabchemnitz/convexhull.py +++ b/extensions/fablabchemnitz/convexhull.py @@ -75,7 +75,7 @@ class ConvexHull(inkex.EffectExtension): line_attribs['transform'] = cloneTransform 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: n_array = [] if element.tag == inkex.addNS('path','svg'): diff --git a/extensions/fablabchemnitz/paperfold.inx b/extensions/fablabchemnitz/paperfold.inx index ae154ac4..0ffcff1c 100644 --- a/extensions/fablabchemnitz/paperfold.inx +++ b/extensions/fablabchemnitz/paperfold.inx @@ -8,6 +8,7 @@ /your/beautiful/3dmodel/file 200 1.0 + 3 @@ -27,6 +28,8 @@ + false + ./inkscape_export/ @@ -35,7 +38,7 @@ false true false - false + false 255 1968208895 1943148287 @@ -43,9 +46,17 @@ 879076607 - - + + + + + false + + + + + false diff --git a/extensions/fablabchemnitz/paperfold.py b/extensions/fablabchemnitz/paperfold.py index 45e8d74b..8bc74983 100644 --- a/extensions/fablabchemnitz/paperfold.py +++ b/extensions/fablabchemnitz/paperfold.py @@ -19,7 +19,7 @@ Paperfold is another flattener for triangle mesh files, heavily based on paperfo Author: Mario Voigt / FabLab Chemnitz Mail: mario.voigt@stadtfabrikanten.org Date: 13.09.2020 -Last patch: 08.05.2021 +Last patch: 10.05.2021 License: GNU GPL v3 To run this you need to install OpenMesh with python pip. @@ -38,606 +38,633 @@ possible import file types -> https://www.graphics.rwth-aachen.de/media/openmesh todo: - 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) -- 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 ... """ -# 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) +class Unfold(inkex.EffectExtension): - 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): - foldingDirection[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = round(math.degrees(mesh.calc_dihedral_angle(originalHalfedges[i])), 3) - # 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): - 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 - foldingDirection = np.empty(numUnfoldedEdges, dtype=float) # Valley folding or mountain folding - - 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 + # 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 + def addVisualisationData(self, mesh, unfoldedMesh, originalHalfedges, unfoldedHalfedges, glueNumber, foldingDirection): for i in range(3): - halfEdgeConnections[originalHalfEdges[i].idx()] = unfoldedHalfEdges[i].idx() - - return [unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection] - -def unfold(mesh, maxNumFaces, printStats): - # 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() - - 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("-----------------------------------------------------------") - - - # 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) - - # Unfold the tree - 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] - - -def writeSVG(self, unfolding, size, randomColorSet): - mesh = unfolding[0] - isFoldingEdge = unfolding[1] - glueNumber = unfolding[3] - foldingDirection = unfolding[4] - - #statistic values - gluePairs = 0 - mountainCuts = 0 - valleyCuts = 0 - coplanarLines = 0 - mountainPerforations = 0 - valleyPerforations = 0 + foldingDirection[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = round(math.degrees(mesh.calc_dihedral_angle(originalHalfedges[i])), self.options.roundingDigits) + # Information, which edges belong together + glueNumber[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = mesh.edge_handle(originalHalfedges[i]).idx() - # Calculate the bounding box - [xmin, ymin, boxSize] = 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 - textStrokewidth = boxSize * self.options.fontSize / 3000 - fontsize = boxSize * self.options.fontSize / 1000 - - 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) - - if self.options.printTriangleNumbers is True: - - for face in mesh.faces(): - centroid = mesh.calc_face_centroid(face) - - 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())) - circle.set("style", "stroke:#000000;stroke-width:" + str(strokewidth/2) + ";fill:none") - - text = textFaceGroup.add(TextElement(id=uniqueMainId + "-textFaceNumber-" + str(face.idx()))) - 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") - - tspan = text.add(Tspan(id=uniqueMainId + "-textFaceNumberTspan-" + str(face.idx()))) - 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") - tspan.text = str(face.idx()) - textFacesGroup.append(textFaceGroup) - - # 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()) - line.set('d', "M " + str(vertex0[0]) + "," + str(vertex0[1]) + " " + str(vertex1[0]) + "," + str(vertex1[1])) - # Colour depending on folding direction - lineStyle = {"fill": "none"} - - dihedralAngle = foldingDirection[edge.idx()] + # Function that unwinds a spanning tree + def unfoldSpanningTree(self, mesh, spanningTree): + try: + unfoldedMesh = om.TriMesh() # the unfolded mesh - if dihedralAngle > 0: - lineStyle.update({"stroke": self.options.colorMountainCut}) - line.set("id", uniqueMainId + "-mountain-cut-" + str(edge.idx())) - mountainCuts += 1 - elif dihedralAngle < 0: - lineStyle.update({"stroke": self.options.colorValleyCut}) - line.set("id", uniqueMainId + "-valley-cut-" + str(edge.idx())) - valleyCuts += 1 - elif 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() - coplanarLines += 1 - - lineStyle.update({"stroke-width":str(strokewidth)}) - lineStyle.update({"stroke-linecap":"butt"}) - lineStyle.update({"stroke-linejoin":"miter"}) - lineStyle.update({"stroke-miterlimit":"4"}) + 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 + foldingDirection = np.empty(numUnfoldedEdges, dtype=float) # Valley folding or mountain folding + + 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 + 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 + self.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] = 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 + self.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] + 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(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() + + if numFaces > self.options.maxNumFaces: + inkex.utils.debug("Aborted. Target STL file has " + str(numFaces) + " faces, but only " + str(maxNumFaces) + " are allowed.") + exit(1) + + 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("-----------------------------------------------------------") + + # 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(): + #inkex.utils.debug("edge.idx = " + str(edge.idx())) + + # 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) + #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) + [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 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] + foldingDirection = unfolding[4] + + #statistic values + gluePairs = 0 + mountainCuts = 0 + valleyCuts = 0 + coplanarEdges = 0 + mountainPerforations = 0 + valleyPerforations = 0 + + # 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 + textStrokewidth = boxSize * self.options.fontSize / 3000 + fontsize = boxSize * self.options.fontSize / 1000 + + 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) + + #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: + + for face in mesh.faces(): + centroid = mesh.calc_face_centroid(face) + + 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())) + circle.set("style", "stroke:#000000;stroke-width:" + str(strokewidth/2) + ";fill:none") + + text = textFaceGroup.add(TextElement(id=uniqueMainId + "-textFaceNumber-" + str(face.idx()))) + 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") + + tspan = text.add(Tspan(id=uniqueMainId + "-textFaceNumberTspan-" + str(face.idx()))) + 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") + tspan.text = str(face.idx()) + textFacesGroup.append(textFaceGroup) + + # 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()) + line.set('d', "M " + str(vertex0[0]) + "," + str(vertex0[1]) + " " + str(vertex1[0]) + "," + str(vertex1[1])) + # Colour depending on folding direction + lineStyle = {"fill": "none"} + + dihedralAngle = foldingDirection[edge.idx()] - # Dotted lines for folding edges - if isFoldingEdge[edge.idx()]: - if self.options.dashes is True: - lineStyle.update({"stroke-dasharray":(str(dashLength) + ", " + str(spaceLength))}) if dihedralAngle > 0: - lineStyle.update({"stroke": self.options.colorMountainPerforates}) - line.set("id", uniqueMainId + "-mountain-perforate-" + str(edge.idx())) - mountainPerforations += 1 - if dihedralAngle < 0: - lineStyle.update({"stroke": self.options.colorValleyPerforates}) - line.set("id", uniqueMainId + "-valley-perforate-" + str(edge.idx())) - valleyPerforations += 1 - if dihedralAngle == 0: + lineStyle.update({"stroke": self.options.colorMountainCut}) + line.set("id", uniqueMainId + "-mountain-cut-" + str(edge.idx())) + mountainCuts += 1 + elif dihedralAngle < 0: + lineStyle.update({"stroke": self.options.colorValleyCut}) + line.set("id", uniqueMainId + "-valley-cut-" + str(edge.idx())) + valleyCuts += 1 + elif 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() - valleyPerforations += 1 - 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 - - lineStyle.update({"stroke-dashoffset":"0"}) - lineStyle.update({"stroke-opacity":"1"}) - - 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()}) - - line.style = lineStyle - - ######################################################## - # Textual things - ######################################################## - 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()))) - 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() - - #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() - - 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 valley cuts: " + str(valleyCuts)) - inkex.utils.debug(" * Number of coplanar lines: " + str(coplanarLines)) - inkex.utils.debug(" * Number of mountain perforations: " + str(mountainPerforations)) - inkex.utils.debug(" * Number of valley perforations: " + str(valleyPerforations)) - inkex.utils.debug("-----------------------------------------------------------") - inkex.utils.debug("Number of glue pairs: {:0.0f}".format(gluePairs / 2)) - - return paperfoldPageGroup + #if self.options.importCoplanarEdges is False: + # line.delete() + coplanarEdges += 1 + + lineStyle.update({"stroke-width":str(strokewidth)}) + lineStyle.update({"stroke-linecap":"butt"}) + lineStyle.update({"stroke-linejoin":"miter"}) + lineStyle.update({"stroke-miterlimit":"4"}) -class Unfold(inkex.EffectExtension): + # Dotted lines for folding edges + if isFoldingEdge[edge.idx()]: + if self.options.dashes is True: + lineStyle.update({"stroke-dasharray":(str(dashLength) + ", " + str(spaceLength))}) + if dihedralAngle > 0: + lineStyle.update({"stroke": self.options.colorMountainPerforates}) + line.set("id", uniqueMainId + "-mountain-perforate-" + str(edge.idx())) + mountainPerforations += 1 + if dihedralAngle < 0: + lineStyle.update({"stroke": self.options.colorValleyPerforates}) + line.set("id", uniqueMainId + "-valley-perforate-" + str(edge.idx())) + valleyPerforations += 1 + 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() + valleyPerforations += 1 + 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 + + lineStyle.update({"stroke-dashoffset":"0"}) + lineStyle.update({"stroke-opacity":"1"}) + + 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()}) + + line.style = lineStyle + + ######################################################## + # Textual things + ######################################################## + 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()))) + 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 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 + + if len(textFacesGroup) == 0: + textEdgesGroup.delete() #delete if empty set + + if len(textGroup) == 0: + textGroup.delete() #delete if empty set + + if self.options.printStats is True: + inkex.utils.debug(" * Number of mountain cuts: " + str(mountainCuts)) + inkex.utils.debug(" * Number of valley cuts: " + str(valleyCuts)) + inkex.utils.debug(" * Number of coplanar edges: " + str(coplanarEdges)) + inkex.utils.debug(" * Number of mountain perforations: " + str(mountainPerforations)) + inkex.utils.debug(" * Number of valley perforations: " + str(valleyPerforations)) + 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 + def add_arguments(self, pars): pars.add_argument("--tab") @@ -645,6 +672,7 @@ class Unfold(inkex.EffectExtension): 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("--scalefactor", type=float, default=1.0, help="Manual scale factor") + pars.add_argument("--roundingDigits", type=int, default=3, help="Digits for rounding") #Output 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("--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("--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("--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 pars.add_argument("--fontSize", type=int, default=15, help="Label font size (%)") @@ -668,6 +698,10 @@ class Unfold(inkex.EffectExtension): 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") + + #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): if not os.path.exists(self.options.inputfile): @@ -676,12 +710,21 @@ class Unfold(inkex.EffectExtension): 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 - 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 # 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]) + [xmin, ymin, boxSize] = self.findBoundingBox(unfolding[0]) if boxSize > maxSize: maxSize = boxSize @@ -693,11 +736,27 @@ class Unfold(inkex.EffectExtension): newColor = '#%02X%02X%02X' % (r(),r(),r()) if newColor not in randomColorSet: 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 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)): - 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 if i != 0: previous_bbox = paperfoldMainGroup[i-1].bounding_box() @@ -715,12 +774,22 @@ class Unfold(inkex.EffectExtension): 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'); 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('width', str(bbox.width + 2 * offset) + doc_units) - root.set('height', str(bbox.height + 2 * offset) + doc_units) + root.set('width', str(bbox.width + 2 * offset) + self.svg.unit) + 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__': Unfold().run() \ No newline at end of file