441 lines
19 KiB
Python
441 lines
19 KiB
Python
"""
|
|
Boolean geometry extrusion.
|
|
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
#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.
|
|
import __init__
|
|
|
|
from fabmetheus_utilities.geometry.creation import lineation
|
|
from fabmetheus_utilities.geometry.creation import solid
|
|
from fabmetheus_utilities.geometry.geometry_utilities.evaluate_elements import setting
|
|
from fabmetheus_utilities.geometry.geometry_utilities import evaluate
|
|
from fabmetheus_utilities.geometry.solids import triangle_mesh
|
|
from fabmetheus_utilities.vector3 import Vector3
|
|
from fabmetheus_utilities.vector3index import Vector3Index
|
|
from fabmetheus_utilities import euclidean
|
|
import math
|
|
|
|
|
|
__author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
|
|
__credits__ = 'Art of Illusion <http://www.artofillusion.org/>'
|
|
__date__ = '$Date: 2008/02/05 $'
|
|
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
|
|
|
|
|
def addLoop(derivation, endMultiplier, loopLists, path, portionDirectionIndex, portionDirections, vertexes):
|
|
'Add an indexed loop to the vertexes.'
|
|
portionDirection = portionDirections[ portionDirectionIndex ]
|
|
if portionDirection.directionReversed == True:
|
|
loopLists.append([])
|
|
loops = loopLists[-1]
|
|
interpolationOffset = derivation.interpolationDictionary['offset']
|
|
offset = interpolationOffset.getVector3ByPortion( portionDirection )
|
|
if endMultiplier != None:
|
|
if portionDirectionIndex == 0:
|
|
setOffsetByMultiplier( interpolationOffset.path[1], interpolationOffset.path[0], endMultiplier, offset )
|
|
elif portionDirectionIndex == len( portionDirections ) - 1:
|
|
setOffsetByMultiplier( interpolationOffset.path[-2], interpolationOffset.path[-1], endMultiplier, offset )
|
|
scale = derivation.interpolationDictionary['scale'].getComplexByPortion( portionDirection )
|
|
twist = derivation.interpolationDictionary['twist'].getYByPortion( portionDirection )
|
|
projectiveSpace = euclidean.ProjectiveSpace()
|
|
if derivation.tiltTop == None:
|
|
tilt = derivation.interpolationDictionary['tilt'].getComplexByPortion( portionDirection )
|
|
projectiveSpace = projectiveSpace.getByTilt( tilt )
|
|
else:
|
|
normals = getNormals( interpolationOffset, offset, portionDirection )
|
|
normalFirst = normals[0]
|
|
normalAverage = getNormalAverage(normals)
|
|
if derivation.tiltFollow and derivation.oldProjectiveSpace != None:
|
|
projectiveSpace = derivation.oldProjectiveSpace.getNextSpace( normalAverage )
|
|
else:
|
|
projectiveSpace = projectiveSpace.getByBasisZTop( normalAverage, derivation.tiltTop )
|
|
derivation.oldProjectiveSpace = projectiveSpace
|
|
projectiveSpace.unbuckle( derivation.maximumUnbuckling, normalFirst )
|
|
projectiveSpace = projectiveSpace.getSpaceByXYScaleAngle( twist, scale )
|
|
loop = []
|
|
if ( abs( projectiveSpace.basisX ) + abs( projectiveSpace.basisY ) ) < 0.0001:
|
|
vector3Index = Vector3Index(len(vertexes))
|
|
addOffsetAddToLists( loop, offset, vector3Index, vertexes )
|
|
loops.append(loop)
|
|
return
|
|
for point in path:
|
|
vector3Index = Vector3Index(len(vertexes))
|
|
projectedVertex = projectiveSpace.getVector3ByPoint(point)
|
|
vector3Index.setToVector3( projectedVertex )
|
|
addOffsetAddToLists( loop, offset, vector3Index, vertexes )
|
|
loops.append(loop)
|
|
|
|
def addNegatives(derivation, negatives, paths):
|
|
'Add pillars output to negatives.'
|
|
portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
|
|
for path in paths:
|
|
loopLists = getLoopListsByPath(derivation, 1.000001, path, portionDirections)
|
|
geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
|
|
negatives.append(geometryOutput)
|
|
|
|
def addNegativesPositives(derivation, negatives, paths, positives):
|
|
'Add pillars output to negatives and positives.'
|
|
portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
|
|
for path in paths:
|
|
endMultiplier = None
|
|
if not euclidean.getIsWiddershinsByVector3(path):
|
|
endMultiplier = 1.000001
|
|
loopLists = getLoopListsByPath(derivation, endMultiplier, path, portionDirections)
|
|
geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
|
|
if endMultiplier == None:
|
|
positives.append(geometryOutput)
|
|
else:
|
|
negatives.append(geometryOutput)
|
|
|
|
def addOffsetAddToLists(loop, offset, vector3Index, vertexes):
|
|
'Add an indexed loop to the vertexes.'
|
|
vector3Index += offset
|
|
loop.append(vector3Index)
|
|
vertexes.append(vector3Index)
|
|
|
|
def addPositives(derivation, paths, positives):
|
|
'Add pillars output to positives.'
|
|
portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
|
|
for path in paths:
|
|
loopLists = getLoopListsByPath(derivation, None, path, portionDirections)
|
|
geometryOutput = triangle_mesh.getPillarsOutput(loopLists)
|
|
positives.append(geometryOutput)
|
|
|
|
def addSpacedPortionDirection( portionDirection, spacedPortionDirections ):
|
|
'Add spaced portion directions.'
|
|
lastSpacedPortionDirection = spacedPortionDirections[-1]
|
|
if portionDirection.portion - lastSpacedPortionDirection.portion > 0.003:
|
|
spacedPortionDirections.append( portionDirection )
|
|
return
|
|
if portionDirection.directionReversed > lastSpacedPortionDirection.directionReversed:
|
|
spacedPortionDirections.append( portionDirection )
|
|
|
|
def addTwistPortions( interpolationTwist, remainderPortionDirection, twistPrecision ):
|
|
'Add twist portions.'
|
|
lastPortionDirection = interpolationTwist.portionDirections[-1]
|
|
if remainderPortionDirection.portion == lastPortionDirection.portion:
|
|
return
|
|
lastTwist = interpolationTwist.getYByPortion( lastPortionDirection )
|
|
remainderTwist = interpolationTwist.getYByPortion( remainderPortionDirection )
|
|
twistSegments = int( math.floor( abs( remainderTwist - lastTwist ) / twistPrecision ) )
|
|
if twistSegments < 1:
|
|
return
|
|
portionDifference = remainderPortionDirection.portion - lastPortionDirection.portion
|
|
twistSegmentsPlusOne = float( twistSegments + 1 )
|
|
for twistSegment in xrange( twistSegments ):
|
|
additionalPortion = portionDifference * float( twistSegment + 1 ) / twistSegmentsPlusOne
|
|
portionDirection = PortionDirection( lastPortionDirection.portion + additionalPortion )
|
|
interpolationTwist.portionDirections.append( portionDirection )
|
|
|
|
def comparePortionDirection( portionDirection, otherPortionDirection ):
|
|
'Comparison in order to sort portion directions in ascending order of portion then direction.'
|
|
if portionDirection.portion > otherPortionDirection.portion:
|
|
return 1
|
|
if portionDirection.portion < otherPortionDirection.portion:
|
|
return - 1
|
|
if portionDirection.directionReversed < otherPortionDirection.directionReversed:
|
|
return - 1
|
|
return portionDirection.directionReversed > otherPortionDirection.directionReversed
|
|
|
|
def getGeometryOutput(derivation, elementNode):
|
|
'Get triangle mesh from attribute dictionary.'
|
|
if derivation == None:
|
|
derivation = ExtrudeDerivation(elementNode)
|
|
if len(euclidean.getConcatenatedList(derivation.target)) == 0:
|
|
print('Warning, in extrude there are no paths.')
|
|
print(elementNode.attributes)
|
|
return None
|
|
return getGeometryOutputByLoops(derivation, derivation.target)
|
|
|
|
def getGeometryOutputByArguments(arguments, elementNode):
|
|
'Get triangle mesh from attribute dictionary by arguments.'
|
|
return getGeometryOutput(None, elementNode)
|
|
|
|
def getGeometryOutputByLoops(derivation, loops):
|
|
'Get geometry output by sorted, nested loops.'
|
|
loops.sort(key=euclidean.getAreaVector3LoopAbsolute, reverse=True)
|
|
complexLoops = euclidean.getComplexPaths(loops)
|
|
nestedRings = []
|
|
for loopIndex, loop in enumerate(loops):
|
|
complexLoop = complexLoops[loopIndex]
|
|
leftPoint = euclidean.getLeftPoint(complexLoop)
|
|
isInFilledRegion = euclidean.getIsInFilledRegion(complexLoops[: loopIndex] + complexLoops[loopIndex + 1 :], leftPoint)
|
|
if isInFilledRegion == euclidean.isWiddershins(complexLoop):
|
|
loop.reverse()
|
|
nestedRing = euclidean.NestedRing()
|
|
nestedRing.boundary = complexLoop
|
|
nestedRing.vector3Loop = loop
|
|
nestedRings.append(nestedRing)
|
|
nestedRings = euclidean.getOrderedNestedRings(nestedRings)
|
|
nestedRings = euclidean.getFlattenedNestedRings(nestedRings)
|
|
portionDirections = getSpacedPortionDirections(derivation.interpolationDictionary)
|
|
if len(nestedRings) < 1:
|
|
return {}
|
|
if len(nestedRings) == 1:
|
|
geometryOutput = getGeometryOutputByNestedRing(derivation, nestedRings[0], portionDirections)
|
|
return solid.getGeometryOutputByManipulation(derivation.elementNode, geometryOutput)
|
|
shapes = []
|
|
for nestedRing in nestedRings:
|
|
shapes.append(getGeometryOutputByNestedRing(derivation, nestedRing, portionDirections))
|
|
return solid.getGeometryOutputByManipulation(derivation.elementNode, {'union' : {'shapes' : shapes}})
|
|
|
|
def getGeometryOutputByNegativesPositives(elementNode, negatives, positives):
|
|
'Get triangle mesh from elementNode, negatives and positives.'
|
|
positiveOutput = triangle_mesh.getUnifiedOutput(positives)
|
|
if len(negatives) < 1:
|
|
return solid.getGeometryOutputByManipulation(elementNode, positiveOutput)
|
|
if len(positives) < 1:
|
|
negativeOutput = triangle_mesh.getUnifiedOutput(negatives)
|
|
return solid.getGeometryOutputByManipulation(elementNode, negativeOutput)
|
|
return solid.getGeometryOutputByManipulation(elementNode, {'difference' : {'shapes' : [positiveOutput] + negatives}})
|
|
|
|
def getGeometryOutputByNestedRing(derivation, nestedRing, portionDirections):
|
|
'Get geometry output by sorted, nested loops.'
|
|
loopLists = getLoopListsByPath(derivation, None, nestedRing.vector3Loop, portionDirections)
|
|
outsideOutput = triangle_mesh.getPillarsOutput(loopLists)
|
|
if len(nestedRing.innerNestedRings) < 1:
|
|
return outsideOutput
|
|
shapes = [outsideOutput]
|
|
for nestedRing.innerNestedRing in nestedRing.innerNestedRings:
|
|
loopLists = getLoopListsByPath(derivation, 1.000001, nestedRing.innerNestedRing.vector3Loop, portionDirections)
|
|
shapes.append(triangle_mesh.getPillarsOutput(loopLists))
|
|
return {'difference' : {'shapes' : shapes}}
|
|
|
|
def getLoopListsByPath(derivation, endMultiplier, path, portionDirections):
|
|
'Get loop lists from path.'
|
|
vertexes = []
|
|
loopLists = [[]]
|
|
derivation.oldProjectiveSpace = None
|
|
for portionDirectionIndex in xrange(len(portionDirections)):
|
|
addLoop(derivation, endMultiplier, loopLists, path, portionDirectionIndex, portionDirections, vertexes)
|
|
return loopLists
|
|
|
|
def getNewDerivation(elementNode):
|
|
'Get new derivation.'
|
|
return ExtrudeDerivation(elementNode)
|
|
|
|
def getNormalAverage(normals):
|
|
'Get normal.'
|
|
if len(normals) < 2:
|
|
return normals[0]
|
|
return (normals[0] + normals[1]).getNormalized()
|
|
|
|
def getNormals( interpolationOffset, offset, portionDirection ):
|
|
'Get normals.'
|
|
normals = []
|
|
portionFrom = portionDirection.portion - 0.0001
|
|
portionTo = portionDirection.portion + 0.0001
|
|
if portionFrom >= 0.0:
|
|
normals.append( ( offset - interpolationOffset.getVector3ByPortion( PortionDirection( portionFrom ) ) ).getNormalized() )
|
|
if portionTo <= 1.0:
|
|
normals.append( ( interpolationOffset.getVector3ByPortion( PortionDirection( portionTo ) ) - offset ).getNormalized() )
|
|
return normals
|
|
|
|
def getSpacedPortionDirections( interpolationDictionary ):
|
|
'Get sorted portion directions.'
|
|
portionDirections = []
|
|
for interpolationDictionaryValue in interpolationDictionary.values():
|
|
portionDirections += interpolationDictionaryValue.portionDirections
|
|
portionDirections.sort( comparePortionDirection )
|
|
if len( portionDirections ) < 1:
|
|
return []
|
|
spacedPortionDirections = [ portionDirections[0] ]
|
|
for portionDirection in portionDirections[1 :]:
|
|
addSpacedPortionDirection( portionDirection, spacedPortionDirections )
|
|
return spacedPortionDirections
|
|
|
|
def insertTwistPortions(derivation, elementNode):
|
|
'Insert twist portions and radian the twist.'
|
|
interpolationDictionary = derivation.interpolationDictionary
|
|
interpolationTwist = Interpolation().getByPrefixX(elementNode, derivation.twistPathDefault, 'twist')
|
|
interpolationDictionary['twist'] = interpolationTwist
|
|
for point in interpolationTwist.path:
|
|
point.y = math.radians(point.y)
|
|
remainderPortionDirections = interpolationTwist.portionDirections[1 :]
|
|
interpolationTwist.portionDirections = [interpolationTwist.portionDirections[0]]
|
|
if elementNode != None:
|
|
twistPrecision = setting.getTwistPrecisionRadians(elementNode)
|
|
for remainderPortionDirection in remainderPortionDirections:
|
|
addTwistPortions(interpolationTwist, remainderPortionDirection, twistPrecision)
|
|
interpolationTwist.portionDirections.append(remainderPortionDirection)
|
|
|
|
def processElementNode(elementNode):
|
|
'Process the xml element.'
|
|
solid.processElementNodeByGeometry(elementNode, getGeometryOutput(None, elementNode))
|
|
|
|
def setElementNodeToEndStart(elementNode, end, start):
|
|
'Set elementNode attribute dictionary to a tilt following path from the start to end.'
|
|
elementNode.attributes['path'] = [start, end]
|
|
elementNode.attributes['tiltFollow'] = 'true'
|
|
elementNode.attributes['tiltTop'] = Vector3(0.0, 0.0, 1.0)
|
|
|
|
def setOffsetByMultiplier(begin, end, multiplier, offset):
|
|
'Set the offset by the multiplier.'
|
|
segment = end - begin
|
|
delta = segment * multiplier - segment
|
|
offset.setToVector3(offset + delta)
|
|
|
|
|
|
class ExtrudeDerivation:
|
|
'Class to hold extrude variables.'
|
|
def __init__(self, elementNode):
|
|
'Initialize.'
|
|
self.elementNode = elementNode
|
|
self.interpolationDictionary = {}
|
|
self.tiltFollow = evaluate.getEvaluatedBoolean(True, elementNode, 'tiltFollow')
|
|
self.tiltTop = evaluate.getVector3ByPrefix(None, elementNode, 'tiltTop')
|
|
self.maximumUnbuckling = evaluate.getEvaluatedFloat(5.0, elementNode, 'maximumUnbuckling')
|
|
scalePathDefault = [Vector3(1.0, 1.0, 0.0), Vector3(1.0, 1.0, 1.0)]
|
|
self.interpolationDictionary['scale'] = Interpolation().getByPrefixZ(elementNode, scalePathDefault, 'scale')
|
|
self.target = evaluate.getTransformedPathsByKey([], elementNode, 'target')
|
|
if self.tiltTop == None:
|
|
offsetPathDefault = [Vector3(), Vector3(0.0, 0.0, 1.0)]
|
|
self.interpolationDictionary['offset'] = Interpolation().getByPrefixZ(elementNode, offsetPathDefault, '')
|
|
tiltPathDefault = [Vector3(), Vector3(0.0, 0.0, 1.0)]
|
|
self.interpolationDictionary['tilt'] = Interpolation().getByPrefixZ(elementNode, tiltPathDefault, 'tilt')
|
|
for point in self.interpolationDictionary['tilt'].path:
|
|
point.x = math.radians(point.x)
|
|
point.y = math.radians(point.y)
|
|
else:
|
|
offsetAlongDefault = [Vector3(), Vector3(1.0, 0.0, 0.0)]
|
|
self.interpolationDictionary['offset'] = Interpolation().getByPrefixAlong(elementNode, offsetAlongDefault, '')
|
|
self.twist = evaluate.getEvaluatedFloat(0.0, elementNode, 'twist')
|
|
self.twistPathDefault = [Vector3(), Vector3(1.0, self.twist) ]
|
|
insertTwistPortions(self, elementNode)
|
|
|
|
|
|
class Interpolation:
|
|
'Class to interpolate a path.'
|
|
def __init__(self):
|
|
'Set index.'
|
|
self.interpolationIndex = 0
|
|
|
|
def __repr__(self):
|
|
'Get the string representation of this Interpolation.'
|
|
return str(self.__dict__)
|
|
|
|
def getByDistances(self):
|
|
'Get by distances.'
|
|
beginDistance = self.distances[0]
|
|
self.interpolationLength = self.distances[-1] - beginDistance
|
|
self.close = abs(0.000001 * self.interpolationLength)
|
|
self.portionDirections = []
|
|
oldDistance = -self.interpolationLength # so the difference should not be close
|
|
for distance in self.distances:
|
|
deltaDistance = distance - beginDistance
|
|
portionDirection = PortionDirection(deltaDistance / self.interpolationLength)
|
|
if abs(deltaDistance - oldDistance) < self.close:
|
|
portionDirection.directionReversed = True
|
|
self.portionDirections.append(portionDirection)
|
|
oldDistance = deltaDistance
|
|
return self
|
|
|
|
def getByPrefixAlong(self, elementNode, path, prefix):
|
|
'Get interpolation from prefix and xml element along the path.'
|
|
if len(path) < 2:
|
|
print('Warning, path is too small in evaluate in Interpolation.')
|
|
return
|
|
if elementNode == None:
|
|
self.path = path
|
|
else:
|
|
self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
|
|
self.distances = [0.0]
|
|
previousPoint = self.path[0]
|
|
for point in self.path[1 :]:
|
|
distanceDifference = abs(point - previousPoint)
|
|
self.distances.append(self.distances[-1] + distanceDifference)
|
|
previousPoint = point
|
|
return self.getByDistances()
|
|
|
|
def getByPrefixX(self, elementNode, path, prefix):
|
|
'Get interpolation from prefix and xml element in the z direction.'
|
|
if len(path) < 2:
|
|
print('Warning, path is too small in evaluate in Interpolation.')
|
|
return
|
|
if elementNode == None:
|
|
self.path = path
|
|
else:
|
|
self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
|
|
self.distances = []
|
|
for point in self.path:
|
|
self.distances.append(point.x)
|
|
return self.getByDistances()
|
|
|
|
def getByPrefixZ(self, elementNode, path, prefix):
|
|
'Get interpolation from prefix and xml element in the z direction.'
|
|
if len(path) < 2:
|
|
print('Warning, path is too small in evaluate in Interpolation.')
|
|
return
|
|
if elementNode == None:
|
|
self.path = path
|
|
else:
|
|
self.path = evaluate.getTransformedPathByPrefix(elementNode, path, prefix)
|
|
self.distances = []
|
|
for point in self.path:
|
|
self.distances.append(point.z)
|
|
return self.getByDistances()
|
|
|
|
def getComparison( self, first, second ):
|
|
'Compare the first with the second.'
|
|
if abs( second - first ) < self.close:
|
|
return 0
|
|
if second > first:
|
|
return 1
|
|
return - 1
|
|
|
|
def getComplexByPortion( self, portionDirection ):
|
|
'Get complex from z portion.'
|
|
self.setInterpolationIndexFromTo( portionDirection )
|
|
return self.oneMinusInnerPortion * self.startVertex.dropAxis() + self.innerPortion * self.endVertex.dropAxis()
|
|
|
|
def getInnerPortion(self):
|
|
'Get inner x portion.'
|
|
fromDistance = self.distances[ self.interpolationIndex ]
|
|
innerLength = self.distances[ self.interpolationIndex + 1 ] - fromDistance
|
|
if abs( innerLength ) == 0.0:
|
|
return 0.0
|
|
return ( self.absolutePortion - fromDistance ) / innerLength
|
|
|
|
def getVector3ByPortion( self, portionDirection ):
|
|
'Get vector3 from z portion.'
|
|
self.setInterpolationIndexFromTo( portionDirection )
|
|
return self.oneMinusInnerPortion * self.startVertex + self.innerPortion * self.endVertex
|
|
|
|
def getYByPortion( self, portionDirection ):
|
|
'Get y from x portion.'
|
|
self.setInterpolationIndexFromTo( portionDirection )
|
|
return self.oneMinusInnerPortion * self.startVertex.y + self.innerPortion * self.endVertex.y
|
|
|
|
def setInterpolationIndex( self, portionDirection ):
|
|
'Set the interpolation index.'
|
|
self.absolutePortion = self.distances[0] + self.interpolationLength * portionDirection.portion
|
|
interpolationIndexes = range( 0, len( self.distances ) - 1 )
|
|
if portionDirection.directionReversed:
|
|
interpolationIndexes.reverse()
|
|
for self.interpolationIndex in interpolationIndexes:
|
|
begin = self.distances[ self.interpolationIndex ]
|
|
end = self.distances[ self.interpolationIndex + 1 ]
|
|
if self.getComparison( begin, self.absolutePortion ) != self.getComparison( end, self.absolutePortion ):
|
|
return
|
|
|
|
def setInterpolationIndexFromTo( self, portionDirection ):
|
|
'Set the interpolation index, the start vertex and the end vertex.'
|
|
self.setInterpolationIndex( portionDirection )
|
|
self.innerPortion = self.getInnerPortion()
|
|
self.oneMinusInnerPortion = 1.0 - self.innerPortion
|
|
self.startVertex = self.path[ self.interpolationIndex ]
|
|
self.endVertex = self.path[ self.interpolationIndex + 1 ]
|
|
|
|
|
|
class PortionDirection:
|
|
'Class to hold a portion and direction.'
|
|
def __init__( self, portion ):
|
|
'Initialize.'
|
|
self.directionReversed = False
|
|
self.portion = portion
|
|
|
|
def __repr__(self):
|
|
'Get the string representation of this PortionDirection.'
|
|
return '%s: %s' % ( self.portion, self.directionReversed )
|