#!/usr/bin/env python3 import openmesh as om import math import inkex import tempfile import os import random import numpy as np import openmesh as om import networkx as nx from lxml import etree from inkex import Transform, TextElement, Tspan, Color, Circle """ Extension for InkScape 1.0 Paperfold is another flattener for triangle mesh files, heavily based on paperfoldmodels by Felix Scholz aka felixfeliz. Author: Mario Voigt / FabLab Chemnitz Mail: mario.voigt@stadtfabrikanten.org Date: 13.09.2020 Last patch: 08.05.2021 License: GNU GPL v3 To run this you need to install OpenMesh with python pip. The algorithm of paperfoldmodels consists of three steps: - Find a minimum spanning tree of the dual graph of the mesh. - Unfold the dual graph. - Remove self-intersections by adding additional cuts along edges. Reference: The code is mostly based on the algorithm presented in a by Straub and Prautzsch (https://geom.ivd.kit.edu/downloads/proj-paper-models_cut_out_sheets.pdf). Module licenses - paperfoldmodels (https://github.com/felixfeliz/paperfoldmodels) - MIT License possible import file types -> https://www.graphics.rwth-aachen.de/media/openmesh_static/Documentations/OpenMesh-8.0-Documentation/a04096.html """ # Compute the third point of a triangle when two points and all edge lengths are given def getThirdPoint(v0, v1, l01, l12, l20): v2rotx = (l01 ** 2 + l20 ** 2 - l12 ** 2) / (2 * l01) v2roty0 = np.sqrt((l01 + l20 + l12) * (l01 + l20 - l12) * (l01 - l20 + l12) * (-l01 + l20 + l12)) / (2 * l01) v2roty1 = - v2roty0 theta = np.arctan2(v1[1] - v0[1], v1[0] - v0[0]) v2trans0 = np.array( [v2rotx * np.cos(theta) - v2roty0 * np.sin(theta), v2rotx * np.sin(theta) + v2roty0 * np.cos(theta), 0]) v2trans1 = np.array( [v2rotx * np.cos(theta) - v2roty1 * np.sin(theta), v2rotx * np.sin(theta) + v2roty1 * np.cos(theta), 0]) return [v2trans0 + v0, v2trans1 + v0] # Check if two lines intersect def lineIntersection(v1, v2, v3, v4, epsilon): d = (v4[1] - v3[1]) * (v2[0] - v1[0]) - (v4[0] - v3[0]) * (v2[1] - v1[1]) u = (v4[0] - v3[0]) * (v1[1] - v3[1]) - (v4[1] - v3[1]) * (v1[0] - v3[0]) v = (v2[0] - v1[0]) * (v1[1] - v3[1]) - (v2[1] - v1[1]) * (v1[0] - v3[0]) if d < 0: u, v, d = -u, -v, -d return ((0 + epsilon) <= u <= (d - epsilon)) and ((0 + epsilon) <= v <= (d - epsilon)) # Check if a point lies inside a triangle def pointInTriangle(A, B, C, P, epsilon): v0 = [C[0] - A[0], C[1] - A[1]] v1 = [B[0] - A[0], B[1] - A[1]] v2 = [P[0] - A[0], P[1] - A[1]] cross = lambda u, v: u[0] * v[1] - u[1] * v[0] u = cross(v2, v0) v = cross(v1, v2) d = cross(v1, v0) if d < 0: u, v, d = -u, -v, -d return u >= (0 + epsilon) and v >= (0 + epsilon) and (u + v) <= (d - epsilon) # Check if two triangles intersect def triangleIntersection(t1, t2, epsilon): if lineIntersection(t1[0], t1[1], t2[0], t2[1], epsilon): return True if lineIntersection(t1[0], t1[1], t2[0], t2[2], epsilon): return True if lineIntersection(t1[0], t1[1], t2[1], t2[2], epsilon): return True if lineIntersection(t1[0], t1[2], t2[0], t2[1], epsilon): return True if lineIntersection(t1[0], t1[2], t2[0], t2[2], epsilon): return True if lineIntersection(t1[0], t1[2], t2[1], t2[2], epsilon): return True if lineIntersection(t1[1], t1[2], t2[0], t2[1], epsilon): return True if lineIntersection(t1[1], t1[2], t2[0], t2[2], epsilon): return True if lineIntersection(t1[1], t1[2], t2[1], t2[2], epsilon): return True inTri = True inTri = inTri and pointInTriangle(t1[0], t1[1], t1[2], t2[0], epsilon) inTri = inTri and pointInTriangle(t1[0], t1[1], t1[2], t2[1], epsilon) inTri = inTri and pointInTriangle(t1[0], t1[1], t1[2], t2[2], epsilon) if inTri == True: return True inTri = True inTri = inTri and pointInTriangle(t2[0], t2[1], t2[2], t1[0], epsilon) inTri = inTri and pointInTriangle(t2[0], t2[1], t2[2], t1[1], epsilon) inTri = inTri and pointInTriangle(t2[0], t2[1], t2[2], t1[2], epsilon) if inTri == True: return True return False # Functions for visualisation and output def addVisualisationData(mesh, unfoldedMesh, originalHalfedges, unfoldedHalfedges, glueNumber, foldingDirection): for i in range(3): 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 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 # 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()] 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"}) # 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 (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() 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 class Unfold(inkex.EffectExtension): def add_arguments(self, pars): pars.add_argument("--tab") #Input 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") #Output pars.add_argument("--printGluePairNumbers", type=inkex.Boolean, default=False, help="Print glue pair numbers on cut edges") pars.add_argument("--printAngles", type=inkex.Boolean, default=False, help="Print folding angles on edges") 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("--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") #Style pars.add_argument("--fontSize", type=int, default=15, help="Label font size (%)") pars.add_argument("--flipLabels", type=inkex.Boolean, default=False, help="Flip labels") pars.add_argument("--dashes", type=inkex.Boolean, default=True, help="Dashes for cut/coplanar edges") pars.add_argument("--saturationsForAngles", type=inkex.Boolean, help="Adjust color saturation for folding edges. The larger the angle the darker the color") pars.add_argument("--separateGluePairsByColor", type=inkex.Boolean, default=False, help="Separate glue pairs by color") pars.add_argument("--colorValleyCut", type=Color, default='255', help="Valley cut edges") pars.add_argument("--colorMountainCut", type=Color, default='1968208895', help="Mountain cut edges") pars.add_argument("--colorCoplanarEdges", type=Color, default='1943148287', help="Coplanar edges") pars.add_argument("--colorValleyPerforates", type=Color, default='3422552319', help="Valley perforation edges") pars.add_argument("--colorMountainPerforates", type=Color, default='879076607', help="Mountain perforation edges") def effect(self): if not os.path.exists(self.options.inputfile): inkex.utils.debug("The input file does not exist. Please select a proper file and try again.") exit(1) 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) # Compute maxSize of the components # All components must be scaled to the same size as the largest component maxSize = 0 for unfolding in unfoldedComponents: [xmin, ymin, boxSize] = findBoundingBox(unfolding[0]) if boxSize > maxSize: maxSize = boxSize #generate random colors for glue pairs randomColorSet = [] if self.options.separateGluePairsByColor: while len(randomColorSet) < len(mesh.edges()): r = lambda: random.randint(0,255) newColor = '#%02X%02X%02X' % (r(),r(),r()) if newColor not in randomColorSet: randomColorSet.append(newColor) # 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) #translate the groups next to each other to remove overlappings if i != 0: previous_bbox = paperfoldMainGroup[i-1].bounding_box() this_bbox = paperfoldPageGroup.bounding_box() paperfoldPageGroup.set("transform","translate(" + str(previous_bbox.left + previous_bbox.width - this_bbox.left) + ", 0.0)") paperfoldMainGroup.append(paperfoldPageGroup) #apply scale factor translation_matrix = [[self.options.scalefactor, 0.0, 0.0], [0.0, self.options.scalefactor, 0.0]] paperfoldMainGroup.transform = Transform(translation_matrix) * paperfoldMainGroup.transform #paperfoldMainGroup.set('transform', 'scale(%f,%f)' % (self.options.scalefactor, self.options.scalefactor)) #adjust canvas to the inserted unfolding if self.options.resizetoimport: bbox = paperfoldMainGroup.bounding_box() namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi')) doc_units = namedView.get(inkex.addNS('document-units', 'inkscape')) root = self.svg.getElement('//svg:svg'); 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) if __name__ == '__main__': Unfold().run()