chiark / gitweb /
Added "split plate" function to project planner, which cuts an STL file up into seper...
authordaid <daid303@gmail.com>
Fri, 25 May 2012 14:30:07 +0000 (16:30 +0200)
committerdaid <daid303@gmail.com>
Fri, 25 May 2012 14:30:07 +0000 (16:30 +0200)
Cura/gui/projectPlanner.py
Cura/util/mesh.py
Cura/util/stl.py
Cura/util/util3d.py

index 9322eef990c396416d77af487c75ea46fb566822..054fbe71678876ef8eb0c2887ad506fde152aaf7 100644 (file)
@@ -157,9 +157,11 @@ class projectPlanner(wx.Frame):
                toolbarUtil.RadioButton(self.toolbar, group, 'object-3d-on.png', 'object-3d-off.png', '3D view', callback=self.On3DClick)\r
                toolbarUtil.RadioButton(self.toolbar, group, 'object-top-on.png', 'object-top-off.png', 'Topdown view', callback=self.OnTopClick).SetValue(True)\r
                self.toolbar.AddSeparator()\r
-               toolbarUtil.NormalButton(self.toolbar, self.OnQuit, 'exit.png', 'Close project planner')\r
-               self.toolbar.AddSeparator()\r
                toolbarUtil.NormalButton(self.toolbar, self.OnPreferences, 'preferences.png', 'Project planner preferences')\r
+               self.toolbar.AddSeparator()\r
+               toolbarUtil.NormalButton(self.toolbar, self.OnCutMesh, 'cut-mesh.png', 'Cut a plate STL into multiple STL files, and add those files to the project.\nNote: Splitting up plates sometimes takes a few minutes.')\r
+               self.toolbar.AddSeparator()\r
+               toolbarUtil.NormalButton(self.toolbar, self.OnQuit, 'exit.png', 'Close project planner')\r
                \r
                self.toolbar.Realize()\r
 \r
@@ -252,6 +254,23 @@ class projectPlanner(wx.Frame):
                prefDialog.Centre()\r
                prefDialog.Show(True)\r
        \r
