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