diff --git a/render_freestyle_svg.py b/render_freestyle_svg.py index 6423847fadb4b7c9873f762e555acb6444312e69..492672429d3184fc867151a45b36dd9e8bfcae5a 100644 --- a/render_freestyle_svg.py +++ b/render_freestyle_svg.py @@ -63,6 +63,7 @@ from bpy.props import ( ) from bpy.app.handlers import persistent from collections import OrderedDict +from functools import partial from mathutils import Vector @@ -78,6 +79,14 @@ namespaces = { "svg": "http://www.w3.org/2000/svg", } +# wrap XMLElem.find, so the namespaces don't need to be given as an argument +def find_xml_elem(obj, search, namespaces, *, all=False): + if all: + return obj.findall(search, namespaces=namespaces) + return obj.find(search, namespaces=namespaces) + +find_svg_elem = partial(find_xml_elem, namespaces=namespaces) + def render_height(scene): return int(scene.render.resolution_y * scene.render.resolution_percentage / 100) @@ -87,6 +96,7 @@ def render_width(scene): return int(scene.render.resolution_x * scene.render.resolution_percentage / 100) +# stores the state of the render, used to differ between animation and single frame renders. class RenderState: # Note that this flag is set to False only after the first frame # has been written to file. @@ -213,10 +223,10 @@ def write_animation(filepath, frame_begin, fps): tree = et.parse(filepath) root = tree.getroot() - linesets = tree.findall(".//svg:g[@inkscape:groupmode='lineset']", namespaces=namespaces) + linesets = find_svg_elem(tree, ".//svg:g[@inkscape:groupmode='lineset']", all=True) for i, lineset in enumerate(linesets): name = lineset.get('id') - frames = lineset.findall(".//svg:g[@inkscape:groupmode='frame']", namespaces=namespaces) + frames = find_svg_elem(lineset, ".//svg:g[@inkscape:groupmode='frame']", all=True) n_of_frames = len(frames) keyTimes = ";".join(str(round(x / n_of_frames, 3)) for x in range(n_of_frames)) + ";1" @@ -313,8 +323,9 @@ class SVGPathShader(StrokeShader): name = self._name scene = bpy.context.scene - # make <g> for lineset as a whole (don't overwrite) - lineset_group = tree.find(".//svg:g[@id='{}']".format(name), namespaces=namespaces) + # create <g> for lineset as a whole (don't overwrite) + # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills. + lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(name)) if lineset_group is None: lineset_group = et.XML('<g/>') lineset_group.attrib = { @@ -323,15 +334,11 @@ class SVGPathShader(StrokeShader): 'inkscape:groupmode': 'lineset', 'inkscape:label': name, } - root.insert(0, lineset_group) + root.append(lineset_group) - # make <g> for the current frame + # create <g> for the current frame id = "frame_{:04n}".format(self.frame_current) - if scene.svg_export.mode == 'ANIMATION': - frame_group = et.XML("<g/>") - frame_group.attrib = {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id} - stroke_group = et.XML("<g/>") stroke_group.attrib = {'xmlns:inkscape': namespaces["inkscape"], 'inkscape:groupmode': 'layer', @@ -340,13 +347,15 @@ class SVGPathShader(StrokeShader): # nest the structure stroke_group.extend(self.elements) if scene.svg_export.mode == 'ANIMATION': + frame_group = et.XML("<g/>") + frame_group.attrib = {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id} frame_group.append(stroke_group) lineset_group.append(frame_group) else: lineset_group.append(stroke_group) # write SVG to file - print("SVG Export: writing to ", self.filepath) + print("SVG Export: writing to", self.filepath) indent_xml(root) tree.write(self.filepath, encoding='ascii', xml_declaration=True) @@ -391,6 +400,7 @@ class SVGFillShader(StrokeShader): root = tree.getroot() name = self._name scene = bpy.context.scene + lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(name)) # create XML elements from the acquired data elems = [] @@ -400,39 +410,30 @@ class SVGFillShader(StrokeShader): for stroke in strokes: elems.append(et.XML("".join(self.pathgen((sv.point for sv in stroke), p, self.h)))) - # make <g> for lineset as a whole (don't overwrite) - lineset_group = tree.find(".//svg:g[@id='{}']".format(name), namespaces=namespaces) - if lineset_group is None: - lineset_group = et.XML('<g/>') - lineset_group.attrib = { - 'id': name, - 'xmlns:inkscape': namespaces["inkscape"], - 'inkscape:groupmode': 'lineset', - 'inkscape:label': name, - } - root.insert(0, lineset_group) - if scene.svg_export.mode == 'ANIMATION': # add the fills to the <g> of the current frame - frame_group = tree.find(".//svg:g[@id='frame_{:04n}']".format(scene.frame_current), namespaces=namespaces) + frame_group = find_svg_elem(lineset_group, ".//svg:g[@id='frame_{:04n}']".format(scene.frame_current)) if frame_group is None: # something has gone very wrong raise RuntimeError("SVGFillShader: frame_group is None") - # add <g> for the strokes of the current frame - stroke_group = et.XML("<g/>") - stroke_group.attrib = {'xmlns:inkscape': namespaces["inkscape"], - 'inkscape:groupmode': 'layer', - 'inkscape:label': 'fills', - 'id': 'fills'} - # reverse fills to get the correct order - stroke_group.extend(reversed(elems)) + # <g> for the fills of the current frame + fill_group = et.XML('<g/>') + fill_group.attrib = { + 'xmlns:inkscape': namespaces["inkscape"], + 'inkscape:groupmode': 'layer', + 'inkscape:label': 'fills', + 'id': 'fills' + } + fill_group.extend(reversed(elems)) if scene.svg_export.mode == 'ANIMATION': - frame_group.insert(0, stroke_group) + frame_group.insert(0, fill_group) else: - lineset_group.insert(0, stroke_group) + # get the current lineset group. if it's None we're in trouble, so may as well error hard. + lineset_group = tree.find(".//svg:g[@id='{}']".format(name), namespaces=namespaces) + lineset_group.insert(0, fill_group) # write SVG to file indent_xml(root) @@ -461,7 +462,7 @@ class SVGPathShaderCallback(ParameterEditorCallback): split = scene.svg_export.split_at_invisible cls.shader = SVGPathShader.from_lineset( lineset, create_path(scene), - render_height(scene), split, scene.frame_current) + render_height(scene), split, scene.frame_current, name=layer.name + '_' + lineset.name) return [cls.shader] @classmethod @@ -489,8 +490,9 @@ class SVGFillShaderCallback(ParameterEditorCallback): # sort according to the distance from camera Operators.sort(pyZBP1D()) # render and write fills - shader = SVGFillShader(create_path(scene), render_height(scene), lineset.name) + shader = SVGFillShader(create_path(scene), render_height(scene), layer.name + '_' + lineset.name) Operators.create(TrueUP1D(), [shader, ]) + shader.write() @@ -559,3 +561,4 @@ def unregister(): if __name__ == "__main__": register() +