Asset Browser: Use single column for asset library menu
[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
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
146 class 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
178 class 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
199 class 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
223 class 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
281 class 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
307 class 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
317 class 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
327 class 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
344 class 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
359 class 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
374 class 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
392 class 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
413 class 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
434 class 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
458 class 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
487 class 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
502 class 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
521 class 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
547 class 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
559 class 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
571 class 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
590 class 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
609 class 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
627 class 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
640 class 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
655 def 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
665 class 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
682 class 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
699 class 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
722 def 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
736 class 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
751 class 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
766 class 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
785 class 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
804 class 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
824 class 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
847 class 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
872 class 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
890 class 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
939 class 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
951 def getQualifiedObjectName(ob):
952     if ob.library is not None:
953         return ob.library.filepath + '/' + ob.name
954     return ob.name
955
956
957 class 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
977 class 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
986 class 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
995 class 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
1031 class 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
1061 class 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
1094 class 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
1109 class 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
1126 class 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
1145 class 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
1159 class 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
1175 class 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
1194 class 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
1209 def 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
1224 def 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
1233 integration_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
1242 def 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)