Freestyle Python API: Updates and speedups for the parameter editor

In addition to D319, this patch updates the parameter editor, the UI of Freestyle.

Using new API functionality and experience gained in making D319, this patch
provides a quite noticable speedup for commonly-used Freestyle linestyle modifiers.

As this patch touches a lot of code (and mainly the foundations) it is likely that
mistakes are made.  The patch has been tested with a regression suite for Freestyle
(https://github.com/folkertdev/freestyle-regression-tests/tree/master), but testing
with scenes used in production is very much appreciated.

Differential revision: https://developer.blender.org/D623

Author: flokkievids (Folkert de Vries)

Reviewed by: kjym3 (Tamito Kajiyama)
This commit is contained in:
Tamito Kajiyama 2014-07-24 11:08:04 +09:00
parent 4eedec8681
commit b408d8af31
Notes: blender-bot 2023-06-12 00:52:52 +02:00
Referenced by commit 1a7c32a0ab, Fix T93320: Freestyle LineStyleModifier blend 'Minimum' error
Referenced by commit 0e2bbd0904, Freestyle: Fix for round/square stroke caps causing line thinning.
Referenced by commit b466a82fa5, Partial fix for T44404: freestyle crashes blender.
Referenced by commit eb8964fb7f, Fix T41464: Material Boundary bug in Freestyle.
5 changed files with 580 additions and 837 deletions

View File

@ -91,28 +91,26 @@ 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
MaterialF0D does not work with Curves and Strokes. Line color
priority is used to pick one of the two materials at material
boundaries.
Note: expects instances of CurvePoint to be iterated over
"""
def __call__(self, inter):
cp = inter.object
assert(isinstance(cp, CurvePoint))
fe = cp.first_svertex.get_fedge(cp.second_svertex)
fe = inter.object.fedge
assert(fe is not None), "CurveMaterialF0D: fe is None"
if fe.is_smooth:
return fe.material
elif fe.material_right.priority > fe.material_left.priority:
return fe.material_right
else:
return fe.material_left
right, left = fe.material_right, fe.material_left
return right if (right.priority > left.priority) else left
class pyInverseCurvature2DAngleF0D(UnaryFunction0DDouble):
@ -131,7 +129,7 @@ class pyCurvilinearLengthF0D(UnaryFunction0DDouble):
class pyDensityAnisotropyF0D(UnaryFunction0DDouble):
"""Estimates the anisotropy of density"""
def __init__(self,level):
def __init__(self, level):
UnaryFunction0DDouble.__init__(self)
self.IsoDensity = ReadCompleteViewMapPixelF0D(level)
self.d0Density = ReadSteerableViewMapPixelF0D(0, level)
@ -145,9 +143,9 @@ class pyDensityAnisotropyF0D(UnaryFunction0DDouble):
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
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):
@ -163,9 +161,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)) - \
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)) - \
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))
@ -184,7 +182,6 @@ class pyViewMapGradientNormF0D(UnaryFunction0DDouble):
CF.read_complete_view_map_pixel(self._l, int(p.x), int(p.y))
return Vector((gx, gy)).length
# -- Functions for 1D elements (curves) -- #
@ -199,11 +196,11 @@ class pyGetSquareInverseProjectedZF1D(UnaryFunction1DDouble):
def __call__(self, inter):
func = GetProjectedZF1D()
z = func(inter)
return (1.0 - z*z)
return (1.0 - pow(z, 2))
class pyDensityAnisotropyF1D(UnaryFunction1DDouble):
def __init__(self,level, integrationType=IntegrationType.MEAN, sampling=2.0):
def __init__(self, level, integrationType=IntegrationType.MEAN, sampling=2.0):
UnaryFunction1DDouble.__init__(self, integrationType)
self._func = pyDensityAnisotropyF0D(level)
self._integration = integrationType
@ -215,7 +212,7 @@ class pyDensityAnisotropyF1D(UnaryFunction1DDouble):
class pyViewMapGradientNormF1D(UnaryFunction1DDouble):
def __init__(self,l, integrationType, sampling=2.0):
def __init__(self, l, integrationType, sampling=2.0):
UnaryFunction1DDouble.__init__(self, integrationType)
self._func = pyViewMapGradientNormF0D(l)
self._integration = integrationType

View File

@ -52,6 +52,7 @@ from freestyle.types import (
UnaryPredicate0D,
UnaryPredicate1D,
Id,
Interface0DIterator,
)
from freestyle.functions import (
Curvature2DAngleF0D,
@ -82,11 +83,10 @@ class pyHigherCurvature2DAngleUP0D(UnaryPredicate0D):
def __init__(self, a):
UnaryPredicate0D.__init__(self)
self._a = a
self.func = Curvature2DAngleF0D()
def __call__(self, inter):
func = Curvature2DAngleF0D()
a = func(inter)
return (a > self._a)
return (self.func(inter) > self._a)
class pyUEqualsUP0D(UnaryPredicate0D):
@ -102,7 +102,7 @@ class pyUEqualsUP0D(UnaryPredicate0D):
class pyVertexNatureUP0D(UnaryPredicate0D):
def __init__(self,nature):
def __init__(self, nature):
UnaryPredicate0D.__init__(self)
self._nature = nature
@ -127,7 +127,7 @@ class pyBackTVertexUP0D(UnaryPredicate0D):
class pyParameterUP0DGoodOne(UnaryPredicate0D):
def __init__(self,pmin,pmax):
def __init__(self, pmin, pmax):
UnaryPredicate0D.__init__(self)
self._m = pmin
self._M = pmax
@ -138,7 +138,7 @@ class pyParameterUP0DGoodOne(UnaryPredicate0D):
class pyParameterUP0D(UnaryPredicate0D):
def __init__(self,pmin,pmax):
def __init__(self, pmin, pmax):
UnaryPredicate0D.__init__(self)
self._m = pmin
self._M = pmax
@ -154,13 +154,13 @@ class pyParameterUP0D(UnaryPredicate0D):
# -- Unary predicates for 1D elements (curves) -- #
class AndUP1D(UnaryPredicate1D):
def __init__(self, *predicates):
UnaryPredicate1D.__init__(self)
self.predicates = predicates
if len(self.predicates) < 2:
raise ValueError("Expected two or more UnaryPredicate1D")
# there are cases in which only one predicate is supplied (in the parameter editor)
if len(self.predicates) < 1:
raise ValueError("Expected two or more UnaryPredicate1D, got ", len(predicates))
def __call__(self, inter):
return all(pred(inter) for pred in self.predicates)
@ -170,8 +170,9 @@ class OrUP1D(UnaryPredicate1D):
def __init__(self, *predicates):
UnaryPredicate1D.__init__(self)
self.predicates = predicates
if len(self.predicates) < 2:
raise ValueError("Expected two or more UnaryPredicate1D")
# there are cases in which only one predicate is supplied (in the parameter editor)
if len(self.predicates) < 1:
raise ValueError("Expected two or more UnaryPredicate1D, got ", len(predicates))
def __call__(self, inter):
return any(pred(inter) for pred in self.predicates)
@ -230,7 +231,7 @@ class pyHigherLengthUP1D(UnaryPredicate1D):
class pyNatureUP1D(UnaryPredicate1D):
def __init__(self,nature):
def __init__(self, nature):
UnaryPredicate1D.__init__(self)
self._nature = nature
self._getNature = CurveNatureF1D()
@ -244,12 +245,14 @@ class pyHigherNumberOfTurnsUP1D(UnaryPredicate1D):
UnaryPredicate1D.__init__(self)
self._n = n
self._a = a
self.func = Curvature2DAngleF0D()
def __call__(self, inter):
func = Curvature2DAngleF0D()
it = inter.vertices_begin()
it = Interface0DIterator(inter)
# sum the turns, check against n
return sum(1 for ve in it if func(it) > self._a) > self._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):
@ -345,7 +348,7 @@ class pyZSmallerUP1D(UnaryPredicate1D):
class pyIsOccludedByUP1D(UnaryPredicate1D):
def __init__(self,id):
def __init__(self, id):
UnaryPredicate1D.__init__(self)
if not isinstance(id, Id):
raise TypeError("pyIsOccludedByUP1D expected freestyle.types.Id, not " + type(id).__name__)
@ -376,7 +379,7 @@ class pyIsOccludedByUP1D(UnaryPredicate1D):
class pyIsInOccludersListUP1D(UnaryPredicate1D):
def __init__(self,id):
def __init__(self, id):
UnaryPredicate1D.__init__(self)
self._id = id
@ -409,7 +412,7 @@ class pyIsOccludedByIdListUP1D(UnaryPredicate1D):
class pyShapeIdListUP1D(UnaryPredicate1D):
def __init__(self,idlist):
def __init__(self, idlist):
UnaryPredicate1D.__init__(self)
self._funcs = tuple(ShapeUP1D(_id, 0) for _id in idlist)
@ -417,7 +420,7 @@ class pyShapeIdListUP1D(UnaryPredicate1D):
return any(func(inter) for func in self._funcs)
## deprecated
# DEPRECATED
class pyShapeIdUP1D(UnaryPredicate1D):
def __init__(self, _id):
UnaryPredicate1D.__init__(self)
@ -429,7 +432,7 @@ class pyShapeIdUP1D(UnaryPredicate1D):
class pyHighDensityAnisotropyUP1D(UnaryPredicate1D):
def __init__(self,threshold, level, sampling=2.0):
def __init__(self, threshold, level, sampling=2.0):
UnaryPredicate1D.__init__(self)
self._l = threshold
self.func = pyDensityAnisotropyF1D(level, IntegrationType.MEAN, sampling)
@ -439,7 +442,7 @@ class pyHighDensityAnisotropyUP1D(UnaryPredicate1D):
class pyHighViewMapGradientNormUP1D(UnaryPredicate1D):
def __init__(self,threshold, l, sampling=2.0):
def __init__(self, threshold, l, sampling=2.0):
UnaryPredicate1D.__init__(self)
self._threshold = threshold
self._GetGradient = pyViewMapGradientNormF1D(l, IntegrationType.MEAN)
@ -463,8 +466,9 @@ class pyDensityVariableSigmaUP1D(UnaryPredicate1D):
self._sampling = sampling
def __call__(self, inter):
sigma = (self._sigmaMax-self._sigmaMin)/(self._lmax-self._lmin)*(self._functor(inter)-self._lmin) + self._sigmaMin
t = (self._tmax-self._tmin)/(self._lmax-self._lmin)*(self._functor(inter)-self._lmin) + self._tmin
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)
@ -480,13 +484,12 @@ class pyClosedCurveUP1D(UnaryPredicate1D):
# -- Binary predicates for 1D elements (curves) -- #
class AndBP1D(BinaryPredicate1D):
def __init__(self, *predicates):
BinaryPredicate1D.__init__(self)
self._predicates = predicates
if len(self.predicates) < 2:
raise ValueError("Expected two or more BinaryPredicate1D")
raise ValueError("Expected two or more BinaryPredicate1D, got ", len(predictates))
def __call__(self, i1, i2):
return all(pred(i1, i2) for pred in self._predicates)
@ -497,7 +500,7 @@ class OrBP1D(BinaryPredicate1D):
BinaryPredicate1D.__init__(self)
self._predicates = predicates
if len(self.predicates) < 2:
raise ValueError("Expected two or more BinaryPredicate1D")
raise ValueError("Expected two or more BinaryPredicate1D, got ", len(predictates))
def __call__(self, i1, i2):
return any(pred(i1, i2) for pred in self._predicates)
@ -551,11 +554,11 @@ class pyNatureBP1D(BinaryPredicate1D):
class pyViewMapGradientNormBP1D(BinaryPredicate1D):
def __init__(self,l, sampling=2.0):
def __init__(self, l, sampling=2.0):
BinaryPredicate1D.__init__(self)
self._GetGradient = pyViewMapGradientNormF1D(l, IntegrationType.MEAN)
def __call__(self, i1,i2):
def __call__(self, i1, i2):
return (self._GetGradient(i1) > self._GetGradient(i2))
@ -565,4 +568,4 @@ class pyShuffleBP1D(BinaryPredicate1D):
random.seed = 1
def __call__(self, inter1, inter2):
return (random.uniform(0,1) < random.uniform(0,1))
return (random.uniform(0, 1) < random.uniform(0, 1))

View File

@ -27,14 +27,21 @@ from _freestyle import (
integrate,
)
from freestyle.types import (
Interface0DIterator,
Stroke,
StrokeVertexIterator,
)
from mathutils import Vector
from functools import lru_cache
from functools import lru_cache, namedtuple
from math import cos, sin, pi
from itertools import tee
# -- 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
@ -55,7 +62,6 @@ def bounding_box(stroke):
x, y = zip(*(svert.point for svert in stroke))
return (Vector((min(x), min(y))), Vector((max(x), max(y))))
# -- General helper functions -- #
@ -72,10 +78,12 @@ def phase_to_direction(length):
results.append((phase, Vector((cos(2 * pi * phase), sin(2 * pi * phase)))))
return results
# A named tuple primitive used for storing data that
# has an upper and lower bound (eg. thickness, range and certain values)
BoundedProperty = namedtuple("BoundedProperty", ["min", "max", "delta"])
# -- helper functions for chaining -- #
def get_chain_length(ve, orientation):
"""Returns the 2d length of a given ViewEdge """
from freestyle.chainingiterators import pyChainSilhouetteGenericIterator
@ -112,156 +120,112 @@ def get_chain_length(ve, orientation):
def find_matching_vertex(id, it):
"""Finds the matching vertexn, or returns None """
"""Finds the matching vertex, or returns None """
return next((ve for ve in it if ve.id == id), None)
# -- helper functions for iterating -- #
def pairwise(iterable, types={Stroke, StrokeVertexIterator}):
"""Yields a tuple containing the previous and current object """
# use .incremented() for types that support it
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 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 tripplewise(iterable):
"""Yields a tuple containing the current object and its immediate neighbors """
a, b, c = tee(iterable)
next(b, None)
next(c, None)
return zip(a, b, c)
def iter_t2d_along_stroke(stroke):
"""
Yields the distance between two stroke vertices
relative to the total stroke length.
"""
""" Yields the progress along the stroke """
total = stroke.length_2d
distance = 0.0
for it, prev, svert in iter_current_previous(stroke):
# 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
t = min(distance / total, 1.0) if total > 0.0 else 0.0
yield (it, t)
yield min(distance / total, 1.0) if total != 0.0 else 0.0
def iter_distance_from_camera(stroke, range_min, range_max):
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.
"""
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
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:
t = (distance - range_min) / normfac
yield (it, t)
yield (svert, 0.0) if range_min > distance else (svert, 1.0)
def iter_distance_from_object(stroke, object, range_min, range_max):
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.
"""
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
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:
t = (distance - range_min) / normfac
yield (it, t)
yield (svert, 0.0) if distance < range_min else (svert, 1.0)
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()
def iter_material_value(stroke, func, attribute):
"Yields a specific material attribute from the vertex' underlying material. "
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]
# main
if attribute == 'DIFF':
value = rgb_to_bw(*material.diffuse[0:3])
elif attribute == 'ALPHA':
value = material.diffuse[3]
elif attribute == 'SPEC':
value = rgb_to_bw(*material.specular[0:3])
# diffuse seperate
elif attribute == 'DIFF_R':
value = material.diffuse[0]
elif attribute == 'DIFF_G':
value = material.diffuse[1]
elif attribute == 'DIFF_B':
value = material.diffuse[2]
# specular seperate
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: " + material_attribute)
yield (it, t)
raise ValueError("unexpected material attribute: " + attribute)
yield (svert, value)
def iter_distance_along_stroke(stroke):
"""
yields the absolute distance between
the current and preceding vertex.
"""
"Yields the absolute distance along the stroke up to the current 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
# 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
# -- mathmatical operations -- #
@ -272,55 +236,73 @@ def stroke_curvature(it):
K = 1 / R
where R is the radius of the circle going through the current vertex and its neighbors
"""
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()
if it.is_end or it.is_begin:
return 0.0
ab = (current - prev)
bc = (succ - current)
ac = (prev - succ)
next = it.incremented().point
prev = it.decremented().point
current = it.object.point
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
ab = (current - prev)
bc = (next - current)
ac = (prev - next)
yield abs(K)
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):
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
"""
# first stroke segment
it_next = it.incremented()
if it.is_begin:
e = it_next.object.point_2d - it.object.point_2d
n = Vector((e[1], -e[0]))
return n.normalized()
# last stroke segment
it_prev = it.decremented()
if it_next.is_end:
e = it.object.point_2d - it_prev.object.point_2d
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 = Vector((e1[1], -e1[0])).normalized()
n2 = Vector((e2[1], -e2[0])).normalized()
n = (n1 + n2)
return n.normalized()
n = len(stroke) - 1
for i, svert in enumerate(stroke):
if i == 0:
e = stroke[i + 1].point - svert.point
yield Vector((e[1], -e[0])).normalized()
elif i == n:
e = svert.point - stroke[i - 1].point
yield Vector((e[1], -e[0])).normalized()
else:
e1 = stroke[i + 1].point - svert.point
e2 = svert.point - stroke[i - 1].point
n1 = Vector((e1[1], -e1[0])).normalized()
n2 = Vector((e2[1], -e2[0])).normalized()
yield (n1 + n2).normalized()
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

View File

@ -178,6 +178,19 @@ static int CurvePoint_second_svertex_set(BPy_CurvePoint *self, PyObject *value,
return 0;
}
PyDoc_STRVAR(CurvePoint_fedge_doc,
"Gets the FEdge for the two SVertices that given CurvePoints consists out of.\n"
"A shortcut for CurvePoint.first_svertex.get_fedge(CurvePoint.second_svertex).\n"
"\n"
":type: :class:`FEdge`");
static PyObject *CurvePoint_fedge_get(BPy_CurvePoint *self, void *UNUSED(closure))
{
SVertex *A = self->cp->A();
Interface0D *B = (Interface0D *)self->cp->B();
return Any_BPy_Interface1D_from_Interface1D(*(A->getFEdge(*B)));
}
PyDoc_STRVAR(CurvePoint_t2d_doc,
"The 2D interpolation parameter.\n"
"\n"
@ -204,6 +217,8 @@ static PyGetSetDef BPy_CurvePoint_getseters[] = {
(char *)CurvePoint_first_svertex_doc, NULL},
{(char *)"second_svertex", (getter)CurvePoint_second_svertex_get, (setter)CurvePoint_second_svertex_set,
(char *)CurvePoint_second_svertex_doc, NULL},
{(char *)"fedge", (getter)CurvePoint_fedge_get, NULL,
CurvePoint_fedge_doc, NULL},
{(char *)"t2d", (getter)CurvePoint_t2d_get, (setter)CurvePoint_t2d_set, (char *)CurvePoint_t2d_doc, NULL},
{NULL, NULL, NULL, NULL, NULL} /* Sentinel */
};