Import images: add file handler
[blender-addons.git] / render_freestyle_svg.py
blob07b8fea4aa3f8d792e317402885fb0fe0f7b84c0
1 # SPDX-FileCopyrightText: 2014-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
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 os.makedirs(dirname, exist_ok=True)
163 return os.path.join(dirname, basename + frame + ".svg")
166 class SVGExporterLinesetPanel(bpy.types.Panel):
167 """Creates a panel in the View Layer context of the properties editor"""
168 bl_idname = "RENDER_PT_SVGExporterLinesetPanel"
169 bl_space_type = 'PROPERTIES'
170 bl_label = "Freestyle Line Style SVG Export"
171 bl_region_type = 'WINDOW'
172 bl_context = "view_layer"
174 def draw(self, context):
175 layout = self.layout
177 scene = context.scene
178 svg = scene.svg_export
179 freestyle = context.window.view_layer.freestyle_settings
181 try:
182 linestyle = freestyle.linesets.active.linestyle
184 except AttributeError:
185 # Linestyles can be removed, so 0 linestyles is possible.
186 # there is nothing to draw in those cases.
187 # see https://developer.blender.org/T49855
188 return
190 else:
191 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
192 row = layout.row()
193 column = row.column()
194 column.prop(linestyle, 'use_export_strokes')
196 column = row.column()
197 column.active = svg.object_fill
198 column.prop(linestyle, 'use_export_fills')
200 row = layout.row()
201 row.prop(linestyle, "stroke_color_mode", expand=True)
204 class SVGExport(bpy.types.PropertyGroup):
205 """Implements the properties for the SVG exporter"""
206 bl_idname = "RENDER_PT_svg_export"
208 use_svg_export: BoolProperty(
209 name="SVG Export",
210 description="Export Freestyle edges to an .svg format",
212 split_at_invisible: BoolProperty(
213 name="Split at Invisible",
214 description="Split the stroke at an invisible vertex",
216 object_fill: BoolProperty(
217 name="Fill Contours",
218 description="Fill the contour with the object's material color",
220 mode: EnumProperty(
221 name="Mode",
222 items=(
223 ('FRAME', "Frame", "Export a single frame", 0),
224 ('ANIMATION', "Animation", "Export an animation", 1),
226 default='FRAME',
228 line_join_type: EnumProperty(
229 name="Line Join",
230 items=(
231 ('MITER', "Miter", "Corners are sharp", 0),
232 ('ROUND', "Round", "Corners are smoothed", 1),
233 ('BEVEL', "Bevel", "Corners are beveled", 2),
235 default='ROUND',
239 class SVGExporterPanel(bpy.types.Panel):
240 """Creates a Panel in the render context of the properties editor"""
241 bl_idname = "RENDER_PT_SVGExporterPanel"
242 bl_space_type = 'PROPERTIES'
243 bl_label = "Freestyle SVG Export"
244 bl_region_type = 'WINDOW'
245 bl_context = "render"
247 def draw_header(self, context):
248 self.layout.prop(context.scene.svg_export, "use_svg_export", text="")
250 def draw(self, context):
251 layout = self.layout
253 scene = context.scene
254 svg = scene.svg_export
255 freestyle = context.window.view_layer.freestyle_settings
257 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
259 row = layout.row()
260 row.prop(svg, "mode", expand=True)
262 row = layout.row()
263 row.prop(svg, "split_at_invisible")
264 row.prop(svg, "object_fill")
266 row = layout.row()
267 row.prop(svg, "line_join_type", expand=True)
270 @persistent
271 def svg_export_header(scene):
272 if not (scene.render.use_freestyle and scene.svg_export.use_svg_export):
273 return
275 # write the header only for the first frame when animation is being rendered
276 if not is_preview_render(scene) and scene.frame_current != scene.frame_start:
277 return
279 # this may fail still. The error is printed to the console.
280 with open(create_path(scene), "w") as f:
281 f.write(svg_primitive.format(render_width(scene), render_height(scene)))
284 @persistent
285 def svg_export_animation(scene):
286 """makes an animation of the exported SVG file """
287 render = scene.render
288 svg = scene.svg_export
290 if render.use_freestyle and svg.use_svg_export and not is_preview_render(scene):
291 write_animation(create_path(scene), scene.frame_start, render.fps)
294 def write_animation(filepath, frame_begin, fps):
295 """Adds animate tags to the specified file."""
296 tree = et.parse(filepath)
297 root = tree.getroot()
299 linesets = find_svg_elem(tree, ".//svg:g[@inkscape:groupmode='lineset']", all=True)
300 for i, lineset in enumerate(linesets):
301 name = lineset.get('id')
302 frames = find_svg_elem(lineset, ".//svg:g[@inkscape:groupmode='frame']", all=True)
303 n_of_frames = len(frames)
304 keyTimes = ";".join(str(round(x / n_of_frames, 3)) for x in range(n_of_frames)) + ";1"
306 style = {
307 'attributeName': 'display',
308 'values': "none;" * (n_of_frames - 1) + "inline;none",
309 'repeatCount': 'indefinite',
310 'keyTimes': keyTimes,
311 'dur': "{:.3f}s".format(n_of_frames / fps),
314 for j, frame in enumerate(frames):
315 id = 'anim_{}_{:06n}'.format(name, j + frame_begin)
316 # create animate tag
317 frame_anim = et.XML('<animate id="{}" begin="{:.3f}s" />'.format(id, (j - n_of_frames) / fps))
318 # add per-lineset style attributes
319 frame_anim.attrib.update(style)
320 # add to the current frame
321 frame.append(frame_anim)
323 # write SVG to file
324 indent_xml(root)
325 tree.write(filepath, encoding='ascii', xml_declaration=True)
328 # - StrokeShaders - #
329 class SVGPathShader(StrokeShader):
330 """Stroke Shader for writing stroke data to a .svg file."""
331 def __init__(self, name, style, filepath, res_y, split_at_invisible, stroke_color_mode, frame_current):
332 StrokeShader.__init__(self)
333 # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
334 self._name = name
335 self.filepath = filepath
336 self.h = res_y
337 self.frame_current = frame_current
338 self.elements = []
339 self.split_at_invisible = split_at_invisible
340 self.stroke_color_mode = stroke_color_mode # BASE | FIRST | LAST
341 self.style = style
344 @classmethod
345 def from_lineset(cls, lineset, filepath, res_y, split_at_invisible, use_stroke_color, frame_current, *, name=""):
346 """Builds a SVGPathShader using data from the given lineset"""
347 name = name or lineset.name
348 linestyle = lineset.linestyle
349 # extract style attributes from the linestyle and scene
350 svg = getCurrentScene().svg_export
351 style = {
352 'fill': 'none',
353 'stroke-width': linestyle.thickness,
354 'stroke-linecap': linestyle.caps.lower(),
355 'stroke-opacity': linestyle.alpha,
356 'stroke': format_rgb(linestyle.color),
357 'stroke-linejoin': svg.line_join_type.lower(),
359 # get dashed line pattern (if specified)
360 if linestyle.use_dashed_line:
361 style['stroke-dasharray'] = ",".join(str(elem) for elem in get_dashed_pattern(linestyle))
362 # return instance
363 return cls(name, style, filepath, res_y, split_at_invisible, use_stroke_color, frame_current)
366 @staticmethod
367 def pathgen(stroke, style, height, split_at_invisible, stroke_color_mode, f=lambda v: not v.attribute.visible):
368 """Generator that creates SVG paths (as strings) from the current stroke """
369 if len(stroke) <= 1:
370 return ""
372 if stroke_color_mode != 'BASE':
373 # try to use the color of the first or last vertex
374 try:
375 index = 0 if stroke_color_mode == 'FIRST' else -1
376 color = format_rgb(stroke[index].attribute.color)
377 style["stroke"] = color
378 except (ValueError, IndexError):
379 # default is linestyle base color
380 pass
382 # put style attributes into a single svg path definition
383 path = '\n<path ' + "".join('{}="{}" '.format(k, v) for k, v in style.items()) + 'd=" M '
385 it = iter(stroke)
386 # start first path
387 yield path
388 for v in it:
389 x, y = v.point
390 yield '{:.3f}, {:.3f} '.format(x, height - y)
391 if split_at_invisible and v.attribute.visible is False:
392 # end current and start new path;
393 yield '" />' + path
394 # fast-forward till the next visible vertex
395 it = itertools.dropwhile(f, it)
396 # yield next visible vertex
397 svert = next(it, None)
398 if svert is None:
399 break
400 x, y = svert.point
401 yield '{:.3f}, {:.3f} '.format(x, height - y)
402 # close current path
403 yield '" />'
405 def shade(self, stroke):
406 stroke_to_paths = "".join(self.pathgen(stroke, self.style, self.h, self.split_at_invisible, self.stroke_color_mode)).split("\n")
407 # convert to actual XML. Empty strokes are empty strings; they are ignored.
408 self.elements.extend(et.XML(elem) for elem in stroke_to_paths if elem) # if len(elem.strip()) > len(self.path))
410 def write(self):
411 """Write SVG data tree to file """
412 tree = et.parse(self.filepath)
413 root = tree.getroot()
414 name = self._name
415 scene = bpy.context.scene
417 # create <g> for lineset as a whole (don't overwrite)
418 # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills.
419 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(name))
420 if lineset_group is None:
421 lineset_group = et.XML('<g/>')
422 lineset_group.attrib = {
423 'id': name,
424 'xmlns:inkscape': namespaces["inkscape"],
425 'inkscape:groupmode': 'lineset',
426 'inkscape:label': name,
428 root.append(lineset_group)
430 # create <g> for the current frame
431 id = "frame_{:04n}".format(self.frame_current)
433 stroke_group = et.XML("<g/>")
434 stroke_group.attrib = {
435 'xmlns:inkscape': namespaces["inkscape"],
436 'inkscape:groupmode': 'layer',
437 'id': 'strokes',
438 'inkscape:label': 'strokes'
440 # nest the structure
441 stroke_group.extend(self.elements)
442 if scene.svg_export.mode == 'ANIMATION':
443 frame_group = et.XML("<g/>")
444 frame_group.attrib = {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id}
445 frame_group.append(stroke_group)
446 lineset_group.append(frame_group)
447 else:
448 lineset_group.append(stroke_group)
450 # write SVG to file
451 print("SVG Export: writing to", self.filepath)
452 indent_xml(root)
453 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
456 class SVGFillBuilder:
457 def __init__(self, filepath, height, name):
458 self.filepath = filepath
459 self._name = name
460 self.stroke_to_fill = partial(self.stroke_to_svg, height=height)
462 @staticmethod
463 def pathgen(vertices, path, height):
464 yield path
465 for point in vertices:
466 x, y = point
467 yield '{:.3f}, {:.3f} '.format(x, height - y)
468 yield ' z" />' # closes the path; connects the current to the first point
471 @staticmethod
472 def get_merged_strokes(strokes):
473 def extend_stroke(stroke, vertices):
474 for vert in map(StrokeVertex, vertices):
475 stroke.insert_vertex(vert, stroke.stroke_vertices_end())
476 return stroke
478 base_strokes = tuple(stroke for stroke in strokes if not is_poly_clockwise(stroke))
479 merged_strokes = OrderedDict((s, list()) for s in base_strokes)
481 for stroke in filter(is_poly_clockwise, strokes):
482 for base in base_strokes:
483 # don't merge when diffuse colors don't match
484 if diffuse_from_stroke(stroke) != diffuse_from_stroke(stroke):
485 continue
486 # only merge when the 'hole' is inside the base
487 elif stroke_inside_stroke(stroke, base):
488 merged_strokes[base].append(stroke)
489 break
490 # if it isn't a hole, it is likely that there are two strokes belonging
491 # to the same object separated by another object. let's try to join them
492 elif (get_object_name(base) == get_object_name(stroke) and
493 diffuse_from_stroke(stroke) == diffuse_from_stroke(stroke)):
494 base = extend_stroke(base, (sv for sv in stroke))
495 break
496 else:
497 # if all else fails, treat this stroke as a base stroke
498 merged_strokes.update({stroke: []})
499 return merged_strokes
502 def stroke_to_svg(self, stroke, height, parameters=None):
503 if parameters is None:
504 *color, alpha = diffuse_from_stroke(stroke)
505 color = tuple(int(255 * c) for c in color)
506 parameters = {
507 'fill_rule': 'evenodd',
508 'stroke': 'none',
509 'fill-opacity': alpha,
510 'fill': 'rgb' + repr(color),
512 param_str = " ".join('{}="{}"'.format(k, v) for k, v in parameters.items())
513 path = '<path {} d=" M '.format(param_str)
514 vertices = (svert.point for svert in stroke)
515 s = "".join(self.pathgen(vertices, path, height))
516 result = et.XML(s)
517 return result
519 def create_fill_elements(self, strokes):
520 """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
521 merged_strokes = self.get_merged_strokes(strokes)
522 for k, v in merged_strokes.items():
523 base = self.stroke_to_fill(k)
524 fills = (self.stroke_to_fill(stroke).get("d") for stroke in v)
525 merged_points = " ".join(fills)
526 base.attrib['d'] += merged_points
527 yield base
529 def write(self, strokes):
530 """Write SVG data tree to file """
532 tree = et.parse(self.filepath)
533 root = tree.getroot()
534 scene = bpy.context.scene
535 name = self._name
537 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(self._name))
538 if lineset_group is None:
539 lineset_group = et.XML('<g/>')
540 lineset_group.attrib = {
541 'id': name,
542 'xmlns:inkscape': namespaces["inkscape"],
543 'inkscape:groupmode': 'lineset',
544 'inkscape:label': name,
546 root.append(lineset_group)
547 print('added new lineset group ', name)
550 # <g> for the fills of the current frame
551 fill_group = et.XML('<g/>')
552 fill_group.attrib = {
553 'xmlns:inkscape': namespaces["inkscape"],
554 'inkscape:groupmode': 'layer',
555 'inkscape:label': 'fills',
556 'id': 'fills'
559 fill_elements = self.create_fill_elements(strokes)
560 fill_group.extend(reversed(tuple(fill_elements)))
561 if scene.svg_export.mode == 'ANIMATION':
562 # add the fills to the <g> of the current frame
563 frame_group = find_svg_elem(lineset_group, ".//svg:g[@id='frame_{:04n}']".format(scene.frame_current))
564 frame_group.insert(0, fill_group)
565 else:
566 lineset_group.insert(0, fill_group)
568 # write SVG to file
569 indent_xml(root)
570 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
573 def stroke_inside_stroke(a, b):
574 box_a = BoundingBox.from_sequence(svert.point for svert in a)
575 box_b = BoundingBox.from_sequence(svert.point for svert in b)
576 return box_a.inside(box_b)
579 def diffuse_from_stroke(stroke, curvemat=CurveMaterialF0D()):
580 material = curvemat(Interface0DIterator(stroke))
581 return material.diffuse
583 # - Callbacks - #
584 class ParameterEditorCallback(object):
585 """Object to store callbacks for the Parameter Editor in"""
586 def lineset_pre(self, scene, layer, lineset):
587 raise NotImplementedError()
589 def modifier_post(self, scene, layer, lineset):
590 raise NotImplementedError()
592 def lineset_post(self, scene, layer, lineset):
593 raise NotImplementedError()
597 class SVGPathShaderCallback(ParameterEditorCallback):
598 @classmethod
599 def poll(cls, scene, linestyle):
600 return scene.render.use_freestyle and scene.svg_export.use_svg_export and linestyle.use_export_strokes
602 @classmethod
603 def modifier_post(cls, scene, layer, lineset):
604 if not cls.poll(scene, lineset.linestyle):
605 return []
607 split = scene.svg_export.split_at_invisible
608 stroke_color_mode = lineset.linestyle.stroke_color_mode
609 cls.shader = SVGPathShader.from_lineset(
610 lineset, create_path(scene),
611 render_height(scene), split, stroke_color_mode, scene.frame_current, name=layer.name + '_' + lineset.name)
612 return [cls.shader]
614 @classmethod
615 def lineset_post(cls, scene, layer, lineset):
616 if not cls.poll(scene, lineset.linestyle):
617 return []
618 cls.shader.write()
621 class SVGFillShaderCallback(ParameterEditorCallback):
622 @classmethod
623 def poll(cls, scene, linestyle):
624 return scene.render.use_freestyle and scene.svg_export.use_svg_export and scene.svg_export.object_fill and linestyle.use_export_fills
626 @classmethod
627 def lineset_post(cls, scene, layer, lineset):
628 if not cls.poll(scene, lineset.linestyle):
629 return
631 # reset the stroke selection (but don't delete the already generated strokes)
632 Operators.reset(delete_strokes=False)
633 # Unary Predicates: visible and correct edge nature
634 upred = AndUP1D(
635 QuantitativeInvisibilityUP1D(0),
636 OrUP1D(ExternalContourUP1D(),
637 pyNatureUP1D(Nature.BORDER)),
639 # select the new edges
640 Operators.select(upred)
641 # Binary Predicates
642 bpred = AndBP1D(
643 MaterialBP1D(),
644 NotBP1D(pyZDiscontinuityBP1D()),
646 bpred = OrBP1D(bpred, AndBP1D(NotBP1D(bpred), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
647 # chain the edges
648 Operators.bidirectional_chain(ChainPredicateIterator(upred, bpred))
649 # export SVG
650 collector = StrokeCollector()
651 Operators.create(TrueUP1D(), [collector])
653 builder = SVGFillBuilder(create_path(scene), render_height(scene), layer.name + '_' + lineset.name)
654 builder.write(collector.strokes)
655 # make strokes used for filling invisible
656 for stroke in collector.strokes:
657 for svert in stroke:
658 svert.attribute.visible = False
662 def indent_xml(elem, level=0, indentsize=4):
663 """Prettifies XML code (used in SVG exporter) """
664 i = "\n" + level * " " * indentsize
665 if len(elem):
666 if not elem.text or not elem.text.strip():
667 elem.text = i + " " * indentsize
668 if not elem.tail or not elem.tail.strip():
669 elem.tail = i
670 for elem in elem:
671 indent_xml(elem, level + 1)
672 if not elem.tail or not elem.tail.strip():
673 elem.tail = i
674 elif level and (not elem.tail or not elem.tail.strip()):
675 elem.tail = i
678 def register_namespaces(namespaces=namespaces):
679 for name, url in namespaces.items():
680 if name != 'svg': # creates invalid xml
681 et.register_namespace(name, url)
683 @persistent
684 def handle_versions(self):
685 # We don't modify startup file because it assumes to
686 # have all the default values only.
687 if not bpy.data.is_saved:
688 return
690 # Revision https://developer.blender.org/rBA861519e44adc5674545fa18202dc43c4c20f2d1d
691 # changed the default for fills.
692 # fix by Sergey https://developer.blender.org/T46150
693 if bpy.data.version <= (2, 76, 0):
694 for linestyle in bpy.data.linestyles:
695 linestyle.use_export_fills = True
699 classes = (
700 SVGExporterPanel,
701 SVGExporterLinesetPanel,
702 SVGExport,
706 def register():
707 linestyle = bpy.types.FreestyleLineStyle
708 linestyle.use_export_strokes = BoolProperty(
709 name="Export Strokes",
710 description="Export strokes for this Line Style",
711 default=True,
713 linestyle.stroke_color_mode = EnumProperty(
714 name="Stroke Color Mode",
715 items=(
716 ('BASE', "Base Color", "Use the linestyle's base color", 0),
717 ('FIRST', "First Vertex", "Use the color of a stroke's first vertex", 1),
718 ('FINAL', "Final Vertex", "Use the color of a stroke's final vertex", 2),
720 default='BASE',
722 linestyle.use_export_fills = BoolProperty(
723 name="Export Fills",
724 description="Export fills for this Line Style",
725 default=False,
728 for cls in classes:
729 bpy.utils.register_class(cls)
730 bpy.types.Scene.svg_export = PointerProperty(type=SVGExport)
733 # add callbacks
734 bpy.app.handlers.render_init.append(render_init)
735 bpy.app.handlers.render_write.append(render_write)
736 bpy.app.handlers.render_pre.append(svg_export_header)
737 bpy.app.handlers.render_complete.append(svg_export_animation)
739 # manipulate shaders list
740 parameter_editor.callbacks_modifiers_post.append(SVGPathShaderCallback.modifier_post)
741 parameter_editor.callbacks_lineset_post.append(SVGPathShaderCallback.lineset_post)
742 parameter_editor.callbacks_lineset_post.append(SVGFillShaderCallback.lineset_post)
744 # register namespaces
745 register_namespaces()
747 # handle regressions
748 bpy.app.handlers.version_update.append(handle_versions)
751 def unregister():
753 for cls in classes:
754 bpy.utils.unregister_class(cls)
755 del bpy.types.Scene.svg_export
756 linestyle = bpy.types.FreestyleLineStyle
757 del linestyle.use_export_strokes
758 del linestyle.use_export_fills
760 # remove callbacks
761 bpy.app.handlers.render_init.remove(render_init)
762 bpy.app.handlers.render_write.remove(render_write)
763 bpy.app.handlers.render_pre.remove(svg_export_header)
764 bpy.app.handlers.render_complete.remove(svg_export_animation)
766 # manipulate shaders list
767 parameter_editor.callbacks_modifiers_post.remove(SVGPathShaderCallback.modifier_post)
768 parameter_editor.callbacks_lineset_post.remove(SVGPathShaderCallback.lineset_post)
769 parameter_editor.callbacks_lineset_post.remove(SVGFillShaderCallback.lineset_post)
771 bpy.app.handlers.version_update.remove(handle_versions)
774 if __name__ == "__main__":
775 register()