diff --git a/core/properties.py b/core/properties.py index a294f2b..074d127 100644 --- a/core/properties.py +++ b/core/properties.py @@ -177,7 +177,19 @@ class AvatarToolkitSceneProperties(PropertyGroup): ('vrc.v_th', 'TH', 'Th as in "think"') ], update=lambda s, c: VisemePreview.update_preview(c) + ) + + eye_tracking_type: EnumProperty( + name=t("EyeTracking.type"), + description=t("EyeTracking.type_desc"), + items=[ + ('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")), + ('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc")) + ], + default='AV3' +) + eye_mode: EnumProperty( name=t("EyeTracking.mode"), items=[ diff --git a/functions/eye_tracking.py b/functions/eye_tracking.py index f8fe3d5..0f1169d 100644 --- a/functions/eye_tracking.py +++ b/functions/eye_tracking.py @@ -29,6 +29,177 @@ VALID_EYE_NAMES = { 'right': ['RightEye', 'Eye_R', 'eye_R', 'eye.R', 'EyeRight', 'right_eye', 'r_eye'] } +class CreateEyesAV3Button(bpy.types.Operator): + """Create eye tracking setup for VRChat Avatar 3.0""" + bl_idname = 'avatar_toolkit.create_eye_tracking_av3' + bl_label = t('EyeTracking.create.av3.label') + bl_description = t('EyeTracking.create.av3.desc') + bl_options = {'REGISTER', 'UNDO'} + + mesh = None + + @classmethod + def poll(cls, context): + toolkit = context.scene.avatar_toolkit + if not toolkit.head or not toolkit.eye_left or not toolkit.eye_right: + return False + return True + + def execute(self, context): + toolkit = context.scene.avatar_toolkit + armature = get_active_armature(context) + + with ProgressTracker(context, 100, "Creating AV3 Eye Tracking") as progress: + try: + context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + progress.step("Setting up bones") + + # Set up bones + head = armature.data.edit_bones.get(toolkit.head) + old_eye_left = armature.data.edit_bones.get(toolkit.eye_left) + old_eye_right = armature.data.edit_bones.get(toolkit.eye_right) + + # Store original names and transformations + left_name = old_eye_left.name + right_name = old_eye_right.name + left_matrix = old_eye_left.matrix.copy() + right_matrix = old_eye_right.matrix.copy() + left_length = old_eye_left.length + right_length = old_eye_right.length + + # Unparent and remove original bones + old_eye_left.parent = None + old_eye_right.parent = None + armature.data.edit_bones.remove(old_eye_left) + armature.data.edit_bones.remove(old_eye_right) + + # Create new eye bones with original names + new_left_eye = armature.data.edit_bones.new(left_name) + new_right_eye = armature.data.edit_bones.new(right_name) + + # Parent them + new_left_eye.parent = head + new_right_eye.parent = head + + # Calculate straight up orientation matrix + straight_up_matrix = mathutils.Matrix.Rotation(math.pi/2, 3, 'X') + + # Apply rotation while preserving position + for eye_data in [(new_left_eye, left_matrix, left_length), + (new_right_eye, right_matrix, right_length)]: + new_eye, orig_matrix, length = eye_data + new_matrix = straight_up_matrix.to_4x4() + new_matrix.translation = orig_matrix.translation + new_eye.matrix = new_matrix + new_eye.length = length + + # Disable mirroring to prevent unwanted behavior + armature.data.use_mirror_x = False + + + progress.step("Finalizing setup") + bpy.ops.object.mode_set(mode='OBJECT') + + self.report({'INFO'}, t('EyeTracking.success')) + return {'FINISHED'} + + except Exception as e: + logger.error(f"Eye tracking setup failed: {str(e)}") + return {'CANCELLED'} + +class CreateEyesSDK2Button(bpy.types.Operator): + """Create eye tracking setup for VRChat SDK2""" + bl_idname = 'avatar_toolkit.create_eye_tracking_sdk2' + bl_label = t('EyeTracking.create.sdk2.label') + bl_description = t('EyeTracking.create.sdk2.desc') + bl_options = {'REGISTER', 'UNDO'} + + mesh = None + + @classmethod + def poll(cls, context): + if not get_all_meshes(context): + return False + + toolkit = context.scene.avatar_toolkit + if not toolkit.head or not toolkit.eye_left or not toolkit.eye_right: + return False + + if toolkit.disable_eye_blinking and toolkit.disable_eye_movement: + return False + + return True + + def execute(self, context): + toolkit = context.scene.avatar_toolkit + armature = get_active_armature(context) + + with ProgressTracker(context, 100, "Creating SDK2 Eye Tracking") as progress: + # Validate setup + validator = EyeTrackingValidator() + is_valid, message = validator.validate_setup(context, toolkit.mesh_name_eye) + if not is_valid: + self.report({'ERROR'}, message) + return {'CANCELLED'} + + try: + context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + progress.step("Setting up bones") + + self.mesh = bpy.data.objects.get(toolkit.mesh_name_eye) + + # Set up bones + head = armature.data.edit_bones.get(toolkit.head) + old_eye_left = armature.data.edit_bones.get(toolkit.eye_left) + old_eye_right = armature.data.edit_bones.get(toolkit.eye_right) + + # Create new eye bones + new_left_eye = armature.data.edit_bones.new('LeftEye') + new_right_eye = armature.data.edit_bones.new('RightEye') + + # Parent them + new_left_eye.parent = head + new_right_eye.parent = head + + # Calculate positions for SDK2 style + fix_eye_position(context, old_eye_left, new_left_eye, head, False) + fix_eye_position(context, old_eye_right, new_right_eye, head, True) + + progress.step("Processing vertex groups") + if not toolkit.disable_eye_movement: + # Switch to object mode for vertex group operations + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + self.mesh.select_set(True) + context.view_layer.objects.active = self.mesh + + copy_vertex_group(self, old_eye_left.name, 'LeftEye') + copy_vertex_group(self, old_eye_right.name, 'RightEye') + + # Return to armature edit mode + context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + progress.step("Processing shape keys") + if not toolkit.disable_eye_blinking: + shapes = [toolkit.wink_left, toolkit.wink_right, + toolkit.lowerlid_left, toolkit.lowerlid_right] + new_shapes = ['vrc.blink_left', 'vrc.blink_right', + 'vrc.lowerlid_left', 'vrc.lowerlid_right'] + + progress.step("Finalizing setup") + bpy.ops.object.mode_set(mode='OBJECT') + toolkit.eye_mode = 'TESTING' + + self.report({'INFO'}, t('EyeTracking.success')) + return {'FINISHED'} + + except Exception as e: + logger.error(f"Eye tracking setup failed: {str(e)}") + return {'CANCELLED'} + class EyeTrackingBackup: def __init__(self): self.backup_path = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json") @@ -126,108 +297,6 @@ class EyeTrackingValidator: return False, t('EyeTracking.validation.missingBones', bones=', '.join(missing_bones)) return True, t('EyeTracking.validation.success') - -class CreateEyesButton(bpy.types.Operator): - bl_idname = 'avatar_toolkit.create_eye_tracking' - bl_label = t('EyeTracking.create.label') - bl_description = t('EyeTracking.create.desc') - bl_options = {'REGISTER', 'UNDO'} - - mesh = None - - @classmethod - def poll(cls, context): - if not get_all_meshes(context): - return False - - toolkit = context.scene.avatar_toolkit - if not toolkit.head or not toolkit.eye_left or not toolkit.eye_right: - return False - - if toolkit.disable_eye_blinking and toolkit.disable_eye_movement: - return False - - return True - - def execute(self, context): - toolkit = context.scene.avatar_toolkit - armature = get_active_armature(context) - - with ProgressTracker(context, 100, "Creating Eye Tracking") as progress: - # Validate setup - validator = EyeTrackingValidator() - is_valid, message = validator.validate_setup(context, toolkit.mesh_name_eye) - if not is_valid: - self.report({'ERROR'}, message) - return {'CANCELLED'} - - # Create backup - backup = EyeTrackingBackup() - if not backup.store_bone_positions(armature): - logger.warning("Failed to create backup") - - try: - # Set active object and mode - context.view_layer.objects.active = armature - bpy.ops.object.mode_set(mode='EDIT') - progress.step("Setting up bones") - - self.mesh = bpy.data.objects.get(toolkit.mesh_name_eye) - - # Set up bones - head = armature.data.edit_bones.get(toolkit.head) - old_eye_left = armature.data.edit_bones.get(toolkit.eye_left) - old_eye_right = armature.data.edit_bones.get(toolkit.eye_right) - - if not toolkit.disable_eye_blinking: - if not all([toolkit.wink_left, toolkit.wink_right, - toolkit.lowerlid_left, toolkit.lowerlid_right]): - self.report({'ERROR'}, t('EyeTracking.error.noShapeSelected')) - return {'CANCELLED'} - - progress.step("Processing vertex groups") - - # Create new eye bones - new_left_eye = armature.data.edit_bones.new('LeftEye') - new_right_eye = armature.data.edit_bones.new('RightEye') - - # Parent them - new_left_eye.parent = head - new_right_eye.parent = head - - # Calculate positions - fix_eye_position(context, old_eye_left, new_left_eye, head, False) - fix_eye_position(context, old_eye_right, new_right_eye, head, True) - - progress.step("Processing shape keys") - - # Process shape keys - if not toolkit.disable_eye_movement: - self.copy_vertex_group(old_eye_left.name, 'LeftEye') - self.copy_vertex_group(old_eye_right.name, 'RightEye') - - # Handle shape keys - shapes = [toolkit.wink_left, toolkit.wink_right, - toolkit.lowerlid_left, toolkit.lowerlid_right] - new_shapes = ['vrc.blink_left', 'vrc.blink_right', - 'vrc.lowerlid_left', 'vrc.lowerlid_right'] - - progress.step("Finalizing setup") - - # Reset modes and cleanup - bpy.ops.object.mode_set(mode='OBJECT') - - # Update scene properties - toolkit.eye_mode = 'TESTING' - - self.report({'INFO'}, t('EyeTracking.success')) - return {'FINISHED'} - - except Exception as e: - logger.error(f"Eye tracking setup failed: {str(e)}") - if backup.restore_bone_positions(get_active_armature(context)): - logger.info("Restored from backup") - return {'CANCELLED'} class StartTestingButton(bpy.types.Operator): bl_idname = 'avatar_toolkit.start_eye_testing' @@ -318,6 +387,8 @@ class StopTestingButton(bpy.types.Operator): eye_left_rot = [] eye_right_rot = [] + bpy.ops.object.mode_set(mode='OBJECT') + return {'FINISHED'} def set_rotation(self, context): @@ -695,6 +766,12 @@ def vertex_group_exists(mesh_obj, group_name): def copy_vertex_group(self, vertex_group, rename_to): """Copy vertex group with new name""" vertex_group_index = 0 + # Select and make mesh active + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + self.mesh.select_set(True) + bpy.context.view_layer.objects.active = self.mesh + for group in self.mesh.vertex_groups: if group.name == vertex_group: self.mesh.vertex_groups.active_index = vertex_group_index @@ -703,6 +780,7 @@ def copy_vertex_group(self, vertex_group, rename_to): break vertex_group_index += 1 + def copy_shape_key(self, context, from_shape, new_names, new_index): """Copy shape key with new name""" blinking = not context.scene.avatar_toolkit.disable_eye_blinking diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 635bc20..ab0cf37 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -303,6 +303,17 @@ "EyeTracking.head_bone_desc": "Select head bone", "EyeTracking.eye_left_desc": "Select left eye bone", "EyeTracking.eye_right_desc": "Select right eye bone", + "EyeTracking.type": "Eye Tracking Type", + "EyeTracking.type_desc": "Select the type of eye tracking setup to create", + "EyeTracking.create.av3.label": "Create AV3 Eye Tracking", + "EyeTracking.create.av3.desc": "Set up eye tracking for VRChat Avatar 3.0", + "EyeTracking.create.sdk2.label": "Create SDK2 Eye Tracking", + "EyeTracking.create.sdk2.desc": "Set up eye tracking for VRChat SDK2", + "EyeTracking.sdk_version": "SDK Version", + "EyeTracking.type.av3": "Avatar 3.0", + "EyeTracking.type.av3_desc": "VRChat Avatar 3.0 eye tracking setup", + "EyeTracking.type.sdk2": "SDK2 (Legacy)", + "EyeTracking.type.sdk2_desc": "VRChat SDK2 eye tracking setup", "Settings.label": "Settings", "Settings.language": "Language", diff --git a/ui/eye_tracking_panel.py b/ui/eye_tracking_panel.py index 4ce4017..8e2c1c7 100644 --- a/ui/eye_tracking_panel.py +++ b/ui/eye_tracking_panel.py @@ -5,7 +5,8 @@ from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.translations import t from ..core.common import get_active_armature, get_all_meshes from ..functions.eye_tracking import ( - CreateEyesButton, + CreateEyesAV3Button, + CreateEyesSDK2Button, StartTestingButton, StopTestingButton, ResetRotationButton, @@ -33,110 +34,153 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): layout = self.layout toolkit = context.scene.avatar_toolkit - # Mode Selection Box - mode_box = layout.box() - col = mode_box.column(align=True) - col.label(text=t("EyeTracking.mode_select"), icon='TOOL_SETTINGS') + # SDK Version Selection Box + sdk_box = layout.box() + col = sdk_box.column(align=True) + col.label(text=t("EyeTracking.sdk_version"), icon='PRESET') col.separator(factor=0.5) - col.prop(toolkit, "eye_mode", expand=True) + row = col.row(align=True) + row.prop(toolkit, "eye_tracking_type", expand=True) - if toolkit.eye_mode == 'CREATION': - # Mesh Setup Box - mesh_box = layout.box() - col = mesh_box.column(align=True) - col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA') + if toolkit.eye_tracking_type == 'SDK2': + # Mode Selection Box + mode_box = layout.box() + col = mode_box.column(align=True) + col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS') col.separator(factor=0.5) - col.prop(toolkit, "mesh_name_eye", text="") + col.prop(toolkit, "eye_mode", expand=True) - # Bone Setup Box - bone_box = layout.box() - col = bone_box.column(align=True) - col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA') - col.separator(factor=0.5) - - armature = get_active_armature(context) - if armature: - col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone")) - col.prop_search(toolkit, "eye_left", armature.data, "bones", text=t("EyeTracking.eye_left")) - col.prop_search(toolkit, "eye_right", armature.data, "bones", text=t("EyeTracking.eye_right")) + if toolkit.eye_mode == 'CREATION': + self.draw_creation_mode(context, layout) else: - col.label(text=t("EyeTracking.no_armature"), icon='ERROR') + self.draw_testing_mode(context, layout) + else: + # AV3 bone setup only + self.draw_av3_setup(context, layout) - # Shapekey Setup Box - shape_box = layout.box() - col = shape_box.column(align=True) - col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA') + def draw_av3_setup(self, context: Context, layout: UILayout) -> None: + toolkit = context.scene.avatar_toolkit + + # Bone Setup Box + bone_box = layout.box() + col = bone_box.column(align=True) + col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA') + col.separator(factor=0.5) + + armature = get_active_armature(context) + if armature: + col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone")) + col.prop_search(toolkit, "eye_left", armature.data, "bones", text=t("EyeTracking.eye_left")) + col.prop_search(toolkit, "eye_right", armature.data, "bones", text=t("EyeTracking.eye_right")) + else: + col.label(text=t("EyeTracking.no_armature"), icon='ERROR') + + # Create Button + row = layout.row(align=True) + row.scale_y = 1.5 + row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY') + + def draw_creation_mode(self, context: Context, layout: UILayout) -> None: + toolkit = context.scene.avatar_toolkit + + # Bone Setup Box + bone_box = layout.box() + col = bone_box.column(align=True) + col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA') + col.separator(factor=0.5) + + armature = get_active_armature(context) + if armature: + col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone")) + col.prop_search(toolkit, "eye_left", armature.data, "bones", text=t("EyeTracking.eye_left")) + col.prop_search(toolkit, "eye_right", armature.data, "bones", text=t("EyeTracking.eye_right")) + else: + col.label(text=t("EyeTracking.no_armature"), icon='ERROR') + + # Mesh Setup Box + mesh_box = layout.box() + col = mesh_box.column(align=True) + col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA') + col.separator(factor=0.5) + col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="") + + # Shape Key Setup Box + shape_box = layout.box() + col = shape_box.column(align=True) + col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA') + col.separator(factor=0.5) + + mesh = bpy.data.objects.get(toolkit.mesh_name_eye) + if mesh and mesh.data.shape_keys: + col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left")) + col.prop_search(toolkit, "wink_right", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_right")) + col.prop_search(toolkit, "lowerlid_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.lowerlid_left")) + col.prop_search(toolkit, "lowerlid_right", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.lowerlid_right")) + else: + col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR') + + # Options Box + options_box = layout.box() + col = options_box.column(align=True) + col.label(text=t("EyeTracking.options"), icon='SETTINGS') + col.separator(factor=0.5) + col.prop(toolkit, "disable_eye_blinking") + col.prop(toolkit, "disable_eye_movement") + if not toolkit.disable_eye_movement: + col.prop(toolkit, "eye_distance") + + # Create Button + row = layout.row(align=True) + row.scale_y = 1.5 + row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY') + + def draw_testing_mode(self, context: Context, layout: UILayout) -> None: + toolkit = context.scene.avatar_toolkit + + if context.mode != 'POSE': + # Testing Start Box + test_box = layout.box() + col = test_box.column(align=True) + col.label(text=t("EyeTracking.testing"), icon='PLAY') col.separator(factor=0.5) - - mesh = bpy.data.objects.get(toolkit.mesh_name_eye) - if mesh and mesh.data.shape_keys: - col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left")) - col.prop_search(toolkit, "wink_right", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_right")) - col.prop_search(toolkit, "lowerlid_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.lowerlid_left")) - col.prop_search(toolkit, "lowerlid_right", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.lowerlid_right")) - else: - col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR') - - # Options Box - options_box = layout.box() - col = options_box.column(align=True) - col.label(text=t("EyeTracking.options"), icon='SETTINGS') + row = col.row(align=True) + row.scale_y = 1.5 + row.operator(StartTestingButton.bl_idname, icon='PLAY') + else: + # Eye Rotation Box + rotation_box = layout.box() + col = rotation_box.column(align=True) + col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE') col.separator(factor=0.5) - col.prop(toolkit, "disable_eye_blinking") - col.prop(toolkit, "disable_eye_movement") - if not toolkit.disable_eye_movement: - col.prop(toolkit, "eye_distance") + col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x")) + col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y")) + col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK') - # Create Button + # Eye Adjustment Box + adjust_box = layout.box() + col = adjust_box.column(align=True) + col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER') + col.separator(factor=0.5) + col.prop(toolkit, "eye_distance") + col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO') + + # Blinking Test Box + blink_box = layout.box() + col = blink_box.column(align=True) + col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF') + col.separator(factor=0.5) + row = col.row(align=True) + row.prop(toolkit, "eye_blink_shape") + row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF') + row = col.row(align=True) + row.prop(toolkit, "eye_lowerlid_shape") + row.operator(TestLowerlid.bl_idname, icon='RESTRICT_VIEW_OFF') + col.operator(ResetBlinkTest.bl_idname, icon='LOOP_BACK') + + # Stop Testing Button row = layout.row(align=True) row.scale_y = 1.5 - row.operator(CreateEyesButton.bl_idname, icon='PLAY') - - else: - if context.mode != 'POSE': - # Testing Start Box - test_box = layout.box() - col = test_box.column(align=True) - col.label(text=t("EyeTracking.testing"), icon='PLAY') - col.separator(factor=0.5) - row = col.row(align=True) - row.scale_y = 1.5 - row.operator(StartTestingButton.bl_idname, icon='PLAY') - else: - # Eye Rotation Box - rotation_box = layout.box() - col = rotation_box.column(align=True) - col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE') - col.separator(factor=0.5) - col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x")) - col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y")) - col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK') - - # Eye Adjustment Box - adjust_box = layout.box() - col = adjust_box.column(align=True) - col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER') - col.separator(factor=0.5) - col.prop(toolkit, "eye_distance") - col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO') - - # Blinking Test Box - blink_box = layout.box() - col = blink_box.column(align=True) - col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF') - col.separator(factor=0.5) - row = col.row(align=True) - row.prop(toolkit, "eye_blink_shape") - row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF') - row = col.row(align=True) - row.prop(toolkit, "eye_lowerlid_shape") - row.operator(TestLowerlid.bl_idname, icon='RESTRICT_VIEW_OFF') - col.operator(ResetBlinkTest.bl_idname, icon='LOOP_BACK') - - # Stop Testing Button - row = layout.row(align=True) - row.scale_y = 1.5 - row.operator(StopTestingButton.bl_idname, icon='PAUSE') + row.operator(StopTestingButton.bl_idname, icon='PAUSE') # Reset Button row = layout.row(align=True)