2 Boolean geometry extrusion.
6 from __future__ import absolute_import
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
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'
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:
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 )
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 )
50 projectiveSpace = projectiveSpace.getByBasisZTop( normalAverage, derivation.tiltTop )
51 derivation.oldProjectiveSpace = projectiveSpace
52 projectiveSpace.unbuckle( derivation.maximumUnbuckling, normalFirst )
53 projectiveSpace = projectiveSpace.getSpaceByXYScaleAngle( twist, scale )
55 if ( abs( projectiveSpace.basisX ) + abs( projectiveSpace.basisY ) ) < 0.0001:
56 vector3Index = Vector3Index(len(vertexes))
57 addOffsetAddToLists( loop, offset, vector3Index, vertexes )
61 vector3Index = Vector3Index(len(vertexes))
62 projectedVertex = projectiveSpace.getVector3ByPoint(point)
63 vector3Index.setToVector3( projectedVertex )
64 addOffsetAddToLists( loop, offset, vector3Index, vertexes )
67 def addNegatives(derivation, negatives, paths):
68 'Add pillars output to negatives.'
69 portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
71 loopLists = getLoopListsByPath(derivation, 1.000001, path, portionDirections)
72 geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
73 negatives.append(geometryOutput)
75 def addNegativesPositives(derivation, negatives, paths, positives):
76 'Add pillars output to negatives and positives.'
77 portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
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)
87 negatives.append(geometryOutput)
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)
95 def addPositives(derivation, paths, positives):
96 'Add pillars output to positives.'
97 portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
99 loopLists = getLoopListsByPath(derivation, None, path, portionDirections)
100 geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
101 positives.append(geometryOutput)
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 )
109 if portionDirection.directionReversed > lastSpacedPortionDirection.directionReversed:
110 spacedPortionDirections.append( portionDirection )
112 def addTwistPortions( interpolationTwist, remainderPortionDirection, twistPrecision ):
113 'Add twist portions.'
114 lastPortionDirection = interpolationTwist.portionDirections[-1]
115 if remainderPortionDirection.portion == lastPortionDirection.portion:
117 lastTwist = interpolationTwist.getYByPortion( lastPortionDirection )
118 remainderTwist = interpolationTwist.getYByPortion( remainderPortionDirection )
119 twistSegments = int( math.floor( abs( remainderTwist - lastTwist ) / twistPrecision ) )
120 if twistSegments < 1:
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 )
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:
133 if portionDirection.portion < otherPortionDirection.portion:
135 if portionDirection.directionReversed < otherPortionDirection.directionReversed:
137 return portionDirection.directionReversed > otherPortionDirection.directionReversed
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)
147 return getGeometryOutputByLoops(derivation, derivation.target)
149 def getGeometryOutputByArguments(arguments, elementNode):
150 'Get triangle mesh from attribute dictionary by arguments.'
151 return getGeometryOutput(None, elementNode)
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)
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):
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:
173 if len(nestedRings) == 1:
174 geometryOutput = getGeometryOutputByNestedRing(derivation, nestedRings[0], portionDirections)
175 return solid.getGeometryOutputByManipulation(derivation.elementNode, geometryOutput)
177 for nestedRing in nestedRings:
178 shapes.append(getGeometryOutputByNestedRing(derivation, nestedRing, portionDirections))
179 return solid.getGeometryOutputByManipulation(derivation.elementNode, {'union' : {'shapes' : shapes}})
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}})
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:
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}}
203 def getLoopListsByPath(derivation, endMultiplier, path, portionDirections):
204 'Get loop lists from path.'
207 derivation.oldProjectiveSpace = None
208 for portionDirectionIndex in xrange(len(portionDirections)):
209 addLoop(derivation, endMultiplier, loopLists, path, portionDirectionIndex, portionDirections, vertexes)
212 def getNewDerivation(elementNode):
213 'Get new derivation.'
214 return ExtrudeDerivation(elementNode)
216 def getNormalAverage(normals):
220 return (normals[0] + normals[1]).getNormalized()
222 def getNormals( interpolationOffset, offset, portionDirection ):
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() )
230 normals.append( ( interpolationOffset.getVector3ByPortion( PortionDirection( portionTo ) ) - offset ).getNormalized() )
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:
241 spacedPortionDirections = [ portionDirections[0] ]
242 for portionDirection in portionDirections[1 :]:
243 addSpacedPortionDirection( portionDirection, spacedPortionDirections )
244 return spacedPortionDirections
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)
261 def processElementNode(elementNode):
262 'Process the xml element.'
263 solid.processElementNodeByGeometry(elementNode, getGeometryOutput(None, elementNode))
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)
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)
278 class ExtrudeDerivation(object):
279 'Class to hold extrude variables.'
280 def __init__(self, elementNode):
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)
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)
306 class Interpolation(object):
307 'Class to interpolate a path.'
310 self.interpolationIndex = 0
313 'Get the string representation of this Interpolation.'
314 return str(self.__dict__)
316 def getByDistances(self):
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
332 def getByPrefixAlong(self, elementNode, path, prefix):
333 'Get interpolation from prefix and xml element along the path.'
335 print('Warning, path is too small in evaluate in Interpolation.')
337 if elementNode == None:
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()
349 def getByPrefixX(self, elementNode, path, prefix):
350 'Get interpolation from prefix and xml element in the z direction.'
352 print('Warning, path is too small in evaluate in Interpolation.')
354 if elementNode == None:
357 self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
359 for point in self.path:
360 self.distances.append(point.x)
361 return self.getByDistances()
363 def getByPrefixZ(self, elementNode, path, prefix):
364 'Get interpolation from prefix and xml element in the z direction.'
366 print('Warning, path is too small in evaluate in Interpolation.')
368 if elementNode == None:
371 self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
373 for point in self.path:
374 self.distances.append(point.z)
375 return self.getByDistances()
377 def getComparison( self, first, second ):
378 'Compare the first with the second.'
379 if abs( second - first ) < self.close:
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()
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:
396 return ( self.absolutePortion - fromDistance ) / innerLength
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
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
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 ):
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 ]
429 class PortionDirection(object):
430 'Class to hold a portion and direction.'
431 def __init__( self, portion ):
433 self.directionReversed = False
434 self.portion = portion
437 'Get the string representation of this PortionDirection.'
438 return '%s: %s' % ( self.portion, self.directionReversed )