[Bf-blender-cvs] [61a330b] master: Freestyle: Built-in SVG exporter.

Tamito Kajiyama noreply at git.blender.org
Sat Oct 18 12:19:11 CEST 2014


Commit: 61a330baca0ff9bb3cf477c04f539ef276a0356f
Author: Tamito Kajiyama
Date:   Sat Oct 18 18:35:29 2014 +0900
Branches: master
https://developer.blender.org/rB61a330baca0ff9bb3cf477c04f539ef276a0356f

Freestyle: Built-in SVG exporter.

Features:
* Both still image and animation rendering, as well as polygon
  fills are supported.
* The exporter creates a new SVG layer for every Freestyle line
  set. The different layers are correctly sorted.
* SVG paths use data from line styles, so the base color of a
  line style becomes the color of paths, idem for dashes and
  stroke thickness.
* Strokes can be split at invisible parts.  This functionality is
  useful when exporting for instance dashed lines or line styles
  with a Blue Print shader
* The exporter can be used not only in the Parameter Editor mode,
  but also from within style modules written for the Python
  Scripting mode.

Acknowledgements:
The author would like to thank Francesco Fantoni and Jarno
Leppänen for their [[ https://github.com/hvfrancesco/freestylesvg | Freestyle SVG exporter ]].

Differential revision: https://developer.blender.org/D785

Author: flokkievids (Folkert de Vries)

Reviewed by: kjym3 (Tamito Kajiyama)

===================================================================

M	release/scripts/freestyle/modules/freestyle/utils.py
M	release/scripts/freestyle/modules/parameter_editor.py
A	release/scripts/freestyle/modules/svg_export.py
M	release/scripts/startup/bl_ui/properties_freestyle.py
A	release/scripts/startup/freestyle_builtins.py
M	source/blender/blenkernel/intern/scene.c
M	source/blender/blenloader/intern/versioning_270.c
M	source/blender/blenloader/intern/versioning_defaults.c
M	source/blender/makesdna/DNA_scene_types.h
M	source/blender/makesrna/intern/rna_scene.c

===================================================================

diff --git a/release/scripts/freestyle/modules/freestyle/utils.py b/release/scripts/freestyle/modules/freestyle/utils.py
index 6c5e1d5..a8e4743 100644
--- a/release/scripts/freestyle/modules/freestyle/utils.py
+++ b/release/scripts/freestyle/modules/freestyle/utils.py
@@ -86,6 +86,20 @@ def bounding_box(stroke):
     x, y = zip(*(svert.point for svert in stroke))
     return (Vector((min(x), min(y))), Vector((max(x), max(y))))
 
+def get_dashed_pattern(linestyle):
+    """Extracts the dashed pattern from the various UI options """
+    pattern = []
+    if linestyle.dash1 > 0 and linestyle.gap1 > 0:
+        pattern.append(linestyle.dash1)
+        pattern.append(linestyle.gap1)
+    if linestyle.dash2 > 0 and linestyle.gap2 > 0:
+        pattern.append(linestyle.dash2)
+        pattern.append(linestyle.gap2)
+    if linestyle.dash3 > 0 and linestyle.gap3 > 0:
+        pattern.append(linestyle.dash3)
+        pattern.append(linestyle.gap3)
+    return pattern
+
 # -- General helper functions -- #
 
 
diff --git a/release/scripts/freestyle/modules/parameter_editor.py b/release/scripts/freestyle/modules/parameter_editor.py
index 9ac5c66..0498213 100644
--- a/release/scripts/freestyle/modules/parameter_editor.py
+++ b/release/scripts/freestyle/modules/parameter_editor.py
@@ -59,6 +59,7 @@ from freestyle.predicates import (
     NotUP1D,
     OrUP1D,
     QuantitativeInvisibilityUP1D,
+    SameShapeIdBP1D,
     TrueBP1D,
     TrueUP1D,
     WithinImageBoundaryUP1D,
@@ -97,7 +98,8 @@ from freestyle.utils import (
     stroke_normal,
     bound,
     pairwise,
-    BoundedProperty
+    BoundedProperty,
+    get_dashed_pattern,
     )
 from _freestyle import (
     blendRamp,
@@ -105,10 +107,19 @@ from _freestyle import (
     evaluateCurveMappingF,
     )
 
+from svg_export import (
+    SVGPathShader,
+    SVGFillShader,
+    ShapeZ,
+    )
+
 import time
+
 from mathutils import Vector
 from math import pi, sin, cos, acos, radians
 from itertools import cycle, tee
+from bpy.path import abspath
+from os.path import isfile
 
 
 class ColorRampModifier(StrokeShader):
@@ -419,7 +430,7 @@ class ColorMaterialShader(ColorRampModifier):
             for svert in it:
                 material = self.func(it)
                 if self.attribute == 'LINE':
-                    b = material.line[0:3] 
+                    b = material.line[0:3]
                 elif self.attribute == 'DIFF':
                     b = material.diffuse[0:3]
                 else:
@@ -887,7 +898,6 @@ integration_types = {
 
 
 # main function for parameter processing
-
 def process(layer_name, lineset_name):
     scene = getCurrentScene()
     layer = scene.render.layers[layer_name]
@@ -1172,24 +1182,51 @@ def process(layer_name, lineset_name):
                 has_tex = True
     if has_tex:
         shaders_list.append(StrokeTextureStepShader(linestyle.texture_spacing))
+    # -- Dashed line -- #
+    if linestyle.use_dashed_line:
+        pattern = get_dashed_pattern(linestyle)
+        if len(pattern) > 0:
+            shaders_list.append(DashedLineShader(pattern))
+    # -- SVG export -- #
+    render = scene.render
+    filepath = abspath(render.svg_path)
+    # if the export path is invalid: log to console, but continue normal rendering
+    if render.use_svg_export:
+        if not isfile(filepath):
+            print("Error: SVG export: path is invalid")
+        else:
+            height = render.resolution_y * render.resolution_percentage / 100
+            split_at_inv = render.svg_split_at_invisible
+            frame_current = scene.frame_current
+            # SVGPathShader: keep reference and add to shader list
+            renderer = SVGPathShader.from_lineset(lineset, filepath, height, split_at_inv, frame_current)
+            shaders_list.append(renderer)
+
     # -- Stroke caps -- #
+    # appended after svg shader to ensure correct svg output
     if linestyle.caps == 'ROUND':
         shaders_list.append(RoundCapShader())
     elif linestyle.caps == 'SQUARE':
         shaders_list.append(SquareCapShader())
-    # -- Dashed line -- #
-    if linestyle.use_dashed_line:
-        pattern = []
-        if linestyle.dash1 > 0 and linestyle.gap1 > 0:
-            pattern.append(linestyle.dash1)
-            pattern.append(linestyle.gap1)
-        if linestyle.dash2 > 0 and linestyle.gap2 > 0:
-            pattern.append(linestyle.dash2)
-            pattern.append(linestyle.gap2)
-        if linestyle.dash3 > 0 and linestyle.gap3 > 0:
-            pattern.append(linestyle.dash3)
-            pattern.append(linestyle.gap3)
-        if len(pattern) > 0:
-            shaders_list.append(DashedLineShader(pattern))
+
     # create strokes using the shaders list
     Operators.create(TrueUP1D(), shaders_list)
+
+    if render.use_svg_export and isfile(filepath):
+        # write svg output to file
+        renderer.write()
+        if render.svg_use_object_fill:
+            # reset the stroke selection (but don't delete the already generated ones)
+            Operators.reset(delete_strokes=False)
+            # shape detection
+            upred = AndUP1D(QuantitativeInvisibilityUP1D(0), ContourUP1D())
+            Operators.select(upred)
+            # chain when the same shape and visible
+            bpred = SameShapeIdBP1D()
+            Operators.bidirectional_chain(ChainPredicateIterator(upred, bpred), NotUP1D(QuantitativeInvisibilityUP1D(0)))
+            # sort according to the distance from camera
+            Operators.sort(ShapeZ(scene))
+            # render and write fills
+            renderer = SVGFillShader(filepath, height, lineset.name)
+            Operators.create(TrueUP1D(), [renderer,])
+            renderer.write()
diff --git a/release/scripts/freestyle/modules/svg_export.py b/release/scripts/freestyle/modules/svg_export.py
new file mode 100644
index 0000000..0956673
--- /dev/null
+++ b/release/scripts/freestyle/modules/svg_export.py
@@ -0,0 +1,305 @@
+import bpy
+import xml.etree.cElementTree as et
+
+from bpy.path import abspath
+from bpy.app.handlers import persistent
+from bpy_extras.object_utils import world_to_camera_view
+
+from freestyle.types import StrokeShader, ChainingIterator, BinaryPredicate1D, Interface0DIterator, AdjacencyIterator
+from freestyle.utils import getCurrentScene, get_dashed_pattern, get_test_stroke
+from freestyle.functions import GetShapeF1D, CurveMaterialF0D
+
+from itertools import dropwhile, repeat
+from collections import OrderedDict
+
+__all__ = (
+    "SVGPathShader",
+    "SVGFillShader",
+    "ShapeZ",
+    "indent_xml",
+    "svg_export_header",
+    "svg_export_animation",
+    )
+
+# register namespaces
+et.register_namespace("", "http://www.w3.org/2000/svg")
+et.register_namespace("inkscape", "http://www.inkscape.org/namespaces/inkscape")
+et.register_namespace("sodipodi", "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd")
+
+
+# use utf-8 here to keep ElementTree happy, end result is utf-16
+svg_primitive = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{:d}" height="{:d}">
+</svg>"""
+
+
+# xml namespaces
+namespaces = {
+    "inkscape": "http://www.inkscape.org/namespaces/inkscape",
+    "svg": "http://www.w3.org/2000/svg",
+    }
+
+# - SVG export - #
+class SVGPathShader(StrokeShader):
+    """Stroke Shader for writing stroke data to a .svg file."""
+    def __init__(self, name, style, filepath, res_y, split_at_invisible, frame_current):
+        StrokeShader.__init__(self)
+        # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
+        self._name = name
+        self.filepath = filepath
+        self.h = res_y
+        self.frame_current = frame_current
+        self.elements = []
+        self.split_at_invisible = split_at_invisible
+        # put style attributes into a single svg path definition
+        self.path = '\n<path ' + "".join('{}="{}" '.format(k, v) for k, v in style.items()) + 'd=" M '
+
+    @classmethod
+    def from_lineset(cls, lineset, filepath, res_y, split_at_invisible, frame_current, *, name=""):
+        """Builds a SVGPathShader using data from the given lineset"""
+        name = name or lineset.name
+        linestyle = lineset.linestyle
+        # extract style attributes from the linestyle
+        style = {
+            'fill': 'none',
+            'stroke-width': linestyle.thickness,
+            'stroke-linecap': linestyle.caps.lower(),
+            'stroke-opacity': linestyle.alpha,
+            'stroke': 'rgb({}, {}, {})'.format(*(int(c * 255) for c in linestyle.color))
+            }
+        # get dashed line pattern (if specified)
+        if linestyle.use_dashed_line:
+            style['stroke-dasharray'] = ",".join(str(elem) for elem in get_dashed_pattern(linestyle))
+        # return instance
+        return cls(name, style, filepath, res_y, split_at_invisible, frame_current)
+
+    @staticmethod
+    def pathgen(stroke, path, height, split_at_invisible, f=lambda v: not v.attribute.visible):
+        """Generator that creates SVG paths (as strings) from the current stroke """
+        it = iter(stroke)
+        # start first path
+        yield path
+        for v in it:
+            x, y = v.point
+            yield '{:.3f}, {:.3f} '.format(x, height - y)
+            if split_at_invisible and v.attribute.visible == False:
+                # end current and start new path;
+                yield '" />' + path
+                # fast-forward till the next visible vertex
+                it = dropwhile(f, it)
+                # yield next visible vertex
+                svert = next(it, None)
+                if svert is None:
+                    break
+                x, y = svert.point
+                yield '{:.3f}, {:.3f} '.format(x, height - y)
+        # close current path
+        yield '" />'
+
+    def shade(self, stroke):
+        stroke_to_paths = "".join(self.pathgen(stroke, self.path, self.h, self.split_at_invisible)).split("\n")
+        # convert to actual XML, check to prevent empty paths
+        self.elements.extend(et.XML(elem) for elem in stroke_to_paths if len(elem.strip()) > len(self.path))
+
+    def write(self):
+        """Write 

@@ Diff output truncated at 10240 characters. @@




More information about the Bf-blender-cvs mailing list