2 Boolean geometry extrusion.
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.
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
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'
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:
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 )
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 )
53 projectiveSpace = projectiveSpace.getByBasisZTop( normalAverage, derivation.tiltTop )
54 derivation.oldProjectiveSpace = projectiveSpace
55 projectiveSpace.unbuckle( derivation.maximumUnbuckling, normalFirst )
56 projectiveSpace = projectiveSpace.getSpaceByXYScaleAngle( twist, scale )
58 if ( abs( projectiveSpace.basisX ) + abs( projectiveSpace.basisY ) ) < 0.0001:
59 vector3Index = Vector3Index(len(vertexes))
60 addOffsetAddToLists( loop, offset, vector3Index, vertexes )
64 vector3Index = Vector3Index(len(vertexes))
65 projectedVertex = projectiveSpace.getVector3ByPoint(point)
66 vector3Index.setToVector3( projectedVertex )
67 addOffsetAddToLists( loop, offset, vector3Index, vertexes )
70 def addNegatives(derivation, negatives, paths):
71 'Add pillars output to negatives.'
72 portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
74 loopLists = getLoopListsByPath(derivation, 1.000001, path, portionDirections)
75 geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
76 negatives.append(geometryOutput)
78 def addNegativesPositives(derivation, negatives, paths, positives):
79 'Add pillars output to negatives and positives.'
80 portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
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)
90 negatives.append(geometryOutput)
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)
98 def addPositives(derivation, paths, positives):
99 'Add pillars output to positives.'
100 portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
102 loopLists = getLoopListsByPath(derivation, None, path, portionDirections)
103 geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
104 positives.append(geometryOutput)
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 )
112 if portionDirection.directionReversed > lastSpacedPortionDirection.directionReversed:
113 spacedPortionDirections.append( portionDirection )
115 def addTwistPortions( interpolationTwist, remainderPortionDirection, twistPrecision ):
116 'Add twist portions.'
117 lastPortionDirection = interpolationTwist.portionDirections[-1]
118 if remainderPortionDirection.portion == lastPortionDirection.portion:
120 lastTwist = interpolationTwist.getYByPortion( lastPortionDirection )
121 remainderTwist = interpolationTwist.getYByPortion( remainderPortionDirection )
122 twistSegments = int( math.floor( abs( remainderTwist - lastTwist ) / twistPrecision ) )
123 if twistSegments < 1:
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 )
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:
136 if portionDirection.portion < otherPortionDirection.portion:
138 if portionDirection.directionReversed < otherPortionDirection.directionReversed:
140 return portionDirection.directionReversed > otherPortionDirection.directionReversed
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)
150 return getGeometryOutputByLoops(derivation, derivation.target)
152 def getGeometryOutputByArguments(arguments, elementNode):
153 'Get triangle mesh from attribute dictionary by arguments.'
154 return getGeometryOutput(None, elementNode)
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)
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):
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:
176 if len(nestedRings) == 1:
177 geometryOutput = getGeometryOutputByNestedRing(derivation, nestedRings[0], portionDirections)
178 return solid.getGeometryOutputByManipulation(derivation.elementNode, geometryOutput)
180 for nestedRing in nestedRings:
181 shapes.append(getGeometryOutputByNestedRing(derivation, nestedRing, portionDirections))
182 return solid.getGeometryOutputByManipulation(derivation.elementNode, {'union' : {'shapes' : shapes}})
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}})
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:
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}}
206 def getLoopListsByPath(derivation, endMultiplier, path, portionDirections):
207 'Get loop lists from path.'
210 derivation.oldProjectiveSpace = None
211 for portionDirectionIndex in xrange(len(portionDirections)):
212 addLoop(derivation, endMultiplier, loopLists, path, portionDirectionIndex, portionDirections, vertexes)
215 def getNewDerivation(elementNode):
216 'Get new derivation.'
217 return ExtrudeDerivation(elementNode)
219 def getNormalAverage(normals):
223 return (normals[0] + normals[1]).getNormalized()
225 def getNormals( interpolationOffset, offset, portionDirection ):
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() )
233 normals.append( ( interpolationOffset.getVector3ByPortion( PortionDirection( portionTo ) ) - offset ).getNormalized() )
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:
244 spacedPortionDirections = [ portionDirections[0] ]
245 for portionDirection in portionDirections[1 :]:
246 addSpacedPortionDirection( portionDirection, spacedPortionDirections )
247 return spacedPortionDirections
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)
264 def processElementNode(elementNode):
265 'Process the xml element.'
266 solid.processElementNodeByGeometry(elementNode, getGeometryOutput(None, elementNode))
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)
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)
281 class ExtrudeDerivation:
282 'Class to hold extrude variables.'
283 def __init__(self, elementNode):
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)
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)
310 'Class to interpolate a path.'
313 self.interpolationIndex = 0
316 'Get the string representation of this Interpolation.'
317 return str(self.__dict__)
319 def getByDistances(self):
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
335 def getByPrefixAlong(self, elementNode, path, prefix):
336 'Get interpolation from prefix and xml element along the path.'
338 print('Warning, path is too small in evaluate in Interpolation.')
340 if elementNode == None:
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()
352 def getByPrefixX(self, elementNode, path, prefix):
353 'Get interpolation from prefix and xml element in the z direction.'
355 print('Warning, path is too small in evaluate in Interpolation.')
357 if elementNode == None:
360 self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
362 for point in self.path:
363 self.distances.append(point.x)
364 return self.getByDistances()
366 def getByPrefixZ(self, elementNode, path, prefix):
367 'Get interpolation from prefix and xml element in the z direction.'
369 print('Warning, path is too small in evaluate in Interpolation.')
371 if elementNode == None:
374 self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
376 for point in self.path:
377 self.distances.append(point.z)
378 return self.getByDistances()
380 def getComparison( self, first, second ):
381 'Compare the first with the second.'
382 if abs( second - first ) < self.close:
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()
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:
399 return ( self.absolutePortion - fromDistance ) / innerLength
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
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
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 ):
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 ]
432 class PortionDirection:
433 'Class to hold a portion and direction.'
434 def __init__( self, portion ):
436 self.directionReversed = False
437 self.portion = portion
440 'Get the string representation of this PortionDirection.'
441 return '%s: %s' % ( self.portion, self.directionReversed )