chiark / gitweb /
Merge pull request #601 from CapnBry/reloadscene
[cura.git] / Cura / util / printableObject.py
1 """
2 The printableObject module contains a printableObject class,
3 which is used to represent a single object that can be printed.
4 A single object can have 1 or more meshes which represent different sections for multi-material extrusion.
5 """
6 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
7
8 import time
9 import math
10 import os
11
12 import numpy
13 numpy.seterr(all='ignore')
14
15 from Cura.util import polygon
16
17 class printableObject(object):
18         """
19         A printable object is an object that can be printed and is on the build platform.
20         It contains 1 or more Meshes. Where more meshes are used for multi-extrusion.
21
22         Each object has a 3x3 transformation matrix to rotate/scale the object.
23         This object also keeps track of the 2D boundary polygon used for object collision in the objectScene class.
24         """
25         def __init__(self, originFilename):
26                 self._originFilename = originFilename
27                 if originFilename is None:
28                         self._name = 'None'
29                 else:
30                         self._name = os.path.basename(originFilename)
31                 if '.' in self._name:
32                         self._name = os.path.splitext(self._name)[0]
33                 self._meshList = []
34                 self._position = numpy.array([0.0, 0.0])
35                 self._matrix = numpy.matrix([[1,0,0],[0,1,0],[0,0,1]], numpy.float64)
36                 self._transformedMin = None
37                 self._transformedMax = None
38                 self._transformedSize = None
39                 self._boundaryCircleSize = None
40                 self._drawOffset = None
41                 self._boundaryHull = None
42                 self._printAreaExtend = numpy.array([[-1,-1],[ 1,-1],[ 1, 1],[-1, 1]], numpy.float32)
43                 self._headAreaExtend = numpy.array([[-1,-1],[ 1,-1],[ 1, 1],[-1, 1]], numpy.float32)
44                 self._headMinSize = numpy.array([1, 1], numpy.float32)
45                 self._printAreaHull = None
46                 self._headAreaHull = None
47                 self._headAreaMinHull = None
48
49                 self._loadAnim = None
50
51         def copy(self):
52                 ret = printableObject(self._originFilename)
53                 ret._matrix = self._matrix.copy()
54                 ret._transformedMin = self._transformedMin.copy()
55                 ret._transformedMax = self._transformedMax.copy()
56                 ret._transformedSize = self._transformedSize.copy()
57                 ret._boundaryCircleSize = self._boundaryCircleSize
58                 ret._boundaryHull = self._boundaryHull.copy()
59                 ret._printAreaExtend = self._printAreaExtend.copy()
60                 ret._printAreaHull = self._printAreaHull.copy()
61                 ret._drawOffset = self._drawOffset.copy()
62                 for m in self._meshList[:]:
63                         m2 = ret._addMesh()
64                         m2.vertexes = m.vertexes
65                         m2.vertexCount = m.vertexCount
66                         m2.vbo = m.vbo
67                         m2.vbo.incRef()
68                 return ret
69
70         def _addMesh(self):
71                 m = mesh(self)
72                 self._meshList.append(m)
73                 return m
74
75         def _postProcessAfterLoad(self):
76                 for m in self._meshList:
77                         m._calculateNormals()
78                 self.processMatrix()
79                 if numpy.max(self.getSize()) > 10000.0:
80                         for m in self._meshList:
81                                 m.vertexes /= 1000.0
82                         self.processMatrix()
83                 if numpy.max(self.getSize()) < 1.0:
84                         for m in self._meshList:
85                                 m.vertexes *= 1000.0
86                         self.processMatrix()
87
88         def applyMatrix(self, m):
89                 self._matrix *= m
90                 self.processMatrix()
91
92         def processMatrix(self):
93                 self._transformedMin = numpy.array([999999999999,999999999999,999999999999], numpy.float64)
94                 self._transformedMax = numpy.array([-999999999999,-999999999999,-999999999999], numpy.float64)
95                 self._boundaryCircleSize = 0
96
97                 hull = numpy.zeros((0, 2), numpy.int)
98                 for m in self._meshList:
99                         transformedVertexes = m.getTransformedVertexes()
100                         hull = polygon.convexHull(numpy.concatenate((numpy.rint(transformedVertexes[:,0:2]).astype(int), hull), 0))
101                         transformedMin = transformedVertexes.min(0)
102                         transformedMax = transformedVertexes.max(0)
103                         for n in xrange(0, 3):
104                                 self._transformedMin[n] = min(transformedMin[n], self._transformedMin[n])
105                                 self._transformedMax[n] = max(transformedMax[n], self._transformedMax[n])
106
107                         #Calculate the boundary circle
108                         transformedSize = transformedMax - transformedMin
109                         center = transformedMin + transformedSize / 2.0
110                         boundaryCircleSize = round(math.sqrt(numpy.max(((transformedVertexes[::,0] - center[0]) * (transformedVertexes[::,0] - center[0])) + ((transformedVertexes[::,1] - center[1]) * (transformedVertexes[::,1] - center[1])) + ((transformedVertexes[::,2] - center[2]) * (transformedVertexes[::,2] - center[2])))), 3)
111                         self._boundaryCircleSize = max(self._boundaryCircleSize, boundaryCircleSize)
112                 self._transformedSize = self._transformedMax - self._transformedMin
113                 self._drawOffset = (self._transformedMax + self._transformedMin) / 2
114                 self._drawOffset[2] = self._transformedMin[2]
115                 self._transformedMax -= self._drawOffset
116                 self._transformedMin -= self._drawOffset
117
118                 self._boundaryHull = polygon.minkowskiHull((hull.astype(numpy.float32) - self._drawOffset[0:2]), numpy.array([[-1,-1],[-1,1],[1,1],[1,-1]],numpy.float32))
119                 self._printAreaHull = polygon.minkowskiHull(self._boundaryHull, self._printAreaExtend)
120                 self.setHeadArea(self._headAreaExtend, self._headMinSize)
121
122         def getName(self):
123                 return self._name
124         def getOriginFilename(self):
125                 return self._originFilename
126         def getPosition(self):
127                 return self._position
128         def setPosition(self, newPos):
129                 self._position = newPos
130         def getMatrix(self):
131                 return self._matrix
132
133         def getMaximum(self):
134                 return self._transformedMax
135         def getMinimum(self):
136                 return self._transformedMin
137         def getSize(self):
138                 return self._transformedSize
139         def getDrawOffset(self):
140                 return self._drawOffset
141         def getBoundaryCircle(self):
142                 return self._boundaryCircleSize
143
144         def setPrintAreaExtends(self, poly):
145                 self._printAreaExtend = poly
146                 self._printAreaHull = polygon.minkowskiHull(self._boundaryHull, self._printAreaExtend)
147
148                 self.setHeadArea(self._headAreaExtend, self._headMinSize)
149
150         def setHeadArea(self, poly, minSize):
151                 self._headAreaExtend = poly
152                 self._headMinSize = minSize
153                 self._headAreaHull = polygon.minkowskiHull(self._printAreaHull, self._headAreaExtend)
154                 pMin = numpy.min(self._printAreaHull, 0) - self._headMinSize
155                 pMax = numpy.max(self._printAreaHull, 0) + self._headMinSize
156                 square = numpy.array([pMin, [pMin[0], pMax[1]], pMax, [pMax[0], pMin[1]]], numpy.float32)
157                 self._headAreaMinHull = polygon.clipConvex(self._headAreaHull, square)
158
159         def mirror(self, axis):
160                 matrix = [[1,0,0], [0, 1, 0], [0, 0, 1]]
161                 matrix[axis][axis] = -1
162                 self.applyMatrix(numpy.matrix(matrix, numpy.float64))
163
164         def getScale(self):
165                 return numpy.array([
166                         numpy.linalg.norm(self._matrix[::,0].getA().flatten()),
167                         numpy.linalg.norm(self._matrix[::,1].getA().flatten()),
168                         numpy.linalg.norm(self._matrix[::,2].getA().flatten())], numpy.float64);
169
170         def setScale(self, scale, axis, uniform):
171                 currentScale = numpy.linalg.norm(self._matrix[::,axis].getA().flatten())
172                 scale /= currentScale
173                 if scale == 0:
174                         return
175                 if uniform:
176                         matrix = [[scale,0,0], [0, scale, 0], [0, 0, scale]]
177                 else:
178                         matrix = [[1.0,0,0], [0, 1.0, 0], [0, 0, 1.0]]
179                         matrix[axis][axis] = scale
180                 self.applyMatrix(numpy.matrix(matrix, numpy.float64))
181
182         def setSize(self, size, axis, uniform):
183                 scale = self.getSize()[axis]
184                 scale = size / scale
185                 if scale == 0:
186                         return
187                 if uniform:
188                         matrix = [[scale,0,0], [0, scale, 0], [0, 0, scale]]
189                 else:
190                         matrix = [[1,0,0], [0, 1, 0], [0, 0, 1]]
191                         matrix[axis][axis] = scale
192                 self.applyMatrix(numpy.matrix(matrix, numpy.float64))
193
194         def resetScale(self):
195                 x = 1/numpy.linalg.norm(self._matrix[::,0].getA().flatten())
196                 y = 1/numpy.linalg.norm(self._matrix[::,1].getA().flatten())
197                 z = 1/numpy.linalg.norm(self._matrix[::,2].getA().flatten())
198                 self.applyMatrix(numpy.matrix([[x,0,0],[0,y,0],[0,0,z]], numpy.float64))
199
200         def resetRotation(self):
201                 x = numpy.linalg.norm(self._matrix[::,0].getA().flatten())
202                 y = numpy.linalg.norm(self._matrix[::,1].getA().flatten())
203                 z = numpy.linalg.norm(self._matrix[::,2].getA().flatten())
204                 self._matrix = numpy.matrix([[x,0,0],[0,y,0],[0,0,z]], numpy.float64)
205                 self.processMatrix()
206
207         def layFlat(self):
208                 transformedVertexes = self._meshList[0].getTransformedVertexes()
209                 minZvertex = transformedVertexes[transformedVertexes.argmin(0)[2]]
210                 dotMin = 1.0
211                 dotV = None
212                 for v in transformedVertexes:
213                         diff = v - minZvertex
214                         len = math.sqrt(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2])
215                         if len < 5:
216                                 continue
217                         dot = (diff[2] / len)
218                         if dotMin > dot:
219                                 dotMin = dot
220                                 dotV = diff
221                 if dotV is None:
222                         return
223                 rad = -math.atan2(dotV[1], dotV[0])
224                 self._matrix *= numpy.matrix([[math.cos(rad), math.sin(rad), 0], [-math.sin(rad), math.cos(rad), 0], [0,0,1]], numpy.float64)
225                 rad = -math.asin(dotMin)
226                 self._matrix *= numpy.matrix([[math.cos(rad), 0, math.sin(rad)], [0,1,0], [-math.sin(rad), 0, math.cos(rad)]], numpy.float64)
227
228
229                 transformedVertexes = self._meshList[0].getTransformedVertexes()
230                 minZvertex = transformedVertexes[transformedVertexes.argmin(0)[2]]
231                 dotMin = 1.0
232                 dotV = None
233                 for v in transformedVertexes:
234                         diff = v - minZvertex
235                         len = math.sqrt(diff[1] * diff[1] + diff[2] * diff[2])
236                         if len < 5:
237                                 continue
238                         dot = (diff[2] / len)
239                         if dotMin > dot:
240                                 dotMin = dot
241                                 dotV = diff
242                 if dotV is None:
243                         return
244                 if dotV[1] < 0:
245                         rad = math.asin(dotMin)
246                 else:
247                         rad = -math.asin(dotMin)
248                 self.applyMatrix(numpy.matrix([[1,0,0], [0, math.cos(rad), math.sin(rad)], [0, -math.sin(rad), math.cos(rad)]], numpy.float64))
249
250         def scaleUpTo(self, size):
251                 vMin = self._transformedMin
252                 vMax = self._transformedMax
253
254                 scaleX1 = (size[0] / 2 - self._position[0]) / ((vMax[0] - vMin[0]) / 2)
255                 scaleY1 = (size[1] / 2 - self._position[1]) / ((vMax[1] - vMin[1]) / 2)
256                 scaleX2 = (self._position[0] + size[0] / 2) / ((vMax[0] - vMin[0]) / 2)
257                 scaleY2 = (self._position[1] + size[1] / 2) / ((vMax[1] - vMin[1]) / 2)
258                 scaleZ = size[2] / (vMax[2] - vMin[2])
259                 scale = min(scaleX1, scaleY1, scaleX2, scaleY2, scaleZ)
260                 if scale > 0:
261                         self.applyMatrix(numpy.matrix([[scale,0,0],[0,scale,0],[0,0,scale]], numpy.float64))
262
263         #Split splits an object with multiple meshes into different objects, where each object is a part of the original mesh that has
264         # connected faces. This is useful to split up plate STL files.
265         def split(self, callback):
266                 ret = []
267                 for oriMesh in self._meshList:
268                         ret += oriMesh.split(callback)
269                 return ret
270
271         def canStoreAsSTL(self):
272                 return len(self._meshList) < 2
273
274         #getVertexIndexList returns an array of vertexes, and an integer array for each mesh in this object.
275         # the integer arrays are indexes into the vertex array for each triangle in the model.
276         def getVertexIndexList(self):
277                 vertexMap = {}
278                 vertexList = []
279                 meshList = []
280                 for m in self._meshList:
281                         verts = m.getTransformedVertexes(True)
282                         meshIdxList = []
283                         for idx in xrange(0, len(verts)):
284                                 v = verts[idx]
285                                 hashNr = int(v[0] * 100) | int(v[1] * 100) << 10 | int(v[2] * 100) << 20
286                                 vIdx = None
287                                 if hashNr in vertexMap:
288                                         for idx2 in vertexMap[hashNr]:
289                                                 if numpy.linalg.norm(v - vertexList[idx2]) < 0.001:
290                                                         vIdx = idx2
291                                 if vIdx is None:
292                                         vIdx = len(vertexList)
293                                         vertexMap[hashNr] = [vIdx]
294                                         vertexList.append(v)
295                                 meshIdxList.append(vIdx)
296                         meshList.append(numpy.array(meshIdxList, numpy.int32))
297                 return numpy.array(vertexList, numpy.float32), meshList
298
299 class mesh(object):
300         """
301         A mesh is a list of 3D triangles build from vertexes. Each triangle has 3 vertexes.
302
303         A "VBO" can be associated with this object, which is used for rendering this object.
304         """
305         def __init__(self, obj):
306                 self.vertexes = None
307                 self.vertexCount = 0
308                 self.vbo = None
309                 self._obj = obj
310
311         def _addFace(self, x0, y0, z0, x1, y1, z1, x2, y2, z2):
312                 n = self.vertexCount
313                 self.vertexes[n][0] = x0
314                 self.vertexes[n][1] = y0
315                 self.vertexes[n][2] = z0
316                 n += 1
317                 self.vertexes[n][0] = x1
318                 self.vertexes[n][1] = y1
319                 self.vertexes[n][2] = z1
320                 n += 1
321                 self.vertexes[n][0] = x2
322                 self.vertexes[n][1] = y2
323                 self.vertexes[n][2] = z2
324                 self.vertexCount += 3
325         
326         def _prepareFaceCount(self, faceNumber):
327                 #Set the amount of faces before loading data in them. This way we can create the numpy arrays before we fill them.
328                 self.vertexes = numpy.zeros((faceNumber*3, 3), numpy.float32)
329                 self.normal = numpy.zeros((faceNumber*3, 3), numpy.float32)
330                 self.vertexCount = 0
331
332         def _calculateNormals(self):
333                 #Calculate the normals
334                 tris = self.vertexes.reshape(self.vertexCount / 3, 3, 3)
335                 normals = numpy.cross( tris[::,1 ] - tris[::,0]  , tris[::,2 ] - tris[::,0] )
336                 lens = numpy.sqrt( normals[:,0]**2 + normals[:,1]**2 + normals[:,2]**2 )
337                 normals[:,0] /= lens
338                 normals[:,1] /= lens
339                 normals[:,2] /= lens
340                 
341                 n = numpy.zeros((self.vertexCount / 3, 9), numpy.float32)
342                 n[:,0:3] = normals
343                 n[:,3:6] = normals
344                 n[:,6:9] = normals
345                 self.normal = n.reshape(self.vertexCount, 3)
346                 self.invNormal = -self.normal
347
348         def _vertexHash(self, idx):
349                 v = self.vertexes[idx]
350                 return int(v[0] * 100) | int(v[1] * 100) << 10 | int(v[2] * 100) << 20
351
352         def _idxFromHash(self, map, idx):
353                 vHash = self._vertexHash(idx)
354                 for i in map[vHash]:
355                         if numpy.linalg.norm(self.vertexes[i] - self.vertexes[idx]) < 0.001:
356                                 return i
357
358         def getTransformedVertexes(self, applyOffsets = False):
359                 if applyOffsets:
360                         pos = self._obj._position.copy()
361                         pos.resize((3))
362                         pos[2] = self._obj.getSize()[2] / 2
363                         offset = self._obj._drawOffset.copy()
364                         offset[2] += self._obj.getSize()[2] / 2
365                         return (numpy.matrix(self.vertexes, copy = False) * numpy.matrix(self._obj._matrix, numpy.float32)).getA() - offset + pos
366                 return (numpy.matrix(self.vertexes, copy = False) * numpy.matrix(self._obj._matrix, numpy.float32)).getA()
367
368         def split(self, callback):
369                 vertexMap = {}
370
371                 vertexToFace = []
372                 for idx in xrange(0, self.vertexCount):
373                         if (idx % 100) == 0:
374                                 callback(idx * 100 / self.vertexCount)
375                         vHash = self._vertexHash(idx)
376                         if vHash not in vertexMap:
377                                 vertexMap[vHash] = []
378                         vertexMap[vHash].append(idx)
379                         vertexToFace.append([])
380
381                 faceList = []
382                 for idx in xrange(0, self.vertexCount, 3):
383                         if (idx % 100) == 0:
384                                 callback(idx * 100 / self.vertexCount)
385                         f = [self._idxFromHash(vertexMap, idx), self._idxFromHash(vertexMap, idx+1), self._idxFromHash(vertexMap, idx+2)]
386                         vertexToFace[f[0]].append(idx / 3)
387                         vertexToFace[f[1]].append(idx / 3)
388                         vertexToFace[f[2]].append(idx / 3)
389                         faceList.append(f)
390
391                 ret = []
392                 doneSet = set()
393                 for idx in xrange(0, len(faceList)):
394                         if idx in doneSet:
395                                 continue
396                         doneSet.add(idx)
397                         todoList = [idx]
398                         meshFaceList = []
399                         while len(todoList) > 0:
400                                 idx = todoList.pop()
401                                 meshFaceList.append(idx)
402                                 for n in xrange(0, 3):
403                                         for i in vertexToFace[faceList[idx][n]]:
404                                                 if not i in doneSet:
405                                                         doneSet.add(i)
406                                                         todoList.append(i)
407
408                         obj = printableObject(self._obj.getOriginFilename())
409                         obj._matrix = self._obj._matrix.copy()
410                         m = obj._addMesh()
411                         m._prepareFaceCount(len(meshFaceList))
412                         for idx in meshFaceList:
413                                 m.vertexes[m.vertexCount] = self.vertexes[faceList[idx][0]]
414                                 m.vertexCount += 1
415                                 m.vertexes[m.vertexCount] = self.vertexes[faceList[idx][1]]
416                                 m.vertexCount += 1
417                                 m.vertexes[m.vertexCount] = self.vertexes[faceList[idx][2]]
418                                 m.vertexCount += 1
419                         obj._postProcessAfterLoad()
420                         ret.append(obj)
421                 return ret