chiark / gitweb /
Add uppercase STL and HEX to file dialog filters for linux/MacOS
[cura.git] / Cura / fabmetheus_utilities / geometry / creation / extrude.py
1 """
2 Boolean geometry extrusion.
3
4 """
5
6 from __future__ import absolute_import
7 #Init has to be imported first because it has code to workaround the python bug where relative imports don't work if the module is imported as a main module.
8 import __init__
9
10 from fabmetheus_utilities.geometry.creation import lineation
11 from fabmetheus_utilities.geometry.creation import solid
12 from fabmetheus_utilities.geometry.geometry_utilities.evaluate_elements import setting
13 from fabmetheus_utilities.geometry.geometry_utilities import evaluate
14 from fabmetheus_utilities.geometry.solids import triangle_mesh
15 from fabmetheus_utilities.vector3 import Vector3
16 from fabmetheus_utilities.vector3index import Vector3Index
17 from fabmetheus_utilities import euclidean
18 import math
19
20
21 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
22 __credits__ = 'Art of Illusion <http://www.artofillusion.org/>'
23 __date__ = '$Date: 2008/02/05 $'
24 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
25
26
27 def addLoop(derivation, endMultiplier, loopLists, path, portionDirectionIndex, portionDirections, vertexes):
28         'Add an indexed loop to the vertexes.'
29         portionDirection = portionDirections[ portionDirectionIndex ]
30         if portionDirection.directionReversed == True:
31                 loopLists.append([])
32         loops = loopLists[-1]
33         interpolationOffset = derivation.interpolationDictionary['offset']
34         offset = interpolationOffset.getVector3ByPortion( portionDirection )
35         if endMultiplier != None:
36                 if portionDirectionIndex == 0:
37                         setOffsetByMultiplier( interpolationOffset.path[1], interpolationOffset.path[0], endMultiplier, offset )
38                 elif portionDirectionIndex == len( portionDirections ) - 1:
39                         setOffsetByMultiplier( interpolationOffset.path[-2], interpolationOffset.path[-1], endMultiplier, offset )
40         scale = derivation.interpolationDictionary['scale'].getComplexByPortion( portionDirection )
41         twist = derivation.interpolationDictionary['twist'].getYByPortion( portionDirection )
42         projectiveSpace = euclidean.ProjectiveSpace()
43         if derivation.tiltTop == None:
44                 tilt = derivation.interpolationDictionary['tilt'].getComplexByPortion( portionDirection )
45                 projectiveSpace = projectiveSpace.getByTilt( tilt )
46         else:
47                 normals = getNormals( interpolationOffset, offset, portionDirection )
48                 normalFirst = normals[0]
49                 normalAverage = getNormalAverage(normals)
50                 if derivation.tiltFollow and derivation.oldProjectiveSpace != None:
51                         projectiveSpace = derivation.oldProjectiveSpace.getNextSpace( normalAverage )
52                 else:
53                         projectiveSpace = projectiveSpace.getByBasisZTop( normalAverage, derivation.tiltTop )
54                 derivation.oldProjectiveSpace = projectiveSpace
55                 projectiveSpace.unbuckle( derivation.maximumUnbuckling, normalFirst )
56         projectiveSpace = projectiveSpace.getSpaceByXYScaleAngle( twist, scale )
57         loop = []
58         if ( abs( projectiveSpace.basisX ) + abs( projectiveSpace.basisY ) ) < 0.0001:
59                 vector3Index = Vector3Index(len(vertexes))
60                 addOffsetAddToLists( loop, offset, vector3Index, vertexes )
61                 loops.append(loop)
62                 return
63         for point in path:
64                 vector3Index = Vector3Index(len(vertexes))
65                 projectedVertex = projectiveSpace.getVector3ByPoint(point)
66                 vector3Index.setToVector3( projectedVertex )
67                 addOffsetAddToLists( loop, offset, vector3Index, vertexes )
68         loops.append(loop)
69
70 def addNegatives(derivation, negatives, paths):
71         'Add pillars output to negatives.'
72         portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
73         for path in paths:
74                 loopLists = getLoopListsByPath(derivation, 1.000001, path, portionDirections)
75                 geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
76                 negatives.append(geometryOutput)
77
78 def addNegativesPositives(derivation, negatives, paths, positives):
79         'Add pillars output to negatives and positives.'
80         portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
81         for path in paths:
82                 endMultiplier = None
83                 if not euclidean.getIsWiddershinsByVector3(path):
84                         endMultiplier = 1.000001
85                 loopLists = getLoopListsByPath(derivation, endMultiplier, path, portionDirections)
86                 geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
87                 if endMultiplier == None:
88                         positives.append(geometryOutput)
89                 else:
90                         negatives.append(geometryOutput)
91
92 def addOffsetAddToLists(loop, offset, vector3Index, vertexes):
93         'Add an indexed loop to the vertexes.'
94         vector3Index += offset
95         loop.append(vector3Index)
96         vertexes.append(vector3Index)
97
98 def addPositives(derivation, paths, positives):
99         'Add pillars output to positives.'
100         portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
101         for path in paths:
102                 loopLists = getLoopListsByPath(derivation, None, path, portionDirections)
103                 geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
104                 positives.append(geometryOutput)
105
106 def addSpacedPortionDirection( portionDirection, spacedPortionDirections ):
107         'Add spaced portion directions.'
108         lastSpacedPortionDirection = spacedPortionDirections[-1]
109         if portionDirection.portion - lastSpacedPortionDirection.portion > 0.003:
110                 spacedPortionDirections.append( portionDirection )
111                 return
112         if portionDirection.directionReversed > lastSpacedPortionDirection.directionReversed:
113                 spacedPortionDirections.append( portionDirection )
114
115 def addTwistPortions( interpolationTwist, remainderPortionDirection, twistPrecision ):
116         'Add twist portions.'
117         lastPortionDirection = interpolationTwist.portionDirections[-1]
118         if remainderPortionDirection.portion == lastPortionDirection.portion:
119                 return
120         lastTwist = interpolationTwist.getYByPortion( lastPortionDirection )
121         remainderTwist = interpolationTwist.getYByPortion( remainderPortionDirection )
122         twistSegments = int( math.floor( abs( remainderTwist - lastTwist ) / twistPrecision ) )
123         if twistSegments < 1:
124                 return
125         portionDifference = remainderPortionDirection.portion - lastPortionDirection.portion
126         twistSegmentsPlusOne = float( twistSegments + 1 )
127         for twistSegment in xrange( twistSegments ):
128                 additionalPortion = portionDifference * float( twistSegment + 1 ) / twistSegmentsPlusOne
129                 portionDirection = PortionDirection( lastPortionDirection.portion + additionalPortion )
130                 interpolationTwist.portionDirections.append( portionDirection )
131
132 def comparePortionDirection( portionDirection, otherPortionDirection ):
133         'Comparison in order to sort portion directions in ascending order of portion then direction.'
134         if portionDirection.portion > otherPortionDirection.portion:
135                 return 1
136         if portionDirection.portion < otherPortionDirection.portion:
137                 return - 1
138         if portionDirection.directionReversed < otherPortionDirection.directionReversed:
139                 return - 1
140         return portionDirection.directionReversed > otherPortionDirection.directionReversed
141
142 def getGeometryOutput(derivation, elementNode):
143         'Get triangle mesh from attribute dictionary.'
144         if derivation == None:
145                 derivation = ExtrudeDerivation(elementNode)
146         if len(euclidean.getConcatenatedList(derivation.target)) == 0:
147                 print('Warning, in extrude there are no paths.')
148                 print(elementNode.attributes)
149                 return None
150         return getGeometryOutputByLoops(derivation, derivation.target)
151
152 def getGeometryOutputByArguments(arguments, elementNode):
153         'Get triangle mesh from attribute dictionary by arguments.'
154         return getGeometryOutput(None, elementNode)
155
156 def getGeometryOutputByLoops(derivation, loops):
157         'Get geometry output by sorted, nested loops.'
158         loops.sort(key=euclidean.getAreaVector3LoopAbsolute, reverse=True)
159         complexLoops = euclidean.getComplexPaths(loops)
160         nestedRings = []
161         for loopIndex, loop in enumerate(loops):
162                 complexLoop = complexLoops[loopIndex]
163                 leftPoint = euclidean.getLeftPoint(complexLoop)
164                 isInFilledRegion = euclidean.getIsInFilledRegion(complexLoops[: loopIndex] + complexLoops[loopIndex + 1 :], leftPoint)
165                 if isInFilledRegion == euclidean.isWiddershins(complexLoop):
166                         loop.reverse()
167                 nestedRing = euclidean.NestedRing()
168                 nestedRing.boundary = complexLoop
169                 nestedRing.vector3Loop = loop
170                 nestedRings.append(nestedRing)
171         nestedRings = euclidean.getOrderedNestedRings(nestedRings)
172         nestedRings = euclidean.getFlattenedNestedRings(nestedRings)
173         portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
174         if len(nestedRings) < 1:
175                 return {}
176         if len(nestedRings) == 1:
177                 geometryOutput = getGeometryOutputByNestedRing(derivation, nestedRings[0], portionDirections)
178                 return solid.getGeometryOutputByManipulation(derivation.elementNode, geometryOutput)
179         shapes = []
180         for nestedRing in nestedRings:
181                 shapes.append(getGeometryOutputByNestedRing(derivation, nestedRing, portionDirections))
182         return solid.getGeometryOutputByManipulation(derivation.elementNode, {'union' : {'shapes' : shapes}})
183
184 def getGeometryOutputByNegativesPositives(elementNode, negatives, positives):
185         'Get triangle mesh from elementNode, negatives and positives.'
186         positiveOutput = triangle_mesh.getUnifiedOutput(positives)
187         if len(negatives) < 1:
188                 return solid.getGeometryOutputByManipulation(elementNode, positiveOutput)
189         if len(positives) < 1:
190                 negativeOutput = triangle_mesh.getUnifiedOutput(negatives)
191                 return solid.getGeometryOutputByManipulation(elementNode, negativeOutput)
192         return solid.getGeometryOutputByManipulation(elementNode, {'difference' : {'shapes' : [positiveOutput] + negatives}})
193
194 def getGeometryOutputByNestedRing(derivation, nestedRing, portionDirections):
195         'Get geometry output by sorted, nested loops.'
196         loopLists = getLoopListsByPath(derivation, None, nestedRing.vector3Loop, portionDirections)
197         outsideOutput = triangle_mesh.getPillarsOutput(loopLists)
198         if len(nestedRing.innerNestedRings) < 1:
199                 return outsideOutput
200         shapes = [outsideOutput]
201         for nestedRing.innerNestedRing in nestedRing.innerNestedRings:
202                 loopLists = getLoopListsByPath(derivation, 1.000001, nestedRing.innerNestedRing.vector3Loop, portionDirections)
203                 shapes.append(triangle_mesh.getPillarsOutput(loopLists))
204         return {'difference' : {'shapes' : shapes}}
205
206 def getLoopListsByPath(derivation, endMultiplier, path, portionDirections):
207         'Get loop lists from path.'
208         vertexes = []
209         loopLists = [[]]
210         derivation.oldProjectiveSpace = None
211         for portionDirectionIndex in xrange(len(portionDirections)):
212                 addLoop(derivation, endMultiplier, loopLists, path, portionDirectionIndex, portionDirections, vertexes)
213         return loopLists
214
215 def getNewDerivation(elementNode):
216         'Get new derivation.'
217         return ExtrudeDerivation(elementNode)
218
219 def getNormalAverage(normals):
220         'Get normal.'
221         if len(normals) < 2:
222                 return normals[0]
223         return (normals[0] + normals[1]).getNormalized()
224
225 def getNormals( interpolationOffset, offset, portionDirection ):
226         'Get normals.'
227         normals = []
228         portionFrom = portionDirection.portion - 0.0001
229         portionTo = portionDirection.portion + 0.0001
230         if portionFrom >= 0.0:
231                 normals.append( ( offset - interpolationOffset.getVector3ByPortion( PortionDirection( portionFrom ) ) ).getNormalized() )
232         if portionTo <= 1.0:
233                 normals.append( ( interpolationOffset.getVector3ByPortion( PortionDirection( portionTo ) ) - offset ).getNormalized() )
234         return normals
235
236 def getSpacedPortionDirections( interpolationDictionary ):
237         'Get sorted portion directions.'
238         portionDirections = []
239         for interpolationDictionaryValue in interpolationDictionary.values():
240                 portionDirections += interpolationDictionaryValue.portionDirections
241         portionDirections.sort( comparePortionDirection )
242         if len( portionDirections ) < 1:
243                 return []
244         spacedPortionDirections = [ portionDirections[0] ]
245         for portionDirection in portionDirections[1 :]:
246                 addSpacedPortionDirection( portionDirection, spacedPortionDirections )
247         return spacedPortionDirections
248
249 def insertTwistPortions(derivation, elementNode):
250         'Insert twist portions and radian the twist.'
251         interpolationDictionary = derivation.interpolationDictionary
252         interpolationTwist = Interpolation().getByPrefixX(elementNode, derivation.twistPathDefault, 'twist')
253         interpolationDictionary['twist'] = interpolationTwist
254         for point in interpolationTwist.path:
255                 point.y = math.radians(point.y)
256         remainderPortionDirections = interpolationTwist.portionDirections[1 :]
257         interpolationTwist.portionDirections = [interpolationTwist.portionDirections[0]]
258         if elementNode != None:
259                 twistPrecision = setting.getTwistPrecisionRadians(elementNode)
260         for remainderPortionDirection in remainderPortionDirections:
261                 addTwistPortions(interpolationTwist, remainderPortionDirection, twistPrecision)
262                 interpolationTwist.portionDirections.append(remainderPortionDirection)
263
264 def processElementNode(elementNode):
265         'Process the xml element.'
266         solid.processElementNodeByGeometry(elementNode, getGeometryOutput(None, elementNode))
267
268 def setElementNodeToEndStart(elementNode, end, start):
269         'Set elementNode attribute dictionary to a tilt following path from the start to end.'
270         elementNode.attributes['path'] = [start, end]
271         elementNode.attributes['tiltFollow'] = 'true'
272         elementNode.attributes['tiltTop'] = Vector3(0.0, 0.0, 1.0)
273
274 def setOffsetByMultiplier(begin, end, multiplier, offset):
275         'Set the offset by the multiplier.'
276         segment = end - begin
277         delta = segment * multiplier - segment
278         offset.setToVector3(offset + delta)
279
280
281 class ExtrudeDerivation:
282         'Class to hold extrude variables.'
283         def __init__(self, elementNode):
284                 'Initialize.'
285                 self.elementNode = elementNode
286                 self.interpolationDictionary = {}
287                 self.tiltFollow = evaluate.getEvaluatedBoolean(True, elementNode, 'tiltFollow')
288                 self.tiltTop = evaluate.getVector3ByPrefix(None, elementNode, 'tiltTop')
289                 self.maximumUnbuckling = evaluate.getEvaluatedFloat(5.0, elementNode, 'maximumUnbuckling')
290                 scalePathDefault = [Vector3(1.0, 1.0, 0.0), Vector3(1.0, 1.0, 1.0)]
291                 self.interpolationDictionary['scale'] = Interpolation().getByPrefixZ(elementNode, scalePathDefault, 'scale')
292                 self.target = evaluate.getTransformedPathsByKey([], elementNode, 'target')
293                 if self.tiltTop == None:
294                         offsetPathDefault = [Vector3(), Vector3(0.0, 0.0, 1.0)]
295                         self.interpolationDictionary['offset'] = Interpolation().getByPrefixZ(elementNode, offsetPathDefault, '')
296                         tiltPathDefault = [Vector3(), Vector3(0.0, 0.0, 1.0)]
297                         self.interpolationDictionary['tilt'] = Interpolation().getByPrefixZ(elementNode, tiltPathDefault, 'tilt')
298                         for point in self.interpolationDictionary['tilt'].path:
299                                 point.x = math.radians(point.x)
300                                 point.y = math.radians(point.y)
301                 else:
302                         offsetAlongDefault = [Vector3(), Vector3(1.0, 0.0, 0.0)]
303                         self.interpolationDictionary['offset'] = Interpolation().getByPrefixAlong(elementNode, offsetAlongDefault, '')
304                 self.twist = evaluate.getEvaluatedFloat(0.0, elementNode, 'twist')
305                 self.twistPathDefault = [Vector3(), Vector3(1.0, self.twist) ]
306                 insertTwistPortions(self, elementNode)
307
308
309 class Interpolation:
310         'Class to interpolate a path.'
311         def __init__(self):
312                 'Set index.'
313                 self.interpolationIndex = 0
314
315         def __repr__(self):
316                 'Get the string representation of this Interpolation.'
317                 return str(self.__dict__)
318
319         def getByDistances(self):
320                 'Get by distances.'
321                 beginDistance = self.distances[0]
322                 self.interpolationLength = self.distances[-1] - beginDistance
323                 self.close = abs(0.000001 * self.interpolationLength)
324                 self.portionDirections = []
325                 oldDistance = -self.interpolationLength # so the difference should not be close
326                 for distance in self.distances:
327                         deltaDistance = distance - beginDistance
328                         portionDirection = PortionDirection(deltaDistance / self.interpolationLength)
329                         if abs(deltaDistance - oldDistance) < self.close:
330                                 portionDirection.directionReversed = True
331                         self.portionDirections.append(portionDirection)
332                         oldDistance = deltaDistance
333                 return self
334
335         def getByPrefixAlong(self, elementNode, path, prefix):
336                 'Get interpolation from prefix and xml element along the path.'
337                 if len(path) < 2:
338                         print('Warning, path is too small in evaluate in Interpolation.')
339                         return
340                 if elementNode == None:
341                         self.path = path
342                 else:
343                         self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
344                 self.distances = [0.0]
345                 previousPoint = self.path[0]
346                 for point in self.path[1 :]:
347                         distanceDifference = abs(point - previousPoint)
348                         self.distances.append(self.distances[-1] + distanceDifference)
349                         previousPoint = point
350                 return self.getByDistances()
351
352         def getByPrefixX(self, elementNode, path, prefix):
353                 'Get interpolation from prefix and xml element in the z direction.'
354                 if len(path) < 2:
355                         print('Warning, path is too small in evaluate in Interpolation.')
356                         return
357                 if elementNode == None:
358                         self.path = path
359                 else:
360                         self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
361                 self.distances = []
362                 for point in self.path:
363                         self.distances.append(point.x)
364                 return self.getByDistances()
365
366         def getByPrefixZ(self, elementNode, path, prefix):
367                 'Get interpolation from prefix and xml element in the z direction.'
368                 if len(path) < 2:
369                         print('Warning, path is too small in evaluate in Interpolation.')
370                         return
371                 if elementNode == None:
372                         self.path = path
373                 else:
374                         self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
375                 self.distances = []
376                 for point in self.path:
377                         self.distances.append(point.z)
378                 return self.getByDistances()
379
380         def getComparison( self, first, second ):
381                 'Compare the first with the second.'
382                 if abs( second - first ) < self.close:
383                         return 0
384                 if second > first:
385                         return 1
386                 return - 1
387
388         def getComplexByPortion( self, portionDirection ):
389                 'Get complex from z portion.'
390                 self.setInterpolationIndexFromTo( portionDirection )
391                 return self.oneMinusInnerPortion * self.startVertex.dropAxis() + self.innerPortion * self.endVertex.dropAxis()
392
393         def getInnerPortion(self):
394                 'Get inner x portion.'
395                 fromDistance = self.distances[ self.interpolationIndex ]
396                 innerLength = self.distances[ self.interpolationIndex + 1 ] - fromDistance
397                 if abs( innerLength ) == 0.0:
398                         return 0.0
399                 return ( self.absolutePortion - fromDistance ) / innerLength
400
401         def getVector3ByPortion( self, portionDirection ):
402                 'Get vector3 from z portion.'
403                 self.setInterpolationIndexFromTo( portionDirection )
404                 return self.oneMinusInnerPortion * self.startVertex + self.innerPortion * self.endVertex
405
406         def getYByPortion( self, portionDirection ):
407                 'Get y from x portion.'
408                 self.setInterpolationIndexFromTo( portionDirection )
409                 return self.oneMinusInnerPortion * self.startVertex.y + self.innerPortion * self.endVertex.y
410
411         def setInterpolationIndex( self, portionDirection ):
412                 'Set the interpolation index.'
413                 self.absolutePortion = self.distances[0] + self.interpolationLength * portionDirection.portion
414                 interpolationIndexes = range( 0, len( self.distances ) - 1 )
415                 if portionDirection.directionReversed:
416                         interpolationIndexes.reverse()
417                 for self.interpolationIndex in interpolationIndexes:
418                         begin = self.distances[ self.interpolationIndex ]
419                         end = self.distances[ self.interpolationIndex + 1 ]
420                         if self.getComparison( begin, self.absolutePortion ) != self.getComparison( end, self.absolutePortion ):
421                                 return
422
423         def setInterpolationIndexFromTo( self, portionDirection ):
424                 'Set the interpolation index, the start vertex and the end vertex.'
425                 self.setInterpolationIndex( portionDirection )
426                 self.innerPortion = self.getInnerPortion()
427                 self.oneMinusInnerPortion = 1.0 - self.innerPortion
428                 self.startVertex = self.path[ self.interpolationIndex ]
429                 self.endVertex = self.path[ self.interpolationIndex + 1 ]
430
431
432 class PortionDirection:
433         'Class to hold a portion and direction.'
434         def __init__( self, portion ):
435                 'Initialize.'
436                 self.directionReversed = False
437                 self.portion = portion
438
439         def __repr__(self):
440                 'Get the string representation of this PortionDirection.'
441                 return '%s: %s' % ( self.portion, self.directionReversed )