В общем представлении гизмо – это такие стрелочки и кружочки на 3D объекте, при помощи которых можно перемещать, вращать и масштабировать меш. Однако в Blender объект “гизмо” более универсален, и его можно использовать для своих нужд в разных рабочих областях. Например, можно создать гизмо для регулировки длины таймлайна.
Автор исходного кода Yann Lanthony (yann-lty).
Для начала нужно создать собственно объект гизмо. Определим для этого класс и наследуем его от bpy.types.Gizmo.
1 |
class GIZMO_GT_RectMouseArea(bpy.types.Gizmo): |
В классе определим функцию setup. Здесь мы задаем внешнюю форму нашего гизмо – прямоугольник, состоящий из двух треугольников. Указываем координаты для каждой точки гизмо так, как будто создаем меш в окне 3D вьюпорта.
1 2 |
def setup(self): self.custom_shape = self.new_custom_shape('TRIS', ((0, 0), (0, 1), (1, 1), (0, 0), (1, 1), (1, 0))) |
Для того чтобы гизмо отрисовывалось по заданным координатам, определим функцию draw.
1 2 |
def draw(self, context): self.draw_custom_shape(self.custom_shape) |
Также нам нужно понимать, когда курсор мышки находится над нашим гизмо-контроллером, чтобы спроектировать взаимодействие с ним пользователя. Для этого определим функцию test_select.
1 2 3 4 5 6 7 8 |
def test_select(self, context, co): left_top_corner = self.matrix_world @ Vector((0, 1, 0, 1)) right_bottom_corner = self.matrix_world @ Vector((1, 0, 0, 1)) # check if co is inside gizmo shape (left_top and right_bottom coordinates) if (left_top_corner[0] <= co[0] <= right_bottom_corner[0]) and (right_bottom_corner[1] <= co[1] <= left_top_corner[1]): return 0 else: return -1 |
Здесь мы сначала определяем верхний левый и нижний правый углы нашего элемента, а затем проверяем, попадают ли текущие координаты мышки, переданные в параметре функции c0, внутрь этой области. Если попадают – возвращаем 0, если нет – -1.
Теперь создадим объект GizmoGroup для взаимодействия с операторами, которые будут управлять нашим элементом контроля.
Определим класс TIMELINE_GGT_Handlers, наследовав его от GizmoGroup.
1 2 3 4 5 |
class TIMELINE_GGT_Handlers(bpy.types.GizmoGroup): bl_label = 'Gizmo Handles' bl_options = {'PERSISTENT', 'SHOW_MODAL_ALL', 'SCALE'} bl_space_type = 'DOPESHEET_EDITOR' bl_region_type = 'WINDOW' |
Определим функцию setup, в которой мы собственно будем создавать гизмо для наших элементов контроля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def setup(self, context): # left gizmo control left_gizmo = self.gizmos.new('GIZMO_GT_RectMouseArea') left_gizmo.color = left_gizmo.color_highlight = 0.0, 1.0, 1.0 left_gizmo.alpha = 0.7 left_gizmo.alpha_highlight = 0.9 left_gizmo.use_draw_modal = True left_gizmo.use_draw_scale = False op = left_gizmo.target_set_operator('scene.range_adjust') setattr(op, 'mode', 'START') self.left_gizmo = left_gizmo # right gizmo control right_gizmo = self.gizmos.new('GIZMO_GT_RectMouseArea') right_gizmo.color = right_gizmo.color_highlight = 0.0, 1.0, 1.0 right_gizmo.alpha = 0.7 right_gizmo.alpha_highlight = 0.9 right_gizmo.use_draw_modal = True right_gizmo.use_draw_scale = False op = right_gizmo.target_set_operator('scene.range_adjust') setattr(op, 'mode', 'END') self.right_gizmo = right_gizmo |
Здесь мы создали два элемента контроля: левый – для начала таймлайна, и правый – для его конца. Для каждого из них мы задаем цвет, прозрачность, подсветку при наведении курсора, возможность отрисовываться модально и трансформироваться вместе с UI Blender.
Также мы назначили на каждый элемент управляющий оператор с idname ‘scene.range_adjust’. Этот оператор будет отвечать собственно за функционирование контроллера.
Для того чтобы различать левый и правый элементы контроля при управлении оператором, мы добавили левому свойство ‘START’, а правому свойство ‘END’.
Теперь определим функцию draw_prepare, которая будет отвечать за отрисовку наших элементов контроля на рабочей области таймлайна.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def draw_prepare(self, context): # get area coordinates of frame_start and frame_end frame_start = context.region.view2d.view_to_region(context.scene.frame_start, 0, clip=False)[0] frame_end = context.region.view2d.view_to_region(context.scene.frame_end, 0, clip=False)[0] # correct size by UI width = 10 * context.preferences.system.ui_scale height = 35 * context.preferences.system.ui_scale # left gizmo self.left_gizmo.matrix_basis[0][3] = frame_start - 0.5 * width self.left_gizmo.matrix_basis[1][3] = 0 self.left_gizmo.matrix_basis[0][0] = width self.left_gizmo.matrix_basis[1][1] = height # right gizmo self.right_gizmo.matrix_basis[0][3] = frame_end - 0.5 * width self.right_gizmo.matrix_basis[1][3] = 0 self.right_gizmo.matrix_basis[0][0] = width self.right_gizmo.matrix_basis[1][1] = height |
Здесь мы определяем фактическое положение границ начала таймлайна и конца таймлайна в экранных координатах. Корректируем размеры наших управляющих элементов так, чтобы они соответствовали общему масштабу интерфейса Blender, а также задаем им общие размеры 10 x 35. И применяем полученные координаты к левому и правому элементам.
Теперь определим оператор, который будет работать в модальном режиме и реализовывать функции управления для наших элементов контроля.
1 2 3 4 |
class SCENE_OT_range_adjust(bpy.types.Operator): bl_idname = 'scene.range_adjust' bl_label = 'Adjust Scene Range' bl_options = {'GRAB_CURSOR_X', 'BLOCKING', 'UNDO'} |
Зададим для оператора два входных параметра: mode и offset. Mode необходим для понимания, какой из контроллеров сейчас обрабатывается, левый или правый, а offset указывает величину текущего смещения контрольного элемента пользователем.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
offset: IntProperty( name='Offset', options={'SKIP_SAVE'}, ) mode: EnumProperty( name='Mode', items=( ('START', 'Start', 'Start frame'), ('END', 'End', 'End frame') ), default='START', options={'SKIP_SAVE'} ) |
Определим стандартные для оператора функции: invoke, execute, modal, cancel.
В функции invoke мы определяем начало работы оператора.
1 2 3 4 5 6 7 8 9 10 11 |
def invoke(self, context, event): self.start_mouse_coords = context.region.view2d.region_to_view( x=event.mouse_region_x, y=event.mouse_region_y ) self.frame_start = context.scene.frame_start self.frame_end = context.scene.frame_end # run as modal context.window.cursor_modal_set('MOVE_X') context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} |
В ней мы фиксируем координаты начального клика мышкой пользователем, запоминаем текущие начальное и конечное значения таймлайна и переводим оператор в модальный режим работы.
В функции modal мы определяем действия оператора при возникновении определенных событий во время работы в модальном режиме.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
def modal(self, context, event): # Cancel if event.type in {'RIGHTMOUSE', 'ESC'}: context.scene.frame_start = self.frame_start context.scene.frame_end = self.frame_end context.window.cursor_modal_restore() return {'CANCELLED'} # Finish if event.type in {'LEFTMOUSE'} and event.value in {'RELEASE'}: context.window.cursor_modal_restore() return {'FINISHED'} # Update if event.type in {'MOUSEMOVE'}: # Recalculate new offset mouse_coords = context.region.view2d.region_to_view( x=event.mouse_region_x, y=event.mouse_region_y ) offset = int(mouse_coords[0] - self.start_mouse_coords[0]) if offset != self.offset: self.offset = offset self.execute(context) return {'RUNNING_MODAL'} |
Здесь мы обрабатываем три события: “отмена” – если пользователь отменяет свое действие, нажатием правой кнопки мышки или клавиши ESC, “завершение” – пользователь кликнул левой кнопкой мышки, и “обновление” – вызывается каждый раз, когда пользователь двигает гизмо-контроллер.
При “отмене” мы возвращаем значения таймлайна в исходное состояние.
При “обновлении” мы обновляем текущее положение курсора и пересчитываем смещение контрольного элемента.
При “завершении” мы запоминаем текущее смещение контрольного элемента от его исходного положения.
Функция execute отрабатывает после возникновения события “завершение”.
1 2 3 4 5 6 |
def execute(self, context): if self.mode == 'START': context.scene.frame_start = min(self.frame_start + self.offset, self.frame_end) else: # 'END' context.scene.frame_end = max(self.frame_end + self.offset, self.frame_start) return {'FINISHED'} |
Здесь мы фиксируем новые значения для таймлайна. Если двигался левый контрольный элемент (‘START’) – для его начала, и если правый (‘END’) – для его конца.
Нам осталось зарегистрировать все определенные нами классы в функции register и выполнить код.
1 2 3 4 |
def register(): bpy.utils.register_class(SCENE_OT_range_adjust) bpy.utils.register_class(GIZMO_GT_RectMouseArea) bpy.utils.register_class(TIMELINE_GGT_Handlers) |
После запуска кода в рабочей области TimeLine появляются два дополнительных прямоугольника. Это и есть наши контрольные гизмо-элементы. Если зажать любой из них и подвигать мышкой, начальная или конечная точка таймлайна будет наглядно изменяться.