Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
This module provides data types of view map components (0D and 1D
elements), base classes for defining line stylization rules
(predicates, functions, chaining iterators, and stroke shaders),
as well as helper functions for style module writing.
"""
# module members
from . import chainingiterators, functions, predicates, shaders, types, utils

View File

@@ -0,0 +1,737 @@
# SPDX-FileCopyrightText: 2014-2023 Blender Authors
#
# 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):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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__(stayInSelection=True, stayInUnvisited=True)
Builds a pyChainSilhouetteGenericIterator object.
:param stayInSelection: True if it is allowed to go out of the selection
:type stayInSelection: bool
:param 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):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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):
"""
Tests whether a ViewEdge belongs to the external contour.
:param ve: The ViewEdge to test.
:type ve: :class:`ViewEdge`
:param orientation: Iteration orientation.
:type orientation: bool
:rtype: bool
"""
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):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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__(nRounds=3,stayInSelection=True)
Builds a pySketchyChainSilhouetteIterator object.
:param nRounds: Number of times every Viewedge is chained.
:type nRounds: int
:param 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 sketchy effect by causing the chain to run from
the start again. (loop over itself again)
:param ve: The candidate ViewEdge, or None to fall back to the current edge.
:type ve: :class:`ViewEdge` | None
:rtype: :class:`ViewEdge` | None
"""
if ve is None:
ve = self.current_edge
if ve.chaining_time_stamp == self._timeStamp:
return None
return ve
def traverse(self, iter):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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__(percent)
Builds a pyFillOcclusionsRelativeChainingIterator object.
:param 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):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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__(length)
Builds a pyFillOcclusionsAbsoluteChainingIterator object.
:param 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):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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__(percent, l)
Builds a pyFillOcclusionsAbsoluteAndRelativeChainingIterator object.
:param percent: The maximal length of the occluded part as a
percentage of the total chain length.
:type percent: float
:param 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 reinitialize the chain length here:
self._length = 0.0
def traverse(self, iter):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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__(percent, l)
Builds a pyFillQi0AbsoluteAndRelativeChainingIterator object.
:param percent: The maximal length of the occluded part as a
percentage of the total chain length.
:type percent: float
:param 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 preferably be evaluated only once.
# Therefore, the chain length is reset here.
self._length = 0.0
def traverse(self, iter):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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__(stayInSelection=True)
Builds a pyNoIdChainSilhouetteIterator object.
:param 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):
"""
Returns the next ViewEdge to chain.
:param iter: An adjacency iterator over the candidate ViewEdges.
:type iter: :class:`AdjacencyIterator`
:return: The next ViewEdge, or None to stop chaining.
:rtype: :class:`ViewEdge` | None
"""
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

View File

