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 #####
22 "name": "Freestyle SVG Exporter",
23 "author": "Folkert de Vries",
25 "blender": (2, 80, 0),
26 "location": "Properties > Render > Freestyle SVG Export",
27 "description": "Exports Freestyle's stylized edges in SVG format",
29 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/render/render_freestyle_svg.html",
30 "support": 'OFFICIAL',
35 import parameter_editor
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 (
53 from freestyle
.utils
import (
61 from freestyle
.functions
import (
65 from freestyle
.predicates
import (
78 QuantitativeInvisibilityUP1D
,
83 from freestyle
.chainingiterators
import ChainPredicateIterator
84 from parameter_editor
import get_dashed_pattern
86 from bpy
.props
import (
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}">
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):
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.
134 # Note that this flag is set to False only after the first frame
135 # has been written to file.
140 def render_init(scene
):
141 RenderState
.is_preview
= True
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
):
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
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
)
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
):
191 scene
= context
.scene
192 svg
= scene
.svg_export
193 freestyle
= context
.window
.view_layer
.freestyle_settings
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
205 layout
.active
= (svg
.use_svg_export
and freestyle
.mode
!= 'SCRIPT')
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')
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(
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",
237 ('FRAME', "Frame", "Export a single frame", 0),
238 ('ANIMATION', "Animation", "Export an animation", 1),
242 line_join_type
: EnumProperty(
245 ('MITER', "Miter", "Corners are sharp", 0),
246 ('ROUND', "Round", "Corners are smoothed", 1),
247 ('BEVEL', "Bevel", "Corners are bevelled", 2),
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
):
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')
274 row
.prop(svg
, "mode", expand
=True)
277 row
.prop(svg
, "split_at_invisible")
278 row
.prop(svg
, "object_fill")
281 row
.prop(svg
, "line_join_type", expand
=True)
285 def svg_export_header(scene
):
286 if not (scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
):
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
:
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
)))
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"
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
)
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
)
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
349 self
.filepath
= filepath
351 self
.frame_current
= frame_current
353 self
.split_at_invisible
= split_at_invisible
354 self
.stroke_color_mode
= stroke_color_mode
# BASE | FIRST | LAST
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
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
))
377 return cls(name
, style
, filepath
, res_y
, split_at_invisible
, use_stroke_color
, frame_current
)
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 """
386 if stroke_color_mode
!= 'BASE':
387 # try to use the color of the first or last vertex
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
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 '
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;
408 # fast-forward till the next visible vertex
409 it
= itertools
.dropwhile(f
, it
)
410 # yield next visible vertex
411 svert
= next(it
, None)
415 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
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))
425 """Write SVG data tree to file """
426 tree
= et
.parse(self
.filepath
)
427 root
= tree
.getroot()
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
= {
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',
452 'inkscape:label': 'strokes'
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
)
462 lineset_group
.append(stroke_group
)
465 print("SVG Export: writing to", self
.filepath
)
467 tree
.write(self
.filepath
, encoding
='ascii', xml_declaration
=True)
470 class SVGFillBuilder
:
471 def __init__(self
, filepath
, height
, name
):
472 self
.filepath
= filepath
474 self
.stroke_to_fill
= partial(self
.stroke_to_svg
, height
=height
)
477 def pathgen(vertices
, path
, height
):
479 for point
in vertices
:
481 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
482 yield ' z" />' # closes the path; connects the current to the first point
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())
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
):
500 # only merge when the 'hole' is inside the base
501 elif stroke_inside_stroke(stroke
, base
):
502 merged_strokes
[base
].append(stroke
)
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
))
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
)
521 'fill_rule': 'evenodd',
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
))
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
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
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
= {
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',
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
)
580 lineset_group
.insert(0, fill_group
)
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
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
):
613 def poll(cls
, scene
, linestyle
):
614 return scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
and linestyle
.use_export_strokes
617 def modifier_post(cls
, scene
, layer
, lineset
):
618 if not cls
.poll(scene
, lineset
.linestyle
):
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
)
629 def lineset_post(cls
, scene
, layer
, lineset
):
630 if not cls
.poll(scene
, lineset
.linestyle
):
635 class SVGFillShaderCallback(ParameterEditorCallback
):
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
641 def lineset_post(cls
, scene
, layer
, lineset
):
642 if not cls
.poll(scene
, lineset
.linestyle
):
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
649 QuantitativeInvisibilityUP1D(0),
650 OrUP1D(ExternalContourUP1D(),
651 pyNatureUP1D(Nature
.BORDER
)),
653 # select the new edges
654 Operators
.select(upred
)
658 NotBP1D(pyZDiscontinuityBP1D()),
660 bpred
= OrBP1D(bpred
, AndBP1D(NotBP1D(bpred
), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
662 Operators
.bidirectional_chain(ChainPredicateIterator(upred
, bpred
))
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
:
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
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():
685 indent_xml(elem
, level
+ 1)
686 if not elem
.tail
or not elem
.tail
.strip():
688 elif level
and (not elem
.tail
or not elem
.tail
.strip()):
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
)
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
:
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
715 SVGExporterLinesetPanel
,
721 linestyle
= bpy
.types
.FreestyleLineStyle
722 linestyle
.use_export_strokes
= BoolProperty(
723 name
="Export Strokes",
724 description
="Export strokes for this Line Style",
727 linestyle
.stroke_color_mode
= EnumProperty(
728 name
="Stroke Color Mode",
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),
736 linestyle
.use_export_fills
= BoolProperty(
738 description
="Export fills for this Line Style",
743 bpy
.utils
.register_class(cls
)
744 bpy
.types
.Scene
.svg_export
= PointerProperty(type=SVGExport
)
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()
762 bpy
.app
.handlers
.version_update
.append(handle_versions
)
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
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__":