This commit implements described in the #104573. The goal is to fix the confusion of the submodule hashes change, which are not ideal for any of the supported git-module configuration (they are either always visible causing confusion, or silently staged and committed, also causing confusion). This commit replaces submodules with a checkout of addons and addons_contrib, covered by the .gitignore, and locale and developer tools are moved to the main repository. This also changes the paths: - /release/scripts are moved to the /scripts - /source/tools are moved to the /tools - /release/datafiles/locale is moved to /locale This is done to avoid conflicts when using bisect, and also allow buildbot to automatically "recover" wgen building older or newer branches/patches. Running `make update` will initialize the local checkout to the changed repository configuration. Another aspect of the change is that the make update will support Github style of remote organization (origin remote pointing to thy fork, upstream remote pointing to the upstream blender/blender.git). Pull Request #104755
644 lines
21 KiB
Python
644 lines
21 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
"""
|
|
This module contains chaining iterators used for the chaining
|
|
operation to construct long strokes by concatenating feature edges
|
|
according to selected chaining rules. The module is also intended to
|
|
be a collection of examples for defining chaining iterators in Python.
|
|
"""
|
|
|
|
__all__ = (
|
|
"ChainPredicateIterator",
|
|
"ChainSilhouetteIterator",
|
|
"pyChainSilhouetteIterator",
|
|
"pyChainSilhouetteGenericIterator",
|
|
"pyExternalContourChainingIterator",
|
|
"pySketchyChainSilhouetteIterator",
|
|
"pySketchyChainingIterator",
|
|
"pyFillOcclusionsRelativeChainingIterator",
|
|
"pyFillOcclusionsAbsoluteChainingIterator",
|
|
"pyFillOcclusionsAbsoluteAndRelativeChainingIterator",
|
|
"pyFillQi0AbsoluteAndRelativeChainingIterator",
|
|
"pyNoIdChainSilhouetteIterator",
|
|
)
|
|
|
|
|
|
# module members
|
|
from _freestyle import (
|
|
ChainPredicateIterator,
|
|
ChainSilhouetteIterator,
|
|
)
|
|
|
|
# constructs for predicate definition in Python
|
|
from freestyle.types import (
|
|
AdjacencyIterator,
|
|
ChainingIterator,
|
|
Nature,
|
|
TVertex,
|
|
)
|
|
from freestyle.predicates import (
|
|
ExternalContourUP1D,
|
|
)
|
|
from freestyle.utils import (
|
|
ContextFunctions as CF,
|
|
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 that follows the edges of the same nature
|
|
following the topology of objects, with decreasing priority for
|
|
silhouettes, then borders, then suggestive contours, then all other edge
|
|
types. A ViewEdge is only chained once.
|
|
"""
|
|
|
|
def __init__(self, stayInSelection=True):
|
|
ChainingIterator.__init__(self, stayInSelection, True, None, True)
|
|
|
|
def init(self):
|
|
pass
|
|
|
|
def traverse(self, iter):
|
|
it = AdjacencyIterator(iter)
|
|
# 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):
|
|
"""
|
|
Natural chaining iterator that follows the edges of the same nature
|
|
following the topology of objects, with decreasing priority for
|
|
silhouettes, then borders, then suggestive contours, then all other
|
|
edge types.
|
|
|
|
.. method:: __init__(self, stayInSelection=True, stayInUnvisited=True)
|
|
|
|
Builds a pyChainSilhouetteGenericIterator object.
|
|
|
|
:arg stayInSelection: True if it is allowed to go out of the selection
|
|
:type stayInSelection: bool
|
|
:arg stayInUnvisited: May the same ViewEdge be chained twice
|
|
:type stayInUnvisited: bool
|
|
"""
|
|
|
|
def __init__(self, stayInSelection=True, stayInUnvisited=True):
|
|
ChainingIterator.__init__(self, stayInSelection, stayInUnvisited, None, True)
|
|
|
|
def init(self):
|
|
pass
|
|
|
|
def traverse(self, iter):
|
|
it = AdjacencyIterator(iter)
|
|
# 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):
|
|
"""Chains by external contour"""
|
|
|
|
def __init__(self):
|
|
ChainingIterator.__init__(self, False, True, None, True)
|
|
self.ExternalContour = ExternalContourUP1D()
|
|
|
|
def init(self):
|
|
self._nEdges = 0
|
|
|
|
def checkViewEdge(self, ve, orientation):
|
|
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 result
|
|
|
|
def traverse(self, iter):
|
|
winner = None
|
|
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
|
|
|
|
if winner is None:
|
|
it = AdjacencyIterator(iter)
|
|
for ve in it:
|
|
if self.checkViewEdge(ve, not it.is_incoming):
|
|
winner = ve
|
|
|
|
return winner
|
|
|
|
|
|
class pySketchyChainSilhouetteIterator(ChainingIterator):
|
|
"""
|
|
Natural chaining iterator with a sketchy multiple touch. It chains the
|
|
same ViewEdge multiple times to achieve a sketchy effect.
|
|
|
|
.. method:: __init__(self, nRounds=3,stayInSelection=True)
|
|
|
|
Builds a pySketchyChainSilhouetteIterator object.
|
|
|
|
:arg nRounds: Number of times every Viewedge is chained.
|
|
:type nRounds: int
|
|
:arg stayInSelection: if False, edges outside of the selection can be chained.
|
|
:type stayInSelection: bool
|
|
"""
|
|
|
|
def __init__(self, nRounds=3, stayInSelection=True):
|
|
ChainingIterator.__init__(self, stayInSelection, False, None, True)
|
|
self._timeStamp = CF.get_time_stamp() + nRounds
|
|
self._nRounds = nRounds
|
|
|
|
def init(self):
|
|
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):
|
|
it = AdjacencyIterator(iter)
|
|
# 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):
|
|
"""
|
|
Chaining iterator designed for sketchy style. It chains the same
|
|
ViewEdge several times in order to produce multiple strokes per
|
|
ViewEdge.
|
|
"""
|
|
|
|
def __init__(self, nRounds=3, stayInSelection=True):
|
|
ChainingIterator.__init__(self, stayInSelection, False, None, True)
|
|
self._timeStamp = CF.get_time_stamp() + nRounds
|
|
self._nRounds = nRounds
|
|
self.t = False
|
|
|
|
def init(self):
|
|
self._timeStamp = CF.get_time_stamp() + self._nRounds
|
|
|
|
def traverse(self, iter):
|
|
winner = None
|
|
found = False
|
|
|
|
for ve in AdjacencyIterator(iter):
|
|
if self.current_edge.id == ve.id:
|
|
found = True
|
|
continue
|
|
winner = ve
|
|
|
|
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:
|
|
return None
|
|
return winner
|
|
|
|
|
|
class pyFillOcclusionsRelativeChainingIterator(ChainingIterator):
|
|
"""
|
|
Chaining iterator that fills small occlusions
|
|
|
|
.. method:: __init__(self, percent)
|
|
|
|
Builds a pyFillOcclusionsRelativeChainingIterator object.
|
|
|
|
:arg percent: The maximal length of the occluded part, expressed
|
|
in a percentage of the total chain length.
|
|
:type percent: float
|
|
"""
|
|
|
|
def __init__(self, percent):
|
|
ChainingIterator.__init__(self, False, True, None, True)
|
|
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.0
|
|
|
|
def traverse(self, iter):
|
|
winner = None
|
|
winnerOrientation = False
|
|
it = AdjacencyIterator(iter)
|
|
# 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:
|
|
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
|
|
break
|
|
|
|
# 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
|
|
|
|
return winner
|
|
|
|
|
|
class pyFillOcclusionsAbsoluteChainingIterator(ChainingIterator):
|
|
"""
|
|
Chaining iterator that fills small occlusions
|
|
|
|
.. method:: __init__(self, length)
|
|
|
|
Builds a pyFillOcclusionsAbsoluteChainingIterator object.
|
|
|
|
:arg length: The maximum length of the occluded part in pixels.
|
|
:type length: int
|
|
"""
|
|
|
|
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
|
|
|
|
def traverse(self, iter):
|
|
winner = None
|
|
winnerOrientation = False
|
|
it = AdjacencyIterator(iter)
|
|
# 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:
|
|
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
|
|
break
|
|
|
|
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
|
|
|
|
|
|
class pyFillOcclusionsAbsoluteAndRelativeChainingIterator(ChainingIterator):
|
|
"""
|
|
Chaining iterator that fills small occlusions regardless of the
|
|
selection.
|
|
|
|
.. method:: __init__(self, percent, l)
|
|
|
|
Builds a pyFillOcclusionsAbsoluteAndRelativeChainingIterator object.
|
|
|
|
:arg percent: The maximal length of the occluded part as a
|
|
percentage of the total chain length.
|
|
:type percent: float
|
|
:arg l: Absolute length.
|
|
:type l: float
|
|
"""
|
|
|
|
def __init__(self, percent, l):
|
|
ChainingIterator.__init__(self, False, True, None, True)
|
|
self._length = 0.0
|
|
self._absLength = l
|
|
self._percent = float(percent)
|
|
|
|
def init(self):
|
|
# each time we're evaluating a chain length
|
|
# we try to do it once. Thus we reinit
|
|
# the chain length here:
|
|
self._length = 0.0
|
|
|
|
def traverse(self, iter):
|
|
winner = None
|
|
winnerOrientation = False
|
|
it = AdjacencyIterator(iter)
|
|
# 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:
|
|
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
|
|
break
|
|
|
|
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 (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):
|
|
return None
|
|
return winner
|
|
|
|
|
|
class pyFillQi0AbsoluteAndRelativeChainingIterator(ChainingIterator):
|
|
"""
|
|
Chaining iterator that fills small occlusions regardless of the
|
|
selection.
|
|
|
|
.. method:: __init__(self, percent, l)
|
|
|
|
Builds a pyFillQi0AbsoluteAndRelativeChainingIterator object.
|
|
|
|
:arg percent: The maximal length of the occluded part as a
|
|
percentage of the total chain length.
|
|
:type percent: float
|
|
:arg l: Absolute length.
|
|
:type l: float
|
|
"""
|
|
|
|
def __init__(self, percent, l):
|
|
ChainingIterator.__init__(self, False, True, None, True)
|
|
self._length = 0.0
|
|
self._absLength = l
|
|
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.0
|
|
|
|
def traverse(self, iter):
|
|
winner = None
|
|
winnerOrientation = False
|
|
it = AdjacencyIterator(iter)
|
|
# 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:
|
|
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
|
|
break
|
|
|
|
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:
|
|
connexl += _cit.object.length_2d
|
|
_cit.increment()
|
|
if _cit.is_begin:
|
|
break
|
|
if (connexl > self._percent * self._length) or (connexl > self._absLength):
|
|
return None
|
|
return winner
|
|
|
|
|
|
class pyNoIdChainSilhouetteIterator(ChainingIterator):
|
|
"""
|
|
Natural chaining iterator that follows the edges of the same nature
|
|
following the topology of objects, with decreasing priority for
|
|
silhouettes, then borders, then suggestive contours, then all other edge
|
|
types. It won't chain the same ViewEdge twice.
|
|
|
|
.. method:: __init__(self, stayInSelection=True)
|
|
|
|
Builds a pyNoIdChainSilhouetteIterator object.
|
|
|
|
:arg stayInSelection: True if it is allowed to go out of the selection
|
|
:type stayInSelection: bool
|
|
"""
|
|
|
|
def __init__(self, stayInSelection=True):
|
|
ChainingIterator.__init__(self, stayInSelection, True, None, True)
|
|
|
|
def init(self):
|
|
pass
|
|
|
|
def traverse(self, iter):
|
|
winner = None
|
|
it = AdjacencyIterator(iter)
|
|
# 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:
|
|
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:
|
|
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:
|
|
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:
|
|
return ve
|
|
return None
|
|
# case of NonTVertex
|
|
else:
|
|
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
|