Fix T62695: OBJ mtllib fails when filename contains spaces
[blender-addons.git] / render_freestyle_svg.py
blob3e3634b1809fe76d1aace3ae3ad870961c3f41f4
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, 72, 1),
26 "location": "Properties > Render > Freestyle SVG Export",
27 "description": "Exports Freestyle's stylized edges in SVG format",
28 "warning": "",
29 "wiki_url": "",
30 "category": "Render",
33 import bpy
34 import parameter_editor
35 import itertools
36 import os
38 import xml.etree.cElementTree as et
40 from bpy.app.handlers import persistent
41 from collections import OrderedDict
42 from functools import partial
43 from mathutils import Vector
45 from freestyle.types import (
46 StrokeShader,
47 Interface0DIterator,
48 Operators,
49 Nature,
50 StrokeVertex,
52 from freestyle.utils import (
53 getCurrentScene,
54 BoundingBox,
55 is_poly_clockwise,
56 StrokeCollector,
57 material_from_fedge,
58 get_object_name,
60 from freestyle.functions import (
61 GetShapeF1D,
62 CurveMaterialF0D,
64 from freestyle.predicates import (
65 AndBP1D,
66 AndUP1D,
67 ContourUP1D,
68 ExternalContourUP1D,
69 MaterialBP1D,
70 NotBP1D,
71 NotUP1D,
72 OrBP1D,
73 OrUP1D,
74 pyNatureUP1D,
75 pyZBP1D,
76 pyZDiscontinuityBP1D,
77 QuantitativeInvisibilityUP1D,
78 SameShapeIdBP1D,
79 TrueBP1D,
80 TrueUP1D,
82 from freestyle.chainingiterators import ChainPredicateIterator
83 from parameter_editor import get_dashed_pattern
85 from bpy.props import (
86 BoolProperty,
87 EnumProperty,
88 PointerProperty,
92 # use utf-8 here to keep ElementTree happy, end result is utf-16
93 svg_primitive = """<?xml version="1.0" encoding="ascii" standalone="no"?>
94 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
95 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
96 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{:d}" height="{:d}">
97 </svg>"""
100 # xml namespaces
101 namespaces = {
102 "inkscape": "http://www.inkscape.org/namespaces/inkscape",
103 "svg": "http://www.w3.org/2000/svg",
104 "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
105 "": "http://www.w3.org/2000/svg",
109 # wrap XMLElem.find, so the namespaces don't need to be given as an argument
110 def find_xml_elem(obj, search, namespaces, *, all=False):
111 if all:
112 return obj.findall(search, namespaces=namespaces)
113 return obj.find(search, namespaces=namespaces)
115 find_svg_elem = partial(find_xml_elem, namespaces=namespaces)
118 def render_height(scene):
119 return int(scene.render.resolution_y * scene.render.resolution_percentage / 100)
122 def render_width(scene):
123 return int(scene.render.resolution_x * scene.render.resolution_percentage / 100)
126 def format_rgb(color):
127 return 'rgb({}, {}, {})'.format(*(int(v * 255) for v in color))
130 # stores the state of the render, used to differ between animation and single frame renders.
131 class RenderState:
133 # Note that this flag is set to False only after the first frame
134 # has been written to file.
135 is_preview = True
138 @persistent
139 def render_init(scene):
140 RenderState.is_preview = True
143 @persistent
144 def render_write(scene):
145 RenderState.is_preview = False
148 def is_preview_render(scene):
149 return RenderState.is_preview or scene.svg_export.mode == 'FRAME'
152 def create_path(scene):
153 """Creates the output path for the svg file"""
154 path = os.path.dirname(scene.render.frame_path())
155 file_dir_path = os.path.dirname(bpy.data.filepath)
157 # try to use the given path if it is absolute
158 if os.path.isabs(path):
159 dirname = path
161 # otherwise, use current file's location as a start for the relative path
162 elif bpy.data.is_saved and file_dir_path:
163 dirname = os.path.normpath(os.path.join(file_dir_path, path))
165 # otherwise, use the folder from which blender was called as the start
166 else:
167 dirname = os.path.abspath(bpy.path.abspath(path))
170 basename = bpy.path.basename(scene.render.filepath)
171 if scene.svg_export.mode == 'FRAME':
172 frame = "{:04d}".format(scene.frame_current)
173 else:
174 frame = "{:04d}-{:04d}".format(scene.frame_start, scene.frame_end)
176 return os.path.join(dirname, basename + frame + ".svg")
179 class SVGExporterLinesetPanel(bpy.types.Panel):
180 """Creates a Panel in the Render Layers context of the properties editor"""
181 bl_idname = "RENDER_PT_SVGExporterLinesetPanel"
182 bl_space_type = 'PROPERTIES'
183 bl_label = "Freestyle Line Style SVG Export"
184 bl_region_type = 'WINDOW'
185 bl_context = "render_layer"
187 def draw(self, context):
188 layout = self.layout
190 scene = context.scene
191 svg = scene.svg_export
192 freestyle = scene.render.layers.active.freestyle_settings
194 try:
195 linestyle = freestyle.linesets.active.linestyle
197 except AttributeError:
198 # Linestyles can be removed, so 0 linestyles is possible.
199 # there is nothing to draw in those cases.
200 # see https://developer.blender.org/T49855
201 return
203 else:
204 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
205 row = layout.row()
206 column = row.column()
207 column.prop(linestyle, 'use_export_strokes')
209 column = row.column()
210 column.active = svg.object_fill
211 column.prop(linestyle, 'use_export_fills')
213 row = layout.row()
214 row.prop(linestyle, "stroke_color_mode", expand=True)
217 class SVGExport(bpy.types.PropertyGroup):
218 """Implements the properties for the SVG exporter"""
219 bl_idname = "RENDER_PT_svg_export"
221 use_svg_export: BoolProperty(
222 name="SVG Export",
223 description="Export Freestyle edges to an .svg format",
225 split_at_invisible: BoolProperty(
226 name="Split at Invisible",
227 description="Split the stroke at an invisible vertex",
229 object_fill: BoolProperty(
230 name="Fill Contours",
231 description="Fill the contour with the object's material color",
233 mode: EnumProperty(
234 name="Mode",
235 items=(
236 ('FRAME', "Frame", "Export a single frame", 0),
237 ('ANIMATION', "Animation", "Export an animation", 1),
239 default='FRAME',
241 line_join_type: EnumProperty(
242 name="Linejoin",
243 items=(
244 ('MITER', "Miter", "Corners are sharp", 0),
245 ('ROUND', "Round", "Corners are smoothed", 1),
246 ('BEVEL', "Bevel", "Corners are bevelled", 2),
248 default='ROUND',
252 class SVGExporterPanel(bpy.types.Panel):
253 """Creates a Panel in the render context of the properties editor"""
254 bl_idname = "RENDER_PT_SVGExporterPanel"
255 bl_space_type = 'PROPERTIES'
256 bl_label = "Freestyle SVG Export"
257 bl_region_type = 'WINDOW'
258 bl_context = "render"
260 def draw_header(self, context):
261 self.layout.prop(context.scene.svg_export, "use_svg_export", text="")
263 def draw(self, context):
264 layout = self.layout
266 scene = context.scene
267 svg = scene.svg_export
268 freestyle = scene.render.layers.active.freestyle_settings
270 layout.active = (svg.use_svg_export and freestyle.mode != 'SCRIPT')
272 row = layout.row()
273 row.prop(svg, "mode", expand=True)
275 row = layout.row()
276 row.prop(svg, "split_at_invisible")
277 row.prop(svg, "object_fill")
279 row = layout.row()
280 row.prop(svg, "line_join_type", expand=True)
283 @persistent
284 def svg_export_header(scene):
285 if not (scene.render.use_freestyle and scene.svg_export.use_svg_export):
286 return
288 # write the header only for the first frame when animation is being rendered
289 if not is_preview_render(scene) and scene.frame_current != scene.frame_start:
290 return
292 # this may fail still. The error is printed to the console.
293 with open(create_path(scene), "w") as f:
294 f.write(svg_primitive.format(render_width(scene), render_height(scene)))
297 @persistent
298 def svg_export_animation(scene):
299 """makes an animation of the exported SVG file """
300 render = scene.render
301 svg = scene.svg_export
303 if render.use_freestyle and svg.use_svg_export and not is_preview_render(scene):
304 write_animation(create_path(scene), scene.frame_start, render.fps)
307 def write_animation(filepath, frame_begin, fps):
308 """Adds animate tags to the specified file."""
309 tree = et.parse(filepath)
310 root = tree.getroot()
312 linesets = find_svg_elem(tree, ".//svg:g[@inkscape:groupmode='lineset']", all=True)
313 for i, lineset in enumerate(linesets):
314 name = lineset.get('id')
315 frames = find_svg_elem(lineset, ".//svg:g[@inkscape:groupmode='frame']", all=True)
316 n_of_frames = len(frames)
317 keyTimes = ";".join(str(round(x / n_of_frames, 3)) for x in range(n_of_frames)) + ";1"
319 style = {
320 'attributeName': 'display',
321 'values': "none;" * (n_of_frames - 1) + "inline;none",
322 'repeatCount': 'indefinite',
323 'keyTimes': keyTimes,
324 'dur': "{:.3f}s".format(n_of_frames / fps),
327 for j, frame in enumerate(frames):
328 id = 'anim_{}_{:06n}'.format(name, j + frame_begin)
329 # create animate tag
330 frame_anim = et.XML('<animate id="{}" begin="{:.3f}s" />'.format(id, (j - n_of_frames) / fps))
331 # add per-lineset style attributes
332 frame_anim.attrib.update(style)
333 # add to the current frame
334 frame.append(frame_anim)
336 # write SVG to file
337 indent_xml(root)
338 tree.write(filepath, encoding='ascii', xml_declaration=True)
341 # - StrokeShaders - #
342 class SVGPathShader(StrokeShader):
343 """Stroke Shader for writing stroke data to a .svg file."""
344 def __init__(self, name, style, filepath, res_y, split_at_invisible, stroke_color_mode, frame_current):
345 StrokeShader.__init__(self)
346 # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
347 self._name = name
348 self.filepath = filepath
349 self.h = res_y
350 self.frame_current = frame_current
351 self.elements = []
352 self.split_at_invisible = split_at_invisible
353 self.stroke_color_mode = stroke_color_mode # BASE | FIRST | LAST
354 self.style = style
357 @classmethod
358 def from_lineset(cls, lineset, filepath, res_y, split_at_invisible, use_stroke_color, frame_current, *, name=""):
359 """Builds a SVGPathShader using data from the given lineset"""
360 name = name or lineset.name
361 linestyle = lineset.linestyle
362 # extract style attributes from the linestyle and scene
363 svg = getCurrentScene().svg_export
364 style = {
365 'fill': 'none',
366 'stroke-width': linestyle.thickness,
367 'stroke-linecap': linestyle.caps.lower(),
368 'stroke-opacity': linestyle.alpha,
369 'stroke': format_rgb(linestyle.color),
370 'stroke-linejoin': svg.line_join_type.lower(),
372 # get dashed line pattern (if specified)
373 if linestyle.use_dashed_line:
374 style['stroke-dasharray'] = ",".join(str(elem) for elem in get_dashed_pattern(linestyle))
375 # return instance
376 return cls(name, style, filepath, res_y, split_at_invisible, use_stroke_color, frame_current)
379 @staticmethod
380 def pathgen(stroke, style, height, split_at_invisible, stroke_color_mode, f=lambda v: not v.attribute.visible):
381 """Generator that creates SVG paths (as strings) from the current stroke """
382 if len(stroke) <= 1:
383 return ""
385 if stroke_color_mode != 'BASE':
386 # try to use the color of the first or last vertex
387 try:
388 index = 0 if stroke_color_mode == 'FIRST' else -1
389 color = format_rgb(stroke[index].attribute.color)
390 style["stroke"] = color
391 except (ValueError, IndexError):
392 # default is linestyle base color
393 pass
395 # put style attributes into a single svg path definition
396 path = '\n<path ' + "".join('{}="{}" '.format(k, v) for k, v in style.items()) + 'd=" M '
398 it = iter(stroke)
399 # start first path
400 yield path
401 for v in it:
402 x, y = v.point
403 yield '{:.3f}, {:.3f} '.format(x, height - y)
404 if split_at_invisible and v.attribute.visible is False:
405 # end current and start new path;
406 yield '" />' + path
407 # fast-forward till the next visible vertex
408 it = itertools.dropwhile(f, it)
409 # yield next visible vertex
410 svert = next(it, None)
411 if svert is None:
412 break
413 x, y = svert.point
414 yield '{:.3f}, {:.3f} '.format(x, height - y)
415 # close current path
416 yield '" />'
418 def shade(self, stroke):
419 stroke_to_paths = "".join(self.pathgen(stroke, self.style, self.h, self.split_at_invisible, self.stroke_color_mode)).split("\n")
420 # convert to actual XML. Empty strokes are empty strings; they are ignored.
421 self.elements.extend(et.XML(elem) for elem in stroke_to_paths if elem) # if len(elem.strip()) > len(self.path))
423 def write(self):
424 """Write SVG data tree to file """
425 tree = et.parse(self.filepath)
426 root = tree.getroot()
427 name = self._name
428 scene = bpy.context.scene
430 # create <g> for lineset as a whole (don't overwrite)
431 # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills.
432 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(name))
433 if lineset_group is None:
434 lineset_group = et.XML('<g/>')
435 lineset_group.attrib = {
436 'id': name,
437 'xmlns:inkscape': namespaces["inkscape"],
438 'inkscape:groupmode': 'lineset',
439 'inkscape:label': name,
441 root.append(lineset_group)
443 # create <g> for the current frame
444 id = "frame_{:04n}".format(self.frame_current)
446 stroke_group = et.XML("<g/>")
447 stroke_group.attrib = {
448 'xmlns:inkscape': namespaces["inkscape"],
449 'inkscape:groupmode': 'layer',
450 'id': 'strokes',
451 'inkscape:label': 'strokes'
453 # nest the structure
454 stroke_group.extend(self.elements)
455 if scene.svg_export.mode == 'ANIMATION':
456 frame_group = et.XML("<g/>")
457 frame_group.attrib = {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id}
458 frame_group.append(stroke_group)
459 lineset_group.append(frame_group)
460 else:
461 lineset_group.append(stroke_group)
463 # write SVG to file
464 print("SVG Export: writing to", self.filepath)
465 indent_xml(root)
466 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
469 class SVGFillBuilder:
470 def __init__(self, filepath, height, name):
471 self.filepath = filepath
472 self._name = name
473 self.stroke_to_fill = partial(self.stroke_to_svg, height=height)
475 @staticmethod
476 def pathgen(vertices, path, height):
477 yield path
478 for point in vertices:
479 x, y = point
480 yield '{:.3f}, {:.3f} '.format(x, height - y)
481 yield ' z" />' # closes the path; connects the current to the first point
484 @staticmethod
485 def get_merged_strokes(strokes):
486 def extend_stroke(stroke, vertices):
487 for vert in map(StrokeVertex, vertices):
488 stroke.insert_vertex(vert, stroke.stroke_vertices_end())
489 return stroke
491 base_strokes = tuple(stroke for stroke in strokes if not is_poly_clockwise(stroke))
492 merged_strokes = OrderedDict((s, list()) for s in base_strokes)
494 for stroke in filter(is_poly_clockwise, strokes):
495 for base in base_strokes:
496 # don't merge when diffuse colors don't match
497 if diffuse_from_stroke(stroke) != diffuse_from_stroke(stroke):
498 continue
499 # only merge when the 'hole' is inside the base
500 elif stroke_inside_stroke(stroke, base):
501 merged_strokes[base].append(stroke)
502 break
503 # if it isn't a hole, it is likely that there are two strokes belonging
504 # to the same object separated by another object. let's try to join them
505 elif (get_object_name(base) == get_object_name(stroke) and
506 diffuse_from_stroke(stroke) == diffuse_from_stroke(stroke)):
507 base = extend_stroke(base, (sv for sv in stroke))
508 break
509 else:
510 # if all else fails, treat this stroke as a base stroke
511 merged_strokes.update({stroke: []})
512 return merged_strokes
515 def stroke_to_svg(self, stroke, height, parameters=None):
516 if parameters is None:
517 *color, alpha = diffuse_from_stroke(stroke)
518 color = tuple(int(255 * c) for c in color)
519 parameters = {
520 'fill_rule': 'evenodd',
521 'stroke': 'none',
522 'fill-opacity': alpha,
523 'fill': 'rgb' + repr(color),
525 param_str = " ".join('{}="{}"'.format(k, v) for k, v in parameters.items())
526 path = '<path {} d=" M '.format(param_str)
527 vertices = (svert.point for svert in stroke)
528 s = "".join(self.pathgen(vertices, path, height))
529 result = et.XML(s)
530 return result
532 def create_fill_elements(self, strokes):
533 """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
534 merged_strokes = self.get_merged_strokes(strokes)
535 for k, v in merged_strokes.items():
536 base = self.stroke_to_fill(k)
537 fills = (self.stroke_to_fill(stroke).get("d") for stroke in v)
538 merged_points = " ".join(fills)
539 base.attrib['d'] += merged_points
540 yield base
542 def write(self, strokes):
543 """Write SVG data tree to file """
545 tree = et.parse(self.filepath)
546 root = tree.getroot()
547 scene = bpy.context.scene
548 name = self._name
550 lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(self._name))
551 if lineset_group is None:
552 lineset_group = et.XML('<g/>')
553 lineset_group.attrib = {
554 'id': name,
555 'xmlns:inkscape': namespaces["inkscape"],
556 'inkscape:groupmode': 'lineset',
557 'inkscape:label': name,
559 root.append(lineset_group)
560 print('added new lineset group ', name)
563 # <g> for the fills of the current frame
564 fill_group = et.XML('<g/>')
565 fill_group.attrib = {
566 'xmlns:inkscape': namespaces["inkscape"],
567 'inkscape:groupmode': 'layer',
568 'inkscape:label': 'fills',
569 'id': 'fills'
572 fill_elements = self.create_fill_elements(strokes)
573 fill_group.extend(reversed(tuple(fill_elements)))
574 if scene.svg_export.mode == 'ANIMATION':
575 # add the fills to the <g> of the current frame
576 frame_group = find_svg_elem(lineset_group, ".//svg:g[@id='frame_{:04n}']".format(scene.frame_current))
577 frame_group.insert(0, fill_group)
578 else:
579 lineset_group.insert(0, fill_group)
581 # write SVG to file
582 indent_xml(root)
583 tree.write(self.filepath, encoding='ascii', xml_declaration=True)
586 def stroke_inside_stroke(a, b):
587 box_a = BoundingBox.from_sequence(svert.point for svert in a)
588 box_b = BoundingBox.from_sequence(svert.point for svert in b)
589 return box_a.inside(box_b)
592 def diffuse_from_stroke(stroke, curvemat=CurveMaterialF0D()):
593 material = curvemat(Interface0DIterator(stroke))
594 return material.diffuse
596 # - Callbacks - #
597 class ParameterEditorCallback(object):
598 """Object to store callbacks for the Parameter Editor in"""
599 def lineset_pre(self, scene, layer, lineset):
600 raise NotImplementedError()
602 def modifier_post(self, scene, layer, lineset):
603 raise NotImplementedError()
605 def lineset_post(self, scene, layer, lineset):
606 raise NotImplementedError()
610 class SVGPathShaderCallback(ParameterEditorCallback):
611 @classmethod
612 def poll(cls, scene, linestyle):
613 return scene.render.use_freestyle and scene.svg_export.use_svg_export and linestyle.use_export_strokes
615 @classmethod
616 def modifier_post(cls, scene, layer, lineset):
617 if not cls.poll(scene, lineset.linestyle):
618 return []
620 split = scene.svg_export.split_at_invisible
621 stroke_color_mode = lineset.linestyle.stroke_color_mode
622 cls.shader = SVGPathShader.from_lineset(
623 lineset, create_path(scene),
624 render_height(scene), split, stroke_color_mode, scene.frame_current, name=layer.name + '_' + lineset.name)
625 return [cls.shader]
627 @classmethod
628 def lineset_post(cls, scene, layer, lineset):
629 if not cls.poll(scene, lineset.linestyle):
630 return []
631 cls.shader.write()
634 class SVGFillShaderCallback(ParameterEditorCallback):
635 @classmethod
636 def poll(cls, scene, linestyle):
637 return scene.render.use_freestyle and scene.svg_export.use_svg_export and scene.svg_export.object_fill and linestyle.use_export_fills
639 @classmethod
640 def lineset_post(cls, scene, layer, lineset):
641 if not cls.poll(scene, lineset.linestyle):
642 return
644 # reset the stroke selection (but don't delete the already generated strokes)
645 Operators.reset(delete_strokes=False)
646 # Unary Predicates: visible and correct edge nature
647 upred = AndUP1D(
648 QuantitativeInvisibilityUP1D(0),
649 OrUP1D(ExternalContourUP1D(),
650 pyNatureUP1D(Nature.BORDER)),
652 # select the new edges
653 Operators.select(upred)
654 # Binary Predicates
655 bpred = AndBP1D(
656 MaterialBP1D(),
657 NotBP1D(pyZDiscontinuityBP1D()),
659 bpred = OrBP1D(bpred, AndBP1D(NotBP1D(bpred), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
660 # chain the edges
661 Operators.bidirectional_chain(ChainPredicateIterator(upred, bpred))
662 # export SVG
663 collector = StrokeCollector()
664 Operators.create(TrueUP1D(), [collector])
666 builder = SVGFillBuilder(create_path(scene), render_height(scene), layer.name + '_' + lineset.name)
667 builder.write(collector.strokes)
668 # make strokes used for filling invisible
669 for stroke in collector.strokes:
670 for svert in stroke:
671 svert.attribute.visible = False
675 def indent_xml(elem, level=0, indentsize=4):
676 """Prettifies XML code (used in SVG exporter) """
677 i = "\n" + level * " " * indentsize
678 if len(elem):
679 if not elem.text or not elem.text.strip():
680 elem.text = i + " " * indentsize
681 if not elem.tail or not elem.tail.strip():
682 elem.tail = i
683 for elem in elem:
684 indent_xml(elem, level + 1)
685 if not elem.tail or not elem.tail.strip():
686 elem.tail = i
687 elif level and (not elem.tail or not elem.tail.strip()):
688 elem.tail = i
691 def register_namespaces(namespaces=namespaces):
692 for name, url in namespaces.items():
693 if name != 'svg': # creates invalid xml
694 et.register_namespace(name, url)
696 @persistent
697 def handle_versions(self):
698 # We don't modify startup file because it assumes to
699 # have all the default values only.
700 if not bpy.data.is_saved:
701 return
703 # Revision https://developer.blender.org/rBA861519e44adc5674545fa18202dc43c4c20f2d1d
704 # changed the default for fills.
705 # fix by Sergey https://developer.blender.org/T46150
706 if bpy.data.version <= (2, 76, 0):
707 for linestyle in bpy.data.linestyles:
708 linestyle.use_export_fills = True
712 classes = (
713 SVGExporterPanel,
714 SVGExporterLinesetPanel,
715 SVGExport,
719 def register():
720 linestyle = bpy.types.FreestyleLineStyle
721 linestyle.use_export_strokes = BoolProperty(
722 name="Export Strokes",
723 description="Export strokes for this Line Style",
724 default=True,
726 linestyle.stroke_color_mode = EnumProperty(
727 name="Stroke Color Mode",
728 items=(
729 ('BASE', "Base Color", "Use the linestyle's base color", 0),
730 ('FIRST', "First Vertex", "Use the color of a stroke's first vertex", 1),
731 ('FINAL', "Final Vertex", "Use the color of a stroke's final vertex", 2),
733 default='BASE',
735 linestyle.use_export_fills = BoolProperty(
736 name="Export Fills",
737 description="Export fills for this Line Style",
738 default=False,
741 for cls in classes:
742 bpy.utils.register_class(cls)
743 bpy.types.Scene.svg_export = PointerProperty(type=SVGExport)
746 # add callbacks
747 bpy.app.handlers.render_init.append(render_init)
748 bpy.app.handlers.render_write.append(render_write)
749 bpy.app.handlers.render_pre.append(svg_export_header)
750 bpy.app.handlers.render_complete.append(svg_export_animation)
752 # manipulate shaders list
753 parameter_editor.callbacks_modifiers_post.append(SVGPathShaderCallback.modifier_post)
754 parameter_editor.callbacks_lineset_post.append(SVGPathShaderCallback.lineset_post)
755 parameter_editor.callbacks_lineset_post.append(SVGFillShaderCallback.lineset_post)
757 # register namespaces
758 register_namespaces()
760 # handle regressions
761 bpy.app.handlers.version_update.append(handle_versions)
764 def unregister():
766 for cls in classes:
767 bpy.utils.unregister_class(cls)
768 del bpy.types.Scene.svg_export
769 linestyle = bpy.types.FreestyleLineStyle
770 del linestyle.use_export_strokes
771 del linestyle.use_export_fills
773 # remove callbacks
774 bpy.app.handlers.render_init.remove(render_init)
775 bpy.app.handlers.render_write.remove(render_write)
776 bpy.app.handlers.render_pre.remove(svg_export_header)
777 bpy.app.handlers.render_complete.remove(svg_export_animation)
779 # manipulate shaders list
780 parameter_editor.callbacks_modifiers_post.remove(SVGPathShaderCallback.modifier_post)
781 parameter_editor.callbacks_lineset_post.remove(SVGPathShaderCallback.lineset_post)
782 parameter_editor.callbacks_lineset_post.remove(SVGFillShaderCallback.lineset_post)
784 bpy.app.handlers.version_update.remove(handle_versions)
787 if __name__ == "__main__":
788 register()