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. @@ _______________________________________________ Bf-blender-cvs mailing list Bf-blender-cvs@blender.org http://lists.blender.org/mailman/listinfo/bf-blender-cvs