chiark / gitweb /
Merge remote-tracking branch 'origin/new-settings' into new-settings
[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("Info: Print one at a time mode disabled. Object too tall.")
182                                                 break
183
184                 if self._lastOneAtATime and self._oneAtATime and not self._lastResultOneAtATime:
185                         if self._sceneView:
186                                 self._sceneView.notification.message("Info: Print one at a time mode re-enabled.")
187
188                 self._lastResultOneAtATime = self._oneAtATime
189                 self._lastOneAtATime = printOneAtATime
190
191                 headArea = numpy.array([[-xMin,-yMin],[ xMax,-yMin],[ xMax, yMax],[-xMin, yMax]], numpy.float32)
192
193                 if obj is None:
194                         for obj in self._objectList:
195                                 obj.setHeadArea(headArea, self._headSizeOffsets)
196                 else:
197                         obj.setHeadArea(headArea, self._headSizeOffsets)
198
199         def isOneAtATime(self):
200                 return self._oneAtATime
201
202         def setExtruderOffset(self, extruderNr, offsetX, offsetY):
203                 self._extruderOffset[extruderNr] = numpy.array([offsetX, offsetY], numpy.float32)
204
205         def objects(self):
206                 return self._objectList
207
208         #Add new object to print area
209         def add(self, obj, positionOnly=False):
210                 if not positionOnly and numpy.max(obj.getSize()[0:2]) > numpy.max(self._machineSize[0:2]) * 2.5:
211                         scale = numpy.max(self._machineSize[0:2]) * 2.5 / numpy.max(obj.getSize()[0:2])
212                         matrix = [[scale,0,0], [0, scale, 0], [0, 0, scale]]
213                         obj.applyMatrix(numpy.matrix(matrix, numpy.float64))
214                 self._findFreePositionFor(obj)
215                 self._objectList.append(obj)
216                 self.updateHeadSize(obj)
217                 self.updateSizeOffsets(True)
218                 self.pushFree(obj)
219
220         def remove(self, obj):
221                 self._objectList.remove(obj)
222
223         #Dual(multiple) extrusion merge
224         def merge(self, obj1, obj2):
225                 self.remove(obj2)
226                 obj1._meshList += obj2._meshList
227                 for m in obj2._meshList:
228                         m._obj = obj1
229                 obj1.processMatrix()
230                 obj1.setPosition((obj1.getPosition() + obj2.getPosition()) / 2)
231                 self.pushFree(obj1)
232
233         def pushFree(self, staticObj = None):
234                 if staticObj is None:
235                         for obj in self._objectList:
236                                 self.pushFree(obj)
237                         return
238                 if not self.checkPlatform(staticObj):
239                         return
240                 pushList = []
241                 for obj in self._objectList:
242                         if obj == staticObj or not self.checkPlatform(obj):
243                                 continue
244                         if self._oneAtATime:
245                                 v = polygon.polygonCollisionPushVector(obj._headAreaMinHull + obj.getPosition(), staticObj._boundaryHull + staticObj.getPosition())
246                         else:
247                                 v = polygon.polygonCollisionPushVector(obj._boundaryHull + obj.getPosition(), staticObj._boundaryHull + staticObj.getPosition())
248                         if type(v) is bool:
249                                 continue
250                         obj.setPosition(obj.getPosition() + v * 1.01)
251                         pushList.append(obj)
252                 for obj in pushList:
253                         self.pushFree(obj)
254
255         def arrangeAll(self, positionOnly=False):
256                 oldList = self._objectList
257                 self._objectList = []
258                 for obj in oldList:
259                         obj.setPosition(numpy.array([0,0], numpy.float32))
260                         self.add(obj, positionOnly)
261
262         def centerAll(self):
263                 minPos = numpy.array([9999999,9999999], numpy.float32)
264                 maxPos = numpy.array([-9999999,-9999999], numpy.float32)
265                 for obj in self._objectList:
266                         pos = obj.getPosition()
267                         size = obj.getSize()
268                         minPos[0] = min(minPos[0], pos[0] - size[0] / 2)
269                         minPos[1] = min(minPos[1], pos[1] - size[1] / 2)
270                         maxPos[0] = max(maxPos[0], pos[0] + size[0] / 2)
271                         maxPos[1] = max(maxPos[1], pos[1] + size[1] / 2)
272                 offset = -(maxPos + minPos) / 2
273                 for obj in self._objectList:
274                         obj.setPosition(obj.getPosition() + offset)
275
276         def printOrder(self):
277                 if self._oneAtATime:
278                         order = _objectOrderFinder(self, self._leftToRight, self._frontToBack, self._gantryHeight).order
279                 else:
280                         order = None
281                 return order
282
283         #Check if two objects are hitting each-other (+ head space).
284         def _checkHit(self, a, b):
285                 if a == b:
286                         return False
287                 if self._oneAtATime:
288                         return polygon.polygonCollision(a._headAreaMinHull + a.getPosition(), b._boundaryHull + b.getPosition())
289                 else:
290                         return polygon.polygonCollision(a._boundaryHull + a.getPosition(), b._boundaryHull + b.getPosition())
291
292         def checkPlatform(self, obj):
293                 objectSink = profile.getProfileSettingFloat("object_sink")
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)