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