huge update for paperfold
This commit is contained in:
parent
8c8614e4f8
commit
9d6d01f845
@ -75,7 +75,7 @@ class ConvexHull(inkex.EffectExtension):
|
|||||||
line_attribs['transform'] = cloneTransform
|
line_attribs['transform'] = cloneTransform
|
||||||
etree.SubElement(g, inkex.addNS('path', 'svg' ), line_attribs)
|
etree.SubElement(g, inkex.addNS('path', 'svg' ), line_attribs)
|
||||||
|
|
||||||
def getControlPoints(self, element, n_array = None): #this does the same as "CTRL + SHIFT + K"
|
def getControlPoints(self, element, n_array = None):
|
||||||
if n_array == None:
|
if n_array == None:
|
||||||
n_array = []
|
n_array = []
|
||||||
if element.tag == inkex.addNS('path','svg'):
|
if element.tag == inkex.addNS('path','svg'):
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
<param name="inputfile" type="path" gui-text="Input File" filetypes="obj,off,ply,stl" mode="file" gui-description="The model to unfold. You can use obj files provided in extensions dir of Inkscape \Poly3DObjects\*.obj to play around">/your/beautiful/3dmodel/file</param>
|
<param name="inputfile" type="path" gui-text="Input File" filetypes="obj,off,ply,stl" mode="file" gui-description="The model to unfold. You can use obj files provided in extensions dir of Inkscape \Poly3DObjects\*.obj to play around">/your/beautiful/3dmodel/file</param>
|
||||||
<param name="maxNumFaces" type="int" min="1" max="99999" gui-text="Maximum allowed faces" gui-description="If the STL file has too much detail it contains a large number of faces. This will make unfolding extremely slow. So we can limit it.">200</param>
|
<param name="maxNumFaces" type="int" min="1" max="99999" gui-text="Maximum allowed faces" gui-description="If the STL file has too much detail it contains a large number of faces. This will make unfolding extremely slow. So we can limit it.">200</param>
|
||||||
<param name="scalefactor" type="float" precision="3" min="0.0001" max="10000.0" gui-text="Manual scale factor" gui-description="default is 1.0">1.0</param>
|
<param name="scalefactor" type="float" precision="3" min="0.0001" max="10000.0" gui-text="Manual scale factor" gui-description="default is 1.0">1.0</param>
|
||||||
|
<param name="roundingDigits" type="int" min="0" max="16" gui-text="Digits for rounding" gui-description="Controls how (nearly) coplanar lines are handled.">3</param>
|
||||||
<separator/>
|
<separator/>
|
||||||
<hbox>
|
<hbox>
|
||||||
<vbox>
|
<vbox>
|
||||||
@ -27,6 +28,8 @@
|
|||||||
<option value="pt">pt</option>
|
<option value="pt">pt</option>
|
||||||
<option value="px">px</option>
|
<option value="px">px</option>
|
||||||
</param>
|
</param>
|
||||||
|
<param name="writeTwoDSTL" type="bool" gui-text="Write 2D STL unfoldings">false</param>
|
||||||
|
<param name="TwoDSTLdir" type="path" mode="folder" gui-text="Location to save exported 2D STL">./inkscape_export/</param>
|
||||||
</vbox>
|
</vbox>
|
||||||
<separator/>
|
<separator/>
|
||||||
<vbox>
|
<vbox>
|
||||||
@ -35,7 +38,7 @@
|
|||||||
<param name="flipLabels" type="bool" gui-text="Flip labels">false</param>
|
<param name="flipLabels" type="bool" gui-text="Flip labels">false</param>
|
||||||
<param name="dashes" type="bool" gui-text="Dashes for cut/coplanar lines">true</param>
|
<param name="dashes" type="bool" gui-text="Dashes for cut/coplanar lines">true</param>
|
||||||
<param name="saturationsForAngles" type="bool" gui-text="Adjust color saturation for folding edges" gui-description="The larger the angle the darker the color">false</param>
|
<param name="saturationsForAngles" type="bool" gui-text="Adjust color saturation for folding edges" gui-description="The larger the angle the darker the color">false</param>
|
||||||
<param name="separateGluePairsByColor" type="bool" gui-text="Separate glue pairs by color">false</param>
|
<param name="separateGluePairsByColor" type="bool" gui-text="Separate glue tab pairs by color" gui-description="Generates random color sets for glue tab pairs">false</param>
|
||||||
<param name="colorValleyCut" type="color" appearance="colorbutton" gui-text="Valley cut edges">255</param>
|
<param name="colorValleyCut" type="color" appearance="colorbutton" gui-text="Valley cut edges">255</param>
|
||||||
<param name="colorMountainCut" type="color" appearance="colorbutton" gui-text="Mountain cut edges">1968208895</param>
|
<param name="colorMountainCut" type="color" appearance="colorbutton" gui-text="Mountain cut edges">1968208895</param>
|
||||||
<param name="colorCoplanarEdges" type="color" appearance="colorbutton" gui-text="Coplanar edges">1943148287</param>
|
<param name="colorCoplanarEdges" type="color" appearance="colorbutton" gui-text="Coplanar edges">1943148287</param>
|
||||||
@ -43,9 +46,17 @@
|
|||||||
<param name="colorMountainPerforates" type="color" appearance="colorbutton" gui-text="Mountain perforation edges">879076607</param>
|
<param name="colorMountainPerforates" type="color" appearance="colorbutton" gui-text="Mountain perforation edges">879076607</param>
|
||||||
</vbox>
|
</vbox>
|
||||||
</hbox>
|
</hbox>
|
||||||
<separator/>
|
</page>
|
||||||
<label appearance="header">Post Processing</label>
|
<page name="tab_postprocessing" gui-text="Post Processing">
|
||||||
|
<label appearance="header">Joinery</label>
|
||||||
|
<label>Joinery only works on ungrouped paths.</label>
|
||||||
|
<param name="joineryMode" type="bool" gui-text="Enable joinery mode" gui-description="Makes flat file instead creating groups. Guarantees compability for joinery.">false</param>
|
||||||
<label appearance="url">https://clementzheng.github.io/joinery</label>
|
<label appearance="url">https://clementzheng.github.io/joinery</label>
|
||||||
|
<label appearance="url">https://www.instructables.com/Joinery-Joints-for-Laser-Cut-Assemblies</label>
|
||||||
|
<label appearance="header">Origami Simulator</label>
|
||||||
|
<label appearance="url">https://origamisimulator.org</label>
|
||||||
|
<label appearance="url">https://github.com/amandaghassaei/OrigamiSimulator</label>
|
||||||
|
<param name="origamiSimulatorMode" type="bool" gui-text="Enable origami simulator mode" gui-description="Overwrites styles to be compatible to origami simulator.">false</param>
|
||||||
</page>
|
</page>
|
||||||
<page name="tab_about" gui-text="About">
|
<page name="tab_about" gui-text="About">
|
||||||
<label appearance="header">Paperfold for Inkscape</label>
|
<label appearance="header">Paperfold for Inkscape</label>
|
||||||
|
@ -19,7 +19,7 @@ Paperfold is another flattener for triangle mesh files, heavily based on paperfo
|
|||||||
Author: Mario Voigt / FabLab Chemnitz
|
Author: Mario Voigt / FabLab Chemnitz
|
||||||
Mail: mario.voigt@stadtfabrikanten.org
|
Mail: mario.voigt@stadtfabrikanten.org
|
||||||
Date: 13.09.2020
|
Date: 13.09.2020
|
||||||
Last patch: 08.05.2021
|
Last patch: 10.05.2021
|
||||||
License: GNU GPL v3
|
License: GNU GPL v3
|
||||||
|
|
||||||
To run this you need to install OpenMesh with python pip.
|
To run this you need to install OpenMesh with python pip.
|
||||||
@ -38,17 +38,18 @@ possible import file types -> https://www.graphics.rwth-aachen.de/media/openmesh
|
|||||||
|
|
||||||
todo:
|
todo:
|
||||||
- debug coplanar color for edges for some cases
|
- debug coplanar color for edges for some cases
|
||||||
- remove empty groups (text)
|
|
||||||
- abort if 0 faces
|
|
||||||
- give hints for joinery preparations (apply transform, ungroup, ...)
|
|
||||||
- update documentation accordingly
|
|
||||||
- make angleRange global for complete unfolding (to match glue pairs between multiple unfoldings)
|
- make angleRange global for complete unfolding (to match glue pairs between multiple unfoldings)
|
||||||
- add angleRange to stats
|
- option to render all triangles in a detached way (overlapping lines/independent) + merge coplanar adjacent triangles to polygons
|
||||||
|
- write tab and slot generator (like joinery/polyhedra extension)
|
||||||
|
- fstl preview
|
||||||
|
- origami simulator docu + add support for opacities
|
||||||
|
- fix line: dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight) # #might fail without throwing any error ...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Unfold(inkex.EffectExtension):
|
||||||
|
|
||||||
# Compute the third point of a triangle when two points and all edge lengths are given
|
# Compute the third point of a triangle when two points and all edge lengths are given
|
||||||
def getThirdPoint(v0, v1, l01, l12, l20):
|
def getThirdPoint(self, v0, v1, l01, l12, l20):
|
||||||
v2rotx = (l01 ** 2 + l20 ** 2 - l12 ** 2) / (2 * l01)
|
v2rotx = (l01 ** 2 + l20 ** 2 - l12 ** 2) / (2 * l01)
|
||||||
v2roty0 = np.sqrt((l01 + l20 + l12) * (l01 + l20 - l12) * (l01 - l20 + l12) * (-l01 + l20 + l12)) / (2 * l01)
|
v2roty0 = np.sqrt((l01 + l20 + l12) * (l01 + l20 - l12) * (l01 - l20 + l12) * (-l01 + l20 + l12)) / (2 * l01)
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ def getThirdPoint(v0, v1, l01, l12, l20):
|
|||||||
|
|
||||||
|
|
||||||
# Check if two lines intersect
|
# Check if two lines intersect
|
||||||
def lineIntersection(v1, v2, v3, v4, epsilon):
|
def lineIntersection(self, v1, v2, v3, v4, epsilon):
|
||||||
d = (v4[1] - v3[1]) * (v2[0] - v1[0]) - (v4[0] - v3[0]) * (v2[1] - v1[1])
|
d = (v4[1] - v3[1]) * (v2[0] - v1[0]) - (v4[0] - v3[0]) * (v2[1] - v1[1])
|
||||||
u = (v4[0] - v3[0]) * (v1[1] - v3[1]) - (v4[1] - v3[1]) * (v1[0] - v3[0])
|
u = (v4[0] - v3[0]) * (v1[1] - v3[1]) - (v4[1] - v3[1]) * (v1[0] - v3[0])
|
||||||
v = (v2[0] - v1[0]) * (v1[1] - v3[1]) - (v2[1] - v1[1]) * (v1[0] - v3[0])
|
v = (v2[0] - v1[0]) * (v1[1] - v3[1]) - (v2[1] - v1[1]) * (v1[0] - v3[0])
|
||||||
@ -73,7 +74,7 @@ def lineIntersection(v1, v2, v3, v4, epsilon):
|
|||||||
return ((0 + epsilon) <= u <= (d - epsilon)) and ((0 + epsilon) <= v <= (d - epsilon))
|
return ((0 + epsilon) <= u <= (d - epsilon)) and ((0 + epsilon) <= v <= (d - epsilon))
|
||||||
|
|
||||||
# Check if a point lies inside a triangle
|
# Check if a point lies inside a triangle
|
||||||
def pointInTriangle(A, B, C, P, epsilon):
|
def pointInTriangle(self, A, B, C, P, epsilon):
|
||||||
v0 = [C[0] - A[0], C[1] - A[1]]
|
v0 = [C[0] - A[0], C[1] - A[1]]
|
||||||
v1 = [B[0] - A[0], B[1] - A[1]]
|
v1 = [B[0] - A[0], B[1] - A[1]]
|
||||||
v2 = [P[0] - A[0], P[1] - A[1]]
|
v2 = [P[0] - A[0], P[1] - A[1]]
|
||||||
@ -87,38 +88,39 @@ def pointInTriangle(A, B, C, P, epsilon):
|
|||||||
|
|
||||||
|
|
||||||
# Check if two triangles intersect
|
# Check if two triangles intersect
|
||||||
def triangleIntersection(t1, t2, epsilon):
|
def triangleIntersection(self, t1, t2, epsilon):
|
||||||
if lineIntersection(t1[0], t1[1], t2[0], t2[1], epsilon): return True
|
if self.lineIntersection(t1[0], t1[1], t2[0], t2[1], epsilon): return True
|
||||||
if lineIntersection(t1[0], t1[1], t2[0], t2[2], epsilon): return True
|
if self.lineIntersection(t1[0], t1[1], t2[0], t2[2], epsilon): return True
|
||||||
if lineIntersection(t1[0], t1[1], t2[1], t2[2], epsilon): return True
|
if self.lineIntersection(t1[0], t1[1], t2[1], t2[2], epsilon): return True
|
||||||
if lineIntersection(t1[0], t1[2], t2[0], t2[1], epsilon): return True
|
if self.lineIntersection(t1[0], t1[2], t2[0], t2[1], epsilon): return True
|
||||||
if lineIntersection(t1[0], t1[2], t2[0], t2[2], epsilon): return True
|
if self.lineIntersection(t1[0], t1[2], t2[0], t2[2], epsilon): return True
|
||||||
if lineIntersection(t1[0], t1[2], t2[1], t2[2], epsilon): return True
|
if self.lineIntersection(t1[0], t1[2], t2[1], t2[2], epsilon): return True
|
||||||
if lineIntersection(t1[1], t1[2], t2[0], t2[1], epsilon): return True
|
if self.lineIntersection(t1[1], t1[2], t2[0], t2[1], epsilon): return True
|
||||||
if lineIntersection(t1[1], t1[2], t2[0], t2[2], epsilon): return True
|
if self.lineIntersection(t1[1], t1[2], t2[0], t2[2], epsilon): return True
|
||||||
if lineIntersection(t1[1], t1[2], t2[1], t2[2], epsilon): return True
|
if self.lineIntersection(t1[1], t1[2], t2[1], t2[2], epsilon): return True
|
||||||
inTri = True
|
inTri = True
|
||||||
inTri = inTri and pointInTriangle(t1[0], t1[1], t1[2], t2[0], epsilon)
|
inTri = inTri and self.pointInTriangle(t1[0], t1[1], t1[2], t2[0], epsilon)
|
||||||
inTri = inTri and pointInTriangle(t1[0], t1[1], t1[2], t2[1], epsilon)
|
inTri = inTri and self.pointInTriangle(t1[0], t1[1], t1[2], t2[1], epsilon)
|
||||||
inTri = inTri and pointInTriangle(t1[0], t1[1], t1[2], t2[2], epsilon)
|
inTri = inTri and self.pointInTriangle(t1[0], t1[1], t1[2], t2[2], epsilon)
|
||||||
if inTri == True: return True
|
if inTri == True: return True
|
||||||
inTri = True
|
inTri = True
|
||||||
inTri = inTri and pointInTriangle(t2[0], t2[1], t2[2], t1[0], epsilon)
|
inTri = inTri and self.pointInTriangle(t2[0], t2[1], t2[2], t1[0], epsilon)
|
||||||
inTri = inTri and pointInTriangle(t2[0], t2[1], t2[2], t1[1], epsilon)
|
inTri = inTri and self.pointInTriangle(t2[0], t2[1], t2[2], t1[1], epsilon)
|
||||||
inTri = inTri and pointInTriangle(t2[0], t2[1], t2[2], t1[2], epsilon)
|
inTri = inTri and self.pointInTriangle(t2[0], t2[1], t2[2], t1[2], epsilon)
|
||||||
if inTri == True: return True
|
if inTri == True: return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Functions for visualisation and output
|
# Functions for visualisation and output
|
||||||
def addVisualisationData(mesh, unfoldedMesh, originalHalfedges, unfoldedHalfedges, glueNumber, foldingDirection):
|
def addVisualisationData(self, mesh, unfoldedMesh, originalHalfedges, unfoldedHalfedges, glueNumber, foldingDirection):
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
foldingDirection[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = round(math.degrees(mesh.calc_dihedral_angle(originalHalfedges[i])), 3)
|
foldingDirection[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = round(math.degrees(mesh.calc_dihedral_angle(originalHalfedges[i])), self.options.roundingDigits)
|
||||||
# Information, which edges belong together
|
# Information, which edges belong together
|
||||||
glueNumber[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = mesh.edge_handle(originalHalfedges[i]).idx()
|
glueNumber[unfoldedMesh.edge_handle(unfoldedHalfedges[i]).idx()] = mesh.edge_handle(originalHalfedges[i]).idx()
|
||||||
|
|
||||||
# Function that unwinds a spanning tree
|
# Function that unwinds a spanning tree
|
||||||
def unfoldSpanningTree(mesh, spanningTree):
|
def unfoldSpanningTree(self, mesh, spanningTree):
|
||||||
|
try:
|
||||||
unfoldedMesh = om.TriMesh() # the unfolded mesh
|
unfoldedMesh = om.TriMesh() # the unfolded mesh
|
||||||
|
|
||||||
numFaces = mesh.n_faces()
|
numFaces = mesh.n_faces()
|
||||||
@ -152,7 +154,7 @@ def unfoldSpanningTree(mesh, spanningTree):
|
|||||||
secondUnfoldedPoint = np.array([edgelengths[0], 0, 0])
|
secondUnfoldedPoint = np.array([edgelengths[0], 0, 0])
|
||||||
|
|
||||||
# We calculate the third point of the triangle from the first two. There are two possibilities
|
# We calculate the third point of the triangle from the first two. There are two possibilities
|
||||||
[thirdUnfolded0, thirdUnfolded1] = getThirdPoint(firstUnfoldedPoint, secondUnfoldedPoint, edgelengths[0],
|
[thirdUnfolded0, thirdUnfolded1] = self.getThirdPoint(firstUnfoldedPoint, secondUnfoldedPoint, edgelengths[0],
|
||||||
edgelengths[1],
|
edgelengths[1],
|
||||||
edgelengths[2])
|
edgelengths[2])
|
||||||
if thirdUnfolded0[1] > 0:
|
if thirdUnfolded0[1] > 0:
|
||||||
@ -183,7 +185,7 @@ def unfoldSpanningTree(mesh, spanningTree):
|
|||||||
# Associated triangle in 3D mesh
|
# Associated triangle in 3D mesh
|
||||||
connections[unfoldedFace.idx()] = startingTriangle.idx()
|
connections[unfoldedFace.idx()] = startingTriangle.idx()
|
||||||
# Folding direction and adhesive number
|
# Folding direction and adhesive number
|
||||||
addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, foldingDirection)
|
self.addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, foldingDirection)
|
||||||
|
|
||||||
halfEdgeConnections = {firstHalfEdge.idx(): firstUnfoldedHalfEdge.idx(),
|
halfEdgeConnections = {firstHalfEdge.idx(): firstUnfoldedHalfEdge.idx(),
|
||||||
secondHalfEdge.idx(): secondUnfoldedHalfEdge.idx(),
|
secondHalfEdge.idx(): secondUnfoldedHalfEdge.idx(),
|
||||||
@ -221,7 +223,7 @@ def unfoldSpanningTree(mesh, spanningTree):
|
|||||||
mesh.calc_edge_length(thirdHalfEdgeInFace)]
|
mesh.calc_edge_length(thirdHalfEdgeInFace)]
|
||||||
|
|
||||||
# We calculate the two possibilities for the third point in the triangle
|
# We calculate the two possibilities for the third point in the triangle
|
||||||
[newUnfoldedVertex0, newUnfoldedVertex1] = getThirdPoint(unfoldedMesh.point(unfoldedFromVertex),
|
[newUnfoldedVertex0, newUnfoldedVertex1] = self.getThirdPoint(unfoldedMesh.point(unfoldedFromVertex),
|
||||||
unfoldedMesh.point(unfoldedToVertex), edgelengths[0],
|
unfoldedMesh.point(unfoldedToVertex), edgelengths[0],
|
||||||
edgelengths[1], edgelengths[2])
|
edgelengths[1], edgelengths[2])
|
||||||
|
|
||||||
@ -236,12 +238,12 @@ def unfoldSpanningTree(mesh, spanningTree):
|
|||||||
unfoldedHalfEdges = [unfoldedLastHalfEdge, secondUnfoldedHalfEdge, thirdUnfoldedHalfEdge]
|
unfoldedHalfEdges = [unfoldedLastHalfEdge, secondUnfoldedHalfEdge, thirdUnfoldedHalfEdge]
|
||||||
|
|
||||||
# Saving the information about edges and page
|
# Saving the information about edges and page
|
||||||
# Dotted line in the output
|
# Dotted one's in the output
|
||||||
unfoldedLastEdge = unfoldedMesh.edge_handle(unfoldedLastHalfEdge)
|
unfoldedLastEdge = unfoldedMesh.edge_handle(unfoldedLastHalfEdge)
|
||||||
isFoldingEdge[unfoldedLastEdge.idx()] = True
|
isFoldingEdge[unfoldedLastEdge.idx()] = True
|
||||||
|
|
||||||
# Gluing number and folding direction
|
# Gluing number and folding direction
|
||||||
addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, foldingDirection)
|
self.addVisualisationData(mesh, unfoldedMesh, originalHalfEdges, unfoldedHalfEdges, glueNumber, foldingDirection)
|
||||||
|
|
||||||
# Related page
|
# Related page
|
||||||
connections[newface.idx()] = dualEdge[1]
|
connections[newface.idx()] = dualEdge[1]
|
||||||
@ -251,25 +253,34 @@ def unfoldSpanningTree(mesh, spanningTree):
|
|||||||
halfEdgeConnections[originalHalfEdges[i].idx()] = unfoldedHalfEdges[i].idx()
|
halfEdgeConnections[originalHalfEdges[i].idx()] = unfoldedHalfEdges[i].idx()
|
||||||
|
|
||||||
return [unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection]
|
return [unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection]
|
||||||
|
except Exception as e:
|
||||||
|
inkex.utils.debug(str(e))
|
||||||
|
inkex.utils.debug("Error: model could not be unfolded. Check for:")
|
||||||
|
inkex.utils.debug(" - watertight model / intact hull")
|
||||||
|
inkex.utils.debug(" - duplicated edges or faces")
|
||||||
|
inkex.utils.debug(" - detached faces or holes")
|
||||||
|
inkex.utils.debug(" - missing units")
|
||||||
|
inkex.utils.debug(" - missing coordinate system")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
def unfold(mesh, maxNumFaces, printStats):
|
|
||||||
|
def unfold(self, mesh):
|
||||||
# Calculate the number of surfaces, edges and corners, as well as the length of the longest shortest edge
|
# Calculate the number of surfaces, edges and corners, as well as the length of the longest shortest edge
|
||||||
numEdges = mesh.n_edges()
|
numEdges = mesh.n_edges()
|
||||||
numVertices = mesh.n_vertices()
|
numVertices = mesh.n_vertices()
|
||||||
numFaces = mesh.n_faces()
|
numFaces = mesh.n_faces()
|
||||||
|
|
||||||
if numFaces > maxNumFaces:
|
if numFaces > self.options.maxNumFaces:
|
||||||
inkex.utils.debug("Aborted. Target STL file has " + str(numFaces) + " faces, but " + str(maxNumFaces) + " are allowed.")
|
inkex.utils.debug("Aborted. Target STL file has " + str(numFaces) + " faces, but only " + str(maxNumFaces) + " are allowed.")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if printStats is True:
|
if self.options.printStats is True:
|
||||||
inkex.utils.debug("Input STL mesh stats:")
|
inkex.utils.debug("Input STL mesh stats:")
|
||||||
inkex.utils.debug("* Number of edges: " + str(numEdges))
|
inkex.utils.debug("* Number of edges: " + str(numEdges))
|
||||||
inkex.utils.debug("* Number of vertices: " + str(numVertices))
|
inkex.utils.debug("* Number of vertices: " + str(numVertices))
|
||||||
inkex.utils.debug("* Number of faces: " + str(numFaces))
|
inkex.utils.debug("* Number of faces: " + str(numFaces))
|
||||||
inkex.utils.debug("-----------------------------------------------------------")
|
inkex.utils.debug("-----------------------------------------------------------")
|
||||||
|
|
||||||
|
|
||||||
# Generate the dual graph of the mesh and calculate the weights
|
# Generate the dual graph of the mesh and calculate the weights
|
||||||
dualGraph = nx.Graph()
|
dualGraph = nx.Graph()
|
||||||
|
|
||||||
@ -285,12 +296,15 @@ def unfold(mesh, maxNumFaces, printStats):
|
|||||||
|
|
||||||
# All edges in the net
|
# All edges in the net
|
||||||
for edge in mesh.edges():
|
for edge in mesh.edges():
|
||||||
|
#inkex.utils.debug("edge.idx = " + str(edge.idx()))
|
||||||
|
|
||||||
# The two sides adjacent to the edge
|
# The two sides adjacent to the edge
|
||||||
face1 = mesh.face_handle(mesh.halfedge_handle(edge, 0))
|
face1 = mesh.face_handle(mesh.halfedge_handle(edge, 0))
|
||||||
face2 = mesh.face_handle(mesh.halfedge_handle(edge, 1))
|
face2 = mesh.face_handle(mesh.halfedge_handle(edge, 1))
|
||||||
|
|
||||||
# The weight
|
# The weight
|
||||||
edgeweight = 1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength)
|
edgeweight = 1.0 - (mesh.calc_edge_length(edge) - minLength) / (maxLength - minLength)
|
||||||
|
#inkex.utils.debug("edgeweight = " + str(edgeweight))
|
||||||
|
|
||||||
# Calculate the centres of the pages (only necessary for visualisation)
|
# Calculate the centres of the pages (only necessary for visualisation)
|
||||||
center1 = (0, 0)
|
center1 = (0, 0)
|
||||||
@ -303,13 +317,14 @@ def unfold(mesh, maxNumFaces, printStats):
|
|||||||
# Add the new nodes and edge to the dual graph
|
# Add the new nodes and edge to the dual graph
|
||||||
dualGraph.add_node(face1.idx(), pos=center1)
|
dualGraph.add_node(face1.idx(), pos=center1)
|
||||||
dualGraph.add_node(face2.idx(), pos=center2)
|
dualGraph.add_node(face2.idx(), pos=center2)
|
||||||
dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight)
|
dualGraph.add_edge(face1.idx(), face2.idx(), idx=edge.idx(), weight=edgeweight) # #might fail without throwing any error ...
|
||||||
|
|
||||||
|
|
||||||
# Calculate the minimum spanning tree
|
# Calculate the minimum spanning tree
|
||||||
spanningTree = nx.minimum_spanning_tree(dualGraph)
|
spanningTree = nx.minimum_spanning_tree(dualGraph)
|
||||||
|
|
||||||
# Unfold the tree
|
# Unfold the tree
|
||||||
fullUnfolding = unfoldSpanningTree(mesh, spanningTree)
|
fullUnfolding = self.unfoldSpanningTree(mesh, spanningTree)
|
||||||
[unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection] = fullUnfolding
|
[unfoldedMesh, isFoldingEdge, connections, glueNumber, foldingDirection] = fullUnfolding
|
||||||
|
|
||||||
|
|
||||||
@ -327,7 +342,7 @@ def unfold(mesh, maxNumFaces, printStats):
|
|||||||
triangle1.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge)))
|
triangle1.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge)))
|
||||||
for halfedge in unfoldedMesh.fh(face2):
|
for halfedge in unfoldedMesh.fh(face2):
|
||||||
triangle2.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge)))
|
triangle2.append(unfoldedMesh.point(unfoldedMesh.from_vertex_handle(halfedge)))
|
||||||
if triangleIntersection(triangle1, triangle2, epsilon):
|
if self.triangleIntersection(triangle1, triangle2, epsilon):
|
||||||
faceIntersections.append([connections[face1.idx()], connections[face2.idx()]])
|
faceIntersections.append([connections[face1.idx()], connections[face2.idx()]])
|
||||||
|
|
||||||
# Find the paths
|
# Find the paths
|
||||||
@ -397,12 +412,13 @@ def unfold(mesh, maxNumFaces, printStats):
|
|||||||
# Unfolding of the components
|
# Unfolding of the components
|
||||||
unfoldings = []
|
unfoldings = []
|
||||||
for component in connectedComponentList:
|
for component in connectedComponentList:
|
||||||
unfoldings.append(unfoldSpanningTree(mesh, spanningTree.subgraph(component)))
|
unfoldings.append(self.unfoldSpanningTree(mesh, spanningTree.subgraph(component)))
|
||||||
|
|
||||||
|
|
||||||
return fullUnfolding, unfoldings
|
return fullUnfolding, unfoldings
|
||||||
|
|
||||||
|
|
||||||
def findBoundingBox(mesh):
|
def findBoundingBox(self, mesh):
|
||||||
firstpoint = mesh.point(mesh.vertex_handle(0))
|
firstpoint = mesh.point(mesh.vertex_handle(0))
|
||||||
xmin = firstpoint[0]
|
xmin = firstpoint[0]
|
||||||
xmax = firstpoint[0]
|
xmax = firstpoint[0]
|
||||||
@ -433,12 +449,12 @@ def writeSVG(self, unfolding, size, randomColorSet):
|
|||||||
gluePairs = 0
|
gluePairs = 0
|
||||||
mountainCuts = 0
|
mountainCuts = 0
|
||||||
valleyCuts = 0
|
valleyCuts = 0
|
||||||
coplanarLines = 0
|
coplanarEdges = 0
|
||||||
mountainPerforations = 0
|
mountainPerforations = 0
|
||||||
valleyPerforations = 0
|
valleyPerforations = 0
|
||||||
|
|
||||||
# Calculate the bounding box
|
# Calculate the bounding box
|
||||||
[xmin, ymin, boxSize] = findBoundingBox(unfolding[0])
|
[xmin, ymin, boxSize] = self.findBoundingBox(unfolding[0])
|
||||||
|
|
||||||
if size > 0:
|
if size > 0:
|
||||||
boxSize = size
|
boxSize = size
|
||||||
@ -472,6 +488,15 @@ def writeSVG(self, unfolding, size, randomColorSet):
|
|||||||
textGroup.add(textFacesGroup)
|
textGroup.add(textFacesGroup)
|
||||||
textGroup.add(textEdgesGroup)
|
textGroup.add(textEdgesGroup)
|
||||||
|
|
||||||
|
#we could write the unfolded mesh as a 2D stl file to disk if we like:
|
||||||
|
if self.options.writeTwoDSTL is True:
|
||||||
|
if not os.path.exists(self.options.TwoDSTLdir):
|
||||||
|
inkex.utils.debug("Export location for 2D STL unfoldings does not exist. Please select a another dir and try again.")
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
om.write_mesh(os.path.join(self.options.TwoDSTLdir, uniqueMainId + "-paperfold-page.stl"), mesh)
|
||||||
|
|
||||||
|
|
||||||
if self.options.printTriangleNumbers is True:
|
if self.options.printTriangleNumbers is True:
|
||||||
|
|
||||||
for face in mesh.faces():
|
for face in mesh.faces():
|
||||||
@ -524,7 +549,7 @@ def writeSVG(self, unfolding, size, randomColorSet):
|
|||||||
line.set("id", uniqueMainId + "-coplanar-edge-" + str(edge.idx()))
|
line.set("id", uniqueMainId + "-coplanar-edge-" + str(edge.idx()))
|
||||||
#if self.options.importCoplanarEdges is False:
|
#if self.options.importCoplanarEdges is False:
|
||||||
# line.delete()
|
# line.delete()
|
||||||
coplanarLines += 1
|
coplanarEdges += 1
|
||||||
|
|
||||||
lineStyle.update({"stroke-width":str(strokewidth)})
|
lineStyle.update({"stroke-width":str(strokewidth)})
|
||||||
lineStyle.update({"stroke-linecap":"butt"})
|
lineStyle.update({"stroke-linecap":"butt"})
|
||||||
@ -614,30 +639,32 @@ def writeSVG(self, unfolding, size, randomColorSet):
|
|||||||
tspanText.append("{:0.2f} {}".format(self.options.scalefactor * math.hypot(vertex1[0] - vertex0[0], vertex1[1] - vertex0[1]), unitToPrint))
|
tspanText.append("{:0.2f} {}".format(self.options.scalefactor * math.hypot(vertex1[0] - vertex0[0], vertex1[1] - vertex0[1]), unitToPrint))
|
||||||
tspan.text = " | ".join(tspanText)
|
tspan.text = " | ".join(tspanText)
|
||||||
|
|
||||||
if (self.options.printGluePairNumbers is False and self.options.printAngles is False and self.options.printLengths is False) or self.options.importCoplanarEdges is False and dihedralAngle == 0:
|
if tspan.text == "": #if no text we remove again to clean up
|
||||||
text.delete()
|
text.delete()
|
||||||
tspan.delete()
|
tspan.delete()
|
||||||
|
|
||||||
#delete unrequired groups if no text labels
|
if len(textFacesGroup) == 0:
|
||||||
if (self.options.printGluePairNumbers is False and self.options.printAngles is False and self.options.printLengths is False) or self.options.importCoplanarEdges is False and dihedralAngle == 0:
|
textFacesGroup.delete() #delete if empty set
|
||||||
textGroup.delete()
|
|
||||||
textFacesGroup.delete()
|
if len(textFacesGroup) == 0:
|
||||||
textEdgesGroup.delete()
|
textEdgesGroup.delete() #delete if empty set
|
||||||
|
|
||||||
|
if len(textGroup) == 0:
|
||||||
|
textGroup.delete() #delete if empty set
|
||||||
|
|
||||||
if self.options.printStats is True:
|
if self.options.printStats is True:
|
||||||
inkex.utils.debug("Folding edges stats:")
|
|
||||||
inkex.utils.debug(" * Number of mountain cuts: " + str(mountainCuts))
|
inkex.utils.debug(" * Number of mountain cuts: " + str(mountainCuts))
|
||||||
inkex.utils.debug(" * Number of valley cuts: " + str(valleyCuts))
|
inkex.utils.debug(" * Number of valley cuts: " + str(valleyCuts))
|
||||||
inkex.utils.debug(" * Number of coplanar lines: " + str(coplanarLines))
|
inkex.utils.debug(" * Number of coplanar edges: " + str(coplanarEdges))
|
||||||
inkex.utils.debug(" * Number of mountain perforations: " + str(mountainPerforations))
|
inkex.utils.debug(" * Number of mountain perforations: " + str(mountainPerforations))
|
||||||
inkex.utils.debug(" * Number of valley perforations: " + str(valleyPerforations))
|
inkex.utils.debug(" * Number of valley perforations: " + str(valleyPerforations))
|
||||||
inkex.utils.debug("-----------------------------------------------------------")
|
inkex.utils.debug(" * Number of glue pairs: {:0.0f}".format(gluePairs / 2))
|
||||||
inkex.utils.debug("Number of glue pairs: {:0.0f}".format(gluePairs / 2))
|
inkex.utils.debug(" * min angle: {:0.2f}".format(minAngle))
|
||||||
|
inkex.utils.debug(" * max angle: {:0.2f}".format(maxAngle))
|
||||||
|
inkex.utils.debug(" * Edge angle range: {:0.2f}".format(angleRange))
|
||||||
|
|
||||||
return paperfoldPageGroup
|
return paperfoldPageGroup
|
||||||
|
|
||||||
class Unfold(inkex.EffectExtension):
|
|
||||||
|
|
||||||
def add_arguments(self, pars):
|
def add_arguments(self, pars):
|
||||||
pars.add_argument("--tab")
|
pars.add_argument("--tab")
|
||||||
|
|
||||||
@ -645,6 +672,7 @@ class Unfold(inkex.EffectExtension):
|
|||||||
pars.add_argument("--inputfile")
|
pars.add_argument("--inputfile")
|
||||||
pars.add_argument("--maxNumFaces", type=int, default=200, help="If the STL file has too much detail it contains a large number of faces. This will make unfolding extremely slow. So we can limit it.")
|
pars.add_argument("--maxNumFaces", type=int, default=200, help="If the STL file has too much detail it contains a large number of faces. This will make unfolding extremely slow. So we can limit it.")
|
||||||
pars.add_argument("--scalefactor", type=float, default=1.0, help="Manual scale factor")
|
pars.add_argument("--scalefactor", type=float, default=1.0, help="Manual scale factor")
|
||||||
|
pars.add_argument("--roundingDigits", type=int, default=3, help="Digits for rounding")
|
||||||
|
|
||||||
#Output
|
#Output
|
||||||
pars.add_argument("--printGluePairNumbers", type=inkex.Boolean, default=False, help="Print glue pair numbers on cut edges")
|
pars.add_argument("--printGluePairNumbers", type=inkex.Boolean, default=False, help="Print glue pair numbers on cut edges")
|
||||||
@ -652,10 +680,12 @@ class Unfold(inkex.EffectExtension):
|
|||||||
pars.add_argument("--printLengths", type=inkex.Boolean, default=False, help="Print lengths on edges")
|
pars.add_argument("--printLengths", type=inkex.Boolean, default=False, help="Print lengths on edges")
|
||||||
pars.add_argument("--printTriangleNumbers", type=inkex.Boolean, default=False, help="Print triangle numbers on faces")
|
pars.add_argument("--printTriangleNumbers", type=inkex.Boolean, default=False, help="Print triangle numbers on faces")
|
||||||
pars.add_argument("--importCoplanarEdges", type=inkex.Boolean, default=False, help="Import coplanar edges")
|
pars.add_argument("--importCoplanarEdges", type=inkex.Boolean, default=False, help="Import coplanar edges")
|
||||||
pars.add_argument("--printStats", type=inkex.Boolean, default=True, help="Show some unfold statistics")
|
pars.add_argument("--printStats", type=inkex.Boolean, default=False, help="Show some unfold statistics")
|
||||||
pars.add_argument("--resizetoimport", type=inkex.Boolean, default=True, help="Resize the canvas to the imported drawing's bounding box")
|
pars.add_argument("--resizetoimport", type=inkex.Boolean, default=True, help="Resize the canvas to the imported drawing's bounding box")
|
||||||
pars.add_argument("--extraborder", type=float, default=0.0)
|
pars.add_argument("--extraborder", type=float, default=0.0)
|
||||||
pars.add_argument("--extraborderUnits")
|
pars.add_argument("--extraborderUnits")
|
||||||
|
pars.add_argument("--writeTwoDSTL", type=inkex.Boolean, default=False, help="Write 2D STL unfoldings")
|
||||||
|
pars.add_argument("--TwoDSTLdir", default="./inkscape_export/", help="Location to save exported 2D STL")
|
||||||
|
|
||||||
#Style
|
#Style
|
||||||
pars.add_argument("--fontSize", type=int, default=15, help="Label font size (%)")
|
pars.add_argument("--fontSize", type=int, default=15, help="Label font size (%)")
|
||||||
@ -669,6 +699,10 @@ class Unfold(inkex.EffectExtension):
|
|||||||
pars.add_argument("--colorValleyPerforates", type=Color, default='3422552319', help="Valley perforation edges")
|
pars.add_argument("--colorValleyPerforates", type=Color, default='3422552319', help="Valley perforation edges")
|
||||||
pars.add_argument("--colorMountainPerforates", type=Color, default='879076607', help="Mountain perforation edges")
|
pars.add_argument("--colorMountainPerforates", type=Color, default='879076607', help="Mountain perforation edges")
|
||||||
|
|
||||||
|
#Post Processing
|
||||||
|
pars.add_argument("--joineryMode", type=inkex.Boolean, default=False, help="Enable joinery mode")
|
||||||
|
pars.add_argument("--origamiSimulatorMode", type=inkex.Boolean, default=False, help="Enable origami simulator mode")
|
||||||
|
|
||||||
def effect(self):
|
def effect(self):
|
||||||
if not os.path.exists(self.options.inputfile):
|
if not os.path.exists(self.options.inputfile):
|
||||||
inkex.utils.debug("The input file does not exist. Please select a proper file and try again.")
|
inkex.utils.debug("The input file does not exist. Please select a proper file and try again.")
|
||||||
@ -676,12 +710,21 @@ class Unfold(inkex.EffectExtension):
|
|||||||
mesh = om.read_trimesh(self.options.inputfile)
|
mesh = om.read_trimesh(self.options.inputfile)
|
||||||
#mesh = om.read_polymesh(self.options.inputfile) #we must work with triangles instead of polygons because the algorithm works with that
|
#mesh = om.read_polymesh(self.options.inputfile) #we must work with triangles instead of polygons because the algorithm works with that
|
||||||
|
|
||||||
fullUnfolded, unfoldedComponents = unfold(mesh, self.options.maxNumFaces, self.options.printStats)
|
fullUnfolded, unfoldedComponents = self.unfold(mesh)
|
||||||
|
|
||||||
|
|
||||||
|
#if len(unfoldedComponents) == 0:
|
||||||
|
# inkex.utils.debug("Error: no components were unfolded.")
|
||||||
|
# exit(1)
|
||||||
|
|
||||||
|
if self.options.printStats is True:
|
||||||
|
inkex.utils.debug("Unfolding components: {:0.0f}".format(len(unfoldedComponents)))
|
||||||
|
|
||||||
# Compute maxSize of the components
|
# Compute maxSize of the components
|
||||||
# All components must be scaled to the same size as the largest component
|
# All components must be scaled to the same size as the largest component
|
||||||
maxSize = 0
|
maxSize = 0
|
||||||
for unfolding in unfoldedComponents:
|
for unfolding in unfoldedComponents:
|
||||||
[xmin, ymin, boxSize] = findBoundingBox(unfolding[0])
|
[xmin, ymin, boxSize] = self.findBoundingBox(unfolding[0])
|
||||||
if boxSize > maxSize:
|
if boxSize > maxSize:
|
||||||
maxSize = boxSize
|
maxSize = boxSize
|
||||||
|
|
||||||
@ -694,10 +737,26 @@ class Unfold(inkex.EffectExtension):
|
|||||||
if newColor not in randomColorSet:
|
if newColor not in randomColorSet:
|
||||||
randomColorSet.append(newColor)
|
randomColorSet.append(newColor)
|
||||||
|
|
||||||
|
#some mode configs:
|
||||||
|
if self.options.joineryMode is True:
|
||||||
|
self.options.separateGluePairsByColor = True #we need random colors in this mode
|
||||||
|
|
||||||
|
if self.options.origamiSimulatorMode is True:
|
||||||
|
self.options.joineryMode = True #we set to true even if false because we need the same flat structure for origami simulator
|
||||||
|
self.options.separateGluePairsByColor = False #we need to have no weird random colors in this mode
|
||||||
|
self.options.colorMountainCut = "#000000" #black
|
||||||
|
self.options.colorValleyCut = "#000000" #black
|
||||||
|
self.options.colorCoplanarEdges = "#000000" #black
|
||||||
|
self.options.colorMountainPerforates = "#ff0000" #red
|
||||||
|
self.options.colorValleyPerforates = "#0000ff" #blue
|
||||||
|
|
||||||
# Create a new container group to attach all paperfolds
|
# Create a new container group to attach all paperfolds
|
||||||
paperfoldMainGroup = self.document.getroot().add(inkex.Group(id=self.svg.get_unique_id("paperfold-"))) #make a new group at root level
|
paperfoldMainGroup = self.document.getroot().add(inkex.Group(id=self.svg.get_unique_id("paperfold-"))) #make a new group at root level
|
||||||
for i in range(len(unfoldedComponents)):
|
for i in range(len(unfoldedComponents)):
|
||||||
paperfoldPageGroup = writeSVG(self, unfoldedComponents[i], maxSize, randomColorSet)
|
if self.options.printStats is True:
|
||||||
|
inkex.utils.debug("-----------------------------------------------------------")
|
||||||
|
inkex.utils.debug("Unfolding component nr.: {:0.0f}".format(i))
|
||||||
|
paperfoldPageGroup = self.writeSVG(unfoldedComponents[i], maxSize, randomColorSet)
|
||||||
#translate the groups next to each other to remove overlappings
|
#translate the groups next to each other to remove overlappings
|
||||||
if i != 0:
|
if i != 0:
|
||||||
previous_bbox = paperfoldMainGroup[i-1].bounding_box()
|
previous_bbox = paperfoldMainGroup[i-1].bounding_box()
|
||||||
@ -715,12 +774,22 @@ class Unfold(inkex.EffectExtension):
|
|||||||
if self.options.resizetoimport:
|
if self.options.resizetoimport:
|
||||||
bbox = paperfoldMainGroup.bounding_box()
|
bbox = paperfoldMainGroup.bounding_box()
|
||||||
namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi'))
|
namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi'))
|
||||||
doc_units = namedView.get(inkex.addNS('document-units', 'inkscape'))
|
|
||||||
root = self.svg.getElement('//svg:svg');
|
root = self.svg.getElement('//svg:svg');
|
||||||
offset = self.svg.unittouu(str(self.options.extraborder) + self.options.extraborderUnits)
|
offset = self.svg.unittouu(str(self.options.extraborder) + self.options.extraborderUnits)
|
||||||
root.set('viewBox', '%f %f %f %f' % (bbox.left - offset, bbox.top - offset, bbox.width + 2 * offset, bbox.height + 2 * offset))
|
root.set('viewBox', '%f %f %f %f' % (bbox.left - offset, bbox.top - offset, bbox.width + 2 * offset, bbox.height + 2 * offset))
|
||||||
root.set('width', str(bbox.width + 2 * offset) + doc_units)
|
root.set('width', str(bbox.width + 2 * offset) + self.svg.unit)
|
||||||
root.set('height', str(bbox.height + 2 * offset) + doc_units)
|
root.set('height', str(bbox.height + 2 * offset) + self.svg.unit)
|
||||||
|
|
||||||
|
#if set, we move all edges (path elements) to the top level
|
||||||
|
if self.options.joineryMode is True:
|
||||||
|
for paperfoldPage in paperfoldMainGroup.getchildren():
|
||||||
|
for child in paperfoldPage:
|
||||||
|
if "-edges" in child.get('id'):
|
||||||
|
for edge in child:
|
||||||
|
edgeTransform = edge.composed_transform()
|
||||||
|
self.document.getroot().append(edge)
|
||||||
|
edge.transform = edgeTransform
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
Unfold().run()
|
Unfold().run()
|
Reference in New Issue
Block a user