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
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
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
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()
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)
+
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
self._loadBinary(f)
f.close()
- self._createOrigonalVertexCopy()
+ self._postProcessAfterLoad()
+ return self
def _loadAscii(self, f):
cnt = 0
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))
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 )
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)
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)))
+