diff --git a/release/scripts/freestyle/modules/freestyle/chainingiterators.py b/release/scripts/freestyle/modules/freestyle/chainingiterators.py index ae0a8a04294..080919f6bc3 100644 --- a/release/scripts/freestyle/modules/freestyle/chainingiterators.py +++ b/release/scripts/freestyle/modules/freestyle/chainingiterators.py @@ -23,6 +23,20 @@ rules. Also intended to be a collection of examples for defining chaining iterators in Python """ +__all__ = ( + "pyChainSilhouetteIterator", + "pyChainSilhouetteGenericIterator", + "pyExternalContourChainingIterator", + "pySketchyChainSilhouetteIterator", + "pySketchyChainingIterator", + "pyFillOcclusionsRelativeChainingIterator", + "pyFillOcclusionsAbsoluteChainingIterator", + "pyFillOcclusionsAbsoluteAndRelativeChainingIterator", + "pyFillQi0AbsoluteAndRelativeChainingIterator", + "pyNoIdChainSilhouetteIterator", + ) + + # module members from _freestyle import ( ChainPredicateIterator, @@ -41,11 +55,30 @@ from freestyle.predicates import ( ) from freestyle.utils import ( ContextFunctions as CF, - stroke_normal, + get_chain_length, + find_matching_vertex, ) + import bpy +NATURES = ( + Nature.SILHOUETTE, + Nature.BORDER, + Nature.CREASE, + Nature.MATERIAL_BOUNDARY, + Nature.EDGE_MARK, + Nature.SUGGESTIVE_CONTOUR, + Nature.VALLEY, + Nature.RIDGE + ) + + +def nature_in_preceding(nature, index): + """ Returns True if given nature appears before index, else False """ + return any(nature & nat for nat in NATURES[:index]) + + class pyChainSilhouetteIterator(ChainingIterator): """Natural chaining iterator @@ -61,43 +94,28 @@ class pyChainSilhouetteIterator(ChainingIterator): pass def traverse(self, iter): - winner = None it = AdjacencyIterator(iter) - tvertex = self.next_vertex - if type(tvertex) is TVertex: - mateVE = tvertex.get_mate(self.current_edge) - while not it.is_end: - ve = it.object - if ve.id == mateVE.id: - winner = ve - break - it.increment() - else: - ## case of NonTVertex - natures = [Nature.SILHOUETTE,Nature.BORDER,Nature.CREASE,Nature.MATERIAL_BOUNDARY,Nature.EDGE_MARK, - Nature.SUGGESTIVE_CONTOUR,Nature.VALLEY,Nature.RIDGE] - for i in range(len(natures)): - currentNature = self.current_edge.nature - if (natures[i] & currentNature) != 0: - count=0 - while not it.is_end: - visitNext = 0 - oNature = it.object.nature - if (oNature & natures[i]) != 0: - if natures[i] != oNature: - for j in range(i): - if (natures[j] & oNature) != 0: - visitNext = 1 - break - if visitNext != 0: - break - count = count+1 - winner = it.object - it.increment() - if count != 1: - winner = None - break - return winner + ## case of TVertex + vertex = self.next_vertex + if type(vertex) is TVertex: + mate = vertex.get_mate(self.current_edge) + return find_matching_vertex(mate.id, it) + ## case of NonTVertex + winner = None + for i, nat in enumerate(NATURES): + if (nat & self.current_edge.nature): + for ve in it: + ve_nat = ve.nature + if (ve_nat & nat): + # search for matches in previous natures. if match -> break + if nat != ve_nat and nature_in_preceding(ve_nat, index=i): + break + # a second match must be an error + if winner is not None: + return None + # assign winner + winner = ve + return winner class pyChainSilhouetteGenericIterator(ChainingIterator): @@ -120,47 +138,30 @@ class pyChainSilhouetteGenericIterator(ChainingIterator): pass def traverse(self, iter): - winner = None it = AdjacencyIterator(iter) - tvertex = self.next_vertex - if type(tvertex) is TVertex: - mateVE = tvertex.get_mate(self.current_edge) - while not it.is_end: - ve = it.object - if ve.id == mateVE.id: - winner = ve - break - it.increment() - else: - ## case of NonTVertex - natures = [Nature.SILHOUETTE,Nature.BORDER,Nature.CREASE,Nature.MATERIAL_BOUNDARY,Nature.EDGE_MARK, - Nature.SUGGESTIVE_CONTOUR,Nature.VALLEY,Nature.RIDGE] - for i in range(len(natures)): - currentNature = self.current_edge.nature - if (natures[i] & currentNature) != 0: - count=0 - while not it.is_end: - visitNext = 0 - oNature = it.object.nature - ve = it.object - if ve.id == self.current_edge.id: - it.increment() - continue - if (oNature & natures[i]) != 0: - if natures[i] != oNature: - for j in range(i): - if (natures[j] & oNature) != 0: - visitNext = 1 - break - if visitNext != 0: - break - count = count+1 - winner = ve - it.increment() - if count != 1: - winner = None - break - return winner + ## case of TVertex + vertex = self.next_vertex + if type(vertex) is TVertex: + mate = vertex.get_mate(self.current_edge) + return find_matching_vertex(mate.id, it) + ## case of NonTVertex + winner = None + for i, nat in enumerate(NATURES): + if (nat & self.current_edge.nature): + for ve in it: + ve_nat = ve.nature + if ve.id == self.current_edge.id: + continue + if (ve_nat & nat): + if nat != ve_nat and nature_in_preceding(ve_nat, index=i): + break + + if winner is not None: + return None + + winner = ve + return winner + return None class pyExternalContourChainingIterator(ChainingIterator): @@ -168,49 +169,40 @@ class pyExternalContourChainingIterator(ChainingIterator): def __init__(self): ChainingIterator.__init__(self, False, True, None, True) - self._isExternalContour = ExternalContourUP1D() + self.ExternalContour = ExternalContourUP1D() def init(self): self._nEdges = 0 - self._isInSelection = 1 def checkViewEdge(self, ve, orientation): - if orientation != 0: - vertex = ve.second_svertex() - else: - vertex = ve.first_svertex() - it = AdjacencyIterator(vertex,1,1) - while not it.is_end: - ave = it.object - if self._isExternalContour(ave): - return True - it.increment() - if bpy.app.debug_freestyle: + vertex = (ve.first_viewvertex if orientation else + ve.last_viewvertex) + + it = AdjacencyIterator(vertex, True, True) + result = any(self.ExternalContour(ave) for ave in it) + # report if there is no result (that's bad) + if not result and bpy.app.debug_freestyle: print("pyExternalContourChainingIterator : didn't find next edge") - return False + + return result def traverse(self, iter): winner = None - it = AdjacencyIterator(iter) - while not it.is_end: - ve = it.object - if self._isExternalContour(ve): - if ve.time_stamp == CF.get_time_stamp(): - winner = ve - it.increment() + self._nEdges += 1 + + it = AdjacencyIterator(iter) + time_stamp = CF.get_time_stamp() + + for ve in it: + if self.ExternalContour(ve) and ve.time_stamp == time_stamp: + winner = ve - self._nEdges = self._nEdges+1 if winner is None: - orient = 1 it = AdjacencyIterator(iter) - while not it.is_end: - ve = it.object - if it.is_incoming: - orient = 0 - good = self.checkViewEdge(ve,orient) - if good != 0: + for ve in it: + if self.checkViewEdge(ve, not it.is_incoming): winner = ve - it.increment() + return winner @@ -227,58 +219,49 @@ class pySketchyChainSilhouetteIterator(ChainingIterator): def __init__(self, nRounds=3,stayInSelection=True): ChainingIterator.__init__(self, stayInSelection, False, None, True) - self._timeStamp = CF.get_time_stamp()+nRounds + self._timeStamp = CF.get_time_stamp() + nRounds self._nRounds = nRounds def init(self): - self._timeStamp = CF.get_time_stamp()+self._nRounds + self._timeStamp = CF.get_time_stamp() + self._nRounds + + # keeping this local saves passing a reference to 'self' around + def make_sketchy(self, ve): + """ + Creates the skeychy effect by causing the chain to run from + the start again. (loop over itself again) + """ + if ve is None: + ve = self.current_edge + if ve.chaining_time_stamp == self._timeStamp: + return None + return ve def traverse(self, iter): - winner = None it = AdjacencyIterator(iter) - tvertex = self.next_vertex - if type(tvertex) is TVertex: - mateVE = tvertex.get_mate(self.current_edge) - while not it.is_end: - ve = it.object - if ve.id == mateVE.id: - winner = ve - break - it.increment() - else: - ## case of NonTVertex - natures = [Nature.SILHOUETTE,Nature.BORDER,Nature.CREASE,Nature.MATERIAL_BOUNDARY,Nature.EDGE_MARK, - Nature.SUGGESTIVE_CONTOUR,Nature.VALLEY,Nature.RIDGE] - for i in range(len(natures)): - currentNature = self.current_edge.nature - if (natures[i] & currentNature) != 0: - count=0 - while not it.is_end: - visitNext = 0 - oNature = it.object.nature - ve = it.object - if ve.id == self.current_edge.id: - it.increment() - continue - if (oNature & natures[i]) != 0: - if (natures[i] != oNature) != 0: - for j in range(i): - if (natures[j] & oNature) != 0: - visitNext = 1 - break - if visitNext != 0: - break - count = count+1 - winner = ve - it.increment() - if count != 1: - winner = None - break - if winner is None: - winner = self.current_edge - if winner.chaining_time_stamp == self._timeStamp: - winner = None - return winner + ## case of TVertex + vertex = self.next_vertex + if type(vertex) is TVertex: + mate = vertex.get_mate(self.current_edge) + return self.make_sketchy(find_matching_vertex(mate.id, it)) + ## case of NonTVertex + winner = None + for i, nat in enumerate(NATURES): + if (nat & self.current_edge.nature): + for ve in it: + if ve.id == self.current_edge.id: + continue + ve_nat = ve.nature + if (ve_nat & nat): + if nat != ve_nat and nature_in_preceding(ve_nat, i): + break + + if winner is not None: + return self.make_sketchy(None) + + winner = ve + break + return self.make_sketchy(winner) class pySketchyChainingIterator(ChainingIterator): @@ -289,30 +272,30 @@ class pySketchyChainingIterator(ChainingIterator): """ def __init__(self, nRounds=3, stayInSelection=True): ChainingIterator.__init__(self, stayInSelection, False, None, True) - self._timeStamp = CF.get_time_stamp()+nRounds + self._timeStamp = CF.get_time_stamp() + nRounds self._nRounds = nRounds + self.t = False def init(self): - self._timeStamp = CF.get_time_stamp()+self._nRounds + self._timeStamp = CF.get_time_stamp() + self._nRounds def traverse(self, iter): winner = None found = False - it = AdjacencyIterator(iter) - while not it.is_end: - ve = it.object - if ve.id == self.current_edge.id: + + for ve in AdjacencyIterator(iter): + if self.current_edge.id == ve.id: found = True - it.increment() continue winner = ve - it.increment() + if not found: # This is a fatal error condition: self.current_edge must be found # among the edges seen by the AdjacencyIterator [bug #35695]. if bpy.app.debug_freestyle: print('pySketchyChainingIterator: current edge not found') return None + if winner is None: winner = self.current_edge if winner.chaining_time_stamp == self._timeStamp: @@ -330,97 +313,61 @@ class pyFillOcclusionsRelativeChainingIterator(ChainingIterator): def __init__(self, percent): ChainingIterator.__init__(self, False, True, None, True) - self._length = 0 + self._length = 0.0 self._percent = float(percent) + self.timestamp = CF.get_time_stamp() def init(self): # A chain's length should preferably be evaluated only once. # Therefore, the chain length is reset here. - self._length = 0 + self._length = 0.0 def traverse(self, iter): winner = None - winnerOrientation = False - winnerOrientation = 0 - #print(self.current_edge.id.first, self.current_edge.id.second) it = AdjacencyIterator(iter) - tvertex = self.next_vertex - if type(tvertex) is TVertex: - mateVE = tvertex.get_mate(self.current_edge) - while not it.is_end: - ve = it.object - if ve.id == mateVE.id: - winner = ve - winnerOrientation = not it.is_incoming - break - it.increment() + ## case of TVertex + vertex = self.next_vertex + if type(vertex) is TVertex: + mate = vertex.get_mate(self.current_edge) + winner = find_matching_vertex(mate.id, it) + winnerOrientation = not it.is_incoming if not it.is_end else False + ## case of NonTVertex else: - ## case of NonTVertex - natures = [Nature.SILHOUETTE,Nature.BORDER,Nature.CREASE,Nature.MATERIAL_BOUNDARY,Nature.EDGE_MARK, - Nature.SUGGESTIVE_CONTOUR,Nature.VALLEY,Nature.RIDGE] - for nat in natures: - if (self.current_edge.nature & nat) != 0: - count=0 - while not it.is_end: - ve = it.object - if (ve.nature & nat) != 0: - count = count+1 + for nat in NATURES: + if (self.current_edge.nature & nat): + for ve in it: + if (ve.nature & nat): + if winner is not None: + return None winner = ve winnerOrientation = not it.is_incoming - it.increment() - if count != 1: - winner = None break - if winner is not None: - # check whether this edge was part of the selection - if winner.time_stamp != CF.get_time_stamp(): - #print("---", winner.id.first, winner.id.second) - # if not, let's check whether it's short enough with - # respect to the chain made without staying in the selection - #------------------------------------------------------------ - # Did we compute the prospective chain length already ? - if self._length == 0: - #if not, let's do it - _it = pyChainSilhouetteGenericIterator(False, False) - _it.begin = winner - _it.current_edge = winner - _it.orientation = winnerOrientation - _it.init() - while not _it.is_end: - ve = _it.object - #print("--------", ve.id.first, ve.id.second) - self._length = self._length + ve.length_2d - _it.increment() - if _it.is_begin: - break; - _it.begin = winner - _it.current_edge = winner - _it.orientation = winnerOrientation - if not _it.is_begin: - _it.decrement() - while (not _it.is_end) and (not _it.is_begin): - ve = _it.object - #print("--------", ve.id.first, ve.id.second) - self._length = self._length + ve.length_2d - _it.decrement() - # let's do the comparison: - # nw let's compute the length of this connex non selected part: - connexl = 0 + # check timestamp to see if this edge was part of the selection + if winner is not None and winner.time_stamp != self.timestamp: + # if the edge wasn't part of the selection, let's see + # whether it's short enough (with respect to self.percent) + # to be included. + if self._length == 0.0: + self._length = get_chain_length(winner, winnerOrientation) + + # check if the gap can be bridged + connexl = 0.0 + _cit = pyChainSilhouetteGenericIterator(False, False) + _cit.begin = winner + _cit.current_edge = winner + _cit.orientation = winnerOrientation + _cit.init() + + while (not _cit.is_end) and _cit.object.time_stamp != self.timestamp: + connexl += _cit.object.length_2d + _cit.increment() + if _cit.is_begin: break + + if connexl > self._percent * self._length: + return None - _cit = pyChainSilhouetteGenericIterator(False, False) - _cit.begin = winner - _cit.current_edge = winner - _cit.orientation = winnerOrientation - _cit.init() - while _cit.is_end == 0 and _cit.object.time_stamp != CF.get_time_stamp(): - ve = _cit.object - #print("-------- --------", ve.id.first, ve.id.second) - connexl = connexl + ve.length_2d - _cit.increment() - if connexl > self._percent * self._length: - winner = None return winner @@ -433,6 +380,7 @@ class pyFillOcclusionsAbsoluteChainingIterator(ChainingIterator): def __init__(self, length): ChainingIterator.__init__(self, False, True, None, True) self._length = float(length) + self.timestamp = CF.get_time_stamp() def init(self): pass @@ -440,53 +388,41 @@ class pyFillOcclusionsAbsoluteChainingIterator(ChainingIterator): def traverse(self, iter): winner = None winnerOrientation = False - #print(self.current_edge.id.first, self.current_edge.id.second) it = AdjacencyIterator(iter) - tvertex = self.next_vertex - if type(tvertex) is TVertex: - mateVE = tvertex.get_mate(self.current_edge) - while not it.is_end: - ve = it.object - if ve.id == mateVE.id: - winner = ve - winnerOrientation = not it.is_incoming - break - it.increment() + ## case of TVertex + vertex = self.next_vertex + if type(vertex) is TVertex: + mate = vertex.get_mate(self.current_edge) + winner = find_matching_vertex(mate.id, it) + winnerOrientation = not it.is_incoming if not it.is_end else False + ## case of NonTVertex else: - ## case of NonTVertex - natures = [Nature.SILHOUETTE,Nature.BORDER,Nature.CREASE,Nature.MATERIAL_BOUNDARY,Nature.EDGE_MARK, - Nature.SUGGESTIVE_CONTOUR,Nature.VALLEY,Nature.RIDGE] - for nat in natures: - if (self.current_edge.nature & nat) != 0: - count=0 - while not it.is_end: - ve = it.object - if (ve.nature & nat) != 0: - count = count+1 + for nat in NATURES: + if (self.current_edge.nature & nat): + for ve in it: + if (ve.nature & nat): + if winner is not None: + return None winner = ve winnerOrientation = not it.is_incoming - it.increment() - if count != 1: - winner = None break - if winner is not None: - # check whether this edge was part of the selection - if winner.time_stamp != CF.get_time_stamp(): - #print("---", winner.id.first, winner.id.second) - # nw let's compute the length of this connex non selected part: - connexl = 0 - _cit = pyChainSilhouetteGenericIterator(False, False) - _cit.begin = winner - _cit.current_edge = winner - _cit.orientation = winnerOrientation - _cit.init() - while _cit.is_end == 0 and _cit.object.time_stamp != CF.get_time_stamp(): - ve = _cit.object - #print("-------- --------", ve.id.first, ve.id.second) - connexl = connexl + ve.length_2d - _cit.increment() - if connexl > self._length: - winner = None + + if winner is not None and winner.time_stamp != self.timestamp: + connexl = 0.0 + _cit = pyChainSilhouetteGenericIterator(False, False) + _cit.begin = winner + _cit.current_edge = winner + _cit.orientation = winnerOrientation + _cit.init() + + while (not _cit.is_end) and _cit.object.time_stamp != self.timestamp: + connexl += _cit.object.length_2d + _cit.increment() + if _cit.is_begin: break + + if connexl > self._length: + return None + return winner @@ -500,7 +436,7 @@ class pyFillOcclusionsAbsoluteAndRelativeChainingIterator(ChainingIterator): """ def __init__(self, percent, l): ChainingIterator.__init__(self, False, True, None, True) - self._length = 0 + self._length = 0.0 self._absLength = l self._percent = float(percent) @@ -508,88 +444,48 @@ class pyFillOcclusionsAbsoluteAndRelativeChainingIterator(ChainingIterator): # each time we're evaluating a chain length # we try to do it once. Thus we reinit # the chain length here: - self._length = 0 + self._length = 0.0 def traverse(self, iter): winner = None winnerOrientation = False - #print(self.current_edge.id.first, self.current_edge.id.second) it = AdjacencyIterator(iter) - tvertex = self.next_vertex - if type(tvertex) is TVertex: - mateVE = tvertex.get_mate(self.current_edge) - while not it.is_end: - ve = it.object - if ve.id == mateVE.id: - winner = ve - winnerOrientation = not it.is_incoming - break - it.increment() + ## case of TVertex + vertex = self.next_vertex + if type(vertex) is TVertex: + mate = vertex.get_mate(self.current_edge) + winner = find_matching_vertex(mate.id, it) + winnerOrientation = not it.is_incoming if not it.is_end else False + ## case of NonTVertex else: - ## case of NonTVertex - natures = [Nature.SILHOUETTE,Nature.BORDER,Nature.CREASE,Nature.MATERIAL_BOUNDARY,Nature.EDGE_MARK, - Nature.SUGGESTIVE_CONTOUR,Nature.VALLEY,Nature.RIDGE] - for nat in natures: - if (self.current_edge.nature & nat) != 0: - count=0 - while not it.is_end: - ve = it.object - if (ve.nature & nat) != 0: - count = count+1 + for nat in NATURES: + if (self.current_edge.nature & nat): + for ve in it: + if (ve.nature & nat): + if winner is not None: + return None winner = ve winnerOrientation = not it.is_incoming - it.increment() - if count != 1: - winner = None break - if winner is not None: - # check whether this edge was part of the selection - if winner.time_stamp != CF.get_time_stamp(): - #print("---", winner.id.first, winner.id.second) - # if not, let's check whether it's short enough with - # respect to the chain made without staying in the selection - #------------------------------------------------------------ - # Did we compute the prospective chain length already ? - if self._length == 0: - #if not, let's do it - _it = pyChainSilhouetteGenericIterator(False, False) - _it.begin = winner - _it.current_edge = winner - _it.orientation = winnerOrientation - _it.init() - while not _it.is_end: - ve = _it.object - #print("--------", ve.id.first, ve.id.second) - self._length = self._length + ve.length_2d - _it.increment() - if _it.is_begin: - break; - _it.begin = winner - _it.current_edge = winner - _it.orientation = winnerOrientation - if not _it.is_begin: - _it.decrement() - while (not _it.is_end) and (not _it.is_begin): - ve = _it.object - #print("--------", ve.id.first, ve.id.second) - self._length = self._length + ve.length_2d - _it.decrement() - # let's do the comparison: - # nw let's compute the length of this connex non selected part: - connexl = 0 + if winner is not None and winner.time_stamp != CF.get_time_stamp(): + + if self._length == 0.0: + self._length = get_chain_length(winner, winnerOrientation) + + connexl = 0.0 _cit = pyChainSilhouetteGenericIterator(False, False) _cit.begin = winner _cit.current_edge = winner _cit.orientation = winnerOrientation _cit.init() - while _cit.is_end == 0 and _cit.object.time_stamp != CF.get_time_stamp(): - ve = _cit.object - #print("-------- --------", ve.id.first, ve.id.second) - connexl = connexl + ve.length_2d + while (not _cit.is_end) and _cit.object.time_stamp != CF.get_time_stamp(): + connexl += _cit.object.length_2d _cit.increment() + if _cit.is_begin: break + if (connexl > self._percent * self._length) or (connexl > self._absLength): - winner = None + return None return winner @@ -603,96 +499,55 @@ class pyFillQi0AbsoluteAndRelativeChainingIterator(ChainingIterator): """ def __init__(self, percent, l): ChainingIterator.__init__(self, False, True, None, True) - self._length = 0 + self._length = 0.0 self._absLength = l - self._percent = float(percent) + self._percent = percent def init(self): # A chain's length should preverably be evaluated only once. # Therefore, the chain length is reset here. - self._length = 0 + self._length = 0.0 def traverse(self, iter): winner = None winnerOrientation = False - - #print(self.current_edge.id.first, self.current_edge.id.second) it = AdjacencyIterator(iter) - tvertex = self.next_vertex - if type(tvertex) is TVertex: - mateVE = tvertex.get_mate(self.current_edge) - while not it.is_end: - ve = it.object - if ve.id == mateVE.id: - winner = ve - winnerOrientation = not it.is_incoming - break - it.increment() + ## case of TVertex + vertex = self.next_vertex + if type(vertex) is TVertex: + mate = vertex.get_mate(self.current_edge) + winner = find_matching_vertex(mate.id, it) + winnerOrientation = not it.is_incoming if not it.is_end else False + ## case of NonTVertex else: - ## case of NonTVertex - natures = [Nature.SILHOUETTE,Nature.BORDER,Nature.CREASE,Nature.MATERIAL_BOUNDARY,Nature.EDGE_MARK, - Nature.SUGGESTIVE_CONTOUR,Nature.VALLEY,Nature.RIDGE] - for nat in natures: - if (self.current_edge.nature & nat) != 0: - count=0 - while not it.is_end: - ve = it.object - if (ve.nature & nat) != 0: - count = count+1 + for nat in NATURES: + if (self.current_edge.nature & nat): + for ve in it: + if (ve.nature & nat): + if winner is not None: + return None winner = ve winnerOrientation = not it.is_incoming - it.increment() - if count != 1: - winner = None break - if winner is not None: - # check whether this edge was part of the selection - if winner.qi != 0: - #print("---", winner.id.first, winner.id.second) - # if not, let's check whether it's short enough with - # respect to the chain made without staying in the selection - #------------------------------------------------------------ - # Did we compute the prospective chain length already ? - if self._length == 0: - #if not, let's do it - _it = pyChainSilhouetteGenericIterator(False, False) - _it.begin = winner - _it.current_edge = winner - _it.orientation = winnerOrientation - _it.init() - while not _it.is_end: - ve = _it.object - #print("--------", ve.id.first, ve.id.second) - self._length = self._length + ve.length_2d - _it.increment() - if _it.is_begin: - break; - _it.begin = winner - _it.current_edge = winner - _it.orientation = winnerOrientation - if not _it.is_begin: - _it.decrement() - while (not _it.is_end) and (not _it.is_begin): - ve = _it.object - #print("--------", ve.id.first, ve.id.second) - self._length = self._length + ve.length_2d - _it.decrement() - # let's do the comparison: - # nw let's compute the length of this connex non selected part: + if winner is not None and winner.qi: + + + if self._length == 0.0: + self._length = get_chain_length(winner, winnerOrientation) + connexl = 0 _cit = pyChainSilhouetteGenericIterator(False, False) _cit.begin = winner _cit.current_edge = winner _cit.orientation = winnerOrientation _cit.init() - while not _cit.is_end and _cit.object.qi != 0: - ve = _cit.object - #print("-------- --------", ve.id.first, ve.id.second) - connexl = connexl + ve.length_2d + while (not _cit.is_end) and _cit.object.qi != 0: + connexl += _cit.object.length_2d _cit.increment() + if _cit.is_begin: break if (connexl > self._percent * self._length) or (connexl > self._absLength): - winner = None + return None return winner @@ -717,63 +572,44 @@ class pyNoIdChainSilhouetteIterator(ChainingIterator): def traverse(self, iter): winner = None it = AdjacencyIterator(iter) - tvertex = self.next_vertex - if type(tvertex) is TVertex: - mateVE = tvertex.get_mate(self.current_edge) - while not it.is_end: - ve = it.object - feB = self.current_edge.last_fedge - feA = ve.first_fedge - vB = feB.second_svertex - vA = feA.first_svertex + # case of TVertex + vertex = self.next_vertex + if type(vertex) is TVertex: + for ve in it: + # case one + vA = self.current_edge.last_fedge.second_svertex + vB = ve.first_fedge.first_svertex if vA.id.first == vB.id.first: - winner = ve - break - feA = self.current_edge.first_fedge - feB = ve.last_fedge - vB = feB.second_svertex - vA = feA.first_svertex + return ve + # case two + vA = self.current_edge.first_fedge.first_svertex + vB = ve.last_fedge.second_svertex if vA.id.first == vB.id.first: - winner = ve - break - feA = self.current_edge.last_fedge - feB = ve.last_fedge - vB = feB.second_svertex - vA = feA.second_svertex + return ve + # case three + vA = self.current_edge.last_fedge.second_svertex + vB = ve.last_fedge.second_svertex if vA.id.first == vB.id.first: - winner = ve - break - feA = self.current_edge.first_fedge - feB = ve.first_fedge - vB = feB.first_svertex - vA = feA.first_svertex + return ve + # case four + vA = self.current_edge.first_fedge.first_svertex + vB = ve.first_fedge.first_svertex if vA.id.first == vB.id.first: - winner = ve - break - it.increment() + return ve + return None + ## case of NonTVertex else: - ## case of NonTVertex - natures = [Nature.SILHOUETTE,Nature.BORDER,Nature.CREASE,Nature.MATERIAL_BOUNDARY,Nature.EDGE_MARK, - Nature.SUGGESTIVE_CONTOUR,Nature.VALLEY,Nature.RIDGE] - for i in range(len(natures)): - currentNature = self.current_edge.nature - if (natures[i] & currentNature) != 0: - count=0 - while not it.is_end: - visitNext = 0 - oNature = it.object.nature - if (oNature & natures[i]) != 0: - if natures[i] != oNature: - for j in range(i): - if (natures[j] & oNature) != 0: - visitNext = 1 - break - if visitNext != 0: - break - count = count+1 - winner = it.object - it.increment() - if count != 1: - winner = None - break - return winner + for i, nat in enumerate(NATURES): + if (nat & self.current_edge.nature): + for ve in it: + ve_nat = ve.nature + if (ve_nat & nat): + if (nat != ve_nat) and any(n & ve_nat for n in NATURES[:i]): + break + + if winner is not None: + return + + winner = ve + return winner + return None diff --git a/release/scripts/freestyle/modules/freestyle/functions.py b/release/scripts/freestyle/modules/freestyle/functions.py index 379e933862c..773d04ddeab 100644 --- a/release/scripts/freestyle/modules/freestyle/functions.py +++ b/release/scripts/freestyle/modules/freestyle/functions.py @@ -91,8 +91,8 @@ from freestyle.utils import integrate from mathutils import Vector -## Functions for 0D elements (vertices) -####################################### + +# -- Functions for 0D elements (vertices) -- # class CurveMaterialF0D(UnaryFunction0DMaterial): @@ -104,7 +104,7 @@ class CurveMaterialF0D(UnaryFunction0DMaterial): cp = inter.object assert(isinstance(cp, CurvePoint)) fe = cp.first_svertex.get_fedge(cp.second_svertex) - assert(fe is not None) + assert(fe is not None), "CurveMaterialF0D: fe is None" return fe.material if fe.is_smooth else fe.material_left @@ -140,11 +140,7 @@ class pyDensityAnisotropyF0D(UnaryFunction0DDouble): c_3 = self.d3Density(inter) cMax = max(max(c_0,c_1), max(c_2,c_3)) cMin = min(min(c_0,c_1), min(c_2,c_3)) - if c_iso == 0: - v = 0 - else: - v = (cMax-cMin)/c_iso - return v + return 0 if (c_iso == 0) else (cMax-cMin) / c_iso class pyViewMapGradientVectorF0D(UnaryFunction0DVec2f): @@ -161,9 +157,9 @@ class pyViewMapGradientVectorF0D(UnaryFunction0DVec2f): def __call__(self, iter): p = iter.object.point_2d gx = CF.read_complete_view_map_pixel(self._l, int(p.x+self._step), int(p.y)) - \ - CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y)) + CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y)) gy = CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y+self._step)) - \ - CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y)) + CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y)) return Vector((gx, gy)) @@ -171,19 +167,18 @@ class pyViewMapGradientNormF0D(UnaryFunction0DDouble): def __init__(self, l): UnaryFunction0DDouble.__init__(self) self._l = l - self._step = pow(2,self._l) + self._step = pow(2, self._l) def __call__(self, iter): p = iter.object.point_2d - gx = CF.read_complete_view_map_pixel(self._l, int(p.x+self._step), int(p.y)) - \ - CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y)) - gy = CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y+self._step)) - \ - CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y)) - grad = Vector((gx, gy)) - return grad.length + gx = CF.read_complete_view_map_pixel(self._l, int(p.x + self._step), int(p.y)) - \ + CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y)) + gy = CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y + self._step)) - \ + CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y)) + return Vector((gx, gy)).length -## Functions for 1D elements (curves) -##################################### + +# -- Functions for 1D elements (curves) -- # class pyGetInverseProjectedZF1D(UnaryFunction1DDouble): diff --git a/release/scripts/freestyle/modules/freestyle/predicates.py b/release/scripts/freestyle/modules/freestyle/predicates.py index ce6f0a35ffe..fede3e3e2da 100644 --- a/release/scripts/freestyle/modules/freestyle/predicates.py +++ b/release/scripts/freestyle/modules/freestyle/predicates.py @@ -51,6 +51,7 @@ from freestyle.types import ( TVertex, UnaryPredicate0D, UnaryPredicate1D, + Id, ) from freestyle.functions import ( Curvature2DAngleF0D, @@ -70,14 +71,15 @@ from freestyle.functions import ( pyDensityAnisotropyF1D, pyViewMapGradientNormF1D, ) + import random -## Unary predicates for 0D elements (vertices) -############################################## +# -- Unary predicates for 0D elements (vertices) -- # + class pyHigherCurvature2DAngleUP0D(UnaryPredicate0D): - def __init__(self,a): + def __init__(self, a): UnaryPredicate0D.__init__(self) self._a = a @@ -88,15 +90,15 @@ class pyHigherCurvature2DAngleUP0D(UnaryPredicate0D): class pyUEqualsUP0D(UnaryPredicate0D): - def __init__(self,u, w): + def __init__(self, u, w): UnaryPredicate0D.__init__(self) self._u = u self._w = w + self._func = pyCurvilinearLengthF0D() def __call__(self, inter): - func = pyCurvilinearLengthF0D() - u = func(inter) - return (u > (self._u-self._w)) and (u < (self._u+self._w)) + u = self._func(inter) + return (u > (self._u - self._w)) and (u < (self._u + self._w)) class pyVertexNatureUP0D(UnaryPredicate0D): @@ -105,26 +107,23 @@ class pyVertexNatureUP0D(UnaryPredicate0D): self._nature = nature def __call__(self, inter): - v = inter.object - return (v.nature & self._nature) != 0 + return bool(inter.object.nature & self._nature) -## check whether an Interface0DIterator -## is a TVertex and is the one that is -## hidden (inferred from the context) class pyBackTVertexUP0D(UnaryPredicate0D): + """ + Check whether an Interface0DIterator + references a TVertex and is the one that is + hidden (inferred from the context) + """ def __init__(self): UnaryPredicate0D.__init__(self) self._getQI = QuantitativeInvisibilityF0D() def __call__(self, iter): - if (iter.object.nature & Nature.T_VERTEX) == 0: + if not (iter.object.nature & Nature.T_VERTEX) or iter.is_end: return False - if iter.is_end: - return False - if self._getQI(iter) != 0: - return True - return False + return self._getQI(iter) != 0 class pyParameterUP0DGoodOne(UnaryPredicate0D): @@ -135,7 +134,7 @@ class pyParameterUP0DGoodOne(UnaryPredicate0D): def __call__(self, inter): u = inter.u - return ((u>=self._m) and (u<=self._M)) + return ((u >= self._m) and (u <= self._M)) class pyParameterUP0D(UnaryPredicate0D): @@ -143,36 +142,39 @@ class pyParameterUP0D(UnaryPredicate0D): UnaryPredicate0D.__init__(self) self._m = pmin self._M = pmax + self._func = Curvature2DAngleF0D() def __call__(self, inter): - func = Curvature2DAngleF0D() - c = func(inter) - b1 = (c>0.1) + c = self._func(inter) + b1 = (c > 0.1) u = inter.u - b = ((u>=self._m) and (u<=self._M)) - return b and b1 + b = ((u >= self._m) and (u <= self._M)) + return (b and b1) + + +# -- Unary predicates for 1D elements (curves) -- # -## Unary predicates for 1D elements (curves) -############################################ class AndUP1D(UnaryPredicate1D): - def __init__(self, pred1, pred2): + def __init__(self, *predicates): UnaryPredicate1D.__init__(self) - self.__pred1 = pred1 - self.__pred2 = pred2 + self.predicates = predicates + if len(self.predicates) < 2: + raise ValueError("Expected two or more UnaryPredicate1D") def __call__(self, inter): - return self.__pred1(inter) and self.__pred2(inter) + return all(pred(inter) for pred in self.predicates) class OrUP1D(UnaryPredicate1D): - def __init__(self, pred1, pred2): + def __init__(self, *predicates): UnaryPredicate1D.__init__(self) - self.__pred1 = pred1 - self.__pred2 = pred2 + self.predicates = predicates + if len(self.predicates) < 2: + raise ValueError("Expected two or more UnaryPredicate1D") def __call__(self, inter): - return self.__pred1(inter) or self.__pred2(inter) + return any(pred(inter) for pred in self.predicates) class NotUP1D(UnaryPredicate1D): @@ -184,6 +186,29 @@ class NotUP1D(UnaryPredicate1D): return not self.__pred(inter) +class ObjectNamesUP1D(UnaryPredicate1D): + def __init__(self, names, negative=False): + UnaryPredicate1D.__init__(self) + self._names = names + self._negative = negative + + def __call__(self, viewEdge): + found = viewEdge.viewshape.name in self._names + return found if not self._negative else not found + + +class QuantitativeInvisibilityRangeUP1D(UnaryPredicate1D): + def __init__(self, qi_start, qi_end): + UnaryPredicate1D.__init__(self) + self.__getQI = QuantitativeInvisibilityF1D() + self.__qi_start = qi_start + self.__qi_end = qi_end + + def __call__(self, inter): + qi = self.__getQI(inter) + return (self.__qi_start <= qi <= self.__qi_end) + + class pyNFirstUP1D(UnaryPredicate1D): def __init__(self, n): UnaryPredicate1D.__init__(self) @@ -191,14 +216,12 @@ class pyNFirstUP1D(UnaryPredicate1D): self.__count = 0 def __call__(self, inter): - self.__count = self.__count + 1 - if self.__count <= self.__n: - return True - return False + self.__count += 1 + return (self.__count <= self.__n) class pyHigherLengthUP1D(UnaryPredicate1D): - def __init__(self,l): + def __init__(self, l): UnaryPredicate1D.__init__(self) self._l = l @@ -213,28 +236,20 @@ class pyNatureUP1D(UnaryPredicate1D): self._getNature = CurveNatureF1D() def __call__(self, inter): - if(self._getNature(inter) & self._nature): - return True - return False + return bool(self._getNature(inter) & self._nature) class pyHigherNumberOfTurnsUP1D(UnaryPredicate1D): - def __init__(self,n,a): + def __init__(self, n, a): UnaryPredicate1D.__init__(self) self._n = n self._a = a def __call__(self, inter): - count = 0 func = Curvature2DAngleF0D() it = inter.vertices_begin() - while not it.is_end: - if func(it) > self._a: - count = count+1 - if count > self._n: - return True - it.increment() - return False + # sum the turns, check against n + return sum(1 for ve in it if func(it) > self._a) > self._n class pyDensityUP1D(UnaryPredicate1D): @@ -278,9 +293,7 @@ class pyHighSteerableViewMapDensityUP1D(UnaryPredicate1D): def __init__(self, threshold, level, integration=IntegrationType.MEAN): UnaryPredicate1D.__init__(self) self._threshold = threshold - self._level = level - self._integration = integration - self._func = GetSteerableViewMapDensityF1D(self._level, self._integration) + self._func = GetSteerableViewMapDensityF1D(level, integration) def __call__(self, inter): return (self._func(inter) > self._threshold) @@ -290,24 +303,17 @@ class pyHighDirectionalViewMapDensityUP1D(UnaryPredicate1D): def __init__(self, threshold, orientation, level, integration=IntegrationType.MEAN, sampling=2.0): UnaryPredicate1D.__init__(self) self._threshold = threshold - self._orientation = orientation - self._level = level - self._integration = integration - self._sampling = sampling + self._func = GetDirectionalViewMapDensityF1D(orientation, level, integration, sampling) def __call__(self, inter): - func = GetDirectionalViewMapDensityF1D(self._orientation, self._level, self._integration, self._sampling) - return (func(inter) > self._threshold) + return (self.func(inter) > self._threshold) class pyHighViewMapDensityUP1D(UnaryPredicate1D): def __init__(self, threshold, level, integration=IntegrationType.MEAN, sampling=2.0): UnaryPredicate1D.__init__(self) self._threshold = threshold - self._level = level - self._integration = integration - self._sampling = sampling - self._func = GetCompleteViewMapDensityF1D(self._level, self._integration, self._sampling) # 2.0 is the smpling + self._func = GetCompleteViewMapDensityF1D(level, integration, sampling) def __call__(self, inter): return (self._func(inter) > self._threshold) @@ -316,67 +322,56 @@ class pyHighViewMapDensityUP1D(UnaryPredicate1D): class pyDensityFunctorUP1D(UnaryPredicate1D): def __init__(self, wsize, threshold, functor, funcmin=0.0, funcmax=1.0, integration=IntegrationType.MEAN): UnaryPredicate1D.__init__(self) - self._wsize = wsize self._threshold = float(threshold) self._functor = functor self._funcmin = float(funcmin) self._funcmax = float(funcmax) - self._integration = integration + self._func = DensityF1D(wsize, integration) def __call__(self, inter): - func = DensityF1D(self._wsize, self._integration) res = self._functor(inter) - k = (res-self._funcmin)/(self._funcmax-self._funcmin) + k = (res - self._funcmin) / (self._funcmax - self._funcmin) return (func(inter) < (self._threshold * k)) class pyZSmallerUP1D(UnaryPredicate1D): - def __init__(self,z, integration=IntegrationType.MEAN): + def __init__(self, z, integration=IntegrationType.MEAN): UnaryPredicate1D.__init__(self) self._z = z - self._integration = integration + self.func = GetProjectedZF1D(integration) def __call__(self, inter): - func = GetProjectedZF1D(self._integration) - return (func(inter) < self._z) + return (self.func(inter) < self._z) class pyIsOccludedByUP1D(UnaryPredicate1D): def __init__(self,id): UnaryPredicate1D.__init__(self) + if not isinstance(id, Id): + raise TypeError("pyIsOccludedByUP1D expected freestyle.types.Id, not " + type(id).__name__) self._id = id def __call__(self, inter): - func = GetShapeF1D() - shapes = func(inter) - for s in shapes: - if(s.id == self._id): - return False + shapes = GetShapeF1D()(inter) + if any(s.id == self._id for s in shapes): + return False + + # construct iterators it = inter.vertices_begin() itlast = inter.vertices_end() itlast.decrement() - v = it.object - vlast = itlast.object - tvertex = v.viewvertex - if type(tvertex) is TVertex: - #print("TVertex: [ ", tvertex.id.first, ",", tvertex.id.second," ]") + + vertex = next(it) + if type(vertex) is TVertex: + eit = vertex.edges_begin() + if any(ve.id == self._id for (ve, incoming) in eit): + return True + + vertex = next(itlast) + if type(vertex) is TVertex: eit = tvertex.edges_begin() - while not eit.is_end: - ve, incoming = eit.object - if ve.id == self._id: - return True - #print("-------", ve.id.first, "-", ve.id.second) - eit.increment() - tvertex = vlast.viewvertex - if type(tvertex) is TVertex: - #print("TVertex: [ ", tvertex.id.first, ",", tvertex.id.second," ]") - eit = tvertex.edges_begin() - while not eit.is_end: - ve, incoming = eit.object - if ve.id == self._id: - return True - #print("-------", ve.id.first, "-", ve.id.second) - eit.increment() + if any(ve.id == self._id for (ve, incoming) in eit): + return True return False @@ -386,12 +381,8 @@ class pyIsInOccludersListUP1D(UnaryPredicate1D): self._id = id def __call__(self, inter): - func = GetOccludersF1D() - occluders = func(inter) - for a in occluders: - if a.id == self._id: - return True - return False + occluders = GetOccludersF1D()(inter) + return any(a.id == self._id for a in occluders) class pyIsOccludedByItselfUP1D(UnaryPredicate1D): @@ -403,11 +394,7 @@ class pyIsOccludedByItselfUP1D(UnaryPredicate1D): def __call__(self, inter): lst1 = self.__func1(inter) lst2 = self.__func2(inter) - for vs1 in lst1: - for vs2 in lst2: - if vs1.id == vs2.id: - return True - return False + return any(vs1.id == vs2.id for vs1 in lst1 for vs2 in lst2) class pyIsOccludedByIdListUP1D(UnaryPredicate1D): @@ -417,27 +404,17 @@ class pyIsOccludedByIdListUP1D(UnaryPredicate1D): self.__func1 = GetOccludersF1D() def __call__(self, inter): - lst1 = self.__func1(inter) - for vs1 in lst1: - for _id in self._idlist: - if vs1.id == _id: - return True - return False + lst1 = self.__func1(inter.object) + return any(vs1.id == _id for vs1 in lst1 for _id in self._idlist) class pyShapeIdListUP1D(UnaryPredicate1D): def __init__(self,idlist): UnaryPredicate1D.__init__(self) - self._idlist = idlist - self._funcs = [] - for _id in idlist: - self._funcs.append(ShapeUP1D(_id.first, _id.second)) + self._funcs = tuple(ShapeUP1D(_id, 0) for _id in idlist) def __call__(self, inter): - for func in self._funcs: - if func(inter) == 1: - return True - return False + return any(func(inter) for func in self._funcs) ## deprecated @@ -447,12 +424,8 @@ class pyShapeIdUP1D(UnaryPredicate1D): self._id = _id def __call__(self, inter): - func = GetShapeF1D() - shapes = func(inter) - for a in shapes: - if a.id == self._id: - return True - return False + shapes = GetShapeF1D()(inter) + return any(a.id == self._id for a in shapes) class pyHighDensityAnisotropyUP1D(UnaryPredicate1D): @@ -473,7 +446,6 @@ class pyHighViewMapGradientNormUP1D(UnaryPredicate1D): def __call__(self, inter): gn = self._GetGradient(inter) - #print(gn) return (gn > self._threshold) @@ -503,53 +475,50 @@ class pyClosedCurveUP1D(UnaryPredicate1D): it = inter.vertices_begin() itlast = inter.vertices_end() itlast.decrement() - vlast = itlast.object - v = it.object - #print(v.id.first, v.id.second) - #print(vlast.id.first, vlast.id.second) - if v.id == vlast.id: - return True - return False + return (next(it).id == next(itlast).id) + + +# -- Binary predicates for 1D elements (curves) -- # -## Binary predicates for 1D elements (curves) -############################################# class AndBP1D(BinaryPredicate1D): - def __init__(self, pred1, pred2): + def __init__(self, *predicates): BinaryPredicate1D.__init__(self) - self.__pred1 = pred1 - self.__pred2 = pred2 + self._predicates = predicates + if len(self.predicates) < 2: + raise ValueError("Expected two or more BinaryPredicate1D") - def __call__(self, inter1, inter2): - return self.__pred1(inter1, inter2) and self.__pred2(inter1, inter2) + def __call__(self, i1, i2): + return all(pred(i1, i2) for pred in self._predicates) class OrBP1D(BinaryPredicate1D): - def __init__(self, pred1, pred2): + def __init__(self, *predicates): BinaryPredicate1D.__init__(self) - self.__pred1 = pred1 - self.__pred2 = pred2 + self._predicates = predicates + if len(self.predicates) < 2: + raise ValueError("Expected two or more BinaryPredicate1D") - def __call__(self, inter1, inter2): - return self.__pred1(inter1, inter2) or self.__pred2(inter1, inter2) + def __call__(self, i1, i2): + return any(pred(i1, i2) for pred in self._predicates) class NotBP1D(BinaryPredicate1D): - def __init__(self, pred): + def __init__(self, predicate): BinaryPredicate1D.__init__(self) - self.__pred = pred + self._predicate = predicate - def __call__(self, inter1, inter2): - return not self.__pred(inter1, inter2) + def __call__(self, i1, i2): + return (not self._precicate(i1, i2)) class pyZBP1D(BinaryPredicate1D): def __init__(self, iType=IntegrationType.MEAN): BinaryPredicate1D.__init__(self) - self._GetZ = GetZF1D(iType) + self.func = GetZF1D(iType) def __call__(self, i1, i2): - return (self._GetZ(i1) > self._GetZ(i2)) + return (self.func(i1) > self.func(i2)) class pyZDiscontinuityBP1D(BinaryPredicate1D): @@ -569,10 +538,10 @@ class pyLengthBP1D(BinaryPredicate1D): class pySilhouetteFirstBP1D(BinaryPredicate1D): def __call__(self, inter1, inter2): bpred = SameShapeIdBP1D() - if (bpred(inter1, inter2) != 1): + if (not bpred(inter1, inter2)): return False if (inter1.nature & Nature.SILHOUETTE): - return (inter2.nature & Nature.SILHOUETTE) != 0 + return bool(inter2.nature & Nature.SILHOUETTE) return (inter1.nature == inter2.nature) @@ -587,16 +556,13 @@ class pyViewMapGradientNormBP1D(BinaryPredicate1D): self._GetGradient = pyViewMapGradientNormF1D(l, IntegrationType.MEAN) def __call__(self, i1,i2): - #print("compare gradient") return (self._GetGradient(i1) > self._GetGradient(i2)) class pyShuffleBP1D(BinaryPredicate1D): def __init__(self): BinaryPredicate1D.__init__(self) - random.seed(1) + random.seed = 1 def __call__(self, inter1, inter2): - r1 = random.uniform(0,1) - r2 = random.uniform(0,1) - return (r1 self.threshold_max: - c = self.threshold_max -## t = (c - self.threshold_min)/(self.threshold_max - self.threshold_min)*(self._thicknessMax-self._thicknessMin) + self._thicknessMin - t = (self.threshold_max - c )/(self.threshold_max - self.threshold_min)*(self._thicknessMax-self._thicknessMin) + self._thicknessMin - it.object.attribute.thickness = (t/2.0, t/2.0) - i = i+1 - it.increment() + it = Interface0DIterator(stroke) + delta_threshold = self.threshold_max - self.threshold_min + delta_thickness = self._thicknessMax - self._thicknessMin + + for svert in it: + c = self._func(it) + c = bound(self.threshold_min, c, self.threshold_max) + t = (self.threshold_max - c) / delta_threshold * delta_thickness + self._thicknessMin + svert.attribute.thickness = (t / 2.0, t / 2.0) class pyIncreasingThicknessShader(StrokeShader): @@ -165,18 +163,14 @@ class pyIncreasingThicknessShader(StrokeShader): self._thicknessMax = thicknessMax def shade(self, stroke): - n = stroke.stroke_vertices_size() - i = 0 - it = stroke.stroke_vertices_begin() - while not it.is_end: - c = float(i)/float(n) - if i < float(n)/2.0: - t = (1.0 - c)*self._thicknessMin + c * self._thicknessMax + n = len(stroke) + for i, svert in enumerate(stroke): + c = i / n + if i < (n * 0.5): + t = (1.0 - c) * self._thicknessMin + c * self._thicknessMax else: - t = (1.0 - c)*self._thicknessMax + c * self._thicknessMin - it.object.attribute.thickness = (t/2.0, t/2.0) - i = i+1 - it.increment() + t = (1.0 - c) * self._thicknessMax + c * self._thicknessMin + svert.attribute.thickness = (t / 2.0, t / 2.0) class pyConstrainedIncreasingThicknessShader(StrokeShader): @@ -191,28 +185,20 @@ class pyConstrainedIncreasingThicknessShader(StrokeShader): self._ratio = ratio def shade(self, stroke): - slength = stroke.length_2d - tmp = self._ratio*slength - maxT = 0.0 - if tmp < self._thicknessMax: - maxT = tmp - else: - maxT = self._thicknessMax - n = stroke.stroke_vertices_size() - i = 0 - it = stroke.stroke_vertices_begin() - while not it.is_end: - att = it.object.attribute - c = float(i)/float(n) - if i < float(n)/2.0: - t = (1.0 - c)*self._thicknessMin + c * maxT + n = len(stroke) + maxT = min(self._ratio * stroke.length_2d, self._thicknessMax) + + for i, svert in enumerate(stroke): + c = i / n + if i < (n * 0.5): + t = (1.0 - c) * self._thicknessMin + c * maxT else: - t = (1.0 - c)*maxT + c * self._thicknessMin - att.thickness = (t/2.0, t/2.0) - if i == n-1: - att.thickness = (self._thicknessMin/2.0, self._thicknessMin/2.0) - i = i+1 - it.increment() + t = (1.0 - c) * maxT + c * self._thicknessMin + + if i == (n - 1): + svert.attribute.thickness = (self._thicknessMin / 2.0, self._thicknessMin / 2.0) + else: + svert.attribute.thickness = (t / 2.0, t / 2.0) class pyDecreasingThicknessShader(StrokeShader): @@ -226,21 +212,14 @@ class pyDecreasingThicknessShader(StrokeShader): def shade(self, stroke): l = stroke.length_2d - tMax = self._thicknessMax - if self._thicknessMax > 0.33*l: - tMax = 0.33*l - tMin = self._thicknessMin - if self._thicknessMin > 0.1*l: - tMin = 0.1*l - n = stroke.stroke_vertices_size() - i = 0 - it = stroke.stroke_vertices_begin() - while not it.is_end: - c = float(i)/float(n) - t = (1.0 - c)*tMax +c*tMin - it.object.attribute.thickness = (t/2.0, t/2.0) - i = i+1 - it.increment() + n = len(stroke) + tMax = min(self._thicknessMax, 0.33 * l) + tMin = min(self._thicknessMin, 0.10 * l) + + for i, svert in enumerate(stroke): + c = i / n + t = (1.0 - c) * tMax + c * tMin + svert.attribute.thickness = (t / 2.0, t / 2.0) class pyNonLinearVaryingThicknessShader(StrokeShader): @@ -248,28 +227,18 @@ class pyNonLinearVaryingThicknessShader(StrokeShader): Assigns thickness to a stroke based on an exponential function """ def __init__(self, thicknessExtremity, thicknessMiddle, exponent): - StrokeShader.__init__(self) self._thicknessMin = thicknessMiddle self._thicknessMax = thicknessExtremity - self._exponent = exponent + self._exp = exponent + StrokeShader.__init__(self) def shade(self, stroke): - n = stroke.stroke_vertices_size() - i = 0 - it = stroke.stroke_vertices_begin() - while not it.is_end: - if i < float(n)/2.0: - c = float(i)/float(n) - else: - c = float(n-i)/float(n) - c = self.smoothC(c, self._exponent) - t = (1.0 - c)*self._thicknessMax + c * self._thicknessMin - it.object.attribute.thickness = (t/2.0, t/2.0) - i = i+1 - it.increment() - - def smoothC(self, a, exp): - return pow(float(a), exp) * pow(2.0, exp) + n = len(stroke) + for i, svert in enumerate(stroke): + c = (i / n) if (i < n / 2.0) else ((n - i) / n) + c = pow(c, self._exp) * pow(2.0, self._exp) + t = (1.0 - c) * self._thicknessMax + c * self._thicknessMin + svert.attribute.thickness = (t / 2.0, t / 2.0) class pySLERPThicknessShader(StrokeShader): @@ -280,29 +249,23 @@ class pySLERPThicknessShader(StrokeShader): StrokeShader.__init__(self) self._thicknessMin = thicknessMin self._thicknessMax = thicknessMax - self._omega = omega + self.omega = omega def shade(self, stroke): - slength = stroke.length_2d - tmp = 0.33*slength - maxT = self._thicknessMax - if tmp < self._thicknessMax: - maxT = tmp - n = stroke.stroke_vertices_size() - i = 0 - it = stroke.stroke_vertices_begin() - while not it.is_end: - c = float(i)/float(n) - if i < float(n)/2.0: - t = sin((1-c)*self._omega)/sinh(self._omega)*self._thicknessMin + sin(c*self._omega)/sinh(self._omega) * maxT + n = len(stroke) + maxT = min(self._thicknessMax, 0.33 * stroke.length_2d) + omega = self.omega + sinhyp = sinh(omega) + for i, svert in enumerate(stroke): + c = i / n + if i < (n * 0.5): + t = sin((1-c) * omega) / sinhyp * self._thicknessMin + sin(c * omega) / sinhyp * maxT else: - t = sin((1-c)*self._omega)/sinh(self._omega)*maxT + sin(c*self._omega)/sinh(self._omega) * self._thicknessMin - it.object.attribute.thickness = (t/2.0, t/2.0) - i = i+1 - it.increment() + t = sin((1-c) * omega) / sinhyp * maxT + sin(c * omega) / sinhyp * self._thicknessMin + svert.attribute.thickness = (t / 2.0, t / 2.0) -class pyTVertexThickenerShader(StrokeShader): ## FIXME +class pyTVertexThickenerShader(StrokeShader): """ Thickens TVertices (visual intersections between two edges) """ @@ -312,46 +275,22 @@ class pyTVertexThickenerShader(StrokeShader): ## FIXME self._n = n def shade(self, stroke): - it = stroke.stroke_vertices_begin() - predTVertex = pyVertexNatureUP0D(Nature.T_VERTEX) - while not it.is_end: - if predTVertex(it) == 1: - it2 = StrokeVertexIterator(it) - it2.increment() - if not (it.is_begin or it2.is_end): - it.increment() - continue - n = self._n - a = self._a - if it.is_begin: - it3 = StrokeVertexIterator(it) - count = 0 - while (not it3.is_end) and count < n: - att = it3.object.attribute - (tr, tl) = att.thickness - r = (a-1.0)/float(n-1)*(float(n)/float(count+1) - 1) + 1 - #r = (1.0-a)/float(n-1)*count + a - att.thickness = (r*tr, r*tl) - it3.increment() - count = count + 1 - if it2.is_end: - it4 = StrokeVertexIterator(it) - count = 0 - while (not it4.is_begin) and count < n: - att = it4.object.attribute - (tr, tl) = att.thickness - r = (a-1.0)/float(n-1)*(float(n)/float(count+1) - 1) + 1 - #r = (1.0-a)/float(n-1)*count + a - att.thickness = (r*tr, r*tl) - it4.decrement() - count = count + 1 - if it4.is_begin: - att = it4.object.attribute - (tr, tl) = att.thickness - r = (a-1.0)/float(n-1)*(float(n)/float(count+1) - 1) + 1 - #r = (1.0-a)/float(n-1)*count + a - att.thickness = (r*tr, r*tl) - it.increment() + n = self._n + a = self._a + + term = (a - 1.0) / (n - 1.0) + + if (stroke[0].nature & Nature.T_VERTEX): + for count, svert in zip(range(n), stroke): + r = term * (n / (count + 1.0) - 1.0) + 1.0 + (tr, tl) = svert.attribute.thickness + svert.attribute.thickness = (r * tr, r * tl) + + if (stroke[-1].nature & Nature.T_VERTEX): + for count, svert in zip(range(n), reversed(stroke)): + r = term * (n / (count + 1.0) - 1.0) + 1.0 + (tr, tl) = svert.attribute.thickness + svert.attribute.thickness = (r * tr, r * tl) class pyImportance2DThicknessShader(StrokeShader): @@ -362,26 +301,18 @@ class pyImportance2DThicknessShader(StrokeShader): """ def __init__(self, x, y, w, kmin, kmax): StrokeShader.__init__(self) - self._x = x - self._y = y - self._w = float(w) - self._kmin = float(kmin) - self._kmax = float(kmax) + self._origin = Vector((x, y)) + self._w = w + self._kmin, self._kmax = kmin, kmax def shade(self, stroke): - origin = Vector((self._x, self._y)) - it = stroke.stroke_vertices_begin() - while not it.is_end: - v = it.object - d = (v.point_2d - self._origin).length - if d > self._w: - k = self._kmin - else: - k = (self._kmax*(self._w-d) + self._kmin*d)/self._w - att = v.attribute - (tr, tl) = att.thickness - att.thickness = (k*tr/2.0, k*tl/2.0) - it.increment() + for svert in stroke: + d = (svert.point_2d - self._origin).length + k = (self._kmin if (d > self._w) else + (self._kmax * (self._w-d) + self._kmin * d) / self._w) + + (tr, tl) = svert.attribute.thickness + svert.attribute.thickness = (k*tr/2.0, k*tl/2.0) class pyImportance3DThicknessShader(StrokeShader): @@ -390,28 +321,18 @@ class pyImportance3DThicknessShader(StrokeShader): """ def __init__(self, x, y, z, w, kmin, kmax): StrokeShader.__init__(self) - self._x = x - self._y = y - self._z = z - self._w = float(w) - self._kmin = float(kmin) - self._kmax = float(kmax) + self._origin = Vector((x, y, z)) + self._w = w + self._kmin, self._kmax = kmin, kmax def shade(self, stroke): - origin = Vector((self._x, self._y, self._z)) - it = stroke.stroke_vertices_begin() - while not it.is_end: - v = it.object - p = v.point_3d - d = (p-origin).length - if d > self._w: - k = self._kmin - else: - k = (self._kmax*(self._w-d) + self._kmin*d)/self._w - att = v.attribute - (tr, tl) = att.thickness - att.thickness = (k*tr/2.0, k*tl/2.0) - it.increment() + for svert in stroke: + d = (svert.point_3d - self._origin).length + k = (self._kmin if (d > self._w) else + (self._kmax * (self._w-d) + self._kmin * d) / self._w) + + (tr, tl) = svert.attribute.thickness + svert.attribute.thickness = (k*tr/2.0, k*tl/2.0) class pyZDependingThicknessShader(StrokeShader): @@ -423,49 +344,35 @@ class pyZDependingThicknessShader(StrokeShader): StrokeShader.__init__(self) self.__min = min self.__max = max - self.__func = GetProjectedZF0D() + self.func = GetProjectedZF0D() def shade(self, stroke): - it = stroke.stroke_vertices_begin() - z_min = 1 - z_max = 0 - while not it.is_end: - z = self.__func(Interface0DIterator(it)) - if z < z_min: - z_min = z - if z > z_max: - z_max = z - it.increment() + it = Interface0DIterator(stroke) + z_indices = tuple(self.func(it) for _ in it) + z_min, z_max = min(1, *z_indices), max(0, *z_indices) z_diff = 1 / (z_max - z_min) - it = stroke.stroke_vertices_begin() - while not it.is_end: - z = (self.__func(Interface0DIterator(it)) - z_min) * z_diff + + for svert, z_index in zip(stroke, z_indices): + z = (z_index - z_min) * z_diff thickness = (1 - z) * self.__max + z * self.__min - it.object.attribute.thickness = (thickness, thickness) - it.increment() + svert.attribute.thickness = (thickness, thickness) -## color modifiers -################## +# -- Color & Alpha Stroke Shaders -- # + class pyConstantColorShader(StrokeShader): """ Assigns a constant color to the stroke """ - def __init__(self, r, g, b, a=1): + def __init__(self,r,g,b, a = 1): StrokeShader.__init__(self) - self._r = r - self._g = g - self._b = b + self._color = (r, g, b) self._a = a - def shade(self, stroke): - it = stroke.stroke_vertices_begin() - while not it.is_end: - att = it.object.attribute - att.color = (self._r, self._g, self._b) - att.alpha = self._a - it.increment() + for svert in stroke: + svert.attribute.color = self._color + svert.attribute.alpha = self._a class pyIncreasingColorShader(StrokeShader): @@ -474,23 +381,18 @@ class pyIncreasingColorShader(StrokeShader): """ def __init__(self,r1,g1,b1,a1, r2,g2,b2,a2): StrokeShader.__init__(self) - self._c1 = [r1,g1,b1,a1] - self._c2 = [r2,g2,b2,a2] + # use 4d vector to simplify math + self._c1 = Vector((r1, g1 ,b1, a1)) + self._c2 = Vector((r2, g2, b2, a2)) def shade(self, stroke): - n = stroke.stroke_vertices_size() - 1 - inc = 0 - it = stroke.stroke_vertices_begin() - while not it.is_end: - att = it.object.attribute - c = float(inc) / float(n) + n = len(stroke) - 1 - att.color = ((1.0 - c) * self._c1[0] + c * self._c2[0], - (1.0 - c) * self._c1[1] + c * self._c2[1], - (1.0 - c) * self._c1[2] + c * self._c2[2]) - att.alpha = (1.0 - c) * self._c1[3] + c * self._c2[3] - inc = inc + 1 - it.increment() + for i, svert in enumerate(stroke): + c = i / n + color = (1 - c) * self._c1 + c * self._c2 + svert.attribute.color = color[:3] + svert.attribute.alpha = color[3] class pyInterpolateColorShader(StrokeShader): @@ -499,23 +401,32 @@ class pyInterpolateColorShader(StrokeShader): """ def __init__(self,r1,g1,b1,a1, r2,g2,b2,a2): StrokeShader.__init__(self) - self._c1 = [r1,g1,b1,a1] - self._c2 = [r2,g2,b2,a2] + # use 4d vector to simplify math + self._c1 = Vector((r1, g1 ,b1, a1)) + self._c2 = Vector((r2, g2, b2, a2)) def shade(self, stroke): - n = stroke.stroke_vertices_size() - 1 - inc = 0 - it = stroke.stroke_vertices_begin() - while not it.is_end: - att = it.object.attribute - u = float(inc) / float(n) - c = 1.0 - 2.0 * abs(u - 0.5) - att.color = ((1.0 - c) * self._c1[0] + c * self._c2[0], - (1.0 - c) * self._c1[1] + c * self._c2[1], - (1.0 - c) * self._c1[2] + c * self._c2[2]) - att.alpha = (1.0-c) * self._c1[3] + c * self._c2[3] - inc = inc+1 - it.increment() + n = len(stroke) - 1 + for i, svert in enumerate(stroke): + c = 1.0 - 2.0 * abs((i / n) - 0.5) + color = (1.0 - c) * self._c1 + c * self._c2 + svert.attribute.color = color[:3] + svert.attribute.alpha = color[3] + + +class pyModulateAlphaShader(StrokeShader): + """ + Limits the stroke's alpha between a min and max value. + """ + def __init__(self, min=0, max=1): + StrokeShader.__init__(self) + self.__min = min + self.__max = max + def shade(self, stroke): + for svert in stroke: + alpha = svert.attribute.alpha + alpha = bound(self.__min, alpha * svert.point.y * 0.0025, self.__max) + svert.attribute.alpha = alpha class pyMaterialColorShader(StrokeShader): @@ -525,61 +436,59 @@ class pyMaterialColorShader(StrokeShader): def __init__(self, threshold=50): StrokeShader.__init__(self) self._threshold = threshold + self._func = MaterialF0D() def shade(self, stroke): - it = stroke.stroke_vertices_begin() - func = MaterialF0D() xn = 0.312713 yn = 0.329016 Yn = 1.0 - un = 4.* xn / (-2.*xn + 12.*yn + 3.) - vn= 9.* yn / (-2.*xn + 12.*yn +3.) - while not it.is_end: - mat = func(Interface0DIterator(it)) + un = 4.0 * xn / (-2.0 * xn + 12.0 * yn + 3.0) + vn = 9.0 * yn / (-2.0 * xn + 12.0 * yn + 3.0) - r = mat.diffuse[0] - g = mat.diffuse[1] - b = mat.diffuse[2] + it = Interface0DIterator(stroke) + for svert in it: + mat = self._func(it) - X = 0.412453*r + 0.35758 *g + 0.180423*b - Y = 0.212671*r + 0.71516 *g + 0.072169*b - Z = 0.019334*r + 0.119193*g + 0.950227*b + r, g, b, *_ = mat.diffuse - if (X, Y, Z) == (0, 0, 0): - X = 0.01 - Y = 0.01 - Z = 0.01 - u = 4.*X / (X + 15.*Y + 3.*Z) - v = 9.*Y / (X + 15.*Y + 3.*Z) + X = 0.412453 * r + 0.35758 * g + 0.180423 * b + Y = 0.212671 * r + 0.71516 * g + 0.072169 * b + Z = 0.019334 * r + 0.11919 * g + 0.950227 * b - L= 116. * pow((Y/Yn),(1./3.)) -16 + if not any((X, Y, Z)): + X = Y = Z = 0.01 + + u = 4.0 * X / (X + 15.0 * Y + 3.0 * Z) + v = 9.0 * Y / (X + 15.0 * Y + 3.0 * Z) + + L= 116. * pow((Y/Yn),(1./3.)) - 16 U = 13. * L * (u - un) V = 13. * L * (v - vn) if L > self._threshold: - L = L/1.3 - U = U+10 + L /= 1.3 + U += 10. else: - L = L +2.5*(100-L)/5. - U = U/3.0 - V = V/3.0 - u = U / (13. * L) + un - v = V / (13. * L) + vn + L = L + 2.5 * (100-L) * 0.2 + U /= 3.0 + V /= 3.0 + + u = U / (13.0 * L) + un + v = V / (13.0 * L) + vn Y = Yn * pow(((L+16.)/116.), 3.) - X = -9.0 * Y * u / ((u - 4.0) * v - u * v) - Z = (9.0 * Y - 15.0 * v * Y - v * X) / (3.0 * v) + X = -9. * Y * u / ((u - 4.)* v - u * v) + Z = (9. * Y - 15*v*Y - v*X) /( 3. * v) r = 3.240479 * X - 1.53715 * Y - 0.498535 * Z g = -0.969256 * X + 1.875991 * Y + 0.041556 * Z b = 0.055648 * X - 0.204043 * Y + 1.057311 * Z - r = max(0,r) - g = max(0,g) - b = max(0,b) + r = max(0, r) + g = max(0, g) + b = max(0, b) - it.object.attribute.color = (r, g, b) - it.increment() + svert.attribute.color = (r, g, b) class pyRandomColorShader(StrokeShader): @@ -588,18 +497,14 @@ class pyRandomColorShader(StrokeShader): """ def __init__(self, s=1): StrokeShader.__init__(self) - random.seed(s) + random.seed = s def shade(self, stroke): - ## pick a random color - c0 = float(random.uniform(15,75))/100.0 - c1 = float(random.uniform(15,75))/100.0 - c2 = float(random.uniform(15,75))/100.0 - #print(c0, c1, c2) - it = stroke.stroke_vertices_begin() - while not it.is_end: - it.object.attribute.color = (c0,c1,c2) - it.increment() + c = (random.uniform(15, 75) * 0.01, + random.uniform(15, 75) * 0.01, + random.uniform(15, 75) * 0.01) + for svert in stroke: + svert.attribute.color = c class py2DCurvatureColorShader(StrokeShader): @@ -608,15 +513,14 @@ class py2DCurvatureColorShader(StrokeShader): A higher curvature will yield a brighter color """ def shade(self, stroke): - it = stroke.stroke_vertices_begin() func = Curvature2DAngleF0D() - while not it.is_end: - c = func(Interface0DIterator(it)) - if c < 0: - print("negative 2D curvature") - color = 10.0 * c/3.1415 - it.object.attribute.color = (color, color, color) - it.increment() + it = Interface0DIterator(stroke) + for svert in it: + c = func(it) + if c < 0 and bpy.app.debug_freestyle: + print("py2DCurvatureColorShader: negative 2D curvature") + color = 10.0 * c / pi + svert.attribute.color = (color, color, color) class pyTimeColorShader(StrokeShader): @@ -627,13 +531,13 @@ class pyTimeColorShader(StrokeShader): def __init__(self, step=0.01): StrokeShader.__init__(self) self._step = step - def shade(self, stroke): - for i, svert in enumerate(iter(stroke)): + for i, svert in enumerate(stroke): c = i * self._step - svert.attribute.color = (c,c,c) + svert.attribute.color = (c, c, c) -## geometry modifiers + +# -- Geometry Stroke Shaders -- # class pySamplingShader(StrokeShader): @@ -659,89 +563,55 @@ class pyBackboneStretcherShader(StrokeShader): self._l = l def shade(self, stroke): - it0 = stroke.stroke_vertices_begin() - it1 = StrokeVertexIterator(it0) - it1.increment() - itn = stroke.stroke_vertices_end() - itn.decrement() - itn_1 = StrokeVertexIterator(itn) - itn_1.decrement() - v0 = it0.object - v1 = it1.object - vn_1 = itn_1.object - vn = itn.object - p0 = v0.point_2d - pn = vn.point_2d - p1 = v1.point_2d - pn_1 = vn_1.point_2d - d1 = (p0 - p1).normalized() - dn = (pn - pn_1).normalized() - newFirst = p0+d1*float(self._l) - newLast = pn+dn*float(self._l) - v0.point = newFirst - vn.point = newLast + # get start and end points + v0, vn = stroke[0], stroke[-1] + p0, pn = v0.point, vn.point + # get the direction + d1 = (p0 - stroke[ 1].point).normalized() + dn = (pn - stroke[-2].point).normalized() + v0.point += d1 * self._l + vn.point += dn * self._l stroke.update_length() class pyLengthDependingBackboneStretcherShader(StrokeShader): """ Stretches the stroke's backbone proportional to the stroke's length + NOTE: you'll probably want an l somewhere between (0.5 - 0). A value that + is too high may yield unexpected results. """ def __init__(self, l): StrokeShader.__init__(self) self._l = l - def shade(self, stroke): - l = stroke.length_2d - stretch = self._l*l - it0 = stroke.stroke_vertices_begin() - it1 = StrokeVertexIterator(it0) - it1.increment() - itn = stroke.stroke_vertices_end() - itn.decrement() - itn_1 = StrokeVertexIterator(itn) - itn_1.decrement() - v0 = it0.object - v1 = it1.object - vn_1 = itn_1.object - vn = itn.object - p0 = v0.point_2d - pn = vn.point_2d - p1 = v1.point_2d - pn_1 = vn_1.point_2d - d1 = (p0 - p1).normalized() - dn = (pn - pn_1).normalized() - newFirst = p0+d1*float(stretch) - newLast = pn+dn*float(stretch) - v0.point = newFirst - vn.point = newLast + # get start and end points + v0, vn = stroke[0], stroke[-1] + p0, pn = v0.point, vn.point + # get the direction + d1 = (p0 - stroke[ 1].point).normalized() + dn = (pn - stroke[-2].point).normalized() + v0.point += d1 * self._l * stroke.length_2d + vn.point += dn * self._l * stroke.length_2d stroke.update_length() - class pyGuidingLineShader(StrokeShader): - """ - Replaces the stroke by its corresponding tangent - """ def shade(self, stroke): - it = stroke.stroke_vertices_begin() ## get the first vertex - itlast = stroke.stroke_vertices_end() ## - itlast.decrement() ## get the last one - t = itlast.object.point - it.object.point ## tangent direction - itmiddle = StrokeVertexIterator(it) ## - while itmiddle.object.u < 0.5: ## look for the stroke middle vertex - itmiddle.increment() ## + # get the tangent direction + t = stroke[-1].point - stroke[0].point + # look for the stroke middle vertex + itmiddle = iter(stroke) + while itmiddle.object.u < 0.5: + itmiddle.increment() + center_vertex = itmiddle.object + # position all the vertices along the tangent for the right part it = StrokeVertexIterator(itmiddle) - it.increment() - while not it.is_end: ## position all the vertices along the tangent for the right part - it.object.point = itmiddle.object.point+t*(it.object.u-itmiddle.object.u) - it.increment() - it = StrokeVertexIterator(itmiddle) - it.decrement() - while not it.is_begin: ## position all the vertices along the tangent for the left part - it.object.point = itmiddle.object.point-t*(itmiddle.object.u-it.object.u) - it.decrement() - it.object.point = itmiddle.object.point-t*itmiddle.object.u ## first vertex + for svert in it: + svert.point = center_vertex.point + t * (svert.u - center_vertex.u) + # position all the vertices along the tangent for the left part + it = StrokeVertexIterator(itmiddle).reversed() + for svert in it: + svert.point = center_vertex.point - t * (center_vertex.u - svert.u) stroke.update_length() @@ -754,25 +624,18 @@ class pyBackboneStretcherNoCuspShader(StrokeShader): self._l = l def shade(self, stroke): - it0 = stroke.stroke_vertices_begin() - it1 = StrokeVertexIterator(it0) - it1.increment() - itn = stroke.stroke_vertices_end() - itn.decrement() - itn_1 = StrokeVertexIterator(itn) - itn_1.decrement() - v0 = it0.object - v1 = it1.object - if (v0.nature & Nature.CUSP) == 0 and (v1.nature & Nature.CUSP) == 0: + + v0, v1 = stroke[0], stroke[1] + vn, vn_1 = stroke[-1], stroke[-2] + + if not (v0.nature & v1.nature & Nature.CUSP): d1 = (v0.point - v1.point).normalized() - newFirst = v0.point+d1*float(self._l) - v0.point = newFirst - vn_1 = itn_1.object - vn = itn.object - if (vn.nature & Nature.CUSP) == 0 and (vn_1.nature & Nature.CUSP) == 0: + v0.point += d1 * self._l + + if not (vn.nature & vn_1.nature & Nature.CUSP): dn = (vn.point - vn_1.point).normalized() - newLast = vn.point + dn * float(self._l) - vn.point = newLast + vn.point += dn * self._l + stroke.update_length() @@ -792,13 +655,9 @@ class pyDiffusion2Shader(StrokeShader): def shade(self, stroke): for i in range (1, self._nbIter): - it = stroke.stroke_vertices_begin() - while not it.is_end: - v = it.object - p1 = v.point - p2 = self._normalInfo(Interface0DIterator(it))*self._lambda*self._curvatureInfo(Interface0DIterator(it)) - v.point = p1+p2 - it.increment() + it = Interface0DIterator(stroke) + for svert in it: + svert.point += self._normalInfo(it) * self._lambda * self._curvatureInfo(it) stroke.update_length() @@ -810,33 +669,36 @@ class pyTipRemoverShader(StrokeShader): StrokeShader.__init__(self) self._l = l + @staticmethod + def check_vertex(v, length): + """ + Returns True if the given strokevertex is less than self._l away + from the stroke's tip and therefore should be removed. + """ + return (v.curvilinear_abscissa < length or v.stroke_length-v.curvilinear_abscissa < length) + def shade(self, stroke): - originalSize = stroke.stroke_vertices_size() - if originalSize < 4: + n = len(stroke) + if n < 4: return - verticesToRemove = [] - oldAttributes = [] - it = stroke.stroke_vertices_begin() - while not it.is_end: - v = it.object - if v.curvilinear_abscissa < self._l or v.stroke_length-v.curvilinear_abscissa < self._l: - verticesToRemove.append(v) - oldAttributes.append(StrokeAttribute(v.attribute)) - it.increment() - if originalSize-len(verticesToRemove) < 2: + + verticesToRemove = tuple(svert for svert in stroke if self.check_vertex(svert, self._l)) + # explicit conversion to StrokeAttribute is needed + oldAttributes = (StrokeAttribute(svert.attribute) for svert in stroke) + + if n - len(verticesToRemove) < 2: return + for sv in verticesToRemove: stroke.remove_vertex(sv) + stroke.update_length() - stroke.resample(originalSize) - if stroke.stroke_vertices_size() != originalSize: + stroke.resample(n) + if len(stroke) != n and bpy.app.debug_freestyle: print("pyTipRemover: Warning: resampling problem") - it = stroke.stroke_vertices_begin() - for a in oldAttributes: - if it.is_end: - break - it.object.attribute = a - it.increment() + + for svert, a in zip(stroke, oldAttributes): + svert.attribute = a stroke.update_length() @@ -845,145 +707,31 @@ class pyTVertexRemoverShader(StrokeShader): Removes t-vertices from the stroke """ def shade(self, stroke): - if stroke.stroke_vertices_size() <= 3: + if len(stroke) < 4: return - predTVertex = pyVertexNatureUP0D(Nature.T_VERTEX) - it = stroke.stroke_vertices_begin() - itlast = stroke.stroke_vertices_end() - itlast.decrement() - if predTVertex(it): - stroke.remove_vertex(it.object) - if predTVertex(itlast): - stroke.remove_vertex(itlast.object) + + v0, vn = stroke[0], stroke[-1] + if (v0.nature & Nature.T_VERTEX): + stroke.remove_vertex(v0) + if (vn.nature & Nature.T_VERTEX): + stroke.remove_vertex(vn) stroke.update_length() -#class pyExtremitiesOrientationShader(StrokeShader): -# def __init__(self, x1,y1,x2=0,y2=0): -# StrokeShader.__init__(self) -# self._v1 = Vector((x1,y1)) -# self._v2 = Vector((x2,y2)) -# def shade(self, stroke): -# #print(self._v1.x,self._v1.y) -# stroke.setBeginningOrientation(self._v1.x,self._v1.y) -# stroke.setEndingOrientation(self._v2.x,self._v2.y) - - class pyHLRShader(StrokeShader): """ Controlls visibility based upon the quantative invisibility (QI) based on hidden line removal (HLR) """ def shade(self, stroke): - originalSize = stroke.stroke_vertices_size() - if originalSize < 4: + if len(stroke) < 4: return - it = stroke.stroke_vertices_begin() - invisible = 0 - it2 = StrokeVertexIterator(it) - it2.increment() - fe = self.get_fedge(it.object, it2.object) - if fe.viewedge.qi != 0: - invisible = 1 - while not it2.is_end: - v = it.object - vnext = it2.object - if (v.nature & Nature.VIEW_VERTEX) != 0: - #if (v.nature & Nature.T_VERTEX) != 0: - fe = self.get_fedge(v, vnext) - qi = fe.viewedge.qi - if qi != 0: - invisible = 1 - else: - invisible = 0 - if invisible: - v.attribute.visible = False - it.increment() - it2.increment() - def get_fedge(self, it1, it2): - return it1.get_fedge(it2) - - -# broken and a mess -class pyTVertexOrientationShader(StrokeShader): - def __init__(self): - StrokeShader.__init__(self) - self._Get2dDirection = Orientation2DF1D() - ## finds the TVertex orientation from the TVertex and - ## the previous or next edge - - def findOrientation(self, tv, ve): - mateVE = tv.get_mate(ve) - if ve.qi != 0 or mateVE.qi != 0: - ait = AdjacencyIterator(tv,1,0) - winner = None - incoming = True - while not ait.is_end: - ave = ait.object - if ave.id != ve.id and ave.id != mateVE.id: - winner = ait.object - if not ait.isIncoming(): # FIXME - incoming = False - break - ait.increment() - if winner is not None: - if not incoming: - direction = self._Get2dDirection(winner.last_fedge) - else: - direction = self._Get2dDirection(winner.first_fedge) - return direction - return None - - def castToTVertex(self, cp): - if cp.t2d() == 0.0: - return cp.first_svertex.viewvertex - elif cp.t2d() == 1.0: - return cp.second_svertex.viewvertex - return None - - def shade(self, stroke): - it = stroke.stroke_vertices_begin() - it2 = StrokeVertexIterator(it) - it2.increment() - ## case where the first vertex is a TVertex - v = it.object - if (v.nature & Nature.T_VERTEX) != 0: - tv = self.castToTVertex(v) - if tv is not None: - ve = self.get_fedge(v, it2.object).viewedge - dir = self.findOrientation(tv, ve) - if dir is not None: - #print(dir.x, dir.y) - v.attribute.set_attribute_vec2("orientation", dir) - while not it2.is_end: - vprevious = it.object - v = it2.object - if (v.nature & Nature.T_VERTEX) != 0: - tv = self.castToTVertex(v) - if tv is not None: - ve = self.get_fedge(vprevious, v).viewedge - dir = self.findOrientation(tv, ve) - if dir is not None: - #print(dir.x, dir.y) - v.attribute.set_attribute_vec2("orientation", dir) - it.increment() - it2.increment() - ## case where the last vertex is a TVertex - v = it.object - if (v.nature & Nature.T_VERTEX) != 0: - itPrevious = StrokeVertexIterator(it) - itPrevious.decrement() - tv = self.castToTVertex(v) - if tv is not None: - ve = self.get_fedge(itPrevious.object, v).viewedge - dir = self.findOrientation(tv, ve) - if dir is not None: - #print(dir.x, dir.y) - v.attribute.set_attribute_vec2("orientation", dir) - - def get_fedge(self, it1, it2): - return it1.get_fedge(it2) + it = iter(stroke) + for v1, v2 in zip(it, it.incremented()): + if (v1.nature & Nature.VIEW_VERTEX): + visible = (v1.get_fedge(v2).viewedge.qi != 0) + v1.attribute.visible = not visible class pySinusDisplacementShader(StrokeShader): @@ -997,19 +745,12 @@ class pySinusDisplacementShader(StrokeShader): self._getNormal = Normal2DF0D() def shade(self, stroke): - it = stroke.stroke_vertices_begin() - while not it.is_end: - v = it.object - #print(self._getNormal.name) - n = self._getNormal(Interface0DIterator(it)) - p = v.point - u = v.u - a = self._a*(1-2*(abs(u-0.5))) - n = n*a*cos(self._f*u*6.28) - #print(n.x, n.y) - v.point = p+n - #v.point = v.point+n*a*cos(f*v.u) - it.increment() + it = Interface0DIterator(stroke) + for svert in it: + normal = self._getNormal(it) + a = self._a * (1 - 2 * (abs(svert.u - 0.5))) + n = normal * a * cos(self._f * svert.u * 6.28) + svert.point += n stroke.update_length() @@ -1027,13 +768,10 @@ class pyPerlinNoise1DShader(StrokeShader): self.__oct = oct def shade(self, stroke): - it = stroke.stroke_vertices_begin() - while not it.is_end: - v = it.object - i = v.projected_x + v.projected_y - nres = self.__noise.turbulence1(i, self.__freq, self.__amp, self.__oct) - v.point = (v.projected_x + nres, v.projected_y + nres) - it.increment() + for svert in stroke: + s = svert.projected_x + svert.projected_y + nres = self.__noise.turbulence1(s, self.__freq, self.__amp, self.__oct) + svert.point = (svert.projected_x + nres, svert.projected_y + nres) stroke.update_length() @@ -1053,12 +791,9 @@ class pyPerlinNoise2DShader(StrokeShader): self.__oct = oct def shade(self, stroke): - it = stroke.stroke_vertices_begin() - while not it.is_end: - v = it.object - nres = self.__noise.turbulence2(v.point_2d, self.__freq, self.__amp, self.__oct) - v.point = (v.projected_x + nres, v.projected_y + nres) - it.increment() + for svert in stroke: + nres = self.__noise.turbulence2(svert.point_2d, self.__freq, self.__amp, self.__oct) + svert.point = (svert.projected_x + nres, svert.projected_y + nres) stroke.update_length() @@ -1073,66 +808,52 @@ class pyBluePrintCirclesShader(StrokeShader): self.__random_radius = random_radius def shade(self, stroke): - it = stroke.stroke_vertices_begin() - if it.is_end: - return - p_min = it.object.point.copy() - p_max = it.object.point.copy() - while not it.is_end: - p = it.object.point - if p.x < p_min.x: - p_min.x = p.x - if p.x > p_max.x: - p_max.x = p.x - if p.y < p_min.y: - p_min.y = p.y - if p.y > p_max.y: - p_max.y = p.y - it.increment() + # get minimum and maximum coordinates + p_min, p_max = bounding_box(stroke) + stroke.resample(32 * self.__turns) - sv_nb = stroke.stroke_vertices_size() -# print("min :", p_min.x, p_min.y) # DEBUG -# print("mean :", p_sum.x, p_sum.y) # DEBUG -# print("max :", p_max.x, p_max.y) # DEBUG -# print("----------------------") # DEBUG -####################################################### - sv_nb = sv_nb // self.__turns + sv_nb = len(stroke) // self.__turns center = (p_min + p_max) / 2 radius = (center.x - p_min.x + center.y - p_min.y) / 2 - p_new = Vector((0.0, 0.0)) -####################################################### R = self.__random_radius C = self.__random_center - i = 0 - it = stroke.stroke_vertices_begin() + + # The directions (and phases) are calculated using a seperate + # function decorated with an lru-cache. This guarantees that + # the directions (involving sin and cos) are calculated as few + # times as possible. + # + # This works because the phases and directions are only + # dependant on the stroke length, and the chance that + # stroke.resample() above produces strokes of the same length + # is quite high. + # + # In tests, the amount of calls to sin() and cos() went from + # over 21000 to just 32 times, yielding a speedup of over 100% + directions = phase_to_direction(sv_nb) + + it = iter(stroke) + for j in range(self.__turns): prev_radius = radius prev_center = center - radius = radius + randint(-R, R) - center = center + Vector((randint(-C, C), randint(-C, C))) - while i < sv_nb and not it.is_end: - t = float(i) / float(sv_nb - 1) - r = prev_radius + (radius - prev_radius) * t - c = prev_center + (center - prev_center) * t - p_new.x = c.x + r * cos(2 * pi * t) - p_new.y = c.y + r * sin(2 * pi * t) - it.object.point = p_new - i = i + 1 - it.increment() - i = 1 - verticesToRemove = [] - while not it.is_end: - verticesToRemove.append(it.object) + radius += randint(-R, R) + center += Vector((randint(-C, C), randint(-C, C))) + + for (phase, direction), svert in zip(directions, it): + r = prev_radius + (radius - prev_radius) * phase + c = prev_center + (center - prev_center) * phase + svert.point = c + r * direction + + if not it.is_end: it.increment() - for sv in verticesToRemove: - stroke.remove_vertex(sv) + for sv in tuple(it): + stroke.remove_vertex(sv) + stroke.update_length() class pyBluePrintEllipsesShader(StrokeShader): - """ - Draws the silhouette of the object as an ellips - """ def __init__(self, turns=1, random_radius=3, random_center=5): StrokeShader.__init__(self) self.__turns = turns @@ -1140,311 +861,300 @@ class pyBluePrintEllipsesShader(StrokeShader): self.__random_radius = random_radius def shade(self, stroke): - it = stroke.stroke_vertices_begin() - if it.is_end: - return - p_min = it.object.point.copy() - p_max = it.object.point.copy() - while not it.is_end: - p = it.object.point - if p.x < p_min.x: - p_min.x = p.x - if p.x > p_max.x: - p_max.x = p.x - if p.y < p_min.y: - p_min.y = p.y - if p.y > p_max.y: - p_max.y = p.y - it.increment() + p_min, p_max = bounding_box(stroke) + stroke.resample(32 * self.__turns) - sv_nb = stroke.stroke_vertices_size() - sv_nb = sv_nb // self.__turns + sv_nb = len(stroke) // self.__turns + center = (p_min + p_max) / 2 radius = center - p_min - p_new = Vector((0.0, 0.0)) -####################################################### + R = self.__random_radius C = self.__random_center - i = 0 - it = stroke.stroke_vertices_begin() + + # for description of the line below, see pyBluePrintCirclesShader + directions = phase_to_direction(sv_nb) + it = iter(stroke) for j in range(self.__turns): prev_radius = radius prev_center = center radius = radius + Vector((randint(-R, R), randint(-R, R))) center = center + Vector((randint(-C, C), randint(-C, C))) - while i < sv_nb and not it.is_end: - t = float(i) / float(sv_nb - 1) - r = prev_radius + (radius - prev_radius) * t - c = prev_center + (center - prev_center) * t - p_new.x = c.x + r.x * cos(2 * pi * t) - p_new.y = c.y + r.y * sin(2 * pi * t) - it.object.point = p_new - i = i + 1 - it.increment() - i = 1 - verticesToRemove = [] - while not it.is_end: - verticesToRemove.append(it.object) + + for (phase, direction), svert in zip(directions, it): + r = prev_radius + (radius - prev_radius) * phase + c = prev_center + (center - prev_center) * phase + svert.point = (c.x + r.x * direction.x, c.y + r.y * direction.y) + + # remove exessive vertices + if not it.is_end: it.increment() - for sv in verticesToRemove: - stroke.remove_vertex(sv) + for sv in tuple(it): + stroke.remove_vertex(sv) + stroke.update_length() class pyBluePrintSquaresShader(StrokeShader): - """ - Draws the silhouette of the object as a square - """ def __init__(self, turns=1, bb_len=10, bb_rand=0): StrokeShader.__init__(self) - self.__turns = turns + self.__turns = turns # does not have any effect atm self.__bb_len = bb_len self.__bb_rand = bb_rand def shade(self, stroke): - it = stroke.stroke_vertices_begin() - if it.is_end: + # this condition will lead to errors later, end now + if len(stroke) < 1: return - p_min = it.object.point.copy() - p_max = it.object.point.copy() - while not it.is_end: - p = it.object.point - if p.x < p_min.x: - p_min.x = p.x - if p.x > p_max.x: - p_max.x = p.x - if p.y < p_min.y: - p_min.y = p.y - if p.y > p_max.y: - p_max.y = p.y - it.increment() + + # get minimum and maximum coordinates + p_min, p_max = bounding_box(stroke) + stroke.resample(32 * self.__turns) - sv_nb = stroke.stroke_vertices_size() -####################################################### - sv_nb = sv_nb // self.__turns - first = sv_nb // 4 - second = 2 * first - third = 3 * first - fourth = sv_nb - p_first = Vector((p_min.x - self.__bb_len, p_min.y)) - p_first_end = Vector((p_max.x + self.__bb_len, p_min.y)) - p_second = Vector((p_max.x, p_min.y - self.__bb_len)) - p_second_end = Vector((p_max.x, p_max.y + self.__bb_len)) - p_third = Vector((p_max.x + self.__bb_len, p_max.y)) - p_third_end = Vector((p_min.x - self.__bb_len, p_max.y)) - p_fourth = Vector((p_min.x, p_max.y + self.__bb_len)) - p_fourth_end = Vector((p_min.x, p_min.y - self.__bb_len)) -####################################################### - R = self.__bb_rand - r = self.__bb_rand // 2 - it = stroke.stroke_vertices_begin() - visible = True + num_segments = len(stroke) // self.__turns + f = num_segments // 4 + # indices of the vertices that will form corners + first, second, third, fourth = (f, f * 2, f * 3, num_segments) + + # construct points of the backbone + bb_len = self.__bb_len + points = ( + Vector((p_min.x - bb_len, p_min.y)), + Vector((p_max.x + bb_len, p_min.y)), + Vector((p_max.x, p_min.y - bb_len)), + Vector((p_max.x, p_max.y + bb_len)), + Vector((p_max.x + bb_len, p_max.y)), + Vector((p_min.x - bb_len, p_max.y)), + Vector((p_min.x, p_max.y + bb_len)), + Vector((p_min.x, p_min.y - bb_len)), + ) + + # add randomization to the points (if needed) + if self.__bb_rand: + R, r = self.__bb_rand, self.__bb_rand // 2 + + randomization_mat = ( + Vector((randint(-R, R), randint(-r, r))), + Vector((randint(-R, R), randint(-r, r))), + Vector((randint(-r, r), randint(-R, R))), + Vector((randint(-r, r), randint(-R, R))), + Vector((randint(-R, R), randint(-r, r))), + Vector((randint(-R, R), randint(-r, r))), + Vector((randint(-r, r), randint(-R, R))), + Vector((randint(-r, r), randint(-R, R))), + ) + + # combine both tuples + points = tuple(p + rand for (p, rand) in zip(points, randomization_mat)) + + + # substract even from uneven; result is length four tuple of vectors + it = iter(points) + old_vecs = tuple(next(it) - current for current in it) + + it = iter(stroke) + verticesToRemove = list() for j in range(self.__turns): - p_first = p_first + Vector((randint(-R, R), randint(-r, r))) - p_first_end = p_first_end + Vector((randint(-R, R), randint(-r, r))) - p_second = p_second + Vector((randint(-r, r), randint(-R, R))) - p_second_end = p_second_end + Vector((randint(-r, r), randint(-R, R))) - p_third = p_third + Vector((randint(-R, R), randint(-r, r))) - p_third_end = p_third_end + Vector((randint(-R, R), randint(-r, r))) - p_fourth = p_fourth + Vector((randint(-r, r), randint(-R, R))) - p_fourth_end = p_fourth_end + Vector((randint(-r, r), randint(-R, R))) - vec_first = p_first_end - p_first - vec_second = p_second_end - p_second - vec_third = p_third_end - p_third - vec_fourth = p_fourth_end - p_fourth - i = 0 - while i < sv_nb and not it.is_end: + for i, svert in zip(range(num_segments), it): if i < first: - p_new = p_first + vec_first * float(i)/float(first - 1) - if i == first - 1: - visible = False + svert.point = points[0] + old_vecs[0] * i / (first - 1) + svert.attribute.visible = (i != first - 1) elif i < second: - p_new = p_second + vec_second * float(i - first)/float(second - first - 1) - if i == second - 1: - visible = False + svert.point = points[2] + old_vecs[1] * (i - first) / (second - first - 1) + svert.attribute.visible = (i != second - 1) elif i < third: - p_new = p_third + vec_third * float(i - second)/float(third - second - 1) - if i == third - 1: - visible = False + svert.point = points[4] + old_vecs[2] * (i - second) / (third - second - 1) + svert.attribute.visible = (i != third - 1) + elif i < fourth: + svert.point = points[6] + old_vecs[3] * (i - third) / (fourth - third - 1) + svert.attribute.visible = (i != fourth - 1) else: - p_new = p_fourth + vec_fourth * float(i - third)/float(fourth - third - 1) - if i == fourth - 1: - visible = False - if it.object is None: - i = i + 1 - it.increment() - if not visible: - visible = True - continue - it.object.point = p_new - it.object.attribute.visible = visible - if not visible: - visible = True - i = i + 1 - it.increment() - verticesToRemove = [] - while not it.is_end: - verticesToRemove.append(it.object) + # special case; remove these vertices + verticesToRemove.append(svert) + + # remove exessive vertices (if any) + if not it.is_end: it.increment() - for sv in verticesToRemove: - stroke.remove_vertex(sv) + verticesToRemove += [svert for svert in it] + for sv in verticesToRemove: + stroke.remove_vertex(sv) stroke.update_length() -# needs a docstring class pyBluePrintDirectedSquaresShader(StrokeShader): + """ + Replaces the stroke with a directed square + """ def __init__(self, turns=1, bb_len=10, mult=1): StrokeShader.__init__(self) self.__mult = mult self.__turns = turns - self.__bb_len = 1 + float(bb_len) / 100 + self.__bb_len = 1 + bb_len * 0.01 def shade(self, stroke): stroke.resample(32 * self.__turns) - p_mean = Vector((0.0, 0.0)) - it = stroke.stroke_vertices_begin() - while not it.is_end: - p = it.object.point - p_mean = p_mean + p - it.increment() - sv_nb = stroke.stroke_vertices_size() - p_mean = p_mean / sv_nb - p_var_xx = 0 - p_var_yy = 0 - p_var_xy = 0 - it = stroke.stroke_vertices_begin() - while not it.is_end: - p = it.object.point - p_var_xx = p_var_xx + pow(p.x - p_mean.x, 2) - p_var_yy = p_var_yy + pow(p.y - p_mean.y, 2) - p_var_xy = p_var_xy + (p.x - p_mean.x) * (p.y - p_mean.y) - it.increment() - p_var_xx = p_var_xx / sv_nb - p_var_yy = p_var_yy / sv_nb - p_var_xy = p_var_xy / sv_nb -## print(p_var_xx, p_var_yy, p_var_xy) - trace = p_var_xx + p_var_yy - det = p_var_xx * p_var_yy - p_var_xy * p_var_xy + n = len(stroke) + + p_mean = (1 / n) * sum((svert.point for svert in stroke), Vector((0.0, 0.0))) + p_var = Vector((0, 0)) + p_var_xy = 0.0 + for d in (svert.point - p_mean for svert in stroke): + p_var += Vector((d.x ** 2, d.y ** 2)) + p_var_xy += d.x * d.y + + # divide by number of vertices + p_var /= n + p_var_xy /= n + trace = p_var.x + p_var.y + det = p_var.x * p_var.y - pow(p_var_xy, 2) + sqrt_coeff = sqrt(trace * trace - 4 * det) - lambda1 = (trace + sqrt_coeff) / 2 - lambda2 = (trace - sqrt_coeff) / 2 -## print(lambda1, lambda2) - theta = atan(2 * p_var_xy / (p_var_xx - p_var_yy)) / 2 -## print(theta) - if p_var_yy > p_var_xx: + lambda1, lambda2 = (trace + sqrt_coeff) / 2, (trace - sqrt_coeff) / 2 + # make sure those numers aren't to small, if they are, rooting them will yield complex numbers + lambda1, lambda2 = max(1e-12, lambda1), max(1e-12, lambda2) + theta = atan(2 * p_var_xy / (p_var.x - p_var.y)) / 2 + + if p_var.y > p_var.x: e1 = Vector((cos(theta + pi / 2), sin(theta + pi / 2))) * sqrt(lambda1) * self.__mult - e2 = Vector((cos(theta + pi), sin(theta + pi))) * sqrt(lambda2) * self.__mult + e2 = Vector((cos(theta + pi ), sin(theta + pi ))) * sqrt(lambda2) * self.__mult else: - e1 = Vector((cos(theta), sin(theta))) * sqrt(lambda1) * self.__mult + e1 = Vector((cos(theta), sin(theta))) * sqrt(lambda1) * self.__mult e2 = Vector((cos(theta + pi / 2), sin(theta + pi / 2))) * sqrt(lambda2) * self.__mult -####################################################### - sv_nb = sv_nb // self.__turns - first = sv_nb // 4 - second = 2 * first - third = 3 * first - fourth = sv_nb + + # partition the stroke + num_segments = len(stroke) // self.__turns + f = num_segments // 4 + # indices of the vertices that will form corners + first, second, third, fourth = (f, f * 2, f * 3, num_segments) + bb_len1 = self.__bb_len bb_len2 = 1 + (bb_len1 - 1) * sqrt(lambda1 / lambda2) - p_first = p_mean - e1 - e2 * bb_len2 - p_second = p_mean - e1 * bb_len1 + e2 - p_third = p_mean + e1 + e2 * bb_len2 - p_fourth = p_mean + e1 * bb_len1 - e2 - vec_first = e2 * bb_len2 * 2 - vec_second = e1 * bb_len1 * 2 - vec_third = vec_first * -1 - vec_fourth = vec_second * -1 -####################################################### - it = stroke.stroke_vertices_begin() - visible = True + points = ( + p_mean - e1 - e2 * bb_len2, + p_mean - e1 * bb_len1 + e2, + p_mean + e1 + e2 * bb_len2, + p_mean + e1 * bb_len1 - e2, + ) + + old_vecs = ( + e2 * bb_len2 * 2, + e1 * bb_len1 * 2, + -e2 * bb_len2 * 2, + -e1 * bb_len1 * 2, + ) + + it = iter(stroke) + verticesToRemove = list() for j in range(self.__turns): - i = 0 - while i < sv_nb: + for i, svert in zip(range(num_segments), it): if i < first: - p_new = p_first + vec_first * float(i)/float(first - 1) - if i == first - 1: - visible = False + svert.point = points[0] + old_vecs[0] * i / (first - 1) + svert.attribute.visible = (i != first - 1) elif i < second: - p_new = p_second + vec_second * float(i - first)/float(second - first - 1) - if i == second - 1: - visible = False + svert.point = points[1] + old_vecs[1] * (i - first) / (second - first - 1) + svert.attribute.visible = (i != second - 1) elif i < third: - p_new = p_third + vec_third * float(i - second)/float(third - second - 1) - if i == third - 1: - visible = False + svert.point = points[2] + old_vecs[2] * (i - second) / (third - second - 1) + svert.attribute.visible = (i != third - 1) + elif i < fourth: + svert.point = points[3] + old_vecs[3] * (i - third) / (fourth - third - 1) + svert.attribute.visible = (i != fourth - 1) else: - p_new = p_fourth + vec_fourth * float(i - third)/float(fourth - third - 1) - if i == fourth - 1: - visible = False - it.object.point = p_new - it.object.attribute.visible = visible - if not visible: - visible = True - i = i + 1 - it.increment() - verticesToRemove = [] - while not it.is_end: - verticesToRemove.append(it.object) + # special case; remove these vertices + verticesToRemove.append(svert) + + # remove exessive vertices + if not it.is_end: it.increment() - for sv in verticesToRemove: - stroke.remove_vertex(sv) + verticesToRemove += [svert for svert in it] + for sv in verticesToRemove: + stroke.remove_vertex(sv) stroke.update_length() -class pyModulateAlphaShader(StrokeShader): - """ - Limits the stroke's alpha between a min and max value - """ - def __init__(self, min=0, max=1): - StrokeShader.__init__(self) - self.__min = min - self.__max = max +# -- various (used in the parameter editor) -- # + + +class RoundCapShader(StrokeShader): + def round_cap_thickness(self, x): + x = max(0.0, min(x, 1.0)) + return pow(1.0 - (x ** 2.0), 0.5) def shade(self, stroke): - it = stroke.stroke_vertices_begin() - while not it.is_end: - alpha = it.object.attribute.alpha - p = it.object.point - alpha = alpha * p.y / 400 - if alpha < self.__min: - alpha = self.__min - elif alpha > self.__max: - alpha = self.__max - it.object.attribute.alpha = alpha - it.increment() + # save the location and attribute of stroke vertices + buffer = tuple((Vector(sv.point), StrokeAttribute(sv.attribute)) for sv in stroke) + nverts = len(buffer) + if nverts < 2: + return + # calculate the number of additional vertices to form caps + thickness_beg = sum(stroke[0].attribute.thickness) + caplen_beg = thickness_beg / 2.0 + nverts_beg = max(5, int(thickness_beg)) + + thickness_end = sum(stroke[-1].attribute.thickness) + caplen_end = (thickness_end) / 2.0 + nverts_end = max(5, int(thickness_end)) + + # adjust the total number of stroke vertices + stroke.resample(nverts + nverts_beg + nverts_end) + # restore the location and attribute of the original vertices + for i, (p, attr) in enumerate(buffer): + stroke[nverts_beg + i].point = p + stroke[nverts_beg + i].attribute = attr + # reshape the cap at the beginning of the stroke + q, attr = buffer[1] + p, attr = buffer[0] + direction = (p - q).normalized() * caplen_beg + n = 1.0 / nverts_beg + R, L = attr.thickness + for t, svert in zip(range(nverts_beg, 0, -1), stroke): + r = self.round_cap_thickness((t + 1) * n) + svert.point = p + direction * t * n + svert.attribute = attr + svert.attribute.thickness = (R * r, L * r) + # reshape the cap at the end of the stroke + q, attr = buffer[-2] + p, attr = buffer[-1] + direction = (p - q).normalized() * caplen_beg + n = 1.0 / nverts_end + R, L = attr.thickness + for t, svert in zip(range(nverts_end, 0, -1), reversed(stroke)): + r = self.round_cap_thickness((t + 1) * n) + svert.point = p + direction * t * n + svert.attribute = attr + svert.attribute.thickness = (R * r, L * r) + # update the curvilinear 2D length of each vertex + stroke.update_length() -## various -class pyDummyShader(StrokeShader): +class SquareCapShader(StrokeShader): def shade(self, stroke): - it = stroke.stroke_vertices_begin() - while not it.is_end: - toto = Interface0DIterator(it) - att = it.object.attribute - att.color = (0.3, 0.4, 0.4) - att.thickness = (0, 5) - it.increment() + # save the location and attribute of stroke vertices + buffer = tuple((Vector(sv.point), StrokeAttribute(sv.attribute)) for sv in stroke) + nverts = len(buffer) + if nverts < 2: + return + # calculate the number of additional vertices to form caps + caplen_beg = sum(stroke[0].attribute.thickness) / 2.0 + nverts_beg = 1 - -class pyDebugShader(StrokeShader): - def shade(self, stroke): - fe = CF.get_selected_fedge() - id1 = fe.first_svertex.id - id2 = fe.second_svertex.id - #print(id1.first, id1.second) - #print(id2.first, id2.second) - it = stroke.stroke_vertices_begin() - found = True - foundfirst = True - foundsecond = False - while not it.is_end: - cp = it.object - if cp.first_svertex.id == id1 or cp.second_svertex.id == id1: - foundfirst = True - if cp.first_svertex.id == id2 or cp.second_svertex.id == id2: - foundsecond = True - if foundfirst and foundsecond: - found = True - break - it.increment() - if found: - print("The selected Stroke id is: ", stroke.id.first, stroke.id.second) + caplen_end = sum(stroke[-1].attribute.thickness) / 2.0 + nverts_end = 1 + # adjust the total number of stroke vertices + stroke.resample(nverts + nverts_beg + nverts_end) + # restore the location and attribute of the original vertices + for i, (p, attr) in zip(range(nverts), buffer): + stroke[nverts_beg + i].point = p + stroke[nverts_beg + i].attribute = attr + # reshape the cap at the beginning of the stroke + q, attr = buffer[1] + p, attr = buffer[0] + stroke[0].point += (p - q).normalized() * caplen_beg + stroke[0].attribute = attr + # reshape the cap at the end of the stroke + q, attr = buffer[-2] + p, attr = buffer[-1] + stroke[-1].point += (p - q).normalized() * caplen_end + stroke[-1].attribute = attr + # update the curvilinear 2D length of each vertex + stroke.update_length() diff --git a/release/scripts/freestyle/modules/freestyle/utils.py b/release/scripts/freestyle/modules/freestyle/utils.py index 24ff86d5ef6..1b576791e9b 100644 --- a/release/scripts/freestyle/modules/freestyle/utils.py +++ b/release/scripts/freestyle/modules/freestyle/utils.py @@ -27,11 +27,275 @@ from _freestyle import ( integrate, ) -# constructs for definition of helper functions in Python -from freestyle.types import ( - StrokeVertexIterator, - ) -import mathutils +from mathutils import Vector +from functools import lru_cache +from math import cos, sin, pi + + +# -- real utility functions -- # + + +def rgb_to_bw(r, g, b): + """ Method to convert rgb to a bw intensity value. """ + return 0.35 * r + 0.45 * g + 0.2 * b + + +def bound(lower, x, higher): + """ Returns x bounded by a maximum and minimum value. equivalent to: + return min(max(x, lower), higher) + """ + # this is about 50% quicker than min(max(x, lower), higher) + return (lower if x <= lower else higher if x >= higher else x) + + +def bounding_box(stroke): + """ + Returns the maximum and minimum coordinates (the bounding box) of the stroke's vertices + """ + x, y = zip(*(svert.point for svert in stroke)) + return (Vector((min(x), min(y))), Vector((max(x), max(y)))) + + +# -- General helper functions -- # + + +@lru_cache(maxsize=32) +def phase_to_direction(length): + """ + Returns a list of tuples each containing: + - the phase + - a Vector with the values of the cosine and sine of 2pi * phase (the direction) + """ + results = list() + for i in range(length): + phase = i / (length - 1) + results.append((phase, Vector((cos(2 * pi * phase), sin(2 * pi * phase))))) + return results + + +# -- helper functions for chaining -- # + + +def get_chain_length(ve, orientation): + """Returns the 2d length of a given ViewEdge """ + from freestyle.chainingiterators import pyChainSilhouetteGenericIterator + length = 0.0 + # setup iterator + _it = pyChainSilhouetteGenericIterator(False, False) + _it.begin = ve + _it.current_edge = ve + _it.orientation = orientation + _it.init() + + # run iterator till end of chain + while not (_it.is_end): + length += _it.object.length_2d + if (_it.is_begin): + # _it has looped back to the beginning; + # break to prevent infinite loop + break + _it.increment() + + # reset iterator + _it.begin = ve + _it.current_edge = ve + _it.orientation = orientation + + # run iterator till begin of chain + if not _it.is_begin: + _it.decrement() + while not (_it.is_end or _it.is_begin): + length += _it.object.length_2d + _it.decrement() + + return length + + +def find_matching_vertex(id, it): + """Finds the matching vertexn, or returns None """ + return next((ve for ve in it if ve.id == id), None) + + +# -- helper functions for iterating -- # + + +def iter_current_previous(stroke): + """ + iterates over the given iterator. yields a tuple of the form + (it, prev, current) + """ + prev = stroke[0] + it = Interface0DIterator(stroke) + for current in it: + yield (it, prev, current) + + +def iter_t2d_along_stroke(stroke): + """ + Yields the distance between two stroke vertices + relative to the total stroke length. + """ + total = stroke.length_2d + distance = 0.0 + for it, prev, svert in iter_current_previous(stroke): + distance += (prev.point - svert.point).length + t = min(distance / total, 1.0) if total > 0.0 else 0.0 + yield (it, t) + + +def iter_distance_from_camera(stroke, range_min, range_max): + """ + Yields the distance to the camera relative to the maximum + possible distance for every stroke vertex, constrained by + given minimum and maximum values. + """ + normfac = range_max - range_min # normalization factor + it = Interface0DIterator(stroke) + for svert in it: + distance = svert.point_3d.length # in the camera coordinate + if distance < range_min: + t = 0.0 + elif distance > range_max: + t = 1.0 + else: + t = (distance - range_min) / normfac + yield (it, t) + + +def iter_distance_from_object(stroke, object, range_min, range_max): + """ + yields the distance to the given object relative to the maximum + possible distance for every stroke vertex, constrained by + given minimum and maximum values. + """ + scene = getCurrentScene() + mv = scene.camera.matrix_world.copy().inverted() # model-view matrix + loc = mv * object.location # loc in the camera coordinate + normfac = range_max - range_min # normalization factor + it = Interface0DIterator(stroke) + for svert in it: + distance = (svert.point_3d - loc).length # in the camera coordinate + if distance < range_min: + t = 0.0 + elif distance > range_max: + t = 1.0 + else: + t = (distance - range_min) / normfac + yield (it, t) + + +def iter_material_color(stroke, material_attribute): + """ + yields the specified material attribute for every stroke vertex. + the material is taken from the object behind the vertex. + """ + func = CurveMaterialF0D() + it = Interface0DIterator(stroke) + for inter in it: + material = func(it) + if material_attribute == 'DIFF': + color = material.diffuse[0:3] + elif material_attribute == 'SPEC': + color = material.specular[0:3] + else: + raise ValueError("unexpected material attribute: " + material_attribute) + yield (it, color) + + +def iter_material_value(stroke, material_attribute): + """ + yields a specific material attribute + from the vertex' underlying material. + """ + func = CurveMaterialF0D() + it = Interface0DIterator(stroke) + for svert in it: + material = func(it) + if material_attribute == 'DIFF': + t = rgb_to_bw(*material.diffuse[0:3]) + elif material_attribute == 'DIFF_R': + t = material.diffuse[0] + elif material_attribute == 'DIFF_G': + t = material.diffuse[1] + elif material_attribute == 'DIFF_B': + t = material.diffuse[2] + elif material_attribute == 'SPEC': + t = rgb_to_bw(*material.specular[0:3]) + elif material_attribute == 'SPEC_R': + t = material.specular[0] + elif material_attribute == 'SPEC_G': + t = material.specular[1] + elif material_attribute == 'SPEC_B': + t = material.specular[2] + elif material_attribute == 'SPEC_HARDNESS': + t = material.shininess + elif material_attribute == 'ALPHA': + t = material.diffuse[3] + else: + raise ValueError("unexpected material attribute: " + material_attribute) + yield (it, t) + + +def iter_distance_along_stroke(stroke): + """ + yields the absolute distance between + the current and preceding vertex. + """ + distance = 0.0 + prev = stroke[0] + it = Interface0DIterator(stroke) + for svert in it: + p = svert.point + distance += (prev - p).length + prev = p.copy() # need a copy because the point can be altered + yield it, distance + + +def iter_triplet(it): + """ + Iterates over it, yielding a tuple containing + the current vertex and its immediate neighbors + """ + prev = next(it) + current = next(it) + for succ in it: + yield prev, current, succ + prev, current = current, succ + + +# -- mathmatical operations -- # + + +def stroke_curvature(it): + """ + Compute the 2D curvature at the stroke vertex pointed by the iterator 'it'. + K = 1 / R + where R is the radius of the circle going through the current vertex and its neighbors + """ + + if it.is_end or it.is_begin: + return 0.0 + + next = it.incremented().point + prev = it.decremented().point + current = it.object.point + + + ab = (current - prev) + bc = (next - current) + ac = (prev - next) + + a, b, c = ab.length, bc.length, ac.length + + try: + area = 0.5 * ab.cross(ac) + K = (4 * area) / (a * b * c) + K = bound(0.0, K, 1.0) + + except ZeroDivisionError: + K = 0.0 + + return K def stroke_normal(it): @@ -42,28 +306,21 @@ def stroke_normal(it): they have already been modified by stroke geometry modifiers. """ # first stroke segment - it_next = StrokeVertexIterator(it) - it_next.increment() + it_next = it.incremented() if it.is_begin: e = it_next.object.point_2d - it.object.point_2d - n = mathutils.Vector((e[1], -e[0])) - n.normalize() - return n + n = Vector((e[1], -e[0])) + return n.normalized() # last stroke segment - it_prev = StrokeVertexIterator(it) - it_prev.decrement() + it_prev = it.decremented() if it_next.is_end: e = it.object.point_2d - it_prev.object.point_2d - n = mathutils.Vector((e[1], -e[0])) - n.normalize() - return n + n = Vector((e[1], -e[0])) + return n.normalized() # two subsequent stroke segments e1 = it_next.object.point_2d - it.object.point_2d e2 = it.object.point_2d - it_prev.object.point_2d - n1 = mathutils.Vector((e1[1], -e1[0])) - n2 = mathutils.Vector((e2[1], -e2[0])) - n1.normalize() - n2.normalize() - n = n1 + n2 - n.normalize() - return n + n1 = Vector((e1[1], -e1[0])).normalized() + n2 = Vector((e2[1], -e2[0])).normalized() + n = (n1 + n2) + return n.normalized()