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.

master
daid 2012-05-25 16:30:07 +02:00
parent daa2066a64
commit 16c043e469
4 changed files with 370 additions and 20 deletions

View 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) 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) toolbarUtil.RadioButton(self.toolbar, group, 'object-top-on.png', 'object-top-off.png', 'Topdown view', callback=self.OnTopClick).SetValue(True)
self.toolbar.AddSeparator() 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') 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() self.toolbar.Realize()
@ -252,6 +254,23 @@ class projectPlanner(wx.Frame):
prefDialog.Centre() prefDialog.Centre()
prefDialog.Show(True) 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): def OnSaveProject(self, e):
dlg=wx.FileDialog(self, "Save project file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE) dlg=wx.FileDialog(self, "Save project file", os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
dlg.SetWildcard("Project files (*.curaproject)|*.curaproject") dlg.SetWildcard("Project files (*.curaproject)|*.curaproject")

View File

@ -1,11 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import __init__ import __init__
import sys import sys, math, re, os, struct, time
import math
import re
import os
import struct
from util import util3d from util import util3d
@ -19,12 +15,12 @@ class mesh(object):
self.vertexes = [] self.vertexes = []
def addFace(self, v0, v1, v2): def addFace(self, v0, v1, v2):
self.faces.append(meshFace(v0, v1, v2))
self.vertexes.append(v0) self.vertexes.append(v0)
self.vertexes.append(v1) self.vertexes.append(v1)
self.vertexes.append(v2) self.vertexes.append(v2)
self.faces.append(meshFace(v0, v1, v2))
def _createOrigonalVertexCopy(self): def _postProcessAfterLoad(self):
self.origonalVertexes = list(self.vertexes) self.origonalVertexes = list(self.vertexes)
for i in xrange(0, len(self.origonalVertexes)): for i in xrange(0, len(self.origonalVertexes)):
self.origonalVertexes[i] = self.origonalVertexes[i].copy() self.origonalVertexes[i] = self.origonalVertexes[i].copy()
@ -96,6 +92,81 @@ class mesh(object):
v.y -= minV.y + (maxV.y - minV.y) / 2 v.y -= minV.y + (maxV.y - minV.y) / 2
self.getMinimumZ() self.getMinimumZ()
if __name__ == '__main__': def splitToParts(self):
for filename in sys.argv[1:]: t0 = time.time()
stlModel().load(filename)
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)

View File

@ -1,11 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import __init__ import __init__
import sys import sys, math, re, os, struct, time
import math
import re
import os
import struct
from util import util3d from util import util3d
from util import mesh from util import mesh
@ -25,7 +21,8 @@ class stlModel(mesh.mesh):
self._loadBinary(f) self._loadBinary(f)
f.close() f.close()
self._createOrigonalVertexCopy() self._postProcessAfterLoad()
return self
def _loadAscii(self, f): def _loadAscii(self, f):
cnt = 0 cnt = 0
@ -54,7 +51,30 @@ class stlModel(mesh.mesh):
v2 = util3d.Vector3(data[9], data[10], data[11]) v2 = util3d.Vector3(data[9], data[10], data[11])
self.addFace(v0, v1, v2) 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__': if __name__ == '__main__':
for filename in sys.argv[1:]: 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))

View File

@ -14,7 +14,7 @@ class Vector3(object):
return Vector3(self.x, self.y, self.z) return Vector3(self.x, self.y, self.z)
def __repr__(self): 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): def __add__(self, v):
return Vector3( self.x + v.x, self.y + v.y, self.z + v.z ) return Vector3( self.x + v.x, self.y + v.y, self.z + v.z )
@ -56,6 +56,9 @@ class Vector3(object):
self.z /= v self.z /= v
return self 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): 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) 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): def max(self, v):
return Vector3(max(self.x, v.x), max(self.y, v.y), max(self.z, v.z)) 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)))