[Bf-extensions-cvs] [1aaf1d7] master: Freestyle SVG Exporter: more robust filling

Folkert de Vries noreply at git.blender.org
Sun May 31 16:41:13 CEST 2015


Commit: 1aaf1d7b767d51a5e38ff18bf6569724bdcd2d7e
Author: Folkert de Vries
Date:   Sun May 31 16:46:21 2015 +0200
Branches: master
https://developer.blender.org/rBA1aaf1d7b767d51a5e38ff18bf6569724bdcd2d7e

Freestyle SVG Exporter: more robust filling

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

M	render_freestyle_svg.py

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

diff --git a/render_freestyle_svg.py b/render_freestyle_svg.py
index 4926724..9328f71 100644
--- a/render_freestyle_svg.py
+++ b/render_freestyle_svg.py
@@ -37,21 +37,47 @@ import os
 
 import xml.etree.cElementTree as et
 
+from bpy.app.handlers import persistent
+from collections import OrderedDict
+from functools import partial
+from mathutils import Vector
+
 from freestyle.types import (
         StrokeShader,
         Interface0DIterator,
         Operators,
+        Nature,
+        StrokeVertex,
         )
-from freestyle.utils import getCurrentScene
-from freestyle.functions import GetShapeF1D, CurveMaterialF0D
+from freestyle.utils import (
+    getCurrentScene,
+    BoundingBox,
+    is_poly_clockwise,
+    StrokeCollector,
+    material_from_fedge,
+    get_object_name,
+    )
+from freestyle.functions import (
+    GetShapeF1D, 
+    CurveMaterialF0D,
+    )
 from freestyle.predicates import (
+        AndBP1D,
         AndUP1D,
         ContourUP1D,
-        SameShapeIdBP1D,
+        ExternalContourUP1D,
+        MaterialBP1D,
+        NotBP1D,
         NotUP1D,
+        OrBP1D,
+        OrUP1D,
+        pyNatureUP1D,
+        pyZBP1D,
+        pyZDiscontinuityBP1D,
         QuantitativeInvisibilityUP1D,
+        SameShapeIdBP1D,
+        TrueBP1D,
         TrueUP1D,
-        pyZBP1D,
         )
 from freestyle.chainingiterators import ChainPredicateIterator
 from parameter_editor import get_dashed_pattern
@@ -61,14 +87,12 @@ from bpy.props import (
         EnumProperty,
         PointerProperty,
         )
-from bpy.app.handlers import persistent
-from collections import OrderedDict
-from functools import partial
-from mathutils import Vector
 
 
 # use utf-8 here to keep ElementTree happy, end result is utf-16
 svg_primitive = """<?xml version="1.0" encoding="ascii" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{:d}" height="{:d}">
 </svg>"""
 
@@ -77,8 +101,11 @@ svg_primitive = """<?xml version="1.0" encoding="ascii" standalone="no"?>
 namespaces = {
     "inkscape": "http://www.inkscape.org/namespaces/inkscape",
     "svg": "http://www.w3.org/2000/svg",
+    "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
+    "": "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:
@@ -98,6 +125,7 @@ def render_width(scene):
 
 # 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.
     is_preview = True
@@ -288,6 +316,7 @@ class SVGPathShader(StrokeShader):
         # 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 """
@@ -340,10 +369,12 @@ class SVGPathShader(StrokeShader):
         id = "frame_{:04n}".format(self.frame_current)
 
         stroke_group = et.XML("<g/>")
-        stroke_group.attrib = {'xmlns:inkscape': namespaces["inkscape"],
-                               'inkscape:groupmode': 'layer',
-                               'id': 'strokes',
-                               'inkscape:label': 'strokes'}
+        stroke_group.attrib = {
+            'xmlns:inkscape': namespaces["inkscape"],
+            'inkscape:groupmode': 'layer',
+            'id': 'strokes',
+            'inkscape:label': 'strokes'
+            }
         # nest the structure
         stroke_group.extend(self.elements)
         if scene.svg_export.mode == 'ANIMATION':
@@ -360,30 +391,11 @@ class SVGPathShader(StrokeShader):
         tree.write(self.filepath, encoding='ascii', xml_declaration=True)
 
 
-class SVGFillShader(StrokeShader):
-    """Creates SVG fills from the current stroke set"""
+class SVGFillBuilder:
     def __init__(self, filepath, height, name):
-        StrokeShader.__init__(self)
-        # use an ordered dict to maintain input and z-order
-        self.shape_map = OrderedDict()
         self.filepath = filepath
-        self.h = height
         self._name = name
