mesh_tools/mesh_relax: pass in smooth factor
[blender-addons.git] / render_freestyle_svg.py
blob0061cc5663e943bdf41802cdd06f7dd2a3f8113e
1 # ##### BEGIN GPL LICENSE BLOCK #####
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.
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.
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.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 bl_info = {
22 "name": "Freestyle SVG Exporter",
23 "author": "Folkert de Vries",
24 "version": (1, 0),
25 "blender": (2, 80, 0),
26 "location": "Properties > Render > Freestyle SVG Export",
27 "description": "Exports Freestyle's stylized edges in SVG format",
28 "warning": "",
29 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/render/render_freestyle_svg.html",
30 "support": 'OFFICIAL',
31 "category": "Render",
34 import bpy
35 import parameter_editor
36 import itertools
37 import os
39 import xml.etree.cElementTree as et
41 from bpy.app.handlers import persistent
42 from collections import OrderedDict
43 from functools import partial
44 from mathutils import Vector
46 from freestyle.types import (
47 StrokeShader,
48 Interface0DIterator,
49 Operators,
50 Nature,
51 StrokeVertex,
53 from freestyle.utils import (
54 getCurrentScene,
55 BoundingBox,
56 is_poly_clockwise,
57 StrokeCollector,
58 material_from_fedge,
59 get_object_name,
61 from freestyle.functions import (
62 GetShapeF1D,
63 CurveMaterialF0D,
65 from freestyle.predicates import (
66 AndBP1D,
67 AndUP1D,
68 ContourUP1D,
69 ExternalContourUP1D,
70 MaterialBP1D,
71 NotBP1D,
72 NotUP1D,
73 OrBP1D,
74 OrUP1D,
75 pyNatureUP1D,
76 pyZBP1D,
77 pyZDiscontinuityBP1D,
78 QuantitativeInvisibilityUP1D,
79 SameShapeIdBP1D,
80 TrueBP1D,
81 TrueUP1D,
83 from freestyle.chainingiterators import ChainPredicateIterator
84 from parameter_editor import get_dashed_pattern
86 from bpy.props import (
87 BoolProperty,
88 EnumProperty,
89 PointerProperty,
93 # use utf-8 here to keep ElementTree happy, end result is utf-16
94 svg_primitive = """<?xml version="1.0" encoding="ascii" standalone="no"?>
95 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
96 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
97 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{:d}" height="{:d}">
98 </svg>"""
101 # xml namespaces
102 namespaces = {
103 "inkscape": "http://www.inkscape.org/namespaces/inkscape",
104 "svg": "http://www.w3.org/2000/svg",
105 "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
106 "": "http://www.w3.org/2000/svg",
110 # wrap XMLElem.find, so the namespaces don't need to be given as an argument
111 def find_xml_elem(obj, search, namespaces, *, all=False):
112 if all:
113 return obj.findall(search, namespaces=namespaces)
114 return obj.find(search, namespaces=namespaces)
116 find_svg_elem = partial(find_xml_elem, namespaces=namespaces)
119 def render_height(scene):
120 return int(scene.render.resolution_y * scene.render.resolution_percentage / 100)
123 def render_width(scene):
124 return int(scene.render.resolution_x * scene.render.resolution_percentage / 100)
127 def format_rgb(color):
128 return 'rgb({}, {}, {})'.format(*(int(v * 255) for v in color))
131 # stores the state of the render, used to differ between animation and single frame renders.
132 class RenderState:
134 # Note that this flag is set to False only after the first frame
135 # has been written to file.
136 is_preview = True
139 @persistent
140 def render_init(scene):
141 RenderState.is_preview = True
144 @persistent
145 def render_write(scene):
146 RenderState.is_preview = False
149 def is_preview_render(scene):
150 return RenderState.is_preview or scene.svg_export.mode == 'FRAME'
153 def create_path(scene):
154 """Creates the output path for the svg file"""
155 path = os.path.dirname(scene.render.frame_path())
156 file_dir_path = os.path.dirname(bpy.data.filepath)
158 # try to use the given path if it is absolute
159 if os.path.isabs(path):
160 dirname = path
162 # otherwise, use current file's location as a start for the relative path
163 elif bpy.data.is_saved and file_dir_path:
164 dirname = os.path.normpath(os.path.join(file_dir_path, path))
166 # otherwise, use the folder from which blender was called as the start
167 else:
168 dirname = os.path.abspath(bpy.path.abspath(path))
171 basename = bpy.path.basename(scene.render.filepath)
172 if scene.svg_export.mode == 'FRAME':
173 frame = "{:04d}".format(scene.frame_current)
174 else:
175 frame = "{:04d}-{:04d}".format(scene.frame_start, scene.frame_end)
177 return os.path.join(dirname, basename + frame + ".svg")
180 class SVGExporterLinesetPanel(bpy.types.Panel):
181 """Creates a Panel in the Render Layers context of the properties editor"""
182 bl_idname = "RENDER_PT_SVGExporterLinesetPanel"
183 bl_space_type = 'PROPERTIES'
184 bl_label = "Freestyle Line Style SVG Export"
185 bl_region_type = 'WINDOW'
186 bl_context = "view_layer"
188 def draw(self, context):
189 layout = self.layout
191 scene = context.scene
192 svg = scene.svg_export
193 freestyle = context.window.view_layer.freestyle_settings
195 try:
196 linestyle = freestyle.linesets.active.linestyle
198 except AttributeError:
199 # Linestyles can be removed, so 0 linestyles is possible.
200 # there is nothing to draw in those cases.
201 # see https://developer.blender.org/T49855
202 return
204 else:
205 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
206 row = layout.row()
207 column = row.column()
208 column.prop(linestyle, 'use_export_strokes')
210 column = row.column()
211 column.active = svg.object_fill
212 column.prop(linestyle, 'use_export_fills')
214 row = layout.row()
215 row.prop(linestyle, "stroke_color_mode", expand=True)
218 class SVGExport(bpy.types.PropertyGroup):
219 """Implements the properties for the SVG exporter"""
220 bl_idname = "RENDER_PT_svg_export"
222 use_svg_export: BoolProperty(
223 name="SVG Export",
224 description="Export Freestyle edges to an .svg format",
226 split_at_invisible: BoolProperty(
227 name="Split at Invisible",
228 description="Split the stroke at an invisible vertex",
230 object_fill: BoolProperty(
231 name="Fill Contours",
232 description="Fill the contour with the object's material color",
234 mode: EnumProperty(
235 name="Mode",
236 items=(
237 ('FRAME', "Frame", "Export a single frame", 0),
238 ('ANIMATION', "Animation", "Export an animation", 1),
240 default='FRAME',
242 line_join_type: EnumProperty(
243 name="Linejoin",
244 items=(
245 ('MITER', "Miter", "Corners are sharp", 0),
246 ('ROUND', "Round", "Corners are smoothed", 1),
247 ('BEVEL', "Bevel", "Corners are bevelled", 2),
249 default='ROUND',
253 class SVGExporterPanel(bpy.types.Panel):
254 """Creates a Panel in the render context of the properties editor"""
255 bl_idname = "RENDER_PT_SVGExporterPanel"
256 bl_space_type = 'PROPERTIES'
257 bl_label = "Freestyle SVG Export"
258 bl_region_type = 'WINDOW'
259 bl_context = "render"
261 def draw_header(self, context):
262 self.layout.prop(context.scene.svg_export, "use_svg_export", text="")
264 def draw(self, context):
265 layout = self.layout
267 scene = context.scene
268 svg = scene.svg_export
269 freestyle = context.window.view_layer.freestyle_settings
271 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
273 row = layout.row()
274 row.prop(svg, "mode", expand=True)
276 row = layout.row()
277 row.prop(svg, "split_at_invisible")
278 row.prop(svg, "object_fill")
280 row = layout.row()
281 row.prop(svg, "line_join_type", expand=True)
284 @persistent
285 def svg_export_header(scene):
286 if not (scene.render.use_freestyle and scene.svg_export.use_svg_export):
287 return
289 # write the header only for the first frame when animation is being rendered
290 if not is_preview_render(scene) and scene.frame_current != scene.frame_start:
291 return
293 # this may fail still. The error is printed to the console.
294 with open(create_path(scene), "w") as f:
295 f.write(svg_primitive.format(render_width(scene), render_height(scene)))
298 @persistent
299 def svg_export_animation(scene):
300 """makes an animation of the exported SVG file """
301 render = scene.render
302 svg = scene.svg_export
304 if render.use_freestyle and svg.use_svg_export and not is_preview_render(scene):
305 write_animation(create_path(scene), scene.frame_start, render.fps)
308 def write_animation(filepath, frame_begin, fps):
309 """Adds animate tags to the specified file."""
310 tree = et.parse(filepath)
311 root = tree.getroot()
313 linesets = find_svg_elem(tree, ".//svg:g[@inkscape:groupmode='lineset']", all=True)
314 for i, lineset in enumerate(linesets):
315 name = lineset.get('id')
316 frames = find_svg_elem(lineset, ".//svg:g[@inkscape:groupmode='frame']", all=True)
317 n_of_frames = len(frames)
318 keyTimes = ";".join(str(round(x / n_of_frames, 3)) for x in range(n_of_frames)) + ";1"
320 style = {
321 'attributeName': 'display',
322 'values': "none;" * (n_of_frames - 1) + "inline;none",
323 'repeatCount': 'indefinite',
324 'keyTimes': keyTimes,
325 'dur': "{:.3f}s".format(n_of_frames / fps),
328 for j, frame in enumerate(frames):
329 id = 'anim_{}_{:06n}'.format(name, j + frame_begin)
330 # create animate tag
331 frame_anim = et.XML('<animate id="{}" begin="{:.3f}s" />'.format(id, (j - n_of_frames) / fps))
332 # add per-lineset style attributes
333 frame_anim.attrib.update(style)
334 # add to the current frame
335 frame.append(frame_anim)
337 # write SVG to file
338 indent_xml(root)
339 tree.write(filepath, encoding='ascii', xml_declaration=True)
342 # - StrokeShaders - #
343 class SVGPathShader(StrokeShader):
344 """Stroke Shader for writing stroke data to a .svg file."""
345 def __init__(self, name, style, filepath, res_y, split_at_invisible, stroke_color_mode, frame_current):
346 StrokeShader.__init__(self)
347 # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
348 self._name = name
349 self.filepath = filepath
350 self.h = res_y
351 self.frame_current = frame_current
352 self.elements = []
353 self.split_at_invisible = split_at_invisible
354 self.stroke_color_mode = stroke_color_mode # BASE | FIRST | LAST
355 self.style = style
358 @classmethod
359 def from_lineset(cls, lineset, filepath, res_y, split_at_invisible, use_stroke_color, frame_current, *, name=""):
360 """Builds a SVGPathShader using data from the given lineset"""
361 name = name or lineset.name
362 linestyle = lineset.linestyle
363 # extract style attributes from the linestyle and scene
364 svg = getCurrentScene().svg_export
365 style = {
366 'fill': 'none',
367 'stroke-width': linestyle.thickness,
368 'stroke-linecap': linestyle.caps.lower(),
369 'stroke-opacity': linestyle.alpha,
370 'stroke': format_rgb(linestyle.color),
371 'stroke-linejoin': svg.line_join_type.lower(),
373 # get dashed line pattern (if specified)
374 if linestyle.use_dashed_line:
375 style['stroke-dasharray'] = ",".join(str(elem) for elem in get_dashed_pattern(linestyle))
376 # return instance
377 return cls(name, style, filepath, res_y, split_at_invisible, use_stroke_color, frame_current)
380 @staticmethod
381 def pathgen(stroke, style, height, split_at_invisible, stroke_color_mode, f=lambda v: not v.attribute.visible):
382 """Generator that creates SVG paths (as strings) from the current stroke """
383 if len(stroke) <= 1:
384 return ""
386 if stroke_color_mode != 'BASE':
387 # try to use the color of the first or last vertex
388 try:
389 index = 0 if stroke_color_mode == 'FIRST' else -1
390 color = format_rgb(stroke[index].attribute.color)
391 style["stroke"] = color
392 except (ValueError, IndexError):
393 # default is linestyle base color
394 pass
396 # put style attributes into a single svg path definition
397 path = '\n<path ' + "".join('{}="{}" '.format(k, v) for k, v in style.items()) + 'd=" M '
399 it = iter(stroke)
400 # start first path
401 yield path
402 for v in it:
403 x, y = v.point
404 yield '{:.3f}, {:.3f} '.format(x, height - y)
405 if split_at_invisible and v.attribute.visible is False:
406 # end current and start new path;
407 yield '" />' + path
408 # fast-forward till the next visible vertex
409 it = itertools.dropwhile(f, it)
410 # yield next visible vertex
411 svert = next(it, None)
412 if svert is None:
413 break
414 x, y = svert.point
415 yield '{:.3f}, {:.3f} '.format(x, height - y)
416 # close current path
417 yield '" />'
419 def shade(self, stroke):
420 stroke_to_paths = "".join(self.pathgen(stroke, self.style, self.h, self.split_at_invisible, self.stroke_color_mode)).split("\n")
421 # convert to actual XML. Empty strokes are empty strings; they are ignored.
422 self.elements.extend(et.XML(elem) for elem in stroke_to_paths if elem) # if len(elem.strip()) > len(self.path))
424 def write(self):
425 """Write SVG data tree to file """
426 tree = et.parse(self.filepath)
427 root = tree.getroot()
428 name = self._name
429 scene = bpy.context.scene
431 # create <g> for lineset as a whole (don't overwrite)
432 # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills.
433 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(name))
434 if lineset_group is None:
435 lineset_group = et.XML('<g/>')
436 lineset_group.attrib = {
437 'id': name,
438 'xmlns:inkscape': namespaces["inkscape"],
439 'inkscape:groupmode': 'lineset',
440 'inkscape:label': name,
442 root.append(lineset_group)
444 # create <g> for the current frame
445 id = "frame_{:04n}".format(self.frame_current)
447 stroke_group = et.XML("<g/>")
448 stroke_group.attrib = {
449 'xmlns:inkscape': namespaces["inkscape"],
450 'inkscape:groupmode': 'layer',
451 'id': 'strokes',
452 'inkscape:label': 'strokes'
454 # nest the structure
455 stroke_group.extend(self.elements)
456 if scene.svg_export.mode == 'ANIMATION':
457 frame_group = et.XML("<g/>")
458 frame_group.attrib = {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id}
459 frame_group.append(stroke_group)
460 lineset_group.append(frame_group)
461 else:
462 lineset_group.append(stroke_group)
464 # write SVG to file
465 print("SVG Export: writing to", self.filepath)
466 indent_xml(root)
467 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
470 class SVGFillBuilder:
471 def __init__(self, filepath, height, name):
472 self.filepath = filepath
473 self._name = name
474 self.stroke_to_fill = partial(self.stroke_to_svg, height=height)
476 @staticmethod
477 def pathgen(vertices, path, height):
478 yield path
479 for point in vertices:
480 x, y = point
481 yield '{:.3f}, {:.3f} '.format(x, height - y)
482 yield ' z" />' # closes the path; connects the current to the first point
485 @staticmethod
486 def get_merged_strokes(strokes):
487 def extend_stroke(stroke, vertices):
488 for vert in map(StrokeVertex, vertices):
489 stroke.insert_vertex(vert, stroke.stroke_vertices_end())
490 return stroke
492 base_strokes = tuple(stroke for stroke in strokes if not is_poly_clockwise(stroke))
493 merged_strokes = OrderedDict((s, list()) for s in base_strokes)
495 for stroke in filter(is_poly_clockwise, strokes):
496 for base in base_strokes:
497 # don't merge when diffuse colors don't match
498 if diffuse_from_stroke(stroke) != diffuse_from_stroke(stroke):
499 continue
500 # only merge when the 'hole' is inside the base
501 elif stroke_inside_stroke(stroke, base):
502 merged_strokes[base].append(stroke)
503 break
504 # if it isn't a hole, it is likely that there are two strokes belonging
505 # to the same object separated by another object. let's try to join them
506 elif (get_object_name(base) == get_object_name(stroke) and
507 diffuse_from_stroke(stroke) == diffuse_from_stroke(stroke)):
508 base = extend_stroke(base, (sv for sv in stroke))
509 break
510 else:
511 # if all else fails, treat this stroke as a base stroke
512 merged_strokes.update({stroke: []})
513 return merged_strokes
516 def stroke_to_svg(self, stroke, height, parameters=None):
517 if parameters is None:
518 *color, alpha = diffuse_from_stroke(stroke)
519 color = tuple(int(255 * c) for c in color)
520 parameters = {
521 'fill_rule': 'evenodd',
522 'stroke': 'none',
523 'fill-opacity': alpha,
524 'fill': 'rgb' + repr(color),
526 param_str = " ".join('{}="{}"'.format(k, v) for k, v in parameters.items())
527 path = '<path {} d=" M '.format(param_str)
528 vertices = (svert.point for svert in stroke)
529 s = "".join(self.pathgen(vertices, path, height))
530 result = et.XML(s)
531 return result
533 def create_fill_elements(self, strokes):
534 """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
535 merged_strokes = self.get_merged_strokes(strokes)
536 for k, v in merged_strokes.items():
537 base = self.stroke_to_fill(k)
538 fills = (self.stroke_to_fill(stroke).get("d") for stroke in v)
539 merged_points = " ".join(fills)
540 base.attrib['d'] += merged_points
541 yield base
543 def write(self, strokes):
544 """Write SVG data tree to file """
546 tree = et.parse(self.filepath)
547 root = tree.getroot()
548 scene = bpy.context.scene
549 name = self._name
551 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(self._name))
552 if lineset_group is None:
553 lineset_group = et.XML('<g/>')
554 lineset_group.attrib = {
555 'id': name,
556 'xmlns:inkscape': namespaces["inkscape"],
557 'inkscape:groupmode': 'lineset',
558 'inkscape:label': name,
560 root.append(lineset_group)
561 print('added new lineset group ', name)
564 # <g> for the fills of the current frame
565 fill_group = et.XML('<g/>')
566 fill_group.attrib = {
567 'xmlns:inkscape': namespaces["inkscape"],
568 'inkscape:groupmode': 'layer',
569 'inkscape:label': 'fills',
570 'id': 'fills'
573 fill_elements = self.create_fill_elements(strokes)
574 fill_group.extend(reversed(tuple(fill_elements)))
575 if scene.svg_export.mode == 'ANIMATION':
576 # add the fills to the <g> of the current frame
577 frame_group = find_svg_elem(lineset_group, ".//svg:g[@id='frame_{:04n}']".format(scene.frame_current))
578 frame_group.insert(0, fill_group)
579 else:
580 lineset_group.insert(0, fill_group)
582 # write SVG to file
583 indent_xml(root)
584 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
587 def stroke_inside_stroke(a, b):
588 box_a = BoundingBox.from_sequence(svert.point for svert in a)
589 box_b = BoundingBox.from_sequence(svert.point for svert in b)
590 return box_a.inside(box_b)
593 def diffuse_from_stroke(stroke, curvemat=CurveMaterialF0D()):
594 material = curvemat(Interface0DIterator(stroke))
595 return material.diffuse
597 # - Callbacks - #
598 class ParameterEditorCallback(object):
599 """Object to store callbacks for the Parameter Editor in"""
600 def lineset_pre(self, scene, layer, lineset):
601 raise NotImplementedError()
603 def modifier_post(self, scene, layer, lineset):
604 raise NotImplementedError()
606 def lineset_post(self, scene, layer, lineset):
607 raise NotImplementedError()
611 class SVGPathShaderCallback(ParameterEditorCallback):
612 @classmethod
613 def poll(cls, scene, linestyle):
614 return scene.render.use_freestyle and scene.svg_export.use_svg_export and linestyle.use_export_strokes
616 @classmethod
617 def modifier_post(cls, scene, layer, lineset):
618 if not cls.poll(scene, lineset.linestyle):
619 return []
621 split = scene.svg_export.split_at_invisible
622 stroke_color_mode = lineset.linestyle.stroke_color_mode
623 cls.shader = SVGPathShader.from_lineset(
624 lineset, create_path(scene),
625 render_height(scene), split, stroke_color_mode, scene.frame_current, name=layer.name + '_' + lineset.name)
626 return [cls.shader]
628 @classmethod
629 def lineset_post(cls, scene, layer, lineset):
630 if not cls.poll(scene, lineset.linestyle):
631 return []
632 cls.shader.write()
635 class SVGFillShaderCallback(ParameterEditorCallback):
636 @classmethod
637 def poll(cls, scene, linestyle):
638 return scene.render.use_freestyle and scene.svg_export.use_svg_export and scene.svg_export.object_fill and linestyle.use_export_fills
640 @classmethod
641 def lineset_post(cls, scene, layer, lineset):
642 if not cls.poll(scene, lineset.linestyle):
643 return
645 # reset the stroke selection (but don't delete the already generated strokes)
646 Operators.reset(delete_strokes=False)
647 # Unary Predicates: visible and correct edge nature
648 upred = AndUP1D(
649 QuantitativeInvisibilityUP1D(0),
650 OrUP1D(ExternalContourUP1D(),
651 pyNatureUP1D(Nature.BORDER)),
653 # select the new edges
654 Operators.select(upred)
655 # Binary Predicates
656 bpred = AndBP1D(
657 MaterialBP1D(),
658 NotBP1D(pyZDiscontinuityBP1D()),
660 bpred = OrBP1D(bpred, AndBP1D(NotBP1D(bpred), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
661 # chain the edges
662 Operators.bidirectional_chain(ChainPredicateIterator(upred, bpred))
663 # export SVG
664 collector = StrokeCollector()
665 Operators.create(TrueUP1D(), [collector])
667 builder = SVGFillBuilder(create_path(scene), render_height(scene), layer.name + '_' + lineset.name)
668 builder.write(collector.strokes)
669 # make strokes used for filling invisible
670 for stroke in collector.strokes:
671 for svert in stroke:
672 svert.attribute.visible = False
676 def indent_xml(elem, level=0, indentsize=4):
677 """Prettifies XML code (used in SVG exporter) """
678 i = "\n" + level * " " * indentsize
679 if len(elem):
680 if not elem.text or not elem.text.strip():
681 elem.text = i + " " * indentsize
682 if not elem.tail or not elem.tail.strip():
683 elem.tail = i
684 for elem in elem:
685 indent_xml(elem, level + 1)
686 if not elem.tail or not elem.tail.strip():
687 elem.tail = i
688 elif level and (not elem.tail or not elem.tail.strip()):
689 elem.tail = i
692 def register_namespaces(namespaces=namespaces):
693 for name, url in namespaces.items():
694 if name != 'svg': # creates invalid xml
695 et.register_namespace(name, url)
697 @persistent
698 def handle_versions(self):
699 # We don't modify startup file because it assumes to
700 # have all the default values only.
701 if not bpy.data.is_saved:
702 return
704 # Revision https://developer.blender.org/rBA861519e44adc5674545fa18202dc43c4c20f2d1d
705 # changed the default for fills.
706 # fix by Sergey https://developer.blender.org/T46150
707 if bpy.data.version <= (2, 76, 0):
708 for linestyle in bpy.data.linestyles:
709 linestyle.use_export_fills = True
713 classes = (
714 SVGExporterPanel,
715 SVGExporterLinesetPanel,
716 SVGExport,
720 def register():
721 linestyle = bpy.types.FreestyleLineStyle
722 linestyle.use_export_strokes = BoolProperty(
723 name="Export Strokes",
724 description="Export strokes for this Line Style",
725 default=True,
727 linestyle.stroke_color_mode = EnumProperty(
728 name="Stroke Color Mode",
729 items=(
730 ('BASE', "Base Color", "Use the linestyle's base color", 0),
731 ('FIRST', "First Vertex", "Use the color of a stroke's first vertex", 1),
732 ('FINAL', "Final Vertex", "Use the color of a stroke's final vertex", 2),
734 default='BASE',
736 linestyle.use_export_fills = BoolProperty(
737 name="Export Fills",
738 description="Export fills for this Line Style",
739 default=False,
742 for cls in classes:
743 bpy.utils.register_class(cls)
744 bpy.types.Scene.svg_export = PointerProperty(type=SVGExport)
747 # add callbacks
748 bpy.app.handlers.render_init.append(render_init)
749 bpy.app.handlers.render_write.append(render_write)
750 bpy.app.handlers.render_pre.append(svg_export_header)
751 bpy.app.handlers.render_complete.append(svg_export_animation)
753 # manipulate shaders list
754 parameter_editor.callbacks_modifiers_post.append(SVGPathShaderCallback.modifier_post)
755 parameter_editor.callbacks_lineset_post.append(SVGPathShaderCallback.lineset_post)
756 parameter_editor.callbacks_lineset_post.append(SVGFillShaderCallback.lineset_post)
758 # register namespaces
759 register_namespaces()
761 # handle regressions
762 bpy.app.handlers.version_update.append(handle_versions)
765 def unregister():
767 for cls in classes:
768 bpy.utils.unregister_class(cls)
769 del bpy.types.Scene.svg_export
770 linestyle = bpy.types.FreestyleLineStyle
771 del linestyle.use_export_strokes
772 del linestyle.use_export_fills
774 # remove callbacks
775 bpy.app.handlers.render_init.remove(render_init)
776 bpy.app.handlers.render_write.remove(render_write)
777 bpy.app.handlers.render_pre.remove(svg_export_header)
778 bpy.app.handlers.render_complete.remove(svg_export_animation)
780 # manipulate shaders list
781 parameter_editor.callbacks_modifiers_post.remove(SVGPathShaderCallback.modifier_post)
782 parameter_editor.callbacks_lineset_post.remove(SVGPathShaderCallback.lineset_post)
783 parameter_editor.callbacks_lineset_post.remove(SVGFillShaderCallback.lineset_post)
785 bpy.app.handlers.version_update.remove(handle_versions)
788 if __name__ == "__main__":
789 register()