Revert "Freestyle: Built-in SVG exporter."
[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     SamplingShader,
79     SpatialNoiseShader,
80     StrokeShader,
81     StrokeTextureStepShader,
82     TipRemoverShader,
83     pyBluePrintCirclesShader,
84     pyBluePrintEllipsesShader,
85     pyBluePrintSquaresShader,
86     RoundCapShader,
87     SquareCapShader,
88     )
89 from freestyle.utils import (
90     ContextFunctions,
91     getCurrentScene,
92     iter_distance_along_stroke,
93     iter_t2d_along_stroke,
94     iter_distance_from_camera,
95     iter_distance_from_object,
96     iter_material_value,
97     stroke_normal,
98     bound,
99     pairwise,
100     BoundedProperty
101     )
102 from _freestyle import (
103     blendRamp,
104     evaluateColorRamp,
105     evaluateCurveMappingF,
106     )
107
108 import time
109 from mathutils import Vector
110 from math import pi, sin, cos, acos, radians
111 from itertools import cycle, tee
112
113
114 class ColorRampModifier(StrokeShader):
115     """Primitive for the color modifiers."""
116     def __init__(self, blend, influence, ramp):
117         StrokeShader.__init__(self)
118         self.blend = blend
119         self.influence = influence
120         self.ramp = ramp
121
122     def evaluate(self, t):
123         col = evaluateColorRamp(self.ramp, t)
124         return col.xyz  # omit alpha
125
126     def blend_ramp(self, a, b):
127         return blendRamp(self.blend, a, self.influence, b)
128
129
130 class ScalarBlendModifier(StrokeShader):
131     """Primitive for alpha and thickness modifiers."""
132     def __init__(self, blend_type, influence):
133         StrokeShader.__init__(self)
134         self.blend_type = blend_type
135         self.influence = influence
136
137     def blend(self, v1, v2):
138         fac = self.influence
139         facm = 1.0 - fac
140         if self.blend_type == 'MIX':
141             v1 = facm * v1 + fac * v2
142         elif self.blend_type == 'ADD':
143             v1 += fac * v2
144         elif self.blend_type == 'MULTIPLY':
145             v1 *= facm + fac * v2
146         elif self.blend_type == 'SUBTRACT':
147             v1 -= fac * v2
148         elif self.blend_type == 'DIVIDE':
149             v1 = facm * v1 + fac * v1 / v2 if v2 != 0.0 else v1
150         elif self.blend_type == 'DIFFERENCE':
151             v1 = facm * v1 + fac * abs(v1 - v2)
152         elif self.blend_type == 'MININUM':
153             v1 = min(fac * v2, v1)
154         elif self.blend_type == 'MAXIMUM':
155             v1 = max(fac * v2, v1)
156         else:
157             raise ValueError("unknown curve blend type: " + self.blend_type)
158         return v1
159
160
161 class CurveMappingModifier(ScalarBlendModifier):
162     def __init__(self, blend, influence, mapping, invert, curve):
163         ScalarBlendModifier.__init__(self, blend, influence)
164         assert mapping in {'LINEAR', 'CURVE'}
165         self.evaluate = getattr(self, mapping)
166         self.invert = invert
167         self.curve = curve
168
169     def LINEAR(self, t):
170         return (1.0 - t) if self.invert else t
171
172     def CURVE(self, t):
173         return evaluateCurveMappingF(self.curve, 0, t)
174
175
176 class ThicknessModifierMixIn:
177     def __init__(self):
178         scene = getCurrentScene()
179         self.persp_camera = (scene.camera.data.type == 'PERSP')
180
181     def set_thickness(self, sv, outer, inner):
182         fe = sv.fedge
183         nature = fe.nature
184         if (nature & Nature.BORDER):
185             if self.persp_camera:
186                 point = -sv.point_3d.normalized()
187                 dir = point.dot(fe.normal_left)
188             else:
189                 dir = fe.normal_left.z
190             if dir < 0.0:  # the back side is visible
191                 outer, inner = inner, outer
192         elif (nature & Nature.SILHOUETTE):
193             if fe.is_smooth:  # TODO more tests needed
194                 outer, inner = inner, outer
195         else:
196             outer = inner = (outer + inner) / 2
197         sv.attribute.thickness = (outer, inner)
198
199
200 class ThicknessBlenderMixIn(ThicknessModifierMixIn):
201     def __init__(self, position, ratio):
202         ThicknessModifierMixIn.__init__(self)
203         self.position = position
204         self.ratio = ratio
205
206     def blend_thickness(self, svert, v):
207         """Blends and sets the thickness."""
208         outer, inner = svert.attribute.thickness
209         fe = svert.fedge
210         v = self.blend(outer + inner, v)
211
212         # Part 1: blend
213         if self.position == 'CENTER':
214             outer = inner = v * 0.5
215         elif self.position == 'INSIDE':
216             outer, inner = 0, v
217         elif self.position == 'OUTSIDE':
218             outer, inner = v, 0
219         elif self.position == 'RELATIVE':
220             outer, inner = v * self.ratio, v - (v * self.ratio)
221         else:
222             raise ValueError("unknown thickness position: " + position)
223
224         # Part 2: set
225         if (fe.nature & Nature.BORDER):
226             if self.persp_camera:
227                 point = -svert.point_3d.normalized()
228                 dir = point.dot(fe.normal_left)
229             else:
230                 dir = fe.normal_left.z
231             if dir < 0.0:  # the back side is visible
232                 outer, inner = inner, outer
233         elif (fe.nature & Nature.SILHOUETTE):
234             if fe.is_smooth:  # TODO more tests needed
235                 outer, inner = inner, outer
236         else:
237             outer = inner = (outer + inner) / 2
238         svert.attribute.thickness = (outer, inner)
239
240
241 class BaseThicknessShader(StrokeShader, ThicknessModifierMixIn):
242     def __init__(self, thickness, position, ratio):
243         StrokeShader.__init__(self)
244         ThicknessModifierMixIn.__init__(self)
245         if position == 'CENTER':
246             self.outer = thickness * 0.5
247             self.inner = thickness - self.outer
248         elif position == 'INSIDE':
249             self.outer = 0
250             self.inner = thickness
251         elif position == 'OUTSIDE':
252             self.outer = thickness
253             self.inner = 0
254         elif position == 'RELATIVE':
255             self.outer = thickness * ratio
256             self.inner = thickness - self.outer
257         else:
258             raise ValueError("unknown thickness position: " + position)
259
260     def shade(self, stroke):
261         for svert in stroke:
262             self.set_thickness(svert, self.outer, self.inner)
263
264
265 # Along Stroke modifiers
266
267 class ColorAlongStrokeShader(ColorRampModifier):
268     """Maps a ramp to the color of the stroke, using the curvilinear abscissa (t)."""
269     def shade(self, stroke):
270         for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
271             a = svert.attribute.color
272             b = self.evaluate(t)
273             svert.attribute.color = self.blend_ramp(a, b)
274
275
276 class AlphaAlongStrokeShader(CurveMappingModifier):
277     """Maps a curve to the alpha/transparancy of the stroke, using the curvilinear abscissa (t)."""
278     def shade(self, stroke):
279         for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
280             a = svert.attribute.alpha
281             b = self.evaluate(t)
282             svert.attribute.alpha = self.blend(a, b)
283
284
285 class ThicknessAlongStrokeShader(ThicknessBlenderMixIn, CurveMappingModifier):
286     """Maps a curve to the thickness of the stroke, using the curvilinear abscissa (t)."""
287     def __init__(self, thickness_position, thickness_ratio,
288                  blend, influence, mapping, invert, curve, value_min, value_max):
289         ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
290         CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
291         self.value = BoundedProperty(value_min, value_max, value_max - value_min)
292
293     def shade(self, stroke):
294         for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
295             b = self.value.min + self.evaluate(t) * self.value.delta
296             self.blend_thickness(svert, b)
297
298
299 # -- Distance from Camera modifiers -- #
300
301 class ColorDistanceFromCameraShader(ColorRampModifier):
302     """Picks a color value from a ramp based on the vertex' distance from the camera."""
303     def __init__(self, blend, influence, ramp, range_min, range_max):
304         ColorRampModifier.__init__(self, blend, influence, ramp)
305         self.range = BoundedProperty(range_min, range_max, range_max - range_min)
306
307     def shade(self, stroke):
308         it = iter_distance_from_camera(stroke, *self.range)
309         for svert, t in it:
310             a = svert.attribute.color
311             b = self.evaluate(t)
312             svert.attribute.color = self.blend_ramp(a, b)
313
314
315 class AlphaDistanceFromCameraShader(CurveMappingModifier):
316     """Picks an alpha value from a curve based on the vertex' distance from the camera"""
317     def __init__(self, blend, influence, mapping, invert, curve, range_min, range_max):
318         CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
319         self.range = BoundedProperty(range_min, range_max, range_max - range_min)
320
321     def shade(self, stroke):
322         it = iter_distance_from_camera(stroke, *self.range)
323         for svert, t in it:
324             a = svert.attribute.alpha
325             b = self.evaluate(t)
326             svert.attribute.alpha = self.blend(a, b)
327
328
329 class ThicknessDistanceFromCameraShader(ThicknessBlenderMixIn, CurveMappingModifier):
330     """Picks a thickness value from a curve based on the vertex' distance from the camera."""
331     def __init__(self, thickness_position, thickness_ratio,
332                  blend, influence, mapping, invert, curve, range_min, range_max, value_min, value_max):
333         ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
334         CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
335         self.range = BoundedProperty(range_min, range_max, range_max - range_min)
336         self.value = BoundedProperty(value_min, value_max, value_max - value_min)
337
338     def shade(self, stroke):
339         for (svert, t) in iter_distance_from_camera(stroke, *self.range):
340             b = self.value.min + self.evaluate(t) * self.value.delta
341             self.blend_thickness(svert, b)
342
343
344 # Distance from Object modifiers
345
346 class ColorDistanceFromObjectShader(ColorRampModifier):
347     """Picks a color value from a ramp based on the vertex' distance from a given object."""
348     def __init__(self, blend, influence, ramp, target, range_min, range_max):
349         ColorRampModifier.__init__(self, blend, influence, ramp)
350         if target is None:
351             raise ValueError("ColorDistanceFromObjectShader: target can't be None ")
352         self.range = BoundedProperty(range_min, range_max, range_max - range_min)
353         # construct a model-view matrix
354         matrix = getCurrentScene().camera.matrix_world.inverted()
355         # get the object location in the camera coordinate
356         self.loc = matrix * target.location
357
358     def shade(self, stroke):
359         it = iter_distance_from_object(stroke, self.loc, *self.range)
360         for svert, t in it:
361             a = svert.attribute.color
362             b = self.evaluate(t)
363             svert.attribute.color = self.blend_ramp(a, b)
364
365
366 class AlphaDistanceFromObjectShader(CurveMappingModifier):
367     """Picks an alpha value from a curve based on the vertex' distance from a given object."""
368     def __init__(self, blend, influence, mapping, invert, curve, target, range_min, range_max):
369         CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
370         if target is None:
371             raise ValueError("AlphaDistanceFromObjectShader: target can't be None ")
372         self.range = BoundedProperty(range_min, range_max, range_max - range_min)
373         # construct a model-view matrix
374         matrix = getCurrentScene().camera.matrix_world.inverted()
375         # get the object location in the camera coordinate
376         self.loc = matrix * target.location
377
378     def shade(self, stroke):
379         it = iter_distance_from_object(stroke, self.loc, *self.range)
380         for svert, t in it:
381             a = svert.attribute.alpha
382             b = self.evaluate(t)
383             svert.attribute.alpha = self.blend(a, b)
384
385
386 class ThicknessDistanceFromObjectShader(ThicknessBlenderMixIn, CurveMappingModifier):
387     """Picks a thickness value from a curve based on the vertex' distance from a given object."""
388     def __init__(self, thickness_position, thickness_ratio,
389                  blend, influence, mapping, invert, curve, target, range_min, range_max, value_min, value_max):
390         ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
391         CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
392         if target is None:
393             raise ValueError("ThicknessDistanceFromObjectShader: target can't be None ")
394         self.range = BoundedProperty(range_min, range_max, range_max - range_min)
395         self.value = BoundedProperty(value_min, value_max, value_max - value_min)
396         # construct a model-view matrix
397         matrix = getCurrentScene().camera.matrix_world.inverted()
398         # get the object location in the camera coordinate
399         self.loc = matrix * target.location
400
401     def shade(self, stroke):
402         it = iter_distance_from_object(stroke, self.loc, *self.range)
403         for svert, t in it:
404             b = self.value.min + self.evaluate(t) * self.value.delta
405             self.blend_thickness(svert, b)
406
407 # Material modifiers
408 class ColorMaterialShader(ColorRampModifier):
409     """Assigns a color to the vertices based on their underlying material."""
410     def __init__(self, blend, influence, ramp, material_attribute, use_ramp):
411         ColorRampModifier.__init__(self, blend, influence, ramp)
412         self.attribute = material_attribute
413         self.use_ramp = use_ramp
414         self.func = CurveMaterialF0D()
415
416     def shade(self, stroke, attributes={'DIFF', 'SPEC', 'LINE'}):
417         it = Interface0DIterator(stroke)
418         if not self.use_ramp and self.attribute in attributes:
419             for svert in it:
420                 material = self.func(it)
421                 if self.attribute == 'LINE':
422                     b = material.line[0:3] 
423                 elif self.attribute == 'DIFF':
424                     b = material.diffuse[0:3]
425                 else:
426                     b = material.specular[0:3]
427                 a = svert.attribute.color
428                 svert.attribute.color = self.blend_ramp(a, b)
429         else:
430             for svert, value in iter_material_value(stroke, self.func, self.attribute):
431                 a = svert.attribute.color
432                 b = self.evaluate(value)
433                 svert.attribute.color = self.blend_ramp(a, b)
434
435 class AlphaMaterialShader(CurveMappingModifier):
436     """Assigns an alpha value to the vertices based on their underlying material."""
437     def __init__(self, blend, influence, mapping, invert, curve, material_attribute):
438         CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
439         self.attribute = material_attribute
440         self.func = CurveMaterialF0D()
441
442     def shade(self, stroke):
443         for svert, value in iter_material_value(stroke, self.func, self.attribute):
444             a = svert.attribute.alpha
445             b = self.evaluate(value)
446             svert.attribute.alpha = self.blend(a, b)
447
448
449 class ThicknessMaterialShader(ThicknessBlenderMixIn, CurveMappingModifier):
450     """Assigns a thickness value to the vertices based on their underlying material."""
451     def __init__(self, thickness_position, thickness_ratio,
452                  blend, influence, mapping, invert, curve, material_attribute, value_min, value_max):
453         ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
454         CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
455         self.attribute = material_attribute
456         self.value = BoundedProperty(value_min, value_max, value_max - value_min)
457         self.func = CurveMaterialF0D()
458
459     def shade(self, stroke):
460         for svert, value in iter_material_value(stroke, self.func, self.attribute):
461             b = self.value.min + self.evaluate(value) * self.value.delta
462             self.blend_thickness(svert, b)
463
464
465 # Calligraphic thickness modifier
466
467
468 class CalligraphicThicknessShader(ThicknessBlenderMixIn, ScalarBlendModifier):
469     """Thickness modifier for achieving a calligraphy-like effect."""
470     def __init__(self, thickness_position, thickness_ratio,
471                  blend_type, influence, orientation, thickness_min, thickness_max):
472         ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
473         ScalarBlendModifier.__init__(self, blend_type, influence)
474         self.orientation = Vector((cos(orientation), sin(orientation)))
475         self.thickness = BoundedProperty(thickness_min, thickness_max, thickness_max - thickness_min)
476         self.func = VertexOrientation2DF0D()
477
478     def shade(self, stroke):
479         it = Interface0DIterator(stroke)
480         for svert in it:
481             dir = self.func(it)
482             if dir.length != 0.0:
483                 dir.normalize()
484                 fac = abs(dir.orthogonal() * self.orientation)
485                 b = self.thickness.min + fac * self.thickness.delta
486             else:
487                 b = self.thickness.min
488             self.blend_thickness(svert, b)
489
490
491 # Geometry modifiers
492
493 class SinusDisplacementShader(StrokeShader):
494     """Displaces the stroke in a sinewave-like shape."""
495     def __init__(self, wavelength, amplitude, phase):
496         StrokeShader.__init__(self)
497         self.wavelength = wavelength
498         self.amplitude = amplitude
499         self.phase = phase / wavelength * 2 * pi
500
501     def shade(self, stroke):
502         # normals are stored in a tuple, so they don't update when we reposition vertices.
503         normals = tuple(stroke_normal(stroke))
504         distances = iter_distance_along_stroke(stroke)
505         coeff = 1 / self.wavelength * 2 * pi
506         for svert, distance, normal in zip(stroke, distances, normals):
507             n = normal * self.amplitude * cos(distance * coeff + self.phase)
508             svert.point += n
509         stroke.update_length()
510
511
512 class PerlinNoise1DShader(StrokeShader):
513     """
514     Displaces the stroke using the curvilinear abscissa.  This means
515     that lines with the same length and sampling interval will be
516     identically distorded.
517     """
518     def __init__(self, freq=10, amp=10, oct=4, angle=radians(45), seed=-1):
519         StrokeShader.__init__(self)
520         self.noise = Noise(seed)
521         self.freq = freq
522         self.amp = amp
523         self.oct = oct
524         self.dir = Vector((cos(angle), sin(angle)))
525
526     def shade(self, stroke):
527         length = stroke.length_2d
528         for svert in stroke:
529             nres = self.noise.turbulence1(length * svert.u, self.freq, self.amp, self.oct)
530             svert.point += nres * self.dir
531         stroke.update_length()
532
533
534 class PerlinNoise2DShader(StrokeShader):
535     """
536     Displaces the stroke using the strokes coordinates.  This means
537     that in a scene no strokes will be distorded identically.
538
539     More information on the noise shaders can be found at:
540     freestyleintegration.wordpress.com/2011/09/25/development-updates-on-september-25/
541     """
542     def __init__(self, freq=10, amp=10, oct=4, angle=radians(45), seed=-1):
543         StrokeShader.__init__(self)
544         self.noise = Noise(seed)
545         self.freq = freq
546         self.amp = amp
547         self.oct = oct
548         self.dir = Vector((cos(angle), sin(angle)))
549
550     def shade(self, stroke):
551         for svert in stroke:
552             projected = Vector((svert.projected_x, svert.projected_y))
553             nres = self.noise.turbulence2(projected, self.freq, self.amp, self.oct)
554             svert.point += nres * self.dir
555         stroke.update_length()
556
557
558 class Offset2DShader(StrokeShader):
559     """Offsets the stroke by a given amount."""
560     def __init__(self, start, end, x, y):
561         StrokeShader.__init__(self)
562         self.start = start
563         self.end = end
564         self.xy = Vector((x, y))
565
566     def shade(self, stroke):
567         # normals are stored in a tuple, so they don't update when we reposition vertices.
568         normals = tuple(stroke_normal(stroke))
569         for svert, normal in zip(stroke, normals):
570             a = self.start + svert.u * (self.end - self.start)
571             svert.point += (normal * a) + self.xy
572         stroke.update_length()
573
574
575 class Transform2DShader(StrokeShader):
576     """Transforms the stroke (scale, rotation, location) around a given pivot point """
577     def __init__(self, pivot, scale_x, scale_y, angle, pivot_u, pivot_x, pivot_y):
578         StrokeShader.__init__(self)
579         self.pivot = pivot
580         self.scale = Vector((scale_x, scale_y))
581         self.cos_theta = cos(angle)
582         self.sin_theta = sin(angle)
583         self.pivot_u = pivot_u
584         self.pivot_x = pivot_x
585         self.pivot_y = pivot_y
586         if pivot not in {'START', 'END', 'CENTER', 'ABSOLUTE', 'PARAM'}:
587             raise ValueError("expected pivot in {'START', 'END', 'CENTER', 'ABSOLUTE', 'PARAM'}, not" + pivot)
588
589     def shade(self, stroke):
590         # determine the pivot of scaling and rotation operations
591         if self.pivot == 'START':
592             pivot = stroke[0].point
593         elif self.pivot == 'END':
594             pivot = stroke[-1].point
595         elif self.pivot == 'CENTER':
596             # minor rounding errors here, because
597             # given v = Vector(a, b), then (v / n) != Vector(v.x / n, v.y / n)
598             pivot = (1 / len(stroke)) * sum((svert.point for svert in stroke), Vector((0.0, 0.0)))
599         elif self.pivot == 'ABSOLUTE':
600             pivot = Vector((self.pivot_x, self.pivot_y))
601         elif self.pivot == 'PARAM':
602             if self.pivot_u < stroke[0].u:
603                 pivot = stroke[0].point
604             else:
605                 for prev, svert in pairwise(stroke):
606                     if self.pivot_u < svert.u:
607                         break
608                 pivot = svert.point + (svert.u - self.pivot_u) * (prev.point - svert.point)
609
610         # apply scaling and rotation operations
611         for svert in stroke:
612             p = (svert.point - pivot)
613             x = p.x * self.scale.x
614             y = p.y * self.scale.y
615             p.x = x * self.cos_theta - y * self.sin_theta
616             p.y = x * self.sin_theta + y * self.cos_theta
617             svert.point = p + pivot
618         stroke.update_length()
619
620
621 # Predicates and helper functions
622
623 class QuantitativeInvisibilityRangeUP1D(UnaryPredicate1D):
624     def __init__(self, qi_start, qi_end):
625         UnaryPredicate1D.__init__(self)
626         self.getQI = QuantitativeInvisibilityF1D()
627         self.qi_start = qi_start
628         self.qi_end = qi_end
629
630     def __call__(self, inter):
631         qi = self.getQI(inter)
632         return self.qi_start <= qi <= self.qi_end
633
634
635 class ObjectNamesUP1D(UnaryPredicate1D):
636     def __init__(self, names, negative):
637         UnaryPredicate1D.__init__(self)
638         self.names = names
639         self.negative = negative
640
641     def __call__(self, viewEdge):
642         found = viewEdge.viewshape.name in self.names
643         if self.negative:
644             return not found
645         return found
646
647
648 # -- Split by dashed line pattern -- #
649
650 class SplitPatternStartingUP0D(UnaryPredicate0D):
651     def __init__(self, controller):
652         UnaryPredicate0D.__init__(self)
653         self.controller = controller
654
655     def __call__(self, inter):
656         return self.controller.start()
657
658
659 class SplitPatternStoppingUP0D(UnaryPredicate0D):
660     def __init__(self, controller):
661         UnaryPredicate0D.__init__(self)
662         self.controller = controller
663
664     def __call__(self, inter):
665         return self.controller.stop()
666
667
668 class SplitPatternController:
669     def __init__(self, pattern, sampling):
670         self.sampling = float(sampling)
671         k = len(pattern) // 2
672         n = k * 2
673         self.start_pos = [pattern[i] + pattern[i + 1] for i in range(0, n, 2)]
674         self.stop_pos = [pattern[i] for i in range(0, n, 2)]
675         self.init()
676
677     def init(self):
678         self.start_len = 0.0
679         self.start_idx = 0
680         self.stop_len = self.sampling
681         self.stop_idx = 0
682
683     def start(self):
684         self.start_len += self.sampling
685         if abs(self.start_len - self.start_pos[self.start_idx]) < self.sampling / 2.0:
686             self.start_len = 0.0
687             self.start_idx = (self.start_idx + 1) % len(self.start_pos)
688             return True
689         return False
690
691     def stop(self):
692         if self.start_len > 0.0:
693             self.init()
694         self.stop_len += self.sampling
695         if abs(self.stop_len - self.stop_pos[self.stop_idx]) < self.sampling / 2.0:
696             self.stop_len = self.sampling
697             self.stop_idx = (self.stop_idx + 1) % len(self.stop_pos)
698             return True
699         return False
700
701
702 # Dashed line
703
704 class DashedLineShader(StrokeShader):
705     def __init__(self, pattern):
706         StrokeShader.__init__(self)
707         self.pattern = pattern
708
709     def shade(self, stroke):
710         start = 0.0  # 2D curvilinear length
711         visible = True
712         # The extra 'sampling' term is added below, because the
713         # visibility attribute of the i-th vertex refers to the
714         # visibility of the stroke segment between the i-th and
715         # (i+1)-th vertices.
716         sampling = 1.0
717         it = stroke.stroke_vertices_begin(sampling)
718         pattern_cycle = cycle(self.pattern)
719         pattern = next(pattern_cycle)
720         for svert in it:
721             pos = it.t  # curvilinear abscissa
722
723             if pos - start + sampling > pattern:
724                 start = pos
725                 pattern = next(pattern_cycle)
726                 visible = not visible
727
728             if not visible:
729                 it.object.attribute.visible = False
730
731
732 # predicates for chaining
733
734 class AngleLargerThanBP1D(BinaryPredicate1D):
735     def __init__(self, angle):
736         BinaryPredicate1D.__init__(self)
737         self.angle = angle
738
739     def __call__(self, i1, i2):
740         sv1a = i1.first_fedge.first_svertex.point_2d
741         sv1b = i1.last_fedge.second_svertex.point_2d
742         sv2a = i2.first_fedge.first_svertex.point_2d
743         sv2b = i2.last_fedge.second_svertex.point_2d
744         if (sv1a - sv2a).length < 1e-6:
745             dir1 = sv1a - sv1b
746             dir2 = sv2b - sv2a
747         elif (sv1b - sv2b).length < 1e-6:
748             dir1 = sv1b - sv1a
749             dir2 = sv2a - sv2b
750         elif (sv1a - sv2b).length < 1e-6:
751             dir1 = sv1a - sv1b
752             dir2 = sv2a - sv2b
753         elif (sv1b - sv2a).length < 1e-6:
754             dir1 = sv1b - sv1a
755             dir2 = sv2b - sv2a
756         else:
757             return False
758         denom = dir1.length * dir2.length
759         if denom < 1e-6:
760             return False
761         x = (dir1 * dir2) / denom
762         return acos(bound(-1.0, x, 1.0)) > self.angle
763
764 # predicates for selection
765
766
767 class LengthThresholdUP1D(UnaryPredicate1D):
768     def __init__(self, length_min=None, length_max=None):
769         UnaryPredicate1D.__init__(self)
770         self.length_min = length_min
771         self.length_max = length_max
772
773     def __call__(self, inter):
774         length = inter.length_2d
775         if self.length_min is not None and length < self.length_min:
776             return False
777         if self.length_max is not None and length > self.length_max:
778             return False
779         return True
780
781
782 class FaceMarkBothUP1D(UnaryPredicate1D):
783     def __call__(self, inter: ViewEdge):
784         fe = inter.first_fedge
785         while fe is not None:
786             if fe.is_smooth:
787                 if fe.face_mark:
788                     return True
789             elif (fe.nature & Nature.BORDER):
790                 if fe.face_mark_left:
791                     return True
792             else:
793                 if fe.face_mark_right and fe.face_mark_left:
794                     return True
795             fe = fe.next_fedge
796         return False
797
798
799 class FaceMarkOneUP1D(UnaryPredicate1D):
800     def __call__(self, inter: ViewEdge):
801         fe = inter.first_fedge
802         while fe is not None:
803             if fe.is_smooth:
804                 if fe.face_mark:
805                     return True
806             elif (fe.nature & Nature.BORDER):
807                 if fe.face_mark_left:
808                     return True
809             else:
810                 if fe.face_mark_right or fe.face_mark_left:
811                     return True
812             fe = fe.next_fedge
813         return False
814
815
816 # predicates for splitting
817
818 class MaterialBoundaryUP0D(UnaryPredicate0D):
819     def __call__(self, it):
820         # can't use only it.is_end here, see commit rBeb8964fb7f19
821         if it.is_begin or it.at_last or it.is_end:
822             return False
823         it.decrement()
824         prev, v, succ = next(it), next(it), next(it)
825         fe = v.get_fedge(prev)
826         idx1 = fe.material_index if fe.is_smooth else fe.material_index_left
827         fe = v.get_fedge(succ)
828         idx2 = fe.material_index if fe.is_smooth else fe.material_index_left
829         return idx1 != idx2
830
831
832 class Curvature2DAngleThresholdUP0D(UnaryPredicate0D):
833     def __init__(self, angle_min=None, angle_max=None):
834         UnaryPredicate0D.__init__(self)
835         self.angle_min = angle_min
836         self.angle_max = angle_max
837         self.func = Curvature2DAngleF0D()
838
839     def __call__(self, inter):
840         angle = pi - self.func(inter)
841         if self.angle_min is not None and angle < self.angle_min:
842             return True
843         if self.angle_max is not None and angle > self.angle_max:
844             return True
845         return False
846
847
848 class Length2DThresholdUP0D(UnaryPredicate0D):
849     def __init__(self, length_limit):
850         UnaryPredicate0D.__init__(self)
851         self.length_limit = length_limit
852         self.t = 0.0
853
854     def __call__(self, inter):
855         t = inter.t  # curvilinear abscissa
856         if t < self.t:
857             self.t = 0.0
858             return False
859         if t - self.t < self.length_limit:
860             return False
861         self.t = t
862         return True
863
864
865 # Seed for random number generation
866
867 class Seed:
868     def __init__(self):
869         self.t_max = 2 ** 15
870         self.t = int(time.time()) % self.t_max
871
872     def get(self, seed):
873         if seed < 0:
874             self.t = (self.t + 1) % self.t_max
875             return self.t
876         return seed
877
878 _seed = Seed()
879
880
881 integration_types = {
882     'MEAN': IntegrationType.MEAN,
883     'MIN': IntegrationType.MIN,
884     'MAX': IntegrationType.MAX,
885     'FIRST': IntegrationType.FIRST,
886     'LAST': IntegrationType.LAST}
887
888
889 # main function for parameter processing
890
891 def process(layer_name, lineset_name):
892     scene = getCurrentScene()
893     layer = scene.render.layers[layer_name]
894     lineset = layer.freestyle_settings.linesets[lineset_name]
895     linestyle = lineset.linestyle
896
897     selection_criteria = []
898     # prepare selection criteria by visibility
899     if lineset.select_by_visibility:
900         if lineset.visibility == 'VISIBLE':
901             selection_criteria.append(
902                 QuantitativeInvisibilityUP1D(0))
903         elif lineset.visibility == 'HIDDEN':
904             selection_criteria.append(
905                 NotUP1D(QuantitativeInvisibilityUP1D(0)))
906         elif lineset.visibility == 'RANGE':
907             selection_criteria.append(
908                 QuantitativeInvisibilityRangeUP1D(lineset.qi_start, lineset.qi_end))
909     # prepare selection criteria by edge types
910     if lineset.select_by_edge_types:
911         edge_type_criteria = []
912         if lineset.select_silhouette:
913             upred = pyNatureUP1D(Nature.SILHOUETTE)
914             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_silhouette else upred)
915         if lineset.select_border:
916             upred = pyNatureUP1D(Nature.BORDER)
917             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_border else upred)
918         if lineset.select_crease:
919             upred = pyNatureUP1D(Nature.CREASE)
920             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_crease else upred)
921         if lineset.select_ridge_valley:
922             upred = pyNatureUP1D(Nature.RIDGE)
923             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_ridge_valley else upred)
924         if lineset.select_suggestive_contour:
925             upred = pyNatureUP1D(Nature.SUGGESTIVE_CONTOUR)
926             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_suggestive_contour else upred)
927         if lineset.select_material_boundary:
928             upred = pyNatureUP1D(Nature.MATERIAL_BOUNDARY)
929             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_material_boundary else upred)
930         if lineset.select_edge_mark:
931             upred = pyNatureUP1D(Nature.EDGE_MARK)
932             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_edge_mark else upred)
933         if lineset.select_contour:
934             upred = ContourUP1D()
935             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_contour else upred)
936         if lineset.select_external_contour:
937             upred = ExternalContourUP1D()
938             edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_external_contour else upred)
939         if lineset.edge_type_combination == 'OR':
940             upred = OrUP1D(*edge_type_criteria)
941         else:
942             upred = AndUP1D(*edge_type_criteria)
943         if upred is not None:
944             if lineset.edge_type_negation == 'EXCLUSIVE':
945                 upred = NotUP1D(upred)
946             selection_criteria.append(upred)
947     # prepare selection criteria by face marks
948     if lineset.select_by_face_marks:
949         if lineset.face_mark_condition == 'BOTH':
950             upred = FaceMarkBothUP1D()
951         else:
952             upred = FaceMarkOneUP1D()
953
954         if lineset.face_mark_negation == 'EXCLUSIVE':
955             upred = NotUP1D(upred)
956         selection_criteria.append(upred)
957     # prepare selection criteria by group of objects
958     if lineset.select_by_group:
959         if lineset.group is not None:
960             names = {ob.name: True for ob in lineset.group.objects}
961             upred = ObjectNamesUP1D(names, lineset.group_negation == 'EXCLUSIVE')
962             selection_criteria.append(upred)
963     # prepare selection criteria by image border
964     if lineset.select_by_image_border:
965         upred = WithinImageBoundaryUP1D(*ContextFunctions.get_border())
966         selection_criteria.append(upred)
967     # select feature edges
968     upred = AndUP1D(*selection_criteria)
969     if upred is None:
970         upred = TrueUP1D()
971     Operators.select(upred)
972     # join feature edges to form chains
973     if linestyle.use_chaining:
974         if linestyle.chaining == 'PLAIN':
975             if linestyle.use_same_object:
976                 Operators.bidirectional_chain(ChainSilhouetteIterator(), NotUP1D(upred))
977             else:
978                 Operators.bidirectional_chain(ChainPredicateIterator(upred, TrueBP1D()), NotUP1D(upred))
979         elif linestyle.chaining == 'SKETCHY':
980             if linestyle.use_same_object:
981                 Operators.bidirectional_chain(pySketchyChainSilhouetteIterator(linestyle.rounds))
982             else:
983                 Operators.bidirectional_chain(pySketchyChainingIterator(linestyle.rounds))
984     else:
985         Operators.chain(ChainPredicateIterator(FalseUP1D(), FalseBP1D()), NotUP1D(upred))
986     # split chains
987     if linestyle.material_boundary:
988         Operators.sequential_split(MaterialBoundaryUP0D())
989     if linestyle.use_angle_min or linestyle.use_angle_max:
990         angle_min = linestyle.angle_min if linestyle.use_angle_min else None
991         angle_max = linestyle.angle_max if linestyle.use_angle_max else None
992         Operators.sequential_split(Curvature2DAngleThresholdUP0D(angle_min, angle_max))
993     if linestyle.use_split_length:
994         Operators.sequential_split(Length2DThresholdUP0D(linestyle.split_length), 1.0)
995     if linestyle.use_split_pattern:
996         pattern = []
997         if linestyle.split_dash1 > 0 and linestyle.split_gap1 > 0:
998             pattern.append(linestyle.split_dash1)
999             pattern.append(linestyle.split_gap1)
1000         if linestyle.split_dash2 > 0 and linestyle.split_gap2 > 0:
1001             pattern.append(linestyle.split_dash2)
1002             pattern.append(linestyle.split_gap2)
1003         if linestyle.split_dash3 > 0 and linestyle.split_gap3 > 0:
1004             pattern.append(linestyle.split_dash3)
1005             pattern.append(linestyle.split_gap3)
1006         if len(pattern) > 0:
1007             sampling = 1.0
1008             controller = SplitPatternController(pattern, sampling)
1009             Operators.sequential_split(SplitPatternStartingUP0D(controller),
1010                                        SplitPatternStoppingUP0D(controller),
1011                                        sampling)
1012     # sort selected chains
1013     if linestyle.use_sorting:
1014         integration = integration_types.get(linestyle.integration_type, IntegrationType.MEAN)
1015         if linestyle.sort_key == 'DISTANCE_FROM_CAMERA':
1016             bpred = pyZBP1D(integration)
1017         elif linestyle.sort_key == '2D_LENGTH':
1018             bpred = Length2DBP1D()
1019         elif linestyle.sort_key == 'PROJECTED_X':
1020             bpred = pyProjectedXBP1D(integration)
1021         elif linestyle.sort_key == 'PROJECTED_Y':
1022             bpred = pyProjectedYBP1D(integration)
1023         if linestyle.sort_order == 'REVERSE':
1024             bpred = NotBP1D(bpred)
1025         Operators.sort(bpred)
1026     # select chains
1027     if linestyle.use_length_min or linestyle.use_length_max:
1028         length_min = linestyle.length_min if linestyle.use_length_min else None
1029         length_max = linestyle.length_max if linestyle.use_length_max else None
1030         Operators.select(LengthThresholdUP1D(length_min, length_max))
1031     if linestyle.use_chain_count:
1032         Operators.select(pyNFirstUP1D(linestyle.chain_count))
1033     # prepare a list of stroke shaders
1034     shaders_list = []
1035     for m in linestyle.geometry_modifiers:
1036         if not m.use:
1037             continue
1038         if m.type == 'SAMPLING':
1039             shaders_list.append(SamplingShader(
1040                 m.sampling))
1041         elif m.type == 'BEZIER_CURVE':
1042             shaders_list.append(BezierCurveShader(
1043                 m.error))
1044         elif m.type == 'SINUS_DISPLACEMENT':
1045             shaders_list.append(SinusDisplacementShader(
1046                 m.wavelength, m.amplitude, m.phase))
1047         elif m.type == 'SPATIAL_NOISE':
1048             shaders_list.append(SpatialNoiseShader(
1049                 m.amplitude, m.scale, m.octaves, m.smooth, m.use_pure_random))
1050         elif m.type == 'PERLIN_NOISE_1D':
1051             shaders_list.append(PerlinNoise1DShader(
1052                 m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed)))
1053         elif m.type == 'PERLIN_NOISE_2D':
1054             shaders_list.append(PerlinNoise2DShader(
1055                 m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed)))
1056         elif m.type == 'BACKBONE_STRETCHER':
1057             shaders_list.append(BackboneStretcherShader(
1058                 m.backbone_length))
1059         elif m.type == 'TIP_REMOVER':
1060             shaders_list.append(TipRemoverShader(
1061                 m.tip_length))
1062         elif m.type == 'POLYGONIZATION':
1063             shaders_list.append(PolygonalizationShader(
1064                 m.error))
1065         elif m.type == 'GUIDING_LINES':
1066             shaders_list.append(GuidingLinesShader(
1067                 m.offset))
1068         elif m.type == 'BLUEPRINT':
1069             if m.shape == 'CIRCLES':
1070                 shaders_list.append(pyBluePrintCirclesShader(
1071                     m.rounds, m.random_radius, m.random_center))
1072             elif m.shape == 'ELLIPSES':
1073                 shaders_list.append(pyBluePrintEllipsesShader(
1074                     m.rounds, m.random_radius, m.random_center))
1075             elif m.shape == 'SQUARES':
1076                 shaders_list.append(pyBluePrintSquaresShader(
1077                     m.rounds, m.backbone_length, m.random_backbone))
1078         elif m.type == '2D_OFFSET':
1079             shaders_list.append(Offset2DShader(
1080                 m.start, m.end, m.x, m.y))
1081         elif m.type == '2D_TRANSFORM':
1082             shaders_list.append(Transform2DShader(
1083                 m.pivot, m.scale_x, m.scale_y, m.angle, m.pivot_u, m.pivot_x, m.pivot_y))
1084     # -- Base color, alpha and thickness -- #
1085     if (not linestyle.use_chaining) or (linestyle.chaining == 'PLAIN' and linestyle.use_same_object):
1086         thickness_position = linestyle.thickness_position
1087     else:
1088         thickness_position = 'CENTER'
1089         import bpy
1090         if bpy.app.debug_freestyle:
1091             print("Warning: Thickness position options are applied when chaining is disabled\n"
1092                   "         or the Plain chaining is used with the Same Object option enabled.")
1093     shaders_list.append(ConstantColorShader(*(linestyle.color), alpha=linestyle.alpha))
1094     shaders_list.append(BaseThicknessShader(linestyle.thickness, thickness_position,
1095                                             linestyle.thickness_ratio))
1096     # -- Modifiers -- #
1097     for m in linestyle.color_modifiers:
1098         if not m.use:
1099             continue
1100         if m.type == 'ALONG_STROKE':
1101             shaders_list.append(ColorAlongStrokeShader(
1102                 m.blend, m.influence, m.color_ramp))
1103         elif m.type == 'DISTANCE_FROM_CAMERA':
1104             shaders_list.append(ColorDistanceFromCameraShader(
1105                 m.blend, m.influence, m.color_ramp,
1106                 m.range_min, m.range_max))
1107         elif m.type == 'DISTANCE_FROM_OBJECT' and m.target is not None:
1108             shaders_list.append(ColorDistanceFromObjectShader(
1109                 m.blend, m.influence, m.color_ramp, m.target,
1110                 m.range_min, m.range_max))
1111         elif m.type == 'MATERIAL':
1112             shaders_list.append(ColorMaterialShader(
1113                 m.blend, m.influence, m.color_ramp, m.material_attribute,
1114                 m.use_ramp))
1115     for m in linestyle.alpha_modifiers:
1116         if not m.use:
1117             continue
1118         if m.type == 'ALONG_STROKE':
1119             shaders_list.append(AlphaAlongStrokeShader(
1120                 m.blend, m.influence, m.mapping, m.invert, m.curve))
1121         elif m.type == 'DISTANCE_FROM_CAMERA':
1122             shaders_list.append(AlphaDistanceFromCameraShader(
1123                 m.blend, m.influence, m.mapping, m.invert, m.curve,
1124                 m.range_min, m.range_max))
1125         elif m.type == 'DISTANCE_FROM_OBJECT' and m.target is not None:
1126             shaders_list.append(AlphaDistanceFromObjectShader(
1127                 m.blend, m.influence, m.mapping, m.invert, m.curve, m.target,
1128                 m.range_min, m.range_max))
1129         elif m.type == 'MATERIAL':
1130             shaders_list.append(AlphaMaterialShader(
1131                 m.blend, m.influence, m.mapping, m.invert, m.curve,
1132                 m.material_attribute))
1133     for m in linestyle.thickness_modifiers:
1134         if not m.use:
1135             continue
1136         if m.type == 'ALONG_STROKE':
1137             shaders_list.append(ThicknessAlongStrokeShader(
1138                 thickness_position, linestyle.thickness_ratio,
1139                 m.blend, m.influence, m.mapping, m.invert, m.curve,
1140                 m.value_min, m.value_max))
1141         elif m.type == 'DISTANCE_FROM_CAMERA':
1142             shaders_list.append(ThicknessDistanceFromCameraShader(
1143                 thickness_position, linestyle.thickness_ratio,
1144                 m.blend, m.influence, m.mapping, m.invert, m.curve,
1145                 m.range_min, m.range_max, m.value_min, m.value_max))
1146         elif m.type == 'DISTANCE_FROM_OBJECT' and m.target is not None:
1147             shaders_list.append(ThicknessDistanceFromObjectShader(
1148                 thickness_position, linestyle.thickness_ratio,
1149                 m.blend, m.influence, m.mapping, m.invert, m.curve, m.target,
1150                 m.range_min, m.range_max, m.value_min, m.value_max))
1151         elif m.type == 'MATERIAL':
1152             shaders_list.append(ThicknessMaterialShader(
1153                 thickness_position, linestyle.thickness_ratio,
1154                 m.blend, m.influence, m.mapping, m.invert, m.curve,
1155                 m.material_attribute, m.value_min, m.value_max))
1156         elif m.type == 'CALLIGRAPHY':
1157             shaders_list.append(CalligraphicThicknessShader(
1158                 thickness_position, linestyle.thickness_ratio,
1159                 m.blend, m.influence,
1160                 m.orientation, m.thickness_min, m.thickness_max))
1161     # -- Textures -- #
1162     has_tex = False
1163     if scene.render.use_shading_nodes:
1164         if linestyle.use_nodes and linestyle.node_tree:
1165             shaders_list.append(BlenderTextureShader(linestyle.node_tree))
1166             has_tex = True
1167     else:
1168         if linestyle.use_texture:
1169             textures = tuple(BlenderTextureShader(slot) for slot in linestyle.texture_slots if slot is not None)
1170             if textures:
1171                 shaders_list.extend(textures)
1172                 has_tex = True
1173     if has_tex:
1174         shaders_list.append(StrokeTextureStepShader(linestyle.texture_spacing))
1175     # -- Stroke caps -- #
1176     if linestyle.caps == 'ROUND':
1177         shaders_list.append(RoundCapShader())
1178     elif linestyle.caps == 'SQUARE':
1179         shaders_list.append(SquareCapShader())
1180     # -- Dashed line -- #
1181     if linestyle.use_dashed_line:
1182         pattern = []
1183         if linestyle.dash1 > 0 and linestyle.gap1 > 0:
1184             pattern.append(linestyle.dash1)
1185             pattern.append(linestyle.gap1)
1186         if linestyle.dash2 > 0 and linestyle.gap2 > 0:
1187             pattern.append(linestyle.dash2)
1188             pattern.append(linestyle.gap2)
1189         if linestyle.dash3 > 0 and linestyle.gap3 > 0:
1190             pattern.append(linestyle.dash3)
1191             pattern.append(linestyle.gap3)
1192         if len(pattern) > 0:
1193             shaders_list.append(DashedLineShader(pattern))
1194     # create strokes using the shaders list
1195     Operators.create(TrueUP1D(), shaders_list)