This is a regression introduced in rBce729677db3e and rBb408d8af31c9. RoundCapShader and SquareCapsShader had to remove (almost) overlapping stroke vertices to avoid sudden thinning of line thickness. For instance, the test .blend file from https://developer.blender.org/T36425#231460 suffered from the reported line thinning (although T36425 was originally caused by a different bug).
1227 lines
40 KiB
Python
1227 lines
40 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
# Filename : shaders.py
|
|
# Authors : Fredo Durand, Stephane Grabli, Francois Sillion, Emmanuel Turquin
|
|
# Date : 11/08/2005
|
|
# Purpose : Stroke shaders to be used for creation of stylized strokes
|
|
|
|
"""
|
|
This module contains stroke shaders used for creation of stylized
|
|
strokes. It is also intended to be a collection of examples for
|
|
shader definition in Python.
|
|
|
|
User-defined stroke shaders inherit the
|
|
:class:`freestyle.types.StrokeShader` class.
|
|
"""
|
|
|
|
__all__ = (
|
|
"BackboneStretcherShader",
|
|
"BezierCurveShader",
|
|
"BlenderTextureShader",
|
|
"CalligraphicShader",
|
|
"ColorNoiseShader",
|
|
"ConstantColorShader",
|
|
"ConstantThicknessShader",
|
|
"ConstrainedIncreasingThicknessShader",
|
|
"GuidingLinesShader",
|
|
"IncreasingColorShader",
|
|
"IncreasingThicknessShader",
|
|
"PolygonalizationShader",
|
|
"RoundCapShader",
|
|
"SamplingShader",
|
|
"SmoothingShader",
|
|
"SpatialNoiseShader",
|
|
"SquareCapShader",
|
|
"StrokeTextureStepShader",
|
|
"ThicknessNoiseShader",
|
|
"TipRemoverShader",
|
|
"py2DCurvatureColorShader",
|
|
"pyBackboneStretcherNoCuspShader",
|
|
"pyBackboneStretcherShader",
|
|
"pyBluePrintCirclesShader",
|
|
"pyBluePrintDirectedSquaresShader",
|
|
"pyBluePrintEllipsesShader",
|
|
"pyBluePrintSquaresShader",
|
|
"pyConstantColorShader",
|
|
"pyConstantThicknessShader",
|
|
"pyConstrainedIncreasingThicknessShader",
|
|
"pyDecreasingThicknessShader",
|
|
"pyDepthDiscontinuityThicknessShader",
|
|
"pyDiffusion2Shader",
|
|
"pyFXSVaryingThicknessWithDensityShader",
|
|
"pyGuidingLineShader",
|
|
"pyHLRShader",
|
|
"pyImportance2DThicknessShader",
|
|
"pyImportance3DThicknessShader",
|
|
"pyIncreasingColorShader",
|
|
"pyIncreasingThicknessShader",
|
|
"pyInterpolateColorShader",
|
|
"pyLengthDependingBackboneStretcherShader",
|
|
"pyMaterialColorShader",
|
|
"pyModulateAlphaShader",
|
|
"pyNonLinearVaryingThicknessShader",
|
|
"pyPerlinNoise1DShader",
|
|
"pyPerlinNoise2DShader",
|
|
"pyRandomColorShader",
|
|
"pySLERPThicknessShader",
|
|
"pySamplingShader",
|
|
"pySinusDisplacementShader",
|
|
"pyTVertexRemoverShader",
|
|
"pyTVertexThickenerShader",
|
|
"pyTimeColorShader",
|
|
"pyTipRemoverShader",
|
|
"pyZDependingThicknessShader",
|
|
)
|
|
|
|
|
|
# module members
|
|
from _freestyle import (
|
|
BackboneStretcherShader,
|
|
BezierCurveShader,
|
|
BlenderTextureShader,
|
|
CalligraphicShader,
|
|
ColorNoiseShader,
|
|
ConstantColorShader,
|
|
ConstantThicknessShader,
|
|
ConstrainedIncreasingThicknessShader,
|
|
GuidingLinesShader,
|
|
IncreasingColorShader,
|
|
IncreasingThicknessShader,
|
|
PolygonalizationShader,
|
|
SamplingShader,
|
|
SmoothingShader,
|
|
SpatialNoiseShader,
|
|
StrokeTextureStepShader,
|
|
ThicknessNoiseShader,
|
|
TipRemoverShader,
|
|
)
|
|
|
|
# constructs for shader definition in Python
|
|
from freestyle.types import (
|
|
Interface0DIterator,
|
|
Nature,
|
|
Noise,
|
|
StrokeAttribute,
|
|
StrokeShader,
|
|
StrokeVertexIterator,
|
|
StrokeVertex,
|
|
)
|
|
from freestyle.functions import (
|
|
Curvature2DAngleF0D,
|
|
DensityF0D,
|
|
GetProjectedZF0D,
|
|
MaterialF0D,
|
|
Normal2DF0D,
|
|
Orientation2DF1D,
|
|
ZDiscontinuityF0D,
|
|
)
|
|
from freestyle.predicates import (
|
|
pyVertexNatureUP0D,
|
|
pyUEqualsUP0D,
|
|
)
|
|
|
|
from freestyle.utils import (
|
|
bound,
|
|
BoundingBox,
|
|
pairwise,
|
|
phase_to_direction,
|
|
)
|
|
|
|
from freestyle.utils import ContextFunctions as CF
|
|
|
|
import bpy
|
|
import random
|
|
|
|
from math import atan, cos, pi, sin, sinh, sqrt
|
|
from mathutils import Vector
|
|
from random import randint
|
|
|
|
|
|
# -- Thickness Stroke Shaders -- #
|
|
|
|
|
|
class pyDepthDiscontinuityThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns a thickness to the stroke based on the stroke's distance
|
|
to the camera (Z-value).
|
|
"""
|
|
def __init__(self, min, max):
|
|
StrokeShader.__init__(self)
|
|
self.a = max - min
|
|
self.b = min
|
|
self.func = ZDiscontinuityF0D()
|
|
|
|
def shade(self, stroke):
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
z = self.func(it)
|
|
thickness = self.a * z + self.b
|
|
svert.attribute.thickness = (thickness, thickness)
|
|
|
|
|
|
class pyConstantThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns a constant thickness along the stroke.
|
|
"""
|
|
def __init__(self, thickness):
|
|
StrokeShader.__init__(self)
|
|
self._thickness = thickness / 2.0
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
svert.attribute.thickness = (self._thickness, self._thickness)
|
|
|
|
|
|
class pyFXSVaryingThicknessWithDensityShader(StrokeShader):
|
|
"""
|
|
Assigns thickness to a stroke based on the density of the diffuse map.
|
|
"""
|
|
def __init__(self, wsize, threshold_min, threshold_max, thicknessMin, thicknessMax):
|
|
StrokeShader.__init__(self)
|
|
self._func = DensityF0D(wsize)
|
|
self.threshold_min = threshold_min
|
|
self.threshold_max = threshold_max
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
|
|
def shade(self, stroke):
|
|
it = Interface0DIterator(stroke)
|
|
delta_threshold = self.threshold_max - self.threshold_min
|
|
delta_thickness = self._thicknessMax - self._thicknessMin
|
|
|
|
for svert in it:
|
|
c = self._func(it)
|
|
c = bound(self.threshold_min, c, self.threshold_max)
|
|
t = (self.threshold_max - c) / delta_threshold * delta_thickness + self._thicknessMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyIncreasingThicknessShader(StrokeShader):
|
|
"""
|
|
Increasingly thickens the stroke.
|
|
"""
|
|
def __init__(self, thicknessMin, thicknessMax):
|
|
StrokeShader.__init__(self)
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
if i < (n * 0.5):
|
|
t = (1.0 - c) * self._thicknessMin + c * self._thicknessMax
|
|
else:
|
|
t = (1.0 - c) * self._thicknessMax + c * self._thicknessMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyConstrainedIncreasingThicknessShader(StrokeShader):
|
|
"""
|
|
Increasingly thickens the stroke, constrained by a ratio of the
|
|
stroke's length.
|
|
"""
|
|
def __init__(self, thicknessMin, thicknessMax, ratio):
|
|
StrokeShader.__init__(self)
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
self._ratio = ratio
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
maxT = min(self._ratio * stroke.length_2d, self._thicknessMax)
|
|
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
if i < (n * 0.5):
|
|
t = (1.0 - c) * self._thicknessMin + c * maxT
|
|
else:
|
|
t = (1.0 - c) * maxT + c * self._thicknessMin
|
|
|
|
if i == (n - 1):
|
|
svert.attribute.thickness = (self._thicknessMin / 2.0, self._thicknessMin / 2.0)
|
|
else:
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyDecreasingThicknessShader(StrokeShader):
|
|
"""
|
|
Inverse of pyIncreasingThicknessShader, decreasingly thickens the stroke.
|
|
"""
|
|
def __init__(self, thicknessMin, thicknessMax):
|
|
StrokeShader.__init__(self)
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
|
|
def shade(self, stroke):
|
|
l = stroke.length_2d
|
|
n = len(stroke)
|
|
tMax = min(self._thicknessMax, 0.33 * l)
|
|
tMin = min(self._thicknessMin, 0.10 * l)
|
|
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
t = (1.0 - c) * tMax + c * tMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyNonLinearVaryingThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness to a stroke based on an exponential function.
|
|
"""
|
|
def __init__(self, thicknessExtremity, thicknessMiddle, exponent):
|
|
self._thicknessMin = thicknessMiddle
|
|
self._thicknessMax = thicknessExtremity
|
|
self._exp = exponent
|
|
StrokeShader.__init__(self)
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
for i, svert in enumerate(stroke):
|
|
c = (i / n) if (i < n / 2.0) else ((n - i) / n)
|
|
c = pow(c, self._exp) * pow(2.0, self._exp)
|
|
t = (1.0 - c) * self._thicknessMax + c * self._thicknessMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pySLERPThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness to a stroke based on spherical linear interpolation.
|
|
"""
|
|
def __init__(self, thicknessMin, thicknessMax, omega=1.2):
|
|
StrokeShader.__init__(self)
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
self.omega = omega
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
maxT = min(self._thicknessMax, 0.33 * stroke.length_2d)
|
|
omega = self.omega
|
|
sinhyp = sinh(omega)
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
if i < (n * 0.5):
|
|
t = sin((1-c) * omega) / sinhyp * self._thicknessMin + sin(c * omega) / sinhyp * maxT
|
|
else:
|
|
t = sin((1-c) * omega) / sinhyp * maxT + sin(c * omega) / sinhyp * self._thicknessMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyTVertexThickenerShader(StrokeShader):
|
|
"""
|
|
Thickens TVertices (visual intersections between two edges).
|
|
"""
|
|
def __init__(self, a=1.5, n=3):
|
|
StrokeShader.__init__(self)
|
|
self._a = a
|
|
self._n = n
|
|
|
|
def shade(self, stroke):
|
|
n = self._n
|
|
a = self._a
|
|
|
|
term = (a - 1.0) / (n - 1.0)
|
|
|
|
if (stroke[0].nature & Nature.T_VERTEX):
|
|
for count, svert in zip(range(n), stroke):
|
|
r = term * (n / (count + 1.0) - 1.0) + 1.0
|
|
(tr, tl) = svert.attribute.thickness
|
|
svert.attribute.thickness = (r * tr, r * tl)
|
|
|
|
if (stroke[-1].nature & Nature.T_VERTEX):
|
|
for count, svert in zip(range(n), reversed(stroke)):
|
|
r = term * (n / (count + 1.0) - 1.0) + 1.0
|
|
(tr, tl) = svert.attribute.thickness
|
|
svert.attribute.thickness = (r * tr, r * tl)
|
|
|
|
|
|
class pyImportance2DThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness based on distance to a given point in 2D space.
|
|
the thickness is inverted, so the vertices closest to the
|
|
specified point have the lowest thickness.
|
|
"""
|
|
def __init__(self, x, y, w, kmin, kmax):
|
|
StrokeShader.__init__(self)
|
|
self._origin = Vector((x, y))
|
|
self._w = w
|
|
self._kmin, self._kmax = kmin, kmax
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
d = (svert.point_2d - self._origin).length
|
|
k = (self._kmin if (d > self._w) else
|
|
(self._kmax * (self._w-d) + self._kmin * d) / self._w)
|
|
|
|
(tr, tl) = svert.attribute.thickness
|
|
svert.attribute.thickness = (k*tr/2.0, k*tl/2.0)
|
|
|
|
|
|
class pyImportance3DThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness based on distance to a given point in 3D space.
|
|
"""
|
|
def __init__(self, x, y, z, w, kmin, kmax):
|
|
StrokeShader.__init__(self)
|
|
self._origin = Vector((x, y, z))
|
|
self._w = w
|
|
self._kmin, self._kmax = kmin, kmax
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
d = (svert.point_3d - self._origin).length
|
|
k = (self._kmin if (d > self._w) else
|
|
(self._kmax * (self._w-d) + self._kmin * d) / self._w)
|
|
|
|
(tr, tl) = svert.attribute.thickness
|
|
svert.attribute.thickness = (k*tr/2.0, k*tl/2.0)
|
|
|
|
|
|
class pyZDependingThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness based on an object's local Z depth (point
|
|
closest to camera is 1, point furthest from camera is zero).
|
|
"""
|
|
def __init__(self, min, max):
|
|
StrokeShader.__init__(self)
|
|
self.__min = min
|
|
self.__max = max
|
|
self.func = GetProjectedZF0D()
|
|
|
|
def shade(self, stroke):
|
|
it = Interface0DIterator(stroke)
|
|
z_indices = tuple(self.func(it) for _ in it)
|
|
z_min, z_max = min(1, *z_indices), max(0, *z_indices)
|
|
z_diff = 1 / (z_max - z_min)
|
|
|
|
for svert, z_index in zip(stroke, z_indices):
|
|
z = (z_index - z_min) * z_diff
|
|
thickness = (1 - z) * self.__max + z * self.__min
|
|
svert.attribute.thickness = (thickness, thickness)
|
|
|
|
|
|
# -- Color & Alpha Stroke Shaders -- #
|
|
|
|
|
|
class pyConstantColorShader(StrokeShader):
|
|
"""
|
|
Assigns a constant color to the stroke.
|
|
"""
|
|
def __init__(self,r,g,b, a = 1):
|
|
StrokeShader.__init__(self)
|
|
self._color = (r, g, b)
|
|
self._a = a
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
svert.attribute.color = self._color
|
|
svert.attribute.alpha = self._a
|
|
|
|
|
|
class pyIncreasingColorShader(StrokeShader):
|
|
"""
|
|
Fades from one color to another along the stroke.
|
|
"""
|
|
def __init__(self,r1,g1,b1,a1, r2,g2,b2,a2):
|
|
StrokeShader.__init__(self)
|
|
# use 4d vector to simplify math
|
|
self._c1 = Vector((r1, g1 ,b1, a1))
|
|
self._c2 = Vector((r2, g2, b2, a2))
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke) - 1
|
|
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
color = (1 - c) * self._c1 + c * self._c2
|
|
svert.attribute.color = color[:3]
|
|
svert.attribute.alpha = color[3]
|
|
|
|
|
|
class pyInterpolateColorShader(StrokeShader):
|
|
"""
|
|
Fades from one color to another and back.
|
|
"""
|
|
def __init__(self,r1,g1,b1,a1, r2,g2,b2,a2):
|
|
StrokeShader.__init__(self)
|
|
# use 4d vector to simplify math
|
|
self._c1 = Vector((r1, g1 ,b1, a1))
|
|
self._c2 = Vector((r2, g2, b2, a2))
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke) - 1
|
|
for i, svert in enumerate(stroke):
|
|
c = 1.0 - 2.0 * abs((i / n) - 0.5)
|
|
color = (1.0 - c) * self._c1 + c * self._c2
|
|
svert.attribute.color = color[:3]
|
|
svert.attribute.alpha = color[3]
|
|
|
|
|
|
class pyModulateAlphaShader(StrokeShader):
|
|
"""
|
|
Limits the stroke's alpha between a min and max value.
|
|
"""
|
|
def __init__(self, min=0, max=1):
|
|
StrokeShader.__init__(self)
|
|
self.__min = min
|
|
self.__max = max
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
alpha = svert.attribute.alpha
|
|
alpha = bound(self.__min, alpha * svert.point.y * 0.0025, self.__max)
|
|
svert.attribute.alpha = alpha
|
|
|
|
|
|
class pyMaterialColorShader(StrokeShader):
|
|
"""
|
|
Assigns the color of the underlying material to the stroke.
|
|
"""
|
|
def __init__(self, threshold=50):
|
|
StrokeShader.__init__(self)
|
|
self._threshold = threshold
|
|
self._func = MaterialF0D()
|
|
|
|
def shade(self, stroke):
|
|
xn = 0.312713
|
|
yn = 0.329016
|
|
Yn = 1.0
|
|
un = 4.0 * xn / (-2.0 * xn + 12.0 * yn + 3.0)
|
|
vn = 9.0 * yn / (-2.0 * xn + 12.0 * yn + 3.0)
|
|
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
mat = self._func(it)
|
|
|
|
r, g, b, *_ = mat.diffuse
|
|
|
|
X = 0.412453 * r + 0.35758 * g + 0.180423 * b
|
|
Y = 0.212671 * r + 0.71516 * g + 0.072169 * b
|
|
Z = 0.019334 * r + 0.11919 * g + 0.950227 * b
|
|
|
|
if not any((X, Y, Z)):
|
|
X = Y = Z = 0.01
|
|
|
|
u = 4.0 * X / (X + 15.0 * Y + 3.0 * Z)
|
|
v = 9.0 * Y / (X + 15.0 * Y + 3.0 * Z)
|
|
|
|
L= 116. * pow((Y/Yn),(1./3.)) - 16
|
|
U = 13. * L * (u - un)
|
|
V = 13. * L * (v - vn)
|
|
|
|
if L > self._threshold:
|
|
L /= 1.3
|
|
U += 10.
|
|
else:
|
|
L = L + 2.5 * (100-L) * 0.2
|
|
U /= 3.0
|
|
V /= 3.0
|
|
|
|
u = U / (13.0 * L) + un
|
|
v = V / (13.0 * L) + vn
|
|
|
|
Y = Yn * pow(((L+16.)/116.), 3.)
|
|
X = -9. * Y * u / ((u - 4.)* v - u * v)
|
|
Z = (9. * Y - 15*v*Y - v*X) /( 3. * v)
|
|
|
|
r = 3.240479 * X - 1.53715 * Y - 0.498535 * Z
|
|
g = -0.969256 * X + 1.875991 * Y + 0.041556 * Z
|
|
b = 0.055648 * X - 0.204043 * Y + 1.057311 * Z
|
|
|
|
r = max(0, r)
|
|
g = max(0, g)
|
|
b = max(0, b)
|
|
|
|
svert.attribute.color = (r, g, b)
|
|
|
|
|
|
class pyRandomColorShader(StrokeShader):
|
|
"""
|
|
Assigns a color to the stroke based on given seed.
|
|
"""
|
|
def __init__(self, s=1):
|
|
StrokeShader.__init__(self)
|
|
random.seed = s
|
|
|
|
def shade(self, stroke):
|
|
c = (random.uniform(15, 75) * 0.01,
|
|
random.uniform(15, 75) * 0.01,
|
|
random.uniform(15, 75) * 0.01)
|
|
for svert in stroke:
|
|
svert.attribute.color = c
|
|
|
|
|
|
class py2DCurvatureColorShader(StrokeShader):
|
|
"""
|
|
Assigns a color (greyscale) to the stroke based on the curvature.
|
|
A higher curvature will yield a brighter color.
|
|
"""
|
|
def shade(self, stroke):
|
|
func = Curvature2DAngleF0D()
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
c = func(it)
|
|
if c < 0 and bpy.app.debug_freestyle:
|
|
print("py2DCurvatureColorShader: negative 2D curvature")
|
|
color = 10.0 * c / pi
|
|
svert.attribute.color = (color, color, color)
|
|
|
|
|
|
class pyTimeColorShader(StrokeShader):
|
|
"""
|
|
Assigns a greyscale value that increases for every vertex.
|
|
The brightness will increase along the stroke.
|
|
"""
|
|
def __init__(self, step=0.01):
|
|
StrokeShader.__init__(self)
|
|
self._step = step
|
|
def shade(self, stroke):
|
|
for i, svert in enumerate(stroke):
|
|
c = i * self._step
|
|
svert.attribute.color = (c, c, c)
|
|
|
|
|
|
# -- Geometry Stroke Shaders -- #
|
|
|
|
|
|
class pySamplingShader(StrokeShader):
|
|
"""
|
|
Resamples the stroke, which gives the stroke the amount of
|
|
vertices specified.
|
|
"""
|
|
def __init__(self, sampling):
|
|
StrokeShader.__init__(self)
|
|
self._sampling = sampling
|
|
|
|
def shade(self, stroke):
|
|
stroke.resample(float(self._sampling))
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBackboneStretcherShader(StrokeShader):
|
|
"""
|
|
Stretches the stroke's backbone by a given length (in pixels).
|
|
"""
|
|
def __init__(self, l):
|
|
StrokeShader.__init__(self)
|
|
self._l = l
|
|
|
|
def shade(self, stroke):
|
|
# get start and end points
|
|
v0, vn = stroke[0], stroke[-1]
|
|
p0, pn = v0.point, vn.point
|
|
# get the direction
|
|
d1 = (p0 - stroke[ 1].point).normalized()
|
|
dn = (pn - stroke[-2].point).normalized()
|
|
v0.point += d1 * self._l
|
|
vn.point += dn * self._l
|
|
stroke.update_length()
|
|
|
|
|
|
class pyLengthDependingBackboneStretcherShader(StrokeShader):
|
|
"""
|
|
Stretches the stroke's backbone proportional to the stroke's length
|
|
NOTE: you'll probably want an l somewhere between (0.5 - 0). A value that
|
|
is too high may yield unexpected results.
|
|
"""
|
|
def __init__(self, l):
|
|
StrokeShader.__init__(self)
|
|
self._l = l
|
|
def shade(self, stroke):
|
|
# get start and end points
|
|
v0, vn = stroke[0], stroke[-1]
|
|
p0, pn = v0.point, vn.point
|
|
# get the direction
|
|
d1 = (p0 - stroke[ 1].point).normalized()
|
|
dn = (pn - stroke[-2].point).normalized()
|
|
v0.point += d1 * self._l * stroke.length_2d
|
|
vn.point += dn * self._l * stroke.length_2d
|
|
stroke.update_length()
|
|
|
|
|
|
class pyGuidingLineShader(StrokeShader):
|
|
def shade(self, stroke):
|
|
# get the tangent direction
|
|
t = stroke[-1].point - stroke[0].point
|
|
# look for the stroke middle vertex
|
|
itmiddle = iter(stroke)
|
|
while itmiddle.object.u < 0.5:
|
|
itmiddle.increment()
|
|
center_vertex = itmiddle.object
|
|
# position all the vertices along the tangent for the right part
|
|
it = StrokeVertexIterator(itmiddle)
|
|
for svert in it:
|
|
svert.point = center_vertex.point + t * (svert.u - center_vertex.u)
|
|
# position all the vertices along the tangent for the left part
|
|
it = StrokeVertexIterator(itmiddle).reversed()
|
|
for svert in it:
|
|
svert.point = center_vertex.point - t * (center_vertex.u - svert.u)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBackboneStretcherNoCuspShader(StrokeShader):
|
|
"""
|
|
Stretches the stroke's backbone, excluding cusp vertices (end junctions).
|
|
"""
|
|
def __init__(self, l):
|
|
StrokeShader.__init__(self)
|
|
self._l = l
|
|
|
|
def shade(self, stroke):
|
|
|
|
v0, v1 = stroke[0], stroke[1]
|
|
vn, vn_1 = stroke[-1], stroke[-2]
|
|
|
|
if not (v0.nature & v1.nature & Nature.CUSP):
|
|
d1 = (v0.point - v1.point).normalized()
|
|
v0.point += d1 * self._l
|
|
|
|
if not (vn.nature & vn_1.nature & Nature.CUSP):
|
|
dn = (vn.point - vn_1.point).normalized()
|
|
vn.point += dn * self._l
|
|
|
|
stroke.update_length()
|
|
|
|
|
|
class pyDiffusion2Shader(StrokeShader):
|
|
"""
|
|
Iteratively adds an offset to the position of each stroke vertex
|
|
in the direction perpendicular to the stroke direction at the
|
|
point. The offset is scaled by the 2D curvature (i.e. how quickly
|
|
the stroke curve is) at the point.
|
|
"""
|
|
def __init__(self, lambda1, nbIter):
|
|
StrokeShader.__init__(self)
|
|
self._lambda = lambda1
|
|
self._nbIter = nbIter
|
|
self._normalInfo = Normal2DF0D()
|
|
self._curvatureInfo = Curvature2DAngleF0D()
|
|
|
|
def shade(self, stroke):
|
|
for i in range (1, self._nbIter):
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
svert.point += self._normalInfo(it) * self._lambda * self._curvatureInfo(it)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyTipRemoverShader(StrokeShader):
|
|
"""
|
|
Removes the tips of the stroke.
|
|
"""
|
|
def __init__(self, l):
|
|
StrokeShader.__init__(self)
|
|
self._l = l
|
|
|
|
@staticmethod
|
|
def check_vertex(v, length):
|
|
# Returns True if the given strokevertex is less than self._l away
|
|
# from the stroke's tip and therefore should be removed.
|
|
return (v.curvilinear_abscissa < length or v.stroke_length-v.curvilinear_abscissa < length)
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
if n < 4:
|
|
return
|
|
|
|
verticesToRemove = tuple(svert for svert in stroke if self.check_vertex(svert, self._l))
|
|
# explicit conversion to StrokeAttribute is needed
|
|
oldAttributes = (StrokeAttribute(svert.attribute) for svert in stroke)
|
|
|
|
if n - len(verticesToRemove) < 2:
|
|
return
|
|
|
|
for sv in verticesToRemove:
|
|
stroke.remove_vertex(sv)
|
|
|
|
stroke.update_length()
|
|
stroke.resample(n)
|
|
if len(stroke) != n and bpy.app.debug_freestyle:
|
|
print("pyTipRemover: Warning: resampling problem")
|
|
|
|
for svert, a in zip(stroke, oldAttributes):
|
|
svert.attribute = a
|
|
stroke.update_length()
|
|
|
|
|
|
class pyTVertexRemoverShader(StrokeShader):
|
|
"""
|
|
Removes t-vertices from the stroke.
|
|
"""
|
|
def shade(self, stroke):
|
|
if len(stroke) < 4:
|
|
return
|
|
|
|
v0, vn = stroke[0], stroke[-1]
|
|
if (v0.nature & Nature.T_VERTEX):
|
|
stroke.remove_vertex(v0)
|
|
if (vn.nature & Nature.T_VERTEX):
|
|
stroke.remove_vertex(vn)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyHLRShader(StrokeShader):
|
|
"""
|
|
Controls visibility based upon the quantitative invisibility (QI)
|
|
based on hidden line removal (HLR).
|
|
"""
|
|
def shade(self, stroke):
|
|
if len(stroke) < 4:
|
|
return
|
|
|
|
it = iter(stroke)
|
|
for v1, v2 in zip(it, it.incremented()):
|
|
if (v1.nature & Nature.VIEW_VERTEX):
|
|
visible = (v1.get_fedge(v2).viewedge.qi != 0)
|
|
v1.attribute.visible = not visible
|
|
|
|
|
|
class pySinusDisplacementShader(StrokeShader):
|
|
"""
|
|
Displaces the stroke in the shape of a sine wave.
|
|
"""
|
|
def __init__(self, f, a):
|
|
StrokeShader.__init__(self)
|
|
self._f = f
|
|
self._a = a
|
|
self._getNormal = Normal2DF0D()
|
|
|
|
def shade(self, stroke):
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
normal = self._getNormal(it)
|
|
a = self._a * (1 - 2 * (abs(svert.u - 0.5)))
|
|
n = normal * a * cos(self._f * svert.u * 6.28)
|
|
svert.point += n
|
|
stroke.update_length()
|
|
|
|
|
|
class pyPerlinNoise1DShader(StrokeShader):
|
|
"""
|
|
Displaces the stroke using the curvilinear abscissa. This means
|
|
that lines with the same length and sampling interval will be
|
|
identically distorded.
|
|
"""
|
|
def __init__(self, freq=10, amp=10, oct=4, seed=-1):
|
|
StrokeShader.__init__(self)
|
|
self.__noise = Noise(seed)
|
|
self.__freq = freq
|
|
self.__amp = amp
|
|
self.__oct = oct
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
s = svert.projected_x + svert.projected_y
|
|
nres = self.__noise.turbulence1(s, self.__freq, self.__amp, self.__oct)
|
|
svert.point = (svert.projected_x + nres, svert.projected_y + nres)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyPerlinNoise2DShader(StrokeShader):
|
|
"""
|
|
Displaces the stroke using the strokes coordinates. This means
|
|
that in a scene no strokes will be distorded identically.
|
|
|
|
More information on the noise shaders can be found at:
|
|
freestyleintegration.wordpress.com/2011/09/25/development-updates-on-september-25/
|
|
"""
|
|
def __init__(self, freq=10, amp=10, oct=4, seed=-1):
|
|
StrokeShader.__init__(self)
|
|
self.__noise = Noise(seed)
|
|
self.__freq = freq
|
|
self.__amp = amp
|
|
self.__oct = oct
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
nres = self.__noise.turbulence2(svert.point_2d, self.__freq, self.__amp, self.__oct)
|
|
svert.point = (svert.projected_x + nres, svert.projected_y + nres)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBluePrintCirclesShader(StrokeShader):
|
|
"""
|
|
Draws the silhouette of the object as a circle.
|
|
"""
|
|
def __init__(self, turns=1, random_radius=3, random_center=5):
|
|
StrokeShader.__init__(self)
|
|
self.__turns = turns
|
|
self.__random_center = random_center
|
|
self.__random_radius = random_radius
|
|
|
|
def shade(self, stroke):
|
|
# get minimum and maximum coordinates
|
|
p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners
|
|
|
|
stroke.resample(32 * self.__turns)
|
|
sv_nb = len(stroke) // self.__turns
|
|
center = (p_min + p_max) / 2
|
|
radius = (center.x - p_min.x + center.y - p_min.y) / 2
|
|
R = self.__random_radius
|
|
C = self.__random_center
|
|
|
|
# The directions (and phases) are calculated using a separate
|
|
# function decorated with an lru-cache. This guarantees that
|
|
# the directions (involving sin and cos) are calculated as few
|
|
# times as possible.
|
|
#
|
|
# This works because the phases and directions are only
|
|
# dependant on the stroke length, and the chance that
|
|
# stroke.resample() above produces strokes of the same length
|
|
# is quite high.
|
|
#
|
|
# In tests, the amount of calls to sin() and cos() went from
|
|
# over 21000 to just 32 times, yielding a speedup of over 100%
|
|
directions = phase_to_direction(sv_nb)
|
|
|
|
it = iter(stroke)
|
|
|
|
for j in range(self.__turns):
|
|
prev_radius = radius
|
|
prev_center = center
|
|
radius += randint(-R, R)
|
|
center += Vector((randint(-C, C), randint(-C, C)))
|
|
|
|
for (phase, direction), svert in zip(directions, it):
|
|
r = prev_radius + (radius - prev_radius) * phase
|
|
c = prev_center + (center - prev_center) * phase
|
|
svert.point = c + r * direction
|
|
|
|
if not it.is_end:
|
|
it.increment()
|
|
for sv in tuple(it):
|
|
stroke.remove_vertex(sv)
|
|
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBluePrintEllipsesShader(StrokeShader):
|
|
def __init__(self, turns=1, random_radius=3, random_center=5):
|
|
StrokeShader.__init__(self)
|
|
self.__turns = turns
|
|
self.__random_center = random_center
|
|
self.__random_radius = random_radius
|
|
|
|
def shade(self, stroke):
|
|
p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners
|
|
|
|
stroke.resample(32 * self.__turns)
|
|
sv_nb = len(stroke) // self.__turns
|
|
|
|
center = (p_min + p_max) / 2
|
|
radius = center - p_min
|
|
|
|
R = self.__random_radius
|
|
C = self.__random_center
|
|
|
|
# for description of the line below, see pyBluePrintCirclesShader
|
|
directions = phase_to_direction(sv_nb)
|
|
it = iter(stroke)
|
|
for j in range(self.__turns):
|
|
prev_radius = radius
|
|
prev_center = center
|
|
radius = radius + Vector((randint(-R, R), randint(-R, R)))
|
|
center = center + Vector((randint(-C, C), randint(-C, C)))
|
|
|
|
for (phase, direction), svert in zip(directions, it):
|
|
r = prev_radius + (radius - prev_radius) * phase
|
|
c = prev_center + (center - prev_center) * phase
|
|
svert.point = (c.x + r.x * direction.x, c.y + r.y * direction.y)
|
|
|
|
# remove excess vertices
|
|
if not it.is_end:
|
|
it.increment()
|
|
for sv in tuple(it):
|
|
stroke.remove_vertex(sv)
|
|
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBluePrintSquaresShader(StrokeShader):
|
|
def __init__(self, turns=1, bb_len=10, bb_rand=0):
|
|
StrokeShader.__init__(self)
|
|
self.__turns = turns # does not have any effect atm
|
|
self.__bb_len = bb_len
|
|
self.__bb_rand = bb_rand
|
|
|
|
def shade(self, stroke):
|
|
# this condition will lead to errors later, end now
|
|
if len(stroke) < 1:
|
|
return
|
|
|
|
# get minimum and maximum coordinates
|
|
p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners
|
|
|
|
stroke.resample(32 * self.__turns)
|
|
num_segments = len(stroke) // self.__turns
|
|
f = num_segments // 4
|
|
# indices of the vertices that will form corners
|
|
first, second, third, fourth = (f, f * 2, f * 3, num_segments)
|
|
|
|
# construct points of the backbone
|
|
bb_len = self.__bb_len
|
|
points = (
|
|
Vector((p_min.x - bb_len, p_min.y)),
|
|
Vector((p_max.x + bb_len, p_min.y)),
|
|
Vector((p_max.x, p_min.y - bb_len)),
|
|
Vector((p_max.x, p_max.y + bb_len)),
|
|
Vector((p_max.x + bb_len, p_max.y)),
|
|
Vector((p_min.x - bb_len, p_max.y)),
|
|
Vector((p_min.x, p_max.y + bb_len)),
|
|
Vector((p_min.x, p_min.y - bb_len)),
|
|
)
|
|
|
|
# add randomization to the points (if needed)
|
|
if self.__bb_rand:
|
|
R, r = self.__bb_rand, self.__bb_rand // 2
|
|
|
|
randomization_mat = (
|
|
Vector((randint(-R, R), randint(-r, r))),
|
|
Vector((randint(-R, R), randint(-r, r))),
|
|
Vector((randint(-r, r), randint(-R, R))),
|
|
Vector((randint(-r, r), randint(-R, R))),
|
|
Vector((randint(-R, R), randint(-r, r))),
|
|
Vector((randint(-R, R), randint(-r, r))),
|
|
Vector((randint(-r, r), randint(-R, R))),
|
|
Vector((randint(-r, r), randint(-R, R))),
|
|
)
|
|
|
|
# combine both tuples
|
|
points = tuple(p + rand for (p, rand) in zip(points, randomization_mat))
|
|
|
|
|
|
# subtract even from uneven; result is length four tuple of vectors
|
|
it = iter(points)
|
|
old_vecs = tuple(next(it) - current for current in it)
|
|
|
|
it = iter(stroke)
|
|
verticesToRemove = list()
|
|
for j in range(self.__turns):
|
|
for i, svert in zip(range(num_segments), it):
|
|
if i < first:
|
|
svert.point = points[0] + old_vecs[0] * i / (first - 1)
|
|
svert.attribute.visible = (i != first - 1)
|
|
elif i < second:
|
|
svert.point = points[2] + old_vecs[1] * (i - first) / (second - first - 1)
|
|
svert.attribute.visible = (i != second - 1)
|
|
elif i < third:
|
|
svert.point = points[4] + old_vecs[2] * (i - second) / (third - second - 1)
|
|
svert.attribute.visible = (i != third - 1)
|
|
elif i < fourth:
|
|
svert.point = points[6] + old_vecs[3] * (i - third) / (fourth - third - 1)
|
|
svert.attribute.visible = (i != fourth - 1)
|
|
else:
|
|
# special case; remove these vertices
|
|
verticesToRemove.append(svert)
|
|
|
|
# remove excess vertices (if any)
|
|
if not it.is_end:
|
|
it.increment()
|
|
verticesToRemove += [svert for svert in it]
|
|
for sv in verticesToRemove:
|
|
stroke.remove_vertex(sv)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBluePrintDirectedSquaresShader(StrokeShader):
|
|
"""
|
|
Replaces the stroke with a directed square.
|
|
"""
|
|
def __init__(self, turns=1, bb_len=10, mult=1):
|
|
StrokeShader.__init__(self)
|
|
self.__mult = mult
|
|
self.__turns = turns
|
|
self.__bb_len = 1 + bb_len * 0.01
|
|
|
|
def shade(self, stroke):
|
|
stroke.resample(32 * self.__turns)
|
|
n = len(stroke)
|
|
|
|
p_mean = (1 / n) * sum((svert.point for svert in stroke), Vector((0.0, 0.0)))
|
|
p_var = Vector((0, 0))
|
|
p_var_xy = 0.0
|
|
for d in (svert.point - p_mean for svert in stroke):
|
|
p_var += Vector((d.x ** 2, d.y ** 2))
|
|
p_var_xy += d.x * d.y
|
|
|
|
# divide by number of vertices
|
|
p_var /= n
|
|
p_var_xy /= n
|
|
trace = p_var.x + p_var.y
|
|
det = p_var.x * p_var.y - pow(p_var_xy, 2)
|
|
|
|
sqrt_coeff = sqrt(trace * trace - 4 * det)
|
|
lambda1, lambda2 = (trace + sqrt_coeff) / 2, (trace - sqrt_coeff) / 2
|
|
# make sure those numbers aren't to small, if they are, rooting them will yield complex numbers
|
|
lambda1, lambda2 = max(1e-12, lambda1), max(1e-12, lambda2)
|
|
theta = atan(2 * p_var_xy / (p_var.x - p_var.y)) / 2
|
|
|
|
if p_var.y > p_var.x:
|
|
e1 = Vector((cos(theta + pi / 2), sin(theta + pi / 2))) * sqrt(lambda1) * self.__mult
|
|
e2 = Vector((cos(theta + pi ), sin(theta + pi ))) * sqrt(lambda2) * self.__mult
|
|
else:
|
|
e1 = Vector((cos(theta), sin(theta))) * sqrt(lambda1) * self.__mult
|
|
e2 = Vector((cos(theta + pi / 2), sin(theta + pi / 2))) * sqrt(lambda2) * self.__mult
|
|
|
|
# partition the stroke
|
|
num_segments = len(stroke) // self.__turns
|
|
f = num_segments // 4
|
|
# indices of the vertices that will form corners
|
|
first, second, third, fourth = (f, f * 2, f * 3, num_segments)
|
|
|
|
bb_len1 = self.__bb_len
|
|
bb_len2 = 1 + (bb_len1 - 1) * sqrt(lambda1 / lambda2)
|
|
points = (
|
|
p_mean - e1 - e2 * bb_len2,
|
|
p_mean - e1 * bb_len1 + e2,
|
|
p_mean + e1 + e2 * bb_len2,
|
|
p_mean + e1 * bb_len1 - e2,
|
|
)
|
|
|
|
old_vecs = (
|
|
e2 * bb_len2 * 2,
|
|
e1 * bb_len1 * 2,
|
|
-e2 * bb_len2 * 2,
|
|
-e1 * bb_len1 * 2,
|
|
)
|
|
|
|
it = iter(stroke)
|
|
verticesToRemove = list()
|
|
for j in range(self.__turns):
|
|
for i, svert in zip(range(num_segments), it):
|
|
if i < first:
|
|
svert.point = points[0] + old_vecs[0] * i / (first - 1)
|
|
svert.attribute.visible = (i != first - 1)
|
|
elif i < second:
|
|
svert.point = points[1] + old_vecs[1] * (i - first) / (second - first - 1)
|
|
svert.attribute.visible = (i != second - 1)
|
|
elif i < third:
|
|
svert.point = points[2] + old_vecs[2] * (i - second) / (third - second - 1)
|
|
svert.attribute.visible = (i != third - 1)
|
|
elif i < fourth:
|
|
svert.point = points[3] + old_vecs[3] * (i - third) / (fourth - third - 1)
|
|
svert.attribute.visible = (i != fourth - 1)
|
|
else:
|
|
# special case; remove these vertices
|
|
verticesToRemove.append(svert)
|
|
|
|
# remove excess vertices
|
|
if not it.is_end:
|
|
it.increment()
|
|
verticesToRemove += [svert for svert in it]
|
|
for sv in verticesToRemove:
|
|
stroke.remove_vertex(sv)
|
|
stroke.update_length()
|
|
|
|
|
|
# -- various (used in the parameter editor) -- #
|
|
|
|
|
|
def iter_stroke_vertices(stroke, epsilon=1e-6):
|
|
yield stroke[0]
|
|
for prev, svert in pairwise(stroke):
|
|
if (prev.point - svert.point).length > epsilon:
|
|
yield svert
|
|
|
|
|
|
class RoundCapShader(StrokeShader):
|
|
def round_cap_thickness(self, x):
|
|
x = max(0.0, min(x, 1.0))
|
|
return pow(1.0 - (x ** 2.0), 0.5)
|
|
|
|
def shade(self, stroke):
|
|
# save the location and attribute of stroke vertices
|
|
buffer = tuple((Vector(sv.point), StrokeAttribute(sv.attribute))
|
|
for sv in iter_stroke_vertices(stroke))
|
|
nverts = len(buffer)
|
|
if nverts < 2:
|
|
return
|
|
# calculate the number of additional vertices to form caps
|
|
thickness_beg = sum(stroke[0].attribute.thickness)
|
|
caplen_beg = thickness_beg / 2.0
|
|
nverts_beg = max(5, int(thickness_beg))
|
|
|
|
thickness_end = sum(stroke[-1].attribute.thickness)
|
|
caplen_end = (thickness_end) / 2.0
|
|
nverts_end = max(5, int(thickness_end))
|
|
|
|
# adjust the total number of stroke vertices
|
|
stroke.resample(nverts + nverts_beg + nverts_end)
|
|
# restore the location and attribute of the original vertices
|
|
for i, (p, attr) in enumerate(buffer):
|
|
stroke[nverts_beg + i].point = p
|
|
stroke[nverts_beg + i].attribute = attr
|
|
# reshape the cap at the beginning of the stroke
|
|
q, attr = buffer[1]
|
|
p, attr = buffer[0]
|
|
direction = (p - q).normalized() * caplen_beg
|
|
n = 1.0 / nverts_beg
|
|
R, L = attr.thickness
|
|
for t, svert in zip(range(nverts_beg, 0, -1), stroke):
|
|
r = self.round_cap_thickness((t + 1) * n)
|
|
svert.point = p + direction * t * n
|
|
svert.attribute = attr
|
|
svert.attribute.thickness = (R * r, L * r)
|
|
# reshape the cap at the end of the stroke
|
|
q, attr = buffer[-2]
|
|
p, attr = buffer[-1]
|
|
direction = (p - q).normalized() * caplen_beg
|
|
n = 1.0 / nverts_end
|
|
R, L = attr.thickness
|
|
for t, svert in zip(range(nverts_end, 0, -1), reversed(stroke)):
|
|
r = self.round_cap_thickness((t + 1) * n)
|
|
svert.point = p + direction * t * n
|
|
svert.attribute = attr
|
|
svert.attribute.thickness = (R * r, L * r)
|
|
# update the curvilinear 2D length of each vertex
|
|
stroke.update_length()
|
|
|
|
|
|
class SquareCapShader(StrokeShader):
|
|
def shade(self, stroke):
|
|
# save the location and attribute of stroke vertices
|
|
buffer = tuple((Vector(sv.point), StrokeAttribute(sv.attribute))
|
|
for sv in iter_stroke_vertices(stroke))
|
|
nverts = len(buffer)
|
|
if nverts < 2:
|
|
return
|
|
# calculate the number of additional vertices to form caps
|
|
caplen_beg = sum(stroke[0].attribute.thickness) / 2.0
|
|
nverts_beg = 1
|
|
|
|
caplen_end = sum(stroke[-1].attribute.thickness) / 2.0
|
|
nverts_end = 1
|
|
# adjust the total number of stroke vertices
|
|
stroke.resample(nverts + nverts_beg + nverts_end)
|
|
# restore the location and attribute of the original vertices
|
|
for i, (p, attr) in zip(range(nverts), buffer):
|
|
stroke[nverts_beg + i].point = p
|
|
stroke[nverts_beg + i].attribute = attr
|
|
# reshape the cap at the beginning of the stroke
|
|
q, attr = buffer[1]
|
|
p, attr = buffer[0]
|
|
stroke[0].point += (p - q).normalized() * caplen_beg
|
|
stroke[0].attribute = attr
|
|
# reshape the cap at the end of the stroke
|
|
q, attr = buffer[-2]
|
|
p, attr = buffer[-1]
|
|
stroke[-1].point += (p - q).normalized() * caplen_end
|
|
stroke[-1].attribute = attr
|
|
# update the curvilinear 2D length of each vertex
|
|
stroke.update_length()
|