Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
B
blender-addons
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Container registry
Model registry
Operate
Environments
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
blender
blender-addons
Commits
1aaf1d7b
Commit
1aaf1d7b
authored
9 years ago
by
Folkert de Vries
Browse files
Options
Downloads
Patches
Plain Diff
Freestyle SVG Exporter: more robust filling
parent
83b911f4
No related branches found
No related tags found
No related merge requests found
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
render_freestyle_svg.py
+170
-77
170 additions, 77 deletions
render_freestyle_svg.py
with
170 additions
and
77 deletions
render_freestyle_svg.py
+
170
−
77
View file @
1aaf1d7b
...
...
@@ -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
():
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment