Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFolkert de Vries <flokkievids@gmail.com>2015-05-31 17:46:21 +0300
committerFolkert de Vries <flokkievids@gmail.com>2015-05-31 17:46:21 +0300
commit1aaf1d7b767d51a5e38ff18bf6569724bdcd2d7e (patch)
tree7f58d4f54b0a748f1823fd27479366f7405bdfff /render_freestyle_svg.py
parent83b911f409b9ddc7dbcacbe3a4e111506f26a350 (diff)
Freestyle SVG Exporter: more robust filling
Diffstat (limited to 'render_freestyle_svg.py')
-rw-r--r--render_freestyle_svg.py247
1 files changed, 170 insertions, 77 deletions
diff --git a/render_freestyle_svg.py b/render_freestyle_svg.py
index 49267242..9328f713 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_elements(strokes)
+ fill_group.extend(reversed(tuple(fill_elements)))
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))
frame_group.insert(0, fill_group)
else:
- # 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
@@ -440,6 +498,16 @@ class SVGFillShader(StrokeShader):
tree.write(self.filepath, encoding='ascii', xml_declaration=True)
+def stroke_inside_stroke(a, b):
+ box_a = BoundingBox.from_sequence(svert.point for svert in a)
+ box_b = BoundingBox.from_sequence(svert.point for svert in b)
+ return box_a.inside(box_b)
+
+
+def diffuse_from_stroke(stroke, curvemat=CurveMaterialF0D()):
+ material = curvemat(Interface0DIterator(stroke))
+ return material.diffuse
+
# - Callbacks - #
class ParameterEditorCallback(object):
"""Object to store callbacks for the Parameter Editor in"""
@@ -452,11 +520,19 @@ class ParameterEditorCallback(object):
def lineset_post(self, scene, layer, lineset):
raise NotImplementedError()
+ @classmethod
+ def evaluate(cls, scene):
+ 'Evaluates whether these callbacks should run'
+ return (
+ scene.render.use_freestyle
+ and scene.svg_export.use_svg_export
+ )
+
class SVGPathShaderCallback(ParameterEditorCallback):
@classmethod
def modifier_post(cls, scene, layer, lineset):
- if not (scene.render.use_freestyle and scene.svg_export.use_svg_export):
+ if not cls.evaluate(scene):
return []
split = scene.svg_export.split_at_invisible
@@ -467,9 +543,8 @@ class SVGPathShaderCallback(ParameterEditorCallback):
@classmethod
def lineset_post(cls, scene, *args):
- if not (scene.render.use_freestyle and scene.svg_export.use_svg_export):
+ if not cls.evaluate(scene):
return
-
cls.shader.write()
@@ -481,19 +556,33 @@ class SVGFillShaderCallback(ParameterEditorCallback):
# reset the stroke selection (but don't delete the already generated strokes)
Operators.reset(delete_strokes=False)
- # shape detection
- upred = AndUP1D(QuantitativeInvisibilityUP1D(0), ContourUP1D())
+ # Unary Predicates: visible and correct edge nature
+ upred = AndUP1D(
+ QuantitativeInvisibilityUP1D(0),
+ OrUP1D(ExternalContourUP1D(),
+ pyNatureUP1D(Nature.BORDER)),
+ )
+ # select the new edges
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(pyZBP1D())
- # render and write fills
- shader = SVGFillShader(create_path(scene), render_height(scene), layer.name + '_' + lineset.name)
- Operators.create(TrueUP1D(), [shader, ])
+ # Binary Predicates
+ bpred = AndBP1D(
+ MaterialBP1D(),
+ NotBP1D(pyZDiscontinuityBP1D()),
+ )
+ bpred = OrBP1D(bpred, AndBP1D(NotBP1D(bpred), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
+ # chain the edges
+ Operators.bidirectional_chain(ChainPredicateIterator(upred, bpred))
+ # export SVG
+ collector = StrokeCollector()
+ Operators.create(TrueUP1D(), [collector])
+
+ builder = SVGFillBuilder(create_path(scene), render_height(scene), layer.name + '_' + lineset.name)
+ builder.write(collector.strokes)
+ # make strokes used for filling invisible
+ for stroke in collector.strokes:
+ for svert in stroke:
+ svert.attribute.visible = False
- shader.write()
def indent_xml(elem, level=0, indentsize=4):
@@ -512,6 +601,12 @@ def indent_xml(elem, level=0, indentsize=4):
elem.tail = i
+def register_namespaces(namespaces=namespaces):
+ for name, url in namespaces.items():
+ if name != 'svg': # creates invalid xml
+ et.register_namespace(name, url)
+
+
classes = (
SVGExporterPanel,
SVGExport,
@@ -536,9 +631,7 @@ def register():
parameter_editor.callbacks_lineset_post.append(SVGFillShaderCallback.lineset_post)
# 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")
+ register_namespaces()
def unregister():