1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19#  Filename : parameter_editor.py
20#  Authors  : Tamito Kajiyama
21#  Date     : 26/07/2010
22#  Purpose  : Interactive manipulation of stylization parameters
23
24from freestyle.types import (
25    BinaryPredicate1D,
26    IntegrationType,
27    Interface0DIterator,
28    Nature,
29    Noise,
30    Operators,
31    StrokeAttribute,
32    UnaryPredicate0D,
33    UnaryPredicate1D,
34    TVertex,
35    Material,
36    ViewEdge,
37)
38from freestyle.chainingiterators import (
39    ChainPredicateIterator,
40    ChainSilhouetteIterator,
41    pySketchyChainSilhouetteIterator,
42    pySketchyChainingIterator,
43)
44from freestyle.functions import (
45    Curvature2DAngleF0D,
46    Normal2DF0D,
47    QuantitativeInvisibilityF1D,
48    VertexOrientation2DF0D,
49    CurveMaterialF0D,
50)
51from freestyle.predicates import (
52    AndUP1D,
53    ContourUP1D,
54    ExternalContourUP1D,
55    FalseBP1D,
56    FalseUP1D,
57    Length2DBP1D,
58    NotBP1D,
59    NotUP1D,
60    OrUP1D,
61    QuantitativeInvisibilityUP1D,
62    TrueBP1D,
63    TrueUP1D,
64    WithinImageBoundaryUP1D,
65    pyNFirstUP1D,
66    pyNatureUP1D,
67    pyProjectedXBP1D,
68    pyProjectedYBP1D,
69    pyZBP1D,
70)
71from freestyle.shaders import (
72    BackboneStretcherShader,
73    BezierCurveShader,
74    BlenderTextureShader,
75    ConstantColorShader,
76    GuidingLinesShader,
77    PolygonalizationShader,
78    pyBluePrintCirclesShader,
79    pyBluePrintEllipsesShader,
80    pyBluePrintSquaresShader,
81    RoundCapShader,
82    SamplingShader,
83    SpatialNoiseShader,
84    SquareCapShader,
85    StrokeShader,
86    StrokeTextureStepShader,
87    ThicknessNoiseShader as thickness_noise,
88    TipRemoverShader,
89)
90from freestyle.utils import (
91    angle_x_normal,
92    bound,
93    BoundedProperty,
94    ContextFunctions,
95    curvature_from_stroke_vertex,
96    getCurrentScene,
97    iter_distance_along_stroke,
98    iter_distance_from_camera,
99    iter_distance_from_object,
100    iter_material_value,
101    iter_t2d_along_stroke,
102    normal_at_I0D,
103    pairwise,
104    simplify,
105    stroke_normal,
106)
107from _freestyle import (
108    blendRamp,
109    evaluateColorRamp,
110    evaluateCurveMappingF,
111)
112
113import time
114import bpy
115import random
116
117from mathutils import Vector
118from math import pi, sin, cos, acos, radians, atan2
119from itertools import cycle, tee
120
121# WARNING: highly experimental, not a stable API
122# lists of callback functions
123# used by the render_freestyle_svg addon
124callbacks_lineset_pre = []
125callbacks_modifiers_post = []
126callbacks_lineset_post = []
127
128
129class ColorRampModifier(StrokeShader):
130    """Primitive for the color modifiers."""
131
132    def __init__(self, blend, influence, ramp):
133        StrokeShader.__init__(self)
134        self.blend = blend
135        self.influence = influence
136        self.ramp = ramp
137
138    def evaluate(self, t):
139        col = evaluateColorRamp(self.ramp, t)
140        return col.xyz  # omit alpha
141
142    def blend_ramp(self, a, b):
143        return blendRamp(self.blend, a, self.influence, b)
144
145
146class ScalarBlendModifier(StrokeShader):
147    """Primitive for alpha and thickness modifiers."""
148
149    def __init__(self, blend_type, influence):
150        StrokeShader.__init__(self)
151        self.blend_type = blend_type
152        self.influence = influence
153
154    def blend(self, v1, v2):
155        fac = self.influence
156        facm = 1.0 - fac
157        if self.blend_type == 'MIX':
158            v1 = facm * v1 + fac * v2
159        elif self.blend_type == 'ADD':
160            v1 += fac * v2
161        elif self.blend_type == 'MULTIPLY':
162            v1 *= facm + fac * v2
163        elif self.blend_type == 'SUBTRACT':
164            v1 -= fac * v2
165        elif self.blend_type == 'DIVIDE':
166            v1 = facm * v1 + fac * v1 / v2 if v2 != 0.0 else v1
167        elif self.blend_type == 'DIFFERENCE':
168            v1 = facm * v1 + fac * abs(v1 - v2)
169        elif self.blend_type == 'MININUM':
170            v1 = min(fac * v2, v1)
171        elif self.blend_type == 'MAXIMUM':
172            v1 = max(fac * v2, v1)
173        else:
174            raise ValueError("unknown curve blend type: " + self.blend_type)
175        return v1
176
177
178class CurveMappingModifier(ScalarBlendModifier):
179    def __init__(self, blend, influence, mapping, invert, curve):
180        ScalarBlendModifier.__init__(self, blend, influence)
181        assert mapping in {'LINEAR', 'CURVE'}
182        self.evaluate = getattr(self, mapping)
183        self.invert = invert
184        self.curve = curve
185
186    def LINEAR(self, t):
187        return (1.0 - t) if self.invert else t
188
189    def CURVE(self, t):
190        # deprecated: return evaluateCurveMappingF(self.curve, 0, t)
191        curve = self.curve
192        curve.initialize()
193        result = curve.evaluate(curve=curve.curves[0], position=t)
194        # float precision errors in t can give a very weird result for evaluate.
195        # therefore, bound the result by the curve's min and max values
196        return bound(curve.clip_min_y, result, curve.clip_max_y)
197
198
199class ThicknessModifierMixIn:
200    def __init__(self):
201        scene = getCurrentScene()
202        self.persp_camera = (scene.camera.data.type == 'PERSP')
203
204    def set_thickness(self, sv, outer, inner):
205        fe = sv.fedge
206        nature = fe.nature
207        if (nature & Nature.BORDER):
208            if self.persp_camera:
209                point = -sv.point_3d.normalized()
210                dir = point.dot(fe.normal_left)
211            else:
212                dir = fe.normal_left.z
213            if dir < 0.0:  # the back side is visible
214                outer, inner = inner, outer
215        elif (nature & Nature.SILHOUETTE):
216            if fe.is_smooth:  # TODO more tests needed
217                outer, inner = inner, outer
218        else:
219            outer = inner = (outer + inner) / 2
220        sv.attribute.thickness = (outer, inner)
221
222
223class ThicknessBlenderMixIn(ThicknessModifierMixIn):
224    def __init__(self, position, ratio):
225        ThicknessModifierMixIn.__init__(self)
226        self.position = position
227        self.ratio = ratio
228
229    def blend_thickness(self, svert, thickness, asymmetric=False):
230        """Blends and sets the thickness with respect to the position, blend mode and symmetry."""
231        if asymmetric:
232            right, left = thickness
233            self.blend_thickness_asymmetric(svert, right, left)
234        else:
235            if type(thickness) not in {int, float}:
236                thickness = sum(thickness)
237            self.blend_thickness_symmetric(svert, thickness)
238
239    def blend_thickness_symmetric(self, svert, v):
240        """Blends and sets the thickness. Thickness is equal on each side of the backbone"""
241        outer, inner = svert.attribute.thickness
242        v = self.blend(outer + inner, v)
243
244        # Part 1: blend
245        if self.position == 'CENTER':
246            outer = inner = v * 0.5
247        elif self.position == 'INSIDE':
248            outer, inner = 0, v
249        elif self.position == 'OUTSIDE':
250            outer, inner = v, 0
251        elif self.position == 'RELATIVE':
252            outer, inner = v * self.ratio, v - (v * self.ratio)
253        else:
254            raise ValueError("unknown thickness position: " + position)
255
256        self.set_thickness(svert, outer, inner)
257
258    def blend_thickness_asymmetric(self, svert, right, left):
259        """Blends and sets the thickness. Thickness may be unequal on each side of the backbone"""
260        # blend the thickness values for both sides. This way, the blend mode is supported.
261        old = svert.attribute.thickness
262        new = (right, left)
263        right, left = (self.blend(*val) for val in zip(old, new))
264
265        fe = svert.fedge
266        nature = fe.nature
267        if (nature & Nature.BORDER):
268            if self.persp_camera:
269                point = -svert.point_3d.normalized()
270                dir = point.dot(fe.normal_left)
271            else:
272                dir = fe.normal_left.z
273            if dir < 0.0:  # the back side is visible
274                right, left = left, right
275        elif (nature & Nature.SILHOUETTE):
276            if fe.is_smooth:  # TODO more tests needed
277                right, left = left, right
278        svert.attribute.thickness = (right, left)
279
280
281class BaseThicknessShader(StrokeShader, ThicknessModifierMixIn):
282    def __init__(self, thickness, position, ratio):
283        StrokeShader.__init__(self)
284        ThicknessModifierMixIn.__init__(self)
285        if position == 'CENTER':
286            self.outer = thickness * 0.5
287            self.inner = thickness - self.outer
288        elif position == 'INSIDE':
289            self.outer = 0
290            self.inner = thickness
291        elif position == 'OUTSIDE':
292            self.outer = thickness
293            self.inner = 0
294        elif position == 'RELATIVE':
295            self.outer = thickness * ratio
296            self.inner = thickness - self.outer
297        else:
298            raise ValueError("unknown thickness position: " + position)
299
300    def shade(self, stroke):
301        for svert in stroke:
302            self.set_thickness(svert, self.outer, self.inner)
303
304
305# Along Stroke modifiers
306
307class ColorAlongStrokeShader(ColorRampModifier):
308    """Maps a ramp to the color of the stroke, using the curvilinear abscissa (t)."""
309
310    def shade(self, stroke):
311        for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
312            a = svert.attribute.color
313            b = self.evaluate(t)
314            svert.attribute.color = self.blend_ramp(a, b)
315
316
317class AlphaAlongStrokeShader(CurveMappingModifier):
318    """Maps a curve to the alpha/transparency of the stroke, using the curvilinear abscissa (t)."""
319
320    def shade(self, stroke):
321        for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
322            a = svert.attribute.alpha
323            b = self.evaluate(t)
324            svert.attribute.alpha = self.blend(a, b)
325
326
327class ThicknessAlongStrokeShader(ThicknessBlenderMixIn, CurveMappingModifier):
328    """Maps a curve to the thickness of the stroke, using the curvilinear abscissa (t)."""
329
330    def __init__(self, thickness_position, thickness_ratio,
331                 blend, influence, mapping, invert, curve, value_min, value_max):
332        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
333        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
334        self.value = BoundedProperty(value_min, value_max)
335
336    def shade(self, stroke):
337        for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
338            b = self.value.min + self.evaluate(t) * self.value.delta
339            self.blend_thickness(svert, b)
340
341
342# -- Distance from Camera modifiers -- #
343
344class ColorDistanceFromCameraShader(ColorRampModifier):
345    """Picks a color value from a ramp based on the vertex' distance from the camera."""
346
347    def __init__(self, blend, influence, ramp, range_min, range_max):
348        ColorRampModifier.__init__(self, blend, influence, ramp)
349        self.range = BoundedProperty(range_min, range_max)
350
351    def shade(self, stroke):
352        it = iter_distance_from_camera(stroke, *self.range)
353        for svert, t in it:
354            a = svert.attribute.color
355            b = self.evaluate(t)
356            svert.attribute.color = self.blend_ramp(a, b)
357
358
359class AlphaDistanceFromCameraShader(CurveMappingModifier):
360    """Picks an alpha value from a curve based on the vertex' distance from the camera"""
361
362    def __init__(self, blend, influence, mapping, invert, curve, range_min, range_max):
363        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
364        self.range = BoundedProperty(range_min, range_max)
365
366    def shade(self, stroke):
367        it = iter_distance_from_camera(stroke, *self.range)
368        for svert, t in it:
369            a = svert.attribute.alpha
370            b = self.evaluate(t)
371            svert.attribute.alpha = self.blend(a, b)
372
373
374class ThicknessDistanceFromCameraShader(ThicknessBlenderMixIn, CurveMappingModifier):
375    """Picks a thickness value from a curve based on the vertex' distance from the camera."""
376
377    def __init__(self, thickness_position, thickness_ratio,
378                 blend, influence, mapping, invert, curve, range_min, range_max, value_min, value_max):
379        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
380        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
381        self.range = BoundedProperty(range_min, range_max)
382        self.value = BoundedProperty(value_min, value_max)
383
384    def shade(self, stroke):
385        for (svert, t) in iter_distance_from_camera(stroke, *self.range):
386            b = self.value.min + self.evaluate(t) * self.value.delta
387            self.blend_thickness(svert, b)
388
389
390# Distance from Object modifiers
391
392class ColorDistanceFromObjectShader(ColorRampModifier):
393    """Picks a color value from a ramp based on the vertex' distance from a given object."""
394
395    def __init__(self, blend, influence, ramp, target, range_min, range_max):
396        ColorRampModifier.__init__(self, blend, influence, ramp)
397        if target is None:
398            raise ValueError("ColorDistanceFromObjectShader: target can't be None ")
399        self.range = BoundedProperty(range_min, range_max)
400        # construct a model-view matrix
401        matrix = getCurrentScene().camera.matrix_world.inverted()
402        # get the object location in the camera coordinate
403        self.loc = matrix @ target.location
404
405    def shade(self, stroke):
406        it = iter_distance_from_object(stroke, self.loc, *self.range)
407        for svert, t in it:
408            a = svert.attribute.color
409            b = self.evaluate(t)
410            svert.attribute.color = self.blend_ramp(a, b)
411
412
413class AlphaDistanceFromObjectShader(CurveMappingModifier):
414    """Picks an alpha value from a curve based on the vertex' distance from a given object."""
415
416    def __init__(self, blend, influence, mapping, invert, curve, target, range_min, range_max):
417        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
418        if target is None:
419            raise ValueError("AlphaDistanceFromObjectShader: target can't be None ")
420        self.range = BoundedProperty(range_min, range_max)
421        # construct a model-view matrix
422        matrix = getCurrentScene().camera.matrix_world.inverted()
423        # get the object location in the camera coordinate
424        self.loc = matrix @ target.location
425
426    def shade(self, stroke):
427        it = iter_distance_from_object(stroke, self.loc, *self.range)
428        for svert, t in it:
429            a = svert.attribute.alpha
430            b = self.evaluate(t)
431            svert.attribute.alpha = self.blend(a, b)
432
433
434class ThicknessDistanceFromObjectShader(ThicknessBlenderMixIn, CurveMappingModifier):
435    """Picks a thickness value from a curve based on the vertex' distance from a given object."""
436
437    def __init__(self, thickness_position, thickness_ratio,
438                 blend, influence, mapping, invert, curve, target, range_min, range_max, value_min, value_max):
439        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
440        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
441        if target is None:
442            raise ValueError("ThicknessDistanceFromObjectShader: target can't be None ")
443        self.range = BoundedProperty(range_min, range_max)
444        self.value = BoundedProperty(value_min, value_max)
445        # construct a model-view matrix
446        matrix = getCurrentScene().camera.matrix_world.inverted()
447        # get the object location in the camera coordinate
448        self.loc = matrix @ target.location
449
450    def shade(self, stroke):
451        it = iter_distance_from_object(stroke, self.loc, *self.range)
452        for svert, t in it:
453            b = self.value.min + self.evaluate(t) * self.value.delta
454            self.blend_thickness(svert, b)
455
456
457# Material modifiers
458class ColorMaterialShader(ColorRampModifier):
459    """Assigns a color to the vertices based on their underlying material."""
460
461    def __init__(self, blend, influence, ramp, material_attribute, use_ramp):
462        ColorRampModifier.__init__(self, blend, influence, ramp)
463        self.attribute = material_attribute
464        self.use_ramp = use_ramp
465        self.func = CurveMaterialF0D()
466
467    def shade(self, stroke, attributes={'DIFF', 'SPEC', 'LINE'}):
468        it = Interface0DIterator(stroke)
469        if not self.use_ramp and self.attribute in attributes:
470            for svert in it:
471                material = self.func(it)
472                if self.attribute == 'LINE':
473                    b = material.line[0:3]
474                elif self.attribute == 'DIFF':
475                    b = material.diffuse[0:3]
476                else:
477                    b = material.specular[0:3]
478                a = svert.attribute.color
479                svert.attribute.color = self.blend_ramp(a, b)
480        else:
481            for svert, value in iter_material_value(stroke, self.func, self.attribute):
482                a = svert.attribute.color
483                b = self.evaluate(value)
484                svert.attribute.color = self.blend_ramp(a, b)
485
486
487class AlphaMaterialShader(CurveMappingModifier):
488    """Assigns an alpha value to the vertices based on their underlying material."""
489
490    def __init__(self, blend, influence, mapping, invert, curve, material_attribute):
491        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
492        self.attribute = material_attribute
493        self.func = CurveMaterialF0D()
494
495    def shade(self, stroke):
496        for svert, value in iter_material_value(stroke, self.func, self.attribute):
497            a = svert.attribute.alpha
498            b = self.evaluate(value)
499            svert.attribute.alpha = self.blend(a, b)
500
501
502class ThicknessMaterialShader(ThicknessBlenderMixIn, CurveMappingModifier):
503    """Assigns a thickness value to the vertices based on their underlying material."""
504
505    def __init__(self, thickness_position, thickness_ratio,
506                 blend, influence, mapping, invert, curve, material_attribute, value_min, value_max):
507        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
508        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
509        self.attribute = material_attribute
510        self.value = BoundedProperty(value_min, value_max)
511        self.func = CurveMaterialF0D()
512
513    def shade(self, stroke):
514        for svert, value in iter_material_value(stroke, self.func, self.attribute):
515            b = self.value.min + self.evaluate(value) * self.value.delta
516            self.blend_thickness(svert, b)
517
518
519# Calligraphic thickness modifier
520
521class CalligraphicThicknessShader(ThicknessBlenderMixIn, ScalarBlendModifier):
522    """Thickness modifier for achieving a calligraphy-like effect."""
523
524    def __init__(self, thickness_position, thickness_ratio,
525                 blend_type, influence, orientation, thickness_min, thickness_max):
526        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
527        ScalarBlendModifier.__init__(self, blend_type, influence)
528        self.orientation = Vector((cos(orientation), sin(orientation)))
529        self.thickness = BoundedProperty(thickness_min, thickness_max)
530        self.func = VertexOrientation2DF0D()
531
532    def shade(self, stroke):
533        it = Interface0DIterator(stroke)
534        for svert in it:
535            dir = self.func(it)
536            if dir.length != 0.0:
537                dir.normalize()
538                fac = abs(dir.orthogonal() @ self.orientation)
539                b = self.thickness.min + fac * self.thickness.delta
540            else:
541                b = self.thickness.min
542            self.blend_thickness(svert, b)
543
544
545# - Tangent Modifiers - #
546
547class TangentColorShader(ColorRampModifier):
548    """Color based on the direction of the stroke"""
549
550    def shade(self, stroke):
551        it = Interface0DIterator(stroke)
552        for svert in it:
553            angle = angle_x_normal(it)
554            fac = self.evaluate(angle / pi)
555            a = svert.attribute.color
556            svert.attribute.color = self.blend_ramp(a, fac)
557
558
559class TangentAlphaShader(CurveMappingModifier):
560    """Alpha transparency based on the direction of the stroke"""
561
562    def shade(self, stroke):
563        it = Interface0DIterator(stroke)
564        for svert in it:
565            angle = angle_x_normal(it)
566            fac = self.evaluate(angle / pi)
567            a = svert.attribute.alpha
568            svert.attribute.alpha = self.blend(a, fac)
569
570
571class TangentThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier):
572    """Thickness based on the direction of the stroke"""
573
574    def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve,
575                 thickness_min, thickness_max):
576        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
577        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
578        self.thickness = BoundedProperty(thickness_min, thickness_max)
579
580    def shade(self, stroke):
581        it = Interface0DIterator(stroke)
582        for svert in it:
583            angle = angle_x_normal(it)
584            thickness = self.thickness.min + self.evaluate(angle / pi) * self.thickness.delta
585            self.blend_thickness(svert, thickness)
586
587
588# - Noise Modifiers - #
589
590class NoiseShader:
591    """Base class for noise shaders"""
592
593    def __init__(self, amplitude, period, seed=512):
594        self.amplitude = amplitude
595        self.scale = 1 / period / seed
596        self.seed = seed
597
598    def noisegen(self, stroke, n1=Noise(), n2=Noise()):
599        """Produces two noise values per StrokeVertex for every vertex in the stroke"""
600        initU1 = stroke.length_2d * self.seed + n1.rand(512) * self.seed
601        initU2 = stroke.length_2d * self.seed + n2.rand() * self.seed
602
603        for svert in stroke:
604            a = n1.turbulence_smooth(self.scale * svert.curvilinear_abscissa + initU1, 2)
605            b = n2.turbulence_smooth(self.scale * svert.curvilinear_abscissa + initU2, 2)
606            yield (svert, a, b)
607
608
609class ThicknessNoiseShader(ThicknessBlenderMixIn, ScalarBlendModifier, NoiseShader):
610    """Thickness based on pseudo-noise"""
611
612    def __init__(self, thickness_position, thickness_ratio, blend_type,
613                 influence, amplitude, period, seed=512, asymmetric=True):
614        ScalarBlendModifier.__init__(self, blend_type, influence)
615        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
616        NoiseShader.__init__(self, amplitude, period, seed)
617        self.asymmetric = asymmetric
618
619    def shade(self, stroke):
620        for svert, noiseval1, noiseval2 in self.noisegen(stroke):
621            (r, l) = svert.attribute.thickness
622            l += noiseval1 * self.amplitude
623            r += noiseval2 * self.amplitude
624            self.blend_thickness(svert, (r, l), self.asymmetric)
625
626
627class ColorNoiseShader(ColorRampModifier, NoiseShader):
628    """Color based on pseudo-noise"""
629
630    def __init__(self, blend, influence, ramp, amplitude, period, seed=512):
631        ColorRampModifier.__init__(self, blend, influence, ramp)
632        NoiseShader.__init__(self, amplitude, period, seed)
633
634    def shade(self, stroke):
635        for svert, noiseval1, noiseval2 in self.noisegen(stroke):
636            position = abs(noiseval1 + noiseval2)
637            svert.attribute.color = self.blend_ramp(svert.attribute.color, self.evaluate(position))
638
639
640class AlphaNoiseShader(CurveMappingModifier, NoiseShader):
641    """Alpha transparency on based pseudo-noise"""
642
643    def __init__(self, blend, influence, mapping, invert, curve, amplitude, period, seed=512):
644        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
645        NoiseShader.__init__(self, amplitude, period, seed)
646
647    def shade(self, stroke, n1=Noise(), n2=Noise()):
648        for svert, noiseval1, noiseval2 in self.noisegen(stroke):
649            position = abs(noiseval1 + noiseval2)
650            svert.attribute.alpha = self.blend(svert.attribute.alpha, self.evaluate(position))
651
652
653# - Crease Angle Modifiers - #
654
655def crease_angle(svert):
656    """Returns the crease angle between the StrokeVertex' two adjacent faces (in radians)"""
657    fe = svert.fedge
658    if not fe or fe.is_smooth or not (fe.nature & Nature.CREASE):
659        return None
660    # make sure that the input is within the domain of the acos function
661    product = bound(-1.0, -fe.normal_left.dot(fe.normal_right), 1.0)
662    return acos(product)
663
664
665class CreaseAngleColorShader(ColorRampModifier):
666    """Color based on the crease angle between two adjacent faces on the underlying geometry"""
667
668    def __init__(self, blend, influence, ramp, angle_min, angle_max):
669        ColorRampModifier.__init__(self, blend, influence, ramp)
670        # angles are (already) in radians
671        self.angle = BoundedProperty(angle_min, angle_max)
672
673    def shade(self, stroke):
674        for svert in stroke:
675            angle = crease_angle(svert)
676            if angle is None:
677                continue
678            t = self.angle.interpolate(angle)
679            svert.attribute.color = self.blend_ramp(svert.attribute.color, self.evaluate(t))
680
681
682class CreaseAngleAlphaShader(CurveMappingModifier):
683    """Alpha transparency based on the crease angle between two adjacent faces on the underlying geometry"""
684
685    def __init__(self, blend, influence, mapping, invert, curve, angle_min, angle_max):
686        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
687        # angles are (already) in radians
688        self.angle = BoundedProperty(angle_min, angle_max)
689
690    def shade(self, stroke):
691        for svert in stroke:
692            angle = crease_angle(svert)
693            if angle is None:
694                continue
695            t = self.angle.interpolate(angle)
696            svert.attribute.alpha = self.blend(svert.attribute.alpha, self.evaluate(t))
697
698
699class CreaseAngleThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier):
700    """Thickness based on the crease angle between two adjacent faces on the underlying geometry"""
701
702    def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve,
703                 angle_min, angle_max, thickness_min, thickness_max):
704        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
705        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
706        # angles are (already) in radians
707        self.angle = BoundedProperty(angle_min, angle_max)
708        self.thickness = BoundedProperty(thickness_min, thickness_max)
709
710    def shade(self, stroke):
711        for svert in stroke:
712            angle = crease_angle(svert)
713            if angle is None:
714                continue
715            t = self.angle.interpolate(angle)
716            thickness = self.thickness.min + self.evaluate(t) * self.thickness.delta
717            self.blend_thickness(svert, thickness)
718
719
720# - Curvature3D Modifiers - #
721
722def normalized_absolute_curvature(svert, bounded_curvature):
723    """
724    Gives the absolute curvature in range [0, 1].
725
726    The actual curvature (Kr) value can be anywhere in the range [-inf, inf], where convex curvature
727    yields a positive value, and concave a negative one. These shaders only look for the magnitude
728    of the 3D curvature, hence the abs()
729    """
730    curvature = curvature_from_stroke_vertex(svert)
731    if curvature is None:
732        return 0.0
733    return bounded_curvature.interpolate(abs(curvature))
734
735
736class Curvature3DColorShader(ColorRampModifier):
737    """Color based on the 3D curvature of the underlying geometry"""
738
739    def __init__(self, blend, influence, ramp, curvature_min, curvature_max):
740        ColorRampModifier.__init__(self, blend, influence, ramp)
741        self.curvature = BoundedProperty(curvature_min, curvature_max)
742
743    def shade(self, stroke):
744        for svert in stroke:
745            t = normalized_absolute_curvature(svert, self.curvature)
746            a = svert.attribute.color
747            b = self.evaluate(t)
748            svert.attribute.color = self.blend_ramp(a, b)
749
750
751class Curvature3DAlphaShader(CurveMappingModifier):
752    """Alpha based on the 3D curvature of the underlying geometry"""
753
754    def __init__(self, blend, influence, mapping, invert, curve, curvature_min, curvature_max):
755        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
756        self.curvature = BoundedProperty(curvature_min, curvature_max)
757
758    def shade(self, stroke):
759        for svert in stroke:
760            t = normalized_absolute_curvature(svert, self.curvature)
761            a = svert.attribute.alpha
762            b = self.evaluate(t)
763            svert.attribute.alpha = self.blend(a, b)
764
765
766class Curvature3DThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier):
767    """Alpha based on the 3D curvature of the underlying geometry"""
768
769    def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve,
770                 curvature_min, curvature_max, thickness_min, thickness_max):
771        ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
772        CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
773        self.curvature = BoundedProperty(curvature_min, curvature_max)
774        self.thickness = BoundedProperty(thickness_min, thickness_max)
775
776    def shade(self, stroke):
777        for svert in stroke:
778            t = normalized_absolute_curvature(svert, self.curvature)
779            thickness = self.thickness.min + self.evaluate(t) * self.thickness.delta
780            self.blend_thickness(svert, thickness)
781
782
783# Geometry modifiers
784
785class SimplificationShader(StrokeShader):
786    """Simplifies a stroke by merging points together"""
787
788    def __init__(self, tolerance):
789        StrokeShader.__init__(self)
790        self.tolerance = tolerance
791
792    def shade(self, stroke):
793        points = tuple(svert.point for svert in stroke)
794        points_simplified = simplify(points, tolerance=self.tolerance)
795
796        it = iter(stroke)
797        for svert, point in zip(it, points_simplified):
798            svert.point = point
799
800        for svert in tuple(it):
801            stroke.remove_vertex(svert)
802
803
804class SinusDisplacementShader(StrokeShader):
805    """Displaces the stroke in a sine wave-like shape."""
806
807    def __init__(self, wavelength, amplitude, phase):
808        StrokeShader.__init__(self)
809        self.wavelength = wavelength
810        self.amplitude = amplitude
811        self.phase = phase / wavelength * 2 * pi
812
813    def shade(self, stroke):
814        # normals are stored in a tuple, so they don't update when we reposition vertices.
815        normals = tuple(stroke_normal(stroke))
816        distances = iter_distance_along_stroke(stroke)
817        coeff = 1 / self.wavelength * 2 * pi
818        for svert, distance, normal in zip(stroke, distances, normals):
819            n = normal * self.amplitude * cos(distance * coeff + self.phase)
820            svert.point += n
821        stroke.update_length()
822
823
824class PerlinNoise1DShader(StrokeShader):
825    """
826    Displaces the stroke using the curvilinear abscissa.  This means
827    that lines with the same length and sampling interval will be
828    identically distorded.
829    """
830
831    def __init__(self, freq=10, amp=10, oct=4, angle=radians(45), seed=-1):
832        StrokeShader.__init__(self)
833        self.noise = Noise(seed)
834        self.freq = freq
835        self.amp = amp
836        self.oct = oct
837        self.dir = Vector((cos(angle), sin(angle)))
838
839    def shade(self, stroke):
840        length = stroke.length_2d
841        for svert in stroke:
842            nres = self.noise.turbulence1(length * svert.u, self.freq, self.amp, self.oct)
843            svert.point += nres * self.dir
844        stroke.update_length()
845
846
847class PerlinNoise2DShader(StrokeShader):
848    """
849    Displaces the stroke using the strokes coordinates.  This means
850    that in a scene no strokes will be distorted identically.
851
852    More information on the noise shaders can be found at:
853    freestyleintegration.wordpress.com/2011/09/25/development-updates-on-september-25/
854    """
855
856    def __init__(self, freq=10, amp=10, oct=4, angle=radians(45), seed=-1):
857        StrokeShader.__init__(self)
858        self.noise = Noise(seed)
859        self.freq = freq
860        self.amp = amp
861        self.oct = oct
862        self.dir = Vector((cos(angle), sin(angle)))
863
864    def shade(self, stroke):
865        for svert in stroke:
866            projected = Vector((svert.projected_x, svert.projected_y))
867            nres = self.noise.turbulence2(projected, self.freq, self.amp, self.oct)
868            svert.point += nres * self.dir
869        stroke.update_length()
870
871
872class Offset2DShader(StrokeShader):
873    """Offsets the stroke by a given amount."""
874
875    def __init__(self, start, end, x, y):
876        StrokeShader.__init__(self)
877        self.start = start
878        self.end = end
879        self.xy = Vector((x, y))
880
881    def shade(self, stroke):
882        # normals are stored in a tuple, so they don't update when we reposition vertices.
883        normals = tuple(stroke_normal(stroke))
884        for svert, normal in zip(stroke, normals):
885            a = self.start + svert.u * (self.end - self.start)
886            svert.point += (normal * a) + self.xy
887        stroke.update_length()
888
889
890class Transform2DShader(StrokeShader):
891    """Transforms the stroke (scale, rotation, location) around a given pivot point """
892
893    def __init__(self, pivot, scale_x, scale_y, angle, pivot_u, pivot_x, pivot_y):
894        StrokeShader.__init__(self)
895        self.pivot = pivot
896        self.scale = Vector((scale_x, scale_y))
897        self.cos_theta = cos(angle)
898        self.sin_theta = sin(angle)
899        self.pivot_u = pivot_u
900        self.pivot_x = pivot_x
901        self.pivot_y = pivot_y
902        if pivot not in {'START', 'END', 'CENTER', 'ABSOLUTE', 'PARAM'}:
903            raise ValueError("expected pivot in {'START', 'END', 'CENTER', 'ABSOLUTE', 'PARAM'}, not" + pivot)
904
905    def shade(self, stroke):
906        # determine the pivot of scaling and rotation operations
907        if self.pivot == 'START':
908            pivot = stroke[0].point
909        elif self.pivot == 'END':
910            pivot = stroke[-1].point
911        elif self.pivot == 'CENTER':
912            # minor rounding errors here, because
913            # given v = Vector(a, b), then (v / n) != Vector(v.x / n, v.y / n)
914            pivot = (1 / len(stroke)) * sum((svert.point for svert in stroke), Vector((0.0, 0.0)))
915        elif self.pivot == 'ABSOLUTE':
916            pivot = Vector((self.pivot_x, self.pivot_y))
917        elif self.pivot == 'PARAM':
918            if self.pivot_u < stroke[0].u:
919                pivot = stroke[0].point
920            else:
921                for prev, svert in pairwise(stroke):
922                    if self.pivot_u < svert.u:
923                        break
924                pivot = svert.point + (svert.u - self.pivot_u) * (prev.point - svert.point)
925
926        # apply scaling and rotation operations
927        for svert in stroke:
928            p = (svert.point - pivot)
929            x = p.x * self.scale.x
930            y = p.y * self.scale.y
931            p.x = x * self.cos_theta - y * self.sin_theta
932            p.y = x * self.sin_theta + y * self.cos_theta
933            svert.point = p + pivot
934        stroke.update_length()
935
936
937# Predicates and helper functions
938
939class QuantitativeInvisibilityRangeUP1D(UnaryPredicate1D):
940    def __init__(self, qi_start, qi_end):
941        UnaryPredicate1D.__init__(self)
942        self.getQI = QuantitativeInvisibilityF1D()
943        self.qi_start = qi_start
944        self.qi_end = qi_end
945
946    def __call__(self, inter):
947        qi = self.getQI(inter)
948        return self.qi_start <= qi <= self.qi_end
949
950
951def getQualifiedObjectName(ob):
952    if ob.library is not None:
953        return ob.library.filepath + '/' + ob.name
954    return ob.name
955
956
957class ObjectNamesUP1D(UnaryPredicate1D):
958    def __init__(self, names, negative):
959        UnaryPredicate1D.__init__(self)
960        self.names = names
961        self.negative = negative
962
963    def getViewShapeName(self, vs):
964        if vs.library_path is not None and len(vs.library_path):
965            return vs.library_path + '/' + vs.name
966        return vs.name
967
968    def __call__(self, viewEdge):
969        found = self.getViewShapeName(viewEdge.viewshape) in self.names
970        if self.negative:
971            return not found
972        return found
973
974
975# -- Split by dashed line pattern -- #
976
977class SplitPatternStartingUP0D(UnaryPredicate0D):
978    def __init__(self, controller):
979        UnaryPredicate0D.__init__(self)
980        self.controller = controller
981
982    def __call__(self, inter):
983        return self.controller.start()
984
985
986class SplitPatternStoppingUP0D(UnaryPredicate0D):
987    def __init__(self, controller):
988        UnaryPredicate0D.__init__(self)
989        self.controller = controller
990
991    def __call__(self, inter):
992        return self.controller.stop()
993
994
995class SplitPatternController:
996    def __init__(self, pattern, sampling):
997        self.sampling = float(sampling)
998        k = len(pattern) // 2
999        n = k * 2
1000        self.start_pos = [pattern[i] + pattern[i + 1] for i in range(0, n, 2)]
1001        self.stop_pos = [pattern[i] for i in range(0, n, 2)]
1002        self.init()
1003
1004    def init(self):
1005        self.start_len = 0.0
1006        self.start_idx = 0
1007        self.stop_len = self.sampling
1008        self.stop_idx = 0
1009
1010    def start(self):
1011        self.start_len += self.sampling
1012        if abs(self.start_len - self.start_pos[self.start_idx]) < self.sampling / 2.0:
1013            self.start_len = 0.0
1014            self.start_idx = (self.start_idx + 1) % len(self.start_pos)
1015            return True
1016        return False
1017
1018    def stop(self):
1019        if self.start_len > 0.0:
1020            self.init()
1021        self.stop_len += self.sampling
1022        if abs(self.stop_len - self.stop_pos[self.stop_idx]) < self.sampling / 2.0:
1023            self.stop_len = self.sampling
1024            self.stop_idx = (self.stop_idx + 1) % len(self.stop_pos)
1025            return True
1026        return False
1027
1028
1029# Dashed line
1030
1031class DashedLineShader(StrokeShader):
1032    def __init__(self, pattern):
1033        StrokeShader.__init__(self)
1034        self.pattern = pattern
1035
1036    def shade(self, stroke):
1037        start = 0.0  # 2D curvilinear length
1038        visible = True
1039        # The extra 'sampling' term is added below, because the
1040        # visibility attribute of the i-th vertex refers to the
1041        # visibility of the stroke segment between the i-th and
1042        # (i+1)-th vertices.
1043        sampling = 1.0
1044        it = stroke.stroke_vertices_begin(sampling)
1045        pattern_cycle = cycle(self.pattern)
1046        pattern = next(pattern_cycle)
1047        for svert in it:
1048            pos = it.t  # curvilinear abscissa
1049
1050            if pos - start + sampling > pattern:
1051                start = pos
1052                pattern = next(pattern_cycle)
1053                visible = not visible
1054
1055            if not visible:
1056                it.object.attribute.visible = False
1057
1058
1059# predicates for chaining
1060
1061class AngleLargerThanBP1D(BinaryPredicate1D):
1062    def __init__(self, angle):
1063        BinaryPredicate1D.__init__(self)
1064        self.angle = angle
1065
1066    def __call__(self, i1, i2):
1067        sv1a = i1.first_fedge.first_svertex.point_2d
1068        sv1b = i1.last_fedge.second_svertex.point_2d
1069        sv2a = i2.first_fedge.first_svertex.point_2d
1070        sv2b = i2.last_fedge.second_svertex.point_2d
1071        if (sv1a - sv2a).length < 1e-6:
1072            dir1 = sv1a - sv1b
1073            dir2 = sv2b - sv2a
1074        elif (sv1b - sv2b).length < 1e-6:
1075            dir1 = sv1b - sv1a
1076            dir2 = sv2a - sv2b
1077        elif (sv1a - sv2b).length < 1e-6:
1078            dir1 = sv1a - sv1b
1079            dir2 = sv2a - sv2b
1080        elif (sv1b - sv2a).length < 1e-6:
1081            dir1 = sv1b - sv1a
1082            dir2 = sv2b - sv2a
1083        else:
1084            return False
1085        denom = dir1.length * dir2.length
1086        if denom < 1e-6:
1087            return False
1088        x = (dir1 * dir2) / denom
1089        return acos(bound(-1.0, x, 1.0)) > self.angle
1090
1091
1092# predicates for selection
1093
1094class LengthThresholdUP1D(UnaryPredicate1D):
1095    def __init__(self, length_min=None, length_max=None):
1096        UnaryPredicate1D.__init__(self)
1097        self.length_min = length_min
1098        self.length_max = length_max
1099
1100    def __call__(self, inter):
1101        length = inter.length_2d
1102        if self.length_min is not None and length < self.length_min:
1103            return False
1104        if self.length_max is not None and length > self.length_max:
1105            return False
1106        return True
1107
1108
1109class FaceMarkBothUP1D(UnaryPredicate1D):
1110    def __call__(self, inter: ViewEdge):
1111        fe = inter.first_fedge
1112        while fe is not None:
1113            if fe.is_smooth:
1114                if fe.face_mark:
1115                    return True
1116            elif (fe.nature & Nature.BORDER):
1117                if fe.face_mark_left:
1118                    return True
1119            else:
1120                if fe.face_mark_right and fe.face_mark_left:
1121                    return True
1122            fe = fe.next_fedge
1123        return False
1124
1125
1126class FaceMarkOneUP1D(UnaryPredicate1D):
1127    def __call__(self, inter: ViewEdge):
1128        fe = inter.first_fedge
1129        while fe is not None:
1130            if fe.is_smooth:
1131                if fe.face_mark:
1132                    return True
1133            elif (fe.nature & Nature.BORDER):
1134                if fe.face_mark_left:
1135                    return True
1136            else:
1137                if fe.face_mark_right or fe.face_mark_left:
1138                    return True
1139            fe = fe.next_fedge
1140        return False
1141
1142
1143# predicates for splitting
1144
1145class MaterialBoundaryUP0D(UnaryPredicate0D):
1146    def __call__(self, it):
1147        # can't use only it.is_end here, see commit rBeb8964fb7f19
1148        if it.is_begin or it.at_last or it.is_end:
1149            return False
1150        it.decrement()
1151        prev, v, succ = next(it), next(it), next(it)
1152        fe = v.get_fedge(prev)
1153        idx1 = fe.material_index if fe.is_smooth else fe.material_index_left
1154        fe = v.get_fedge(succ)
1155        idx2 = fe.material_index if fe.is_smooth else fe.material_index_left
1156        return idx1 != idx2
1157
1158
1159class Curvature2DAngleThresholdUP0D(UnaryPredicate0D):
1160    def __init__(self, angle_min=None, angle_max=None):
1161        UnaryPredicate0D.__init__(self)
1162        self.angle_min = angle_min
1163        self.angle_max = angle_max
1164        self.func = Curvature2DAngleF0D()
1165
1166    def __call__(self, inter):
1167        angle = pi - self.func(inter)
1168        if self.angle_min is not None and angle < self.angle_min:
1169            return True
1170        if self.angle_max is not None and angle > self.angle_max:
1171            return True
1172        return False
1173
1174
1175class Length2DThresholdUP0D(UnaryPredicate0D):
1176    def __init__(self, length_limit):
1177        UnaryPredicate0D.__init__(self)
1178        self.length_limit = length_limit
1179        self.t = 0.0
1180
1181    def __call__(self, inter):
1182        t = inter.t  # curvilinear abscissa
1183        if t < self.t:
1184            self.t = 0.0
1185            return False
1186        if t - self.t < self.length_limit:
1187            return False
1188        self.t = t
1189        return True
1190
1191
1192# Seed for random number generation
1193
1194class Seed:
1195    def __init__(self):
1196        self.t_max = 2 ** 15
1197        self.t = int(time.time()) % self.t_max
1198
1199    def get(self, seed):
1200        if seed < 0:
1201            self.t = (self.t + 1) % self.t_max
1202            return self.t
1203        return seed
1204
1205
1206_seed = Seed()
1207
1208
1209def get_dashed_pattern(linestyle):
1210    """Extracts the dashed pattern from the various UI options """
1211    pattern = []
1212    if linestyle.dash1 > 0 and linestyle.gap1 > 0:
1213        pattern.append(linestyle.dash1)
1214        pattern.append(linestyle.gap1)
1215    if linestyle.dash2 > 0 and linestyle.gap2 > 0:
1216        pattern.append(linestyle.dash2)
1217        pattern.append(linestyle.gap2)
1218    if linestyle.dash3 > 0 and linestyle.gap3 > 0:
1219        pattern.append(linestyle.dash3)
1220        pattern.append(linestyle.gap3)
1221    return pattern
1222
1223
1224def get_grouped_objects(group):
1225    for ob in group.objects:
1226        if ob.instance_type == 'COLLECTION' and ob.instance_collection is not None:
1227            for dupli in get_grouped_objects(ob.instance_collection):
1228                yield dupli
1229        else:
1230            yield ob
1231
1232
1233integration_types = {
1234    'MEAN': IntegrationType.MEAN,
1235    'MIN': IntegrationType.MIN,
1236    'MAX': IntegrationType.MAX,
1237    'FIRST': IntegrationType.FIRST,
1238    'LAST': IntegrationType.LAST}
1239
1240
1241# main function for parameter processing
1242def process(layer_name, lineset_name):
1243    scene = getCurrentScene()
1244    layer = scene.view_layers[layer_name]
1245    lineset = layer.freestyle_settings.linesets[lineset_name]
1246    linestyle = lineset.linestyle
1247
1248    # execute line set pre-processing callback functions
1249    for fn in callbacks_lineset_pre:
1250        fn(scene, layer, lineset)
1251
1252    selection_criteria = []
1253    # prepare selection criteria by visibility
1254    if lineset.select_by_visibility:
1255        if lineset.visibility == 'VISIBLE':
1256            selection_criteria.append(
1257                QuantitativeInvisibilityUP1D(0))
1258        elif lineset.visibility == 'HIDDEN':
1259            selection_criteria.append(
1260                NotUP1D(QuantitativeInvisibilityUP1D(0)))
1261        elif lineset.visibility == 'RANGE':
1262            selection_criteria.append(
1263                QuantitativeInvisibilityRangeUP1D(lineset.qi_start, lineset.qi_end))
1264    # prepare selection criteria by edge types
1265    if lineset.select_by_edge_types:
1266        edge_type_criteria = []
1267        if lineset.select_silhouette:
1268            upred = pyNatureUP1D(Nature.SILHOUETTE)
1269            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_silhouette else upred)
1270        if lineset.select_border:
1271            upred = pyNatureUP1D(Nature.BORDER)
1272            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_border else upred)
1273        if lineset.select_crease:
1274            upred = pyNatureUP1D(Nature.CREASE)
1275            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_crease else upred)
1276        if lineset.select_ridge_valley:
1277            upred = pyNatureUP1D(Nature.RIDGE)
1278            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_ridge_valley else upred)
1279        if lineset.select_suggestive_contour:
1280            upred = pyNatureUP1D(Nature.SUGGESTIVE_CONTOUR)
1281            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_suggestive_contour else upred)
1282        if lineset.select_material_boundary:
1283            upred = pyNatureUP1D(Nature.MATERIAL_BOUNDARY)
1284            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_material_boundary else upred)
1285        if lineset.select_edge_mark:
1286            upred = pyNatureUP1D(Nature.EDGE_MARK)
1287            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_edge_mark else upred)
1288        if lineset.select_contour:
1289            upred = ContourUP1D()
1290            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_contour else upred)
1291        if lineset.select_external_contour:
1292            upred = ExternalContourUP1D()
1293            edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_external_contour else upred)
1294        if edge_type_criteria:
1295            if lineset.edge_type_combination == 'OR':
1296                upred = OrUP1D(*edge_type_criteria)
1297            else:
1298                upred = AndUP1D(*edge_type_criteria)
1299            if lineset.edge_type_negation == 'EXCLUSIVE':
1300                upred = NotUP1D(upred)
1301            selection_criteria.append(upred)
1302    # prepare selection criteria by face marks
1303    if lineset.select_by_face_marks:
1304        if lineset.face_mark_condition == 'BOTH':
1305            upred = FaceMarkBothUP1D()
1306        else:
1307            upred = FaceMarkOneUP1D()
1308
1309        if lineset.face_mark_negation == 'EXCLUSIVE':
1310            upred = NotUP1D(upred)
1311        selection_criteria.append(upred)
1312    # prepare selection criteria by group of objects
1313    if lineset.select_by_collection:
1314        if lineset.collection is not None:
1315            names = {getQualifiedObjectName(ob): True for ob in get_grouped_objects(lineset.collection)}
1316            upred = ObjectNamesUP1D(names, lineset.collection_negation == 'EXCLUSIVE')
1317            selection_criteria.append(upred)
1318    # prepare selection criteria by image border
1319    if lineset.select_by_image_border:
1320        upred = WithinImageBoundaryUP1D(*ContextFunctions.get_border())
1321        selection_criteria.append(upred)
1322    # select feature edges
1323    if selection_criteria:
1324        upred = AndUP1D(*selection_criteria)
1325    else:
1326        upred = TrueUP1D()
1327    Operators.select(upred)
1328    # join feature edges to form chains
1329    if linestyle.use_chaining:
1330        if linestyle.chaining == 'PLAIN':
1331            if linestyle.use_same_object:
1332                Operators.bidirectional_chain(ChainSilhouetteIterator(), NotUP1D(upred))
1333            else:
1334                Operators.bidirectional_chain(ChainPredicateIterator(upred, TrueBP1D()), NotUP1D(upred))
1335        elif linestyle.chaining == 'SKETCHY':
1336            if linestyle.use_same_object:
1337                Operators.bidirectional_chain(pySketchyChainSilhouetteIterator(linestyle.rounds))
1338            else:
1339                Operators.bidirectional_chain(pySketchyChainingIterator(linestyle.rounds))
1340    else:
1341        Operators.chain(ChainPredicateIterator(FalseUP1D(), FalseBP1D()), NotUP1D(upred))
1342    # split chains
1343    if linestyle.material_boundary:
1344        Operators.sequential_split(MaterialBoundaryUP0D())
1345    if linestyle.use_angle_min or linestyle.use_angle_max:
1346        angle_min = linestyle.angle_min if linestyle.use_angle_min else None
1347        angle_max = linestyle.angle_max if linestyle.use_angle_max else None
1348        Operators.sequential_split(Curvature2DAngleThresholdUP0D(angle_min, angle_max))
1349    if linestyle.use_split_length:
1350        Operators.sequential_split(Length2DThresholdUP0D(linestyle.split_length), 1.0)
1351    if linestyle.use_split_pattern:
1352        pattern = []
1353        if linestyle.split_dash1 > 0 and linestyle.split_gap1 > 0:
1354            pattern.append(linestyle.split_dash1)
1355            pattern.append(linestyle.split_gap1)
1356        if linestyle.split_dash2 > 0 and linestyle.split_gap2 > 0:
1357            pattern.append(linestyle.split_dash2)
1358            pattern.append(linestyle.split_gap2)
1359        if linestyle.split_dash3 > 0 and linestyle.split_gap3 > 0:
1360            pattern.append(linestyle.split_dash3)
1361            pattern.append(linestyle.split_gap3)
1362        if len(pattern) > 0:
1363            sampling = 1.0
1364            controller = SplitPatternController(pattern, sampling)
1365            Operators.sequential_split(SplitPatternStartingUP0D(controller),
1366                                       SplitPatternStoppingUP0D(controller),
1367                                       sampling)
1368    # sort selected chains
1369    if linestyle.use_sorting:
1370        integration = integration_types.get(linestyle.integration_type, IntegrationType.MEAN)
1371        if linestyle.sort_key == 'DISTANCE_FROM_CAMERA':
1372            bpred = pyZBP1D(integration)
1373        elif linestyle.sort_key == '2D_LENGTH':
1374            bpred = Length2DBP1D()
1375        elif linestyle.sort_key == 'PROJECTED_X':
1376            bpred = pyProjectedXBP1D(integration)
1377        elif linestyle.sort_key == 'PROJECTED_Y':
1378            bpred = pyProjectedYBP1D(integration)
1379        if linestyle.sort_order == 'REVERSE':
1380            bpred = NotBP1D(bpred)
1381        Operators.sort(bpred)
1382    # select chains
1383    if linestyle.use_length_min or linestyle.use_length_max:
1384        length_min = linestyle.length_min if linestyle.use_length_min else None
1385        length_max = linestyle.length_max if linestyle.use_length_max else None
1386        Operators.select(LengthThresholdUP1D(length_min, length_max))
1387    if linestyle.use_chain_count:
1388        Operators.select(pyNFirstUP1D(linestyle.chain_count))
1389    # prepare a list of stroke shaders
1390    shaders_list = []
1391    for m in linestyle.geometry_modifiers:
1392        if not m.use:
1393            continue
1394        if m.type == 'SAMPLING':
1395            shaders_list.append(SamplingShader(
1396                m.sampling))
1397        elif m.type == 'BEZIER_CURVE':
1398            shaders_list.append(BezierCurveShader(
1399                m.error))
1400        elif m.type == 'SIMPLIFICATION':
1401            shaders_list.append(SimplificationShader(m.tolerance))
1402        elif m.type == 'SINUS_DISPLACEMENT':
1403            shaders_list.append(SinusDisplacementShader(
1404                m.wavelength, m.amplitude, m.phase))
1405        elif m.type == 'SPATIAL_NOISE':
1406            shaders_list.append(SpatialNoiseShader(
1407                m.amplitude, m.scale, m.octaves, m.smooth, m.use_pure_random))
1408        elif m.type == 'PERLIN_NOISE_1D':
1409            shaders_list.append(PerlinNoise1DShader(
1410                m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed)))
1411        elif m.type == 'PERLIN_NOISE_2D':
1412            shaders_list.append(PerlinNoise2DShader(
1413                m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed)))
1414        elif m.type == 'BACKBONE_STRETCHER':
1415            shaders_list.append(BackboneStretcherShader(
1416                m.backbone_length))
1417        elif m.type == 'TIP_REMOVER':
1418            shaders_list.append(TipRemoverShader(
1419                m.tip_length))
1420        elif m.type == 'POLYGONIZATION':
1421            shaders_list.append(PolygonalizationShader(
1422                m.error))
1423        elif m.type == 'GUIDING_LINES':
1424            shaders_list.append(GuidingLinesShader(
1425                m.offset))
1426        elif m.type == 'BLUEPRINT':
1427            if m.shape == 'CIRCLES':
1428                shaders_list.append(pyBluePrintCirclesShader(
1429                    m.rounds, m.random_radius, m.random_center))
1430            elif m.shape == 'ELLIPSES':
1431                shaders_list.append(pyBluePrintEllipsesShader(
1432                    m.rounds, m.random_radius, m.random_center))
1433            elif m.shape == 'SQUARES':
1434                shaders_list.append(pyBluePrintSquaresShader(
1435                    m.rounds, m.backbone_length, m.random_backbone))
1436        elif m.type == '2D_OFFSET':
1437            shaders_list.append(Offset2DShader(
1438                m.start, m.end, m.x, m.y))
1439        elif m.type == '2D_TRANSFORM':
1440            shaders_list.append(Transform2DShader(
1441                m.pivot, m.scale_x, m.scale_y, m.angle, m.pivot_u, m.pivot_x, m.pivot_y))
1442    # -- Base color, alpha and thickness -- #
1443    if (not linestyle.use_chaining) or (linestyle.chaining == 'PLAIN' and linestyle.use_same_object):
1444        thickness_position = linestyle.thickness_position
1445    else:
1446        thickness_position = 'CENTER'
1447        import bpy
1448        if bpy.app.debug_freestyle:
1449            print("Warning: Thickness position options are applied when chaining is disabled\n"
1450                  "         or the Plain chaining is used with the Same Object option enabled.")
1451    shaders_list.append(ConstantColorShader(*(linestyle.color), alpha=linestyle.alpha))
1452    shaders_list.append(BaseThicknessShader(linestyle.thickness, thickness_position,
1453                                            linestyle.thickness_ratio))
1454    # -- Modifiers -- #
1455    for m in linestyle.color_modifiers:
1456        if not m.use:
1457            continue
1458        if m.type == 'ALONG_STROKE':
1459            shaders_list.append(ColorAlongStrokeShader(
1460                m.blend, m.influence, m.color_ramp))
1461        elif m.type == 'DISTANCE_FROM_CAMERA':
1462            shaders_list.append(ColorDistanceFromCameraShader(
1463                m.blend, m.influence, m.color_ramp,
1464                m.range_min, m.range_max))
1465        elif m.type == 'DISTANCE_FROM_OBJECT':
1466            if m.target is not None:
1467                shaders_list.append(ColorDistanceFromObjectShader(
1468                    m.blend, m.influence, m.color_ramp, m.target,
1469                    m.range_min, m.range_max))
1470        elif m.type == 'MATERIAL':
1471            shaders_list.append(ColorMaterialShader(
1472                m.blend, m.influence, m.color_ramp, m.material_attribute,
1473                m.use_ramp))
1474        elif m.type == 'TANGENT':
1475            shaders_list.append(TangentColorShader(
1476                m.blend, m.influence, m.color_ramp))
1477        elif m.type == 'CREASE_ANGLE':
1478            shaders_list.append(CreaseAngleColorShader(
1479                m.blend, m.influence, m.color_ramp,
1480                m.angle_min, m.angle_max))
1481        elif m.type == 'CURVATURE_3D':
1482            shaders_list.append(Curvature3DColorShader(
1483                m.blend, m.influence, m.color_ramp,
1484                m.curvature_min, m.curvature_max))
1485        elif m.type == 'NOISE':
1486            shaders_list.append(ColorNoiseShader(
1487                m.blend, m.influence, m.color_ramp,
1488                m.amplitude, m.period, m.seed))
1489    for m in linestyle.alpha_modifiers:
1490        if not m.use:
1491            continue
1492        if m.type == 'ALONG_STROKE':
1493            shaders_list.append(AlphaAlongStrokeShader(
1494                m.blend, m.influence, m.mapping, m.invert, m.curve))
1495        elif m.type == 'DISTANCE_FROM_CAMERA':
1496            shaders_list.append(AlphaDistanceFromCameraShader(
1497                m.blend, m.influence, m.mapping, m.invert, m.curve,
1498                m.range_min, m.range_max))
1499        elif m.type == 'DISTANCE_FROM_OBJECT':
1500            if m.target is not None:
1501                shaders_list.append(AlphaDistanceFromObjectShader(
1502                    m.blend, m.influence, m.mapping, m.invert, m.curve, m.target,
1503                    m.range_min, m.range_max))
1504        elif m.type == 'MATERIAL':
1505            shaders_list.append(AlphaMaterialShader(
1506                m.blend, m.influence, m.mapping, m.invert, m.curve,
1507                m.material_attribute))
1508        elif m.type == 'TANGENT':
1509            shaders_list.append(TangentAlphaShader(
1510                m.blend, m.influence, m.mapping, m.invert, m.curve,))
1511        elif m.type == 'CREASE_ANGLE':
1512            shaders_list.append(CreaseAngleAlphaShader(
1513                m.blend, m.influence, m.mapping, m.invert, m.curve,
1514                m.angle_min, m.angle_max))
1515        elif m.type == 'CURVATURE_3D':
1516            shaders_list.append(Curvature3DAlphaShader(
1517                m.blend, m.influence, m.mapping, m.invert, m.curve,
1518                m.curvature_min, m.curvature_max))
1519        elif m.type == 'NOISE':
1520            shaders_list.append(AlphaNoiseShader(
1521                m.blend, m.influence, m.mapping, m.invert, m.curve,
1522                m.amplitude, m.period, m.seed))
1523    for m in linestyle.thickness_modifiers:
1524        if not m.use:
1525            continue
1526        if m.type == 'ALONG_STROKE':
1527            shaders_list.append(ThicknessAlongStrokeShader(
1528                thickness_position, linestyle.thickness_ratio,
1529                m.blend, m.influence, m.mapping, m.invert, m.curve,
1530                m.value_min, m.value_max))
1531        elif m.type == 'DISTANCE_FROM_CAMERA':
1532            shaders_list.append(ThicknessDistanceFromCameraShader(
1533                thickness_position, linestyle.thickness_ratio,
1534                m.blend, m.influence, m.mapping, m.invert, m.curve,
1535                m.range_min, m.range_max, m.value_min, m.value_max))
1536        elif m.type == 'DISTANCE_FROM_OBJECT':
1537            if m.target is not None:
1538                shaders_list.append(ThicknessDistanceFromObjectShader(
1539                    thickness_position, linestyle.thickness_ratio,
1540                    m.blend, m.influence, m.mapping, m.invert, m.curve, m.target,
1541                    m.range_min, m.range_max, m.value_min, m.value_max))
1542        elif m.type == 'MATERIAL':
1543            shaders_list.append(ThicknessMaterialShader(
1544                thickness_position, linestyle.thickness_ratio,
1545                m.blend, m.influence, m.mapping, m.invert, m.curve,
1546                m.material_attribute, m.value_min, m.value_max))
1547        elif m.type == 'CALLIGRAPHY':
1548            shaders_list.append(CalligraphicThicknessShader(
1549                thickness_position, linestyle.thickness_ratio,
1550                m.blend, m.influence,
1551                m.orientation, m.thickness_min, m.thickness_max))
1552        elif m.type == 'TANGENT':
1553            shaders_list.append(TangentThicknessShader(
1554                thickness_position, linestyle.thickness_ratio,
1555                m.blend, m.influence, m.mapping, m.invert, m.curve,
1556                m.thickness_min, m.thickness_max))
1557        elif m.type == 'NOISE':
1558            shaders_list.append(ThicknessNoiseShader(
1559                thickness_position, linestyle.thickness_ratio,
1560                m.blend, m.influence,
1561                m.amplitude, m.period, m.seed, m.use_asymmetric))
1562        elif m.type == 'CREASE_ANGLE':
1563            shaders_list.append(CreaseAngleThicknessShader(
1564                thickness_position, linestyle.thickness_ratio,
1565                m.blend, m.influence, m.mapping, m.invert, m.curve,
1566                m.angle_min, m.angle_max, m.thickness_min, m.thickness_max))
1567        elif m.type == 'CURVATURE_3D':
1568            shaders_list.append(Curvature3DThicknessShader(
1569                thickness_position, linestyle.thickness_ratio,
1570                m.blend, m.influence, m.mapping, m.invert, m.curve,
1571                m.curvature_min, m.curvature_max, m.thickness_min, m.thickness_max))
1572        else:
1573            raise RuntimeError("No Thickness modifier with type", type(m), m)
1574    # -- Textures -- #
1575    has_tex = False
1576    if linestyle.use_nodes and linestyle.node_tree:
1577        shaders_list.append(BlenderTextureShader(linestyle.node_tree))
1578        has_tex = True
1579    if has_tex:
1580        shaders_list.append(StrokeTextureStepShader(linestyle.texture_spacing))
1581
1582    # execute post-base stylization callbacks
1583    for fn in callbacks_modifiers_post:
1584        shaders_list.extend(fn(scene, layer, lineset))
1585
1586    # -- Stroke caps -- #
1587    if linestyle.caps == 'ROUND':
1588        shaders_list.append(RoundCapShader())
1589    elif linestyle.caps == 'SQUARE':
1590        shaders_list.append(SquareCapShader())
1591
1592    # -- Dashed line -- #
1593    if linestyle.use_dashed_line:
1594        pattern = get_dashed_pattern(linestyle)
1595        if len(pattern) > 0:
1596            shaders_list.append(DashedLineShader(pattern))
1597
1598    # create strokes using the shaders list
1599    Operators.create(TrueUP1D(), shaders_list)
1600
1601    # execute line set post-processing callback functions
1602    for fn in callbacks_lineset_post:
1603        fn(scene, layer, lineset)
1604