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