glTF exporter: Manage all 4 types of Vertex Colors (corner/point - byte/float)
[blender-addons.git] / render_freestyle_svg.py
blobfb8836277f12ae4e602035b8ec9af2d81272a29a
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Freestyle SVG Exporter",
5 "author": "Folkert de Vries",
6 "version": (1, 0),
7 "blender": (2, 80, 0),
8 "location": "Properties > Render > Freestyle SVG Export",
9 "description": "Exports Freestyle's stylized edges in SVG format",
10 "warning": "",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/render/render_freestyle_svg.html",
12 "support": 'OFFICIAL',
13 "category": "Render",
16 import bpy
17 import parameter_editor
18 import itertools
19 import os
21 import xml.etree.cElementTree as et
23 from bpy.app.handlers import persistent
24 from collections import OrderedDict
25 from functools import partial
26 from mathutils import Vector
28 from freestyle.types import (
29 StrokeShader,
30 Interface0DIterator,
31 Operators,
32 Nature,
33 StrokeVertex,
35 from freestyle.utils import (
36 getCurrentScene,
37 BoundingBox,
38 is_poly_clockwise,
39 StrokeCollector,
40 material_from_fedge,
41 get_object_name,
43 from freestyle.functions import (
44 GetShapeF1D,
45 CurveMaterialF0D,
47 from freestyle.predicates import (
48 AndBP1D,
49 AndUP1D,
50 ContourUP1D,
51 ExternalContourUP1D,
52 MaterialBP1D,
53 NotBP1D,
54 NotUP1D,
55 OrBP1D,
56 OrUP1D,
57 pyNatureUP1D,
58 pyZBP1D,
59 pyZDiscontinuityBP1D,
60 QuantitativeInvisibilityUP1D,
61 SameShapeIdBP1D,
62 TrueBP1D,
63 TrueUP1D,
65 from freestyle.chainingiterators import ChainPredicateIterator
66 from parameter_editor import get_dashed_pattern
68 from bpy.props import (
69 BoolProperty,
70 EnumProperty,
71 PointerProperty,
75 # use utf-8 here to keep ElementTree happy, end result is utf-16
76 svg_primitive = """<?xml version="1.0" encoding="ascii" standalone="no"?>
77 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
78 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
79 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{:d}" height="{:d}">
80 </svg>"""
83 # xml namespaces
84 namespaces = {
85 "inkscape": "http://www.inkscape.org/namespaces/inkscape",
86 "svg": "http://www.w3.org/2000/svg",
87 "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
88 "": "http://www.w3.org/2000/svg",
92 # wrap XMLElem.find, so the namespaces don't need to be given as an argument
93 def find_xml_elem(obj, search, namespaces, *, all=False):
94 if all:
95 return obj.findall(search, namespaces=namespaces)
96 return obj.find(search, namespaces=namespaces)
98 find_svg_elem = partial(find_xml_elem, namespaces=namespaces)
101 def render_height(scene):
102 return int(scene.render.resolution_y * scene.render.resolution_percentage / 100)
105 def render_width(scene):
106 return int(scene.render.resolution_x * scene.render.resolution_percentage / 100)
109 def format_rgb(color):
110 return 'rgb({}, {}, {})'.format(*(int(v * 255) for v in color))
113 # stores the state of the render, used to differ between animation and single frame renders.
114 class RenderState:
116 # Note that this flag is set to False only after the first frame
117 # has been written to file.
118 is_preview = True
121 @persistent
122 def render_init(scene):
123 RenderState.is_preview = True
126 @persistent
127 def render_write(scene):
128 RenderState.is_preview = False
131 def is_preview_render(scene):
132 return RenderState.is_preview or scene.svg_export.mode == 'FRAME'
135 def create_path(scene):
136 """Creates the output path for the svg file"""
137 path = os.path.dirname(scene.render.frame_path())
138 file_dir_path = os.path.dirname(bpy.data.filepath)
140 # try to use the given path if it is absolute
141 if os.path.isabs(path):
142 dirname = path
144 # otherwise, use current file's location as a start for the relative path
145 elif bpy.data.is_saved and file_dir_path:
146 dirname = os.path.normpath(os.path.join(file_dir_path, path))
148 # otherwise, use the folder from which blender was called as the start
149 else:
150 dirname = os.path.abspath(bpy.path.abspath(path))
153 basename = bpy.path.basename(scene.render.filepath)
154 if scene.svg_export.mode == 'FRAME':
155 frame = "{:04d}".format(scene.frame_current)
156 else:
157 frame = "{:04d}-{:04d}".format(scene.frame_start, scene.frame_end)
159 return os.path.join(dirname, basename + frame + ".svg")
162 class SVGExporterLinesetPanel(bpy.types.Panel):
163 """Creates a Panel in the Render Layers context of the properties editor"""
164 bl_idname = "RENDER_PT_SVGExporterLinesetPanel"
165 bl_space_type = 'PROPERTIES'
166 bl_label = "Freestyle Line Style SVG Export"
167 bl_region_type = 'WINDOW'
168 bl_context = "view_layer"
170 def draw(self, context):
171 layout = self.layout
173 scene = context.scene
174 svg = scene.svg_export
175 freestyle = context.window.view_layer.freestyle_settings
177 try:
178 linestyle = freestyle.linesets.active.linestyle
180 except AttributeError:
181 # Linestyles can be removed, so 0 linestyles is possible.
182 # there is nothing to draw in those cases.
183 # see https://developer.blender.org/T49855
184 return
186 else:
187 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
188 row = layout.row()
189 column = row.column()
190 column.prop(linestyle, 'use_export_strokes')
192 column = row.column()
193 column.active = svg.object_fill
194 column.prop(linestyle, 'use_export_fills')
196 row = layout.row()
197 row.prop(linestyle, "stroke_color_mode", expand=True)
200 class SVGExport(bpy.types.PropertyGroup):
201 """Implements the properties for the SVG exporter"""
202 bl_idname = "RENDER_PT_svg_export"
204 use_svg_export: BoolProperty(
205 name="SVG Export",
206 description="Export Freestyle edges to an .svg format",
208 split_at_invisible: BoolProperty(
209 name="Split at Invisible",
210 description="Split the stroke at an invisible vertex",
212 object_fill: BoolProperty(
213 name="Fill Contours",
214 description="Fill the contour with the object's material color",
216 mode: EnumProperty(
217 name="Mode",
218 items=(
219 ('FRAME', "Frame", "Export a single frame", 0),
220 ('ANIMATION', "Animation", "Export an animation", 1),
222 default='FRAME',
224 line_join_type: EnumProperty(
225 name="Line Join",
226 items=(
227 ('MITER', "Miter", "Corners are sharp", 0),
228 ('ROUND', "Round", "Corners are smoothed", 1),
229 ('BEVEL', "Bevel", "Corners are beveled", 2),
231 default='ROUND',
235 class SVGExporterPanel(bpy.types.Panel):
236 """Creates a Panel in the render context of the properties editor"""
237 bl_idname = "RENDER_PT_SVGExporterPanel"
238 bl_space_type = 'PROPERTIES'
239 bl_label = "Freestyle SVG Export"
240 bl_region_type = 'WINDOW'
241 bl_context = "render"
243 def draw_header(self, context):
244 self.layout.prop(context.scene.svg_export, "use_svg_export", text="")
246 def draw(self, context):
247 layout = self.layout
249 scene = context.scene
250 svg = scene.svg_export
251 freestyle = context.window.view_layer.freestyle_settings
253 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
255 row = layout.row()
256 row.prop(svg, "mode", expand=True)
258 row = layout.row()
259 row.prop(svg, "split_at_invisible")
260 row.prop(svg, "object_fill")
262 row = layout.row()
263 row.prop(svg, "line_join_type", expand=True)
266 @persistent
267 def svg_export_header(scene):
268 if not (scene.render.use_freestyle and scene.svg_export.use_svg_export):
269 return
271 # write the header only for the first frame when animation is being rendered
272 if not is_preview_render(scene) and scene.frame_current != scene.frame_start:
273 return
275 # this may fail still. The error is printed to the console.
276 with open(create_path(scene), "w") as f:
277 f.write(svg_primitive.format(render_width(scene), render_height(scene)))
280 @persistent
281 def svg_export_animation(scene):
282 """makes an animation of the exported SVG file """
283 render = scene.render
284 svg = scene.svg_export
286 if render.use_freestyle and svg.use_svg_export and not is_preview_render(scene):
287 write_animation(create_path(scene), scene.frame_start, render.fps)
290 def write_animation(filepath, frame_begin, fps):
291 """Adds animate tags to the specified file."""
292 tree = et.parse(filepath)
293 root = tree.getroot()
295 linesets = find_svg_elem(tree, ".//svg:g[@inkscape:groupmode='lineset']", all=True)
296 for i, lineset in enumerate(linesets):
297 name = lineset.get('id')
298 frames = find_svg_elem(lineset, ".//svg:g[@inkscape:groupmode='frame']", all=True)
299 n_of_frames = len(frames)
300 keyTimes = ";".join(str(round(x / n_of_frames, 3)) for x in range(n_of_frames)) + ";1"
302 style = {
303 'attributeName': 'display',
304 'values': "none;" * (n_of_frames - 1) + "inline;none",
305 'repeatCount': 'indefinite',
306 'keyTimes': keyTimes,
307 'dur': "{:.3f}s".format(n_of_frames / fps),
310 for j, frame in enumerate(frames):
311 id = 'anim_{}_{:06n}'.format(name, j + frame_begin)
312 # create animate tag
313 frame_anim = et.XML('<animate id="{}" begin="{:.3f}s" />'.format(id, (j - n_of_frames) / fps))
314 # add per-lineset style attributes
315 frame_anim.attrib.update(style)
316 # add to the current frame
317 frame.append(frame_anim)
319 # write SVG to file
320 indent_xml(root)
321 tree.write(filepath, encoding='ascii', xml_declaration=True)
324 # - StrokeShaders - #
325 class SVGPathShader(StrokeShader):
326 """Stroke Shader for writing stroke data to a .svg file."""
327 def __init__(self, name, style, filepath, res_y, split_at_invisible, stroke_color_mode, frame_current):
328 StrokeShader.__init__(self)
329 # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
330 self._name = name
331 self.filepath = filepath
332 self.h = res_y
333 self.frame_current = frame_current
334 self.elements = []
335 self.split_at_invisible = split_at_invisible
336 self.stroke_color_mode = stroke_color_mode # BASE | FIRST | LAST
337 self.style = style
340 @classmethod
341 def from_lineset(cls, lineset, filepath, res_y, split_at_invisible, use_stroke_color, frame_current, *, name=""):
342 """Builds a SVGPathShader using data from the given lineset"""
343 name = name or lineset.name
344 linestyle = lineset.linestyle
345 # extract style attributes from the linestyle and scene
346 svg = getCurrentScene().svg_export
347 style = {
348 'fill': 'none',
349 'stroke-width': linestyle.thickness,
350 'stroke-linecap': linestyle.caps.lower(),
351 'stroke-opacity': linestyle.alpha,
352 'stroke': format_rgb(linestyle.color),
353 'stroke-linejoin': svg.line_join_type.lower(),
355 # get dashed line pattern (if specified)
356 if linestyle.use_dashed_line:
357 style['stroke-dasharray'] = ",".join(str(elem) for elem in get_dashed_pattern(linestyle))
358 # return instance
359 return cls(name, style, filepath, res_y, split_at_invisible, use_stroke_color, frame_current)
362 @staticmethod
363 def pathgen(stroke, style, height, split_at_invisible, stroke_color_mode, f=lambda v: not v.attribute.visible):
364 """Generator that creates SVG paths (as strings) from the current stroke """
365 if len(stroke) <= 1:
366 return ""
368 if stroke_color_mode != 'BASE':
369 # try to use the color of the first or last vertex
370 try:
371 index = 0 if stroke_color_mode == 'FIRST' else -1
372 color = format_rgb(stroke[index].attribute.color)
373 style["stroke"] = color
374 except (ValueError, IndexError):
375 # default is linestyle base color
376 pass
378 # put style attributes into a single svg path definition
379 path = '\n<path ' + "".join('{}="{}" '.format(k, v) for k, v in style.items()) + 'd=" M '
381 it = iter(stroke)
382 # start first path
383 yield path
384 for v in it:
385 x, y = v.point
386 yield '{:.3f}, {:.3f} '.format(x, height - y)
387 if split_at_invisible and v.attribute.visible is False:
388 # end current and start new path;
389 yield '" />' + path
390 # fast-forward till the next visible vertex
391 it = itertools.dropwhile(f, it)
392 # yield next visible vertex
393 svert = next(it, None)
394 if svert is None:
395 break
396 x, y = svert.point
397 yield '{:.3f}, {:.3f} '.format(x, height - y)
398 # close current path
399 yield '" />'
401 def shade(self, stroke):
402 stroke_to_paths = "".join(self.pathgen(stroke, self.style, self.h, self.split_at_invisible, self.stroke_color_mode)).split("\n")
403 # convert to actual XML. Empty strokes are empty strings; they are ignored.
404 self.elements.extend(et.XML(elem) for elem in stroke_to_paths if elem) # if len(elem.strip()) > len(self.path))
406 def write(self):
407 """Write SVG data tree to file """
408 tree = et.parse(self.filepath)
409 root = tree.getroot()
410 name = self._name
411 scene = bpy.context.scene
413 # create <g> for lineset as a whole (don't overwrite)
414 # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills.
415 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(name))
416 if lineset_group is None:
417 lineset_group = et.XML('<g/>')
418 lineset_group.attrib = {
419 'id': name,
420 'xmlns:inkscape': namespaces["inkscape"],
421 'inkscape:groupmode': 'lineset',
422 'inkscape:label': name,
424 root.append(lineset_group)
426 # create <g> for the current frame
427 id = "frame_{:04n}".format(self.frame_current)
429 stroke_group = et.XML("<g/>")
430 stroke_group.attrib = {
431 'xmlns:inkscape': namespaces["inkscape"],
432 'inkscape:groupmode': 'layer',
433 'id': 'strokes',
434 'inkscape:label': 'strokes'
436 # nest the structure
437 stroke_group.extend(self.elements)
438 if scene.svg_export.mode == 'ANIMATION':
439 frame_group = et.XML("<g/>")
440 frame_group.attrib = {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id}
441 frame_group.append(stroke_group)
442 lineset_group.append(frame_group)
443 else:
444 lineset_group.append(stroke_group)
446 # write SVG to file
447 print("SVG Export: writing to", self.filepath)
448 indent_xml(root)
449 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
452 class SVGFillBuilder:
453 def __init__(self, filepath, height, name):
454 self.filepath = filepath
455 self._name = name
456 self.stroke_to_fill = partial(self.stroke_to_svg, height=height)
458 @staticmethod
459 def pathgen(vertices, path, height):
460 yield path
461 for point in vertices:
462 x, y = point
463 yield '{:.3f}, {:.3f} '.format(x, height - y)
464 yield ' z" />' # closes the path; connects the current to the first point
467 @staticmethod
468 def get_merged_strokes(strokes):
469 def extend_stroke(stroke, vertices):
470 for vert in map(StrokeVertex, vertices):
471 stroke.insert_vertex(vert, stroke.stroke_vertices_end())
472 return stroke
474 base_strokes = tuple(stroke for stroke in strokes if not is_poly_clockwise(stroke))
475 merged_strokes = OrderedDict((s, list()) for s in base_strokes)
477 for stroke in filter(is_poly_clockwise, strokes):
478 for base in base_strokes:
479 # don't merge when diffuse colors don't match
480 if diffuse_from_stroke(stroke) != diffuse_from_stroke(stroke):
481 continue
482 # only merge when the 'hole' is inside the base
483 elif stroke_inside_stroke(stroke, base):
484 merged_strokes[base].append(stroke)
485 break
486 # if it isn't a hole, it is likely that there are two strokes belonging
487 # to the same object separated by another object. let's try to join them
488 elif (get_object_name(base) == get_object_name(stroke) and
489 diffuse_from_stroke(stroke) == diffuse_from_stroke(stroke)):
490 base = extend_stroke(base, (sv for sv in stroke))
491 break
492 else:
493 # if all else fails, treat this stroke as a base stroke
494 merged_strokes.update({stroke: []})
495 return merged_strokes
498 def stroke_to_svg(self, stroke, height, parameters=None):
499 if parameters is None:
500 *color, alpha = diffuse_from_stroke(stroke)
501 color = tuple(int(255 * c) for c in color)
502 parameters = {
503 'fill_rule': 'evenodd',
504 'stroke': 'none',
505 'fill-opacity': alpha,
506 'fill': 'rgb' + repr(color),
508 param_str = " ".join('{}="{}"'.format(k, v) for k, v in parameters.items())
509 path = '<path {} d=" M '.format(param_str)
510 vertices = (svert.point for svert in stroke)
511 s = "".join(self.pathgen(vertices, path, height))
512 result = et.XML(s)
513 return result
515 def create_fill_elements(self, strokes):
516 """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
517 merged_strokes = self.get_merged_strokes(strokes)
518 for k, v in merged_strokes.items():
519 base = self.stroke_to_fill(k)
520 fills = (self.stroke_to_fill(stroke).get("d") for stroke in v)
521 merged_points = " ".join(fills)
522 base.attrib['d'] += merged_points
523 yield base
525 def write(self, strokes):
526 """Write SVG data tree to file """
528 tree = et.parse(self.filepath)
529 root = tree.getroot()
530 scene = bpy.context.scene
531 name = self._name
533 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(self._name))
534 if lineset_group is None:
535 lineset_group = et.XML('<g/>')
536 lineset_group.attrib = {
537 'id': name,
538 'xmlns:inkscape': namespaces["inkscape"],
539 'inkscape:groupmode': 'lineset',
540 'inkscape:label': name,
542 root.append(lineset_group)
543 print('added new lineset group ', name)
546 # <g> for the fills of the current frame
547 fill_group = et.XML('<g/>')
548 fill_group.attrib = {
549 'xmlns:inkscape': namespaces["inkscape"],
550 'inkscape:groupmode': 'layer',
551 'inkscape:label': 'fills',
552 'id': 'fills'
555 fill_elements = self.create_fill_elements(strokes)
556 fill_group.extend(reversed(tuple(fill_elements)))
557 if scene.svg_export.mode == 'ANIMATION':
558 # add the fills to the <g> of the current frame
559 frame_group = find_svg_elem(lineset_group, ".//svg:g[@id='frame_{:04n}']".format(scene.frame_current))
560 frame_group.insert(0, fill_group)
561 else:
562 lineset_group.insert(0, fill_group)
564 # write SVG to file
565 indent_xml(root)
566 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
569 def stroke_inside_stroke(a, b):
570 box_a = BoundingBox.from_sequence(svert.point for svert in a)
571 box_b = BoundingBox.from_sequence(svert.point for svert in b)
572 return box_a.inside(box_b)
575 def diffuse_from_stroke(stroke, curvemat=CurveMaterialF0D()):
576 material = curvemat(Interface0DIterator(stroke))
577 return material.diffuse
579 # - Callbacks - #
580 class ParameterEditorCallback(object):
581 """Object to store callbacks for the Parameter Editor in"""
582 def lineset_pre(self, scene, layer, lineset):
583 raise NotImplementedError()
585 def modifier_post(self, scene, layer, lineset):
586 raise NotImplementedError()
588 def lineset_post(self, scene, layer, lineset):
589 raise NotImplementedError()
593 class SVGPathShaderCallback(ParameterEditorCallback):
594 @classmethod
595 def poll(cls, scene, linestyle):
596 return scene.render.use_freestyle and scene.svg_export.use_svg_export and linestyle.use_export_strokes
598 @classmethod
599 def modifier_post(cls, scene, layer, lineset):
600 if not cls.poll(scene, lineset.linestyle):
601 return []
603 split = scene.svg_export.split_at_invisible
604 stroke_color_mode = lineset.linestyle.stroke_color_mode
605 cls.shader = SVGPathShader.from_lineset(
606 lineset, create_path(scene),
607 render_height(scene), split, stroke_color_mode, scene.frame_current, name=layer.name + '_' + lineset.name)
608 return [cls.shader]
610 @classmethod
611 def lineset_post(cls, scene, layer, lineset):
612 if not cls.poll(scene, lineset.linestyle):
613 return []
614 cls.shader.write()
617 class SVGFillShaderCallback(ParameterEditorCallback):
618 @classmethod
619 def poll(cls, scene, linestyle):
620 return scene.render.use_freestyle and scene.svg_export.use_svg_export and scene.svg_export.object_fill and linestyle.use_export_fills
622 @classmethod
623 def lineset_post(cls, scene, layer, lineset):
624 if not cls.poll(scene, lineset.linestyle):
625 return
627 # reset the stroke selection (but don't delete the already generated strokes)
628 Operators.reset(delete_strokes=False)
629 # Unary Predicates: visible and correct edge nature
630 upred = AndUP1D(
631 QuantitativeInvisibilityUP1D(0),
632 OrUP1D(ExternalContourUP1D(),
633 pyNatureUP1D(Nature.BORDER)),
635 # select the new edges
636 Operators.select(upred)
637 # Binary Predicates
638 bpred = AndBP1D(
639 MaterialBP1D(),
640 NotBP1D(pyZDiscontinuityBP1D()),
642 bpred = OrBP1D(bpred, AndBP1D(NotBP1D(bpred), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
643 # chain the edges
644 Operators.bidirectional_chain(ChainPredicateIterator(upred, bpred))
645 # export SVG
646 collector = StrokeCollector()
647 Operators.create(TrueUP1D(), [collector])
649 builder = SVGFillBuilder(create_path(scene), render_height(scene), layer.name + '_' + lineset.name)
650 builder.write(collector.strokes)
651 # make strokes used for filling invisible
652 for stroke in collector.strokes:
653 for svert in stroke:
654 svert.attribute.visible = False
658 def indent_xml(elem, level=0, indentsize=4):
659 """Prettifies XML code (used in SVG exporter) """
660 i = "\n" + level * " " * indentsize
661 if len(elem):
662 if not elem.text or not elem.text.strip():
663 elem.text = i + " " * indentsize
664 if not elem.tail or not elem.tail.strip():
665 elem.tail = i
666 for elem in elem:
667 indent_xml(elem, level + 1)
668 if not elem.tail or not elem.tail.strip():
669 elem.tail = i
670 elif level and (not elem.tail or not elem.tail.strip()):
671 elem.tail = i
674 def register_namespaces(namespaces=namespaces):
675 for name, url in namespaces.items():
676 if name != 'svg': # creates invalid xml
677 et.register_namespace(name, url)
679 @persistent
680 def handle_versions(self):
681 # We don't modify startup file because it assumes to
682 # have all the default values only.
683 if not bpy.data.is_saved:
684 return
686 # Revision https://developer.blender.org/rBA861519e44adc5674545fa18202dc43c4c20f2d1d
687 # changed the default for fills.
688 # fix by Sergey https://developer.blender.org/T46150
689 if bpy.data.version <= (2, 76, 0):
690 for linestyle in bpy.data.linestyles:
691 linestyle.use_export_fills = True
695 classes = (
696 SVGExporterPanel,
697 SVGExporterLinesetPanel,
698 SVGExport,
702 def register():
703 linestyle = bpy.types.FreestyleLineStyle
704 linestyle.use_export_strokes = BoolProperty(
705 name="Export Strokes",
706 description="Export strokes for this Line Style",
707 default=True,
709 linestyle.stroke_color_mode = EnumProperty(
710 name="Stroke Color Mode",
711 items=(
712 ('BASE', "Base Color", "Use the linestyle's base color", 0),
713 ('FIRST', "First Vertex", "Use the color of a stroke's first vertex", 1),
714 ('FINAL', "Final Vertex", "Use the color of a stroke's final vertex", 2),
716 default='BASE',
718 linestyle.use_export_fills = BoolProperty(
719 name="Export Fills",
720 description="Export fills for this Line Style",
721 default=False,
724 for cls in classes:
725 bpy.utils.register_class(cls)
726 bpy.types.Scene.svg_export = PointerProperty(type=SVGExport)
729 # add callbacks
730 bpy.app.handlers.render_init.append(render_init)
731 bpy.app.handlers.render_write.append(render_write)
732 bpy.app.handlers.render_pre.append(svg_export_header)
733 bpy.app.handlers.render_complete.append(svg_export_animation)
735 # manipulate shaders list
736 parameter_editor.callbacks_modifiers_post.append(SVGPathShaderCallback.modifier_post)
737 parameter_editor.callbacks_lineset_post.append(SVGPathShaderCallback.lineset_post)
738 parameter_editor.callbacks_lineset_post.append(SVGFillShaderCallback.lineset_post)
740 # register namespaces
741 register_namespaces()
743 # handle regressions
744 bpy.app.handlers.version_update.append(handle_versions)
747 def unregister():
749 for cls in classes:
750 bpy.utils.unregister_class(cls)
751 del bpy.types.Scene.svg_export
752 linestyle = bpy.types.FreestyleLineStyle
753 del linestyle.use_export_strokes
754 del linestyle.use_export_fills
756 # remove callbacks
757 bpy.app.handlers.render_init.remove(render_init)
758 bpy.app.handlers.render_write.remove(render_write)
759 bpy.app.handlers.render_pre.remove(svg_export_header)
760 bpy.app.handlers.render_complete.remove(svg_export_animation)
762 # manipulate shaders list
763 parameter_editor.callbacks_modifiers_post.remove(SVGPathShaderCallback.modifier_post)
764 parameter_editor.callbacks_lineset_post.remove(SVGPathShaderCallback.lineset_post)
765 parameter_editor.callbacks_lineset_post.remove(SVGFillShaderCallback.lineset_post)
767 bpy.app.handlers.version_update.remove(handle_versions)
770 if __name__ == "__main__":
771 register()