From: daid Date: Fri, 25 May 2012 14:30:07 +0000 (+0200) Subject: Added "split plate" function to project planner, which cuts an STL file up into seper... X-Git-Tag: 12.07~46 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=commitdiff_plain;h=16c043e4695a1c570beee9f14d8dc9792d464b0a;p=cura.git Added "split plate" function to project planner, which cuts an STL file up into seperate pieces. Useful to print items with the project planner that are only distributed in plate form. --- 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))) +