+       def OnCutMesh(self, e):\r
+               dlg=wx.FileDialog(self, "Open file to cut", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)\r
+               dlg.SetWildcard("STL files (*.stl)|*.stl;*.STL")\r
+               if dlg.ShowModal() == wx.ID_OK:\r
+                       filename = dlg.GetPath()\r
+                       parts = stl.stlModel().load(filename).splitToParts()\r
+                       for part in parts:\r
+                               partFilename = filename[:filename.rfind('.')] + "_part%d.stl" % (parts.index(part))\r
+                               stl.saveAsSTL(part, partFilename)\r
+                               item = ProjectObject(self, partFilename)\r
+                               self.list.append(item)\r
+                               self.selection = item\r
+                               self._updateListbox()\r
+                               self.OnListSelect(None)\r
+               self.preview.Refresh()\r
+               dlg.Destroy()\r
+       \r
        def OnSaveProject(self, e):\r
                dlg=wx.FileDialog(self, "Save project file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)\r
                dlg.SetWildcard("Project files (*.curaproject)|*.curaproject")\r
index 63c105d486fda40784090600ea8d4730521392a3..1dbd8cd7ea98a76bd229d56e26ff3d38dce5698e 100644 (file)
@@ -1,11 +1,7 @@
 from __future__ import absolute_import
 import __init__
 
-import sys
-import math
-import re
-import os
-import struct
+import sys, math, re, os, struct, time
 
 from util import util3d
 
@@ -19,12 +15,12 @@ class mesh(object):
                self.vertexes = []
 
        def addFace(self, v0, v1, v2):
-               self.faces.append(meshFace(v0, v1, v2))
                self.vertexes.append(v0)
                self.vertexes.append(v1)
                self.vertexes.append(v2)
+               self.faces.append(meshFace(v0, v1, v2))
 
-       def _createOrigonalVertexCopy(self):
+       def _postProcessAfterLoad(self):
                self.origonalVertexes = list(self.vertexes)
                for i in xrange(0, len(self.origonalVertexes)):
                        self.origonalVertexes[i] = self.origonalVertexes[i].copy()
@@ -96,6 +92,81 @@ class mesh(object):
                        v.y -= minV.y + (maxV.y - minV.y) / 2
                self.getMinimumZ()
 
-if __name__ == '__main__':
-       for filename in sys.argv[1:]:
-               stlModel().load(filename)
+       def splitToParts(self):
+               t0 = time.time()
+
+               print "%f: " % (time.time() - t0), "Splitting a model with %d vertexes." % (len(self.vertexes))
+               removeDict = {}
+               tree = util3d.AABBTree()
+               off = util3d.Vector3(0.0001,0.0001,0.0001)
+               newVertexList = []
+               for v in self.vertexes:
+                       e = util3d.AABB(v-off, v+off)
+                       q = tree.query(e)
+                       if len(q) < 1:
+                               e.vector = v
+                               tree.insert(e)
+                               newVertexList.append(v)
+                       else:
+                               removeDict[v] = q[0].vector
+               print "%f: " % (time.time() - t0), "Marked %d duplicate vertexes for removal." % (len(removeDict))
+               
+               #Make facelists so we can quickly remove all the vertexes.
+               for v in self.vertexes:
+                       v.faceList = []
+               for f in self.faces:
+                       f.v[0].faceList.append(f)
+                       f.v[1].faceList.append(f)
+                       f.v[2].faceList.append(f)
+
+               self.vertexes = newVertexList
+               for v1 in removeDict.iterkeys():
+                       v0 = removeDict[v1]
+                       for f in v1.faceList:
+                               if f.v[0] == v1:
+                                       f.v[0] = v0
+                               if f.v[1] == v1:
+                                       f.v[1] = v0
+                               if f.v[2] == v1:
+                                       f.v[2] = v0
+               print "%f: " % (time.time() - t0), "Building face lists after vertex removal."
+               for v in self.vertexes:
+                       v.faceList = []
+               for f in self.faces:
+                       f.v[0].faceList.append(f)
+                       f.v[1].faceList.append(f)
+                       f.v[2].faceList.append(f)
+               
+               print "%f: " % (time.time() - t0), "Building parts."
+               partList = []
+               doneSet = set()
+               for f in self.faces:
+                       if not f in doneSet:
+                               partList.append(self._createPartFromFacewalk(f, doneSet))
+               print "%f: " % (time.time() - t0), "Split into %d parts" % (len(partList))
+               return partList
+
+       def _createPartFromFacewalk(self, startFace, doneSet):
+               m = mesh()
+               todoList = [startFace]
+               doneSet.add(startFace)
+               while len(todoList) > 0:
+                       f = todoList.pop()
+                       m._partAddFacewalk(f, doneSet, todoList)
+               return m
+
+       def _partAddFacewalk(self, f, doneSet, todoList):
+               self.addFace(f.v[0], f.v[1], f.v[2])
+               for f1 in f.v[0].faceList:
+                       if f1 not in doneSet:
+                               todoList.append(f1)
+                               doneSet.add(f1)
+               for f1 in f.v[1].faceList:
+                       if f1 not in doneSet:
+                               todoList.append(f1)
+                               doneSet.add(f1)
+               for f1 in f.v[2].faceList:
+                       if f1 not in doneSet:
+                               todoList.append(f1)
+                               doneSet.add(f1)
+
index 8ba47a3d000439cd36bf2107d45fa820f5e7f788..7e3e6421e368ce82c13da93e1cd767c964a3624d 100644 (file)
@@ -1,11 +1,7 @@
 from __future__ import absolute_import
 import __init__
 
-import sys
-import math
-import re
-import os
-import struct
+import sys, math, re, os, struct, time
 
 from util import util3d
 from util import mesh
@@ -25,7 +21,8 @@ class stlModel(mesh.mesh):
                        self._loadBinary(f)
                f.close()
                
-               self._createOrigonalVertexCopy()
+               self._postProcessAfterLoad()
+               return self
        
        def _loadAscii(self, f):
                cnt = 0
@@ -54,7 +51,30 @@ class stlModel(mesh.mesh):
                        v2 = util3d.Vector3(data[9], data[10], data[11])
                        self.addFace(v0, v1, v2)
 
+def saveAsSTL(mesh, filename):
+       f = open(filename, 'wb')
+       #Write the STL binary header. This can contain any info, except for "SOLID" at the start.
+       f.write(("CURA BINARY STL EXPORT. " + time.strftime('%a %d %b %Y %H:%M:%S')).ljust(80, '\000'))
+       #Next follow 4 binary bytes containing the amount of faces, and then the face information.
+       f.write(struct.pack("<I", len(mesh.faces)))
+       for face in mesh.faces:
+               v1 = face.v[0]
+               v2 = face.v[1]
+               v3 = face.v[2]
+               normal = (v2 - v1).cross(v3 - v1)
+               normal.normalize()
+               f.write(struct.pack("<fff", normal.x, normal.y, normal.z))
+               f.write(struct.pack("<fff", v1.x, v1.y, v1.z))
+               f.write(struct.pack("<fff", v2.x, v2.y, v2.z))
+               f.write(struct.pack("<fff", v3.x, v3.y, v3.z))
+               f.write(struct.pack("<H", 0))
+       f.close()
+
 if __name__ == '__main__':
        for filename in sys.argv[1:]:
-               stlModel().load(filename)
+               m = stlModel().load(filename)
+               print "Loaded %d faces" % (len(m.faces))
+               parts = m.splitToParts()
+               for p in parts:
+                       saveAsSTL(p, "export_%i.stl" % parts.index(p))
 
index d5411fc9c493e623ef82689242e591b422f96953..b5140b275d3a1c63664d030606ef7cd1c6d7a9d3 100644 (file)
@@ -14,7 +14,7 @@ class Vector3(object):
                return Vector3(self.x, self.y, self.z)
 
        def __repr__(self):
-               return '%s, %s, %s' % ( self.x, self.y, self.z )
+               return '[%s, %s, %s]' % ( self.x, self.y, self.z )
 
        def __add__(self, v):
                return Vector3( self.x + v.x, self.y + v.y, self.z + v.z )
@@ -56,6 +56,9 @@ class Vector3(object):
                self.z /= v
                return self
 
+       def almostEqual(self, v):
+               return (abs(self.x - v.x) + abs(self.y - v.y) + abs(self.z - v.z)) < 0.00001
+       
        def cross(self, v):
                return Vector3(self.y * v.z - self.z * v.y, -self.x * v.z + self.z * v.x, self.x * v.y - self.y * v.x)
 
@@ -75,3 +78,240 @@ class Vector3(object):
        def max(self, v):
                return Vector3(max(self.x, v.x), max(self.y, v.y), max(self.z, v.z))
 
+class AABB(object):
+       def __init__(self, vMin, vMax):
+               self.vMin = vMin
+               self.vMax = vMax
+       
+       def getPerimeter(self):
+               return (self.vMax.x - self.vMax.x) + (self.vMax.y - self.vMax.y) + (self.vMax.z - self.vMax.z)
+       
+       def combine(self, aabb):
+               return AABB(self.vMin.min(aabb.vMin), self.vMax.max(aabb.vMax))
+       
+       def overlap(self, aabb):
+               if aabb.vMin.x - self.vMax.x > 0.0 or aabb.vMin.y - self.vMax.y > 0.0 or aabb.vMin.z - self.vMax.z > 0.0:
+                       return False
+               if self.vMin.x - aabb.vMax.x > 0.0 or self.vMin.y - aabb.vMax.y > 0.0 or self.vMin.z - aabb.vMax.z > 0.0:
+                       return False
+               return True
+       
+       def __repr__(self):
+               return "AABB:%s - %s" % (str(self.vMin), str(self.vMax))
+
+class _AABBNode(object):
+       def __init__(self, aabb):
+               self.child1 = None
+               self.child2 = None
+               self.parent = None
+               self.height = 0
+               self.aabb = aabb
+       
+       def isLeaf(self):
+               return self.child1 == None
+       
+class AABBTree(object):
+       def __init__(self):
+               self.root = None
+       
+       def insert(self, aabb):
+               newNode = _AABBNode(aabb)
+               if self.root == None:
+                       self.root = newNode
+                       return
+               
+               node = self.root
+               while not node.isLeaf():
+                       child1 = node.child1
+                       child2 = node.child2
+                       
+                       area = node.aabb.getPerimeter()
+                       combinedAABB = node.aabb.combine(aabb)
+                       combinedArea = combinedAABB.getPerimeter()
+                       
+                       cost = 2.0 * combinedArea
+                       inheritanceCost = 2.0 * (combinedArea - area)
+
+                       if child1.isLeaf():
+                               cost1 = aabb.combine(child1.aabb).getPerimeter() + inheritanceCost
+                       else:
+                               oldArea = child1.aabb.getPerimeter()
+                               newArea = aabb.combine(child1.aabb).getPerimeter()
+                               cost1 = (newArea - oldArea) + inheritanceCost
+
+                       if child2.isLeaf():
+                               cost2 = aabb.combine(child1.aabb).getPerimeter() + inheritanceCost
+                       else:
+                               oldArea = child2.aabb.getPerimeter()
+                               newArea = aabb.combine(child2.aabb).getPerimeter()
+                               cost2 = (newArea - oldArea) + inheritanceCost
+
+                       if cost < cost1 and cost < cost2:
+                               break
+
+                       if cost1 < cost2:
+                               node = child1
+                       else:
+                               node = child2
+
+               sibling = node
+
+               # Create a new parent.
+               oldParent = sibling.parent
+               newParent = _AABBNode(aabb.combine(sibling.aabb))
+               newParent.parent = oldParent
+               newParent.height = sibling.height + 1
+
+               if oldParent != None:
+                       # The sibling was not the root.
+                       if oldParent.child1 == sibling:
+                               oldParent.child1 = newParent
+                       else:
+                               oldParent.child2 = newParent
+
+                       newParent.child1 = sibling
+                       newParent.child2 = newNode
+                       sibling.parent = newParent
+                       newNode.parent = newParent
+               else:
+                       # The sibling was the root.
+                       newParent.child1 = sibling
+                       newParent.child2 = newNode
+                       sibling.parent = newParent
+                       newNode.parent = newParent
+                       self.root = newParent
+
+               # Walk back up the tree fixing heights and AABBs
+               node = newNode.parent
+               while node != None:
+                       node = self._balance(node)
+
+                       child1 = node.child1
+                       child2 = node.child2
+
+                       node.height = 1 + max(child1.height, child2.height)
+                       node.aabb = child1.aabb.combine(child2.aabb)
+
+                       node = node.parent
+
+       def _balance(self, A):
+               if A.isLeaf() or A.height < 2:
+                       return A
+
+               B = A.child1
+               C = A.child2
+               
+               balance = C.height - B.height
+
+               # Rotate C up
+               if balance > 1:
+                       F = C.child1;
+                       G = C.child2;
+
+                       # Swap A and C
+                       C.child1 = A;
+                       C.parent = A.parent;
+                       A.parent = C;
+
+                       # A's old parent should point to C
+                       if C.parent != None:
+                               if C.parent.child1 == A:
+                                       C.parent.child1 = C
+                               else:
+                                       C.parent.child2 = C
+                       else:
+                               self.root = C
+
+                       # Rotate
+                       if F.height > G.height:
+                               C.child2 = F
+                               A.child2 = G
+                               G.parent = A
+                               A.aabb = B.aabb.combine(G.aabb)
+                               C.aabb = A.aabb.combine(F.aabb)
+
+                               A.height = 1 + Math.max(B.height, G.height)
+                               C.height = 1 + Math.max(A.height, F.height)
+                       else:
+                               C.child2 = G
+                               A.child2 = F
+                               F.parent = A
+                               A.aabb = B.aabb.combine(F.aabb)
+                               C.aabb = A.aabb.combine(G.aabb)
+
+                               A.height = 1 + max(B.height, F.height)
+                               C.height = 1 + max(A.height, G.height)
+
+                       return C;
+
+               # Rotate B up
+               if balance < -1:
+                       D = B.child1
+                       E = B.child2
+
+                       # Swap A and B
+                       B.child1 = A
+                       B.parent = A.parent
+                       A.parent = B
+
+                       # A's old parent should point to B
+                       if B.parent != None:
+                               if B.parent.child1 == A:
+                                       B.parent.child1 = B
+                               else:
+                                       B.parent.child2 = B
+                       else:
+                               self.root = B
+
+                       # Rotate
+                       if D.height > E.height:
+                               B.child2 = D
+                               A.child1 = E
+                               E.parent = A
+                               A.aabb = C.aabb.combine(E.aabb)
+                               B.aabb = A.aabb.combine(D.aabb)
+
+                               A.height = 1 + max(C.height, E.height)
+                               B.height = 1 + max(A.height, D.height)
+                       else:
+                               B.child2 = E
+                               A.child1 = D
+                               D.parent = A
+                               A.aabb = C.aabb.combine(D.aabb)
+                               B.aabb = A.aabb.combine(E.aabb)
+
+                               A.height = 1 + max(C.height, D.height)
+                               B.height = 1 + max(A.height, E.height)
+
+                       return B
+
+               return A
+
+       def query(self, aabb):
+               resultList = []
+               if self.root != None:
+                       self._query(self.root, aabb, resultList)
+               return resultList
+       
+       def _query(self, node, aabb, resultList):
+               if not aabb.overlap(node.aabb):
+                       return
+               if node.isLeaf():
+                       resultList.append(node.aabb)
+               else:
+                       self._query(node.child1, aabb, resultList)
+                       self._query(node.child2, aabb, resultList)
+
+       def __repr__(self):
+               s = "AABBTree:\n"
+               s += str(self.root.aabb)
+               return s
+
+if __name__ == '__main__':
+       tree = AABBTree()
+       tree.insert(AABB(Vector3(0,0,0), Vector3(0,0,0)))
+       tree.insert(AABB(Vector3(1,1,1), Vector3(1,1,1)))
+       tree.insert(AABB(Vector3(0.5,0.5,0.5), Vector3(0.5,0.5,0.5)))
+       print tree
+       print tree.query(AABB(Vector3(0,0,0), Vector3(0,0,0)))
+