Files
blender/scripts/modules/_graphviz_export.py
Campbell Barton 1216651ca9 PyAPI: make internal modules explicitly "private"
Rename modules in `./scripts/modules/` to use an underscore prefix to
make it clear they aren't intended to be part of public API's. This
also means there is no implication that these modules should be stable,
allowing us to change them based on Blender's internal usage.

The following modules have been marked as private:

- `animsys_refactor`
- `bl_console_utils`
- `bl_i18n_utils`
- `bl_previews_utils`
- `bl_rna_utils`
- `bl_text_utils`
- `bl_ui_utils`
- `bpy_restrict_state`
- `console_python`
- `console_shell`
- `graphviz_export`
- `keyingsets_utils`
- `rna_info`
- `rna_manual_reference`
- `rna_xml`

Note that we could further re-arrange these modules
(under `_bpy_internal` in some cases), this change is mainly to mark
them as private, further changes can be handed on a case-by-case basis.

Ref !147773
2025-10-13 09:35:09 +00:00

195 lines
6.2 KiB
Python

# SPDX-FileCopyrightText: 2009-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
header = '''
digraph ancestors {
graph [fontsize=30 labelloc="t" label="" splines=false overlap=true, rankdir=BT];
ratio = "auto" ;
'''
footer = '''
}
'''
def compat_str(text, line_length=0):
if line_length:
text_ls = []
while len(text) > line_length:
text_ls.append(text[:line_length])
text = text[line_length:]
if text:
text_ls.append(text)
text = '\n '.join(text_ls)
# text = text.replace(".", ".\n")
# text = text.replace("]", "]\n")
text = text.replace("\n", "\\n")
text = text.replace('"', '\\"')
return text
def graph_armature(obj, filepath, FAKE_PARENT=True, CONSTRAINTS=True, DRIVERS=True, XTRA_INFO=True):
CONSTRAINTS = DRIVERS = True
fileobject = open(filepath, "w")
fw = fileobject.write
fw(header)
fw('label = "{:s}::{:s}" ;'.format(bpy.data.filepath.split("/")[-1].split("\\")[-1], obj.name))
arm = obj.data
bones = [bone.name for bone in arm.bones]
bones.sort()
print("")
for bone in bones:
b = arm.bones[bone]
print(">>", bone, ["*>", "->"][b.use_connect], getattr(getattr(b, "parent", ""), "name", ""))
label = [bone]
bone = arm.bones[bone]
for key, value in obj.pose.bones[bone.name].items():
if key.startswith("_"):
continue
value_type = type(value)
if value_type is float:
value = "{:.3f}".format(value)
elif value_type is str:
value = compat_str(value)
label.append("{:s} = {:s}".format(key, value))
opts = [
"shape=box",
"regular=1",
"style=filled",
"fixedsize=false",
'label="{:s}"'.format(compat_str('\n'.join(label))),
]
if bone.name.startswith('ORG'):
opts.append("fillcolor=yellow")
else:
opts.append("fillcolor=white")
fw('"{:s}" [{:s}];\n'.format(bone.name, ','.join(opts)))
fw('\n\n# Hierarchy:\n')
# Root node.
if FAKE_PARENT:
fw('"Object::{:s}" [];\n'.format(obj.name))
for bone in bones:
bone = arm.bones[bone]
parent = bone.parent
if parent:
parent_name = parent.name
connected = bone.use_connect
elif FAKE_PARENT:
parent_name = "Object::{:s}".format(obj.name)
connected = False
else:
continue
opts = ["dir=forward", "weight=2", "arrowhead=normal"]
if not connected:
opts.append("style=dotted")
fw('"{:s}" -> "{:s}" [{:s}] ;\n'.format(bone.name, parent_name, ','.join(opts)))
del bone
# constraints
if CONSTRAINTS:
fw('\n\n# Constraints:\n')
for bone in bones:
pbone = obj.pose.bones[bone]
# must be ordered
for constraint in pbone.constraints:
subtarget = getattr(constraint, "subtarget", "")
if subtarget:
# TODO, not internal links
opts = [
'dir=forward',
"weight=1",
"arrowhead=normal",
"arrowtail=none",
"constraint=false",
'color="red"',
'labelfontsize=4',
]
if XTRA_INFO:
label = "{:s}\n{:s}".format(constraint.type, constraint.name)
opts.append('label="{:s}"'.format(compat_str(label)))
fw('"{:s}" -> "{:s}" [{:s}] ;\n'.format(pbone.name, subtarget, ','.join(opts)))
# Drivers
if DRIVERS:
fw('\n\n# Drivers:\n')
def rna_path_as_pbone(rna_path):
if not rna_path.startswith("pose.bones["):
return None
# rna_path_bone = rna_path[:rna_path.index("]") + 1]
# return obj.path_resolve(rna_path_bone)
bone_name = rna_path.split("[")[1].split("]")[0]
return obj.pose.bones[bone_name[1:-1]]
animation_data = obj.animation_data
if animation_data:
fcurve_drivers = [fcurve_driver for fcurve_driver in animation_data.drivers]
fcurve_drivers.sort(key=lambda fcurve_driver: fcurve_driver.data_path)
for fcurve_driver in fcurve_drivers:
rna_path = fcurve_driver.data_path
pbone = rna_path_as_pbone(rna_path)
if pbone:
for var in fcurve_driver.driver.variables:
for target in var.targets:
pbone_target = rna_path_as_pbone(target.data_path)
rna_path_target = target.data_path
if pbone_target:
opts = [
'dir=forward',
"weight=1",
"arrowhead=normal",
"arrowtail=none",
"constraint=false",
'color="blue"',
"labelfontsize=4",
]
display_source = rna_path.replace("pose.bones", "")
display_target = rna_path_target.replace("pose.bones", "")
if XTRA_INFO:
label = "{:s}\\n{:s}".format(display_source, display_target)
opts.append('label="{:s}"'.format(compat_str(label)))
fw('"{:s}" -> "{:s}" [{:s}] ;\n'.format(pbone_target.name, pbone.name, ','.join(opts)))
fw(footer)
fileobject.close()
'''
print(".", end="")
import sys
sys.stdout.flush()
'''
print("\nSaved:", filepath)
return True
if __name__ == "__main__":
import os
tmppath = "/tmp/test.dot"
graph_armature(bpy.context.object, tmppath, CONSTRAINTS=True, DRIVERS=True)
os.system("dot -Tpng {:s} > {:s}; eog {:s} &".format(tmppath, tmppath + '.png', tmppath + '.png'))