import itertools
import bpy
import ddg
[docs]def freestylify_curve(curve):
"""Enable freestyle on edges of a curve (hacky).
This function is a workaround for the problem that freestyle doesn't work on edges
that don't belong to a large enough face. The function works by extruding all edges
of the curve and slightly nudging the newly created vertices. It then enables
freestyle on one edge of each newly created edge pair.
The original *curve* is not altered. Creates a copy with freestyle "enabled" and
links it to the original curve's collection, or the current scene collection if
*curve* is not linked to a collection.
Parameters
----------
curve : bpy.types.Object
Curve or 1D mesh
Returns
-------
bpy.types.Object
Freestylified clone of *curve*. This is always a mesh object.
Notes
-----
Blender will output a bunch of "Warning: degenerated triangle detected, correcting"
warnings. The nudge amount does not seem to influence this.
"""
# Create a copy of the curve and link it to a collection.
# I think users_collection is the tuple of all collections the object is linked to,
# it's not documented particularly well. We choose an arbitrary one. If the object
# is unlinked, we link it to the main scene collection.
if curve.users_collection:
curve_copy_collection = curve.users_collection[0]
else:
curve_copy_collection = bpy.context.scene.collection
curve_copy = ddg.blender.object.copy(curve, collection=curve_copy_collection)
curve_copy.name = curve.name + " (freestylified)"
# Convert curves to meshes so we can count and extrude edges
if curve_copy.type == "CURVE":
with ddg.blender.context.mode(curve_copy):
curve_copy.select_set(True)
bpy.ops.object.convert() # target="MESH" is the default.
# Do this here because this only works after conversion to mesh but before extrusion
original_num_edges = len(curve_copy.data.edges)
# Deselect all objects, in particular the original curve. The context manager does
# not handle this and both objects get edited if we don't do this.
# We are already in the correct context (object mode) after exiting the last
# context.
bpy.ops.object.select_all(action="DESELECT")
with ddg.blender.context.mode(
bobj=curve_copy, mode="EDIT", mesh_select_mode={"EDGE"}, exit_mode="OBJECT"
):
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={"value": [1e-4, 0, 0]})
# This works empirically, but is it guaranteed that it's always the first
# original_num_edges edges that are the original edges of the curve? It would be
# pretty weird if this didn't hold.
# We have to do this at the end because use_freestyle_mark is inherited by extruded
# edges.
for edge in itertools.islice(curve_copy.data.edges, original_num_edges):
edge.use_freestyle_mark = True
return curve_copy
[docs]def mark_all_edges(bobj_or_mesh):
"""Mark all the edges of a Blender object for freestyle.
Parameters
----------
bobj: bpy.types.Object or bpy.types.Mesh
The Blender object of which the edges should be marked.
"""
mesh = ddg.blender.object.get_data(bobj_or_mesh)
for edge in mesh.edges:
edge.use_freestyle_mark = True
return bobj_or_mesh
[docs]def settings(scene=None, view_layer=None):
"""Initialize freestyle settings.
Parameters
----------
scene : bpy.types.Scene (default=None)
The scene to apply freestyle to. If `None`, then `bpy.context.scene`
will be set to scene.
view_layer : bpy.types.ViewLayer (default=None)
The view layer of `scene` to set freestyle to. If `None`, then
`scene.view_layers[0]` will be set as view layer.
Returns
-------
bpy.types.FreestyleSettings
The newly initialized freestyle settings.
"""
if scene is None:
scene = bpy.context.scene
if view_layer is None:
view_layer = scene.view_layers[0]
# set render parameters for freestyle
render = scene.render
render.use_freestyle = True
render.line_thickness_mode = "RELATIVE"
# set view_layer parameters for freestyle
view_layer.use_freestyle = True
fs_settings = view_layer.freestyle_settings
fs_settings.use_view_map_cache = True
fs_settings.mode = "EDITOR"
# Clean existing linesets
linesets = fs_settings.linesets
for key in linesets.keys():
lineset = linesets[key]
linesets.remove(lineset)
return fs_settings
[docs]def lineset(fs_settings, name):
"""Add a freestyle lineset to the settings.
Parameters
----------
fs_settings: bpy.types.FreestyleSettings
The freestyle settings to add the lineset to.
name: str
The name to give to the lineset.
Returns
-------
bpy.types.FreestyleLineSet
"""
return fs_settings.linesets.new(name)
[docs]def linestyle(name):
"""Make a new freestyle linesetyle.
Parameters
----------
name: str
The name to give to the linestyle.
Returns
-------
bpy.types.FreestyleLineStyle
"""
return bpy.data.linestyles.new(name)
[docs]def lineset_to_marked_visible(fs_lineset):
"""Setup a lineset to match marked and visible edges.
Parameters
----------
fs_lineset: bpy.types.FreestyleLineSet
The lineset to setup.
Returns
-------
bpy.types.FreestyleLineSet
"""
fs_lineset.select_by_edge_types = True
fs_lineset.select_by_visibility = True
fs_lineset.edge_type_negation = "INCLUSIVE"
fs_lineset.edge_type_combination = "OR"
fs_lineset.select_edge_mark = True
fs_lineset.visibility = "VISIBLE"
return fs_lineset
[docs]def lineset_to_marked_hidden(fs_lineset):
"""Setup a lineset to match marked and hidden edges.
Parameters
----------
fs_lineset: bpy.types.FreestyleLineSet
The lineset to setup.
Returns
-------
bpy.types.FreestyleLineSet
"""
fs_lineset.select_by_edge_types = True
fs_lineset.select_by_visibility = True
fs_lineset.edge_type_negation = "INCLUSIVE"
fs_lineset.edge_type_combination = "OR"
fs_lineset.select_edge_mark = True
fs_lineset.visibility = "HIDDEN"
return fs_lineset
[docs]def linestyle_to_plain(fs_linestyle, thickness=1.3):
"""Set a linestyle to plain line.
Parameters
----------
fs_linestyle: bpy.types.FreestyleLineStyle
The lineset to setup.
thickness: float
The thickness of the line.
Returns
-------
bpy.types.FreestyleLineStyle
"""
fs_linestyle.thickness = thickness
fs_linestyle.use_dashed_line = False
return fs_linestyle
[docs]def linestyle_to_dashed(fs_linestyle, thickness=1, dash=5, gap=5):
"""Set a linestyle to dashed line.
Parameters
----------
fs_linestyle: bpy.types.FreestyleLineStyle
The lineset to setup.
thickness: float
The thickness of the line.
dash: float
The length of the dashes.
gap: float
The length of the gaps between the dashes.
Returns
-------
bpy.types.FreestyleLineStyle
"""
fs_linestyle.thickness = thickness
fs_linestyle.use_dashed_line = True
fs_linestyle.dash1 = dash
fs_linestyle.gap1 = gap
return fs_linestyle
[docs]def setup_freestyle():
"""Setup default freestyle settings.
Returns
-------
bpy.types.FreestyleSettings
"""
fs_settings = settings()
# marked visible edges appear as plain lines
plain_edges = lineset(fs_settings, "Plain Edges")
lineset_to_marked_visible(plain_edges)
plain_style = linestyle("Plain Style")
linestyle_to_plain(plain_style)
plain_edges.linestyle = plain_style
# marked hidden edges appear as dashed lines
dashed_edges = lineset(fs_settings, "Dashed Edges")
lineset_to_marked_hidden(dashed_edges)
dashed_style = linestyle("Dashed Style")
linestyle_to_dashed(dashed_style)
dashed_edges.linestyle = dashed_style
return fs_settings