chiark / gitweb /
Merge branch 'new-settings' into devel
[cura.git] / Cura / util / objectScene.py
1 """
2 The objectScene module contain a objectScene class,
3 this class contains a group of printableObjects that are located on the build platform.
4
5 The objectScene handles the printing order of these objects, and if they collide.
6 """
7 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
8 import random
9 import numpy
10
11 from Cura.util import profile
12 from Cura.util import polygon
13
14 class _objectOrder(object):
15         """
16         Internal object used by the _objectOrderFinder to keep track of a possible order in which to print objects.
17         """
18         def __init__(self, order, todo):
19                 """
20                 :param order:   List of indexes in which to print objects, ordered by printing order.
21                 :param todo:    List of indexes which are not yet inserted into the order list.
22                 """
23                 self.order = order
24                 self.todo = todo
25
26 class _objectOrderFinder(object):
27         """
28         Internal object used by the Scene class to figure out in which order to print objects.
29         """
30         def __init__(self, scene, leftToRight, frontToBack, gantryHeight):
31                 self._scene = scene
32                 self._objs = scene.objects()
33                 self._leftToRight = leftToRight
34                 self._frontToBack = frontToBack
35                 initialList = []
36                 for n in xrange(0, len(self._objs)):
37                         if scene.checkPlatform(self._objs[n]):
38                                 initialList.append(n)
39                 if len(initialList) == 1:
40                         self.order = initialList
41                         return
42                 for n in initialList:
43                         if self._objs[n].getSize()[2] > gantryHeight and len(initialList) > 1:
44                                 self.order = None
45                                 return
46                 if len(initialList) == 0:
47                         self.order = []
48                         return
49
50                 self._hitMap = [None] * (max(initialList)+1)
51                 for a in initialList:
52                         self._hitMap[a] = [False] * (max(initialList)+1)
53                         for b in initialList:
54                                 self._hitMap[a][b] = self._checkHit(a, b)
55
56                 #Check if we have 2 files that overlap so that they can never be printed one at a time.
57                 for a in initialList:
58                         for b in initialList:
59                                 if a != b and self._hitMap[a][b] and self._hitMap[b][a]:
60                                         self.order = None
61                                         return
62
63                 initialList.sort(self._objIdxCmp)
64
65                 n = 0
66                 self._todo = [_objectOrder([], initialList)]
67                 while len(self._todo) > 0:
68                         n += 1
69                         current = self._todo.pop()
70                         #print len(self._todo), len(current.order), len(initialList), current.order
71                         for addIdx in current.todo:
72                                 if not self._checkHitFor(addIdx, current.order) and not self._checkBlocks(addIdx, current.todo):
73                                         todoList = current.todo[:]
74                                         todoList.remove(addIdx)
75                                         order = current.order[:] + [addIdx]
76                                         if len(todoList) == 0:
77                                                 self._todo = None
78                                                 self.order = order
79                                                 return
80                                         self._todo.append(_objectOrder(order, todoList))
81                 self.order = None
82
83         def _objIdxCmp(self, a, b):
84                 scoreA = sum(self._hitMap[a])
85                 scoreB = sum(self._hitMap[b])
86                 return scoreA - scoreB
87
88         def _checkHitFor(self, addIdx, others):
89                 for idx in others:
90                         if self._hitMap[addIdx][idx]:
91                                 return True
92                 return False
93
94         def _checkBlocks(self, addIdx, others):
95                 for idx in others:
96                         if addIdx != idx and self._hitMap[idx][addIdx]:
97                                 return True
98                 return False
99
100         #Check if printing one object will cause printhead colission with other object.
101         def _checkHit(self, addIdx, idx):
102                 obj = self._scene._objectList[idx]
103                 addObj = self._scene._objectList[addIdx]
104                 return polygon.polygonCollision(obj._boundaryHull + obj.getPosition(), addObj._headAreaHull + addObj.getPosition())
105
106 class Scene(object):
107         """
108         The scene class keep track of an collection of objects on a build platform and their state.
109         It can figure out in which order to print them (if any) and if an object can be printed at all.
110         """
111         def __init__(self, sceneView=None):
112                 self._objectList = []
113                 self._sizeOffsets = numpy.array([0.0,0.0], numpy.float32)
114                 self._machineSize = numpy.array([100,100,100], numpy.float32)
115                 self._headSizeOffsets = numpy.array([18.0,18.0], numpy.float32)
116                 self._minExtruderCount = None
117                 self._extruderOffset = [numpy.array([0,0], numpy.float32)] * 4
118
119                 #Print order variables
120                 self._leftToRight = False
121                 self._frontToBack = True
122                 self._gantryHeight = 60
123                 self._oneAtATime = True
124
125                 self._lastOneAtATime = False
126                 self._lastResultOneAtATime = True
127                 self._sceneView = sceneView
128
129         # update the physical machine dimensions
130         def updateMachineDimensions(self):
131                 self._machineSize = numpy.array([profile.getMachineSettingFloat('machine_width'), profile.getMachineSettingFloat('machine_depth'), profile.getMachineSettingFloat('machine_height')])
132                 self._machinePolygons = profile.getMachineSizePolygons()
133                 self.updateHeadSize()
134
135         # Size offsets are offsets caused by brim, skirt, etc.
136         def updateSizeOffsets(self, force=False):
137                 newOffsets = numpy.array(profile.calculateObjectSizeOffsets(), numpy.float32)
138                 minExtruderCount = profile.minimalExtruderCount()
139                 if not force and numpy.array_equal(self._sizeOffsets, newOffsets) and self._minExtruderCount == minExtruderCount:
140                         return
141                 self._sizeOffsets = newOffsets
142                 self._minExtruderCount = minExtruderCount
143
144                 extends = [numpy.array([[-newOffsets[0],-newOffsets[1]],[ newOffsets[0],-newOffsets[1]],[ newOffsets[0], newOffsets[1]],[-newOffsets[0], newOffsets[1]]], numpy.float32)]
145                 for n in xrange(1, 4):
146                         headOffset = numpy.array([[0, 0], [-profile.getMachineSettingFloat('extruder_offset_x%d' % (n)), -profile.getMachineSettingFloat('extruder_offset_y%d' % (n))]], numpy.float32)
147                         extends.append(polygon.minkowskiHull(extends[n-1], headOffset))
148                 if minExtruderCount > 1:
149                         extends[0] = extends[1]
150
151                 for obj in self._objectList:
152                         obj.setPrintAreaExtends(extends[len(obj._meshList) - 1])
153
154         #size of the printing head.
155         def updateHeadSize(self, obj = None):
156                 xMin = profile.getMachineSettingFloat('extruder_head_size_min_x')
157                 xMax = profile.getMachineSettingFloat('extruder_head_size_max_x')
158                 yMin = profile.getMachineSettingFloat('extruder_head_size_min_y')
159                 yMax = profile.getMachineSettingFloat('extruder_head_size_max_y')
160                 gantryHeight = profile.getMachineSettingFloat('extruder_head_size_height')
161                 objectSink = profile.getProfileSettingFloat("object_sink")
162
163                 self._leftToRight = xMin < xMax
164                 self._frontToBack = yMin < yMax
165                 self._headSizeOffsets[0] = min(xMin, xMax)
166                 self._headSizeOffsets[1] = min(yMin, yMax)
167                 self._gantryHeight = gantryHeight
168
169                 printOneAtATime = profile.getPreference('oneAtATime') == 'True'
170                 self._oneAtATime = self._gantryHeight > 0 and printOneAtATime
171                 if self._oneAtATime:
172                         if not self._lastOneAtATime:
173                                 #print mode was changed by user. We need to reset that value to test with current scene content
174                                 self._lastResultOneAtATime = True
175
176                         for objIter in self._objectList:
177                                 if objIter.getSize()[2] - objectSink > self._gantryHeight:
178                                         self._oneAtATime = False
179                                         if self._lastResultOneAtATime:
180                                                 if self._sceneView:
181                                                         self._sceneView.notification.message("Object must be shorter than {}mm for this printer/toolhead. Reduce object size or swap to \"All at once\" mode. ".format(self._gantryHeight))
182                                                 break
183
184                 self._lastResultOneAtATime = self._oneAtATime
185                 self._lastOneAtATime = printOneAtATime
186
187                 headArea = numpy.array([[-xMin,-yMin],[ xMax,-yMin],[ xMax, yMax],[-xMin, yMax]], numpy.float32)
188
189                 if obj is None:
190                         for obj in self._objectList:
191                                 obj.setHeadArea(headArea, self._headSizeOffsets)
192                 else:
193                         obj.setHeadArea(headArea, self._headSizeOffsets)
194
195         def isOneAtATime(self):
196                 return self._oneAtATime
197
198         def setExtruderOffset(self, extruderNr, offsetX, offsetY):
199                 self._extruderOffset[extruderNr] = numpy.array([offsetX, offsetY], numpy.float32)
200
201         def objects(self):
202                 return self._objectList
203
204         #Add new object to print area
205         def add(self, obj, positionOnly=False):
206                 if not positionOnly and numpy.max(obj.getSize()[0:2]) > numpy.max(self._machineSize[0:2]) * 2.5:
207                         scale = numpy.max(self._machineSize[0:2]) * 2.5 / numpy.max(obj.getSize()[0:2])
208                         matrix = [[scale,0,0], [0, scale, 0], [0, 0, scale]]
209                         obj.applyMatrix(numpy.matrix(matrix, numpy.float64))
210                 self._findFreePositionFor(obj)
211                 self._objectList.append(obj)
212                 self.updateHeadSize(obj)
213                 self.updateSizeOffsets(True)
214                 self.pushFree(obj)
215
216         def remove(self, obj):
217                 self._objectList.remove(obj)
218
219         #Dual(multiple) extrusion merge
220         def merge(self, obj1, obj2):
221                 self.remove(obj2)
222                 obj1._meshList += obj2._meshList
223                 for m in obj2._meshList:
224                         m._obj = obj1
225                 obj1.processMatrix()
226                 obj1.setPosition((obj1.getPosition() + obj2.getPosition()) / 2)
227                 self.pushFree(obj1)
228
229         def pushFree(self, staticObj = None):
230                 if staticObj is None:
231                         for obj in self._objectList:
232                                 self.pushFree(obj)
233                         return
234                 if not self.checkPlatform(staticObj):
235                         return
236                 pushList = []
237                 for obj in self._objectList:
238                         if obj == staticObj or not self.checkPlatform(obj):
239                                 continue
240                         if self._oneAtATime:
241                                 v = polygon.polygonCollisionPushVector(obj._headAreaMinHull + obj.getPosition(), staticObj._boundaryHull + staticObj.getPosition())
242                         else:
243                                 v = polygon.polygonCollisionPushVector(obj._boundaryHull + obj.getPosition(), staticObj._boundaryHull + staticObj.getPosition())
244                         if type(v) is bool:
245                                 continue
246                         obj.setPosition(obj.getPosition() + v * 1.01)
247                         pushList.append(obj)
248                 for obj in pushList:
249                         self.pushFree(obj)
250
251         def arrangeAll(self, positionOnly=False):
252                 oldList = self._objectList
253                 self._objectList = []
254                 for obj in oldList:
255                         obj.setPosition(numpy.array([0,0], numpy.float32))
256                         self.add(obj, positionOnly)
257
258         def centerAll(self):
259                 minPos = numpy.array([9999999,9999999], numpy.float32)
260                 maxPos = numpy.array([-9999999,-9999999], numpy.float32)
261                 for obj in self._objectList:
262                         pos = obj.getPosition()
263                         size = obj.getSize()
264                         minPos[0] = min(minPos[0], pos[0] - size[0] / 2)
265                         minPos[1] = min(minPos[1], pos[1] - size[1] / 2)
266                         maxPos[0] = max(maxPos[0], pos[0] + size[0] / 2)
267                         maxPos[1] = max(maxPos[1], pos[1] + size[1] / 2)
268                 offset = -(maxPos + minPos) / 2
269                 for obj in self._objectList:
270                         obj.setPosition(obj.getPosition() + offset)
271
272         def printOrder(self):
273                 if self._oneAtATime:
274                         order = _objectOrderFinder(self, self._leftToRight, self._frontToBack, self._gantryHeight).order
275                 else:
276                         order = None
277                 return order
278
279         #Check if two objects are hitting each-other (+ head space).
280         def _checkHit(self, a, b):
281                 if a == b:
282                         return False
283                 if self._oneAtATime:
284                         return polygon.polygonCollision(a._headAreaMinHull + a.getPosition(), b._boundaryHull + b.getPosition())
285                 else:
286                         return polygon.polygonCollision(a._boundaryHull + a.getPosition(), b._boundaryHull + b.getPosition())
287
288         def checkPlatform(self, obj):
289                 objectSink = profile.getProfileSettingFloat("object_sink")
290
291                 area = obj._printAreaHull + obj.getPosition()
292                 if obj.getSize()[2] - objectSink > self._machineSize[2]:
293                         return False
294                 if not polygon.fullInside(area, self._machinePolygons[0]):
295                         return False
296                 #Check the "no go zones"
297                 for poly in self._machinePolygons[1:]:
298                         if polygon.polygonCollision(poly, area):
299                                 return False
300                 return True
301
302         def _findFreePositionFor(self, obj):
303                 posList = []
304                 for a in self._objectList:
305                         p = a.getPosition()
306                         if self._oneAtATime:
307                                 s = (a.getSize()[0:2] + obj.getSize()[0:2]) / 2 + self._sizeOffsets + self._headSizeOffsets + numpy.array([4,4], numpy.float32)
308                         else:
309                                 s = (a.getSize()[0:2] + obj.getSize()[0:2]) / 2 + numpy.array([4,4], numpy.float32)
310                         posList.append(p + s * ( 1.0, 1.0))
311                         posList.append(p + s * ( 0.0, 1.0))
312                         posList.append(p + s * (-1.0, 1.0))
313                         posList.append(p + s * ( 1.0, 0.0))
314                         posList.append(p + s * (-1.0, 0.0))
315                         posList.append(p + s * ( 1.0,-1.0))
316                         posList.append(p + s * ( 0.0,-1.0))
317                         posList.append(p + s * (-1.0,-1.0))
318
319                 best = None
320                 bestDist = None
321                 for p in posList:
322                         obj.setPosition(p)
323                         ok = True
324                         for a in self._objectList:
325                                 if self._checkHit(a, obj):
326                                         ok = False
327                                         break
328                         if not ok:
329                                 continue
330                         dist = numpy.linalg.norm(p)
331                         if not self.checkPlatform(obj):
332                                 dist *= 3
333                         if best is None or dist < bestDist:
334                                 best = p
335                                 bestDist = dist
336                 if best is not None:
337                         obj.setPosition(best)