-
-    def shade(self, stroke, func=GetShapeF1D(), curvemat=CurveMaterialF0D()):
-        shape = func(stroke)[0].id.first
-        item = self.shape_map.get(shape)
-        if len(stroke) > 2:
-            if item is not None:
-                item[0].append(stroke)
-            else:
-                # the shape is not yet present, let's create it.
-                material = curvemat(Interface0DIterator(stroke))
-                *color, alpha = material.diffuse
-                self.shape_map[shape] = ([stroke], color, alpha)
-        # make the strokes of the second drawing invisible
-        for v in stroke:
-            v.attribute.visible = False
+        self.stroke_to_fill = partial(self.stroke_to_svg, height=height)
 
     @staticmethod
     def pathgen(vertices, path, height):
@@ -391,48 +403,94 @@ class SVGFillShader(StrokeShader):
         for point in vertices:
             x, y = point
             yield '{:.3f}, {:.3f} '.format(x, height - y)
-        yield 'z" />'  # closes the path; connects the current to the first point
+        yield ' z" />'  # closes the path; connects the current to the first point
 
-    def write(self):
+
+    @staticmethod
+    def get_merged_strokes(strokes):
+        def extend_stroke(stroke, vertices):
+            for vert in map(StrokeVertex, vertices):
+                stroke.insert_vertex(vert, stroke.stroke_vertices_end())
+            return stroke
+
+        base_strokes = tuple(stroke for stroke in strokes if not is_poly_clockwise(stroke))
+        merged_strokes = OrderedDict((s, list()) for s in base_strokes)
+
+        for stroke in filter(is_poly_clockwise, strokes):
+            for base in base_strokes:
+                # don't merge when diffuse colors don't match
+                if diffuse_from_stroke(stroke) != diffuse_from_stroke(stroke):
+                    continue
+                # only merge when the 'hole' is inside the base
+                elif stroke_inside_stroke(stroke, base):
+                    merged_strokes[base].append(stroke)
+                    break
+                # if it isn't a hole, it is likely that there are two strokes belonging
+                # to the same object separated by another object. let's try to join them
+                elif (get_object_name(base) == get_object_name(stroke) and 
+                      diffuse_from_stroke(stroke) == diffuse_from_stroke(stroke)):
+                    base = extend_stroke(base, (sv for sv in stroke))
+                    break
+            else:
+                # if all else fails, treat this stroke as a base stroke
+                merged_strokes.update({stroke:  []})
+        return merged_strokes
+
+
+    def stroke_to_svg(self, stroke, height, parameters=None):
+        if parameters is None:
+            *color, alpha = diffuse_from_stroke(stroke)
+            color = tuple(int(255 * c) for c in color)
+            parameters = {
+                'fill_rule': 'evenodd',
+                'stroke': 'none',
+                'fill-opacity': alpha,
+                'fill': 'rgb' + repr(color),
+            }
+        param_str = " ".join('{}="{}"'.format(k, v) for k, v in parameters.items())
+        path = '<path {} d=" M '.format(param_str)
+        vertices = (svert.point for svert in stroke)
+        s = "".join(self.pathgen(vertices, path, height))
+        result = et.XML(s)
+        return result
+
+    def create_fill_elements(self, strokes):
+        """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
+        merged_strokes = self.get_merged_strokes(strokes)
+        for k, v in merged_strokes.items():
+            base = self.stroke_to_fill(k)
+            fills = (self.stroke_to_fill(stroke).get("d") for stroke in v)
+            merged_points = " ".join(fills)
+            base.attrib['d'] += merged_points
+            yield base 
+
+    def write(self, strokes):
         """Write SVG data tree to file """
-        # initialize SVG
+
         tree = et.parse(self.filepath)
         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 = []
-        path = '<path fill-rule="evenodd" stroke="none" fill-opacity="{}" fill="rgb({}, {}, {})"  d=" M '
-        for strokes, col, alpha in self.shape_map.values():
-            p = path.format(alpha, *(int(255 * c) for c in col))
-            for stroke in strokes:
-                elems.append(et.XML("".join(self.pathgen((sv.point for sv in stroke), p, self.h))))
-
-        if scene.svg_export.mode == 'ANIMATION':
-            # add the fills to the <g> of the current frame
-            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")
-
+        lineset_group = find_svg_elem(tree, ".//svg:g[@id='{}']".format(self._name))
+        if lineset_group is None:
+            print("searched for {}, but could not find a <g> with that id".format(self._name))
+            return
 
         # <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'
+            'inkscape:groupmode': 'layer',
+            'inkscape:label': 'fills',
+            'id': 'fills'
            }
 
-        fill_group.extend(reversed(elems))
+        fill_elements = self.create_fill_

@@ Diff output truncated at 10240 characters. @@



More information about the Bf-extensions-cvs mailing list