@@ -0,0 +1,313 @@
# SPDX-FileCopyrightText: 2014-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
This module contains functions operating on vertices (0D elements) and
polylines (1D elements). The module is also intended to be a
collection of examples for function definition in Python.
User-defined functions inherit one of the following base classes,
depending on the object type (0D or 1D) to operate on and the return
value type:
- :class:`freestyle.types.UnaryFunction0DDouble`
- :class:`freestyle.types.UnaryFunction0DEdgeNature`
- :class:`freestyle.types.UnaryFunction0DFloat`
- :class:`freestyle.types.UnaryFunction0DId`
- :class:`freestyle.types.UnaryFunction0DMaterial`
- :class:`freestyle.types.UnaryFunction0DUnsigned`
- :class:`freestyle.types.UnaryFunction0DVec2f`
- :class:`freestyle.types.UnaryFunction0DVec3f`
- :class:`freestyle.types.UnaryFunction0DVectorViewShape`
- :class:`freestyle.types.UnaryFunction0DViewShape`
- :class:`freestyle.types.UnaryFunction1DDouble`
- :class:`freestyle.types.UnaryFunction1DEdgeNature`
- :class:`freestyle.types.UnaryFunction1DFloat`
- :class:`freestyle.types.UnaryFunction1DUnsigned`
- :class:`freestyle.types.UnaryFunction1DVec2f`
- :class:`freestyle.types.UnaryFunction1DVec3f`
- :class:`freestyle.types.UnaryFunction1DVectorViewShape`
- :class:`freestyle.types.UnaryFunction1DVoid`
"""
__all__ = (
"ChainingTimeStampF1D",
"Curvature2DAngleF0D",
"Curvature2DAngleF1D",
"CurveMaterialF0D",
"CurveNatureF0D",
"CurveNatureF1D",
"DensityF0D",
"DensityF1D",
"GetCompleteViewMapDensityF1D",
"GetCurvilinearAbscissaF0D",
"GetDirectionalViewMapDensityF1D",
"GetOccludeeF0D",
"GetOccludeeF1D",
"GetOccludersF0D",
"GetOccludersF1D",
"GetParameterF0D",
"GetProjectedXF0D",
"GetProjectedXF1D",
"GetProjectedYF0D",
"GetProjectedYF1D",
"GetProjectedZF0D",
"GetProjectedZF1D",
"GetShapeF0D",
"GetShapeF1D",
"GetSteerableViewMapDensityF1D",
"GetViewMapGradientNormF0D",
"GetViewMapGradientNormF1D",
"GetXF0D",
"GetXF1D",
"GetYF0D",
"GetYF1D",
"GetZF0D",
"GetZF1D",
"IncrementChainingTimeStampF1D",
"LocalAverageDepthF0D",
"LocalAverageDepthF1D",
"MaterialF0D",
"Normal2DF0D",
"Normal2DF1D",
"Orientation2DF1D",
"Orientation3DF1D",
"QuantitativeInvisibilityF0D",
"QuantitativeInvisibilityF1D",
"ReadCompleteViewMapPixelF0D",
"ReadMapPixelF0D",
"ReadSteerableViewMapPixelF0D",
"ShapeIdF0D",
"TimeStampF1D",
"VertexOrientation2DF0D",
"VertexOrientation3DF0D",
"ZDiscontinuityF0D",
"ZDiscontinuityF1D",
"pyCurvilinearLengthF0D",
"pyDensityAnisotropyF0D",
"pyDensityAnisotropyF1D",
"pyGetInverseProjectedZF1D",
"pyGetSquareInverseProjectedZF1D",
"pyInverseCurvature2DAngleF0D",
"pyViewMapGradientNormF0D",
"pyViewMapGradientNormF1D",
"pyViewMapGradientVectorF0D",
)
# module members
from _freestyle import (
ChainingTimeStampF1D,
Curvature2DAngleF0D,
Curvature2DAngleF1D,
CurveNatureF0D,
CurveNatureF1D,
DensityF0D,
DensityF1D,
GetCompleteViewMapDensityF1D,
GetCurvilinearAbscissaF0D,
GetDirectionalViewMapDensityF1D,
GetOccludeeF0D,
GetOccludeeF1D,
GetOccludersF0D,
GetOccludersF1D,
GetParameterF0D,
GetProjectedXF0D,
GetProjectedXF1D,
GetProjectedYF0D,
GetProjectedYF1D,
GetProjectedZF0D,
GetProjectedZF1D,
GetShapeF0D,
GetShapeF1D,
GetSteerableViewMapDensityF1D,
GetViewMapGradientNormF0D,
GetViewMapGradientNormF1D,
GetXF0D,
GetXF1D,
GetYF0D,
GetYF1D,
GetZF0D,
GetZF1D,
IncrementChainingTimeStampF1D,
LocalAverageDepthF0D,
LocalAverageDepthF1D,
MaterialF0D,
Normal2DF0D,
Normal2DF1D,
Orientation2DF1D,
Orientation3DF1D,
QuantitativeInvisibilityF0D,
QuantitativeInvisibilityF1D,
ReadCompleteViewMapPixelF0D,
ReadMapPixelF0D,
ReadSteerableViewMapPixelF0D,
ShapeIdF0D,
TimeStampF1D,
VertexOrientation2DF0D,
VertexOrientation3DF0D,
ZDiscontinuityF0D,
ZDiscontinuityF1D,
)
# constructs for function definition in Python
from freestyle.types import (
CurvePoint,
IntegrationType,
UnaryFunction0DDouble,
UnaryFunction0DMaterial,
UnaryFunction0DVec2f,
UnaryFunction1DDouble,
)
from freestyle.utils import ContextFunctions as CF
from freestyle.utils import integrate
from mathutils import Vector
# -- Functions for 0D elements (vertices) -- #
class CurveMaterialF0D(UnaryFunction0DMaterial):
"""
A replacement of the built-in MaterialF0D for stroke creation.
MaterialF0D does not work with Curves and Strokes. Line color
priority is used to pick one of the two materials at material
boundaries.
Notes: expects instances of CurvePoint to be iterated over
can return None if no fedge can be found
"""
def __call__(self, inter):
fe = inter.object.fedge
if fe is None:
return None
if fe.is_smooth:
return fe.material
else:
right, left = fe.material_right, fe.material_left
return right if (right.priority > left.priority) else left
class pyInverseCurvature2DAngleF0D(UnaryFunction0DDouble):
def __call__(self, inter):
func = Curvature2DAngleF0D()
c = func(inter)
return (3.1415 - c)
class pyCurvilinearLengthF0D(UnaryFunction0DDouble):
def __call__(self, inter):
cp = inter.object
assert isinstance(cp, CurvePoint)
return cp.t2d
class pyDensityAnisotropyF0D(UnaryFunction0DDouble):
"""Estimates the anisotropy of density."""
def __init__(self, level):
UnaryFunction0DDouble.__init__(self)
self.IsoDensity = ReadCompleteViewMapPixelF0D(level)
self.d0Density = ReadSteerableViewMapPixelF0D(0, level)
self.d1Density = ReadSteerableViewMapPixelF0D(1, level)
self.d2Density = ReadSteerableViewMapPixelF0D(2, level)
self.d3Density = ReadSteerableViewMapPixelF0D(3, level)
def __call__(self, inter):
c_iso = self.IsoDensity(inter)
c_0 = self.d0Density(inter)
c_1 = self.d1Density(inter)
c_2 = self.d2Density(inter)
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))
return 0 if (c_iso == 0) else (cMax - cMin) / c_iso
class pyViewMapGradientVectorF0D(UnaryFunction0DVec2f):
"""
Returns the gradient vector for a pixel.
.. method:: __init__(level)
Builds a pyViewMapGradientVectorF0D object.
:param level: the level at which to compute the gradient
:type level: int
"""
def __init__(self, level):
UnaryFunction0DVec2f.__init__(self)
self._l = level
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))
return Vector((gx, gy))
class pyViewMapGradientNormF0D(UnaryFunction0DDouble):
def __init__(self, l):
UnaryFunction0DDouble.__init__(self)
self._l = 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))
return Vector((gx, gy)).length
# -- Functions for 1D elements (curves) -- #
class pyGetInverseProjectedZF1D(UnaryFunction1DDouble):
def __call__(self, inter):
func = GetProjectedZF1D()
z = func(inter)
return (1.0 - z)
class pyGetSquareInverseProjectedZF1D(UnaryFunction1DDouble):
def __call__(self, inter):
func = GetProjectedZF1D()
z = func(inter)
return (1.0 - pow(z, 2))
class pyDensityAnisotropyF1D(UnaryFunction1DDouble):
def __init__(self, level, integrationType=IntegrationType.MEAN, sampling=2.0):
UnaryFunction1DDouble.__init__(self, integrationType)
self._func = pyDensityAnisotropyF0D(level)
self._integration = integrationType
self._sampling = sampling
def __call__(self, inter):
v = integrate(
self._func, inter.points_begin(
self._sampling), inter.points_end(
self._sampling), self._integration)
return v
class pyViewMapGradientNormF1D(UnaryFunction1DDouble):
def __init__(self, l, integrationType, sampling=2.0):
UnaryFunction1DDouble.__init__(self, integrationType)
self._func = pyViewMapGradientNormF0D(l)
self._integration = integrationType
self._sampling = sampling
def __call__(self, inter):
v = integrate(
self._func, inter.points_begin(
self._sampling), inter.points_end(
self._sampling), self._integration)
return v

View File

@@ -0,0 +1,670 @@
# SPDX-FileCopyrightText: 2014-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
This module contains predicates operating on vertices (0D elements)
and polylines (1D elements). It is also intended to be a collection
of examples for predicate definition in Python.
User-defined predicates inherit one of the following base classes,
depending on the object type (0D or 1D) to operate on and the arity
(unary or binary):
- :class:`freestyle.types.BinaryPredicate0D`
- :class:`freestyle.types.BinaryPredicate1D`
- :class:`freestyle.types.UnaryPredicate0D`
- :class:`freestyle.types.UnaryPredicate1D`
"""
__all__ = (
"AndBP1D",
"AndUP1D",
"ContourUP1D",
"DensityLowerThanUP1D",
"EqualToChainingTimeStampUP1D",
"EqualToTimeStampUP1D",
"ExternalContourUP1D",
"FalseBP1D",
"FalseUP0D",
"FalseUP1D",
"Length2DBP1D",
"MaterialBP1D",
"NotBP1D",
"NotUP1D",
"ObjectNamesUP1D",
"OrBP1D",
"OrUP1D",
"QuantitativeInvisibilityRangeUP1D",
"QuantitativeInvisibilityUP1D",
"SameShapeIdBP1D",
"ShapeUP1D",
"TrueBP1D",
"TrueUP0D",
"TrueUP1D",
"ViewMapGradientNormBP1D",
"WithinImageBoundaryUP1D",
"pyBackTVertexUP0D",
"pyClosedCurveUP1D",
"pyDensityFunctorUP1D",
"pyDensityUP1D",
"pyDensityVariableSigmaUP1D",
"pyHighDensityAnisotropyUP1D",
"pyHighDirectionalViewMapDensityUP1D",
"pyHighSteerableViewMapDensityUP1D",
"pyHighViewMapDensityUP1D",
"pyHighViewMapGradientNormUP1D",
"pyHigherCurvature2DAngleUP0D",
"pyHigherLengthUP1D",
"pyHigherNumberOfTurnsUP1D",
"pyIsInOccludersListUP1D",
"pyIsOccludedByIdListUP1D",
"pyIsOccludedByItselfUP1D",
"pyIsOccludedByUP1D",
"pyLengthBP1D",
"pyLowDirectionalViewMapDensityUP1D",
"pyLowSteerableViewMapDensityUP1D",
"pyNFirstUP1D",
"pyNatureBP1D",
"pyNatureUP1D",
"pyParameterUP0D",
"pyParameterUP0DGoodOne",
"pyProjectedXBP1D",
"pyProjectedYBP1D",
"pyShapeIdListUP1D",
"pyShapeIdUP1D",
"pyShuffleBP1D",
"pySilhouetteFirstBP1D",
"pyUEqualsUP0D",
"pyVertexNatureUP0D",
"pyViewMapGradientNormBP1D",
"pyZBP1D",
"pyZDiscontinuityBP1D",
"pyZSmallerUP1D",
)
# module members
from _freestyle import (
ContourUP1D,
DensityLowerThanUP1D,
EqualToChainingTimeStampUP1D,
EqualToTimeStampUP1D,
ExternalContourUP1D,
FalseBP1D,
FalseUP0D,
FalseUP1D,
Length2DBP1D,
QuantitativeInvisibilityUP1D,
SameShapeIdBP1D,
ShapeUP1D,
TrueBP1D,
TrueUP0D,
TrueUP1D,
ViewMapGradientNormBP1D,
WithinImageBoundaryUP1D,
)
# constructs for predicate definition in Python
from freestyle.types import (
BinaryPredicate1D,
Id,
IntegrationType,
Interface0DIterator,
Nature,
TVertex,
UnaryPredicate0D,
UnaryPredicate1D,
)
from freestyle.functions import (
Curvature2DAngleF0D,
CurveNatureF1D,
DensityF1D,
GetCompleteViewMapDensityF1D,
GetCurvilinearAbscissaF0D,
GetDirectionalViewMapDensityF1D,
GetOccludersF1D,
GetProjectedXF1D,
GetProjectedYF1D,
GetProjectedZF1D,
GetShapeF1D,
GetSteerableViewMapDensityF1D,
GetZF1D,
QuantitativeInvisibilityF0D,
ZDiscontinuityF1D,
pyCurvilinearLengthF0D,
pyDensityAnisotropyF1D,
pyViewMapGradientNormF1D,
)
from freestyle.utils import material_from_fedge
import random
# -- Unary predicates for 0D elements (vertices) -- #
class pyHigherCurvature2DAngleUP0D(UnaryPredicate0D):
def __init__(self, a):
UnaryPredicate0D.__init__(self)
self._a = a
self.func = Curvature2DAngleF0D()
def __call__(self, inter):
return (self.func(inter) > self._a)
class pyUEqualsUP0D(UnaryPredicate0D):
def __init__(self, u, w):
UnaryPredicate0D.__init__(self)
self._u = u
self._w = w
self._func = pyCurvilinearLengthF0D()
def __call__(self, inter):
u = self._func(inter)
return (u > (self._u - self._w)) and (u < (self._u + self._w))
class pyVertexNatureUP0D(UnaryPredicate0D):
def __init__(self, nature):
UnaryPredicate0D.__init__(self)
self._nature = nature
def __call__(self, inter):
return bool(inter.object.nature & self._nature)
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 not (iter.object.nature & Nature.T_VERTEX) or iter.is_end:
return False
return self._getQI(iter) != 0
class pyParameterUP0DGoodOne(UnaryPredicate0D):
def __init__(self, pmin, pmax):
UnaryPredicate0D.__init__(self)
self._m = pmin
self._M = pmax
def __call__(self, inter):
u = inter.u
return ((u >= self._m) and (u <= self._M))
class pyParameterUP0D(UnaryPredicate0D):
def __init__(self, pmin, pmax):
UnaryPredicate0D.__init__(self)
self._m = pmin
self._M = pmax
self._func = Curvature2DAngleF0D()
def __call__(self, inter):
c = self._func(inter)
b1 = (c > 0.1)
u = inter.u
b = ((u >= self._m) and (u <= self._M))
return (b and b1)
# -- Unary predicates for 1D elements (curves) -- #
class AndUP1D(UnaryPredicate1D):
def __init__(self, *predicates):
UnaryPredicate1D.__init__(self)
self.predicates = predicates
correct_types = all(isinstance(p, UnaryPredicate1D) for p in self.predicates)
if not (correct_types and predicates):
raise TypeError("%s: Expected one or more UnaryPredicate1D, got %r" %
(self.__class__.__name__, self.predicates))
def __call__(self, inter):
return all(pred(inter) for pred in self.predicates)
class OrUP1D(UnaryPredicate1D):
def __init__(self, *predicates):
UnaryPredicate1D.__init__(self)
self.predicates = predicates
correct_types = all(isinstance(p, UnaryPredicate1D) for p in self.predicates)
if not (correct_types and predicates):
raise TypeError("%s: Expected one or more UnaryPredicate1D, got %r" %
(self.__class__.__name__, self.predicates))
def __call__(self, inter):
return any(pred(inter) for pred in self.predicates)
class NotUP1D(UnaryPredicate1D):
def __init__(self, pred):
UnaryPredicate1D.__init__(self)
self.predicate = pred
def __call__(self, inter):
return not self.predicate(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)
self.__n = n
self.__count = 0
def __call__(self, inter):
self.__count += 1
return (self.__count <= self.__n)
class pyHigherLengthUP1D(UnaryPredicate1D):
def __init__(self, l):
UnaryPredicate1D.__init__(self)
self._l = l
def __call__(self, inter):
return (inter.length_2d > self._l)
class pyNatureUP1D(UnaryPredicate1D):
def __init__(self, nature):
UnaryPredicate1D.__init__(self)
self._nature = nature
self._getNature = CurveNatureF1D()
def __call__(self, inter):
return bool(self._getNature(inter) & self._nature)
class pyHigherNumberOfTurnsUP1D(UnaryPredicate1D):
def __init__(self, n, a):
UnaryPredicate1D.__init__(self)
self._n = n
self._a = a
self.func = Curvature2DAngleF0D()
def __call__(self, inter):
it = Interface0DIterator(inter)
# sum the turns, check against n
return sum(1 for _ in it if self.func(it) > self._a) > self._n
# interesting fact, the line above is 70% faster than:
# return sum(self.func(it) > self._a for _ in it) > self._n
class pyDensityUP1D(UnaryPredicate1D):
def __init__(self, wsize, threshold, integration=IntegrationType.MEAN, sampling=2.0):
UnaryPredicate1D.__init__(self)
self._wsize = wsize
self._threshold = threshold
self._integration = integration
self._func = DensityF1D(self._wsize, self._integration, sampling)
def __call__(self, inter):
return (self._func(inter) < self._threshold)
class pyLowSteerableViewMapDensityUP1D(UnaryPredicate1D):
def __init__(self, threshold, level, integration=IntegrationType.MEAN):
UnaryPredicate1D.__init__(self)
self._threshold = threshold
self._level = level
self._integration = integration
def __call__(self, inter):
func = GetSteerableViewMapDensityF1D(self._level, self._integration)
return (func(inter) < self._threshold)
class pyLowDirectionalViewMapDensityUP1D(UnaryPredicate1D):
def __init__(self, threshold, orientation, level, integration=IntegrationType.MEAN):
UnaryPredicate1D.__init__(self)
self._threshold = threshold
self._orientation = orientation
self._level = level
self._integration = integration
def __call__(self, inter):
func = GetDirectionalViewMapDensityF1D(self._orientation, self._level, self._integration)
return (func(inter) < self._threshold)
class pyHighSteerableViewMapDensityUP1D(UnaryPredicate1D):
def __init__(self, threshold, level, integration=IntegrationType.MEAN):
UnaryPredicate1D.__init__(self)
self._threshold = threshold
self._func = GetSteerableViewMapDensityF1D(level, integration)
def __call__(self, inter):
return (self._func(inter) > self._threshold)
class pyHighDirectionalViewMapDensityUP1D(UnaryPredicate1D):
def __init__(self, threshold, orientation, level, integration=IntegrationType.MEAN, sampling=2.0):
UnaryPredicate1D.__init__(self)
self._threshold = threshold
self._func = GetDirectionalViewMapDensityF1D(orientation, level, integration, sampling)
def __call__(self, inter):
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._func = GetCompleteViewMapDensityF1D(level, integration, sampling)
def __call__(self, inter):
return (self._func(inter) > self._threshold)
class pyDensityFunctorUP1D(UnaryPredicate1D):
def __init__(self, wsize, threshold, functor, funcmin=0.0, funcmax=1.0, integration=IntegrationType.MEAN):
UnaryPredicate1D.__init__(self)
self._threshold = float(threshold)
self._functor = functor
self._funcmin = float(funcmin)
self._funcmax = float(funcmax)
self._func = DensityF1D(wsize, integration)
def __call__(self, inter):
res = self._functor(inter)
k = (res - self._funcmin) / (self._funcmax - self._funcmin)
return (func(inter) < (self._threshold * k))
class pyZSmallerUP1D(UnaryPredicate1D):
def __init__(self, z, integration=IntegrationType.MEAN):
UnaryPredicate1D.__init__(self)
self._z = z
self.func = GetProjectedZF1D(integration)
def __call__(self, inter):
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):
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()
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()
if any(ve.id == self._id for (ve, incoming) in eit):
return True
return False
class pyIsInOccludersListUP1D(UnaryPredicate1D):
def __init__(self, id):
UnaryPredicate1D.__init__(self)
self._id = id
def __call__(self, inter):
occluders = GetOccludersF1D()(inter)
return any(a.id == self._id for a in occluders)
class pyIsOccludedByItselfUP1D(UnaryPredicate1D):
def __init__(self):
UnaryPredicate1D.__init__(self)
self.__func1 = GetOccludersF1D()
self.__func2 = GetShapeF1D()
def __call__(self, inter):
lst1 = self.__func1(inter)
lst2 = self.__func2(inter)
return any(vs1.id == vs2.id for vs1 in lst1 for vs2 in lst2)
class pyIsOccludedByIdListUP1D(UnaryPredicate1D):
def __init__(self, idlist):
UnaryPredicate1D.__init__(self)
self._idlist = idlist
self.__func1 = GetOccludersF1D()
def __call__(self, inter):
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._funcs = tuple(ShapeUP1D(_id, 0) for _id in idlist)
def __call__(self, inter):
return any(func(inter) for func in self._funcs)
# DEPRECATED
class pyShapeIdUP1D(UnaryPredicate1D):
def __init__(self, _id):
UnaryPredicate1D.__init__(self)
self._id = _id
def __call__(self, inter):
shapes = GetShapeF1D()(inter)
return any(a.id == self._id for a in shapes)
class pyHighDensityAnisotropyUP1D(UnaryPredicate1D):
def __init__(self, threshold, level, sampling=2.0):
UnaryPredicate1D.__init__(self)
self._l = threshold
self.func = pyDensityAnisotropyF1D(level, IntegrationType.MEAN, sampling)
def __call__(self, inter):
return (self.func(inter) > self._l)
class pyHighViewMapGradientNormUP1D(UnaryPredicate1D):
def __init__(self, threshold, l, sampling=2.0):
UnaryPredicate1D.__init__(self)
self._threshold = threshold
self._GetGradient = pyViewMapGradientNormF1D(l, IntegrationType.MEAN)
def __call__(self, inter):
gn = self._GetGradient(inter)
return (gn > self._threshold)
class pyDensityVariableSigmaUP1D(UnaryPredicate1D):
def __init__(self, functor, sigmaMin, sigmaMax, lmin, lmax, tmin,
tmax, integration=IntegrationType.MEAN, sampling=2.0):
UnaryPredicate1D.__init__(self)
self._functor = functor
self._sigmaMin = float(sigmaMin)
self._sigmaMax = float(sigmaMax)
self._lmin = float(lmin)
self._lmax = float(lmax)
self._tmin = tmin
self._tmax = tmax
self._integration = integration
self._sampling = sampling
def __call__(self, inter):
result = self._functor(inter) - self._lmin
sigma = (self._sigmaMax - self._sigmaMin) / (self._lmax - self._lmin) * result + self._sigmaMin
t = (self._tmax - self._tmin) / (self._lmax - self._lmin) * result + self._tmin
sigma = max(sigma, self._sigmaMin)
self._func = DensityF1D(sigma, self._integration, self._sampling)
return (self._func(inter) < t)
class pyClosedCurveUP1D(UnaryPredicate1D):
def __call__(self, inter):
it = inter.vertices_begin()
itlast = inter.vertices_end()
itlast.decrement()
return (next(it).id == next(itlast).id)
# -- Binary predicates for 1D elements (curves) -- #
class AndBP1D(BinaryPredicate1D):
def __init__(self, *predicates):
BinaryPredicate1D.__init__(self)
self.predicates = tuple(predicates)
correct_types = all(isinstance(p, BinaryPredicate1D) for p in self.predicates)
if not (correct_types and predicates):
raise TypeError("%s: Expected one or more BinaryPredicate1D, got %r" %
(self.__class__.__name__, self.predicates))
def __call__(self, i1, i2):
return all(pred(i1, i2) for pred in self.predicates)
class OrBP1D(BinaryPredicate1D):
def __init__(self, *predicates):
BinaryPredicate1D.__init__(self)
self.predicates = tuple(predicates)
correct_types = all(isinstance(p, BinaryPredicate1D) for p in self.predicates)
if not (correct_types and predicates):
raise TypeError("%s: Expected one or more BinaryPredicate1D, got %r" %
(self.__class__.__name__, self.predicates))
def __call__(self, i1, i2):
return any(pred(i1, i2) for pred in self.predicates)
class NotBP1D(BinaryPredicate1D):
def __init__(self, predicate):
BinaryPredicate1D.__init__(self)
self.predicate = predicate
def __call__(self, i1, i2):
return (not self.predicate(i1, i2))
class pyZBP1D(BinaryPredicate1D):
def __init__(self, iType=IntegrationType.MEAN):
BinaryPredicate1D.__init__(self)
self.func = GetZF1D(iType)
def __call__(self, i1, i2):
return (self.func(i1) > self.func(i2))
class pyProjectedXBP1D(BinaryPredicate1D):
def __init__(self, iType=IntegrationType.MEAN):
BinaryPredicate1D.__init__(self)
self.func = GetProjectedXF1D(iType)
def __call__(self, i1, i2):
return (self.func(i1) > self.func(i2))
class pyProjectedYBP1D(BinaryPredicate1D):
def __init__(self, iType=IntegrationType.MEAN):
BinaryPredicate1D.__init__(self)
self.func = GetProjectedYF1D(iType)
def __call__(self, i1, i2):
return (self.func(i1) > self.func(i2))
class pyZDiscontinuityBP1D(BinaryPredicate1D):
def __init__(self, iType=IntegrationType.MEAN):
BinaryPredicate1D.__init__(self)
self._GetZDiscontinuity = ZDiscontinuityF1D(iType)
def __call__(self, i1, i2):
return (self._GetZDiscontinuity(i1) > self._GetZDiscontinuity(i2))
class pyLengthBP1D(BinaryPredicate1D):
def __call__(self, i1, i2):
return (i1.length_2d > i2.length_2d)
class pySilhouetteFirstBP1D(BinaryPredicate1D):
def __call__(self, inter1, inter2):
bpred = SameShapeIdBP1D()
if (not bpred(inter1, inter2)):
return False
if (inter1.nature & Nature.SILHOUETTE):
return bool(inter2.nature & Nature.SILHOUETTE)
return (inter1.nature == inter2.nature)
class pyNatureBP1D(BinaryPredicate1D):
def __call__(self, inter1, inter2):
return (inter1.nature & inter2.nature)
class pyViewMapGradientNormBP1D(BinaryPredicate1D):
def __init__(self, l, sampling=2.0):
BinaryPredicate1D.__init__(self)
self._GetGradient = pyViewMapGradientNormF1D(l, IntegrationType.MEAN)
def __call__(self, i1, i2):
return (self._GetGradient(i1) > self._GetGradient(i2))
class pyShuffleBP1D(BinaryPredicate1D):
def __init__(self):
BinaryPredicate1D.__init__(self)
random.seed = 1
def __call__(self, inter1, inter2):
return (random.uniform(0, 1) < random.uniform(0, 1))
class MaterialBP1D(BinaryPredicate1D):
"""Checks whether the two supplied ViewEdges have the same material."""
def __call__(self, i1, i2):
fedges = (fe for ve in (i1, i2) for fe in (ve.first_fedge, ve.last_fedge))
materials = {material_from_fedge(fe) for fe in fedges}
return len(materials) < 2

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,159 @@
# SPDX-FileCopyrightText: 2014-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
This module contains core classes of the Freestyle Python API,
including data types of view map components (0D and 1D elements), base
classes for user-defined line stylization rules (predicates,
functions, chaining iterators, and stroke shaders), and operators.
Class hierarchy:
- :class:`BBox`
- :class:`BinaryPredicate0D`
- :class:`BinaryPredicate1D`
- :class:`Id`
- :class:`Interface0D`
- :class:`CurvePoint`
- :class:`StrokeVertex`
- :class:`SVertex`
- :class:`ViewVertex`
- :class:`NonTVertex`
- :class:`TVertex`
- :class:`Interface1D`
- :class:`Curve`
- :class:`Chain`
- :class:`FEdge`
- :class:`FEdgeSharp`
- :class:`FEdgeSmooth`
- :class:`Stroke`
- :class:`ViewEdge`
- :class:`Iterator`
- :class:`AdjacencyIterator`
- :class:`CurvePointIterator`
- :class:`Interface0DIterator`
- :class:`SVertexIterator`
- :class:`StrokeVertexIterator`
- :class:`ViewEdgeIterator`
- :class:`ChainingIterator`
- :class:`orientedViewEdgeIterator`
- :class:`Material`
- :class:`Noise`
- :class:`Operators`
- :class:`SShape`
- :class:`StrokeAttribute`
- :class:`StrokeShader`
- :class:`UnaryFunction0D`
- :class:`UnaryFunction0DDouble`
- :class:`UnaryFunction0DEdgeNature`
- :class:`UnaryFunction0DFloat`
- :class:`UnaryFunction0DId`
- :class:`UnaryFunction0DMaterial`
- :class:`UnaryFunction0DUnsigned`
- :class:`UnaryFunction0DVec2f`
- :class:`UnaryFunction0DVec3f`
- :class:`UnaryFunction0DVectorViewShape`
- :class:`UnaryFunction0DViewShape`
- :class:`UnaryFunction1D`
- :class:`UnaryFunction1DDouble`
- :class:`UnaryFunction1DEdgeNature`
- :class:`UnaryFunction1DFloat`
- :class:`UnaryFunction1DUnsigned`
- :class:`UnaryFunction1DVec2f`
- :class:`UnaryFunction1DVec3f`
- :class:`UnaryFunction1DVectorViewShape`
- :class:`UnaryFunction1DVoid`
- :class:`UnaryPredicate0D`
- :class:`UnaryPredicate1D`
- :class:`ViewMap`
- :class:`ViewShape`
- :class:`IntegrationType`
- :class:`MediumType`
- :class:`Nature`
"""
# module members
from _freestyle import (
AdjacencyIterator,
BBox,
BinaryPredicate0D,
BinaryPredicate1D,
Chain,
ChainingIterator,
Curve,
CurvePoint,
CurvePointIterator,
FEdge,
FEdgeSharp,
FEdgeSmooth,
Id,
IntegrationType,
Interface0D,
Interface0DIterator,
Interface1D,
Iterator,
Material,
MediumType,
Nature,
Noise,
NonTVertex,
Operators,
SShape,
SVertex,
SVertexIterator,
Stroke,
StrokeAttribute,
StrokeShader,
StrokeVertex,
StrokeVertexIterator,
TVertex,
UnaryFunction0D,
UnaryFunction0DDouble,
UnaryFunction0DEdgeNature,
UnaryFunction0DFloat,
UnaryFunction0DId,
UnaryFunction0DMaterial,
UnaryFunction0DUnsigned,
UnaryFunction0DVec2f,
UnaryFunction0DVec3f,
UnaryFunction0DVectorViewShape,
UnaryFunction0DViewShape,
UnaryFunction1D,
UnaryFunction1DDouble,
UnaryFunction1DEdgeNature,
UnaryFunction1DFloat,
UnaryFunction1DUnsigned,
UnaryFunction1DVec2f,
UnaryFunction1DVec3f,
UnaryFunction1DVectorViewShape,
UnaryFunction1DVoid,
UnaryPredicate0D,
UnaryPredicate1D,
ViewEdge,
ViewEdgeIterator,
ViewMap,
ViewShape,
ViewVertex,
orientedViewEdgeIterator,
)

View File

@@ -0,0 +1,682 @@
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
This module contains helper functions used for Freestyle style module
writing.
"""
__all__ = (
"angle_x_normal",
"bound",
"bounding_box",
"BoundingBox",
"ContextFunctions",
"curvature_from_stroke_vertex",
"find_matching_vertex",
"get_chain_length",
"get_object_name",
"get_strokes",
"get_test_stroke",
"getCurrentScene",
"integrate",
"is_poly_clockwise",
"iter_distance_along_stroke",
"iter_distance_from_camera",
"iter_distance_from_object",
"iter_material_value",
"iter_t2d_along_stroke",
"material_from_fedge",
"normal_at_I0D",
"pairwise",
"phase_to_direction",
"rgb_to_bw",
"simplify",
"stroke_curvature",
"stroke_normal",
"StrokeCollector",
"tripplewise",
)
# module members
from _freestyle import (
ContextFunctions,
getCurrentScene,
integrate,
)
# constructs for helper functions in Python
from freestyle.types import (
Interface0DIterator,
Stroke,
StrokeShader,
StrokeVertexIterator,
Operators,
)
from mathutils import Vector
from functools import lru_cache, namedtuple
from math import cos, sin, pi, atan2
from itertools import tee, compress
# -- types -- #
# A named tuple primitive used for storing data that has an upper and
# lower bound (e.g., thickness, range and certain other values)
class BoundedProperty(namedtuple("BoundedProperty", ["min", "max", "delta"])):
def __new__(cls, minimum, maximum, delta=None):
if delta is None:
delta = abs(maximum - minimum)
return super().__new__(cls, minimum, maximum, delta)
def interpolate(self, val):
result = (self.max - val) / self.delta
return 1.0 - bound(0, result, 1)
# -- real utility functions -- #
def rgb_to_bw(r, g, b):
"""Method to convert rgb to a bw intensity value.
:param r: Red channel (0..1).
:type r: float
:param g: Green channel (0..1).
:type g: float
:param b: Blue channel (0..1).
:type b: float
:rtype: float
"""
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)
:param lower: Lower bound.
:type lower: float
:param x: Value to clamp.
:type x: float
:param higher: Upper bound.
:type higher: float
:rtype: float
"""
# this is about 50% quicker than min(max(x, lower), higher)
return (lower if x <= lower else higher if x >= higher else x)
def get_strokes():
"""Get all strokes that are currently available"""
return tuple(map(Operators.get_stroke_from_index, range(Operators.get_strokes_size())))
def is_poly_clockwise(stroke):
"""True if the stroke is orientated in a clockwise way, False otherwise
:param stroke: A stroke whose orientation is tested.
:type stroke: :class:`Stroke`
:rtype: bool
"""
v = sum((v2.point.x - v1.point.x) * (v1.point.y + v2.point.y) for v1, v2 in pairwise(stroke))
v1, v2 = stroke[0], stroke[-1]
if (v1.point - v2.point).length > 1e-3:
v += (v2.point.x - v1.point.x) * (v1.point.y + v2.point.y)
return v > 0
def get_object_name(stroke):
"""Returns the name of the object that this stroke is drawn on.
:param stroke: A stroke.
:type stroke: :class:`Stroke`
:rtype: str | None
"""
fedge = stroke[0].fedge
if fedge is None:
return None
return fedge.viewedge.viewshape.name
def material_from_fedge(fe):
"""Get the diffuse RGBA color from an FEdge.
:param fe: An FEdge.
:type fe: :class:`FEdge`
:rtype: :class:`Material` | None
"""
if fe is None:
return None
if fe.is_smooth:
material = fe.material
else:
right, left = fe.material_right, fe.material_left
material = right if (right.priority > left.priority) else left
return material
def bounding_box(stroke):
"""
Returns the maximum and minimum coordinates (the bounding box) of the stroke's vertices
:param stroke: A stroke.
:type stroke: :class:`Stroke`
:rtype: tuple[:class:`mathutils.Vector`, :class:`mathutils.Vector`]
"""
x, y = zip(*(svert.point for svert in stroke))
return (Vector((min(x), min(y))), Vector((max(x), max(y))))
def normal_at_I0D(it: Interface0DIterator) -> Vector:
"""Normal at an Interface0D object. In contrast to Normal2DF0D this
function uses the actual data instead of underlying Fedge objects.
:param it: An iterator over Interface0D objects.
:type it: :class:`Interface0DIterator`
:rtype: :class:`mathutils.Vector`
"""
if it.at_last and it.is_begin:
# corner-case
return Vector((0, 0))
elif it.at_last:
it.decrement()
a, b = it.object, next(it)
elif it.is_begin:
a, b = it.object, next(it)
# give iterator back in original state
it.decrement()
elif it.is_end:
# Just fail hard: this should not happen.
raise StopIteration()
else:
# this case sometimes has a small difference with Normal2DF0D (1e-3 -ish)
it.decrement()
a = it.object
_curr, b = next(it), next(it)
# give iterator back in original state
it.decrement()
return (b.point - a.point).orthogonal().normalized()
def angle_x_normal(it: Interface0DIterator):
"""unsigned angle between a Point's normal and the X axis, in radians
:param it: An iterator over Interface0D objects.
:type it: :class:`Interface0DIterator`
:rtype: float
"""
normal = normal_at_I0D(it)
return abs(atan2(normal[1], normal[0]))
def curvature_from_stroke_vertex(svert):
"""The 3D curvature of an stroke vertex' underlying geometry
The result is None or in the range [-inf, inf]
:param svert: A stroke vertex.
:type svert: :class:`StrokeVertex`
:rtype: float | None
"""
c1 = svert.first_svertex.curvatures
c2 = svert.second_svertex.curvatures
if c1 is None and c2 is None:
Kr = None
elif c1 is None:
Kr = c2[4]
elif c2 is None:
Kr = c1[4]
else:
Kr = c1[4] + svert.t2d * (c2[4] - c1[4])
return Kr
# -- 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
# Simplification of a set of points; based on `simplify.js`:
# See: https://mourner.github.io/simplify-js
def getSquareSegmentDistance(p, p1, p2):
"""
Square distance between point and a segment
"""
x, y = p1
dx, dy = (p2 - p1)
if dx or dy:
t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy)
if t > 1:
x, y = p2
elif t > 0:
x += dx * t
y += dy * t
dx, dy = p.x - x, p.y - y
return dx * dx + dy * dy
def simplifyDouglasPeucker(points, tolerance):
length = len(points)
markers = [0] * length
first = 0
last = length - 1
first_stack = []
last_stack = []
markers[first] = 1
markers[last] = 1
while last:
max_sqdist = 0
for i in range(first, last):
sqdist = getSquareSegmentDistance(points[i], points[first], points[last])
if sqdist > max_sqdist:
index = i
max_sqdist = sqdist
if max_sqdist > tolerance:
markers[index] = 1
first_stack.append(first)
last_stack.append(index)
first_stack.append(index)
last_stack.append(last)
first = first_stack.pop() if first_stack else None
last = last_stack.pop() if last_stack else None
return tuple(compress(points, markers))
def simplify(points, tolerance):
"""Simplifies a set of points.
:param points: Points to simplify.
:type points: Sequence[:class:`mathutils.Vector`]
:param tolerance: Maximum allowed deviation from the original curve.
:type tolerance: float
:rtype: tuple
"""
return simplifyDouglasPeucker(points, tolerance * tolerance)
class BoundingBox:
"""Object representing a bounding box consisting out of 2 2D vectors"""
__slots__ = (
"minimum",
"maximum",
"size",
"corners",
)
def __init__(self, minimum: Vector, maximum: Vector):
self.minimum = minimum
self.maximum = maximum
if len(minimum) != len(maximum):
raise TypeError("Expected two vectors of size 2, got", minimum, maximum)
self.size = len(minimum)
self.corners = (minimum, maximum)
def __repr__(self):
return "BoundingBox({!r}, {!r})".format(self.minimum, self.maximum)
@classmethod
def from_sequence(cls, sequence):
"""BoundingBox from sequence of 2D or 3D Vector objects.
:param sequence: An iterable of vectors to compute the box from.
:type sequence: Iterable[:class:`mathutils.Vector`]
:rtype: :class:`BoundingBox`
"""
x, y = zip(*sequence)
mini = Vector((min(x), min(y)))
maxi = Vector((max(x), max(y)))
return cls(mini, maxi)
def inside(self, other):
"""True if self inside other, False otherwise.
:param other: Another bounding box to test containment against.
:type other: :class:`BoundingBox`
:rtype: bool
"""
if self.size != other.size:
raise TypeError("Expected two BoundingBox of the same size, got", self, other)
return (self.minimum.x >= other.minimum.x and self.minimum.y >= other.minimum.y and
self.maximum.x <= other.maximum.x and self.maximum.y <= other.maximum.y)
class StrokeCollector(StrokeShader):
"""Collects and Stores stroke objects"""
def __init__(self):
StrokeShader.__init__(self)
self.strokes = []
def shade(self, stroke):
"""
:param stroke: The stroke to collect.
:type stroke: :class:`Stroke`
"""
self.strokes.append(stroke)
# -- helper functions for chaining -- #
def get_chain_length(ve, orientation):
"""Returns the 2d length of a given ViewEdge.
:param ve: The ViewEdge whose chain length to compute.
:type ve: :class:`ViewEdge`
:param orientation: Direction in which to traverse the chain.
:type orientation: bool
:rtype: float
"""
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 vertex, or returns None.
:param id: The ID to match.
:type id: :class:`Id`
:param it: An iterator over candidate ViewEdges.
:type it: :class:`AdjacencyIterator`
:rtype: :class:`ViewEdge` | None
"""
return next((ve for ve in it if ve.id == id), None)
# -- helper functions for iterating -- #
def pairwise(iterable, types=None):
"""Yields a tuple containing the previous and current object.
:param iterable: An iterable of items.
:type iterable: Iterable[Any]
:param types: Container types for which the iterator's ``incremented()``
method is used instead of standard tee-based pairing. When ``None``
defaults to ``(Stroke, StrokeVertexIterator)``.
:type types: tuple[type, ...] | None
"""
# use .incremented() for types that support it
if types is None:
types = (Stroke, StrokeVertexIterator)
if type(iterable) in types:
it = iter(iterable)
return zip(it, it.incremented())
else:
a, b = tee(iterable)
next(b, None)
return zip(a, b)
def tripplewise(iterable):
"""Yields a tuple containing the current object and its immediate neighbors.
:param iterable: An iterable of items.
:type iterable: Iterable[Any]
"""
a, b, c = tee(iterable)
next(b, None)
next(c, None)
return zip(a, b, c)
def iter_t2d_along_stroke(stroke):
"""Yields the progress along the stroke.
:param stroke: A stroke.
:type stroke: :class:`Stroke`
"""
total = stroke.length_2d
distance = 0.0
# yield for the comparison from the first vertex to itself
yield 0.0
for prev, svert in pairwise(stroke):
distance += (prev.point - svert.point).length
yield min(distance / total, 1.0) if total != 0.0 else 0.0
def iter_distance_from_camera(stroke, range_min, range_max, normfac):
"""
Yields the distance to the camera relative to the maximum
possible distance for every stroke vertex, constrained by
given minimum and maximum values.
:param stroke: A stroke.
:type stroke: :class:`Stroke`
:param range_min: Distances below this value are clamped to 0.
:type range_min: float
:param range_max: Distances above this value are clamped to 1.
:type range_max: float
:param normfac: Normalization factor applied to ``distance - range_min``.
:type normfac: float
"""
for svert in stroke:
# length in the camera coordinate
distance = svert.point_3d.length
if range_min < distance < range_max:
yield (svert, (distance - range_min) / normfac)
else:
yield (svert, 0.0) if range_min > distance else (svert, 1.0)
def iter_distance_from_object(stroke, location, range_min, range_max, normfac):
"""
yields the distance to the given object relative to the maximum
possible distance for every stroke vertex, constrained by
given minimum and maximum values.
:param stroke: A stroke.
:type stroke: :class:`Stroke`
:param location: Reference location in 3D space.
:type location: :class:`mathutils.Vector`
:param range_min: Distances below this value are clamped to 0.
:type range_min: float
:param range_max: Distances above this value are clamped to 1.
:type range_max: float
:param normfac: Normalization factor applied to ``distance - range_min``.
:type normfac: float
"""
for svert in stroke:
distance = (svert.point_3d - location).length # in the camera coordinate
if range_min < distance < range_max:
yield (svert, (distance - range_min) / normfac)
else:
yield (svert, 0.0) if distance < range_min else (svert, 1.0)
def iter_material_value(stroke, func, attribute):
"""Yields a specific material attribute from the vertex' underlying material.
:param stroke: A stroke.
:type stroke: :class:`Stroke`
:param func: A function returning a material for the iterator's current vertex.
:type func: Callable[[:class:`Interface0DIterator`], :class:`Material`]
:param attribute: The material attribute name (e.g. ``LINE``, ``DIFF``, ``ALPHA``).
:type attribute: str
"""
it = Interface0DIterator(stroke)
for svert in it:
material = func(it)
# main
if attribute == 'LINE':
value = rgb_to_bw(*material.line[0:3])
elif attribute == 'DIFF':
value = rgb_to_bw(*material.diffuse[0:3])
elif attribute == 'SPEC':
value = rgb_to_bw(*material.specular[0:3])
# line separate
elif attribute == 'LINE_R':
value = material.line[0]
elif attribute == 'LINE_G':
value = material.line[1]
elif attribute == 'LINE_B':
value = material.line[2]
elif attribute == 'LINE_A':
value = material.line[3]
# diffuse separate
elif attribute == 'DIFF_R':
value = material.diffuse[0]
elif attribute == 'DIFF_G':
value = material.diffuse[1]
elif attribute == 'DIFF_B':
value = material.diffuse[2]
elif attribute == 'ALPHA':
value = material.diffuse[3]
# specular separate
elif attribute == 'SPEC_R':
value = material.specular[0]
elif attribute == 'SPEC_G':
value = material.specular[1]
elif attribute == 'SPEC_B':
value = material.specular[2]
elif attribute == 'SPEC_HARDNESS':
value = material.shininess
else:
raise ValueError("unexpected material attribute: " + attribute)
yield (svert, value)
def iter_distance_along_stroke(stroke):
"""Yields the absolute distance along the stroke up to the current vertex.
:param stroke: A stroke.
:type stroke: :class:`Stroke`
"""
distance = 0.0
# the positions need to be copied, because they are changed in the calling function
points = tuple(svert.point.copy() for svert in stroke)
yield distance
for prev, curr in pairwise(points):
distance += (prev - curr).length
yield distance
# -- mathematical 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
:param it: An iterator over a stroke's vertices.
:type it: :class:`StrokeVertexIterator`
"""
for _ in it:
if (it.is_begin or it.is_end):
yield 0.0
continue
else:
it.decrement()
prev, current, succ = it.object.point.copy(), next(it).point.copy(), next(it).point.copy()
# return the iterator in an unchanged state
it.decrement()
ab = (current - prev)
bc = (succ - current)
ac = (prev - succ)
a, b, c = ab.length, bc.length, ac.length
try:
area = 0.5 * ab.cross(ac)
K = (4 * area) / (a * b * c)
except ZeroDivisionError:
K = 0.0
yield abs(K)
def stroke_normal(stroke):
"""
Compute the 2D normal at the stroke vertex pointed by the iterator
'it'. It is noted that Normal2DF0D computes normals based on
underlying FEdges instead, which is inappropriate for strokes when
they have already been modified by stroke geometry modifiers.
The returned normals are dynamic: they update when the
vertex position (and therefore the vertex normal) changes.
for use in geometry modifiers it is advised to
cast this generator function to a tuple or list
:param stroke: A stroke.
:type stroke: :class:`Stroke`
"""
it = iter(stroke)
yield from (normal_at_I0D(it) for _ in it)
def get_test_stroke():
"""Returns a static stroke object for testing """
from freestyle.types import Stroke, Interface0DIterator, StrokeVertexIterator, SVertex, Id, StrokeVertex
# points for our fake stroke
points = (Vector((1.0, 5.0, 3.0)), Vector((1.0, 2.0, 9.0)),
Vector((6.0, 2.0, 3.0)), Vector((7.0, 2.0, 3.0)),
Vector((2.0, 6.0, 3.0)), Vector((2.0, 8.0, 3.0)))
ids = (Id(0, 0), Id(1, 1), Id(2, 2), Id(3, 3), Id(4, 4), Id(5, 5))
stroke = Stroke()
it = iter(stroke)
for svert in map(SVertex, points, ids):
stroke.insert_vertex(StrokeVertex(svert), it)
it = iter(stroke)
stroke.update_length()
return stroke

File diff suppressed because it is too large Load Diff