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.
parent
daa2066a64
commit
16c043e469
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
Loading…
Reference in New Issue