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