chiark / gitweb /
Increment version number
[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                 if profile.getPreference('startMode') == 'Simple':
163                         objectSink = float(profile.settingsDictionary["object_sink"].getDefault())
164
165                 self._leftToRight = xMin < xMax
166                 self._frontToBack = yMin < yMax
167                 self._headSizeOffsets[0] = min(xMin, xMax)
168                 self._headSizeOffsets[1] = min(yMin, yMax)
169                 self._gantryHeight = gantryHeight
170
171                 printOneAtATime = profile.getPreference('oneAtATime') == 'True'
172                 self._oneAtATime = self._gantryHeight > 0 and printOneAtATime
173                 if self._oneAtATime:
174                         if not self._lastOneAtATime:
175                                 #print mode was changed by user. We need to reset that value to test with current scene content
176                                 self._lastResultOneAtATime = True
177
178                         for objIter in self._objectList:
179                                 if objIter.getSize()[2] - objectSink > self._gantryHeight:
180                                         self._oneAtATime = False
181                                         if self._lastResultOneAtATime:
182                                                 if self._sceneView:
183                                                         self._sceneView.notification.message("Object must be shorter than {}mm for this printer/tool head. Reduce object size or swap to \"All at once\" mode. ".format(self._gantryHeight))
184                                                 break
185
186                 self._lastResultOneAtATime = self._oneAtATime
187                 self._lastOneAtATime = printOneAtATime
188
189                 headArea = numpy.array([[-xMin,-yMin],[ xMax,-yMin],[ xMax, yMax],[-xMin, yMax]], numpy.float32)
190
191                 if obj is None:
192                         for obj in self._objectList:
193                                 obj.setHeadArea(headArea, self._headSizeOffsets)
194                 else:
195                         obj.setHeadArea(headArea, self._headSizeOffsets)
196
197         def isOneAtATime(self):
198                 return self._oneAtATime
199
200         def setExtruderOffset(self, extruderNr, offsetX, offsetY):
201                 self._extruderOffset[extruderNr] = numpy.array([offsetX, offsetY], numpy.float32)
202
203         def objects(self):
204                 return self._objectList
205
206         #Add new object to print area
207         def add(self, obj, positionOnly=False):
208                 if not positionOnly and numpy.max(obj.getSize()[0:2]) > numpy.max(self._machineSize[0:2]) * 2.5:
209                         scale = numpy.max(self._machineSize[0:2]) * 2.5 / numpy.max(obj.getSize()[0:2])
210                         matrix = [[scale,0,0], [0, scale, 0], [0, 0, scale]]
211                         obj.applyMatrix(numpy.matrix(matrix, numpy.float64))
212                 self._findFreePositionFor(obj)
213                 self._objectList.append(obj)
214                 self.updateHeadSize(obj)
215                 self.updateSizeOffsets(True)
216                 self.pushFree(obj)
217
218         def remove(self, obj):
219                 self._objectList.remove(obj)
220
221         #Dual(multiple) extrusion merge
222         def merge(self, obj1, obj2):
223                 self.remove(obj2)
224                 obj1._meshList += obj2._meshList
225                 for m in obj2._meshList:
226                         m._obj = obj1
227                 obj1.processMatrix()
228                 obj1.setPosition((obj1.getPosition() + obj2.getPosition()) / 2)
229                 self.pushFree(obj1)
230
231         def pushFree(self, staticObj = None):
232                 if staticObj is None:
233                         for obj in self._objectList:
234                                 self.pushFree(obj)
235                         return
236                 if not self.checkPlatform(staticObj):
237                         return
238                 pushList = []
239                 for obj in self._objectList:
240                         if obj == staticObj or not self.checkPlatform(obj):
241                                 continue
242                         if self._oneAtATime:
243                                 v = polygon.polygonCollisionPushVector(obj._headAreaMinHull + obj.getPosition(), staticObj._boundaryHull + staticObj.getPosition())
244                         else:
245                                 v = polygon.polygonCollisionPushVector(obj._boundaryHull + obj.getPosition(), staticObj._boundaryHull + staticObj.getPosition())
246                         if type(v) is bool:
247                                 continue
248                         obj.setPosition(obj.getPosition() + v * 1.01)
249                         pushList.append(obj)
250                 for obj in pushList:
251                         self.pushFree(obj)
252
253         def arrangeAll(self, positionOnly=False):
254                 oldList = self._objectList
255                 self._objectList = []
256                 for obj in oldList:
257                         obj.setPosition(numpy.array([0,0], numpy.float32))
258                         self.add(obj, positionOnly)
259
260         def centerAll(self):
261                 minPos = numpy.array([9999999,9999999], numpy.float32)
262                 maxPos = numpy.array([-9999999,-9999999], numpy.float32)
263                 for obj in self._objectList:
264                         pos = obj.getPosition()
265                         size = obj.getSize()
266                         minPos[0] = min(minPos[0], pos[0] - size[0] / 2)
267                         minPos[1] = min(minPos[1], pos[1] - size[1] / 2)
268                         maxPos[0] = max(maxPos[0], pos[0] + size[0] / 2)
269                         maxPos[1] = max(maxPos[1], pos[1] + size[1] / 2)
270                 offset = -(maxPos + minPos) / 2
271                 for obj in self._objectList:
272                         obj.setPosition(obj.getPosition() + offset)
273
274         def printOrder(self):
275                 if self._oneAtATime:
276                         order = _objectOrderFinder(self, self._leftToRight, self._frontToBack, self._gantryHeight).order
277                 else:
278                         order = None
279                 return order
280
281         #Check if two objects are hitting each-other (+ head space).
282         def _checkHit(self, a, b):
283                 if a == b:
284                         return False
285                 if self._oneAtATime:
286                         return polygon.polygonCollision(a._headAreaMinHull + a.getPosition(), b._boundaryHull + b.getPosition())
287                 else:
288                         return polygon.polygonCollision(a._boundaryHull + a.getPosition(), b._boundaryHull + b.getPosition())
289
290         def checkPlatform(self, obj):
291                 objectSink = profile.getProfileSettingFloat("object_sink")
292                 if profile.getPreference('startMode') == 'Simple':
293                         objectSink = float(profile.settingsDictionary["object_sink"].getDefault())
294
295                 area = obj._printAreaHull + obj.getPosition()
296                 if obj.getSize()[2] - objectSink > self._machineSize[2]:
297                         return False
298                 if not polygon.fullInside(area, self._machinePolygons[0]):
299                         return False
300                 #Check the "no go zones"
301                 for poly in self._machinePolygons[1:]:
302                         if polygon.polygonCollision(poly, area):
303                                 return False
304                 return True
305
306         def _findFreePositionFor(self, obj):
307                 posList = []
308                 for a in self._objectList:
309                         p = a.getPosition()
310                         if self._oneAtATime:
311                                 s = (a.getSize()[0:2] + obj.getSize()[0:2]) / 2 + self._sizeOffsets + self._headSizeOffsets + numpy.array([4,4], numpy.float32)
312                         else:
313                                 s = (a.getSize()[0:2] + obj.getSize()[0:2]) / 2 + numpy.array([4,4], numpy.float32)
314                         posList.append(p + s * ( 1.0, 1.0))
315                         posList.append(p + s * ( 0.0, 1.0))
316                         posList.append(p + s * (-1.0, 1.0))
317                         posList.append(p + s * ( 1.0, 0.0))
318                         posList.append(p + s * (-1.0, 0.0))
319                         posList.append(p + s * ( 1.0,-1.0))
320                         posList.append(p + s * ( 0.0,-1.0))
321                         posList.append(p + s * (-1.0,-1.0))
322
323                 best = None
324                 bestDist = None
325                 for p in posList:
326                         obj.setPosition(p)
327                         ok = True
328                         for a in self._objectList:
329                                 if self._checkHit(a, obj):
330                                         ok = False
331                                         break
332                         if not ok:
333                                 continue
334                         dist = numpy.linalg.norm(p)
335                         if not self.checkPlatform(obj):
336                                 dist *= 3
337                         if best is None or dist < bestDist:
338                                 best = p
339                                 bestDist = dist
340                 if best is not None:
341                         obj.setPosition(best)