Fix T78854: Cell fracture fails in background mode
[blender-addons.git] / render_freestyle_svg.py
blob4d49a1258dde583f11b4a62da9f0f0166c441739
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 bl_info = {
6 "name": "Freestyle SVG Exporter",
7 "author": "Folkert de Vries",
8 "version": (1, 0),
9 "blender": (2, 80, 0),
10 "location": "Properties > Render > Freestyle SVG Export",
11 "description": "Exports Freestyle's stylized edges in SVG format",
12 "warning": "",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/render/render_freestyle_svg.html",
14 "support": 'OFFICIAL',
15 "category": "Render",
18 import bpy
19 import parameter_editor
20 import itertools
21 import os
23 import xml.etree.cElementTree as et
25 from bpy.app.handlers import persistent
26 from collections import OrderedDict
27 from functools import partial
28 from mathutils import Vector
30 from freestyle.types import (
31 StrokeShader,
32 Interface0DIterator,
33 Operators,
34 Nature,
35 StrokeVertex,
37 from freestyle.utils import (
38 getCurrentScene,
39 BoundingBox,
40 is_poly_clockwise,
41 StrokeCollector,
42 material_from_fedge,
43 get_object_name,
45 from freestyle.functions import (
46 GetShapeF1D,
47 CurveMaterialF0D,
49 from freestyle.predicates import (
50 AndBP1D,
51 AndUP1D,
52 ContourUP1D,
53 ExternalContourUP1D,
54 MaterialBP1D,
55 NotBP1D,
56 NotUP1D,
57 OrBP1D,
58 OrUP1D,
59 pyNatureUP1D,
60 pyZBP1D,
61 pyZDiscontinuityBP1D,
62 QuantitativeInvisibilityUP1D,
63 SameShapeIdBP1D,
64 TrueBP1D,
65 TrueUP1D,
67 from freestyle.chainingiterators import ChainPredicateIterator
68 from parameter_editor import get_dashed_pattern
70 from bpy.props import (
71 BoolProperty,
72 EnumProperty,
73 PointerProperty,
77 # use utf-8 here to keep ElementTree happy, end result is utf-16
78 svg_primitive = """<?xml version="1.0" encoding="ascii" standalone="no"?>
79 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
80 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
81 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{:d}" height="{:d}">
82 </svg>"""
85 # xml namespaces
86 namespaces = {
87 "inkscape": "http://www.inkscape.org/namespaces/inkscape",
88 "svg": "http://www.w3.org/2000/svg",
89 "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
90 "": "http://www.w3.org/2000/svg",
94 # wrap XMLElem.find, so the namespaces don't need to be given as an argument
95 def find_xml_elem(obj, search, namespaces, *, all=False):
96 if all:
97 return obj.findall(search, namespaces=namespaces)
98 return obj.find(search, namespaces=namespaces)
100 find_svg_elem = partial(find_xml_elem, namespaces=namespaces)
103 def render_height(scene):
104 return int(scene.render.resolution_y * scene.render.resolution_percentage / 100)
107 def render_width(scene):
108 return int(scene.render.resolution_x * scene.render.resolution_percentage / 100)
111 def format_rgb(color):
112 return 'rgb({}, {}, {})'.format(*(int(v * 255) for v in color))
115 # stores the state of the render, used to differ between animation and single frame renders.
116 class RenderState:
118 # Note that this flag is set to False only after the first frame
119 # has been written to file.
120 is_preview = True
123 @persistent
124 def render_init(scene):
125 RenderState.is_preview = True
128 @persistent
129 def render_write(scene):
130 RenderState.is_preview = False
133 def is_preview_render(scene):
134 return RenderState.is_preview or scene.svg_export.mode == 'FRAME'
137 def create_path(scene):
138 """Creates the output path for the svg file"""
139 path = os.path.dirname(scene.render.frame_path())
140 file_dir_path = os.path.dirname(bpy.data.filepath)
142 # try to use the given path if it is absolute
143 if os.path.isabs(path):
144 dirname = path
146 # otherwise, use current file's location as a start for the relative path
147 elif bpy.data.is_saved and file_dir_path:
148 dirname = os.path.normpath(os.path.join(file_dir_path, path))
150 # otherwise, use the folder from which blender was called as the start
151 else:
152 dirname = os.path.abspath(bpy.path.abspath(path))
155 basename = bpy.path.basename(scene.render.filepath)
156 if scene.svg_export.mode == 'FRAME':
157 frame = "{:04d}".format(scene.frame_current)
158 else:
159 frame = "{:04d}-{:04d}".format(scene.frame_start, scene.frame_end)
161 return os.path.join(dirname, basename + frame + ".svg")
164 class SVGExporterLinesetPanel(bpy.types.Panel):
165 """Creates a Panel in the Render Layers context of the properties editor"""
166 bl_idname = "RENDER_PT_SVGExporterLinesetPanel"
167 bl_space_type = 'PROPERTIES'
168 bl_label = "Freestyle Line Style SVG Export"
169 bl_region_type = 'WINDOW'
170 bl_context = "view_layer"
172 def draw(self, context):
173 layout = self.layout
175 scene = context.scene
176 svg = scene.svg_export
177 freestyle = context.window.view_layer.freestyle_settings
179 try:
180 linestyle = freestyle.linesets.active.linestyle
182 except AttributeError:
183 # Linestyles can be removed, so 0 linestyles is possible.
184 # there is nothing to draw in those cases.
185 # see https://developer.blender.org/T49855
186 return
188 else:
189 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
190 row = layout.row()
191 column = row.column()
192 column.prop(linestyle, 'use_export_strokes')
194 column = row.column()
195 column.active = svg.object_fill
196 column.prop(linestyle, 'use_export_fills')
198 row = layout.row()
199 row.prop(linestyle, "stroke_color_mode", expand=True)
202 class SVGExport(bpy.types.PropertyGroup):
203 """Implements the properties for the SVG exporter"""
204 bl_idname = "RENDER_PT_svg_export"
206 use_svg_export: BoolProperty(
207 name="SVG Export",
208 description="Export Freestyle edges to an .svg format",
210 split_at_invisible: BoolProperty(
211 name="Split at Invisible",
212 description="Split the stroke at an invisible vertex",
214 object_fill: BoolProperty(
215 name="Fill Contours",
216 description="Fill the contour with the object's material color",
218 mode: EnumProperty(
219 name="Mode",
220 items=(
221 ('FRAME', "Frame", "Export a single frame", 0),
222 ('ANIMATION', "Animation", "Export an animation", 1),
224 default='FRAME',
226 line_join_type: EnumProperty(
227 name="Line Join",
228 items=(
229 ('MITER', "Miter", "Corners are sharp", 0),
230 ('ROUND', "Round", "Corners are smoothed", 1),
231 ('BEVEL', "Bevel", "Corners are beveled", 2),
233 default='ROUND',
237 class SVGExporterPanel(bpy.types.Panel):
238 """Creates a Panel in the render context of the properties editor"""
239 bl_idname = "RENDER_PT_SVGExporterPanel"
240 bl_space_type = 'PROPERTIES'
241 bl_label = "Freestyle SVG Export"
242 bl_region_type = 'WINDOW'
243 bl_context = "render"
245 def draw_header(self, context):
246 self.layout.prop(context.scene.svg_export, "use_svg_export", text="")
248 def draw(self, context):
249 layout = self.layout
251 scene = context.scene
252 svg = scene.svg_export
253 freestyle = context.window.view_layer.freestyle_settings
255 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
257 row = layout.row()
258 row.prop(svg, "mode", expand=True)
260 row = layout.row()
261 row.prop(svg, "split_at_invisible")
262 row.prop(svg, "object_fill")
264 row = layout.row()
265 row.prop(svg, "line_join_type", expand=True)
268 @persistent
269 def svg_export_header(scene):
270 if not (scene.render.use_freestyle and scene.svg_export.use_svg_export):
271 return
273 # write the header only for the first frame when animation is being rendered
274 if not is_preview_render(scene) and scene.frame_current != scene.frame_start:
275 return
277 # this may fail still. The error is printed to the console.
278 with open(create_path(scene), "w") as f:
279 f.write(svg_primitive.format(render_width(scene), render_height(scene)))
282 @persistent
283 def svg_export_animation(scene):
284 """makes an animation of the exported SVG file """
285 render = scene.render
286 svg = scene.svg_export
288 if render.use_freestyle and svg.use_svg_export and not is_preview_render(scene):
289 write_animation(create_path(scene), scene.frame_start, render.fps)
292 def write_animation(filepath, frame_begin, fps):
293 """Adds animate tags to the specified file."""
294 tree = et.parse(filepath)
295 root = tree.getroot()
297 linesets = find_svg_elem(tree, ".//svg:g[@inkscape:groupmode='lineset']", all=True)
298 for i, lineset in enumerate(linesets):
299 name = lineset.get('id')
300 frames = find_svg_elem(lineset, ".//svg:g[@inkscape:groupmode='frame']", all=True)
301 n_of_frames = len(frames)
302 keyTimes = ";".join(str(round(x / n_of_frames, 3)) for x in range(n_of_frames)) + ";1"
304 style = {
305 'attributeName': 'display',
306 'values': "none;" * (n_of_frames - 1) + "inline;none",
307 'repeatCount': 'indefinite',
308 'keyTimes': keyTimes,
309 'dur': "{:.3f}s".format(n_of_frames / fps),
312 for j, frame in enumerate(frames):
313 id = 'anim_{}_{:06n}'.format(name, j + frame_begin)
314 # create animate tag
315 frame_anim = et.XML('<animate id="{}" begin="{:.3f}s" />'.format(id, (j - n_of_frames) / fps))
316 # add per-lineset style attributes
317 frame_anim.attrib.update(style)
318 # add to the current frame
319 frame.append(frame_anim)
321 # write SVG to file
322 indent_xml(root)
323 tree.write(filepath, encoding='ascii', xml_declaration=True)
326 # - StrokeShaders - #
327 class SVGPathShader(StrokeShader):
328 """Stroke Shader for writing stroke data to a .svg file."""
329 def __init__(self, name, style, filepath, res_y, split_at_invisible, stroke_color_mode, frame_current):
330 StrokeShader.__init__(self)
331 # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
332 self._name = name
333 self.filepath = filepath
334 self.h = res_y
335 self.frame_current = frame_current
336 self.elements = []
337 self.split_at_invisible = split_at_invisible
338 self.stroke_color_mode = stroke_color_mode # BASE | FIRST | LAST
339 self.style = style
342 @classmethod
343 def from_lineset(cls, lineset, filepath, res_y, split_at_invisible, use_stroke_color, frame_current, *, name=""):
344 """Builds a SVGPathShader using data from the given lineset"""
345 name = name or lineset.name
346 linestyle = lineset.linestyle
347 # extract style attributes from the linestyle and scene
348 svg = getCurrentScene().svg_export
349 style = {
350 'fill': 'none',
351 'stroke-width': linestyle.thickness,
352 'stroke-linecap': linestyle.caps.lower(),
353 'stroke-opacity': linestyle.alpha,
354 'stroke': format_rgb(linestyle.color),
355 'stroke-linejoin': svg.line_join_type.lower(),
357 # get dashed line pattern (if specified)
358 if linestyle.use_dashed_line:
359 style['stroke-dasharray'] = ",".join(str(elem) for elem in get_dashed_pattern(linestyle))
360 # return instance
361 return cls(name, style, filepath, res_y, split_at_invisible, use_stroke_color, frame_current)
364 @staticmethod
365 def pathgen(stroke, style, height, split_at_invisible, stroke_color_mode, f=lambda v: not v.attribute.visible):
366 """Generator that creates SVG paths (as strings) from the current stroke """
367 if len(stroke) <= 1:
368 return ""
370 if stroke_color_mode != 'BASE':
371 # try to use the color of the first or last vertex
372 try:
373 index = 0 if stroke_color_mode == 'FIRST' else -1
374 color = format_rgb(stroke[index].attribute.color)
375 style["stroke"] = color
376 except (ValueError, IndexError):
377 # default is linestyle base color
378 pass
380 # put style attributes into a single svg path definition
381 path = '\n<path ' + "".join('{}="{}" '.format(k, v) for k, v in style.items()) + 'd=" M '
383 it = iter(stroke)
384 # start first path
385 yield path
386 for v in it:
387 x, y = v.point
388 yield '{:.3f}, {:.3f} '.format(x, height - y)
389 if split_at_invisible and v.attribute.visible is False:
390 # end current and start new path;
391 yield '" />' + path
392 # fast-forward till the next visible vertex
393 it = itertools.dropwhile(f, it)
394 # yield next visible vertex
395 svert = next(it, None)
396 if svert is None:
397 break
398 x, y = svert.point
399 yield '{:.3f}, {:.3f} '.format(x, height - y)
400 # close current path
401 yield '" />'
403 def shade(self, stroke):
404 stroke_to_paths = "".join(self.pathgen(stroke, self.style, self.h, self.split_at_invisible, self.stroke_color_mode)).split("\n")
405 # convert to actual XML. Empty strokes are empty strings; they are ignored.
406 self.elements.extend(et.XML(elem) for elem in stroke_to_paths if elem) # if len(elem.strip()) > len(self.path))
408 def write(self):
409 """Write SVG data tree to file """
410 tree = et.parse(self.filepath)
411 root = tree.getroot()
412 name = self._name
413 scene = bpy.context.scene
415 # create <g> for lineset as a whole (don't overwrite)
416 # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills.
417 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(name))
418 if lineset_group is None:
419 lineset_group = et.XML('<g/>')
420 lineset_group.attrib = {
421 'id': name,
422 'xmlns:inkscape': namespaces["inkscape"],
423 'inkscape:groupmode': 'lineset',
424 'inkscape:label': name,
426 root.append(lineset_group)
428 # create <g> for the current frame
429 id = "frame_{:04n}".format(self.frame_current)
431 stroke_group = et.XML("<g/>")
432 stroke_group.attrib = {
433 'xmlns:inkscape': namespaces["inkscape"],
434 'inkscape:groupmode': 'layer',
435 'id': 'strokes',
436 'inkscape:label': 'strokes'
438 # nest the structure
439 stroke_group.extend(self.elements)
440 if scene.svg_export.mode == 'ANIMATION':
441 frame_group = et.XML("<g/>")
442 frame_group.attrib = {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id}
443 frame_group.append(stroke_group)
444 lineset_group.append(frame_group)
445 else:
446 lineset_group.append(stroke_group)
448 # write SVG to file
449 print("SVG Export: writing to", self.filepath)
450 indent_xml(root)
451 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
454 class SVGFillBuilder:
455 def __init__(self, filepath, height, name):
456 self.filepath = filepath
457 self._name = name
458 self.stroke_to_fill = partial(self.stroke_to_svg, height=height)
460 @staticmethod
461 def pathgen(vertices, path, height):
462 yield path
463 for point in vertices:
464 x, y = point
465 yield '{:.3f}, {:.3f} '.format(x, height - y)
466 yield ' z" />' # closes the path; connects the current to the first point
469 @staticmethod
470 def get_merged_strokes(strokes):
471 def extend_stroke(stroke, vertices):
472 for vert in map(StrokeVertex, vertices):
473 stroke.insert_vertex(vert, stroke.stroke_vertices_end())
474 return stroke
476 base_strokes = tuple(stroke for stroke in strokes if not is_poly_clockwise(stroke))
477 merged_strokes = OrderedDict((s, list()) for s in base_strokes)
479 for stroke in filter(is_poly_clockwise, strokes):
480 for base in base_strokes:
481 # don't merge when diffuse colors don't match
482 if diffuse_from_stroke(stroke) != diffuse_from_stroke(stroke):
483 continue
484 # only merge when the 'hole' is inside the base
485 elif stroke_inside_stroke(stroke, base):
486 merged_strokes[base].append(stroke)
487 break
488 # if it isn't a hole, it is likely that there are two strokes belonging
489 # to the same object separated by another object. let's try to join them
490 elif (get_object_name(base) == get_object_name(stroke) and
491 diffuse_from_stroke(stroke) == diffuse_from_stroke(stroke)):
492 base = extend_stroke(base, (sv for sv in stroke))
493 break
494 else:
495 # if all else fails, treat this stroke as a base stroke
496 merged_strokes.update({stroke: []})
497 return merged_strokes
500 def stroke_to_svg(self, stroke, height, parameters=None):
501 if parameters is None:
502 *color, alpha = diffuse_from_stroke(stroke)
503 color = tuple(int(255 * c) for c in color)
504 parameters = {
505 'fill_rule': 'evenodd',
506 'stroke': 'none',
507 'fill-opacity': alpha,
508 'fill': 'rgb' + repr(color),
510 param_str = " ".join('{}="{}"'.format(k, v) for k, v in parameters.items())
511 path = '<path {} d=" M '.format(param_str)
512 vertices = (svert.point for svert in stroke)
513 s = "".join(self.pathgen(vertices, path, height))
514 result = et.XML(s)
515 return result
517 def create_fill_elements(self, strokes):
518 """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
519 merged_strokes = self.get_merged_strokes(strokes)
520 for k, v in merged_strokes.items():
521 base = self.stroke_to_fill(k)
522 fills = (self.stroke_to_fill(stroke).get("d") for stroke in v)
523 merged_points = " ".join(fills)
524 base.attrib['d'] += merged_points
525 yield base
527 def write(self, strokes):
528 """Write SVG data tree to file """
530 tree = et.parse(self.filepath)
531 root = tree.getroot()
532 scene = bpy.context.scene
533 name = self._name
535 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(self._name))
536 if lineset_group is None:
537 lineset_group = et.XML('<g/>')
538 lineset_group.attrib = {
539 'id': name,
540 'xmlns:inkscape': namespaces["inkscape"],
541 'inkscape:groupmode': 'lineset',
542 'inkscape:label': name,
544 root.append(lineset_group)
545 print('added new lineset group ', name)
548 # <g> for the fills of the current frame
549 fill_group = et.XML('<g/>')
550 fill_group.attrib = {
551 'xmlns:inkscape': namespaces["inkscape"],
552 'inkscape:groupmode': 'layer',
553 'inkscape:label': 'fills',
554 'id': 'fills'
557 fill_elements = self.create_fill_elements(strokes)
558 fill_group.extend(reversed(tuple(fill_elements)))
559 if scene.svg_export.mode == 'ANIMATION':
560 # add the fills to the <g> of the current frame
561 frame_group = find_svg_elem(lineset_group, ".//svg:g[@id='frame_{:04n}']".format(scene.frame_current))
562 frame_group.insert(0, fill_group)
563 else:
564 lineset_group.insert(0, fill_group)
566 # write SVG to file
567 indent_xml(root)
568 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
571 def stroke_inside_stroke(a, b):
572 box_a = BoundingBox.from_sequence(svert.point for svert in a)
573 box_b = BoundingBox.from_sequence(svert.point for svert in b)
574 return box_a.inside(box_b)
577 def diffuse_from_stroke(stroke, curvemat=CurveMaterialF0D()):
578 material = curvemat(Interface0DIterator(stroke))
579 return material.diffuse
581 # - Callbacks - #
582 class ParameterEditorCallback(object):
583 """Object to store callbacks for the Parameter Editor in"""
584 def lineset_pre(self, scene, layer, lineset):
585 raise NotImplementedError()
587 def modifier_post(self, scene, layer, lineset):
588 raise NotImplementedError()
590 def lineset_post(self, scene, layer, lineset):
591 raise NotImplementedError()
595 class SVGPathShaderCallback(ParameterEditorCallback):
596 @classmethod
597 def poll(cls, scene, linestyle):
598 return scene.render.use_freestyle and scene.svg_export.use_svg_export and linestyle.use_export_strokes
600 @classmethod
601 def modifier_post(cls, scene, layer, lineset):
602 if not cls.poll(scene, lineset.linestyle):
603 return []
605 split = scene.svg_export.split_at_invisible
606 stroke_color_mode = lineset.linestyle.stroke_color_mode
607 cls.shader = SVGPathShader.from_lineset(
608 lineset, create_path(scene),
609 render_height(scene), split, stroke_color_mode, scene.frame_current, name=layer.name + '_' + lineset.name)
610 return [cls.shader]
612 @classmethod
613 def lineset_post(cls, scene, layer, lineset):
614 if not cls.poll(scene, lineset.linestyle):
615 return []
616 cls.shader.write()
619 class SVGFillShaderCallback(ParameterEditorCallback):
620 @classmethod
621 def poll(cls, scene, linestyle):
622 return scene.render.use_freestyle and scene.svg_export.use_svg_export and scene.svg_export.object_fill and linestyle.use_export_fills
624 @classmethod
625 def lineset_post(cls, scene, layer, lineset):
626 if not cls.poll(scene, lineset.linestyle):
627 return
629 # reset the stroke selection (but don't delete the already generated strokes)
630 Operators.reset(delete_strokes=False)
631 # Unary Predicates: visible and correct edge nature
632 upred = AndUP1D(
633 QuantitativeInvisibilityUP1D(0),
634 OrUP1D(ExternalContourUP1D(),
635 pyNatureUP1D(Nature.BORDER)),
637 # select the new edges
638 Operators.select(upred)
639 # Binary Predicates
640 bpred = AndBP1D(
641 MaterialBP1D(),
642 NotBP1D(pyZDiscontinuityBP1D()),
644 bpred = OrBP1D(bpred, AndBP1D(NotBP1D(bpred), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
645 # chain the edges
646 Operators.bidirectional_chain(ChainPredicateIterator(upred, bpred))
647 # export SVG
648 collector = StrokeCollector()
649 Operators.create(TrueUP1D(), [collector])
651 builder = SVGFillBuilder(create_path(scene), render_height(scene), layer.name + '_' + lineset.name)
652 builder.write(collector.strokes)
653 # make strokes used for filling invisible
654 for stroke in collector.strokes:
655 for svert in stroke:
656 svert.attribute.visible = False
660 def indent_xml(elem, level=0, indentsize=4):
661 """Prettifies XML code (used in SVG exporter) """
662 i = "\n" + level * " " * indentsize
663 if len(elem):
664 if not elem.text or not elem.text.strip():
665 elem.text = i + " " * indentsize
666 if not elem.tail or not elem.tail.strip():
667 elem.tail = i
668 for elem in elem:
669 indent_xml(elem, level + 1)
670 if not elem.tail or not elem.tail.strip():
671 elem.tail = i
672 elif level and (not elem.tail or not elem.tail.strip()):
673 elem.tail = i
676 def register_namespaces(namespaces=namespaces):
677 for name, url in namespaces.items():
678 if name != 'svg': # creates invalid xml
679 et.register_namespace(name, url)
681 @persistent
682 def handle_versions(self):
683 # We don't modify startup file because it assumes to
684 # have all the default values only.
685 if not bpy.data.is_saved:
686 return
688 # Revision https://developer.blender.org/rBA861519e44adc5674545fa18202dc43c4c20f2d1d
689 # changed the default for fills.
690 # fix by Sergey https://developer.blender.org/T46150
691 if bpy.data.version <= (2, 76, 0):
692 for linestyle in bpy.data.linestyles:
693 linestyle.use_export_fills = True
697 classes = (
698 SVGExporterPanel,
699 SVGExporterLinesetPanel,
700 SVGExport,
704 def register():
705 linestyle = bpy.types.FreestyleLineStyle
706 linestyle.use_export_strokes = BoolProperty(
707 name="Export Strokes",
708 description="Export strokes for this Line Style",
709 default=True,
711 linestyle.stroke_color_mode = EnumProperty(
712 name="Stroke Color Mode",
713 items=(
714 ('BASE', "Base Color", "Use the linestyle's base color", 0),
715 ('FIRST', "First Vertex", "Use the color of a stroke's first vertex", 1),
716 ('FINAL', "Final Vertex", "Use the color of a stroke's final vertex", 2),
718 default='BASE',
720 linestyle.use_export_fills = BoolProperty(
721 name="Export Fills",
722 description="Export fills for this Line Style",
723 default=False,
726 for cls in classes:
727 bpy.utils.register_class(cls)
728 bpy.types.Scene.svg_export = PointerProperty(type=SVGExport)
731 # add callbacks
732 bpy.app.handlers.render_init.append(render_init)
733 bpy.app.handlers.render_write.append(render_write)
734 bpy.app.handlers.render_pre.append(svg_export_header)
735 bpy.app.handlers.render_complete.append(svg_export_animation)
737 # manipulate shaders list
738 parameter_editor.callbacks_modifiers_post.append(SVGPathShaderCallback.modifier_post)
739 parameter_editor.callbacks_lineset_post.append(SVGPathShaderCallback.lineset_post)
740 parameter_editor.callbacks_lineset_post.append(SVGFillShaderCallback.lineset_post)
742 # register namespaces
743 register_namespaces()
745 # handle regressions
746 bpy.app.handlers.version_update.append(handle_versions)
749 def unregister():
751 for cls in classes:
752 bpy.utils.unregister_class(cls)
753 del bpy.types.Scene.svg_export
754 linestyle = bpy.types.FreestyleLineStyle
755 del linestyle.use_export_strokes
756 del linestyle.use_export_fills
758 # remove callbacks
759 bpy.app.handlers.render_init.remove(render_init)
760 bpy.app.handlers.render_write.remove(render_write)
761 bpy.app.handlers.render_pre.remove(svg_export_header)
762 bpy.app.handlers.render_complete.remove(svg_export_animation)
764 # manipulate shaders list
765 parameter_editor.callbacks_modifiers_post.remove(SVGPathShaderCallback.modifier_post)
766 parameter_editor.callbacks_lineset_post.remove(SVGPathShaderCallback.lineset_post)
767 parameter_editor.callbacks_lineset_post.remove(SVGFillShaderCallback.lineset_post)
769 bpy.app.handlers.version_update.remove(handle_versions)
772 if __name__ == "__main__":
773 register()