diff --git a/Cura/gui/projectPlanner.py b/Cura/gui/projectPlanner.py index 9322eef9..054fbe71 100644 --- a/Cura/gui/projectPlanner.py +++ b/Cura/gui/projectPlanner.py @@ -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) toolbarUtil.RadioButton(self.toolbar, group, 'object-top-on.png', 'object-top-off.png', 'Topdown view', callback=self.OnTopClick).SetValue(True) self.toolbar.AddSeparator() - toolbarUtil.NormalButton(self.toolbar, self.OnQuit, 'exit.png', 'Close project planner') - self.toolbar.AddSeparator() toolbarUtil.NormalButton(self.toolbar, self.OnPreferences, 'preferences.png', 'Project planner preferences') + self.toolbar.AddSeparator() + 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.') + self.toolbar.AddSeparator() + toolbarUtil.NormalButton(self.toolbar, self.OnQuit, 'exit.png', 'Close project planner') self.toolbar.Realize() @@ -252,6 +254,23 @@ class projectPlanner(wx.Frame): prefDialog.Centre() prefDialog.Show(True) + def OnCutMesh(self, e): + dlg=wx.FileDialog(self, "Open file to cut", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST) + dlg.SetWildcard("STL files (*.stl)|*.stl;*.STL") + if dlg.ShowModal() == wx.ID_OK: + filename = dlg.GetPath() + parts = stl.stlModel().load(filename).splitToParts() + for part in parts: + partFilename = filename[:filename.rfind('.')] + "_part%d.stl" % (parts.index(part)) + stl.saveAsSTL(part, partFilename) + item = ProjectObject(self, partFilename) + self.list.append(item) + self.selection = item + self._updateListbox() + self.OnListSelect(None) + self.preview.Refresh() + dlg.Destroy() + def OnSaveProject(self, e): dlg=wx.FileDialog(self, "Save project file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE) dlg.SetWildcard("Project files (*.curaproject)|*.curaproject") diff --git a/Cura/util/mesh.py b/Cura/util/mesh.py index 63c105d4..1dbd8cd7 100644 --- a/Cura/util/mesh.py +++ b/Cura/util/mesh.py @@ -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) + diff --git a/Cura/util/stl.py b/Cura/util/stl.py index 8ba47a3d..7e3e6421 100644 --- a/Cura/util/stl.py +++ b/Cura/util/stl.py @@ -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(" 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))) +