Source code for robot_designer_plugin.operators.segments

# #####
# This file is part of the RobotDesigner of the Neurorobotics subproject (SP10)
# in the Human Brain Project (HBP).
# It has been forked from the RobotEditor (https://gitlab.com/h2t/roboteditor)
# developed at the Karlsruhe Institute of Technology in the
# High Performance Humanoid Technologies Laboratory (H2T).
# #####

# ##### 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 2
#  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, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# #####
#
# Copyright (c) 2015, Karlsruhe Institute of Technology (KIT)
# Copyright (c) 2016, FZI Forschungszentrum Informatik
#
# Changes:
#
#   2015-01-16: Stefan Ulbrich (FZI), Major refactoring. Integrated into complex plugin framework.
#
# ######

"""
Sphinx-autodoc tag
"""

# System imports
from math import degrees, radians

# Blender imports
import bpy
from bpy.props import StringProperty, BoolProperty

# RobotDesigner imports
from .rigid_bodies import *
from .helpers import (
    _mat3_to_vec_roll,
    ModelSelected,
    SingleSegmentSelected,
    PoseMode,
    AtLeastOneSegmentSelected,
    NotEditMode,
)


[docs]@RDOperator.Preconditions(ModelSelected) @PluginManager.register_class class SelectSegment(RDOperator): """ :term:`Operator<operator>` for selecting a segment. If :attr:`segment_name` is empty, all segments will be deselected """ bl_idname = config.OPERATOR_PREFIX + "select_segment" bl_label = "Select Segment" segment_name: StringProperty() @RDOperator.OperatorLogger def execute(self, context): if not ( context.active_object.type == "ARMATURE" ): # or context.active_object.type == 'MESH' raise Exception("BoneSelectionException") model = bpy.context.active_object for b in model.data.bones: b.select = False # Alternative to do this: # mode = context.mode # bpy.ops.object.mode_set(mode='EDIT') # bpy.ops.armature.select_all(action='DESELECT') # bpy.ops.object.mode_set(mode=mode) # Second alternative: # mode = context.mode # bpy.ops.object.mode_set(mode='EDIT') # for b in context.selected_bones: # b.select = False # bpy.ops.object.mode_set(mode=mode) if self.segment_name: model.data.bones.active = model.data.bones[self.segment_name] model.data.bones.active.select = True else: model.data.bones.active = None return {"FINISHED"}
[docs] @classmethod def run(cls, segment_name=""): return super().run(**cls.pass_keywords())
[docs]@RDOperator.Preconditions(ModelSelected, SingleSegmentSelected) @PluginManager.register_class class RenameSegment(RDOperator): """ :term:`operator` for renaming an active bone """ bl_idname = config.OPERATOR_PREFIX + "rename_segment" bl_label = "Rename Active Segment" new_name: StringProperty(name="Enter new name:") @RDOperator.OperatorLogger def execute(self, context): old_name = context.active_bone.name context.active_bone.name = self.new_name if context.active_bone.RobotDesigner.joint_name == old_name + "_joint": context.active_bone.RobotDesigner.joint_name = self.new_name + "_joint" return {"FINISHED"}
[docs] def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)
[docs] @classmethod def run(cls, new_name=""): return super().run(**cls.pass_keywords())
[docs]@RDOperator.Preconditions(ModelSelected, SingleSegmentSelected) @PluginManager.register_class class InsertNewParentSegment(RDOperator): """ :term:`operator` for create new parent segment for the currently selected segment. """ bl_idname = config.OPERATOR_PREFIX + "createparentbone" bl_label = "Create New Parent Bone" segment_name: StringProperty(name="Enter new parent bone name:")
[docs] @classmethod def run(cls, segment_name): return super().run(**cls.pass_keywords())
@RDOperator.OperatorLogger def execute(self, context): current_segment_name = context.active_bone.name parent_segment_name = ( context.active_bone.parent.name if context.active_bone.parent else "" ) self.logger.info("{} {}".format(current_segment_name, parent_segment_name)) bpy.ops.pose.select_all( action="DESELECT" ) # todo make an operator that switches context CreateNewSegment.run(segment_name=self.segment_name) new_segment_name = context.active_bone.name if parent_segment_name: AssignParentSegment.run(parent_name=parent_segment_name) SelectSegment.run(segment_name=current_segment_name) AssignParentSegment.run(parent_name=new_segment_name) # rearrange parent pointers accordingly in edit mode # current_mode = context.object.mode # bpy.ops.object.mode_set(mode='EDIT', toggle=False) # # new_editbone = context.active_bone # if context.active_bone.parent: # parent_name = context.active_bone.parent.name # parent_editbone = context.active_object.data.edit_bones[parent_name] # else: # parent_editbone = None # # new_editbone.parent = parent_editbone # # current_editbone = context.active_object.data.edit_bones[current_segment_name] # current_editbone.parent = new_editbone # # bpy.ops.object.mode_set(mode=current_mode, toggle=False) UpdateSegments.run(segment_name=self.segment_name, recurse=True) return {"FINISHED"}
[docs] def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)
[docs]@PluginManager.register_class class AssignParentSegment(RDOperator): """ :term:`operator` for assigning a parent to a segment. """ bl_idname = config.OPERATOR_PREFIX + "assignparentbone" bl_label = "Assign Parent Bone" parent_name: StringProperty()
[docs] @classmethod def run(cls, parent_name): return super().run(**cls.pass_keywords())
@RDOperator.OperatorLogger def execute(self, context): # arm = context.active_object current_segment_name = context.active_bone.name current_mode = bpy.context.object.mode bpy.ops.object.mode_set(mode="EDIT", toggle=False) new_parent_editbone = context.active_object.data.edit_bones[self.parent_name] current_editbone = context.active_object.data.edit_bones[current_segment_name] current_editbone.parent = new_parent_editbone bpy.ops.object.mode_set(mode=current_mode, toggle=False) UpdateSegments.run(segment_name=current_segment_name) return {"FINISHED"}
[docs]@RDOperator.Preconditions(ModelSelected, AtLeastOneSegmentSelected, NotEditMode) @PluginManager.register_class class ImportBlenderArmature(RDOperator): """ Set :term:`pose` properties and mark all selected bones as known to the robot designer. """ bl_idname = config.OPERATOR_PREFIX + "importnative" bl_label = "(Re)Import Bones"
[docs] def execute_on_bone(self, bone): self.logger.info("Importing bone {}".format(bone.name)) # Make the UpdateSegments operator consider this bone as if it has not been taken control of yet. # Otherwise UpdateSegments would mess up the bones transform as soon as the first RD related property is changed. # Example: # - have this bone down a bone hierarchy. It happens to be managed by RD already. # - Further down we do bpy.context.active_bone.RobotDesigner.Euler.x.value = xyz[0] # - Triggers UpdateSegments # - y, z, etc still filled with garbage. # - UpdateSegments uses garbage to compute and assign new bone transform. bone.RobotDesigner.RD_Bone = False parent = bone.parent if parent is not None: m = parent.matrix_local.inverted() @ bone.matrix_local else: m = bone.matrix_local euler = m.to_euler() xyz = m.translation bone.RobotDesigner.Euler.x.value = xyz[0] bone.RobotDesigner.Euler.y.value = xyz[1] bone.RobotDesigner.Euler.z.value = xyz[2] bone.RobotDesigner.Euler.alpha.value = round(degrees(euler[0]), 0) bone.RobotDesigner.Euler.beta.value = round(degrees(euler[1]), 0) bone.RobotDesigner.Euler.gamma.value = round(degrees(euler[2]), 0) bone.RobotDesigner.RD_Bone = True
@RDOperator.OperatorLogger def execute(self, context): armature = bpy.context.active_object # Done via names because I get crashes if I keep references. Perhaps due to angling pointers inside kept references? selected_bone_names = [str(b.name) for b in armature.data.bones] for bname in selected_bone_names: SelectSegment.run(bname) # required by property update callback. self.execute_on_bone(armature.data.bones[bname]) return {"FINISHED"}
[docs]@RDOperator.Preconditions(ModelSelected, AtLeastOneSegmentSelected, NotEditMode) @PluginManager.register_class class ConvertVertexMapSkinning(RDOperator): """ :term:`operator` Operator for converting vertex weight based skinning to use of the bone parent property. Bone parent will be assigned to the bone with the largest total weight. Vertex weighting will be disabled on the mesh. """ bl_idname = config.OPERATOR_PREFIX + "convert_vertexmap_skinning" bl_label = "Assign Selected Bones Via Vertex Maps"
[docs] def allow_connect_to_that_bone_because_vertex_weight(self, bone, obj): """ There can only be one parent_bone. So in case of multiple VG's we have to decide which one to take.""" total_weights = [] for vg in obj.vertex_groups: total_weight = 0.0 for i in range(len(obj.data.vertices)): try: total_weight += vg.weight(i) except RuntimeError: pass total_weights.append((vg.name, total_weight)) bone_with_largest_weight, _ = max(total_weights, key=lambda x: x[1]) return bone_with_largest_weight == bone.name
[docs] def allow_connect_to_that_bone(self, bone, obj): return obj.parent_bone == bone.name or ( bone.name in obj.vertex_groups and self.allow_connect_to_that_bone_because_vertex_weight(bone, obj) )
[docs] def stop_vertex_group_from_interfering(self, armature, obj, context): try: obj.modifiers[armature.name].use_vertex_groups = False except KeyError: # This is the normal case actually, i.e. the object has no vertex weighting w.r.t. that bone. pass
[docs] def execute_on_bone(self, bone, armature, context): meshes_to_connect = [ ch for ch in armature.children if self.allow_connect_to_that_bone(bone, ch) ] for obj in meshes_to_connect: self.logger.debug( "Attempt to attach geometry {} to {}".format(obj.name, bone.name) ) self.stop_vertex_group_from_interfering(armature, obj, context) # We just use the operators that we already have. # Assign geometry operates on selected items - one bone and one mesh. SelectGeometry.run(geometry_name=obj.name)
# AssignGeometry.run( # attach_collision_geometry=(global_properties.mesh_type == "COLLISION") # ) @RDOperator.OperatorLogger def execute(self, context): armature = bpy.context.active_object bone_names = [str(b.name) for b in armature.data.bones if b.select] for bname in bone_names: SelectSegment.run(bname) # required by property update callback. self.execute_on_bone(armature.data.bones[bname], armature, context) return {"FINISHED"}
[docs]@RDOperator.Preconditions(ModelSelected, SingleSegmentSelected) @PluginManager.register_class class DeleteSegment(RDOperator): """ :term:`operator` for deleting a the selected segment *ALL* of its children. """ bl_idname = config.OPERATOR_PREFIX + "deletebone" bl_label = "Delete Segment And ALL Its Children" confirmation: BoolProperty(name="Are you sure?") @RDOperator.OperatorLogger def execute(self, context): if self.confirmation: current_mode = context.object.mode bpy.ops.object.mode_set(mode="EDIT", toggle=False) for bone in context.active_object.data.edit_bones: bone.select = False for bone in context.active_bone.children_recursive: bone.select = True context.active_bone.select = True if context.active_bone.parent is not None: parent_name = context.active_bone.parent.name else: parent_name = None bpy.ops.armature.delete() bpy.ops.object.mode_set(mode=current_mode, toggle=False) if parent_name is not None: SelectSegment.run(segment_name=parent_name) return {"FINISHED"}
[docs] def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)
[docs]@RDOperator.Preconditions(ModelSelected, SingleSegmentSelected) @PluginManager.register_class class SetDefaultJointName(RDOperator): """ :term:`operator` for setting joint name to a default name """ bl_idname = config.OPERATOR_PREFIX + "default_joint_name" bl_label = "Set Joint Name to Default Name" @RDOperator.OperatorLogger def execute(self, context): context.active_bone.RobotDesigner.joint_name = ( context.active_bone.name + "_joint" ) return {"FINISHED"}
[docs]@RDOperator.Preconditions(ModelSelected) @PluginManager.register_class class SetDefaultJointNameAll(RDOperator): """ :term:`operator` for setting joint name to a default name for all segments """ bl_idname = config.OPERATOR_PREFIX + "default_joint_name_all" bl_label = "Set Joint Name to Default Name for All Segments" @RDOperator.OperatorLogger def execute(self, context): if not ( context.active_object.type == "ARMATURE" ): # or context.active_object.type == 'MESH' raise Exception("BoneSelectionException") model = bpy.context.active_object for b in model.data.bones: b.RobotDesigner.joint_name = b.name + "_joint" return {"FINISHED"}
[docs]@RDOperator.Preconditions(ModelSelected) @PluginManager.register_class class CreateNewSegment(RDOperator): """ :term:`Operator <operator>` for creating new robot segments and add it to the current model. If a segment is already selected, the new segment is added as a child segment. A call :class:`SelectSegment` before might be necessary. """ bl_idname = config.OPERATOR_PREFIX + "create_segment" bl_label = "Create New Segment" # model_name = StringProperty() segment_name: StringProperty(name="Enter new segment name:") # parent_name = StringProperty(default="")
[docs] @classmethod def run(cls, segment_name): # , parent_name=""): return super().run(**cls.pass_keywords())
@RDOperator.OperatorLogger @RDOperator.Postconditions(ModelSelected, SingleSegmentSelected) def execute(self, context): current_mode = bpy.context.object.mode selected_segments = [i for i in context.active_object.data.bones if i.select] if len(selected_segments): parent_name = selected_segments[0].name else: parent_name = "" bpy.ops.object.mode_set(mode="EDIT", toggle=False) bone = context.active_object.data.edit_bones.new(self.segment_name) segment_name = self.segment_name bone.head = (0, 0, 0) # Dummy bone.tail = (0, 0, 1) # Dummy bone.lock = True if parent_name: self.logger.debug(parent_name) bone.parent = context.active_object.data.edit_bones[parent_name] bpy.ops.object.mode_set(mode="POSE", toggle=False) SelectSegment.run(segment_name=segment_name) context.active_bone.RobotDesigner.RD_Bone = True # default parent joint name of link context.active_bone.RobotDesigner.joint_name = segment_name + "_joint" if not parent_name: context.active_bone.RobotDesigner.Euler.alpha.value = 90.0 context.active_bone.RobotDesigner.DH.alpha.value = 90.0 bpy.ops.pose.constraint_add(type="LIMIT_ROTATION") bpy.context.object.pose.bones[segment_name].constraints[ 0 ].name = "RobotDesignerConstraint" bpy.ops.object.mode_set(mode=current_mode, toggle=False) self.logger.info( "Current mode after: {} ({})".format(bpy.context.object.mode, current_mode) ) self.logger.debug("Segment created. ({} -> .format()".format(parent_name, self.segment_name)) UpdateSegments.run(recurse=True, segment_name=self.segment_name) return {"FINISHED"}
[docs] def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)
[docs]@PluginManager.register_class class UpdateSegments(RDOperator): """ :term:`operator` for updating the :term:`robot models` after parameters changed. If a :term:`segment` name is given it will proceed recursively. """ bl_idname = config.OPERATOR_PREFIX + "udpate_model" bl_label = "Update Model" # model_name = StringProperty() segment_name: StringProperty(default="") recurse: BoolProperty(default=True) @RDOperator.Postconditions(ModelSelected) @RDOperator.OperatorLogger # @RDOperator.Postconditions(ModelSelected) # @Preconditions(ModelSelected) def execute(self, context): current_mode = bpy.context.object.mode self.logger.debug( "UpdateSegments: recurse={}, bone={}".format( str(self.recurse), str(self.segment_name)) ) armature_data_name = context.active_object.data.name if self.segment_name: segment_name = ( bpy.data.armatures[armature_data_name].bones[self.segment_name].name ) # Isn't this the identity operation?? else: segment_name = bpy.data.armatures[armature_data_name].bones[0].name SelectSegment.run(segment_name=self.segment_name) if ( not bpy.data.armatures[armature_data_name] .bones[segment_name] .RobotDesigner.RD_Bone ): self.logger.info("Not updated (not a RD segment): {}".format(segment_name)) return {"FINISHED"} bone = bpy.data.armatures[armature_data_name].bones[segment_name] # Transforms as per RD spec. matrix, joint_matrix = bone.RobotDesigner.getTransform() bpy.ops.object.mode_set(mode="EDIT", toggle=False) editbone = bpy.data.armatures[armature_data_name].edit_bones[ bpy.data.armatures[armature_data_name].bones[segment_name].name ] editbone.use_inherit_rotation = True # Express desired matrix in frame of the Armature if editbone.parent is not None: transform = editbone.parent.matrix.copy() matrix = transform @ matrix # Adjust bone properties to match RD transform specs. # Try to move it around rigidly. Keep length. pos = matrix.to_translation() axis, roll = _mat3_to_vec_roll(matrix.to_3x3()) length = editbone.length editbone.head = pos # Changes length. editbone.tail = pos + length * axis editbone.roll = roll bpy.ops.object.mode_set(mode=current_mode, toggle=False) # update pose bpy.ops.object.mode_set(mode="POSE", toggle=False) pose_bone = bpy.context.object.pose.bones[segment_name] pose_bone.matrix_basis = joint_matrix # Local variables for updating the constraints # These refer to the settings pertaining to the RD. joint_axis = ( bpy.data.armatures[armature_data_name] .bones[segment_name] .RobotDesigner.axis ) min_rot = ( bpy.data.armatures[armature_data_name] .bones[segment_name] .RobotDesigner.theta.min ) max_rot = ( bpy.data.armatures[armature_data_name] .bones[segment_name] .RobotDesigner.theta.max ) jointMode = ( bpy.data.armatures[armature_data_name] .bones[segment_name] .RobotDesigner.jointMode ) jointValue = ( bpy.data.armatures[armature_data_name] .bones[segment_name] .RobotDesigner.theta.value ) if jointMode == "REVOLUTE": if "RobotDesignerConstraint" not in pose_bone.constraints: bpy.ops.pose.constraint_add(type="LIMIT_ROTATION") bpy.context.object.pose.bones[segment_name].constraints[ 0 ].name = "RobotDesignerConstraint" constraint = [ i for i in pose_bone.constraints if i.type == "LIMIT_ROTATION" ][0] constraint.name = "RobotDesignerConstraint" constraint.owner_space = "LOCAL" constraint.use_limit_x = True constraint.use_limit_y = True constraint.use_limit_z = True constraint.min_x = 0.0 constraint.min_y = 0.0 constraint.min_z = 0.0 constraint.max_x = 0.0 constraint.max_y = 0.0 constraint.max_z = 0.0 if joint_axis == "X": constraint.min_x = radians(min_rot) constraint.max_x = radians(max_rot) elif joint_axis == "Y": constraint.min_y = radians(min_rot) constraint.max_y = radians(max_rot) elif joint_axis == "Z": constraint.min_z = radians(min_rot) constraint.max_z = radians(max_rot) elif "RobotDesignerConstraint" in pose_bone.constraints: pose_bone.constraints.remove( pose_bone.constraints["RobotDesignerConstraint"] ) # ------------------------------------------------------- bpy.ops.object.mode_set(mode=current_mode, toggle=False) children_names = [ i.name for i in bpy.data.armatures[armature_data_name].bones[segment_name].children ] for child_name in children_names: UpdateSegments.run(segment_name=child_name, recurse=self.recurse) SelectSegment.run(segment_name=segment_name) return {"FINISHED"}