Source code for linkforge.blender.operators.control_ops

"""Add a joint to the ros2_control system.

This operator allows users to select a joint from the robot's kinematic
tree and include it in the ROS 2 control configuration, setting up
default command and state interfaces.
"""

from __future__ import annotations

import typing

import bpy
from bpy.props import IntProperty, StringProperty

from ..utils.decorators import OperatorReturn, safe_execute
from ..utils.property_helpers import get_joint_props, get_robot_props

if typing.TYPE_CHECKING:
    from bpy.types import Context, Operator

else:
    # Runtime fallback for mock environments where bpy.types might be partially loaded.
    Context = typing.Any
    Operator = getattr(getattr(bpy, "types", object), "Operator", object)


[docs] class LINKFORGE_OT_add_ros2_control_joint(Operator): bl_idname = "linkforge.add_ros2_control_joint" bl_label = "Add Joint" bl_description = "Add a joint from the robot's kinematic tree to the control system" bl_options = {"REGISTER", "UNDO"} joint_name: StringProperty(name="Joint Name") # type: ignore
[docs] @classmethod def poll(cls, context: Context) -> bool: """Check if operators can run. Args: context: The current Blender context. Returns: True if the scene has LinkForge properties initialized. """ return bool(context.scene and get_robot_props(context.scene))
[docs] @safe_execute def execute(self, context: Context) -> OperatorReturn: """Execute the operator. Args: context: The execution context. Returns: Set containing the execution state (e.g., {'FINISHED'}). """ scene = context.scene if not scene or not (props := get_robot_props(scene)): return {"CANCELLED"} # Find the target joint object we intend to add target_joint_obj = next( ( obj for obj in scene.objects if obj.type == "EMPTY" and (jp := get_joint_props(obj)) and jp.is_robot_joint and jp.joint_name == self.joint_name ), None, ) # Check if joint or its specific object already exists in collection for item in props.ros2_control_joints: # Check if name exactly matches OR physical object exactly matches if item.name == self.joint_name or ( item.joint_obj is not None and target_joint_obj is not None and item.joint_obj == target_joint_obj ): self.report( {"WARNING"}, f"Joint '{self.joint_name}' is already in the control system" ) return {"CANCELLED"} # Add to collection item = props.ros2_control_joints.add() item.name = self.joint_name item.joint_obj = target_joint_obj # Set default interfaces item.cmd_position = True item.state_position = True item.state_velocity = True # Set as active index props.ros2_control_active_joint_index = len(props.ros2_control_joints) - 1 self.report({"INFO"}, f"Added joint '{self.joint_name}' to control system") return {"FINISHED"}
[docs] class LINKFORGE_OT_remove_ros2_control_joint(Operator): """Remove a joint from the ros2_control system. This operator removes the currently selected joint from the ROS 2 control configuration list. """ bl_idname = "linkforge.remove_ros2_control_joint" bl_label = "Remove Joint" bl_description = "Remove the selected joint from the control system" bl_options = {"REGISTER", "UNDO"}
[docs] @classmethod def poll(cls, context: Context) -> bool: """Check if operators can run. Args: context: The current Blender context. Returns: True if there are joints in the control system. """ if not context.scene: return False props = get_robot_props(context.scene) return bool(props and len(props.ros2_control_joints) > 0)
[docs] @safe_execute def execute(self, context: Context) -> OperatorReturn: """Execute the operator. Args: context: The execution context. Returns: Set containing the execution state. """ if not context.scene or not (props := get_robot_props(context.scene)): return {"CANCELLED"} index = props.ros2_control_active_joint_index if 0 <= index < len(props.ros2_control_joints): joint_name = props.ros2_control_joints[index].name props.ros2_control_joints.remove(index) # Update active index props.ros2_control_active_joint_index = max(0, index - 1) self.report({"INFO"}, f"Removed joint '{joint_name}' from control system") return {"FINISHED"} return {"CANCELLED"}
[docs] class LINKFORGE_OT_move_ros2_control_joint(Operator): """Move a joint up or down in the control interface list. This is a UI helper operator to reorder how joints appear in the LinkForge control panel. """ bl_idname = "linkforge.move_ros2_control_joint" bl_label = "Move Joint" bl_description = "Move joint up or down in the list (cosmetic only)" bl_options = {"REGISTER", "UNDO"} direction: StringProperty() # type: ignore
[docs] @classmethod def poll(cls, context: Context) -> bool: """Check if operators can run. Args: context: The current Blender context. Returns: True if there are multiple joints to move. """ if not context.scene: return False props = get_robot_props(context.scene) return bool(props and len(props.ros2_control_joints) > 1)
[docs] @safe_execute def execute(self, context: Context) -> OperatorReturn: """Execute the operator. Args: context: The execution context. Returns: Set containing the execution state. """ if not context.scene or not (props := get_robot_props(context.scene)): return {"CANCELLED"} index = props.ros2_control_active_joint_index new_index = index if self.direction == "UP" and index > 0: new_index = index - 1 elif self.direction == "DOWN" and index < len(props.ros2_control_joints) - 1: new_index = index + 1 else: return {"CANCELLED"} props.ros2_control_joints.move(index, new_index) props.ros2_control_active_joint_index = new_index return {"FINISHED"}
[docs] class LINKFORGE_OT_add_ros2_control_parameter(Operator): """Add a parameter to ros2_control (global or joint). This operator adds a new key-value pair to either the global hardware parameters or the parameters of the currently selected joint. """ bl_idname = "linkforge.add_ros2_control_parameter" bl_label = "Add Parameter" bl_description = "Add a key-value parameter to the control system" bl_options = {"REGISTER", "UNDO"} target: StringProperty(default="GLOBAL") # type: ignore
[docs] @classmethod def poll(cls, context: Context) -> bool: """Check if the operator can be executed. Args: context: The current Blender context. Returns: True if LinkForge properties are initialized in the scene. """ return bool(context.scene and get_robot_props(context.scene))
[docs] @safe_execute def execute(self, context: Context) -> OperatorReturn: """Execute the addition of a parameter. Args: context: The execution context. Returns: Set containing the execution state. """ scene = context.scene if not scene or not (props := get_robot_props(scene)): return {"CANCELLED"} if self.target == "GLOBAL": param = props.ros2_control_parameters.add() param.name = "param" param.value = "0.0" else: # Joint-level index = props.ros2_control_active_joint_index if 0 <= index < len(props.ros2_control_joints): joint = props.ros2_control_joints[index] param = joint.parameters.add() param.name = "param" param.value = "0.0" return {"FINISHED"}
[docs] class LINKFORGE_OT_remove_ros2_control_parameter(Operator): """Remove a parameter from ros2_control. This operator deletes a key-value pair from either the global hardware parameters or a specific joint's parameter list. """ bl_idname = "linkforge.remove_ros2_control_parameter" bl_label = "Remove Parameter" bl_description = "Remove a hardware or joint parameter" bl_options = {"REGISTER", "UNDO"} target: StringProperty(default="GLOBAL") # type: ignore index: IntProperty(default=-1) # type: ignore
[docs] @classmethod def poll(cls, context: Context) -> bool: """Check if the operator can be executed. Args: context: The current Blender context. Returns: True if LinkForge properties are initialized. """ return bool(context.scene and get_robot_props(context.scene))
[docs] @safe_execute def execute(self, context: Context) -> OperatorReturn: """Execute the removal of a parameter. Args: context: The execution context. Returns: Set containing the execution state. """ scene = context.scene if not scene or not (props := get_robot_props(scene)): return {"CANCELLED"} if self.target == "GLOBAL": items = props.ros2_control_parameters idx = self.index if 0 <= idx < len(items): items.remove(idx) else: # Joint-level joint_idx = props.ros2_control_active_joint_index if 0 <= joint_idx < len(props.ros2_control_joints): joint = props.ros2_control_joints[joint_idx] items = joint.parameters idx = self.index if self.index >= 0 else len(items) - 1 if 0 <= idx < len(items): items.remove(idx) return {"FINISHED"}
[docs] class LINKFORGE_OT_purge_ros2_control_data(Operator): """Clear all joints and parameters from the ros2_control configuration.""" bl_idname = "linkforge.purge_ros2_control_data" bl_label = "Purge Control Data" bl_description = "Clear all joints and parameters from the control system" bl_options = {"REGISTER", "UNDO"}
[docs] @classmethod def poll(cls, context: Context) -> bool: """Check if operators can run.""" return bool(context.scene and get_robot_props(context.scene))
[docs] @safe_execute def execute(self, context: Context) -> OperatorReturn: """Execute the purge.""" scene = context.scene if not scene or not (props := get_robot_props(scene)): return {"CANCELLED"} props.ros2_control_joints.clear() props.ros2_control_parameters.clear() props.ros2_control_active_joint_index = 0 self.report({"INFO"}, "Purged all ROS 2 control data") return {"FINISHED"}
# Registration classes = [ LINKFORGE_OT_add_ros2_control_joint, LINKFORGE_OT_remove_ros2_control_joint, LINKFORGE_OT_move_ros2_control_joint, LINKFORGE_OT_add_ros2_control_parameter, LINKFORGE_OT_remove_ros2_control_parameter, LINKFORGE_OT_purge_ros2_control_data, ]
[docs] def register() -> None: """Register operators.""" for cls in classes: try: bpy.utils.register_class(cls) except ValueError: bpy.utils.unregister_class(cls) bpy.utils.register_class(cls)
[docs] def unregister() -> None: """Unregister operators.""" for cls in reversed(classes): bpy.utils.unregister_class(cls)
if __name__ == "__main__": register()