# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# ***** END GPL LICENSE BLOCK *****
#
bl_info = {
"name": "Macros Recorder",
"author": "dairin0d",
"version": (1, 4, 6),
"blender": (2, 6, 0),
"location": "Text Editor -> Text -> Record Macro",
"description": "Record macros to text blocks",
"warning": "",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"\
"Scripts/Development/Macros_Recorder",
"tracker_url": "https://github.com/dairin0d/macros-recorder/issues",
"category": "Development"}
#============================================================================#
import bpy
from mathutils import Vector, Matrix, Quaternion, Euler, Color
from collections import namedtuple
#============================================================================#
"""
TODO:
* analyze info log too (some lines may contain invalid syntax, though)
Parametric modeling:
* see Sverchok? (node-based) http://nikitron.cc.ua/sverch/html/nodes.html
* originally, I had a thought about something similar, though
with heavy emphasis on building the parametric operations list
from the user actions (using a separate scene to build object(s) from actual operators)
Hmm, numpy is already included in Blender since some version
moth3r suggests to take a look at the "pinning values in 3d view" addon
"""
bpy_props = {
bpy.props.BoolProperty,
bpy.props.BoolVectorProperty,
bpy.props.IntProperty,
bpy.props.IntVectorProperty,
bpy.props.FloatProperty,
bpy.props.FloatVectorProperty,
bpy.props.StringProperty,
bpy.props.EnumProperty,
bpy.props.PointerProperty,
bpy.props.CollectionProperty,
}
def is_bpy_prop(value):
if isinstance(value, tuple) and (len(value) == 2):
if (value[0] in bpy_props) and isinstance(value[1], dict):
return True
return False
def iter_public_bpy_props(cls, exclude_hidden=False):
for key in dir(cls):
if key.startswith("_"):
continue
value = getattr(cls, key)
if is_bpy_prop(value):
if exclude_hidden:
options = value[1].get("options", "")
if 'HIDDEN' in options:
continue
yield (key, value)
def types2props(tp, empty_enum_to_string=True):
options = set()
if tp.is_hidden:
options.add('HIDDEN')
if tp.is_skip_save:
options.add('SKIP_SAVE')
if tp.is_animatable:
options.add('ANIMATABLE')
if tp.is_enum_flag:
options.add('ENUM_FLAG')
kwargs = dict(name=tp.name, description=tp.description,
options=options)
if tp.type in ('POINTER', 'COLLECTION'):
kwargs["type"] = tp.fixed_type
pp = bpy.props.CollectionProperty(**kwargs)
elif tp.type == 'STRING':
kwargs["default"] = tp.default
kwargs["maxlen"] = tp.length_max
pp = bpy.props.StringProperty(**kwargs)
elif tp.type == 'ENUM':
defaults = (set(tp.default_flag) if tp.is_enum_flag
else tp.default)
items = [(item.identifier, item.name, item.description)
for item in tp.enum_items]
ids = set(item.identifier for item in tp.enum_items)
if tp.is_enum_flag:
for id in tuple(defaults):
if id not in ids:
defaults.discard(id)
else:
if defaults not in ids:
defaults = (ids[0] if ids else '')
if (not ids) and empty_enum_to_string:
pp = bpy.props.StringProperty(**kwargs)
else:
if ids:
kwargs["default"] = defaults
kwargs["items"] = items
pp = bpy.props.EnumProperty(**kwargs)
else:
is_not_array = (tp.array_length == 0)
subtype_map = {'COORDINATES':'XYZ', 'LAYER_MEMBERSHIP':'LAYER'}
kwargs["subtype"] = subtype_map.get(tp.subtype, tp.subtype)
if is_not_array:
kwargs["default"] = tp.default
else:
kwargs["default"] = tuple(tp.default_array)
kwargs["size"] = tp.array_length
if tp.type != 'BOOLEAN':
kwargs["min"] = tp.hard_min
kwargs["max"] = tp.hard_max
kwargs["soft_min"] = tp.soft_min
kwargs["soft_max"] = tp.soft_max
kwargs["step"] = tp.step
if tp.type == 'FLOAT':
kwargs["precision"] = tp.precision
kwargs["unit"] = tp.unit
if tp.type == 'BOOLEAN':
if is_not_array:
pp = bpy.props.BoolProperty(**kwargs)
else:
pp = bpy.props.BoolVectorProperty(**kwargs)
elif tp.type == 'INT':
if is_not_array:
pp = bpy.props.IntProperty(**kwargs)
else:
pp = bpy.props.IntVectorProperty(**kwargs)
elif tp.type == 'FLOAT':
if is_not_array:
pp = bpy.props.FloatProperty(**kwargs)
else:
pp = bpy.props.FloatVectorProperty(**kwargs)
return pp
def get_instance_type_or_emulator(obj):
if hasattr(obj, "get_instance"): return type(obj.get_instance())
rna_type = obj.get_rna_type()
rna_props = rna_type.properties
# namedtuple fields can't start with underscores, but so do rna props
return namedtuple(rna_type.identifier, rna_props.keys())(*rna_props.values()) # For Blender 2.79.6
def get_rna_type(obj):
if hasattr(obj, "rna_type"): return obj.rna_type
if hasattr(obj, "get_rna"): return obj.get_rna().rna_type
return obj.get_rna_type() # For Blender 2.79.6
def get_op(idname):
category_name, op_name = idname.split(".")
category = getattr(bpy.ops, category_name)
return getattr(category, op_name)
class CurrentGeneratorProperties(bpy.types.PropertyGroup):
pass
def repr_props(obj, limit_to=None):
rna_props = get_rna_type(obj).properties
args = {}
for k, rna in rna_props.items():
if k == "rna_type":
continue
elif (limit_to is not None) and (k not in limit_to):
continue
v = getattr(obj, k)
if rna.type == 'POINTER':
v = repr_props(v)
elif rna.type == 'COLLECTION':
v = [repr_props(item) for item in v]
else:
if type(v).__name__ == "bpy_prop_array":
v = tuple(v)
args[k] = v
return args
def repr_op_call(op):
idname = op.bl_idname.replace("_OT_", ".").lower()
args = repr_props(op)
args = [("%s=%s" % (k, repr(v))) for k, v in args.items()]
return ("bpy.ops.%s(%s)" % (idname, ", ".join(args)))
class StringItem(bpy.types.PropertyGroup):
value = bpy.props.StringProperty()
class SceneMacros(bpy.types.PropertyGroup):
ops = bpy.props.CollectionProperty(type=StringItem)
def clear(self):
while self.ops:
self.ops.remove(0)
def _add(self, op):
if isinstance(op, str):
entry = op
else:
entry = repr_op_call(op)
op_storage = self.ops.add()
op_storage.value = entry
def add(self, op):
self._add(op)
def add_diff(self, diff):
for op in diff:
self._add(op)
def replace_last(self, op):
if not self.ops:
op_storage = self.ops.add()
else:
op_storage = self.ops[len(self.ops) - 1]
op_storage.value = repr_op_call(op)
def write_macro_text(self, textblock):
# NOTE: we can't do a 'live update', because if the user
# undoes past the point of textblock creation, any access
# to texts might crash Blender (at least this happens when
# you try to change operator's arguments after its execution)
textblock.clear()
as_script = bpy.context.window_manager.record_macro_as_script
if as_script:
tabs = ""
code_template = """
import bpy
from mathutils import Vector, Matrix, Quaternion, Euler, Color
context = bpy.context
{2}
""".strip()
else:
tabs = " "
code_template = """
import bpy
from mathutils import Vector, Matrix, Quaternion, Euler, Color
class MacroOperator(bpy.types.Operator):
bl_idname = "macro.{0}"
bl_label = "{1}"
def execute(self, context):
{2}
return {3}
def register():
bpy.utils.register_module(__name__)
def unregister():
bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()
""".strip()
op_name = textblock.name.replace(".", "_")
op_label = bpy.path.display_name(textblock.name.replace(".", " "))
lines = "\n".join((tabs + op.value) for op in self.ops)
code = code_template.format(op_name, op_label, lines, "{'FINISHED'}")
textblock.write(code)
class SceneDiff:
def __init__(self, context):
scene = context.scene
wm = context.window_manager
self.scene_hash = hash(scene)
self.operators_count = len(wm.operators)
self.selected = None
self.active = None
self.cursor = None
self.pivot = None
self.pivot_align = None
self.orientation = None
self.proportional = None
self.proportional_edit = None
self.proportional_falloff = None
def process(self, context):
scene = context.scene
active_obj = context.object
undo_redo = False
scene_hash = hash(scene)
if self.scene_hash != scene_hash:
self.scene_hash = scene_hash
undo_redo = True
is_updated = False
if active_obj:
if 'EDIT' in active_obj.mode:
if active_obj.is_updated or active_obj.is_updated_data:
is_updated = True
data = active_obj.data
if data.is_updated or data.is_updated_data:
is_updated = True
selected = set(obj.name for obj in context.selected_objects)
active = (active_obj.name if active_obj else None)
proportional = scene.tool_settings.use_proportional_edit_objects
proportional_edit = scene.tool_settings.proportional_edit
proportional_falloff = scene.tool_settings.proportional_edit_falloff
cursor = Vector(scene.cursor_location)
v3d = MacroRecorder.v3d
if v3d:
cursor = v3d.cursor_location
pivot = v3d.pivot_point
pivot_align = v3d.use_pivot_point_align
orientation = v3d.transform_orientation
else:
pivot = None
pivot_align = None
orientation = None
if self.selected is None:
self.selected = selected
if self.active is None:
self.active = active
if self.proportional is None:
self.proportional = proportional
if self.proportional_edit is None:
self.proportional_edit = proportional_edit
if self.proportional_falloff is None:
self.proportional_falloff = proportional_falloff
if self.cursor is None:
self.cursor = cursor
if self.pivot is None:
self.pivot = pivot
if self.pivot_align is None:
self.pivot_align = pivot_align
if self.orientation is None:
self.orientation = orientation
wm = context.window_manager
operators_count = len(wm.operators)
if (operators_count != self.operators_count) or undo_redo or is_updated:
n_added = operators_count - self.operators_count
if n_added > 0:
scene.macros.add_diff(wm.operators[-n_added:])
elif undo_redo:
scene.macros.add_diff(wm.operators)
elif is_updated and (n_added == 0) and wm.operators:
scene.macros.replace_last(wm.operators[-1])
self.operators_count = operators_count
else:
selected_diff = selected.difference(self.selected)
unselected_diff = self.selected.difference(selected)
prefix = "context.scene.objects"
for name in unselected_diff:
scene.macros.add("%s[%s].select = False" %
(prefix, repr(name)))
for name in selected_diff:
scene.macros.add("%s[%s].select = True" %
(prefix, repr(name)))
if active != self.active:
scene.macros.add("{0}.active = {0}[{1}]".format(
prefix, repr(name)))
if cursor != self.cursor:
cursor_context = ("space_data" if v3d else "scene")
scene.macros.add("context.%s.cursor_location = %s" %
(cursor_context, repr(cursor)))
if proportional != self.proportional:
scene.macros.add("context.scene.tool_settings."\
"use_proportional_edit_objects = %s" %
repr(proportional))
if proportional_edit != self.proportional_edit:
scene.macros.add("context.scene.tool_settings."\
"proportional_edit = %s" %
repr(proportional_edit))
if proportional_falloff != self.proportional_falloff:
scene.macros.add("context.scene.tool_settings."\
"proportional_edit_falloff = %s" %
repr(proportional_falloff))
if (pivot is not None) and (pivot != self.pivot):
scene.macros.add("context.space_data.pivot_point = %s" %
repr(pivot))
if (pivot_align is not None) and (pivot_align != self.pivot_align):
scene.macros.add("context.space_data."\
"use_pivot_point_align = %s" %
repr(pivot_align))
if (orientation is not None) and (orientation != self.orientation):
scene.macros.add("context.space_data."\
"transform_orientation = %s" %
repr(orientation))
if selected != self.selected:
self.selected = selected
if active != self.active:
self.active = active
if proportional != self.proportional:
self.proportional = proportional
if proportional_edit != self.proportional_edit:
self.proportional_edit = proportional_edit
if proportional_falloff != self.proportional_falloff:
self.proportional_falloff = proportional_falloff
if cursor != self.cursor:
self.cursor = cursor
if pivot != self.pivot:
self.pivot = pivot
if pivot_align != self.pivot_align:
self.pivot_align = pivot_align
if orientation != self.orientation:
self.orientation = orientation
class MacroRecorder(bpy.types.Operator):
"""Record operators to a text block"""
bl_idname = "wm.record_macro"
bl_label = "Toggle macro recording"
v3d = None
@classmethod
def poll(cls, context):
return context.space_data.type in {'TEXT_EDITOR', 'VIEW_3D'}
def invoke(self, context, event):
global is_macro_recording
global macro_window
global macro_recorder
if not is_macro_recording:
macro_recorder = SceneDiff(context)
for scene in bpy.data.scenes:
scene.macros.clear()
is_macro_recording = True
macro_window = context.window
bpy.ops.ed.undo_push(message="Record Macro")
if context.space_data.type == 'VIEW_3D':
MacroRecorder.v3d = context.space_data
else:
MacroRecorder.v3d = None
else:
text_block = bpy.data.texts.new("macro")
context.scene.macros.write_macro_text(text_block)
if context.space_data.type == 'TEXT_EDITOR':
context.space_data.text = text_block
else:
self.report({'INFO'}, "Created %s" % text_block.name)
is_macro_recording = False
macro_window = None
macro_text_block = None
MacroRecorder.v3d = None
bpy.ops.ed.undo_push(message="End Recording")
macro_recorder = None
return {'FINISHED'}
is_macro_recording = False
macro_window = None
macro_recorder = None
def process_diff(scene):
if not is_macro_recording:
return
if bpy.context.window != macro_window:
return
macro_recorder.process(bpy.context)
procgen_attrname = "~current_procedural_generator_properties~"
class RegenerateProceduralObject(bpy.types.Operator):
"""Regenerate procedural object"""
bl_idname = "object.regenerate_procedural_object"
bl_label = "Regenerate procedural object"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
wm = context.window_manager
obj = context.object
if not ((context.mode == 'OBJECT') and obj):
return False
if obj.procedural_generator:
return True
elif wm.operators and ('REGISTER' in wm.operators[-1].bl_options):
return True
return False
def idname_params(self, obj):
i = obj.procedural_generator.index("(")
op_idname = obj.procedural_generator[:i].split(".")[-2:]
op_params = obj.procedural_generator[(i + 1):-1]
return op_idname, op_params
def get_datablocks(self, obj_type):
datablocks = {
'MESH':'meshes', 'CURVE':'curves','SURFACE':'curves',
'META':'metaballs', 'FONT':'fonts', 'ARMATURE':'armatures',
'LATTICE':'lattices', 'EMPTY':None, 'CAMERA':'cameras',
'LAMP':'lamps', 'SPEAKER':'speakers',
}[obj_type]
if datablocks:
datablocks = getattr(bpy.data, datablocks)
return datablocks
def invoke(self, context, event):
forbidden = {"bl_rna", "rna_type", "name"}
cls = CurrentGeneratorProperties
for k in list(cls.__dict__.keys()):
if not (k.startswith("__") or (k in forbidden)):
delattr(cls, k)
obj = context.object
if not obj.procedural_generator:
wm = context.window_manager
op = wm.operators[-1]
obj.procedural_generator = repr_op_call(op)
# If we don't push an undo level, the next time
# execute() will be called, obj.procedural_generator
# would revert to empty string
bpy.ops.ed.undo_push(message="Store procedural parameters")
op_idname, op_params = self.idname_params(obj)
op = get_op(".".join(op_idname))
op_class = get_instance_type_or_emulator(op)
rna_props = get_rna_type(op).properties
for k in dir(op_class):
if not (k.startswith("__") or (k in forbidden)):
setattr(cls, k, getattr(op_class, k))
# Not all operators have their properties declared using bpy.props
# (e.g. most of built-in AddMesh operators)
for k, v in rna_props.items():
if hasattr(cls, k):
continue
if not (k.startswith("__") or (k in forbidden)):
v = types2props(v)
setattr(cls, k, v)
def set_params(**kwargs):
sub_op = getattr(context.window_manager, procgen_attrname)
for k, v, in kwargs.items():
setattr(sub_op, k, v)
eval("set_params(%s)" % op_params)
if not hasattr(cls, "draw"):
def draw(self, context):
sub_op = getattr(context.window_manager, procgen_attrname)
layout = self.layout
cls = type(sub_op)
bpy_props = [kv[0] for kv in iter_public_bpy_props(cls, True)]
for kv in iter_public_bpy_props(cls, True):
sublayout = layout.column()
sublayout.prop(sub_op, kv[0])
setattr(type(self), "draw", draw)
else:
def draw(self, context):
sub_op = getattr(context.window_manager, procgen_attrname)
sub_op.layout = self.layout
try:
sub_op.draw(context)
except Exception:
# E.g. Sapling addon gives this error:
# AttributeError: 'CurrentGeneratorProperties'
# object has no attribute 'properties'
pass
setattr(type(self), "draw", draw)
return self.execute(context)
def execute(self, context):
obj = context.object
op_idname, op_params = self.idname_params(obj)
sub_op = getattr(context.window_manager, procgen_attrname)
args = {}
def set_params(**kwargs):
for k, v, in kwargs.items():
v = getattr(sub_op, k)
if type(v).__name__ == "bpy_prop_array":
v = tuple(v)
args[k] = v
eval("set_params(%s)" % op_params)
args = [("%s=%s" % (k, repr(v))) for k, v in args.items()]
procgen = ("bpy.ops.%s(%s)" % (".".join(op_idname), ", ".join(args)))
obj.procedural_generator = procgen
scene_objects = context.scene.objects
n_objs = len(scene_objects)
selected = list(context.selected_objects)
eval(procgen)
old_data = obj.data
old_data_name = obj.data.name
datablocks = self.get_datablocks(obj.type)
# Most recently added objects have lowest indices
obj.data = scene_objects[0].data
for i in range(len(scene_objects) - n_objs):
tmp_obj = scene_objects[0]
scene_objects.unlink(tmp_obj)
bpy.data.objects.remove(tmp_obj)
if old_data and (old_data.users == 0):
if datablocks:
datablocks.remove(old_data)
if obj.data:
obj.data.name = old_data_name
scene_objects.active = obj
for sel_obj in selected:
sel_obj.select = True
return {'FINISHED'}
# This is necessary to make Blender register the operator
# as an operator that draws something
def draw(self, context):
pass
class VIEW3D_PT_macro(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_label = "Macro"
def draw(self, context):
pass
def draw_header(self, context):
layout = self.layout
icon = ('CANCEL' if is_macro_recording else 'REC')
layout.operator("wm.record_macro", text="", icon=icon)
if context.mode == 'OBJECT':
obj = context.object
if obj and obj.procedural_generator:
icon = 'FILE_REFRESH'
else:
icon = 'FILE_TICK'
layout.operator("object.regenerate_procedural_object",
text="", icon=icon)
def menu_func_draw(self, context):
text = ("Recording... (Stop)" if is_macro_recording else "Record Macro")
icon = ('CANCEL' if is_macro_recording else 'REC')
self.layout.operator("wm.record_macro", text=text, icon=icon)
self.layout.prop(context.window_manager, "record_macro_as_script")
#============================================================================#
def register():
bpy.utils.register_class(StringItem)
bpy.utils.register_class(SceneMacros)
bpy.utils.register_class(MacroRecorder)
bpy.utils.register_class(CurrentGeneratorProperties)
bpy.utils.register_class(RegenerateProceduralObject)
bpy.utils.register_class(VIEW3D_PT_macro)
bpy.types.Scene.macros = bpy.props.PointerProperty(type=SceneMacros)
bpy.types.Object.procedural_generator = bpy.props.StringProperty()
setattr(bpy.types.WindowManager, procgen_attrname,
bpy.props.PointerProperty(type=CurrentGeneratorProperties))
setattr(bpy.types.WindowManager, "record_macro_as_script",
bpy.props.BoolProperty(name="Record Macro as script"))
bpy.types.TEXT_MT_text.append(menu_func_draw)
bpy.app.handlers.scene_update_post.append(process_diff)
def unregister():
bpy.app.handlers.scene_update_post.remove(process_diff)
bpy.types.TEXT_MT_text.remove(menu_func_draw)
delattr(bpy.types.WindowManager, "record_macro_as_script")
delattr(bpy.types.WindowManager, procgen_attrname)
del bpy.types.Object.procedural_generator
del bpy.types.Scene.macros
bpy.utils.unregister_class(VIEW3D_PT_macro)
bpy.utils.unregister_class(RegenerateProceduralObject)
bpy.utils.unregister_class(CurrentGeneratorProperties)
bpy.utils.unregister_class(MacroRecorder)
bpy.utils.unregister_class(SceneMacros)
bpy.utils.unregister_class(StringItem)
if __name__ == "__main__":
register()