1021 lines
51 KiB
Python
1021 lines
51 KiB
Python
|
#!/usr/bin/env python3
|
||
|
import math
|
||
|
import inkex
|
||
|
from inkex import Transform, TextElement, Tspan, Color, Circle, PathElement, CubicSuperPath
|
||
|
import os
|
||
|
import random
|
||
|
import numpy as np
|
||
|
import openmesh as om
|
||
|
import networkx as nx
|
||
|
from lxml import etree
|
||
|
import copy
|
||
|
|
||
|
"""
|
||
|
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: 10.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
|
||
|
|
||
|
todo:
|
||
|
- 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
|
||
|
- fix line: dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight) # #might fail without throwing any error (silent aborts) ...
|
||
|
- option to set fill color per face
|
||
|
- add some way to merge coplanar triangles (tri-faces) to polygons and keep those polygons (facets) intact. At the moment facets are getting destroyed. Not good for some papercrafts
|
||
|
"""
|
||
|
|
||
|
class Paperfold(inkex.EffectExtension):
|
||
|
|
||
|
angleRangeCalculated = False #set to true after first calculation iteration (needed globally)
|
||
|
minAngle = 0
|
||
|
minAngle = 0
|
||
|
angleRange = 0
|
||
|
|
||
|
def getElementChildren(self, element, elements = None):
|
||
|
if elements == None:
|
||
|
elements = []
|
||
|
if element.tag != inkex.addNS('g','svg'):
|
||
|
elements.append(element)
|
||
|
for child in element.getchildren():
|
||
|
self.getElementChildren(child, elements)
|
||
|
return elements
|
||
|
|
||
|
# 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)
|
||
|
val = (l01 + l20 + l12) * (l01 + l20 - l12) * (l01 - l20 + l12) * (-l01 + l20 + l12)
|
||
|
v2roty0 = np.sqrt(abs(val)) / (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, dihedralAngles):
|
||
|
for i in range(3):
|
||
|
dihedralAngles[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()
|
||
|
|
||
|
# Function that unwinds a spanning tree
|
||
|
def unfoldSpanningTree(self, mesh, spanningTree):
|
||
|
try:
|
||
|
unfoldedMesh = om.TriMesh() # the unfolded mesh
|
||
|
|
||
|
numFaces = mesh.n_faces()
|
||
|
sizeTree = spanningTree.number_of_edges()
|
||
|
numUnfoldedEdges = 3 * numFaces - sizeTree
|
||
|
|
||
|
isFoldingEdge = np.zeros(numUnfoldedEdges, dtype=bool) # Indicates whether an edge is folded or cut
|
||
|
glueNumber = np.empty(numUnfoldedEdges, dtype=int) # Saves with which edge is glued together
|
||
|
dihedralAngles = 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
|
||
|
|
||
|
numFaces = mesh.n_faces()
|
||
|
sizeTree = spanningTree.number_of_edges()
|
||
|
numUnfoldedEdges = 3 * numFaces - sizeTree
|
||
|
|
||
|
# 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, dihedralAngles)
|
||
|
|
||
|
if self.angleRangeCalculated is False:
|
||
|
self.minAngle = min(dihedralAngles)
|
||
|
self.maxAngle = max(dihedralAngles)
|
||
|
#sometimes weird large value are returned, like -34345645435464565453356788761029782
|
||
|
if self.minAngle < -180.0:
|
||
|
self.minAngle = -180.0
|
||
|
if self.maxAngle > 180.0:
|
||
|
self.maxAngle = 180.0
|
||
|
self.angleRange = self.maxAngle - self.minAngle
|
||
|
#self.msg(minAngle)
|
||
|
#self.msg(maxAngle)
|
||
|
#self.msg(angleRange)
|
||
|
self.angleRangeCalculated = True
|
||
|
|
||
|
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):
|
||
|
try:
|
||
|
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, dihedralAngles)
|
||
|
|
||
|
# Related page
|
||
|
connections[newface.idx()] = dualEdge[1]
|
||
|
|
||
|
# Identify the half edges
|
||
|
for i in range(3):
|
||
|
halfEdgeConnections[originalHalfEdges[i].idx()] = unfoldedHalfEdges[i].idx()
|
||
|
except Exception as e:
|
||
|
inkex.utils.debug("Error walking the dual tree at dualEdge {}".format(e))
|
||
|
exit(1)
|
||
|
return [unfoldedMesh, isFoldingEdge, connections, glueNumber, dihedralAngles]
|
||
|
except Exception as e:
|
||
|
inkex.utils.debug("Error: model could not be unfolded. Check for:")
|
||
|
inkex.utils.debug(" - watertight model / intact hull")
|
||
|
inkex.utils.debug(" - duplicated edges or faces")
|
||
|
inkex.utils.debug(" - detached faces or holes")
|
||
|
inkex.utils.debug(" - missing units")
|
||
|
inkex.utils.debug(" - missing coordinate system")
|
||
|
inkex.utils.debug(" - multiple bodies in one file")
|
||
|
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( self.options.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)
|
||
|
|
||
|
if self.options.experimentalWeights is True:
|
||
|
if round(math.degrees(mesh.calc_dihedral_angle(edge)), self.options.roundingDigits) > 0:
|
||
|
edgeweight = 1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength)
|
||
|
if round(math.degrees(mesh.calc_dihedral_angle(edge)), self.options.roundingDigits) < 0:
|
||
|
edgeweight = -(1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength))
|
||
|
if round(math.degrees(mesh.calc_dihedral_angle(edge)), self.options.roundingDigits) == 0:
|
||
|
edgeweight = 0.0
|
||
|
|
||
|
#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, dihedralAngles] = 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]
|
||
|
dihedralAngles = unfolding[4]
|
||
|
|
||
|
#statistic values
|
||
|
gluePairs = 0
|
||
|
cuts = 0
|
||
|
coplanarEdges = 0
|
||
|
mountainFolds = 0
|
||
|
valleyFolds = 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
|
||
|
|
||
|
# 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)
|
||
|
|
||
|
|
||
|
#########################################################
|
||
|
# Nmbering triangle faces with circle around
|
||
|
#########################################################
|
||
|
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="{:0.6f}".format(centroid[0]), cy="{:0.6f}".format(centroid[1]), r="{:0.6f}".format(fontsize)))
|
||
|
circle.set('id', uniqueMainId + "-textFaceCricle-" + str(face.idx()))
|
||
|
circle.set("style", "stroke:#000000;stroke-width:{:0.6f}".format(strokewidth/2) + ";fill:none")
|
||
|
|
||
|
text = textFaceGroup.add(TextElement(id=uniqueMainId + "-textFaceNumber-" + str(face.idx())))
|
||
|
text.set("x", "{:0.6f}".format(centroid[0]))
|
||
|
text.set("y", "{:0.6f}".format(centroid[1] + fontsize / 3))
|
||
|
text.set("font-size", "{:0.6f}".format(fontsize))
|
||
|
text.set("style", "stroke-width {:0.6f}".format(textStrokeWidth) + ";text-anchor:middle;text-align:center")
|
||
|
|
||
|
tspan = text.add(Tspan(id=uniqueMainId + "-textFaceNumberTspan-" + str(face.idx())))
|
||
|
tspan.set("x", "{:0.6f}".format(centroid[0]))
|
||
|
tspan.set("y", "{:0.6f}".format(centroid[1] + fontsize / 3))
|
||
|
tspan.set("style", "stroke-width {:0.6f}".format(textStrokeWidth) + ";text-anchor:middle;text-align:center")
|
||
|
tspan.text = str(face.idx())
|
||
|
textFacesGroup.append(textFaceGroup)
|
||
|
|
||
|
#########################################################
|
||
|
# Nmbering triangle edges and style them according to their type
|
||
|
#########################################################
|
||
|
# 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(PathElement())
|
||
|
line.set('d', "M {:0.6f},{:0.6f} {:0.6f},{:0.6f}".format(vertex0[0], vertex0[1], vertex1[0], vertex1[1]))
|
||
|
# Colour depending on folding direction
|
||
|
lineStyle = {"fill": "none"}
|
||
|
|
||
|
lineStyle.update({"stroke": self.options.colorCutEdges})
|
||
|
line.set("id", uniqueMainId + "-cut-edge-" + str(edge.idx()))
|
||
|
|
||
|
lineStyle.update({"stroke-width": "{:0.6f}".format(strokewidth)})
|
||
|
lineStyle.update({"stroke-linecap":"butt"})
|
||
|
lineStyle.update({"stroke-linejoin":"miter"})
|
||
|
lineStyle.update({"stroke-miterlimit":"4"})
|
||
|
|
||
|
dihedralAngle = dihedralAngles[edge.idx()]
|
||
|
|
||
|
# Dotted lines for folding edges
|
||
|
if isFoldingEdge[edge.idx()]:
|
||
|
if self.options.dashes is True:
|
||
|
lineStyle.update({"stroke-dasharray":"{:0.6f}, {:0.6f}".format(dashLength, spaceLength)})
|
||
|
if dihedralAngle > 0:
|
||
|
lineStyle.update({"stroke": self.options.colorMountainFolds})
|
||
|
line.set("id", uniqueMainId + "-mountain-fold-" + str(edge.idx()))
|
||
|
mountainFolds += 1
|
||
|
if dihedralAngle < 0:
|
||
|
lineStyle.update({"stroke": self.options.colorValleyFolds})
|
||
|
line.set("id", uniqueMainId + "-valley-fold-" + str(edge.idx()))
|
||
|
valleyFolds += 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()
|
||
|
coplanarEdges += 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.0"})
|
||
|
lineStyle.update({"stroke-opacity":"1.0"})
|
||
|
|
||
|
if self.options.edgeStyle == "saturationsForAngles":
|
||
|
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 / self.angleRange) * 100 #percentage values
|
||
|
hslColor.saturation = newSaturation
|
||
|
lineStyle.update({"stroke":hslColor.to_rgb()})
|
||
|
|
||
|
elif self.options.edgeStyle == "opacitiesForAngles":
|
||
|
if dihedralAngle != 0: #we dont want to apply opacity adjustments for zero angle lines because they would be invisible then
|
||
|
opacity = abs(dihedralAngle / 180)
|
||
|
lineStyle.update({"stroke-opacity": "{:0.6f}".format(opacity)})
|
||
|
|
||
|
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", "{:0.6f}".format(position[0]))
|
||
|
text.set("y", "{:0.6f}".format(position[1]))
|
||
|
text.set("font-size", "{:0.6f}".format(fontsize))
|
||
|
text.set("style", "stroke-width {:0.6f}".format(textStrokeWidth) + ";text-anchor:middle;text-align:center")
|
||
|
text.set("transform", "rotate({:0.6f} {:0.6f} {:0.6f})".format(rotation, position[0], position[1]))
|
||
|
|
||
|
tspan = text.add(Tspan())
|
||
|
tspan.set("x", "{:0.6f}".format(position[0]))
|
||
|
tspan.set("y", "{:0.6f}".format(position[1]))
|
||
|
tspan.set("style", "stroke-width {:0.6f}".format(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 and dihedralAngle != 0.0:
|
||
|
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()
|
||
|
|
||
|
'''
|
||
|
merge cutting edges to single contour. code ripped off from "join path" extension
|
||
|
'''
|
||
|
if self.options.merge_cut_lines is True:
|
||
|
cutEdges = []
|
||
|
|
||
|
#find all cutting edges - they have to be sorted to build up a clean continuous line
|
||
|
for edge in edgesGroup:
|
||
|
edge_id = edge.get('id')
|
||
|
if "cut-edge-" in edge_id:
|
||
|
cutEdges.append(edge)
|
||
|
|
||
|
#find the cutting edge which starts at the previous cutting edge end point
|
||
|
paths = {p.get('id'): self.getPartsFromCubicSuper(CubicSuperPath(p.get('d'))) for p in cutEdges }
|
||
|
pathIds = [p.get('id') for p in cutEdges]
|
||
|
|
||
|
startPathId = pathIds[0]
|
||
|
pathIds = self.getArrangedIds(paths, startPathId)
|
||
|
|
||
|
newParts = []
|
||
|
firstElem = None
|
||
|
for key in pathIds:
|
||
|
parts = paths[key]
|
||
|
# ~ parts = getPartsFromCubicSuper(cspath)
|
||
|
start = parts[0][0][0]
|
||
|
elem = self.svg.getElementById(key)
|
||
|
|
||
|
if(len(newParts) == 0):
|
||
|
newParts += parts[:]
|
||
|
firstElem = elem
|
||
|
else:
|
||
|
if(self.vectCmpWithMargin(start, newParts[-1][-1][-1], margin = .01)):
|
||
|
newParts[-1] += parts[0]
|
||
|
else:
|
||
|
newSeg = [newParts[-1][-1][-1], newParts[-1][-1][-1], start, start]
|
||
|
newParts[-1].append(newSeg)
|
||
|
newParts[-1] += parts[0]
|
||
|
|
||
|
if(len(parts) > 1):
|
||
|
newParts += parts[1:]
|
||
|
|
||
|
parent = elem.getparent()
|
||
|
parent.remove(elem)
|
||
|
|
||
|
newElem = copy.copy(firstElem)
|
||
|
oldId = firstElem.get('id')
|
||
|
newElem.set('d', CubicSuperPath(self.getCubicSuperFromParts(newParts)))
|
||
|
newElem.set('id', oldId + '_joined')
|
||
|
parent.append(newElem) #insert at the end
|
||
|
|
||
|
if len(textFacesGroup) == 0:
|
||
|
textFacesGroup.delete() #delete if empty set
|
||
|
|
||
|
if len(textEdgesGroup) == 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 cuts: " + str(cuts))
|
||
|
inkex.utils.debug(" * Number of coplanar edges: " + str(coplanarEdges))
|
||
|
inkex.utils.debug(" * Number of mountain folds: " + str(mountainFolds))
|
||
|
inkex.utils.debug(" * Number of valley folds: " + str(valleyFolds))
|
||
|
inkex.utils.debug(" * Number of glue pairs: {:0.0f}".format(gluePairs / 2))
|
||
|
inkex.utils.debug(" * min angle: {:0.2f}".format(self.minAngle))
|
||
|
inkex.utils.debug(" * max angle: {:0.2f}".format(self.maxAngle))
|
||
|
inkex.utils.debug(" * Edge angle range: {:0.2f}".format(self.angleRange))
|
||
|
|
||
|
return paperfoldPageGroup
|
||
|
|
||
|
|
||
|
def floatCmpWithMargin(self, float1, float2, margin):
|
||
|
return abs(float1 - float2) < margin
|
||
|
|
||
|
|
||
|
def vectCmpWithMargin(self, vect1, vect2, margin):
|
||
|
return all(self.floatCmpWithMargin(vect2[i], vect1[i], margin) for i in range(0, len(vect1)))
|
||
|
|
||
|
|
||
|
def getPartsFromCubicSuper(self, cspath):
|
||
|
parts = []
|
||
|
for subpath in cspath:
|
||
|
part = []
|
||
|
prevBezPt = None
|
||
|
for i, bezierPt in enumerate(subpath):
|
||
|
if(prevBezPt != None):
|
||
|
seg = [prevBezPt[1], prevBezPt[2], bezierPt[0], bezierPt[1]]
|
||
|
part.append(seg)
|
||
|
prevBezPt = bezierPt
|
||
|
parts.append(part)
|
||
|
return parts
|
||
|
|
||
|
|
||
|
def getCubicSuperFromParts(self, parts):
|
||
|
cbsuper = []
|
||
|
for part in parts:
|
||
|
subpath = []
|
||
|
lastPt = None
|
||
|
pt = None
|
||
|
for seg in part:
|
||
|
if(pt == None):
|
||
|
ptLeft = seg[0]
|
||
|
pt = seg[0]
|
||
|
ptRight = seg[1]
|
||
|
subpath.append([ptLeft, pt, ptRight])
|
||
|
ptLeft = seg[2]
|
||
|
pt = seg[3]
|
||
|
subpath.append([ptLeft, pt, pt])
|
||
|
cbsuper.append(subpath)
|
||
|
return cbsuper
|
||
|
|
||
|
|
||
|
def getArrangedIds(self, pathMap, startPathId):
|
||
|
nextPathId = startPathId
|
||
|
orderPathIds = [nextPathId]
|
||
|
|
||
|
#Arrange in order
|
||
|
while(len(orderPathIds) < len(pathMap)):
|
||
|
minDist = 9e+100 #A large float
|
||
|
closestId = None
|
||
|
np = pathMap[nextPathId]
|
||
|
npPts = [np[-1][-1][-1]]
|
||
|
if(len(orderPathIds) == 1):#compare both the ends for the first path
|
||
|
npPts.append(np[0][0][0])
|
||
|
|
||
|
for key in pathMap:
|
||
|
if(key in orderPathIds):
|
||
|
continue
|
||
|
parts = pathMap[key]
|
||
|
start = parts[0][0][0]
|
||
|
end = parts[-1][-1][-1]
|
||
|
|
||
|
for i, npPt in enumerate(npPts):
|
||
|
dist = abs(start[0] - npPt[0]) + abs(start[1] - npPt[1])
|
||
|
if(dist < minDist):
|
||
|
minDist = dist
|
||
|
closestId = key
|
||
|
dist = abs(end[0] - npPt[0]) + abs(end[1] - npPt[1])
|
||
|
if(dist < minDist):
|
||
|
minDist = dist
|
||
|
pathMap[key] = [[[pts for pts in reversed(seg)] for seg in \
|
||
|
reversed(part)] for part in reversed(parts)]
|
||
|
closestId = key
|
||
|
|
||
|
#If start point of the first path is closer reverse its direction
|
||
|
if(i > 0 and closestId == key):
|
||
|
pathMap[nextPathId] = [[[pts for pts in reversed(seg)] for seg in \
|
||
|
reversed(part)] for part in reversed(np)]
|
||
|
|
||
|
orderPathIds.append(closestId)
|
||
|
nextPathId = closestId
|
||
|
return orderPathIds
|
||
|
|
||
|
|
||
|
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")
|
||
|
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")
|
||
|
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("--experimentalWeights", type=inkex.Boolean, default=False, help="Mess around with algorithm")
|
||
|
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("--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 (%)")
|
||
|
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("--merge_cut_lines", type=inkex.Boolean, default=True, help="Merge cut lines")
|
||
|
pars.add_argument("--edgeStyle", help="Adjust color saturation or opacity 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("--colorCutEdges", type=Color, default='255', help="Cut edges")
|
||
|
pars.add_argument("--colorCoplanarEdges", type=Color, default='1943148287', help="Coplanar edges")
|
||
|
pars.add_argument("--colorValleyFolds", type=Color, default='3422552319', help="Valley fold edges")
|
||
|
pars.add_argument("--colorMountainFolds", type=Color, default='879076607', help="Mountain fold 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):
|
||
|
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 ONLY
|
||
|
|
||
|
fullUnfolded, unfoldedComponents = self.unfold(mesh)
|
||
|
unfoldComponentCount = len(unfoldedComponents)
|
||
|
|
||
|
#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(unfoldComponentCount))
|
||
|
|
||
|
# 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] = self.findBoundingBox(unfolding[0])
|
||
|
if boxSize > maxSize:
|
||
|
maxSize = boxSize
|
||
|
|
||
|
xSpacing = maxSize / unfoldComponentCount * 0.1 # 10% spacing between each component; calculated by max box size
|
||
|
|
||
|
#########################################################
|
||
|
# mode config for joinery:
|
||
|
#########################################################
|
||
|
if self.options.joineryMode is True:
|
||
|
self.options.separateGluePairsByColor = True #we need random colors in this mode
|
||
|
|
||
|
|
||
|
#########################################################
|
||
|
# mode config for origami simulator:
|
||
|
#########################################################
|
||
|
'''
|
||
|
required style for Origami Simulator:
|
||
|
colors:
|
||
|
- #ff0000 (red) - mountain folds
|
||
|
- #0000ff (blue) - valley folds
|
||
|
- #000000 (black) - boundary cuts (for both the outline of the pattern and any internal holes)
|
||
|
- #ffff00 (yellow) - coplonar triangle edges ("facet creases") (no support for polygons > 3 edges)
|
||
|
- #00ff00 (green) - thin slits
|
||
|
- #ff00ff (magenta) - undriven creases (swing freely)
|
||
|
|
||
|
opacity:
|
||
|
- final fold angle of a mountain or valley fold is set by its opacity. Any fold angle between 0° and 180° may be used. For example:
|
||
|
- 1.0 = 180° (fully folded)
|
||
|
- 0.5 = 90°
|
||
|
- 0 = 0° (flat)
|
||
|
'''
|
||
|
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.edgeStyle = "opacitiesForAngles" #highly important for simulation
|
||
|
self.options.dashes = False
|
||
|
self.options.printGluePairNumbers = False
|
||
|
self.options.printAngles = False
|
||
|
self.options.printLengths = False
|
||
|
self.options.importCoplanarEdges = True
|
||
|
self.options.colorCutEdges = "#000000" #black
|
||
|
self.options.colorCoplanarEdges = "#ffff00" #yellow
|
||
|
self.options.colorMountainFolds = "#ff0000" #red
|
||
|
self.options.colorValleyFolds = "#0000ff" #blue
|
||
|
|
||
|
#generate random colors; used to identify glue tab pairs
|
||
|
randomColorSet = []
|
||
|
if self.options.separateGluePairsByColor:
|
||
|
while len(randomColorSet) < len(mesh.edges()):
|
||
|
r = lambda: random.randint(0,255)
|
||
|
newColor = '#%02X%02X%02X' % (r(),r(),r())
|
||
|
if newColor not in randomColorSet:
|
||
|
randomColorSet.append(newColor)
|
||
|
|
||
|
# 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)):
|
||
|
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()
|
||
|
#as TextElement, Tspan and Circle cause wrong BBox calculation, we have to make it more complex
|
||
|
previous_bbox = inkex.BoundingBox()
|
||
|
for child in self.getElementChildren(paperfoldMainGroup[i-1]):
|
||
|
if not isinstance (child, inkex.TextElement) and \
|
||
|
not isinstance (child, inkex.Tspan) and \
|
||
|
not isinstance (child, inkex.Circle):
|
||
|
transform = inkex.Transform()
|
||
|
parent = child.getparent()
|
||
|
if parent is not None and isinstance(parent, inkex.ShapeElement):
|
||
|
transform = parent.composed_transform()
|
||
|
previous_bbox += child.bounding_box(transform)
|
||
|
|
||
|
#this_bbox = paperfoldPageGroup.bounding_box()
|
||
|
this_bbox = inkex.BoundingBox()
|
||
|
for child in self.getElementChildren(paperfoldPageGroup):
|
||
|
#as TextElement, Tspan and Circle cause wrong BBox calculation, we have to make it more complex
|
||
|
if not isinstance (child, inkex.TextElement) and \
|
||
|
not isinstance (child, inkex.Tspan) and \
|
||
|
not isinstance (child, inkex.Circle):
|
||
|
transform = inkex.Transform()
|
||
|
parent = child.getparent()
|
||
|
if parent is not None and isinstance(parent, inkex.ShapeElement):
|
||
|
transform = parent.composed_transform()
|
||
|
this_bbox += child.bounding_box(transform)
|
||
|
|
||
|
#self.msg(previous_bbox)
|
||
|
#self.msg(this_bbox)
|
||
|
paperfoldPageGroup.set("transform", "translate({:0.6f}, 0.0)".format(previous_bbox.left + previous_bbox.width - this_bbox.left + xSpacing))
|
||
|
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'))
|
||
|
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', "{:0.6f}{}".format(bbox.width + 2 * offset, self.svg.unit))
|
||
|
root.set('height', "{:0.6f}{}".format(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__':
|
||
|
Paperfold().run()
|