├── .gitignore ├── LICENSE.txt ├── README.rst ├── __init__.py ├── make_readme.py └── operators ├── __init__.py ├── add_transform ├── __init__.py └── add_transform.py ├── adjust_alpha ├── __init__.py ├── adjust_alpha.py └── draw_alpha_controls.py ├── autocrop ├── __init__.py └── autocrop.py ├── call_menu ├── __init__.py ├── call_menu.py ├── insert_keyframe.py └── menu_insert_keyframe.py ├── crop ├── __init__.py ├── crop.py ├── crop_scale.py ├── draw_crop.py ├── get_perpendicular_point.py ├── set_corners.py └── set_quads.py ├── delete ├── __init__.py └── delete.py ├── duplicate ├── __init__.py ├── duplicate.py └── get_vertical_translation.py ├── grab ├── __init__.py └── grab.py ├── group ├── __init__.py └── group.py ├── meta_toggle ├── __init__.py └── meta_toggle.py ├── mouse_track ├── __init__.py └── mouse_track.py ├── pixelate ├── __init__.py ├── draw_pixelate_controls.py └── pixelate.py ├── rotate ├── __init__.py ├── apply_strip_rotation.py └── rotate.py ├── scale ├── __init__.py └── scale.py ├── select ├── __init__.py └── select.py ├── set_cursor2d ├── __init__.py ├── get_important_edge_points.py └── set_cursor2d.py ├── track_transform ├── __init__.py └── track_transform.py └── utils ├── __init__.py ├── draw ├── __init__.py ├── colors.py ├── draw_arrows.py ├── draw_axes.py ├── draw_line.py ├── draw_px_point.py ├── draw_snap.py ├── draw_square.py └── draw_stippled_line.py ├── func_constrain_axis.py ├── func_constrain_axis_mmb.py ├── geometry ├── __init__.py ├── get_group_box.py ├── get_pos_x.py ├── get_pos_y.py ├── get_post_rot_bbox.py ├── get_preview_offset.py ├── get_res_factor.py ├── get_strip_box.py ├── get_strip_corners.py ├── get_transform_box.py ├── mouse_to_res.py ├── reposition_strip.py ├── reposition_transform_strip.py ├── rotate_point.py ├── set_pos_x.py └── set_pos_y.py ├── process_input.py └── selection ├── __init__.py ├── ensure_transforms.py ├── get_highest_transform.py ├── get_input_tree.py ├── get_nontransforms.py ├── get_transforms.py └── get_visible_strips.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | _addon_linker.py 7 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "VSE Transform Tools", 3 | "description": "Quickly manipulate video strips in Blender's Video Sequence Editor", 4 | "author": "kgeogeo, DoubleZ, doakey3, NathanLovato", 5 | "version": (1, 2, 8), 6 | "blender": (2, 80, 0), 7 | "wiki_url": "https://github.com/doakey3/VSE_Transform_Tools", 8 | "tracker_url": "https://github.com/doakey3/VSE_Transform_Tools/issues", 9 | "category": "Sequencer" 10 | } 11 | 12 | """ 13 | RegEx Classname 14 | =============== 15 | [A-Z][A-Z0-9_]*_{ABBREV}_[A-Za-z0-9_]+ 16 | 17 | Abbrev 18 | ------ 19 | Header: _HT_ 20 | Menu: _MT_ 21 | Operator: _OT_ 22 | Panel: _PT_ 23 | UIList: _UL_ 24 | """ 25 | 26 | import bpy 27 | from bpy.types import WorkSpaceTool 28 | 29 | from .operators import * 30 | 31 | handle_2d_cursor = None 32 | 33 | from .operators.utils.draw import draw_line 34 | 35 | 36 | def draw_callback_px_2d_cursor(self, context): 37 | c2d = context.region.view2d.view_to_region( 38 | context.scene.seq_cursor2d_loc[0], 39 | context.scene.seq_cursor2d_loc[1], clip=False) 40 | 41 | v1 = [c2d[0] - 5, c2d[1]] 42 | v2 = [c2d[0] + 5, c2d[1]] 43 | 44 | draw_line(v1, v2, 1, (1, 0, 0, 1)) 45 | 46 | v1 = [c2d[0], c2d[1] - 5] 47 | v2 = [c2d[0], c2d[1] + 5] 48 | 49 | draw_line(v1, v2, 1, (1, 0, 0, 1)) 50 | 51 | 52 | def Add_Icon_Pivot_Point(self, context): 53 | layout = self.layout 54 | layout.prop( 55 | context.scene, "seq_pivot_type", text='', 56 | expand=False, icon_only=True 57 | ) 58 | 59 | 60 | def Add_Menu(self, context): 61 | layout = self.layout 62 | st = context.space_data 63 | 64 | if st.view_type in {'PREVIEW', 'SEQUENCER_PREVIEW'}: 65 | layout.menu("SEQUENCER_MT_transform_tools_menu") 66 | 67 | 68 | def update_seq_cursor2d_loc(self, context): 69 | context.area.tag_redraw() 70 | 71 | 72 | def update_pivot_point(self, context): 73 | bpy.ops.vse_transform_tools.initialize_pivot() 74 | 75 | 76 | class PREV_OT_initialize_pivot(bpy.types.Operator): 77 | """ 78 | The pivot icon won't show up if blender opens already on pivot type 79 | 2. This operator should be called whenever an action occurs on a 80 | strip. 81 | 82 | This has to be in the init file because of the global variable 83 | "handle_2d_cursor" 84 | """ 85 | bl_idname = "vse_transform_tools.initialize_pivot" 86 | bl_label = "Make the pivot point appear if pivot styles is currently 2D cursor" 87 | 88 | @classmethod 89 | def poll(cls, context): 90 | ret = False 91 | if (context.scene.sequence_editor and 92 | context.space_data.type == 'SEQUENCE_EDITOR'): 93 | return True 94 | return False 95 | 96 | def execute(self, context): 97 | global handle_2d_cursor 98 | scene = context.scene 99 | args = (self, context) 100 | 101 | if scene.seq_pivot_type == '2' and not handle_2d_cursor: 102 | handle_2d_cursor = bpy.types.SpaceSequenceEditor.draw_handler_add( 103 | draw_callback_px_2d_cursor, args, 'PREVIEW', 'POST_PIXEL') 104 | elif not scene.seq_pivot_type == '2' and handle_2d_cursor: 105 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 106 | handle_2d_cursor, 'PREVIEW') 107 | handle_2d_cursor = None 108 | 109 | return {'FINISHED'} 110 | 111 | 112 | class SEQUENCER_MT_transform_tools_menu(bpy.types.Menu): 113 | bl_label = "Transform" 114 | bl_idname = "SEQUENCER_MT_transform_tools_menu" 115 | 116 | @classmethod 117 | def poll(cls, context): 118 | st = context.space_data 119 | return st.view_type in {'PREVIEW', 'SEQUENCER_PREVIEW'} 120 | 121 | def draw(self, context): 122 | layout = self.layout 123 | layout.operator_context = 'INVOKE_REGION_PREVIEW' 124 | #st = context.space_data 125 | 126 | #if st.view_type in {'PREVIEW', 'SEQUENCER_PREVIEW'}: 127 | 128 | layout.operator("vse_transform_tools.add_transform") 129 | 130 | layout.separator() 131 | 132 | layout.operator("vse_transform_tools.grab") 133 | # layout.operator("vse_transform_tools.grab", 'G', 'PRESS', alt=True, shift=False) 134 | layout.operator("vse_transform_tools.scale") 135 | # layout.operator("vse_transform_tools.scale", 'S', 'PRESS', alt=True) 136 | layout.operator("vse_transform_tools.rotate") 137 | # layout.operator("vse_transform_tools.rotate", 'R', 'PRESS', alt=True) 138 | 139 | layout.separator() 140 | 141 | layout.operator("vse_transform_tools.crop") 142 | layout.operator("vse_transform_tools.autocrop") 143 | # layout.operator("vse_transform_tools.crop", 'C', 'PRESS', alt=True) 144 | 145 | layout.separator() 146 | 147 | layout.operator("vse_transform_tools.delete") 148 | # layout.operator("vse_transform_tools.delete", "DEL", "PRESS", shift=True) 149 | layout.operator("vse_transform_tools.duplicate") 150 | 151 | layout.separator() 152 | 153 | layout.operator("vse_transform_tools.call_menu", text="Insert Keyframe") 154 | layout.operator("vse_transform_tools.mouse_track") 155 | 156 | layout.separator() 157 | 158 | layout.operator("vse_transform_tools.adjust_alpha") 159 | # layout.operator("vse_transform_tools.adjust_alpha", 'Q', 'PRESS', alt=True) 160 | layout.operator("vse_transform_tools.pixelate") 161 | 162 | layout.separator() 163 | 164 | layout.operator("vse_transform_tools.group", text="Make Meta Strip") 165 | # layout.operator("vse_transform_tools.group", 'G', 'PRESS', ctrl=False, alt=True, shift=True) 166 | layout.operator("vse_transform_tools.meta_toggle") 167 | 168 | layout.operator_context = 'INVOKE_DEFAULT' 169 | 170 | 171 | class vse_transform_tools_select(WorkSpaceTool): 172 | bl_space_type='SEQUENCE_EDITOR' 173 | bl_context_mode='PREVIEW' 174 | bl_idname = "transform_tool.select" 175 | bl_label = "Select" 176 | bl_description = ( 177 | "Move Strip in the Preview" 178 | ) 179 | bl_icon = "ops.generic.select" 180 | bl_widget = None 181 | operator="transform.translate", 182 | bl_keymap = ( 183 | ("vse_transform_tools.select", {"type": 'LEFTMOUSE', "value": 'PRESS'}, 184 | {"properties": []}), 185 | ("vse_transform_tools.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, 186 | {"properties": []}), 187 | ("vse_transform_tools.select", {"type": 'A', "value": 'PRESS'}, 188 | {"properties": []}), 189 | ) 190 | 191 | def draw_settings(context, layout, tool): 192 | tool.operator_properties("vse_transform_tools.select") 193 | scene = context.scene 194 | strip = scene.sequence_editor.active_strip 195 | if scene and strip and strip.type == 'TRANSFORM': 196 | layout.label(text=strip.name) 197 | 198 | class vse_transform_tools_grab(WorkSpaceTool): 199 | bl_space_type='SEQUENCE_EDITOR' 200 | bl_context_mode='PREVIEW' 201 | bl_idname = "transform_tool.grab" 202 | bl_label = "Move" 203 | bl_description = ( 204 | "Move Strip in Preview" 205 | ) 206 | bl_icon = "ops.transform.translate" 207 | bl_widget = None 208 | operator="transform.translate", 209 | bl_keymap = ( 210 | ("vse_transform_tools.grab", {"type": 'LEFTMOUSE', "value": 'PRESS'}, 211 | {"properties": []}), 212 | ) 213 | 214 | @classmethod 215 | def poll(cls, context): 216 | if context.scene and context.scene.sequence_editor and context.scene.sequence_editor.active_strip: 217 | return context.scene.sequence_editor.active_strip.type == 'TRANSFORM' 218 | else: 219 | return False 220 | 221 | def draw_settings(context, layout, tool): 222 | scene = context.scene 223 | strip = scene.sequence_editor.active_strip 224 | if scene and strip and strip.type == 'TRANSFORM': 225 | tool.operator_properties("vse_transform_tools.grab") 226 | layout.prop(strip, "interpolation") 227 | layout.prop(strip, "translation_unit") 228 | layout.prop(strip, "translate_start_x", text="X") 229 | layout.prop(strip, "translate_start_y", text="Y") 230 | 231 | 232 | class vse_transform_tools_rotate(WorkSpaceTool): 233 | bl_space_type='SEQUENCE_EDITOR' 234 | bl_context_mode='PREVIEW' 235 | bl_idname = "transform_tool.rotate" 236 | bl_label = "Rotate" 237 | bl_description = ( 238 | "Rotate Strip in Preview" 239 | ) 240 | bl_icon = "ops.transform.rotate" 241 | bl_widget = None 242 | operator="transform.translate", 243 | bl_keymap = ( 244 | ("vse_transform_tools.rotate", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), 245 | ("vse_transform_tools.rotate", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, 246 | {"properties": []}), 247 | ) 248 | 249 | @classmethod 250 | def poll(cls, context): 251 | if context.scene and context.scene.sequence_editor and context.scene.sequence_editor.active_strip: 252 | return context.scene.sequence_editor.active_strip.type == 'TRANSFORM' 253 | else: 254 | return False 255 | 256 | def draw_settings(context, layout, tool): 257 | tool.operator_properties("vse_transform_tools.rotate") 258 | scene = context.scene 259 | strip = scene.sequence_editor.active_strip 260 | if scene and strip and strip.type == 'TRANSFORM': 261 | layout.prop(strip, "rotation_start", text="Rotation") 262 | 263 | 264 | class vse_transform_tools_scale(WorkSpaceTool): 265 | bl_space_type='SEQUENCE_EDITOR' 266 | bl_context_mode='PREVIEW' 267 | bl_idname = "transform_tool.scale" 268 | bl_label = "Scale" 269 | bl_description = ( 270 | "Scale Strip in Preview" 271 | ) 272 | bl_icon = "ops.transform.resize" 273 | bl_widget = None 274 | operator="transform.translate", 275 | bl_keymap = ( 276 | ("vse_transform_tools.scale", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), 277 | ("vse_transform_tools.scale", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, 278 | {"properties": []}), 279 | ) 280 | 281 | @classmethod 282 | def poll(cls, context): 283 | if context.scene and context.scene.sequence_editor and context.scene.sequence_editor.active_strip: 284 | return context.scene.sequence_editor.active_strip.type == 'TRANSFORM' 285 | else: 286 | return False 287 | 288 | def draw_settings(context, layout, tool): 289 | tool.operator_properties("vse_transform_tools.scale") 290 | scene = context.scene 291 | strip = scene.sequence_editor.active_strip 292 | if scene and strip and strip.type == 'TRANSFORM': 293 | layout.prop(strip, "interpolation") 294 | layout.prop(strip, "translation_unit") 295 | layout.prop(strip, "scale_start_x", text="X") 296 | layout.prop(strip, "scale_start_y", text="Y") 297 | 298 | 299 | class vse_transform_tools_crop(WorkSpaceTool): 300 | bl_space_type='SEQUENCE_EDITOR' 301 | bl_context_mode='PREVIEW' 302 | bl_idname = "transform_tool.crop" 303 | bl_label = "Crop" 304 | bl_description = ( 305 | "Crop Strip in Preview" 306 | ) 307 | bl_icon = "ops.sequencer.blade" 308 | bl_widget = None 309 | operator="transform.translate", 310 | bl_keymap = ( 311 | ("vse_transform_tools.crop", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), 312 | ("vse_transform_tools.crop", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, 313 | {"properties": []}), 314 | ) 315 | 316 | @classmethod 317 | def poll(cls, context): 318 | if context.scene and context.scene.sequence_editor and context.scene.sequence_editor.active_strip: 319 | return context.scene.sequence_editor.active_strip.type == 'TRANSFORM' 320 | else: 321 | return False 322 | 323 | def draw_settings(context, layout, tool): 324 | tool.operator_properties("vse_transform_tools.crop") 325 | scene = context.scene 326 | strip = scene.sequence_editor.active_strip 327 | if scene and strip and strip.type == 'TRANSFORM': 328 | layout.label(text=strip.name) 329 | 330 | 331 | class SEQUENCER_PT_track_transform_ui(bpy.types.Panel): 332 | bl_space_type = "SEQUENCE_EDITOR" 333 | bl_region_type = "UI" 334 | bl_label = "VSE_Transform_Tools" 335 | bl_options = {"DEFAULT_CLOSED"} 336 | bl_category = "Tools" 337 | 338 | @classmethod 339 | def poll(cls, context): 340 | return context.space_data.view_type == 'SEQUENCER' 341 | 342 | def draw(self, context): 343 | scene = context.scene 344 | layout = self.layout 345 | 346 | # TRANSFORM FROM 2D TRACK 347 | box = layout.box() 348 | row = box.row() 349 | row.label(text="Transform from 2D Track") 350 | row = box.row() 351 | row.prop(scene, "vse_transform_tools_use_rotation", text="Rotation") 352 | row.prop(scene, "vse_transform_tools_use_scale", text="Scale") 353 | 354 | row = box.row() 355 | row.prop(scene, "vse_transform_tools_tracker_1") 356 | row = box.row() 357 | row.prop(scene, "vse_transform_tools_tracker_2") 358 | if scene.vse_transform_tools_use_rotation or scene.vse_transform_tools_use_scale: 359 | row.enabled = True 360 | else: 361 | row.enabled = False 362 | 363 | row = box.row() 364 | row.operator("vse_transform_tools.track_transform") 365 | 366 | 367 | def get_tracker_list(self, context): 368 | tracks = [("None", "None", "")] 369 | for movieclip in bpy.data.movieclips: 370 | for track in movieclip.tracking.tracks: 371 | tracks.append((track.name, track.name, "")) 372 | return tracks 373 | 374 | 375 | def init_properties(): 376 | bpy.types.Scene.seq_cursor2d_loc = bpy.props.IntVectorProperty( 377 | name="Scales", 378 | description="location of the cursor2d", 379 | subtype='XYZ', 380 | default=(50, 50), 381 | size=2, 382 | step=1, 383 | update=update_seq_cursor2d_loc 384 | ) 385 | 386 | item_pivot_point = ( 387 | ('0', 'Median Point', '', 'PIVOT_MEDIAN', 0), 388 | ('1', 'Individual Origins', '', 'PIVOT_BOUNDBOX', 1), 389 | ('2', '2D Cursor', '', 'PIVOT_CURSOR', 2), 390 | ('3', 'Active Strip', '', 'PIVOT_ACTIVE', 3) 391 | ) 392 | bpy.types.Scene.seq_pivot_type = bpy.props.EnumProperty( 393 | name="Pivot Point", 394 | default="1", 395 | items=item_pivot_point, 396 | update=update_pivot_point 397 | ) 398 | 399 | bpy.types.SEQUENCER_HT_header.append(Add_Icon_Pivot_Point) 400 | 401 | bpy.types.Scene.vse_transform_tools_use_rotation = bpy.props.BoolProperty( 402 | name="Rotation", 403 | default=True 404 | ) 405 | 406 | bpy.types.Scene.vse_transform_tools_use_scale = bpy.props.BoolProperty( 407 | name="Scale", 408 | default=True 409 | ) 410 | 411 | bpy.types.Scene.vse_transform_tools_tracker_1 = bpy.props.EnumProperty( 412 | name="Tracker 1", 413 | items=get_tracker_list 414 | ) 415 | 416 | bpy.types.Scene.vse_transform_tools_tracker_2 = bpy.props.EnumProperty( 417 | name="Tracker 2", 418 | items=get_tracker_list 419 | ) 420 | 421 | classes = [ 422 | PREV_OT_initialize_pivot, 423 | PREV_OT_set_cursor_2d, 424 | PREV_OT_add_transform, 425 | PREV_OT_select, 426 | PREV_OT_grab, 427 | PREV_OT_scale, 428 | PREV_OT_rotate, 429 | PREV_OT_autocrop, 430 | PREV_OT_delete, 431 | PREV_OT_duplicate, 432 | PREV_OT_group, 433 | PREV_OT_meta_toggle, 434 | PREV_OT_adjust_alpha, 435 | PREV_OT_call_menu, 436 | PREV_OT_insert_keyframe, 437 | PREV_MT_menu_insert_keyframe, 438 | PREV_OT_pixelate, 439 | PREV_OT_mouse_track, 440 | PREV_OT_crop, 441 | SEQUENCER_MT_transform_tools_menu, 442 | SEQUENCER_OT_track_transform, 443 | SEQUENCER_PT_track_transform_ui 444 | ] 445 | 446 | addon_keymaps = [] 447 | 448 | def register(): 449 | from bpy.utils import register_class 450 | for cls in classes: 451 | register_class(cls) 452 | 453 | init_properties() 454 | 455 | wm = bpy.context.window_manager 456 | km = wm.keyconfigs.addon.keymaps.new(name="SequencerPreview", space_type="SEQUENCE_EDITOR", region_type="WINDOW") 457 | 458 | kmi = km.keymap_items.new("vse_transform_tools.add_transform", 'T', 'PRESS', shift=True) 459 | 460 | kmi = km.keymap_items.new("vse_transform_tools.grab", 'G', 'PRESS', alt=True, shift=False) 461 | kmi = km.keymap_items.new("vse_transform_tools.grab", 'G', 'PRESS') 462 | 463 | kmi = km.keymap_items.new("vse_transform_tools.scale", 'S', 'PRESS', alt=True) 464 | kmi = km.keymap_items.new("vse_transform_tools.scale", 'S', 'PRESS') 465 | 466 | kmi = km.keymap_items.new("vse_transform_tools.rotate", 'R', 'PRESS', alt=True) 467 | kmi = km.keymap_items.new("vse_transform_tools.rotate", 'R', 'PRESS') 468 | 469 | kmi = km.keymap_items.new("vse_transform_tools.autocrop", 'C', 'PRESS', shift=True) 470 | 471 | kmi = km.keymap_items.new("vse_transform_tools.crop", 'C', 'PRESS', alt=True) 472 | kmi = km.keymap_items.new("vse_transform_tools.crop", 'C', 'PRESS') 473 | 474 | kmi = km.keymap_items.new("vse_transform_tools.delete", "DEL", "PRESS", shift=True) 475 | kmi = km.keymap_items.new("vse_transform_tools.delete", "DEL", "PRESS") 476 | 477 | kmi = km.keymap_items.new("vse_transform_tools.duplicate", "D", 'PRESS', shift=True) 478 | 479 | kmi = km.keymap_items.new("vse_transform_tools.group", 'G', 'PRESS', ctrl=False, alt=True, shift=True) 480 | kmi = km.keymap_items.new("vse_transform_tools.group", 'G', 'PRESS', ctrl=True) 481 | 482 | kmi = km.keymap_items.new("vse_transform_tools.meta_toggle", "TAB", "PRESS") 483 | 484 | kmi = km.keymap_items.new("vse_transform_tools.adjust_alpha", 'Q', 'PRESS', alt=True) 485 | kmi = km.keymap_items.new("vse_transform_tools.adjust_alpha", 'Q', 'PRESS') 486 | 487 | kmi = km.keymap_items.new("vse_transform_tools.call_menu", 'I', 'PRESS') 488 | 489 | kmi = km.keymap_items.new("vse_transform_tools.pixelate", 'P', 'PRESS') 490 | 491 | kmi = km.keymap_items.new("vse_transform_tools.mouse_track", 'M', 'PRESS') 492 | 493 | #smb = bpy.data.window_managers["WinMan"].keyconfigs.active.preferences.select_mouse 494 | #smb = bpy.context.user_preferences.inputs.select_mouse 495 | 496 | smb = "RIGHT" 497 | kmi = km.keymap_items.new("vse_transform_tools.select", smb + 'MOUSE', 'PRESS') 498 | kmi = km.keymap_items.new("vse_transform_tools.select", smb + 'MOUSE', 'PRESS', shift=True) 499 | kmi = km.keymap_items.new("vse_transform_tools.select", 'A', 'PRESS') 500 | 501 | omb = "LEFT" 502 | kmi = km.keymap_items.new("vse_transform_tools.set_cursor2d", omb + 'MOUSE', 'PRESS', shift=True) 503 | kmi = km.keymap_items.new("vse_transform_tools.set_cursor2d", omb + 'MOUSE', 'PRESS', ctrl=True) 504 | 505 | addon_keymaps.append(km) 506 | 507 | bpy.utils.register_tool(vse_transform_tools_select, after={"builtin.sample"}, separator=True, group=False) 508 | bpy.utils.register_tool(vse_transform_tools_grab) 509 | bpy.utils.register_tool(vse_transform_tools_rotate) 510 | bpy.utils.register_tool(vse_transform_tools_scale) 511 | bpy.utils.register_tool(vse_transform_tools_crop) 512 | 513 | bpy.types.SEQUENCER_MT_editor_menus.append(Add_Menu) 514 | 515 | def unregister(): 516 | from bpy.utils import unregister_class 517 | for cls in reversed(classes): 518 | unregister_class(cls) 519 | 520 | wm = bpy.context.window_manager 521 | for km in addon_keymaps: 522 | wm.keyconfigs.addon.keymaps.remove(km) 523 | addon_keymaps.clear() 524 | 525 | del bpy.types.Scene.seq_cursor2d_loc 526 | del bpy.types.Scene.seq_pivot_type 527 | bpy.types.SEQUENCER_HT_header.remove(Add_Icon_Pivot_Point) 528 | del bpy.types.Scene.vse_transform_tools_use_rotation 529 | del bpy.types.Scene.vse_transform_tools_use_scale 530 | del bpy.types.Scene.vse_transform_tools_tracker_1 531 | del bpy.types.Scene.vse_transform_tools_tracker_2 532 | 533 | bpy.utils.unregister_tool(vse_transform_tools_select) 534 | bpy.utils.unregister_tool(vse_transform_tools_grab) 535 | bpy.utils.unregister_tool(vse_transform_tools_rotate) 536 | bpy.utils.unregister_tool(vse_transform_tools_scale) 537 | bpy.utils.unregister_tool(vse_transform_tools_crop) 538 | 539 | bpy.types.SEQUENCER_MT_editor_menus.remove(Add_Menu) 540 | -------------------------------------------------------------------------------- /make_readme.py: -------------------------------------------------------------------------------- 1 | import math 2 | from markdown2 import markdown 3 | 4 | 5 | def make_toc_label(label, description): 6 | """ 7 | Make a table-of-contents item with a link to the operator and 8 | description for it's tooltip. 9 | """ 10 | description = reflow_paragraph(description, 31) 11 | 12 | hla = label.replace(' ', '_') 13 | html_link = ''.join([ 14 | '', 15 | label, '']) 16 | return html_link 17 | 18 | 19 | def reflow_paragraph(text, space, leading_space=''): 20 | ''' 21 | Reflow a flattened paragraph so it fits inside horizontal 22 | space 23 | ''' 24 | words = text.split(' ') 25 | growing_string = leading_space 26 | output_list = [] 27 | 28 | while len(words) > 0: 29 | if growing_string == leading_space: 30 | growing_string += words[0] 31 | words.pop(0) 32 | elif len(growing_string + ' ' + words[0]) <= space: 33 | growing_string += ' ' + words[0] 34 | words.pop(0) 35 | else: 36 | output_list.append(growing_string + '\n') 37 | growing_string = leading_space 38 | output_list.append(growing_string) 39 | return ''.join(output_list) 40 | 41 | 42 | def make_toc(info): 43 | """ 44 | Generate a table of contents from the operator info 45 | """ 46 | toc = [''] 47 | 48 | labels = [] 49 | 50 | for key in sorted(info.keys()): 51 | label = info[key]['name'] 52 | description = info[key]['description'] 53 | labels.append(make_toc_label(label, description)) 54 | 55 | columns = [[], [], [], []] 56 | row_count = math.ceil(len(labels) / len(columns)) 57 | 58 | i = 0 59 | col = 0 60 | while i < len(labels): 61 | for x in range(row_count): 62 | try: 63 | columns[col].append(labels[i]) 64 | i += 1 65 | except IndexError: 66 | break 67 | col += 1 68 | 69 | dead_column = False 70 | column_width = str(int((1 / len(columns)) * 888)) + 'px' 71 | for row in range(row_count): 72 | toc.append(' ') 73 | 74 | for col in range(len(columns)): 75 | try: 76 | toc.append(' ') 77 | except IndexError: 78 | if dead_column == False: 79 | remaining_rows = row_count - row 80 | toc.append(' ') 81 | dead_column = True 82 | toc.append(' ') 83 | toc.append('
' + columns[col][row] + '
') 84 | 85 | return '\n'.join(toc) 86 | 87 | 88 | def make_seg_label(label): 89 | """ 90 | Make the title label for an operator segment with a link back to the 91 | matching table of contents label 92 | """ 93 | hla = label.replace(' ', '_') 94 | seg_label = ''.join([ 95 | ' ', '

', '', 96 | label, '', '

']) 97 | return seg_label 98 | 99 | 100 | def make_shortcuts_table(op_dict): 101 | """ 102 | Make a table showing all the keyboard shortcuts, their functions, 103 | and a demo for a given operator 104 | """ 105 | shortcuts = [] 106 | functions = [] 107 | demo = op_dict['demo'] 108 | 109 | for i in range(len(op_dict['shortcuts'])): 110 | shortcuts.append(op_dict['shortcuts'][i].split(';')[0].strip()) 111 | functions.append(op_dict['shortcuts'][i].split(';')[1].strip()) 112 | 113 | for i in range(len(shortcuts)): 114 | hotkeys = shortcuts[i].split(' ') 115 | for x in range(len(hotkeys)): 116 | hotkeys[x] = '' + hotkeys[x].strip().upper() + '' 117 | shortcuts[i] = ''.join(hotkeys) 118 | 119 | hotkeys_width = str(int((888 - 256) * 0.33)) + 'px' 120 | function_width = str(int((888 - 256) * 0.66)) + 'px' 121 | table = [''] 122 | table.append(' ') 123 | table.append(' ') 124 | 125 | if len(functions) > 0: 126 | table.append(' ') 127 | if demo != '': 128 | table.append(' ') 129 | 130 | for i in range(len(shortcuts)): 131 | table.append(' ') 132 | 133 | table.append(' ') 134 | table.append(' ') 135 | 136 | if i == 0 and demo != '': 137 | table.append(' ') 138 | 139 | table.append(' ') 140 | table.append('
ShortcutFunctionDemo
' + ''.join(shortcuts[i]) + '' + markdown(functions[i]) + '' + '
') 141 | return '\n'.join(table) 142 | 143 | 144 | def make_operator_segments(info): 145 | """ 146 | Using the operator info, make segments that put all that info together. 147 | """ 148 | segments = [] 149 | 150 | for key in sorted(info.keys()): 151 | label = make_seg_label(info[key]['name']) 152 | description = markdown(info[key]['description']) 153 | shortcut_table = make_shortcuts_table(info[key]) 154 | 155 | segments.append('\n'.join([label, description, shortcut_table])) 156 | return '\n'.join(segments) 157 | 158 | 159 | def make_readme(): 160 | """ 161 | Generate a nice-looking readme. 162 | """ 163 | 164 | readme_path = 'README.rst' 165 | 166 | title = """ 167 |

168 | VSE_Transform_Tools
169 |

170 | """ 171 | 172 | intro = markdown(""" 173 | ## Installation 174 | 1. Go to the [Releases](https://github.com/doakey3/VSE_Transform_Tools/releases) page and download the latest `VSE_Transform_Tools.zip` 175 | 2. Open Blender 176 | 3. Go to File > User Preferences > Addons 177 | 4. Click "Install From File" and navigate to the downloaded .zip file and install 178 | 5. Check the box next to "VSE Transform Tools" 179 | 6. Save User Settings so the addon remains active every time you open Blender 180 | 181 | Use the correct release for your Blender version. Add-ons for Blender 2.80 and above will not work for Blender 2.79 182 | """.strip(), extras=['cuddled_lists']) 183 | 184 | operator_info = { 185 | 'vse_transform_tools.add_transform': { 186 | 'name': 'Add Transform', 187 | 'description': "A transform modifier must be added to a strip before the strip can be scaled or rotated by this addon. If you're planning to make keyframes to adjust the scale or the rotation, ensure that you are modifying a transform strip by adding one with this operator.", 188 | 'shortcuts': ['T; Add Transform'], 189 | 'demo': 'https://i.imgur.com/v4racQW.gif', 190 | }, 191 | 'vse_transform_tools.adjust_alpha': { 192 | 'name': 'Adjust Alpha', 193 | 'description': "", 194 | 'shortcuts': ['Q; Begin alpha adjusting', 'Ctrl; Round to nearest tenth', 'RIGHTMOUSE; Escape alpha adjust mode', 'LEFTMOUSE; Set alpha, end alpha adjust mode', 'RET; Set Alpha, end alpha adjust mode', 'ZERO ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE PERIOD; Set alpha to value entered'], 195 | 'demo': 'https://i.imgur.com/PNsjamH.gif', 196 | }, 197 | 'vse_transform_tools.autocrop': { 198 | 'name': 'Autocrop', 199 | 'description': "Sets the scene resolution to fit all visible content in the preview window without changing strip sizes.", 200 | 'shortcuts': ['SHIFT C; Autocrop'], 201 | 'demo': 'https://i.imgur.com/IarxF14.gif', 202 | }, 203 | 'vse_transform_tools.call_menu': { 204 | 'name': 'Call Menu', 205 | 'description': "Bring up the menu for inserting a keyframe. Alternatively, you may enable automatic keyframing.
![Automatic Keyframe Insertion](https://i.imgur.com/kFtT1ja.jpg)", 206 | 'shortcuts': ['I; Call menu'], 207 | 'demo': 'https://i.imgur.com/9Cx6XKj.gif', 208 | }, 209 | 'vse_transform_tools.crop': { 210 | 'name': 'Crop', 211 | 'description': "", 212 | 'shortcuts': ['C; Begin/Set cropping, adding a transform if needed', 'ESC; Escape crop mode', 'LEFTMOUSE; Click the handles to drag', 'RET; Set crop, end cropping', 'Alt C; Uncrop'], 213 | 'demo': 'https://i.imgur.com/k4r2alY.gif', 214 | }, 215 | 'vse_transform_tools.delete': { 216 | 'name': 'Delete', 217 | 'description': "Deletes all selected strips as well as any strips that are inputs of those strips. For example, deleting a transform strip with this operator will also delete the strip it was transforming.", 218 | 'shortcuts': ['DEL; Delete', 'Shift DEL; Delete strips and remove any other strips in the timeline with the same source. For scene strips, the scenes themselves will also be deleted.'], 219 | 'demo': 'https://i.imgur.com/B0L7XoV.gif', 220 | }, 221 | 'vse_transform_tools.duplicate': { 222 | 'name': 'Duplicate', 223 | 'description': "Duplicates all selected strips and any strips that are inputs of those strips. Calls the Grab operator immediately after duplicating.", 224 | 'shortcuts': ['Shift D; Duplicate'], 225 | 'demo': 'https://i.imgur.com/IJh7v3z.gif', 226 | }, 227 | 'vse_transform_tools.grab': { 228 | 'name': 'Grab', 229 | 'description': "", 230 | 'shortcuts': ['G; Grab', 'Shift; Hold to enable fine tuning', 'Ctrl; Hold to enable snapping', 'RIGHTMOUSE; Escape grab mode', 'Esc; Escape grab mode', 'LEFTMOUSE; Set position, end grab mode', 'RET; Set position, end grab mode', 'ZERO ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE PERIOD; Set position by value entered', 'X Y; Constrain grabbing to the respective axis', 'MIDDLEMOUSE; Constrain grabbing to axis', 'ALT G; Set position to 0,0'], 231 | 'demo': 'https://i.imgur.com/yQCFI0s.gif', 232 | }, 233 | 'vse_transform_tools.group': { 234 | 'name': 'Group', 235 | 'description': "", 236 | 'shortcuts': ['Ctrl G; Group together selected sequences', 'Alt Shift G; Ungroup selected meta strip'], 237 | 'demo': '' 238 | }, 239 | 'vse_transform_tools.meta_toggle': { 240 | 'name': 'Meta Toggle', 241 | 'description': "Toggles the selected strip if it is a META. If the selected strip is not a meta, recursively checks inputs until a META strip is encountered and toggles it. If no META is found, this operator does nothing.", 242 | 'shortcuts': ['TAB; Meta toggle'], 243 | 'demo': 'https://i.imgur.com/ya0nEgV.gif', 244 | }, 245 | 'vse_transform_tools.mouse_track': { 246 | 'name': 'Mouse Track', 247 | 'description': 'Select a transform strip or a strip with "image offset" enabled. Press Alt+A to play, hold M to continuously add keyframes to transform strip while tracking the position of the mouse.', 248 | 'shortcuts': ['M; Hold to add keyframes, release to stop'], 249 | 'demo': 'https://i.imgur.com/6091cqv.gif', 250 | }, 251 | 'vse_transform_tools.pixelate': { 252 | 'name': 'Pixelate', 253 | 'description': "Pixelate a clip by adding 2 transform modifiers: 1 shrinking, 1 expanding.", 254 | 'shortcuts': ['P; Pixelate'], 255 | 'demo': 'https://i.imgur.com/u8nUPj6.gif', 256 | }, 257 | 'vse_transform_tools.rotate': { 258 | 'name': 'Rotate', 259 | 'description': "", 260 | 'shortcuts': ['R; Begin rotating, adding transform if needed.', 'Shift; Hold to enable fine tuning', 'Ctrl; Hold to enable stepwise rotation', 'RIGHTMOUSE; Escape rotate mode', 'Esc; Escape rotate mode', 'LEFTMOUSE; Set rotation, end rotate mode', 'RET; Set rotation, end rotate mode', 'ZERO ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE PERIOD; Set rotation to value entered', 'ALT R; Set rotation to 0 degrees'], 261 | 'demo': 'https://i.imgur.com/3ru1Xl6.gif', 262 | }, 263 | 'vse_transform_tools.scale': { 264 | 'name': 'Scale', 265 | 'description': "", 266 | 'shortcuts': ['S; Begin scaling, adding transform if needed.', 'Shift; hold to enable fine tuning', 'Ctrl; Hold to enable snapping', 'RIGHTMOUSE; Escape scaling mode', 'ESC; escape scaling mode', 'LEFTMOUSE; Set scale, end scaling mode', 'RET; Set scale, end scaling mode', 'ZERO ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE PERIOD; Set scale by value entered', 'X Y; Constrain scaling to respective axis', 'MIDDLEMOUSE; Constrain scaling to axis', 'Alt S; Unscale'], 267 | 'demo': 'https://i.imgur.com/oAxSEYB.gif', 268 | }, 269 | 'vse_transform_tools.select': { 270 | 'name': 'Select', 271 | 'description': "", 272 | 'shortcuts': ['RIGHTMOUSE; Select visible strip', 'SHIFT; Enable multi selection', 'A; Toggle selection'], 273 | 'demo': 'https://i.imgur.com/EVzmMAm.gif', 274 | }, 275 | 'vse_transform_tools.set_cursor2d': { 276 | 'name': 'Set Cursor 2D', 277 | 'description': "Set the pivot point (point of origin) location. This will affect how strips are rotated and scaled.", 278 | 'shortcuts': ['LEFTMOUSE; Cusor 2D to mouse position', 'Ctrl LEFTMOUSE; Snap cursor 2D to nearest strip corner or mid-point'], 279 | 'demo': 'https://i.imgur.com/1uTD9C1.gif', 280 | }, 281 | 'vse_transform_tools.track_transform': { 282 | 'name': 'Track Transform', 283 | 'description': 'Use a pair of track points to pin a strip to another. The UI for this tool is located in the menu to the right of the sequencer in the Tools submenu. To pin rotation and/or scale, you must use 2 tracking points.
![UI](https://i.imgur.com/wEZLu8a.jpg)', 284 | 'shortcuts': [';'], 285 | 'demo': 'https://i.imgur.com/nWto3hH.gif', 286 | }, 287 | } 288 | 289 | toc_title = "

Operators

" 290 | 291 | table_of_contents = make_toc(operator_info) 292 | 293 | operator_segments = make_operator_segments(operator_info) 294 | 295 | html = '\n'.join([title, intro, toc_title, table_of_contents, operator_segments]) 296 | 297 | lines = html.split('\n') 298 | for i in range(len(lines)): 299 | lines[i] = ' ' + lines[i] 300 | 301 | readme = '.. raw:: html\n\n' + '\n'.join(lines) 302 | 303 | with open(readme_path, 'w') as f: 304 | f.write(readme) 305 | 306 | if __name__ == "__main__": 307 | make_readme() 308 | -------------------------------------------------------------------------------- /operators/__init__.py: -------------------------------------------------------------------------------- 1 | from .set_cursor2d import PREV_OT_set_cursor_2d 2 | from .add_transform import PREV_OT_add_transform 3 | from .select import PREV_OT_select 4 | from .grab import PREV_OT_grab 5 | from .scale import PREV_OT_scale 6 | from .rotate import PREV_OT_rotate 7 | from .autocrop import PREV_OT_autocrop 8 | from .delete import PREV_OT_delete 9 | from .duplicate import PREV_OT_duplicate 10 | from .group import PREV_OT_group 11 | from .meta_toggle import PREV_OT_meta_toggle 12 | from .adjust_alpha import PREV_OT_adjust_alpha 13 | from .call_menu import PREV_OT_call_menu 14 | from .call_menu import PREV_OT_insert_keyframe 15 | from .call_menu import PREV_MT_menu_insert_keyframe 16 | from .pixelate import PREV_OT_pixelate 17 | from .mouse_track import PREV_OT_mouse_track 18 | from .crop import PREV_OT_crop 19 | from .track_transform import SEQUENCER_OT_track_transform 20 | -------------------------------------------------------------------------------- /operators/add_transform/__init__.py: -------------------------------------------------------------------------------- 1 | from .add_transform import PREV_OT_add_transform 2 | -------------------------------------------------------------------------------- /operators/add_transform/add_transform.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..utils.geometry import get_transform_box 4 | from ..utils.geometry import get_strip_box 5 | from ..utils.selection import get_input_tree 6 | 7 | class PREV_OT_add_transform(bpy.types.Operator): 8 | """ 9 | Adds a transform modifier to the selected strip(s) 10 | """ 11 | bl_idname = "vse_transform_tools.add_transform" 12 | bl_label = "Add Transform" 13 | bl_description = "Add transform modifier to the selected strip(s)" 14 | bl_options = {'REGISTER', 'UNDO'} 15 | 16 | @classmethod 17 | def poll(cls, context): 18 | if context.scene.sequence_editor: 19 | return True 20 | return False 21 | 22 | def execute(self, context): 23 | 24 | scene = context.scene 25 | 26 | selected_strips = [] 27 | for strip in context.selected_sequences: 28 | if not strip.type == 'SOUND': 29 | selected_strips.append(strip) 30 | 31 | for strip in selected_strips: 32 | strip.use_float = True 33 | 34 | bpy.ops.sequencer.select_all(action='DESELECT') 35 | scene.sequence_editor.active_strip = strip 36 | bpy.ops.sequencer.effect_strip_add(type="TRANSFORM") 37 | 38 | transform_strip = context.scene.sequence_editor.active_strip 39 | transform_strip.name = "[TR]-%s" % strip.name 40 | 41 | transform_strip.blend_type = 'ALPHA_OVER' 42 | transform_strip.blend_alpha = strip.blend_alpha 43 | 44 | tree = get_input_tree(transform_strip)[1::] 45 | for child in tree: 46 | child.mute = True 47 | 48 | if not strip.use_crop: 49 | strip.use_crop = True 50 | strip.crop.min_x = 0 51 | strip.crop.max_x = 0 52 | strip.crop.min_y = 0 53 | strip.crop.max_y = 0 54 | 55 | if strip.use_translation: 56 | if strip.type == 'TRANSFORM': 57 | left, right, bottom, top = get_transform_box(strip) 58 | else: 59 | left, right, bottom, top = get_strip_box(strip) 60 | 61 | width = right - left 62 | height = top - bottom 63 | 64 | res_x = context.scene.render.resolution_x 65 | res_y = context.scene.render.resolution_y 66 | 67 | ratio_x = width / res_x 68 | ratio_y = height / res_y 69 | 70 | transform_strip.scale_start_x = ratio_x 71 | transform_strip.scale_start_y = ratio_y 72 | 73 | offset_x = strip.transform.offset_x 74 | offset_y = strip.transform.offset_y 75 | 76 | flip_x = 1 77 | if strip.use_flip_x: 78 | flip_x = -1 79 | 80 | flip_y = 1 81 | if strip.use_flip_y: 82 | flip_y = -1 83 | 84 | pos_x = offset_x + (width / 2) - (res_x / 2) 85 | pos_x *= flip_x 86 | 87 | pos_y = offset_y + (height / 2) - (res_y / 2) 88 | pos_y *= flip_y 89 | 90 | if transform_strip.translation_unit == 'PERCENT': 91 | pos_x = (pos_x / res_x) * 100 92 | pos_y = (pos_y / res_y) * 100 93 | 94 | transform_strip.translate_start_x = pos_x 95 | transform_strip.translate_start_y = pos_y 96 | 97 | strip.use_translation = False 98 | 99 | return {'FINISHED'} 100 | -------------------------------------------------------------------------------- /operators/adjust_alpha/__init__.py: -------------------------------------------------------------------------------- 1 | from .adjust_alpha import PREV_OT_adjust_alpha 2 | -------------------------------------------------------------------------------- /operators/adjust_alpha/adjust_alpha.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from mathutils import Vector 4 | 5 | from .draw_alpha_controls import draw_alpha_controls 6 | 7 | from ..utils import process_input 8 | 9 | 10 | class PREV_OT_adjust_alpha(bpy.types.Operator): 11 | """ 12 | ![Demo](https://i.imgur.com/PNsjamH.gif) 13 | """ 14 | bl_idname = "vse_transform_tools.adjust_alpha" 15 | bl_label = "Adjust Alpha" 16 | bl_description = "Adjust alpha (opacity) of strips in the Image Preview" 17 | bl_options = {'REGISTER', 'UNDO'} 18 | 19 | first_mouse = Vector((0, 0)) 20 | pos = Vector((0, 0)) 21 | alpha_init = 0 22 | fac = 0 23 | key_val = '' 24 | 25 | tab = [] 26 | 27 | handle_alpha = None 28 | 29 | @classmethod 30 | def poll(cls, context): 31 | scene = context.scene 32 | if (scene.sequence_editor and 33 | scene.sequence_editor.active_strip and 34 | scene.sequence_editor.active_strip.select): 35 | return True 36 | return False 37 | 38 | def modal(self, context, event): 39 | context.area.tag_redraw() 40 | w = context.region.width 41 | 42 | mouse_x = event.mouse_region_x 43 | mouse_x += (self.alpha_init * w) / 5 44 | mouse_y = event.mouse_region_y 45 | 46 | self.pos = Vector((mouse_x, mouse_y)) 47 | self.pos -= self.first_mouse 48 | 49 | if self.pos.x < 0: 50 | self.pos.x = 0 51 | if self.pos.x > w / 5: 52 | self.pos.x = w / 5 53 | self.fac = self.pos.x / (w / 5) 54 | 55 | process_input(self, event.type, event.value) 56 | if self.key_val != '': 57 | try: 58 | self.fac = abs(float(self.key_val)) 59 | if self.fac > 1: 60 | self.fac = abs(float('0.' + self.key_val.replace('.', ''))) 61 | self.pos.x = self.fac * (w / 5) 62 | except ValueError: 63 | pass 64 | 65 | precision = 3 66 | if event.ctrl: 67 | precision = 1 68 | 69 | self.fac = round(self.fac, precision) 70 | for strip in self.tab: 71 | strip.blend_alpha = self.fac 72 | 73 | if (event.type == 'LEFTMOUSE' or 74 | event.type == 'RET' or 75 | event.type == 'NUMPAD_ENTER'): 76 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 77 | self.handle_alpha, 'PREVIEW') 78 | 79 | scene = context.scene 80 | if scene.tool_settings.use_keyframe_insert_auto: 81 | for strip in self.tab: 82 | strip.keyframe_insert(data_path='blend_alpha') 83 | 84 | return {'FINISHED'} 85 | 86 | if event.type == 'ESC' or event.type == 'RIGHTMOUSE': 87 | for strip in self.tab: 88 | strip.blend_alpha = self.alpha_init 89 | 90 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 91 | self.handle_alpha, 'PREVIEW') 92 | return {'FINISHED'} 93 | 94 | return {'RUNNING_MODAL'} 95 | 96 | def invoke(self, context, event): 97 | if event.alt: 98 | for strip in context.selected_sequences: 99 | strip.blend_alpha = 1.0 100 | return {'FINISHED'} 101 | 102 | else: 103 | mouse_x = event.mouse_region_x 104 | mouse_y = event.mouse_region_y 105 | self.first_mouse = Vector((mouse_x, mouse_y)) 106 | 107 | opacities = [] 108 | 109 | selected_strips = [] 110 | for strip in context.selected_sequences: 111 | if not strip.type == 'SOUND': 112 | selected_strips.append(strip) 113 | 114 | for strip in selected_strips: 115 | self.tab.append(strip) 116 | opacities.append(strip.blend_alpha) 117 | 118 | self.alpha_init = max(opacities) 119 | 120 | self.key_val != '' 121 | 122 | args = (self, context) 123 | self.handle_alpha = bpy.types.SpaceSequenceEditor.draw_handler_add( 124 | draw_alpha_controls, args, 'PREVIEW', 'POST_PIXEL') 125 | context.window_manager.modal_handler_add(self) 126 | 127 | return {'RUNNING_MODAL'} 128 | -------------------------------------------------------------------------------- /operators/adjust_alpha/draw_alpha_controls.py: -------------------------------------------------------------------------------- 1 | from ..utils.draw import draw_line 2 | from ..utils.draw import draw_square 3 | 4 | import blf 5 | 6 | def draw_alpha_controls(self, context): 7 | """ 8 | Draws the line, 2 boxes, and the control square 9 | """ 10 | w = context.region.width 11 | h = context.region.height 12 | line_width = 2 * (w / 10) 13 | offset_x = (line_width / 2) - (line_width * self.alpha_init) 14 | x = self.first_mouse.x + offset_x 15 | y = self.first_mouse.y + self.pos.y 16 | 17 | v1 = [(-w / 10) + x, y] 18 | v2 = [(w / 10) + x, y] 19 | 20 | color = (0, 0.75, 1, 1) 21 | 22 | draw_line(v1, v2, 1, color) 23 | 24 | vertex = [x - (w / 10) + self.pos.x, y] 25 | draw_square(vertex, 10, color) 26 | 27 | # Numbers 28 | font_id = 0 29 | blf.position(font_id, vertex[0] - 20, vertex[1] + 10, 0) 30 | blf.size(font_id, 20, 72) 31 | blf.draw(font_id, str(self.fac)) 32 | -------------------------------------------------------------------------------- /operators/autocrop/__init__.py: -------------------------------------------------------------------------------- 1 | from .autocrop import PREV_OT_autocrop 2 | -------------------------------------------------------------------------------- /operators/autocrop/autocrop.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | 4 | from ..utils.selection import get_visible_strips 5 | from ..utils.selection import get_transforms 6 | from ..utils.selection import get_nontransforms 7 | 8 | from ..utils.geometry import get_group_box 9 | from ..utils.geometry import reposition_strip 10 | from ..utils.geometry import reposition_transform_strip 11 | 12 | 13 | class PREV_OT_autocrop(bpy.types.Operator): 14 | """ 15 | Sets the scene resolution to fit all visible content in 16 | the preview window without changing strip sizes. 17 | """ 18 | bl_idname = "vse_transform_tools.autocrop" 19 | bl_label = "Autocrop" 20 | bl_description = "Collapse canvas to fit visible content" 21 | bl_options = {'REGISTER', 'UNDO'} 22 | 23 | @classmethod 24 | def poll(cls, context): 25 | scene = context.scene 26 | if scene.sequence_editor: 27 | return True 28 | return False 29 | 30 | def execute(self, context): 31 | scene = context.scene 32 | 33 | strips = get_visible_strips() 34 | 35 | if len(strips) == 0: 36 | return {'FINISHED'} 37 | 38 | group_box = get_group_box(strips) 39 | 40 | #all_strips = scene.sequence_editor.sequences_all 41 | #inputs = [] 42 | #for strip in all_strips: 43 | # if hasattr(strip, "input_1"): 44 | # inputs.append(strip.input_1) 45 | # if hasattr(strip, "input_2"): 46 | # inputs.append(strip.input_2) 47 | 48 | #parents = [] 49 | #for strip in all_strips: 50 | # if not strip in inputs and not strip.type == "SOUND": 51 | # parents.append(strip) 52 | 53 | min_left, max_right, min_bottom, max_top = group_box 54 | 55 | total_width = max_right - min_left 56 | total_height = max_top - min_bottom 57 | 58 | nontransforms = get_nontransforms(strips) 59 | for strip in nontransforms: 60 | reposition_strip(strip, group_box) 61 | 62 | transforms = get_transforms(strips) 63 | for strip in transforms: 64 | reposition_transform_strip(strip, group_box) 65 | 66 | scene.render.resolution_x = total_width 67 | scene.render.resolution_y = total_height 68 | 69 | return {'FINISHED'} 70 | -------------------------------------------------------------------------------- /operators/call_menu/__init__.py: -------------------------------------------------------------------------------- 1 | from .call_menu import PREV_OT_call_menu 2 | from .insert_keyframe import PREV_OT_insert_keyframe 3 | from .menu_insert_keyframe import PREV_MT_menu_insert_keyframe 4 | -------------------------------------------------------------------------------- /operators/call_menu/call_menu.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class PREV_OT_call_menu(bpy.types.Operator): 5 | """ 6 | ![Demo](https://i.imgur.com/9Cx6XKj.gif) 7 | 8 | You may also enable automatic keyframe insertion. 9 | 10 | ![Automatic Keyframe Insertion](https://i.imgur.com/kFtT1ja.jpg) 11 | """ 12 | bl_idname = "vse_transform_tools.call_menu" 13 | bl_label = "Call Menu" 14 | bl_description = "Open keyframe insertion menu" 15 | 16 | @classmethod 17 | def poll(cls, context): 18 | if (context.scene.sequence_editor and 19 | context.space_data.type == 'SEQUENCE_EDITOR' and 20 | context.region.type == 'PREVIEW'): 21 | return True 22 | return False 23 | 24 | def execute(self, context): 25 | bpy.ops.wm.call_menu(name="VSE_MT_Insert_keyframe_Menu") 26 | return {'FINISHED'} 27 | -------------------------------------------------------------------------------- /operators/call_menu/insert_keyframe.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class PREV_OT_insert_keyframe(bpy.types.Operator): 5 | bl_idname = "vse_transform_tools.insert_keyframe" 6 | bl_label = "Transform Insert KeyFrame" 7 | bl_options = {'REGISTER', 'UNDO'} 8 | 9 | ch: bpy.props.IntVectorProperty( 10 | name="ch", 11 | default=(0, 0, 0, 0, 0), 12 | size=5 13 | ) 14 | 15 | @classmethod 16 | def poll(cls, context): 17 | ret = False 18 | if context.scene.sequence_editor: 19 | ret = True 20 | return ret and context.space_data.type == 'SEQUENCE_EDITOR' 21 | 22 | def execute(self, context): 23 | for seq in context.scene.sequence_editor.sequences: 24 | if seq.select and seq.type == 'TRANSFORM': 25 | if self.ch[0] == 1: 26 | seq.keyframe_insert( 27 | data_path="translate_start_x") 28 | seq.keyframe_insert( 29 | data_path="translate_start_y") 30 | 31 | if self.ch[1] == 1: 32 | seq.keyframe_insert( 33 | data_path="rotation_start") 34 | 35 | if self.ch[2] == 1: 36 | seq.keyframe_insert( 37 | data_path="scale_start_x") 38 | seq.keyframe_insert( 39 | data_path="scale_start_y") 40 | 41 | if self.ch[3] == 1: 42 | seq.keyframe_insert( 43 | data_path="blend_alpha") 44 | 45 | if self.ch[4] == 1 and seq.input_1.use_crop: 46 | seq.input_1.crop.keyframe_insert( 47 | data_path="min_x") 48 | seq.input_1.crop.keyframe_insert( 49 | data_path="max_x") 50 | seq.input_1.crop.keyframe_insert( 51 | data_path="min_y") 52 | seq.input_1.crop.keyframe_insert( 53 | data_path="max_y") 54 | 55 | elif seq.select and not seq.type == "SOUND": 56 | if self.ch[0] == 1 and seq.use_translation: 57 | seq.transform.keyframe_insert(data_path="offset_x") 58 | seq.transform.keyframe_insert(data_path="offset_y") 59 | 60 | if self.ch[4] == 1 and seq.use_crop: 61 | seq.transform.keyframe_insert(data_path="offset_x") 62 | seq.transform.keyframe_insert(data_path="offset_y") 63 | 64 | seq.crop.keyframe_insert( 65 | data_path="min_x") 66 | seq.crop.keyframe_insert( 67 | data_path="max_x") 68 | seq.crop.keyframe_insert( 69 | data_path="min_y") 70 | seq.crop.keyframe_insert( 71 | data_path="max_y") 72 | 73 | if self.ch[3] == 1: 74 | seq.keyframe_insert( 75 | data_path="blend_alpha", frame=cf) 76 | 77 | # Apparently redrawing is bad... 78 | # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) 79 | # https://docs.blender.org/api/blender_python_api_2_78_release/info_gotcha.html 80 | # So instead, we can update blender like this: 81 | scene = context.scene 82 | scrubbing = False 83 | if scene.use_audio_scrub: 84 | scrubbing = True 85 | scene.use_audio_scrub = False 86 | 87 | context.scene.frame_current += 1 88 | context.scene.frame_current -= 1 89 | 90 | if scrubbing: 91 | scene.use_audio_scrub = True 92 | 93 | return {'FINISHED'} 94 | -------------------------------------------------------------------------------- /operators/call_menu/menu_insert_keyframe.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class PREV_MT_menu_insert_keyframe(bpy.types.Menu): 5 | bl_label = "Insert KeyFrame Menu" 6 | bl_idname = "VSE_MT_Insert_keyframe_Menu" 7 | 8 | def draw(self, context): 9 | types = [] 10 | use_translations = [] 11 | use_crops = [] 12 | for strip in bpy.context.selected_sequences: 13 | types.append(strip.type) 14 | use_translations.append(strip.use_translation) 15 | use_crops.append(strip.use_crop) 16 | 17 | layout = self.layout 18 | 19 | if "TRANSFORM" in types: 20 | layout.operator("vse_transform_tools.insert_keyframe", 21 | text="Location").ch = (1, 0, 0, 0, 0) 22 | 23 | layout.operator("vse_transform_tools.insert_keyframe", 24 | text="Rotation").ch = (0, 1, 0, 0, 0) 25 | 26 | layout.operator("vse_transform_tools.insert_keyframe", 27 | text="Scale").ch = (0, 0, 1, 0, 0) 28 | 29 | layout.operator("vse_transform_tools.insert_keyframe", 30 | text="LocRot").ch = (1, 1, 0, 0, 0) 31 | 32 | layout.operator("vse_transform_tools.insert_keyframe", 33 | text="LocScale").ch =(1, 0, 1, 0, 0) 34 | 35 | layout.operator("vse_transform_tools.insert_keyframe", 36 | text="RotScale").ch = (0, 1, 1, 0, 0) 37 | 38 | layout.operator("vse_transform_tools.insert_keyframe", 39 | text="LocRotScale").ch = (1, 1, 1, 0, 0) 40 | 41 | layout.separator() 42 | 43 | layout.operator("vse_transform_tools.insert_keyframe", 44 | text="Crop").ch = (0, 0, 0, 0, 1) 45 | 46 | layout.separator() 47 | 48 | if not all(elem == "SOUND" for elem in types): 49 | if True in use_translations: 50 | layout.operator("vse_transform_tools.insert_keyframe", 51 | text="Location").ch = (1, 0, 0, 0, 0) 52 | 53 | layout.operator("vse_transform_tools.insert_keyframe", 54 | text="Alpha").ch = (0, 0, 0, 1, 0) 55 | 56 | if True in use_crops: 57 | layout.operator("vse_transform_tools.insert_keyframe", 58 | text="Crop").ch = (0, 0, 0, 0, 1) 59 | 60 | if "TRANSFORM" in types: 61 | layout.separator() 62 | 63 | layout.operator("vse_transform_tools.insert_keyframe", 64 | text="All").ch = (1, 1, 1, 1, 1) 65 | -------------------------------------------------------------------------------- /operators/crop/__init__.py: -------------------------------------------------------------------------------- 1 | from .crop import PREV_OT_crop 2 | -------------------------------------------------------------------------------- /operators/crop/crop.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import copy 3 | 4 | import math 5 | from mathutils import Vector 6 | from mathutils.geometry import intersect_point_quad_2d 7 | 8 | from ..utils.geometry import get_preview_offset 9 | from ..utils.geometry import rotate_point 10 | 11 | from ..utils.selection import get_highest_transform 12 | 13 | from .draw_crop import draw_crop 14 | from .crop_scale import crop_scale 15 | 16 | 17 | class PREV_OT_crop(bpy.types.Operator): 18 | """ 19 | Crop the active strip 20 | """ 21 | bl_idname = "vse_transform_tools.crop" 22 | bl_label = "Crop" 23 | bl_description = "Crop a strip in the Image Preview" 24 | bl_options = {'REGISTER', 'UNDO'} 25 | 26 | init_pos_x = 0 27 | init_pos_y = 0 28 | 29 | mouse_pos = Vector([-1, -1]) 30 | current_mouse = Vector([-1, -1]) 31 | 32 | corners = [Vector([-1, -1]), Vector([-1, -1]), Vector([-1, -1]), 33 | Vector([-1, -1])] 34 | max_corners = [Vector([-1, -1]), Vector([-1, -1]), Vector([-1, -1]), 35 | Vector([-1, -1])] 36 | corner_quads = [] 37 | 38 | clicked_quad = None 39 | 40 | handle_crop = None 41 | 42 | crop_left = 0 43 | crop_right = 0 44 | crop_bottom = 0 45 | crop_top = 0 46 | 47 | scale_factor_x = 1.0 48 | scale_factor_y = 1.0 49 | 50 | @classmethod 51 | def poll(cls, context): 52 | scene = context.scene 53 | if (scene.sequence_editor and 54 | scene.sequence_editor.active_strip and 55 | scene.sequence_editor.active_strip.select): 56 | return True 57 | return False 58 | 59 | 60 | def modal(self, context, event): 61 | context.area.tag_redraw() 62 | 63 | if event.type == 'LEFTMOUSE' and event.value == 'PRESS': 64 | mouse_x = event.mouse_region_x 65 | mouse_y = event.mouse_region_y 66 | 67 | self.mouse_pos = Vector([mouse_x, mouse_y]) 68 | self.current_mouse = Vector([mouse_x, mouse_y]) 69 | 70 | for i in range(len(self.corner_quads)): 71 | quad = self.corner_quads[i] 72 | 73 | bl = quad[0] 74 | tl = quad[1] 75 | tr = quad[2] 76 | br = quad[3] 77 | 78 | intersects = intersect_point_quad_2d( 79 | self.mouse_pos, bl, tl, tr, br) 80 | 81 | if intersects: 82 | self.clicked_quad = i 83 | 84 | elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': 85 | offset_x, offset_y, fac, preview_zoom = get_preview_offset() 86 | 87 | active_strip = context.scene.sequence_editor.active_strip 88 | if active_strip.type == "TRANSFORM": 89 | angle = math.radians(active_strip.rotation_start) 90 | else: 91 | angle = 0 92 | 93 | origin = self.max_corners[2] - self.max_corners[0] 94 | 95 | bl = rotate_point(self.max_corners[0], -angle, origin) 96 | tl = rotate_point(self.max_corners[1], -angle, origin) 97 | tr = rotate_point(self.max_corners[2], -angle, origin) 98 | br = rotate_point(self.max_corners[3], -angle, origin) 99 | 100 | bl_current = rotate_point(self.corners[0], -angle, origin) 101 | tl_current = rotate_point(self.corners[1], -angle, origin) 102 | tr_current = rotate_point(self.corners[2], -angle, origin) 103 | br_current = rotate_point(self.corners[3], -angle, origin) 104 | 105 | if self.clicked_quad is not None: 106 | for i in range(len(self.corners)): 107 | if i == 0: 108 | self.crop_left = bl_current.x - bl.x 109 | self.crop_left /= preview_zoom * fac 110 | 111 | self.crop_bottom = bl_current.y - bl.y 112 | self.crop_bottom /= preview_zoom * fac 113 | elif i == 1: 114 | self.crop_left = tl_current.x - tl.x 115 | self.crop_left /= preview_zoom * fac 116 | 117 | self.crop_top = tl.y - tl_current.y 118 | self.crop_top /= preview_zoom * fac 119 | 120 | elif i == 2: 121 | self.crop_right = tr.x - tr_current.x 122 | self.crop_right /= preview_zoom * fac 123 | 124 | self.crop_top = tr.y - tr_current.y 125 | self.crop_top /= preview_zoom * fac 126 | 127 | elif i == 3: 128 | self.crop_right = br.x - br_current.x 129 | self.crop_right /= preview_zoom * fac 130 | 131 | self.crop_bottom = br_current.y - br.y 132 | self.crop_bottom /= preview_zoom * fac 133 | 134 | self.clicked_quad = None 135 | self.mouse_pos = Vector([-1, -1]) 136 | 137 | elif event.type == 'MOUSEMOVE' and self.clicked_quad is not None: 138 | mouse_x = event.mouse_region_x 139 | mouse_y = event.mouse_region_y 140 | self.current_mouse = Vector([mouse_x, mouse_y]) 141 | 142 | elif event.type in ['MIDDLEMOUSE', 'WHEELDOWNMOUSE', 143 | 'WHEELUPMOUSE', 'RIGHT_ARROW', 'LEFT_ARROW']: 144 | return {'PASS_THROUGH'} 145 | 146 | elif event.type in ['C', 'RET', 'NUMPAD_ENTER'] and event.value == 'PRESS': 147 | offset_x, offset_y, fac, preview_zoom = get_preview_offset() 148 | 149 | active_strip = context.scene.sequence_editor.active_strip 150 | crops = [self.crop_left / self.scale_factor_x, 151 | self.crop_right / self.scale_factor_x, 152 | self.crop_bottom / self.scale_factor_y, 153 | self.crop_top / self.scale_factor_y, 154 | ] 155 | 156 | scene = context.scene 157 | 158 | if active_strip.type == "TRANSFORM": 159 | crop_scale(self, active_strip, crops) 160 | 161 | strip_in = active_strip.input_1 162 | 163 | if scene.tool_settings.use_keyframe_insert_auto: 164 | cf = context.scene.frame_current 165 | active_strip.keyframe_insert(data_path='translate_start_x') 166 | active_strip.keyframe_insert(data_path='translate_start_y') 167 | active_strip.keyframe_insert(data_path='scale_start_x') 168 | active_strip.keyframe_insert(data_path='scale_start_y') 169 | 170 | strip_in.crop.keyframe_insert(data_path='min_x') 171 | strip_in.crop.keyframe_insert(data_path='max_x') 172 | strip_in.crop.keyframe_insert(data_path='min_y') 173 | strip_in.crop.keyframe_insert(data_path='max_y') 174 | 175 | else: 176 | active_strip.use_crop = True 177 | active_strip.crop.min_x = crops[0] 178 | active_strip.crop.max_x = crops[1] 179 | active_strip.crop.min_y = crops[2] 180 | active_strip.crop.max_y = crops[3] 181 | 182 | active_strip.transform.offset_x = self.init_pos_x - self.init_crop_left + crops[0] 183 | active_strip.transform.offset_y = self.init_pos_y - self.init_crop_bottom + crops[2] 184 | 185 | if scene.tool_settings.use_keyframe_insert_auto: 186 | active_strip.crop.keyframe_insert(data_path='min_x') 187 | active_strip.crop.keyframe_insert(data_path='max_x') 188 | active_strip.crop.keyframe_insert(data_path='min_y') 189 | active_strip.crop.keyframe_insert(data_path='max_y') 190 | 191 | active_strip.transform.keyframe_insert(data_path="offset_x") 192 | active_strip.transform.keyframe_insert(data_path="offset_y") 193 | 194 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 195 | self.handle_crop, 'PREVIEW') 196 | return {'FINISHED'} 197 | 198 | elif (event.alt and event.type == 'C') or event.type == 'ESC': 199 | active_strip = context.scene.sequence_editor.active_strip 200 | crops = [self.init_crop_left, self.init_crop_right, 201 | self.init_crop_bottom, self.init_crop_top] 202 | 203 | if active_strip.type == "TRANSFORM": 204 | crop_scale(self, active_strip, crops) 205 | 206 | else: 207 | active_strip.use_crop = True 208 | active_strip.crop.min_x = crops[0] 209 | active_strip.crop.max_x = crops[1] 210 | active_strip.crop.min_y = crops[2] 211 | active_strip.crop.max_y = crops[3] 212 | 213 | active_strip.transform.offset_x = crops[0] 214 | active_strip.transform.offset_y = crops[2] 215 | 216 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 217 | self.handle_crop, 'PREVIEW') 218 | return {'FINISHED'} 219 | 220 | return {'RUNNING_MODAL'} 221 | 222 | def invoke(self, context, event): 223 | scene = context.scene 224 | 225 | res_x = scene.render.resolution_x 226 | res_y = scene.render.resolution_y 227 | 228 | strip = get_highest_transform(scene.sequence_editor.active_strip) 229 | scene.sequence_editor.active_strip = strip 230 | 231 | if not strip.type == "TRANSFORM" and not strip.use_translation: 232 | bpy.ops.vse_transform_tools.add_transform() 233 | strip.select = False 234 | strip = scene.sequence_editor.active_strip 235 | 236 | elif not strip.type == "TRANSFORM" and strip.use_translation: 237 | 238 | strip.use_translation = True 239 | strip.use_crop = True 240 | 241 | self.init_crop_left = strip.crop.min_x 242 | self.init_crop_right = strip.crop.max_x 243 | self.init_crop_bottom = strip.crop.min_y 244 | self.init_crop_top = strip.crop.max_y 245 | 246 | self.init_pos_x = strip.transform.offset_x 247 | self.init_pos_y = strip.transform.offset_y 248 | 249 | self.crop_left = strip.crop.min_x 250 | self.crop_right = strip.crop.max_x 251 | self.crop_bottom = strip.crop.min_y 252 | self.crop_top = strip.crop.max_y 253 | 254 | strip.crop.min_x = 0 255 | strip.crop.max_x = 0 256 | strip.crop.min_y = 0 257 | strip.crop.max_y = 0 258 | 259 | strip.transform.offset_x -= self.init_crop_left 260 | strip.transform.offset_y -= self.init_crop_bottom 261 | 262 | if event.alt: 263 | return {'FINISHED'} 264 | 265 | if strip.type == "TRANSFORM": 266 | strip_in = strip.input_1 267 | 268 | if not strip_in.use_crop: 269 | strip_in.use_crop = True 270 | 271 | strip_in.crop.min_x = 0 272 | strip_in.crop.max_x = 0 273 | strip_in.crop.min_y = 0 274 | strip_in.crop.max_y = 0 275 | 276 | if event.alt: 277 | crop_scale(self, strip, [0, 0, 0, 0]) 278 | return {'FINISHED'} 279 | 280 | crop_scale(self, strip, [0, 0, 0, 0]) 281 | 282 | args = (self, context) 283 | self.handle_crop = bpy.types.SpaceSequenceEditor.draw_handler_add( 284 | draw_crop, args, 'PREVIEW', 'POST_PIXEL') 285 | context.window_manager.modal_handler_add(self) 286 | 287 | return {'RUNNING_MODAL'} 288 | -------------------------------------------------------------------------------- /operators/crop/crop_scale.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | 4 | from ..utils.geometry import get_transform_box 5 | from ..utils.geometry import get_pos_x 6 | from ..utils.geometry import get_pos_y 7 | from ..utils.geometry import set_pos_x 8 | from ..utils.geometry import set_pos_y 9 | from ..utils.geometry import get_preview_offset 10 | 11 | 12 | def crop_scale(self, strip, crops): 13 | """ 14 | Set the strip_in crop and the strip's scale and position. 15 | 16 | When cropping, the transform strip's position and scale must change 17 | along with the crop it's input in order to keep the same location in 18 | the preview window. 19 | 20 | Parameters 21 | ---------- 22 | strip : bpy.types.Sequence 23 | This is the transform strip that will be cropped 24 | crops : list of int 25 | Left, right, bottom, top crops to apply to the strip 26 | """ 27 | context = bpy.context 28 | scene = context.scene 29 | 30 | res_x = scene.render.resolution_x 31 | res_y = scene.render.resolution_y 32 | 33 | left, right, bottom, top = get_transform_box(strip) 34 | width = right - left 35 | height = top - bottom 36 | 37 | strip_in = strip.input_1 38 | 39 | crop_left, crop_right, crop_bottom, crop_top = crops 40 | 41 | rot = math.radians(strip.rotation_start) 42 | 43 | cos = math.cos(rot) 44 | sin = math.sin(rot) 45 | 46 | crop_xl = strip_in.crop.min_x 47 | crop_xr = strip_in.crop.max_x 48 | crop_yb = strip_in.crop.min_y 49 | crop_yt = strip_in.crop.max_y 50 | 51 | crop_adjust_l = crop_left - crop_xl 52 | crop_adjust_r = crop_right - crop_xr 53 | crop_adjust_b = crop_bottom - crop_yb 54 | crop_adjust_t = crop_top - crop_yt 55 | 56 | proxy_facs = { 57 | 'NONE': 1.0, 58 | 'SCENE': 1.0, 59 | 'FULL': 1.0, 60 | 'PROXY_100': 1.0, 61 | 'PROXY_75': 0.75, 62 | 'PROXY_50': 0.5, 63 | 'PROXY_25': 0.25 64 | } 65 | 66 | proxy_key = context.space_data.proxy_render_size 67 | proxy_fac = proxy_facs[proxy_key] 68 | 69 | orig_width = res_x 70 | orig_height = res_y 71 | if hasattr(strip_in, 'elements'): 72 | orig_width = strip_in.elements[0].orig_width 73 | orig_height = strip_in.elements[0].orig_height 74 | 75 | if not (strip_in.type == 'IMAGE' and proxy_fac < 1.0): 76 | orig_width /= proxy_fac 77 | orig_height /= proxy_fac 78 | 79 | elif strip_in.type == "SCENE": 80 | strip_scene = bpy.data.scenes[strip_in.name] 81 | 82 | orig_width = strip_scene.render.resolution_x 83 | orig_height = strip_scene.render.resolution_y 84 | 85 | strip_in.crop.min_x = crop_left 86 | strip_in.crop.max_x = crop_right 87 | strip_in.crop.min_y = crop_bottom 88 | strip_in.crop.max_y = crop_top 89 | 90 | # Find the scale_x growth factor 91 | scl_x = 0 92 | if orig_width - crop_left - crop_right > 0: 93 | scl_x = (orig_width - crop_left - crop_right) / (orig_width - crop_xl - crop_xr) 94 | 95 | scl_y = 0 96 | if orig_height - crop_top - crop_bottom > 0: 97 | scl_y = (orig_height - crop_bottom - crop_top) / (orig_height - crop_yb - crop_yt) 98 | 99 | strip.scale_start_x *= scl_x 100 | strip.scale_start_y *= scl_y 101 | 102 | # Find the translation difference between old and new 103 | width_diff = (width * scl_x) - width 104 | height_diff = (height * scl_y) - height 105 | 106 | left_shift = 0 107 | right_shift = 0 108 | if abs(crop_adjust_l + crop_adjust_r) > 0: 109 | left_shift = width_diff * (crop_adjust_l / (crop_adjust_l + crop_adjust_r)) 110 | right_shift = width_diff * (crop_adjust_r / (crop_adjust_l + crop_adjust_r)) 111 | 112 | bottom_shift = 0 113 | top_shift = 0 114 | if abs(crop_adjust_b + crop_adjust_t) > 0: 115 | bottom_shift = height_diff * (crop_adjust_b / (crop_adjust_b + crop_adjust_t)) 116 | top_shift = height_diff * (crop_adjust_t / (crop_adjust_b + crop_adjust_t)) 117 | 118 | pos_x = get_pos_x(strip) 119 | pos_y = get_pos_y(strip) 120 | 121 | pos_x -= (left_shift * cos) / 2 122 | pos_x += (right_shift * cos) / 2 123 | pos_x += (bottom_shift * sin) / 2 124 | pos_x -= (top_shift * sin) / 2 125 | 126 | pos_y -= (bottom_shift * cos) / 2 127 | pos_y += (top_shift * cos) / 2 128 | pos_y -= (left_shift * sin) / 2 129 | pos_y += (right_shift * sin) / 2 130 | 131 | strip.translate_start_x = set_pos_x(strip, pos_x) 132 | strip.translate_start_y = set_pos_y(strip, pos_y) 133 | 134 | self.scale_factor_x = (res_x / orig_width) * strip.scale_start_x 135 | self.scale_factor_y = (res_y / orig_height) * strip.scale_start_y 136 | 137 | self.init_crop_left = crop_xl 138 | self.init_crop_right = crop_xr 139 | self.init_crop_bottom = crop_yb 140 | self.init_crop_top = crop_yt 141 | 142 | offset_x, offset_y, fac, preview_zoom = get_preview_offset() 143 | self.crop_left = crop_xl * self.scale_factor_x 144 | self.crop_right = crop_xr * self.scale_factor_x 145 | self.crop_bottom = crop_yb * self.scale_factor_y 146 | self.crop_top = crop_yt * self.scale_factor_y 147 | -------------------------------------------------------------------------------- /operators/crop/draw_crop.py: -------------------------------------------------------------------------------- 1 | import gpu 2 | from gpu_extras.batch import batch_for_shader 3 | 4 | from .set_corners import set_corners 5 | from .set_quads import set_quads 6 | 7 | from ..utils.draw import draw_line 8 | 9 | def draw_crop(self, context): 10 | active_strip = context.scene.sequence_editor.active_strip 11 | 12 | active_color = context.preferences.themes[0].sequence_editor.active_strip 13 | active_color = (active_color[0], active_color[1], active_color[2], 1.0) 14 | color = context.preferences.themes[0].sequence_editor.frame_current 15 | color = (color[0], color[1], color[2], 1.0) 16 | outline_color = (0, 0, 0, 1) 17 | 18 | set_corners(self, context) 19 | set_quads(self, context) 20 | 21 | vertices = [] 22 | for corner in self.corners: 23 | vertices.append([corner[0], corner[1]]) 24 | 25 | draw_line(vertices[0], vertices[1], 2, outline_color) 26 | draw_line(vertices[1], vertices[2], 2, outline_color) 27 | draw_line(vertices[2], vertices[3], 2, outline_color) 28 | draw_line(vertices[3], vertices[0], 2, outline_color) 29 | 30 | draw_line(vertices[0], vertices[1], 1, color) 31 | draw_line(vertices[1], vertices[2], 1, color) 32 | draw_line(vertices[2], vertices[3], 1, color) 33 | draw_line(vertices[3], vertices[0], 1, color) 34 | 35 | for i in range(len(self.corner_quads)): 36 | quad = self.corner_quads[i] 37 | 38 | bl = quad[0] 39 | tl = quad[1] 40 | tr = quad[2] 41 | br = quad[3] 42 | 43 | vertices = [bl, br, tl, tr] 44 | 45 | indices = ((0, 1, 2), (2, 1, 3)) 46 | 47 | shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') 48 | batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) 49 | 50 | shader.bind() 51 | 52 | if self.clicked_quad == i: 53 | shader.uniform_float("color", active_color) 54 | 55 | else: 56 | shader.uniform_float("color", color) 57 | 58 | batch.draw(shader) 59 | -------------------------------------------------------------------------------- /operators/crop/get_perpendicular_point.py: -------------------------------------------------------------------------------- 1 | from mathutils import Vector 2 | from mathutils.geometry import intersect_point_quad_2d 3 | from mathutils.geometry import intersect_line_line_2d 4 | 5 | 6 | def get_perpendicular_point(pt, bl, tl, tr, br): 7 | ''' 8 | Return the point if it is inside the quad, else, return 9 | a point on the border of the quad. 10 | 11 | This function helps prevent the user from dragging crop handles 12 | outside the strip's area. 13 | ''' 14 | 15 | intersects = intersect_point_quad_2d( 16 | pt, bl, tl, tr, br) 17 | 18 | if intersects: 19 | return pt 20 | 21 | elif pt.x <= bl.x and pt.y <= bl.y: 22 | return Vector(bl) 23 | elif pt.x <= tl.x and pt.y >= tl.y: 24 | return Vector(tl) 25 | elif pt.x >= tr.x and pt.y >= tr.y: 26 | return Vector(tr) 27 | elif pt.x >= br.x and pt.y <= br.y: 28 | return Vector(br) 29 | 30 | max_x = max([tr.x, br.x]) 31 | min_x = min([tl.x, bl.x]) 32 | 33 | max_y = max([tl.y, tr.y]) 34 | min_y = min([bl.y, br.y]) 35 | 36 | # pt left of left side 37 | if (pt.x <= tl.x or pt.x <= bl.x) and (pt.y >= bl.y and pt.y <= tl.y): 38 | right = Vector([max_x, pt.y]) 39 | intersection = intersect_line_line_2d(bl, tl, pt, right) 40 | 41 | # pt right of right side 42 | elif (pt.x >= br.x or pt.x >= tr.x) and (pt.y >= br.y and pt.y <= tr.y): 43 | left = Vector([min_x, pt.y]) 44 | intersection = intersect_line_line_2d(br, tr, pt, left) 45 | 46 | # pt above top side 47 | elif (pt.y >= tl.y or pt.y >= tr.y) and (pt.x >= tl.x and pt.x <= tr.x): 48 | bottom = Vector([pt.x, min_y]) 49 | intersection = intersect_line_line_2d(tl, tr, pt, bottom) 50 | 51 | # pt below bottom side 52 | elif (pt.y <= bl.y or pt.y <= br.y) and (pt.x >= bl.x and pt.x <= br.x): 53 | top = Vector([pt.x, max_y]) 54 | intersection = intersect_line_line_2d(bl, br, pt, top) 55 | 56 | return intersection 57 | -------------------------------------------------------------------------------- /operators/crop/set_corners.py: -------------------------------------------------------------------------------- 1 | import math 2 | from mathutils import Vector 3 | 4 | from .get_perpendicular_point import get_perpendicular_point 5 | from ..utils.geometry import get_strip_corners 6 | from ..utils.geometry import get_preview_offset 7 | from ..utils.geometry import rotate_point 8 | 9 | 10 | def set_corners(self, context): 11 | """ 12 | Set the crop corner handles based on how they are dragged by the 13 | user. 14 | """ 15 | active_strip = context.scene.sequence_editor.active_strip 16 | if active_strip.type == "TRANSFORM": 17 | angle = math.radians(active_strip.rotation_start) 18 | else: 19 | angle = 0 20 | 21 | sin = math.sin(angle) 22 | cos = math.cos(angle) 23 | 24 | offset_x, offset_y, fac, preview_zoom = get_preview_offset() 25 | 26 | self.max_corners = get_strip_corners(active_strip) 27 | for corner in self.max_corners: 28 | corner.x = (corner.x * preview_zoom * fac) + offset_x 29 | corner.y = (corner.y * preview_zoom * fac) + offset_y 30 | 31 | origin = self.max_corners[2] - self.max_corners[0] 32 | 33 | bl = rotate_point(self.max_corners[0], -angle, origin) 34 | tl = rotate_point(self.max_corners[1], -angle, origin) 35 | tr = rotate_point(self.max_corners[2], -angle, origin) 36 | br = rotate_point(self.max_corners[3], -angle, origin) 37 | 38 | vec = self.current_mouse - self.mouse_pos 39 | 40 | crop_left = self.crop_left * preview_zoom * fac 41 | crop_bottom = self.crop_bottom * preview_zoom * fac 42 | crop_top = self.crop_top * preview_zoom * fac 43 | crop_right = self.crop_right * preview_zoom * fac 44 | 45 | cushion = 10 46 | if self.clicked_quad is not None: 47 | for i in range(len(self.max_corners)): 48 | # Bottom Left Clicked 49 | if self.clicked_quad == 0: 50 | pt_x = bl.x + (vec.x * cos) + (vec.y * sin) + crop_left 51 | pt_y = bl.y + (vec.y * cos) - (vec.x * sin) + crop_bottom 52 | pt = get_perpendicular_point( 53 | Vector([pt_x, pt_y]), bl, tl, tr, br) 54 | 55 | self.corners[2].x = tr.x - crop_right 56 | self.corners[2].y = tr.y - crop_top 57 | 58 | if pt.x > self.corners[2].x - cushion: 59 | pt.x = self.corners[2].x - cushion 60 | if pt.y > self.corners[2].y - cushion: 61 | pt.y = self.corners[2].y - cushion 62 | 63 | self.corners[0] = pt 64 | 65 | self.corners[1].x = tl.x + (pt.x - bl.x) 66 | self.corners[1].y = tl.y - crop_top 67 | 68 | self.corners[3].x = br.x - crop_right 69 | self.corners[3].y = br.y + (pt.y - bl.y) 70 | 71 | break 72 | # Top Left Clicked 73 | elif self.clicked_quad == 1: 74 | pt_x = tl.x + (vec.x * cos) + (vec.y * sin) + crop_left 75 | pt_y = tl.y + (vec.y * cos) - (vec.x * sin) - crop_top 76 | pt = get_perpendicular_point( 77 | Vector([pt_x, pt_y]), bl, tl, tr, br) 78 | 79 | self.corners[3].x = br.x - crop_right 80 | self.corners[3].y = br.y + crop_bottom 81 | 82 | if pt.x > self.corners[3].x - cushion: 83 | pt.x = self.corners[3].x - cushion 84 | if pt.y < self.corners[3].y + cushion: 85 | pt.y = self.corners[3].y + cushion 86 | 87 | self.corners[1] = pt 88 | 89 | self.corners[0].x = bl.x + (pt.x - tl.x) 90 | self.corners[0].y = bl.y + crop_bottom 91 | 92 | self.corners[2].x = tr.x - crop_right 93 | self.corners[2].y = tr.y - (tl.y - pt.y) 94 | 95 | break 96 | # Top Right Clicked 97 | elif self.clicked_quad == 2: 98 | pt_x = tr.x + (vec.x * cos) + (vec.y * sin) - crop_right 99 | pt_y = tr.y + (vec.y * cos) - (vec.x * sin) - crop_top 100 | pt = get_perpendicular_point( 101 | Vector([pt_x, pt_y]), bl, tl, tr, br) 102 | 103 | self.corners[0].x = bl.x + crop_left 104 | self.corners[0].y = bl.y + crop_bottom 105 | 106 | if pt.x < self.corners[0].x + cushion: 107 | pt.x = self.corners[0].x + cushion 108 | if pt.y < self.corners[0].y + cushion: 109 | pt.y = self.corners[0].y + cushion 110 | 111 | self.corners[2] = pt 112 | 113 | self.corners[1].x = tl.x + crop_left 114 | self.corners[1].y = tl.y - (tr.y - pt.y) 115 | 116 | self.corners[3].x = br.x - (tr.x - pt.x) 117 | self.corners[3].y = br.y + crop_bottom 118 | # Bottom Right Clicked 119 | elif self.clicked_quad == 3: 120 | pt_x = br.x + (vec.x * cos) + (vec.y * sin) - crop_right 121 | pt_y = br.y + (vec.y * cos) - (vec.x * sin) + crop_bottom 122 | pt = get_perpendicular_point( 123 | Vector([pt_x, pt_y]), bl, tl, tr, br) 124 | 125 | self.corners[1].x = tl.x + crop_left 126 | self.corners[1].y = tl.y - crop_top 127 | 128 | if pt.x < self.corners[1].x + cushion: 129 | pt.x = self.corners[1].x + cushion 130 | if pt.y > self.corners[1].y - cushion: 131 | pt.y = self.corners[1].y - cushion 132 | 133 | self.corners[3] = pt 134 | 135 | self.corners[0].x = bl.x + crop_left 136 | self.corners[0].y = bl.y + (pt.y - br.y) 137 | 138 | self.corners[2].x = tr.x - (tr.x - pt.x) 139 | self.corners[2].y = tr.y - crop_top 140 | 141 | else: 142 | self.corners[0].x = bl.x + crop_left 143 | self.corners[0].y = bl.y + crop_bottom 144 | 145 | self.corners[1].x = tl.x + crop_left 146 | self.corners[1].y = tl.y - crop_top 147 | 148 | self.corners[2].x = tr.x - crop_right 149 | self.corners[2].y = tr.y - crop_top 150 | 151 | self.corners[3].x = br.x - crop_right 152 | self.corners[3].y = br.y + crop_bottom 153 | 154 | self.corners[0] = rotate_point(self.corners[0], angle, origin) 155 | self.corners[1] = rotate_point(self.corners[1], angle, origin) 156 | self.corners[2] = rotate_point(self.corners[2], angle, origin) 157 | self.corners[3] = rotate_point(self.corners[3], angle, origin) 158 | -------------------------------------------------------------------------------- /operators/crop/set_quads.py: -------------------------------------------------------------------------------- 1 | import math 2 | from mathutils import Vector 3 | from ..utils.geometry import rotate_point 4 | 5 | 6 | def set_quads(self, context): 7 | """ 8 | Set the location of the crop handles 9 | """ 10 | self.corner_quads = [] 11 | 12 | active_strip = context.scene.sequence_editor.active_strip 13 | if active_strip.type == "TRANSFORM": 14 | angle = math.radians(active_strip.rotation_start) 15 | else: 16 | angle = 0 17 | 18 | rect_size = 5.5 19 | 20 | for corner in self.corners: 21 | origin = corner 22 | 23 | x1 = corner.x - rect_size 24 | x2 = corner.x + rect_size 25 | y1 = corner.y - rect_size 26 | y2 = corner.y + rect_size 27 | 28 | bl = rotate_point(Vector([x1, y1]), angle, origin) 29 | tl = rotate_point(Vector([x1, y2]), angle, origin) 30 | tr = rotate_point(Vector([x2, y2]), angle, origin) 31 | br = rotate_point(Vector([x2, y1]), angle, origin) 32 | 33 | self.corner_quads.append([bl, tl, tr, br]) 34 | -------------------------------------------------------------------------------- /operators/delete/__init__.py: -------------------------------------------------------------------------------- 1 | from .delete import PREV_OT_delete 2 | -------------------------------------------------------------------------------- /operators/delete/delete.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ..utils.selection import get_input_tree 3 | 4 | 5 | class PREV_OT_delete(bpy.types.Operator): 6 | """ 7 | Deletes all selected strips as well as any strips that are inputs 8 | of those strips. 9 | For example, deleting a transform strip with this operator will 10 | also delete the strip it was transforming. 11 | """ 12 | bl_idname = "vse_transform_tools.delete" 13 | bl_label = "Delete" 14 | bl_description = "Delete selected and their inputs recursively" 15 | bl_options = {'REGISTER', 'UNDO'} 16 | 17 | @classmethod 18 | def poll(cls, context): 19 | scene = context.scene 20 | if (scene.sequence_editor and 21 | scene.sequence_editor.active_strip): 22 | return True 23 | return False 24 | 25 | def invoke(self, context, event): 26 | scene = context.scene 27 | selected = [] 28 | 29 | for strip in context.selected_sequences: 30 | if strip not in selected: 31 | selected.extend(get_input_tree(strip)) 32 | for strip in selected: 33 | strip.select = True 34 | 35 | delete_count = len(selected) 36 | 37 | dead_scenes = [] 38 | if event.shift: 39 | for strip in selected: 40 | if strip.type == "SCENE": 41 | for s in scene.sequence_editor.sequences_all: 42 | if s.type == "SCENE" and s.scene == strip.scene: 43 | s.select = True 44 | delete_count += 1 45 | dead_scenes.append(strip.scene) 46 | elif hasattr(strip, 'filepath'): 47 | for s in scene.sequence_editor.sequences_all: 48 | if hasattr(s, 'filepath') and s.filepath == strip.filepath: 49 | s.select = True 50 | delete_count += 1 51 | 52 | bpy.ops.sequencer.delete() 53 | 54 | for sce in dead_scenes: 55 | for obj in sce.objects: 56 | bpy.data.objects.remove(obj, True) 57 | bpy.data.scenes.remove(sce, True) 58 | 59 | selection_length = len(selected) 60 | report_message = ' '.join( 61 | ['Deleted', str(selection_length), 'sequence']) 62 | 63 | if selection_length > 1: 64 | report_message += 's' 65 | 66 | self.report({'INFO'}, report_message) 67 | 68 | return {"FINISHED"} 69 | -------------------------------------------------------------------------------- /operators/duplicate/__init__.py: -------------------------------------------------------------------------------- 1 | from .duplicate import PREV_OT_duplicate 2 | -------------------------------------------------------------------------------- /operators/duplicate/duplicate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..utils.selection import get_input_tree 4 | 5 | from .get_vertical_translation import get_vertical_translation 6 | 7 | 8 | class PREV_OT_duplicate(bpy.types.Operator): 9 | """ 10 | Duplicates all selected strips and any strips that are inputs 11 | of those strips. 12 | Calls the Grab operator immediately after duplicating. 13 | """ 14 | bl_idname = "vse_transform_tools.duplicate" 15 | bl_label = "Duplicate" 16 | bl_description = "Duplicate selected and their inputs recursively" 17 | bl_options = {'REGISTER', 'UNDO'} 18 | 19 | @classmethod 20 | def poll(cls, context): 21 | scene = context.scene 22 | if (scene.sequence_editor and 23 | scene.sequence_editor.active_strip): 24 | return True 25 | return False 26 | 27 | def invoke(self, context, event): 28 | 29 | selected = context.selected_sequences 30 | 31 | duplicated = [] 32 | 33 | for strip in selected: 34 | if strip not in duplicated: 35 | bpy.ops.sequencer.select_all(action="DESELECT") 36 | 37 | tree = get_input_tree(strip) 38 | for seq in tree: 39 | seq.select = True 40 | 41 | duplicated.extend(tree) 42 | 43 | vertical_translation = get_vertical_translation( 44 | context.selected_sequences) 45 | 46 | bpy.ops.sequencer.duplicate_move( 47 | SEQUENCER_OT_duplicate={}, 48 | TRANSFORM_OT_seq_slide={ 49 | "value": (0, vertical_translation)}) 50 | 51 | return bpy.ops.vse_transform_tools.grab('INVOKE_DEFAULT') 52 | -------------------------------------------------------------------------------- /operators/duplicate/get_vertical_translation.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from operator import attrgetter 3 | 4 | 5 | def get_vertical_translation(strips): 6 | """ 7 | Determine how many channels up the strips need to be moved 8 | in order to accomodate them all 9 | 10 | Parameters 11 | ---------- 12 | strips : list of bpy.types.Sequence() 13 | A group of strips in the sequencer 14 | 15 | Returns 16 | ------- 17 | int 18 | The number of channels UP that the group of strips need to move 19 | in order to prevent any conflicts with other strips in the 20 | sequencer. 21 | """ 22 | scene = bpy.context.scene 23 | 24 | min_channel = min(strips, key=attrgetter('channel')).channel 25 | max_channel = max(strips, key=attrgetter('channel')).channel 26 | 27 | channel_count = (max_channel - min_channel) + 1 28 | 29 | frame_start = min(strips, 30 | key=attrgetter('frame_start')).frame_start 31 | frame_end = max(strips, 32 | key=attrgetter('frame_final_end')).frame_final_end 33 | 34 | all_sequences = list(sorted(scene.sequence_editor.sequences, 35 | key=lambda x: x.frame_start)) 36 | 37 | blocked_channels = [] 38 | for seq in all_sequences: 39 | if (seq not in strips and 40 | seq.frame_start <= frame_end and 41 | seq.frame_final_end >= frame_start): 42 | blocked_channels.append(seq.channel) 43 | elif seq.frame_start > frame_end: 44 | break 45 | 46 | i = max_channel + 1 47 | while True: 48 | for x in range(i, i + channel_count): 49 | conflict = False 50 | if x in blocked_channels: 51 | conflict = True 52 | break 53 | if not conflict: 54 | return x - max_channel 55 | i += 1 56 | -------------------------------------------------------------------------------- /operators/grab/__init__.py: -------------------------------------------------------------------------------- 1 | from .grab import PREV_OT_grab 2 | -------------------------------------------------------------------------------- /operators/grab/grab.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | 4 | from ..utils import process_input 5 | from ..utils import func_constrain_axis_mmb 6 | from ..utils import func_constrain_axis 7 | 8 | from ..utils.geometry import get_pos_x 9 | from ..utils.geometry import get_pos_y 10 | from ..utils.geometry import set_pos_x 11 | from ..utils.geometry import set_pos_y 12 | from ..utils.geometry import get_res_factor 13 | from ..utils.geometry import get_group_box 14 | from ..utils.geometry import get_strip_box 15 | from ..utils.geometry import mouse_to_res 16 | from ..utils.geometry import get_preview_offset 17 | 18 | from ..utils.selection import ensure_transforms 19 | from ..utils.selection import get_highest_transform 20 | from ..utils.selection import get_visible_strips 21 | 22 | from ..utils.draw import draw_snap 23 | 24 | 25 | class PREV_OT_grab(bpy.types.Operator): 26 | """ 27 | Move strip(s) in 2D space 28 | """ 29 | bl_idname = "vse_transform_tools.grab" 30 | bl_label = "Grab" 31 | bl_description = "Change position of strips in Image Preview Window" 32 | bl_options = {'REGISTER', 'UNDO', 'GRAB_CURSOR', 'BLOCKING'} 33 | 34 | local_axis = True 35 | axis_x = True 36 | axis_y = True 37 | choose_axis = False 38 | 39 | first_mouse_pos = Vector([0, 0]) 40 | pos_clic = Vector([0, 0]) 41 | mouse_pos = Vector([0, 0]) 42 | center_area = Vector([0, 0]) 43 | vec_act = Vector([0, 0]) 44 | 45 | group_width = 0 46 | group_height = 0 47 | 48 | tab_init = [] 49 | tab = [] 50 | 51 | key_val = '' 52 | key_period = False 53 | key_period_val = 1 54 | 55 | handle_axes = None 56 | handle_snap = None 57 | 58 | slow_factor = 10 59 | pre_slow_vec = Vector([0, 0]) 60 | reduction_vec = Vector([0, 0]) 61 | slow_act_fm = Vector([0, 0]) 62 | 63 | horizontal_interests = [] 64 | vertical_interests = [] 65 | 66 | initially_shifted = False 67 | 68 | last_snap_orientation = "" 69 | last_line_loc = None 70 | orientation_conflict_winner = 0 71 | 72 | @classmethod 73 | def poll(cls, context): 74 | scene = context.scene 75 | if (scene.sequence_editor and 76 | scene.sequence_editor.active_strip): 77 | return True 78 | return False 79 | 80 | def modal(self, context, event): 81 | if self.tab: 82 | scene = context.scene 83 | res_x = scene.render.resolution_x 84 | res_y = scene.render.resolution_y 85 | 86 | mouse_x = event.mouse_region_x 87 | mouse_y = event.mouse_region_y 88 | 89 | mouse_vec = Vector([mouse_x, mouse_y]) 90 | self.mouse_pos = mouse_to_res(mouse_vec) 91 | 92 | self.vec_act = self.mouse_pos - self.reduction_vec - self.first_mouse_pos 93 | 94 | func_constrain_axis_mmb(self, context, event.type, event.value, 0) 95 | 96 | func_constrain_axis(self, context, event.type, event.value, 0) 97 | 98 | process_input(self, event.type, event.value) 99 | if self.key_val != '': 100 | try: 101 | if self.axis_y and not self.axis_x: 102 | self.vec_act = Vector([0, float(self.key_val)]) 103 | else: 104 | self.vec_act = Vector([float(self.key_val), 0]) 105 | except ValueError: 106 | pass 107 | 108 | if not self.initially_shifted and 'SHIFT' in event.type and event.value == 'PRESS' and self.key_val == '': 109 | self.pre_slow_vec = self.mouse_pos 110 | 111 | elif not self.initially_shifted and 'SHIFT' in event.type and event.value == 'RELEASE' and self.key_val == '': 112 | self.vec_act = (self.pre_slow_vec - self.first_mouse_pos - self.reduction_vec) + self.slow_act_fm 113 | self.reduction_vec = self.reduction_vec + ((self.mouse_pos - self.pre_slow_vec) * (self.slow_factor - 1)) / self.slow_factor 114 | 115 | elif not self.initially_shifted and event.shift and self.key_val == '': 116 | self.slow_act_fm = (self.mouse_pos - self.pre_slow_vec) / self.slow_factor 117 | self.vec_act = (self.pre_slow_vec - self.first_mouse_pos - self.reduction_vec) + self.slow_act_fm 118 | 119 | elif 'SHIFT' in event.type and event.value == 'RELEASE': 120 | self.initially_shifted = False 121 | 122 | info_x = round(self.vec_act.x, 5) 123 | info_y = round(self.vec_act.y, 5) 124 | if not self.axis_x: 125 | self.vec_act = Vector((0, self.vec_act.y)) 126 | context.area.header_text_set("D: %.4f along global Y" % info_y) 127 | if not self.axis_y: 128 | self.vec_act = Vector((self.vec_act.x, 0)) 129 | context.area.header_text_set("D: %.4f along global X" % info_x) 130 | if self.axis_x and self.axis_y: 131 | context.area.header_text_set("Dx: %.4f Dy: %.4f" % (info_x, info_y)) 132 | 133 | snap_distance = int(max([res_x, res_y]) / 100) 134 | 135 | group_pos_x = self.center_area.x + self.vec_act.x 136 | group_pos_y = self.center_area.y + self.vec_act.y 137 | 138 | current_left = group_pos_x - (self.group_width / 2) 139 | current_right = group_pos_x + (self.group_width / 2) 140 | current_bottom = group_pos_y - (self.group_height / 2) 141 | current_top = group_pos_y + (self.group_height / 2) 142 | 143 | trans_offset_x = 0 144 | trans_offset_y = 0 145 | 146 | orientations = [] 147 | line_locs = [] 148 | offset_x, offset_y, fac, preview_zoom = get_preview_offset() 149 | if event.ctrl: 150 | for line in self.horizontal_interests: 151 | if (current_left < line + snap_distance and 152 | current_left > line - snap_distance): 153 | trans_offset_x = line - current_left 154 | 155 | line_locs.append((line * fac * preview_zoom) + offset_x) 156 | orientations.append("VERTICAL") 157 | 158 | break 159 | if (current_right > line - snap_distance and 160 | current_right < line + snap_distance): 161 | trans_offset_x = line - current_right 162 | 163 | line_locs.append((line * fac * preview_zoom) + offset_x) 164 | orientations.append("VERTICAL") 165 | 166 | for line in self.vertical_interests: 167 | if (current_bottom < line + snap_distance and 168 | current_bottom > line - snap_distance): 169 | trans_offset_y = line - current_bottom 170 | 171 | line_locs.append((line * fac * preview_zoom) + offset_y) 172 | orientations.append("HORIZONTAL") 173 | 174 | break 175 | if (current_top > line - snap_distance and 176 | current_top < line + snap_distance): 177 | trans_offset_y = line - current_top 178 | 179 | line_locs.append((line * fac * preview_zoom) + offset_y) 180 | orientations.append("HORIZONTAL") 181 | 182 | orientation = "" 183 | line_loc = None 184 | if len(orientations) > 1 and self.last_snap_orientation != "" and self.orientation_conflict_winner == -1: 185 | index = orientations.index(self.last_snap_orientation) 186 | orientations.pop(index) 187 | line_locs.pop(index) 188 | 189 | self.orientation_conflict_winner = int(not index) 190 | 191 | orientation = orientations[0] 192 | line_loc = line_locs[0] 193 | 194 | elif len(orientations) > 1 and self.last_snap_orientation == "" and self.orientation_conflict_winner == -1: 195 | self.orientation_conflict_winner = 0 196 | orientation = orientations[0] 197 | line_loc = line_locs[0] 198 | 199 | elif len(orientations) > 1: 200 | orientation = orientations[self.orientation_conflict_winner] 201 | line_loc = line_locs[self.orientation_conflict_winner] 202 | 203 | elif len(orientations) > 0: 204 | self.orientation_conflict_winner = -1 205 | orientation = orientations[0] 206 | line_loc = line_locs[0] 207 | 208 | if orientation != "" and (self.handle_snap == None or "RNA_HANDLE_REMOVED" in str(self.handle_snap)): 209 | args = (self, line_loc, orientation) 210 | self.handle_snap = bpy.types.SpaceSequenceEditor.draw_handler_add( 211 | draw_snap, args, 'PREVIEW', 'POST_PIXEL') 212 | self.last_snap_orientation = orientation 213 | self.last_line_loc = line_loc 214 | elif (orientation != self.last_snap_orientation or line_loc != self.last_line_loc) and self.handle_snap != None and not "RNA_HANDLE_REMOVED" in str(self.handle_snap): 215 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_snap, 'PREVIEW') 216 | self.last_snap_orientation = orientation 217 | self.last_line_loc = line_loc 218 | 219 | elif self.handle_snap != None and not "RNA_HANDLE_REMOVED" in str(self.handle_snap): 220 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_snap, 'PREVIEW') 221 | 222 | for strip, init_pos in zip(self.tab, self.tab_init): 223 | pos_x = init_pos[0] + self.vec_act.x + trans_offset_x 224 | pos_y = init_pos[1] + self.vec_act.y + trans_offset_y 225 | 226 | if strip.type == "TRANSFORM": 227 | strip.translate_start_x = set_pos_x(strip, pos_x) 228 | strip.translate_start_y = set_pos_y(strip, pos_y) 229 | else: 230 | strip.transform.offset_x = pos_x 231 | strip.transform.offset_y = pos_y 232 | 233 | if (event.type == 'LEFTMOUSE' or 234 | event.type == 'RET' or 235 | event.type == 'NUMPAD_ENTER' or 236 | not self.tab): 237 | if self.handle_axes: 238 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 239 | self.handle_axes, 'PREVIEW') 240 | 241 | if self.handle_snap != None and not "RNA_HANDLE_REMOVED" in str(self.handle_snap): 242 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_snap, 'PREVIEW') 243 | 244 | if scene.tool_settings.use_keyframe_insert_auto: 245 | for strip in self.tab: 246 | if strip.type == "TRANSFORM": 247 | strip.keyframe_insert(data_path='translate_start_x') 248 | strip.keyframe_insert(data_path='translate_start_y') 249 | else: 250 | strip.transform.keyframe_insert(data_path='offset_x') 251 | strip.transform.keyframe_insert(data_path='offset_y') 252 | 253 | context.area.header_text_set(None) 254 | return {'FINISHED'} 255 | 256 | if event.type == 'ESC' or event.type == 'RIGHTMOUSE': 257 | if self.handle_axes: 258 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 259 | self.handle_axes, 'PREVIEW') 260 | 261 | if self.handle_snap != None and not "RNA_HANDLE_REMOVED" in str(self.handle_snap): 262 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_snap, 'PREVIEW') 263 | 264 | for strip, init_pos in zip(self.tab, self.tab_init): 265 | if strip.type == "TRANSFORM": 266 | strip.translate_start_x = set_pos_x(strip, init_pos[0]) 267 | strip.translate_start_y = set_pos_y(strip, init_pos[1]) 268 | else: 269 | strip.transform.offset_x = init_pos[0] 270 | strip.transform.offset_y = init_pos[1] 271 | 272 | 273 | context.area.header_text_set(None) 274 | return {'FINISHED'} 275 | 276 | return {'RUNNING_MODAL'} 277 | 278 | return {'FINISHED'} 279 | 280 | def invoke(self, context, event): 281 | scene = context.scene 282 | 283 | res_x = scene.render.resolution_x 284 | res_y = scene.render.resolution_y 285 | 286 | if event.alt: 287 | selected = bpy.context.selected_sequences 288 | for strip in selected: 289 | transform = get_highest_transform(strip) 290 | if transform.type == 'TRANSFORM': 291 | strip.translate_start_x = 0 292 | strip.translate_start_y = 0 293 | elif transform.use_translation: 294 | box = get_strip_box(transform) 295 | width = box[1] - box[0] 296 | height = box[3] - box[2] 297 | transform.transform.offset_x = (res_x / 2) - (width / 2) 298 | transform.transform.offset_y = (res_y / 2) - (height / 2) 299 | return {'FINISHED'} 300 | 301 | else: 302 | mouse_x = event.mouse_region_x 303 | mouse_y = event.mouse_region_y 304 | 305 | self.key_val = '' 306 | 307 | self.tab = [] 308 | self.tab_init = [] 309 | self.center_area = Vector([0, 0]) 310 | 311 | self.group_width = 0 312 | self.group_height = 0 313 | 314 | self.horizontal_interests = [0, res_x] 315 | self.vertical_interests = [0, res_y] 316 | 317 | mouse_vec = Vector([mouse_x, mouse_y]) 318 | self.first_mouse_pos = mouse_to_res(mouse_vec) 319 | 320 | fac = get_res_factor() 321 | 322 | #self.tab = ensure_transforms() 323 | 324 | image_offset_strips = [] 325 | selected = context.selected_sequences 326 | for strip in selected: 327 | if not strip.type == 'SOUND': 328 | if strip.use_translation: 329 | image_offset_strips.append(strip) 330 | strip.select = False 331 | 332 | self.tab = ensure_transforms() 333 | self.tab.extend(image_offset_strips) 334 | visible_strips = get_visible_strips() 335 | 336 | for strip in visible_strips: 337 | if strip not in self.tab: 338 | left, right, bottom, top = get_group_box([strip]) 339 | 340 | self.horizontal_interests.append(left) 341 | self.horizontal_interests.append(right) 342 | 343 | self.vertical_interests.append(bottom) 344 | self.vertical_interests.append(top) 345 | 346 | for strip in self.tab: 347 | strip.select = True 348 | if strip.type == "TRANSFORM": 349 | pos_x = get_pos_x(strip) 350 | pos_y = get_pos_y(strip) 351 | else: 352 | pos_x = strip.transform.offset_x 353 | pos_y = strip.transform.offset_y 354 | 355 | self.tab_init.append([pos_x, pos_y]) 356 | 357 | if self.tab: 358 | group_box = get_group_box(self.tab) 359 | min_left, max_right, min_bottom, max_top = group_box 360 | 361 | self.group_width = max_right - min_left 362 | self.group_height = max_top - min_bottom 363 | 364 | center_x = (min_left + (self.group_width / 2)) 365 | center_y = (min_bottom + (self.group_height / 2)) 366 | 367 | self.center_area = Vector([center_x, center_y]) 368 | 369 | # Prevents weird behavior if this op is called by 370 | # bpy.ops.vse_transform_tools.duplicate() 371 | if event.shift: 372 | self.initially_shifted = True 373 | 374 | context.window_manager.modal_handler_add(self) 375 | return {'RUNNING_MODAL'} 376 | -------------------------------------------------------------------------------- /operators/group/__init__.py: -------------------------------------------------------------------------------- 1 | from .group import PREV_OT_group 2 | -------------------------------------------------------------------------------- /operators/group/group.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..utils.geometry import get_group_box 4 | from ..utils.selection import get_input_tree 5 | 6 | class PREV_OT_group(bpy.types.Operator): 7 | bl_idname = "vse_transform_tools.group" 8 | bl_label = "Group" 9 | bl_description = "Group VSE Strips together" 10 | bl_options = {'REGISTER', 'UNDO'} 11 | 12 | @classmethod 13 | def poll(cls, context): 14 | scene = context.scene 15 | if (scene.sequence_editor and len(context.selected_sequences) > 0): 16 | return True 17 | return False 18 | 19 | def invoke(self, context, event): 20 | if event.alt: 21 | bpy.ops.sequencer.meta_separate() 22 | return {"FINISHED"} 23 | 24 | selected = context.selected_sequences 25 | 26 | for strip in selected: 27 | tree = get_input_tree(strip) 28 | for child in tree: 29 | child.select = True 30 | if not child in selected: 31 | selected.append(child) 32 | 33 | left, right, bottom, top = get_group_box(selected) 34 | 35 | res_x = context.scene.render.resolution_x 36 | res_y = context.scene.render.resolution_y 37 | 38 | bpy.ops.sequencer.meta_make() 39 | 40 | active = context.scene.sequence_editor.active_strip 41 | active.use_translation = True 42 | active.blend_type = "ALPHA_OVER" 43 | 44 | if not (left == right == bottom == top == 0): 45 | active.use_crop = True 46 | active.crop.min_x = left 47 | active.crop.max_x = res_x - right 48 | 49 | active.crop.min_y = bottom 50 | active.crop.max_y = res_y - top 51 | 52 | active.transform.offset_x = left 53 | active.transform.offset_y = bottom 54 | 55 | 56 | return {"FINISHED"} 57 | -------------------------------------------------------------------------------- /operators/meta_toggle/__init__.py: -------------------------------------------------------------------------------- 1 | from .meta_toggle import PREV_OT_meta_toggle 2 | -------------------------------------------------------------------------------- /operators/meta_toggle/meta_toggle.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ..utils.selection import get_input_tree 3 | 4 | 5 | class PREV_OT_meta_toggle(bpy.types.Operator): 6 | """ 7 | Toggles the selected strip if it is a META. If the selected strip is 8 | not a meta, recursively checks inputs until a META strip is 9 | encountered and toggles it. If no META is found, this operator does 10 | nothing. 11 | """ 12 | bl_idname = "vse_transform_tools.meta_toggle" 13 | bl_label = "Meta Toggle" 14 | bl_description = "Toggle the Meta to reveal sequences within" 15 | bl_options = {'REGISTER', 'UNDO'} 16 | 17 | @classmethod 18 | def poll(cls, context): 19 | scene = context.scene 20 | if (scene.sequence_editor): 21 | return True 22 | return False 23 | 24 | def invoke(self, context, event): 25 | scene = context.scene 26 | 27 | active = scene.sequence_editor.active_strip 28 | 29 | children = get_input_tree(active) 30 | for child in children: 31 | try: 32 | if child.type == "META": 33 | bpy.ops.sequencer.select_all(action="DESELECT") 34 | scene.sequence_editor.active_strip = child 35 | child.select = True 36 | return bpy.ops.sequencer.meta_toggle('INVOKE_DEFAULT') 37 | 38 | except AttributeError: 39 | pass 40 | 41 | return bpy.ops.sequencer.meta_toggle('INVOKE_DEFAULT') 42 | -------------------------------------------------------------------------------- /operators/mouse_track/__init__.py: -------------------------------------------------------------------------------- 1 | from .mouse_track import PREV_OT_mouse_track 2 | -------------------------------------------------------------------------------- /operators/mouse_track/mouse_track.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | import math 4 | 5 | from ..utils.geometry import mouse_to_res 6 | 7 | class PREV_OT_mouse_track(bpy.types.Operator): 8 | """ 9 | Track mouse position and apply as keyframes to a transform modifier 10 | """ 11 | bl_idname = "vse_transform_tools.mouse_track" 12 | bl_label = "Mouse Track" 13 | bl_description = "Track mouse position and apply as keyframes to a transform modifier" 14 | bl_options = {'REGISTER', 'UNDO', 'GRAB_CURSOR', 'BLOCKING'} 15 | 16 | @classmethod 17 | def poll(cls, context): 18 | scene = context.scene 19 | strip = scene.sequence_editor.active_strip 20 | if (scene.sequence_editor and strip): 21 | if strip.type == "TRANSFORM": 22 | return True 23 | elif strip.use_translation: 24 | return True 25 | return False 26 | 27 | def modal(self, context, event): 28 | scene = context.scene 29 | fps = scene.render.fps / scene.render.fps_base 30 | fc = scene.frame_current 31 | cushion = fc + math.ceil(fps / 2) 32 | 33 | strip = scene.sequence_editor.active_strip 34 | 35 | res_x = scene.render.resolution_x 36 | res_y = scene.render.resolution_y 37 | 38 | mouse_x = event.mouse_region_x 39 | mouse_y = event.mouse_region_y 40 | 41 | mouse_vec = Vector([mouse_x, mouse_y]) 42 | mouse_pos = mouse_to_res(mouse_vec) 43 | 44 | if strip.type == "TRANSFORM": 45 | x = mouse_pos.x - (res_x / 2) 46 | y = mouse_pos.y - (res_y / 2) 47 | 48 | if strip.translation_unit == 'PERCENT': 49 | x = ((mouse_pos.x * 100) / res_x) - 50 50 | y = ((mouse_pos.y * 100) / res_y) - 50 51 | 52 | strip.translate_start_x = x 53 | strip.translate_start_y = y 54 | 55 | for i in range(fc, cushion): 56 | try: 57 | strip.keyframe_delete('translate_start_x', frame=i) 58 | strip.keyframe_delete('translate_start_y', frame=i) 59 | except RuntimeError: 60 | pass 61 | 62 | strip.keyframe_insert( 63 | data_path="translate_start_x") 64 | strip.keyframe_insert( 65 | data_path="translate_start_y") 66 | 67 | else: 68 | strip.transform.offset_x = mouse_pos.x 69 | strip.transform.offset_y = mouse_pos.y 70 | 71 | for i in range(fc, cushion): 72 | try: 73 | strip.transform.keyframe_delete('offset_x', frame=i) 74 | strip.transform.keyframe_delete('offset_y', frame=i) 75 | except RuntimeError: 76 | pass 77 | 78 | strip.transform.keyframe_insert( 79 | data_path="offset_x") 80 | strip.transform.keyframe_insert( 81 | data_path="offset_y") 82 | 83 | if event.type == 'M' and event.value == 'RELEASE': 84 | return {'FINISHED'} 85 | 86 | return {'RUNNING_MODAL'} 87 | 88 | def invoke(self, context, event): 89 | self.initial_frame = context.scene.frame_current 90 | context.window_manager.modal_handler_add(self) 91 | return {'RUNNING_MODAL'} 92 | -------------------------------------------------------------------------------- /operators/pixelate/__init__.py: -------------------------------------------------------------------------------- 1 | from .pixelate import PREV_OT_pixelate 2 | -------------------------------------------------------------------------------- /operators/pixelate/draw_pixelate_controls.py: -------------------------------------------------------------------------------- 1 | from ..utils.draw import draw_line 2 | from ..utils.draw import draw_square 3 | 4 | import blf 5 | 6 | def draw_pixelate_controls(self, context): 7 | """ 8 | Draws the line, 2 boxes, and the control square 9 | """ 10 | w = context.region.width 11 | h = context.region.height 12 | line_width = 2 * (w / 10) 13 | offset_x = (line_width / 2) - (line_width * self.pixel_factor) 14 | x = self.first_mouse.x + offset_x 15 | y = self.first_mouse.y + self.pos.y 16 | 17 | v1 = [(-w / 10) + x, y] 18 | v2 = [(w / 10) + x, y] 19 | 20 | color = (0, 0.75, 1, 1) 21 | 22 | draw_line(v1, v2, 1, color) 23 | 24 | vertex = [x - (w / 10) + self.pos.x, y] 25 | draw_square(vertex, 10, color) 26 | 27 | # Numbers 28 | font_id = 0 29 | blf.position(font_id, vertex[0] - 20, vertex[1] + 10, 0) 30 | blf.size(font_id, 20, 72) 31 | blf.draw(font_id, str(self.fac)) 32 | -------------------------------------------------------------------------------- /operators/pixelate/pixelate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | 5 | from .draw_pixelate_controls import draw_pixelate_controls 6 | 7 | from ..utils.selection import get_input_tree 8 | from ..utils import process_input 9 | 10 | 11 | class PREV_OT_pixelate(bpy.types.Operator): 12 | """ 13 | Pixelate a clip by adding 2 transform effects: 1 shrinking, 14 | 1 expanding. 15 | """ 16 | bl_idname = "vse_transform_tools.pixelate" 17 | bl_label = "Pixelate" 18 | bl_description = "Pixelate a strip" 19 | bl_options = {'REGISTER', 'UNDO'} 20 | 21 | first_mouse = Vector((0, 0)) 22 | pos = Vector((0, 0)) 23 | pixel_factor = 0.0 24 | handle_pixelation = None 25 | fac = 0 26 | 27 | key_val = '' 28 | 29 | tab = [] 30 | 31 | @classmethod 32 | def poll(cls, context): 33 | scene = context.scene 34 | if (scene.sequence_editor and 35 | scene.sequence_editor.active_strip and 36 | scene.sequence_editor.active_strip.select): 37 | return True 38 | return False 39 | 40 | def modal(self, context, event): 41 | scene = context.scene 42 | res_x = scene.render.resolution_x 43 | res_y = scene.render.resolution_y 44 | 45 | context.area.tag_redraw() 46 | w = context.region.width 47 | 48 | mouse_x = event.mouse_region_x 49 | mouse_x += (self.pixel_factor * w) / 5 50 | mouse_y = event.mouse_region_y 51 | 52 | self.pos = Vector([mouse_x, mouse_y]) 53 | self.pos -= self.first_mouse 54 | 55 | if self.pos.x < 0: 56 | self.pos.x = 0.0 57 | if self.pos.x > w / 5: 58 | self.pos.x = w / 5 59 | self.fac = self.pos.x / (w / 5) 60 | 61 | process_input(self, event.type, event.value) 62 | if self.key_val != '': 63 | try: 64 | self.fac = abs(float(self.key_val)) 65 | if self.fac > 1: 66 | self.fac = abs(float('0.' + self.key_val.replace('.', ''))) 67 | self.pos.x = self.fac * (w / 5) 68 | except ValueError: 69 | pass 70 | 71 | precision = 2 72 | if event.ctrl: 73 | precision = 1 74 | 75 | self.fac = round(self.fac, precision) 76 | 77 | if (event.type == 'LEFTMOUSE' or 78 | event.type == 'RET' or 79 | event.type == 'NUMPAD_ENTER'): 80 | 81 | if self.fac > 0: 82 | selected_strips = [] 83 | for strip in context.selected_sequences: 84 | if not strip.type == 'SOUND': 85 | selected_strips.append(strip) 86 | 87 | for strip in selected_strips: 88 | channel = strip.channel 89 | 90 | bpy.ops.sequencer.select_all(action='DESELECT') 91 | scene.sequence_editor.active_strip = strip 92 | bpy.ops.sequencer.effect_strip_add(type="TRANSFORM") 93 | 94 | shrinker = context.scene.sequence_editor.active_strip 95 | shrinker.name = "SHRINKER-%s" % strip.name 96 | shrinker.interpolation = "NONE" 97 | 98 | bpy.ops.sequencer.effect_strip_add(type="TRANSFORM") 99 | expander = context.scene.sequence_editor.active_strip 100 | expander.name = "EXPANDER-%s" % strip.name 101 | expander.blend_alpha = strip.blend_alpha 102 | expander.interpolation = "NONE" 103 | expander.use_float = True 104 | 105 | tree = get_input_tree(expander)[1::] 106 | for child in tree: 107 | child.mute = True 108 | child.select = True 109 | 110 | bpy.ops.sequencer.meta_make() 111 | meta = context.scene.sequence_editor.active_strip 112 | meta.name = "PIX-%s" % strip.name 113 | meta.channel = channel 114 | meta.blend_type = "ALPHA_OVER" 115 | 116 | pixel_fac = self.fac * 100 117 | 118 | shrink_fac = (100 / pixel_fac) / 100 119 | 120 | shrink_factor_x = (100 / (res_x / math.ceil(res_x * shrink_fac))) / 100 121 | shrink_factor_y = (100 / (res_y / math.ceil(res_y * shrink_fac))) / 100 122 | 123 | shrinker.scale_start_x = shrink_factor_x 124 | shrinker.scale_start_y = shrink_factor_y 125 | 126 | expand_factor_x = 1 / shrink_factor_x 127 | expand_factor_y = 1 / shrink_factor_y 128 | 129 | expander.scale_start_x = expand_factor_x 130 | expander.scale_start_y = expand_factor_y 131 | 132 | shrunk_x = round(res_x * shrink_factor_x) 133 | if shrunk_x % 2 == 1: 134 | expander.translate_start_x = ((-1 / (shrunk_x - 1)) / 2) * 100 135 | 136 | shrunk_y = round(res_y * shrink_factor_y) 137 | if shrunk_y % 2 == 1: 138 | expander.translate_start_y = ((-1 / (shrunk_y - 1)) / 2) * 100 139 | 140 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 141 | self.handle_pixelation, 'PREVIEW') 142 | 143 | return {'FINISHED'} 144 | 145 | else: 146 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 147 | self.handle_pixelation, 'PREVIEW') 148 | return {'FINISHED'} 149 | 150 | if event.type == 'ESC' or event.type == 'RIGHTMOUSE': 151 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 152 | self.handle_pixelation, 'PREVIEW') 153 | return {'FINISHED'} 154 | 155 | return {'RUNNING_MODAL'} 156 | 157 | def invoke(self, context, event): 158 | scene = context.scene 159 | 160 | mouse_x = event.mouse_region_x 161 | mouse_y = event.mouse_region_y 162 | self.first_mouse = Vector([mouse_x, mouse_y]) 163 | 164 | self.pixel_factor = 0.0 165 | self.key_val != '' 166 | 167 | args = (self, context) 168 | self.handle_pixelation = bpy.types.SpaceSequenceEditor.draw_handler_add( 169 | draw_pixelate_controls, args, 'PREVIEW', 'POST_PIXEL') 170 | context.window_manager.modal_handler_add(self) 171 | 172 | return {'RUNNING_MODAL'} 173 | -------------------------------------------------------------------------------- /operators/rotate/__init__.py: -------------------------------------------------------------------------------- 1 | from .rotate import PREV_OT_rotate 2 | -------------------------------------------------------------------------------- /operators/rotate/apply_strip_rotation.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | 5 | from ..utils.geometry import set_pos_x 6 | from ..utils.geometry import set_pos_y 7 | from ..utils.geometry import rotate_point 8 | from ..utils.geometry import get_res_factor 9 | 10 | 11 | def apply_strip_rotation(self, strip, rot, init_rot, init_t, event): 12 | """ 13 | Update a strip's rotation & position 14 | 15 | Parameters 16 | ---------- 17 | strip : bpy.types.Sequence 18 | The transform strip that is being rotated 19 | rot : float 20 | The change in rotation since init_rot 21 | init_t : list 22 | the [x, y] position of the strip 23 | event : bpy.types.Event 24 | Allows us to check if ctrl is pressed 25 | """ 26 | flip_x = 1 27 | if strip.use_flip_x: 28 | flip_x = -1 29 | 30 | flip_y = 1 31 | if strip.use_flip_y: 32 | flip_y = -1 33 | 34 | strip_rot = init_rot + (flip_x * flip_y * rot) 35 | 36 | if event.ctrl: 37 | strip_rot = math.ceil(strip_rot / self.stepwise_increment) 38 | strip_rot *= self.stepwise_increment 39 | 40 | pivot_type = bpy.context.scene.seq_pivot_type 41 | 42 | if (pivot_type == '1' or 43 | pivot_type in ['0', '3'] and len(self.tab) == 1): 44 | strip.rotation_start = strip_rot 45 | 46 | elif pivot_type in ['0', '3'] and len(self.tab) > 1: 47 | pos_init = Vector([init_t[0], init_t[1]]) 48 | 49 | pos_flip_x = flip_x * self.center_real.x 50 | pos_flip_y = flip_y * self.center_real.y 51 | pos_flip = Vector([pos_flip_x, pos_flip_y]) 52 | 53 | pos_init -= pos_flip 54 | point_rot = flip_x * flip_y * math.radians(rot) 55 | 56 | np = rotate_point(pos_init, point_rot) 57 | if event.ctrl: 58 | p_rot_degs = math.radians(strip_rot - init_rot) 59 | point_rot = flip_x * flip_y * p_rot_degs 60 | np = rotate_point(pos_init, point_rot) 61 | 62 | pos_x = np.x + flip_x * self.center_real.x 63 | pos_x = set_pos_x(strip, pos_x) 64 | 65 | pos_y = np.y + flip_y * self.center_real.y 66 | pos_y = set_pos_y(strip, pos_y) 67 | 68 | if np.x == 0 and np.y == 0: 69 | strip.rotation_start = strip_rot 70 | 71 | else: 72 | strip.rotation_start = strip_rot 73 | strip.translate_start_x = pos_x 74 | strip.translate_start_y = pos_y 75 | 76 | elif pivot_type == '2': 77 | fac = get_res_factor() 78 | 79 | pos_x = flip_x * bpy.context.scene.seq_cursor2d_loc[0] 80 | pos_y = flip_y * bpy.context.scene.seq_cursor2d_loc[1] 81 | 82 | center_c2d = Vector([pos_x, pos_y]) 83 | center_c2d /= fac 84 | 85 | pos_init = Vector([init_t[0], init_t[1]]) 86 | pos_init -= center_c2d 87 | 88 | point_rot = flip_x * flip_y * math.radians(rot) 89 | 90 | np = rotate_point(pos_init, point_rot) 91 | if event.ctrl: 92 | p_rot_degs = math.radians(strip_rot - init_rot) 93 | point_rot = flip_x * flip_y * p_rot_degs 94 | np = rotate_point(pos_init, point_rot) 95 | 96 | strip.rotation_start = strip_rot 97 | 98 | pos_x = set_pos_x(strip, np.x + center_c2d.x) 99 | pos_y = set_pos_y(strip, np.y + center_c2d.y) 100 | 101 | strip.translate_start_x = pos_x 102 | strip.translate_start_y = pos_y 103 | -------------------------------------------------------------------------------- /operators/rotate/rotate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | 5 | from ..utils import process_input 6 | 7 | from ..utils.geometry import get_res_factor 8 | from ..utils.geometry import rotate_point 9 | from ..utils.geometry import get_pos_x 10 | from ..utils.geometry import get_pos_y 11 | from ..utils.geometry import set_pos_x 12 | from ..utils.geometry import set_pos_y 13 | 14 | from ..utils.selection import ensure_transforms 15 | 16 | from ..utils.draw import draw_px_point 17 | 18 | from .apply_strip_rotation import apply_strip_rotation 19 | 20 | 21 | class PREV_OT_rotate(bpy.types.Operator): 22 | """ 23 | Rotate selected strip(s) 24 | """ 25 | bl_idname = "vse_transform_tools.rotate" 26 | bl_label = "Rotate" 27 | bl_description = "Rotate strips in the Image Preview" 28 | bl_options = {'REGISTER', 'UNDO', 'GRAB_CURSOR', 'BLOCKING'} 29 | 30 | first_mouse = Vector([0, 0]) 31 | tab_init = [] 32 | tab_init_t = [] 33 | tab = [] 34 | 35 | center_area = Vector([0, 0]) 36 | center_real = Vector([0, 0]) 37 | 38 | mouse_pos = Vector([-1, -1]) 39 | 40 | rot_prev = 0 41 | vec_init = Vector([0, 0]) 42 | vec_prev = Vector([0, 0]) 43 | vec_act = Vector([0, 0]) 44 | 45 | key_val = '' 46 | key_period = False 47 | key_period_val = 1 48 | 49 | stepwise_increment = 5 50 | slow_factor = 10 51 | 52 | @classmethod 53 | def poll(cls, context): 54 | scene = context.scene 55 | if (scene.sequence_editor and 56 | scene.sequence_editor.active_strip): 57 | return True 58 | return False 59 | 60 | def modal(self, context, event): 61 | context.area.tag_redraw() 62 | 63 | if self.tab: 64 | self.mouse_pos = Vector([event.mouse_region_x, event.mouse_region_y]) 65 | 66 | self.vec_act = Vector([event.mouse_region_x, event.mouse_region_y]) 67 | self.vec_act -= self.center_area 68 | 69 | rot = math.degrees(-self.vec_prev.angle_signed(self.vec_act)) 70 | 71 | if event.shift: 72 | rot /= self.slow_factor 73 | 74 | self.rot_prev += rot 75 | self.rot_prev %= 360 76 | 77 | self.vec_prev = Vector(self.vec_act) 78 | 79 | rot = float(self.rot_prev) 80 | 81 | if abs(self.init_rot - (rot - 360)) < abs(self.init_rot - rot): 82 | rot = rot - 360 83 | 84 | process_input(self, event.type, event.value) 85 | if self.key_val != '': 86 | try: 87 | rot = -float(self.key_val) 88 | except ValueError: 89 | pass 90 | 91 | for i in range(len(self.tab)): 92 | strip = self.tab[i] 93 | init_rot = self.tab_init[i] 94 | init_t = self.tab_init_t[i] 95 | 96 | apply_strip_rotation( 97 | self, strip, rot, init_rot, init_t, event) 98 | 99 | if event.ctrl: 100 | rot = math.ceil(rot / self.stepwise_increment) 101 | rot *= self.stepwise_increment 102 | rot %= 360 103 | 104 | info_rot = (rot) 105 | context.area.header_text_set("Rotation %.4f " % info_rot) 106 | 107 | if (event.type == 'LEFTMOUSE' or 108 | event.type == 'RET' or 109 | event.type == 'NUMPAD_ENTER' or 110 | not self.tab): 111 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 112 | self.handle_line, 'PREVIEW') 113 | 114 | scene = context.scene 115 | if scene.tool_settings.use_keyframe_insert_auto: 116 | pivot_type = context.scene.seq_pivot_type 117 | if (pivot_type == '0' and len(self.tab) > 1) or pivot_type == '2': 118 | for strip in self.tab: 119 | strip.keyframe_insert(data_path='translate_start_x') 120 | strip.keyframe_insert(data_path='translate_start_y') 121 | strip.keyframe_insert(data_path='rotation_start') 122 | elif pivot_type == '1' or pivot_type == '3' or (pivot_type == '0' and len(self.tab) == 1): 123 | for strip in self.tab: 124 | strip.keyframe_insert(data_path='rotation_start') 125 | 126 | context.area.header_text_set(None) 127 | return {'FINISHED'} 128 | 129 | if event.type == 'ESC' or event.type == 'RIGHTMOUSE': 130 | for i in range(len(self.tab)): 131 | strip = self.tab[i] 132 | init_rot = self.tab_init[i] 133 | init_t = self.tab_init_t[i] 134 | 135 | strip.rotation_start = init_rot 136 | strip.translate_start_x = set_pos_x(strip, init_t[0]) 137 | strip.translate_start_y = set_pos_y(strip, init_t[1]) 138 | 139 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 140 | self.handle_line, 'PREVIEW') 141 | context.area.header_text_set(None) 142 | return {'FINISHED'} 143 | 144 | return {'RUNNING_MODAL'} 145 | 146 | def invoke(self, context, event): 147 | scene = context.scene 148 | view2d = context.region.view2d 149 | 150 | bpy.ops.vse_transform_tools.initialize_pivot() 151 | 152 | if event.alt: 153 | selected = ensure_transforms() 154 | for strip in selected: 155 | strip.select = True 156 | strip.rotation_start = 0.0 157 | return {'FINISHED'} 158 | 159 | else: 160 | fac = get_res_factor() 161 | self.tab_init = [] 162 | self.tab = [] 163 | self.tab_init_t = [] 164 | self.center_real = Vector([0, 0]) 165 | self.center_area = Vector([0, 0]) 166 | self.key_val = '' 167 | self.slow_additions = [] 168 | rotated_count = 0 169 | 170 | self.tab = ensure_transforms() 171 | active_strip = scene.sequence_editor.active_strip 172 | 173 | for strip in self.tab: 174 | strip.select = True 175 | pos_x = get_pos_x(strip) 176 | pos_y = get_pos_y(strip) 177 | 178 | self.tab_init.append(strip.rotation_start) 179 | self.tab_init_t.append([pos_x, pos_y]) 180 | 181 | flip_x = 1 182 | if strip.use_flip_x: 183 | flip_x = -1 184 | 185 | flip_y = 1 186 | if strip.use_flip_y: 187 | flip_y = -1 188 | 189 | self.center_real += Vector(( 190 | flip_x * pos_x, flip_y * pos_y)) 191 | self.center_area += Vector(( 192 | flip_x * pos_x, flip_y * pos_y)) 193 | 194 | rotated_count += 1 195 | 196 | if self.tab: 197 | self.center_real /= rotated_count 198 | if context.scene.seq_pivot_type == '2': 199 | cur_loc = context.scene.seq_cursor2d_loc 200 | pos = view2d.view_to_region(cur_loc[0], cur_loc[1]) 201 | self.center_area = Vector(pos) 202 | 203 | elif context.scene.seq_pivot_type == '3': 204 | flip_x = 1 205 | if strip.use_flip_x: 206 | flip_x = -1 207 | 208 | flip_y = 1 209 | if strip.use_flip_y: 210 | flip_y = -1 211 | 212 | pos_x = get_pos_x(active_strip) 213 | pos_y = get_pos_y(active_strip) 214 | 215 | self.center_real = Vector(( 216 | flip_x * pos_x, flip_y * pos_y)) 217 | 218 | pos_x *= flip_x * fac 219 | pos_y *= flip_y * fac 220 | 221 | view_2d = context.region.view2d 222 | pos = view_2d.view_to_region(pos_x, pos_y) 223 | self.center_area = Vector(pos) 224 | 225 | else: 226 | self.center_area /= rotated_count 227 | 228 | pos_x = self.center_area.x * fac 229 | pos_y = self.center_area.y * fac 230 | pos = view2d.view_to_region( 231 | pos_x, pos_y, clip=False) 232 | self.center_area = Vector(pos) 233 | 234 | self.vec_init = Vector( 235 | (event.mouse_region_x, event.mouse_region_y)) 236 | 237 | self.vec_init -= self.center_area 238 | 239 | self.vec_prev = Vector(self.vec_init) 240 | 241 | self.init_rot = active_strip.rotation_start 242 | 243 | args = (self, context) 244 | self.handle_line = bpy.types.SpaceSequenceEditor.draw_handler_add( 245 | draw_px_point, args, 'PREVIEW', 'POST_PIXEL') 246 | context.window_manager.modal_handler_add(self) 247 | 248 | return {'RUNNING_MODAL'} 249 | -------------------------------------------------------------------------------- /operators/scale/__init__.py: -------------------------------------------------------------------------------- 1 | from .scale import PREV_OT_scale 2 | -------------------------------------------------------------------------------- /operators/scale/scale.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | 4 | from ..utils.geometry import get_pos_x 5 | from ..utils.geometry import get_pos_y 6 | from ..utils.geometry import set_pos_x 7 | from ..utils.geometry import set_pos_y 8 | from ..utils.geometry import get_res_factor 9 | from ..utils.geometry import get_group_box 10 | from ..utils.geometry import get_preview_offset 11 | 12 | from ..utils import func_constrain_axis_mmb 13 | from ..utils import func_constrain_axis 14 | from ..utils import process_input 15 | 16 | from ..utils.selection import get_visible_strips 17 | from ..utils.selection import ensure_transforms 18 | from ..utils.selection import get_highest_transform 19 | 20 | from ..utils.draw import draw_px_point 21 | from ..utils.draw import draw_snap 22 | 23 | class PREV_OT_scale(bpy.types.Operator): 24 | """ 25 | ![Demo](https://i.imgur.com/oAxSEYB.gif) 26 | """ 27 | bl_idname = "vse_transform_tools.scale" 28 | bl_label = "Scale" 29 | bl_description = "Scale strips in Image Preview Window" 30 | bl_options = {'REGISTER', 'UNDO', 'GRAB_CURSOR', 'BLOCKING'} 31 | 32 | axis_x = True 33 | axis_y = True 34 | choose_axis = False 35 | 36 | first_mouse = Vector([0, 0]) 37 | pos_clic = Vector([0, 0]) 38 | mouse_pos = Vector([-1, -1]) 39 | 40 | vec_init = Vector([0, 0]) 41 | vec_act = Vector([0, 0]) 42 | vec_prev = Vector([0, 0]) 43 | 44 | center_c2d = Vector([0, 0]) 45 | 46 | center_area = Vector([0, 0]) 47 | center_real = Vector([0, 0]) 48 | 49 | key_val = '' 50 | 51 | handle_axes = None 52 | handle_line = None 53 | handle_snap = None 54 | 55 | group_width = 0 56 | group_height = 0 57 | 58 | slow_factor = 10 59 | scale_prev = 1.0 60 | 61 | horizontal_interests = [] 62 | vertical_interests = [] 63 | 64 | original_group_box = [Vector([0, 0]), Vector([0, 0]), Vector([0, 0]), Vector([0, 0])] 65 | 66 | last_snap_orientation = "" 67 | last_line_loc = None 68 | orientation_conflict_winner = 0 69 | 70 | @classmethod 71 | def poll(cls, context): 72 | scene = context.scene 73 | if (scene.sequence_editor and 74 | scene.sequence_editor.active_strip): 75 | return True 76 | return False 77 | 78 | def modal(self, context, event): 79 | scene = context.scene 80 | 81 | res_x = scene.render.resolution_x 82 | res_y = scene.render.resolution_y 83 | 84 | if self.tab: 85 | self.mouse_pos = Vector([event.mouse_region_x, event.mouse_region_y]) 86 | self.vec_act = self.mouse_pos - self.center_area 87 | 88 | addition = (self.vec_act.length - self.vec_prev.length) / self.vec_init.length 89 | if event.shift: 90 | addition /= self.slow_factor 91 | 92 | self.scale_prev += addition 93 | 94 | diff = float(self.scale_prev) 95 | 96 | self.vec_prev = Vector(self.vec_act) 97 | 98 | func_constrain_axis_mmb( 99 | self, context, event.type, event.value, 100 | self.sign_rot * context.scene.sequence_editor.active_strip.rotation_start) 101 | 102 | func_constrain_axis( 103 | self, context, event.type, event.value, 104 | self.sign_rot * context.scene.sequence_editor.active_strip.rotation_start) 105 | 106 | process_input(self, event.type, event.value) 107 | if self.key_val != '': 108 | try: 109 | diff = abs(float(self.key_val)) 110 | except ValueError: 111 | pass 112 | 113 | diff_x = 1 114 | if self.axis_x: 115 | diff_x = diff 116 | 117 | diff_y = 1 118 | if self.axis_y: 119 | diff_y = diff 120 | 121 | precision = 5 122 | snap_distance = int(max([res_x, res_y]) / 100) 123 | if event.ctrl: 124 | 125 | if context.scene.seq_pivot_type == '2': 126 | origin_point = Vector([self.center_c2d.x + (res_x / 2), self.center_c2d.y + (res_y / 2)]) 127 | else: 128 | origin_x = self.center_real.x + (res_x / 2) 129 | origin_y = self.center_real.y + (res_y / 2) 130 | origin_point = Vector([origin_x, origin_y]) 131 | 132 | orig_left = self.original_group_box[0] 133 | trans_diff_l = (((origin_point.x - orig_left) * diff_x) - (origin_point.x - orig_left)) 134 | current_left = orig_left - trans_diff_l 135 | 136 | orig_right = self.original_group_box[1] 137 | trans_diff_r = (((origin_point.x - orig_right) * diff_x) - (origin_point.x - orig_right)) 138 | current_right = orig_right - trans_diff_r 139 | 140 | orig_bottom = self.original_group_box[2] 141 | trans_diff_b = (((origin_point.y - orig_bottom) * diff_y) - (origin_point.y - orig_bottom)) 142 | current_bottom = orig_bottom - trans_diff_b 143 | 144 | orig_top = self.original_group_box[3] 145 | trans_diff_t = (((origin_point.y - orig_top) * diff_y) - (origin_point.y - orig_top)) 146 | current_top = orig_top - trans_diff_t 147 | 148 | orientations = [] 149 | line_locs = [] 150 | offset_x, offset_y, fac, preview_zoom = get_preview_offset() 151 | 152 | for line in self.horizontal_interests: 153 | 154 | if (current_left < line + snap_distance and 155 | current_left > line - snap_distance and 156 | abs(origin_point.x - orig_left) > snap_distance): 157 | scale_to_line = (origin_point.x - line) / (origin_point.x - orig_left) 158 | 159 | if self.axis_y: 160 | diff_y *= (scale_to_line / diff_x) 161 | diff_x = scale_to_line 162 | 163 | line_locs.append((line * fac * preview_zoom) + offset_x) 164 | orientations.append("VERTICAL") 165 | 166 | break 167 | 168 | if (current_right > line - snap_distance and 169 | current_right < line + snap_distance and 170 | abs(origin_point.x - orig_right) > snap_distance): 171 | scale_to_line = (origin_point.x - line) / (origin_point.x - orig_right) 172 | 173 | if self.axis_y: 174 | diff_y *= (scale_to_line / diff_x) 175 | diff_x = scale_to_line 176 | 177 | line_locs.append((line * fac * preview_zoom) + offset_x) 178 | orientations.append("VERTICAL") 179 | 180 | for line in self.vertical_interests: 181 | if (current_bottom < line + snap_distance and 182 | current_bottom > line - snap_distance and 183 | abs(origin_point.y - orig_bottom) > snap_distance): 184 | scale_to_line = (origin_point.y - line) / (origin_point.y - orig_bottom) 185 | 186 | if self.axis_x: 187 | diff_x *= (scale_to_line / diff_y) 188 | diff_y = scale_to_line 189 | 190 | line_locs.append((line * fac * preview_zoom) + offset_y) 191 | orientations.append("HORIZONTAL") 192 | 193 | break 194 | 195 | if (current_top > line - snap_distance and 196 | current_top < line + snap_distance and 197 | abs(origin_point.y - orig_top) > snap_distance): 198 | 199 | scale_to_line = (origin_point.y - line) / (origin_point.y - orig_top) 200 | if self.axis_x: 201 | diff_x *= (scale_to_line / diff_y) 202 | diff_y = scale_to_line 203 | 204 | line_locs.append((line * fac * preview_zoom) + offset_y) 205 | orientations.append("HORIZONTAL") 206 | 207 | orientation = "" 208 | line_loc = None 209 | if len(orientations) > 1 and self.last_snap_orientation != "" and self.orientation_conflict_winner == -1: 210 | index = orientations.index(self.last_snap_orientation) 211 | orientations.pop(index) 212 | line_locs.pop(index) 213 | 214 | self.orientation_conflict_winner = int(not index) 215 | 216 | orientation = orientations[0] 217 | line_loc = line_locs[0] 218 | 219 | elif len(orientations) > 1 and self.last_snap_orientation == "" and self.orientation_conflict_winner == -1: 220 | self.orientation_conflict_winner = 0 221 | orientation = orientations[0] 222 | line_loc = line_locs[0] 223 | 224 | elif len(orientations) > 1: 225 | orientation = orientations[self.orientation_conflict_winner] 226 | line_loc = line_locs[self.orientation_conflict_winner] 227 | 228 | elif len(orientations) > 0: 229 | self.orientation_conflict_winner = 0 230 | orientation = orientations[0] 231 | line_loc = line_locs[0] 232 | 233 | if orientation != "" and (self.handle_snap == None or "RNA_HANDLE_REMOVED" in str(self.handle_snap)): 234 | args = (self, line_loc, orientation) 235 | self.handle_snap = bpy.types.SpaceSequenceEditor.draw_handler_add( 236 | draw_snap, args, 'PREVIEW', 'POST_PIXEL') 237 | self.last_snap_orientation = orientation 238 | self.last_line_loc = line_loc 239 | elif (orientation != self.last_snap_orientation or line_loc != self.last_line_loc) and self.handle_snap != None and not "RNA_HANDLE_REMOVED" in str(self.handle_snap): 240 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_snap, 'PREVIEW') 241 | self.last_snap_orientation = orientation 242 | self.last_line_loc = line_loc 243 | 244 | elif self.handle_snap != None and not "RNA_HANDLE_REMOVED" in str(self.handle_snap): 245 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_snap, 'PREVIEW') 246 | 247 | info_x = round(diff_x, precision) 248 | info_y = round(diff_y, precision) 249 | if not self.axis_x: 250 | context.area.header_text_set("Scale: %.4f along local Y" % info_y) 251 | if not self.axis_y: 252 | context.area.header_text_set("Scale: %.4f along local X" % info_x) 253 | if self.axis_x and self.axis_y : 254 | context.area.header_text_set("Scale X:%.4f Y: %.4f" % (info_x, info_y)) 255 | 256 | for strip, init_s, init_t in zip(self.tab, self.tab_init_s, self.tab_init_t): 257 | strip.scale_start_x = init_s[0] * round(diff_x, precision) 258 | strip.scale_start_y = init_s[1] * round(diff_y, precision) 259 | 260 | flip_x = 1 261 | if strip.use_flip_x: 262 | flip_x = -1 263 | 264 | flip_y = 1 265 | if strip.use_flip_y: 266 | flip_y = -1 267 | 268 | if context.scene.seq_pivot_type in ['0', '3']: 269 | strip.translate_start_x = set_pos_x(strip, (init_t[0] - flip_x * self.center_real.x) * round(diff_x, precision) + flip_x * self.center_real.x) 270 | strip.translate_start_y = set_pos_y(strip, (init_t[1] - flip_y * self.center_real.y) * round(diff_y, precision) + flip_y * self.center_real.y) 271 | 272 | if context.scene.seq_pivot_type == '2': 273 | strip.translate_start_x = set_pos_x(strip, (init_t[0] - self.center_c2d.x) * round(diff_x, precision) + self.center_c2d.x) 274 | strip.translate_start_y = set_pos_y(strip, (init_t[1] - self.center_c2d.y) * round(diff_y, precision) + self.center_c2d.y) 275 | 276 | if (event.type == 'LEFTMOUSE' or 277 | event.type == 'RET' or 278 | event.type == 'NUMPAD_ENTER' or 279 | not self.tab): 280 | 281 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_line, 'PREVIEW') 282 | 283 | if self.handle_snap != None and not "RNA_HANDLE_REMOVED" in str(self.handle_snap): 284 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_snap, 'PREVIEW') 285 | 286 | scene = context.scene 287 | if scene.tool_settings.use_keyframe_insert_auto: 288 | pivot_type = context.scene.seq_pivot_type 289 | if (pivot_type == '0' and len(self.tab) > 1) or pivot_type == '2': 290 | for strip in self.tab: 291 | strip.keyframe_insert(data_path='translate_start_x') 292 | strip.keyframe_insert(data_path='translate_start_y') 293 | strip.keyframe_insert(data_path='scale_start_x') 294 | strip.keyframe_insert(data_path='scale_start_y') 295 | elif pivot_type == '1' or pivot_type == '3' or (pivot_type == '0' and len(self.tab) == 1): 296 | for strip in self.tab: 297 | strip.keyframe_insert(data_path='scale_start_x') 298 | strip.keyframe_insert(data_path='scale_start_y') 299 | 300 | if self.handle_axes: 301 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_axes, 'PREVIEW') 302 | context.area.header_text_set(None) 303 | return {'FINISHED'} 304 | 305 | if event.type == 'ESC' or event.type == 'RIGHTMOUSE': 306 | for strip, init_s, init_t in zip(self.tab, self.tab_init_s, self.tab_init_t): 307 | strip.scale_start_x = init_s[0] 308 | strip.scale_start_y = init_s[1] 309 | strip.translate_start_x = set_pos_x(strip, init_t[0]) 310 | strip.translate_start_y = set_pos_y(strip, init_t[1]) 311 | 312 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_line, 'PREVIEW') 313 | 314 | if self.handle_snap != None and not "RNA_HANDLE_REMOVED" in str(self.handle_snap): 315 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_snap, 'PREVIEW') 316 | 317 | if self.handle_axes: 318 | bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle_axes, 'PREVIEW') 319 | context.area.header_text_set(None) 320 | return {'FINISHED'} 321 | 322 | else: 323 | return {'FINISHED'} 324 | return {'RUNNING_MODAL'} 325 | 326 | def invoke(self, context, event): 327 | scene = context.scene 328 | bpy.ops.vse_transform_tools.initialize_pivot() 329 | 330 | res_x = scene.render.resolution_x 331 | res_y = scene.render.resolution_y 332 | 333 | self.horizontal_interests = [0, res_x] 334 | self.vertical_interests = [0, res_y] 335 | 336 | if event.alt : 337 | selected = context.selected_sequences 338 | for strip in selected: 339 | transform = get_highest_transform(strip) 340 | if transform.type == 'TRANSFORM': 341 | reset_transform_scale(transform) 342 | else: 343 | transform.use_translation = True 344 | transform.blend_type = 'ALPHA_OVER' 345 | 346 | return {'FINISHED'} 347 | 348 | else: 349 | self.tab_init_s = [] 350 | self.tab_init_t = [] 351 | 352 | self.tab = [] 353 | 354 | self.center_real = Vector([0, 0]) 355 | self.center_area = Vector([0, 0]) 356 | 357 | self.key_val = '' 358 | self.old_key_val = '0' 359 | 360 | fac = get_res_factor() 361 | 362 | scaled_count = 0 363 | 364 | self.tab = ensure_transforms() 365 | visible_strips = get_visible_strips() 366 | 367 | for strip in visible_strips: 368 | if strip not in self.tab: 369 | left, right, bottom, top = get_group_box([strip]) 370 | 371 | self.horizontal_interests.append(left) 372 | self.horizontal_interests.append(right) 373 | 374 | self.vertical_interests.append(bottom) 375 | self.vertical_interests.append(top) 376 | 377 | for strip in self.tab: 378 | strip.select = True 379 | self.tab_init_s.append([strip.scale_start_x, strip.scale_start_y]) 380 | self.tab_init_t.append([get_pos_x(strip), get_pos_y(strip)]) 381 | 382 | flip_x = 1 383 | if strip.use_flip_x: 384 | flip_x = -1 385 | 386 | flip_y = 1 387 | if strip.use_flip_y: 388 | flip_y = -1 389 | 390 | self.sign_rot = flip_x * flip_y 391 | 392 | center_x = flip_x * get_pos_x(strip) 393 | center_y = flip_y * get_pos_y(strip) 394 | self.center_real += Vector([center_x, center_y]) 395 | self.center_area += Vector([center_x, center_y]) 396 | 397 | if len(self.tab) > 0: 398 | self.center_real /= len(self.tab) 399 | self.original_group_box = get_group_box(self.tab) 400 | if scene.seq_pivot_type == '2': 401 | cursor_x = context.scene.seq_cursor2d_loc[0] 402 | cursor_y = context.scene.seq_cursor2d_loc[1] 403 | cursor_pos = context.region.view2d.view_to_region(cursor_x, cursor_y) 404 | self.center_area = Vector(cursor_pos) 405 | 406 | self.center_c2d = Vector((flip_x * context.scene.seq_cursor2d_loc[0], flip_y * context.scene.seq_cursor2d_loc[1])) / fac 407 | 408 | elif scene.seq_pivot_type == '3': 409 | active_strip = scene.sequence_editor.active_strip 410 | 411 | flip_x = 1 412 | if active_strip.use_flip_x: 413 | flip_x = -1 414 | 415 | flip_y = 1 416 | if active_strip.use_flip_y: 417 | flip_y = -1 418 | 419 | pos_x = flip_x * get_pos_x(active_strip) 420 | pos_y = flip_y * get_pos_y(active_strip) 421 | 422 | self.center_real = Vector([pos_x, pos_y]) 423 | 424 | pos = context.region.view2d.view_to_region(pos_x * fac, pos_y * fac) 425 | self.center_area = Vector(pos) 426 | 427 | else: 428 | self.center_area /= len(self.tab) 429 | 430 | pos_x = self.center_area.x * fac 431 | pos_y = self.center_area.y * fac 432 | pos = context.region.view2d.view_to_region(pos_x, pos_y,clip=False) 433 | self.center_area = Vector(pos) 434 | 435 | self.vec_init = Vector( 436 | (event.mouse_region_x, event.mouse_region_y)) 437 | self.vec_init -= self.center_area 438 | 439 | self.vec_prev = Vector(self.vec_init) 440 | 441 | args = (self, context) 442 | self.handle_line = bpy.types.SpaceSequenceEditor.draw_handler_add( 443 | draw_px_point, args, 'PREVIEW', 'POST_PIXEL') 444 | 445 | context.window_manager.modal_handler_add(self) 446 | return {'RUNNING_MODAL'} 447 | return {'FINISHED'} 448 | 449 | 450 | def reset_transform_scale(strip): 451 | """Reset a strip to it's factor""" 452 | strip_in = strip.input_1 453 | res_x = bpy.context.scene.render.resolution_x 454 | res_y = bpy.context.scene.render.resolution_y 455 | 456 | if hasattr(strip_in, 'elements'): 457 | len_crop_x = strip_in.elements[0].orig_width 458 | len_crop_y = strip_in.elements[0].orig_height 459 | 460 | if strip_in.use_crop: 461 | len_crop_x -= (strip_in.crop.min_x + strip_in.crop.max_x) 462 | len_crop_y -= (strip_in.crop.min_y + strip_in.crop.max_y) 463 | 464 | ratio_x = len_crop_x / res_x 465 | ratio_y = len_crop_y / res_y 466 | 467 | strip.scale_start_x = ratio_x 468 | strip.scale_start_y = ratio_y 469 | 470 | elif strip_in.type == "SCENE": 471 | strip_scene = strip_in.scene 472 | 473 | ratio_x = strip_scene.render.resolution_x / res_x 474 | ratio_y = strip_scene.render.resolution_y / res_y 475 | 476 | strip.scale_start_x = ratio_x 477 | strip.scale_start_y = ratio_y 478 | 479 | else: 480 | strip.scale_start_x = 1.0 481 | strip.scale_start_y = 1.0 482 | -------------------------------------------------------------------------------- /operators/select/__init__.py: -------------------------------------------------------------------------------- 1 | from .select import PREV_OT_select 2 | -------------------------------------------------------------------------------- /operators/select/select.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from mathutils import Vector 4 | from mathutils.geometry import intersect_point_quad_2d 5 | 6 | from ..utils.geometry import get_strip_corners 7 | from ..utils.geometry import get_preview_offset 8 | from ..utils.geometry import mouse_to_res 9 | 10 | from ..utils.selection import get_visible_strips 11 | 12 | from ..utils.geometry import get_preview_offset 13 | from ..utils.geometry import get_strip_corners 14 | 15 | from ..utils.draw import draw_line 16 | 17 | def draw_select(context, seconds, fadeout_duration): 18 | active_color = context.preferences.themes[0].sequence_editor.active_strip 19 | select_color = context.preferences.themes[0].sequence_editor.selected_strip 20 | outline_color = (0, 0, 0, 0.2) 21 | 22 | opacity = 1 - (seconds / fadeout_duration) 23 | 24 | active_strip = context.scene.sequence_editor.active_strip 25 | 26 | offset_x, offset_y, fac, preview_zoom = get_preview_offset() 27 | 28 | for strip in context.selected_sequences: 29 | if strip == active_strip: 30 | color = (active_color[0], active_color[1], active_color[2], opacity) 31 | else: 32 | color = (select_color[0], select_color[1], select_color[2], opacity) 33 | 34 | corners = get_strip_corners(strip) 35 | vertices = [] 36 | for corner in corners: 37 | corner_x = int(corner[0] * preview_zoom * fac) + offset_x 38 | corner_y = int(corner[1] * preview_zoom * fac) + offset_y 39 | vertices.append([corner_x, corner_y]) 40 | 41 | draw_line(vertices[0], vertices[1], 2, outline_color) 42 | draw_line(vertices[1], vertices[2], 2, outline_color) 43 | draw_line(vertices[2], vertices[3], 2, outline_color) 44 | draw_line(vertices[3], vertices[0], 2, outline_color) 45 | 46 | draw_line(vertices[0], vertices[1], 1, color) 47 | draw_line(vertices[1], vertices[2], 1, color) 48 | draw_line(vertices[2], vertices[3], 1, color) 49 | draw_line(vertices[3], vertices[0], 1, color) 50 | 51 | 52 | class PREV_OT_select(bpy.types.Operator): 53 | """ 54 | Selects a strip(s) when clicked 55 | """ 56 | bl_idname = "vse_transform_tools.select" 57 | bl_label = "Select" 58 | bl_description = "Select visible sequences from the Image Preview" 59 | 60 | timer = None 61 | seconds = 0 62 | fadeout_duration = 100 63 | handle_select = None 64 | 65 | @classmethod 66 | def poll(self, context): 67 | if context.scene.sequence_editor: 68 | return True 69 | return False 70 | 71 | def modal(self, context, event): 72 | context.area.tag_redraw() 73 | 74 | if event.type == 'TIMER': 75 | self.seconds += 0.01 76 | 77 | if self.seconds > self.fadeout_duration: 78 | context.window_manager.event_timer_remove(self.timer) 79 | 80 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 81 | self.handle_select, 'PREVIEW') 82 | 83 | return {'FINISHED'} 84 | 85 | return {'PASS_THROUGH'} 86 | 87 | def invoke(self, context, event): 88 | #bpy.ops.vse_transform_tools.initialize_pivot() 89 | 90 | scene = context.scene 91 | 92 | mouse_x = event.mouse_region_x 93 | mouse_y = event.mouse_region_y 94 | 95 | mouse_vec = Vector([mouse_x, mouse_y]) 96 | vector = mouse_to_res(mouse_vec) 97 | 98 | current_frame = scene.frame_current 99 | current_strips = [] 100 | 101 | sequence_editor = scene.sequence_editor 102 | selection_list = [] 103 | 104 | strips = get_visible_strips() 105 | 106 | if 'MOUSE' in event.type: 107 | for strip in reversed(strips): 108 | 109 | corners = get_strip_corners(strip) 110 | 111 | bottom_left = Vector(corners[0]) 112 | top_left = Vector(corners[1]) 113 | top_right = Vector(corners[2]) 114 | bottom_right = Vector(corners[3]) 115 | 116 | intersects = intersect_point_quad_2d( 117 | vector, bottom_left, top_left, top_right, 118 | bottom_right) 119 | 120 | if intersects and not event.type == 'A': 121 | selection_list.append(strip) 122 | if not event.shift: 123 | bpy.ops.sequencer.select_all(action='DESELECT') 124 | strip.select = True 125 | scene.sequence_editor.active_strip = strip 126 | break 127 | else: 128 | if not strip.select: 129 | strip.select = True 130 | scene.sequence_editor.active_strip = strip 131 | break 132 | else: 133 | strip.select = True 134 | break 135 | if not selection_list and not event.shift and not event.type == 'A': 136 | bpy.ops.sequencer.select_all(action='DESELECT') 137 | 138 | if strip.blend_type in ['CROSS', 'REPLACE']: 139 | return {'FINISHED'} 140 | 141 | elif event.type == 'A': 142 | all_selected = True 143 | for strip in strips: 144 | if not strip.select: 145 | all_selected = False 146 | 147 | bpy.ops.sequencer.select_all(action='DESELECT') 148 | 149 | if not all_selected: 150 | for strip in strips: 151 | strip.select = True 152 | 153 | args = (context, self.seconds, self.fadeout_duration) 154 | self.handle_select = bpy.types.SpaceSequenceEditor.draw_handler_add( 155 | draw_select, args, 'PREVIEW', 'POST_PIXEL') 156 | 157 | self.timer = context.window_manager.event_timer_add(0.01, window=context.window) 158 | 159 | context.window_manager.modal_handler_add(self) 160 | 161 | return {'RUNNING_MODAL'} 162 | -------------------------------------------------------------------------------- /operators/set_cursor2d/__init__.py: -------------------------------------------------------------------------------- 1 | from .set_cursor2d import PREV_OT_set_cursor_2d 2 | -------------------------------------------------------------------------------- /operators/set_cursor2d/get_important_edge_points.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | 5 | from ..utils.selection import get_visible_strips 6 | 7 | from ..utils.geometry import rotate_point 8 | from ..utils.geometry import get_transform_box 9 | from ..utils.geometry import get_strip_box 10 | from ..utils.geometry import get_strip_corners 11 | 12 | 13 | def get_important_edge_points(): 14 | """ 15 | Get the edge locations for where a user may want to snap the cursor 16 | to. 17 | 18 | This is the top left, top middle, top right, right middle, bottom 19 | right, bottom middle, bottom left, left middle, and center points of 20 | all visible strips in the preview window.:: 21 | 22 | x------x------x 23 | | | 24 | x x x 25 | | | 26 | x------x------x 27 | 28 | Returns 29 | ------- 30 | list of mathutils.Vector 31 | """ 32 | scene = bpy.context.scene 33 | 34 | strips = get_visible_strips() 35 | 36 | res_x = scene.render.resolution_x 37 | res_y = scene.render.resolution_y 38 | 39 | bl = Vector([0, 0]) 40 | l = Vector([0, res_y / 2]) 41 | tl = Vector([0, res_y]) 42 | t = Vector([res_x / 2, res_y]) 43 | tr = Vector([res_x, res_y]) 44 | r = Vector([res_x, res_y / 2]) 45 | br = Vector([res_x, 0]) 46 | b = Vector([res_x / 2, 0]) 47 | origin = Vector([res_x / 2, res_y / 2]) 48 | 49 | vectors = [bl, l, tl, t, tr, r, br, b, origin] 50 | 51 | for vec in vectors: 52 | vec.x -= (res_x / 2) 53 | vec.y -= (res_y / 2) 54 | 55 | 56 | important_edge_points = vectors 57 | for strip in strips: 58 | if strip.type == "TRANSFORM": 59 | left, right, bottom, top = get_transform_box(strip) 60 | 61 | else: 62 | left, right, bottom, top = get_strip_box(strip) 63 | 64 | mid_x = left+ ((right - left) / 2) 65 | mid_y = bottom + ((top - bottom) / 2) 66 | 67 | l = Vector([left, mid_y]) 68 | r = Vector([right, mid_y]) 69 | t = Vector([mid_x, top]) 70 | b = Vector([mid_x, bottom]) 71 | 72 | origin = Vector([mid_x, mid_y]) 73 | 74 | if strip.type == "TRANSFORM": 75 | angle = math.radians(strip.rotation_start) 76 | 77 | l = rotate_point(l, angle, origin=origin) 78 | r = rotate_point(r, angle, origin=origin) 79 | t = rotate_point(t, angle, origin=origin) 80 | b = rotate_point(b, angle, origin=origin) 81 | 82 | bl, tl, tr, br = get_strip_corners(strip) 83 | 84 | vectors = [bl, l, tl, t, tr, r, br, b, origin] 85 | 86 | for vec in vectors: 87 | vec.x -= (res_x / 2) 88 | vec.y -= (res_y / 2) 89 | 90 | important_edge_points.extend(vectors) 91 | 92 | return important_edge_points 93 | -------------------------------------------------------------------------------- /operators/set_cursor2d/set_cursor2d.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | 5 | from .get_important_edge_points import get_important_edge_points 6 | 7 | 8 | class PREV_OT_set_cursor_2d(bpy.types.Operator): 9 | """ 10 | Set the pivot point (point of origin) location. This will affect 11 | how strips are rotated and scaled. 12 | """ 13 | bl_label = "Set Cursor2D" 14 | bl_idname = "vse_transform_tools.set_cursor2d" 15 | bl_description = "Set the pivot point location" 16 | 17 | @classmethod 18 | def poll(cls, context): 19 | if (context.scene.sequence_editor and 20 | context.scene.seq_pivot_type == '2'): 21 | return True 22 | return False 23 | 24 | def invoke(self, context, event): 25 | bpy.ops.vse_transform_tools.initialize_pivot() 26 | 27 | mouse_x = event.mouse_region_x 28 | mouse_y = event.mouse_region_y 29 | 30 | pos = context.region.view2d.region_to_view(mouse_x, mouse_y) 31 | mouse_pos = Vector(pos) 32 | 33 | if event.ctrl: 34 | snap_points = get_important_edge_points() 35 | point = min(snap_points, key = lambda x: (x - mouse_pos).length) 36 | point.x = round(point.x) 37 | point.y = round(point.y) 38 | 39 | context.scene.seq_cursor2d_loc = [int(point.x), int(point.y)] 40 | 41 | else: 42 | context.scene.seq_cursor2d_loc = [round(pos[0]), round(pos[1])] 43 | 44 | return {'PASS_THROUGH'} 45 | -------------------------------------------------------------------------------- /operators/track_transform/__init__.py: -------------------------------------------------------------------------------- 1 | from .track_transform import SEQUENCER_OT_track_transform 2 | -------------------------------------------------------------------------------- /operators/track_transform/track_transform.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ..utils.selection import get_input_tree 3 | import math 4 | 5 | 6 | class SEQUENCER_OT_track_transform(bpy.types.Operator): 7 | """ 8 | Use a pair of track points to pin a strip to another. The UI for 9 | this tool is located in the menu to the right of the sequencer in 10 | the "Tools" submenu. 11 | 12 | ![UI](https://i.imgur.com/wEZLu8a.jpg) 13 | 14 | To pin rotation and/or scale, you must use 2 tracking points. 15 | 16 | More information on [this youtube video](https://www.youtube.com/watch?v=X885Uv1dzFY) 17 | """ 18 | bl_idname = "vse_transform_tools.track_transform" 19 | bl_label = "Track Transform" 20 | bl_description = "Pin selected transform strip to tracker(s)" 21 | bl_options = {"REGISTER", "UNDO"} 22 | 23 | @classmethod 24 | def poll(cls, context): 25 | scene = context.scene 26 | 27 | if (scene.vse_transform_tools_tracker_1 != "None" and 28 | scene.sequence_editor and 29 | scene.sequence_editor.active_strip and 30 | scene.sequence_editor.active_strip.type == "TRANSFORM"): 31 | return True 32 | return False 33 | 34 | def execute(self, context): 35 | scene = context.scene 36 | res_x = scene.render.resolution_x 37 | res_y = scene.render.resolution_y 38 | 39 | tracker_names = [] 40 | 41 | for movieclip in bpy.data.movieclips: 42 | for track in movieclip.tracking.tracks: 43 | if track.name == scene.vse_transform_tools_tracker_1: 44 | pos_track = track 45 | break 46 | 47 | start_frame = scene.frame_current 48 | 49 | offset_x = -1 50 | offset_y = -1 51 | for marker in pos_track.markers: 52 | if marker.frame == scene.frame_current: 53 | offset_x = marker.co.x * res_x 54 | offset_y = marker.co.y * res_y 55 | break 56 | 57 | if offset_x == -1 or offset_y == -1: 58 | offset_x = pos_track.markers[0].co.x * res_x 59 | offset_y = pos_track.markers[0].co.y * res_y 60 | 61 | active = scene.sequence_editor.active_strip 62 | 63 | if active.translation_unit == "PERCENT": 64 | active.translate_start_x = (active.translate_start_x - ((offset_x / res_x) * 100) + 50) / 2 65 | active.translate_start_y = (active.translate_start_y - ((offset_y / res_y) * 100) + 50) / 2 66 | 67 | else: 68 | active.translate_start_x = ((active.translate_start_x + (res_x / 2)) - offset_x) / 2 69 | active.translate_start_y = ((active.translate_start_y + (res_y / 2)) - offset_y) / 2 70 | 71 | active.scale_start_x = active.scale_start_x / 2 72 | active.scale_start_y = active.scale_start_y / 2 73 | 74 | for strip in context.selected_sequences: 75 | if not strip == active: 76 | strip.select = False 77 | 78 | bpy.ops.sequencer.effect_strip_add(type="TRANSFORM") 79 | 80 | transform_strip = context.scene.sequence_editor.active_strip 81 | transform_strip.name = "[TRACKED]-%s" % strip.name 82 | transform_strip.blend_type = 'ALPHA_OVER' 83 | transform_strip.use_uniform_scale = True 84 | transform_strip.scale_start_x = 2.0 85 | 86 | tree = get_input_tree(transform_strip)[1::] 87 | for child in tree: 88 | child.mute = True 89 | 90 | for marker in pos_track.markers: 91 | scene.frame_current = marker.frame 92 | transform_strip.translate_start_x = ((((marker.co.x * res_x) - (res_x / 2)) / res_x) * 100) 93 | transform_strip.translate_start_y = ((((marker.co.y * res_y) - (res_y / 2)) / res_y) * 100) 94 | 95 | transform_strip.keyframe_insert( 96 | data_path="translate_start_x", frame=scene.frame_current) 97 | transform_strip.keyframe_insert( 98 | data_path="translate_start_y", frame=scene.frame_current) 99 | 100 | ref_track = None 101 | if scene.vse_transform_tools_use_rotation or scene.vse_transform_tools_use_scale: 102 | for movieclip in bpy.data.movieclips: 103 | for track in movieclip.tracking.tracks: 104 | if track.name == scene.vse_transform_tools_tracker_2: 105 | ref_track = track 106 | break 107 | 108 | if scene.vse_transform_tools_use_rotation and ref_track: 109 | for marker in ref_track.markers: 110 | if marker.frame == start_frame: 111 | 112 | p1 = None 113 | for pos_marker in pos_track.markers: 114 | if pos_marker.frame == marker.frame: 115 | p1 = (pos_marker.co.x * res_x, pos_marker.co.y * res_y) 116 | break 117 | 118 | if not p1: 119 | return {"FINISHED"} 120 | 121 | p2 = (marker.co.x * res_x, marker.co.y * res_y) 122 | offset_angle = calculate_angle(p1, p2) 123 | break 124 | 125 | for marker in ref_track.markers: 126 | scene.frame_current = marker.frame 127 | p1 = None 128 | for pos_marker in pos_track.markers: 129 | if pos_marker.frame == marker.frame: 130 | p1 = (pos_marker.co.x * res_x, pos_marker.co.y * res_y) 131 | break 132 | if not p1: 133 | break 134 | 135 | p2 = (marker.co.x * res_x, marker.co.y * res_y) 136 | angle = calculate_angle(p1, p2) - offset_angle 137 | 138 | transform_strip.rotation_start = angle 139 | transform_strip.keyframe_insert( 140 | data_path="rotation_start", frame=scene.frame_current) 141 | 142 | if scene.vse_transform_tools_use_scale and ref_track: 143 | for marker in ref_track.markers: 144 | if marker.frame == start_frame: 145 | 146 | p1 = None 147 | for pos_marker in pos_track.markers: 148 | if pos_marker.frame == marker.frame: 149 | p1 = (pos_marker.co.x * res_x, pos_marker.co.y * res_y) 150 | break 151 | 152 | if not p1: 153 | return {"FINISHED"} 154 | 155 | p2 = (marker.co.x * res_x, marker.co.y * res_y) 156 | init_distance = distance_formula(p1, p2) 157 | 158 | for marker in ref_track.markers: 159 | scene.frame_current = marker.frame 160 | p1 = None 161 | for pos_marker in pos_track.markers: 162 | if pos_marker.frame == marker.frame: 163 | p1 = (pos_marker.co.x * res_x, pos_marker.co.y * res_y) 164 | break 165 | if not p1: 166 | break 167 | 168 | p2 = (marker.co.x * res_x, marker.co.y * res_y) 169 | distance = distance_formula(p1, p2) 170 | scl = (distance / init_distance) * 2.0 171 | transform_strip.scale_start_x = scl 172 | transform_strip.keyframe_insert( 173 | data_path="scale_start_x", frame=scene.frame_current) 174 | 175 | scene.frame_current = start_frame 176 | 177 | return {'FINISHED'} 178 | 179 | 180 | def calculate_angle(p1, p2): 181 | """ 182 | Calculate the angle formed by p1, p2, and the x axis 183 | 184 | Parameters 185 | ---------- 186 | p1 : list of float 187 | X & Y coordinates 188 | p2 : list of float 189 | X & Y coordinates 190 | 191 | Returns 192 | ------- 193 | angle : float 194 | """ 195 | a = p2[1] - p1[1] 196 | b = p2[0] - p1[0] 197 | 198 | p1p2 = math.degrees(math.atan2(a, b)) 199 | 200 | return p1p2 201 | 202 | 203 | def distance_formula(p1, p2): 204 | """ 205 | Calculate the distance between 2 points on a 2D Cartesian coordinate plane 206 | """ 207 | x = p2[0] - p1[0] 208 | y = p2[1] - p1[1] 209 | 210 | distance = math.sqrt(x**2 + y**2) 211 | return distance 212 | -------------------------------------------------------------------------------- /operators/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .func_constrain_axis import func_constrain_axis 2 | from .func_constrain_axis_mmb import func_constrain_axis_mmb 3 | from .process_input import process_input -------------------------------------------------------------------------------- /operators/utils/draw/__init__.py: -------------------------------------------------------------------------------- 1 | from .draw_axes import draw_axes 2 | from .draw_line import draw_line 3 | from .draw_px_point import draw_px_point 4 | from .draw_snap import draw_snap 5 | from .draw_square import draw_square 6 | -------------------------------------------------------------------------------- /operators/utils/draw/colors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions to get colors from the user's theme. The functions convert colors to lists so we 3 | can use them for drawing directly. 4 | """ 5 | 6 | 7 | def get_color_gizmo_primary(context): 8 | color = context.preferences.themes[0].user_interface.gizmo_primary 9 | return _color_to_list(color) 10 | 11 | 12 | def get_color_gizmo_secondary(context): 13 | color = context.preferences.themes[0].user_interface.gizmo_secondary 14 | return _color_to_list(color) 15 | 16 | 17 | def get_color_axis_x(context): 18 | color = context.preferences.themes[0].user_interface.axis_x 19 | return _color_to_list(color) 20 | 21 | 22 | def get_color_axis_y(context): 23 | color = context.preferences.themes[0].user_interface.axis_y 24 | return _color_to_list(color) 25 | 26 | 27 | def get_color_axis_z(context): 28 | color = context.preferences.themes[0].user_interface.axis_z 29 | return _color_to_list(color) 30 | 31 | 32 | def _color_to_list(color): 33 | """Converts a Blender Color to a list of 4 color values to use with shaders and drawing""" 34 | return list(color) + [1.0] 35 | -------------------------------------------------------------------------------- /operators/utils/draw/draw_arrows.py: -------------------------------------------------------------------------------- 1 | import math 2 | from .draw_line import draw_line 3 | 4 | def get_next_point(origin, angle, distance): 5 | """ 6 | Get the next point given an origin, an angle, and a distance to go 7 | """ 8 | v2_x = origin[0] + (math.cos(angle) * distance) 9 | v2_y = origin[1] + (math.sin(angle) * distance) 10 | 11 | return [v2_x, v2_y] 12 | 13 | 14 | def draw_arrows(v1, v2, thickness, arrow_length, color, angle_offset=0): 15 | if v2[0] == v1[0]: 16 | if v2[1] > v1[1]: 17 | angle = math.radians(-90) 18 | else: 19 | angle = math.radians(90) 20 | else: 21 | angle = math.atan((v2[1] - v1[1]) / (v2[0] - v1[0])) 22 | 23 | if v2[0] > v1[0]: 24 | pass 25 | elif v2[1] < v1[1]: 26 | angle -= math.radians(180) 27 | 28 | else: 29 | angle += math.radians(180) 30 | 31 | angle += math.radians(angle_offset) 32 | 33 | r_start = get_next_point(v2, angle, thickness * 3) 34 | r_end = get_next_point(r_start, angle, arrow_length) 35 | 36 | r_upper_ala_end = get_next_point(r_end, angle + math.radians(210), arrow_length * 0.667) 37 | r_lower_ala_end = get_next_point(r_end, angle + math.radians(-210), arrow_length * 0.667) 38 | 39 | 40 | angle += math.radians(180) 41 | 42 | l_start = get_next_point(v2, angle, thickness * 3) 43 | l_end = get_next_point(l_start, angle, arrow_length) 44 | 45 | l_upper_ala_end = get_next_point(l_end, angle + math.radians(-210), arrow_length * 0.667) 46 | l_lower_ala_end = get_next_point(l_end, angle + math.radians(210), arrow_length * 0.667) 47 | 48 | draw_line(r_start, r_end, thickness, color) 49 | draw_line(r_end, r_upper_ala_end, thickness, color) 50 | draw_line(r_end, r_lower_ala_end, thickness, color) 51 | 52 | draw_line(l_start, l_end, thickness, color) 53 | draw_line(l_end, l_upper_ala_end, thickness, color) 54 | draw_line(l_end, l_lower_ala_end, thickness, color) 55 | -------------------------------------------------------------------------------- /operators/utils/draw/draw_axes.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..geometry.get_group_box import get_group_box 4 | from ..geometry.get_preview_offset import get_preview_offset 5 | 6 | from .draw_line import draw_line 7 | from .draw_stippled_line import draw_stippled_line 8 | 9 | from .colors import get_color_axis_x, get_color_axis_y 10 | 11 | 12 | def draw_axes(self, context, angle): 13 | transforms = [] 14 | strips = bpy.context.selected_sequences 15 | for strip in strips: 16 | if strip.type == 'TRANSFORM': 17 | transforms.append(strip) 18 | 19 | group_box = get_group_box(transforms) 20 | min_left, max_right, min_bottom, max_top = group_box 21 | group_width = max_right - min_left 22 | group_height = max_top - min_bottom 23 | 24 | group_pos_x = min_left + (group_width / 2) 25 | group_pos_y = min_bottom + (group_height / 2) 26 | 27 | offset_x, offset_y, fac, preview_zoom = get_preview_offset() 28 | 29 | x = (group_pos_x * fac * preview_zoom) + offset_x 30 | y = (group_pos_y * fac * preview_zoom) + offset_y 31 | 32 | color_axis_x = get_color_axis_x(context) 33 | color_axis_y = get_color_axis_y(context) 34 | thickness = 1 35 | stipple_length = 10 36 | far = 10000 37 | 38 | if self.choose_axis and not self.axis_y: 39 | draw_line([-far, y], [far, y], 2, color_axis_x) 40 | draw_stippled_line([x, -far], [x, far], thickness, stipple_length, color_axis_y) 41 | elif self.choose_axis and not self.axis_x: 42 | draw_stippled_line([-far, y], [far, y], thickness, stipple_length, color_axis_x) 43 | draw_line([x, -far], [x, far], thickness, color_axis_y) 44 | elif self.axis_x: 45 | draw_line([-far, y], [far, y], thickness, color_axis_x) 46 | elif self.axis_y: 47 | draw_line([x, -far], [x, far], thickness, color_axis_y) 48 | -------------------------------------------------------------------------------- /operators/utils/draw/draw_line.py: -------------------------------------------------------------------------------- 1 | import math 2 | import gpu 3 | from gpu_extras.batch import batch_for_shader 4 | 5 | def draw_line(v1, v2, thickness, color): 6 | if (v2[0] == v1[0]): 7 | x = thickness 8 | y = 0 9 | elif (v2[1] == v1[1]): 10 | x = 0 11 | y = thickness 12 | 13 | else: 14 | angle = math.atan((v2[1] - v1[1]) / (v2[0] - v1[0])) 15 | x = thickness * math.sin(angle) 16 | y = thickness * math.cos(angle) 17 | 18 | vertices = ( 19 | (v1[0] + x, v1[1] - y), 20 | (v2[0] + x, v2[1] - y), 21 | (v1[0] - x, v1[1] + y), 22 | (v2[0] - x, v2[1] + y), 23 | ) 24 | 25 | indices = ((0, 1, 2), (2, 1, 3)) 26 | 27 | shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') 28 | batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) 29 | 30 | shader.bind() 31 | shader.uniform_float("color", color) 32 | batch.draw(shader) 33 | -------------------------------------------------------------------------------- /operators/utils/draw/draw_px_point.py: -------------------------------------------------------------------------------- 1 | from mathutils import Vector 2 | from .draw_stippled_line import draw_stippled_line 3 | from .draw_arrows import draw_arrows 4 | 5 | def draw_px_point(self, context): 6 | """ 7 | Draws the handle seen when rotating or scaling 8 | """ 9 | # Stopped working after API change --> theme = context.user_preferences.themes['Default'] 10 | #active_color = theme.view_3d.object_active 11 | 12 | #color = (active_color[0], active_color[1], active_color[2], 1.0) 13 | 14 | color = (0, 0, 0, 1.0) 15 | 16 | v1 = [self.center_area.x, self.center_area.y] 17 | v2 = [self.mouse_pos.x, self.mouse_pos.y] 18 | 19 | if self.mouse_pos != Vector([-1, -1]): 20 | draw_stippled_line(v1, v2, 0.5, 4, color) 21 | if hasattr(self, 'rot_prev'): 22 | draw_arrows(v1, v2, 1.5, 12, color, angle_offset=90) 23 | else: 24 | draw_arrows(v1, v2, 1.5, 12, color) 25 | -------------------------------------------------------------------------------- /operators/utils/draw/draw_snap.py: -------------------------------------------------------------------------------- 1 | import bgl 2 | from mathutils import Vector 3 | 4 | from .draw_line import draw_line 5 | 6 | 7 | def draw_snap(self, loc, orientation): 8 | """ 9 | Draws the snap lines 10 | """ 11 | color = (1.0, 1.0, 0.0, 0.5) 12 | outline_color = (0, 0, 0, 0.2) 13 | 14 | if orientation == "VERTICAL": 15 | v1 = [loc, -10000] 16 | v2 = [loc, 10000] 17 | 18 | elif orientation == "HORIZONTAL": 19 | v1 = [-10000, loc] 20 | v2 = [10000, loc] 21 | 22 | draw_line(v1, v2, 1.5, outline_color) 23 | draw_line(v1, v2, 0.5, color) 24 | -------------------------------------------------------------------------------- /operators/utils/draw/draw_square.py: -------------------------------------------------------------------------------- 1 | import gpu 2 | from gpu_extras.batch import batch_for_shader 3 | 4 | def draw_square(vertex, thickness, color): 5 | """ 6 | Draws a square 7 | """ 8 | r = thickness / 2 9 | 10 | vertices = ( 11 | (vertex[0] - r, vertex[1] - r), 12 | (vertex[0] + r, vertex[1] - r), 13 | (vertex[0] - r, vertex[1] + r), 14 | (vertex[0] + r, vertex[1] + r) 15 | ) 16 | 17 | indices = ((0, 1, 2), (2, 1, 3)) 18 | 19 | shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') 20 | batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) 21 | 22 | shader.bind() 23 | shader.uniform_float("color", color) 24 | batch.draw(shader) 25 | -------------------------------------------------------------------------------- /operators/utils/draw/draw_stippled_line.py: -------------------------------------------------------------------------------- 1 | import math 2 | from math import sqrt 3 | import gpu 4 | from gpu_extras.batch import batch_for_shader 5 | 6 | from .draw_line import draw_line 7 | 8 | def distance_formula(p1, p2): 9 | """ 10 | Calculate the distance between 2 points on a 2D Cartesian coordinate plane 11 | 12 | Parameters 13 | ---------- 14 | p1: list of int 15 | The x and y of a point, ie: [1, 0] 16 | p2: list of int 17 | The x and y of a point, ie: [2, 3] 18 | 19 | Returns 20 | ------- 21 | distance: float 22 | The distance between the 2 points 23 | """ 24 | x = p2[0] - p1[0] 25 | y = p2[1] - p1[1] 26 | 27 | distance = sqrt(x**2 + y**2) 28 | return distance 29 | 30 | def get_next_point(origin, angle, distance): 31 | """ 32 | Get the next point given an origin, an angle, and a distance to go 33 | """ 34 | v2_x = origin[0] + (math.cos(angle) * distance) 35 | v2_y = origin[1] + (math.sin(angle) * distance) 36 | 37 | return [v2_x, v2_y] 38 | 39 | def draw_stippled_line(v1, v2, thickness, stipple_length, color): 40 | distance = distance_formula(v1, v2) 41 | 42 | count = int(distance / (stipple_length + (thickness * 3))) 43 | 44 | if v2[0] == v1[0]: 45 | if v2[1] > v1[1]: 46 | angle = math.radians(-90) 47 | else: 48 | angle = math.radians(90) 49 | else: 50 | angle = math.atan((v2[1] - v1[1]) / (v2[0] - v1[0])) 51 | 52 | if v2[0] > v1[0]: 53 | pass 54 | elif v2[1] < v1[1]: 55 | angle -= math.radians(180) 56 | 57 | else: 58 | angle += math.radians(180) 59 | 60 | pairs = [] 61 | 62 | last_point = v1 63 | for i in range(count): 64 | new_point = get_next_point(last_point, angle, stipple_length) 65 | pairs.append([last_point, new_point]) 66 | last_point = get_next_point(new_point, angle, (thickness * 3)) 67 | 68 | for pair in pairs: 69 | draw_line(pair[0], pair[1], thickness, color) 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /operators/utils/func_constrain_axis.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .draw import draw_axes 3 | 4 | 5 | def func_constrain_axis(self, context, key, value, angle): 6 | if len(self.tab) > 1: 7 | angle = 0 8 | if key in ['X','Y']: 9 | if self.handle_axes == None: 10 | args = (self, context, angle) 11 | self.handle_axes = bpy.types.SpaceSequenceEditor.draw_handler_add( 12 | draw_axes, args, 'PREVIEW', 'POST_PIXEL') 13 | if key == 'X' and value == 'PRESS': 14 | if self.axis_x == True and self.axis_y == True: 15 | self.axis_y = False 16 | elif self.axis_x == True and self.axis_y == False: 17 | self.axis_y = True 18 | elif self.axis_x == False and self.axis_y == True: 19 | self.axis_y = False 20 | self.axis_x = True 21 | 22 | if key == 'Y' and value == 'PRESS': 23 | if self.axis_x == True and self.axis_y == True: 24 | self.axis_x = False 25 | elif self.axis_x == False and self.axis_y == True: 26 | self.axis_x = True 27 | elif self.axis_x == True and self.axis_y == False: 28 | self.axis_y = True 29 | self.axis_x = False 30 | 31 | if self.axis_x and self.axis_y: 32 | if self.handle_axes: 33 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 34 | self.handle_axes, 'PREVIEW') 35 | self.handle_axes = None 36 | -------------------------------------------------------------------------------- /operators/utils/func_constrain_axis_mmb.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | from mathutils import Quaternion 5 | 6 | from .draw import draw_axes 7 | 8 | 9 | def func_constrain_axis_mmb(self, context, key, value, angle): 10 | if len(self.tab) > 1: 11 | angle = 0 12 | if key == 'MIDDLEMOUSE': 13 | if value == 'PRESS': 14 | if self.handle_axes == None: 15 | args = (self, context, angle) 16 | self.handle_axes = bpy.types.SpaceSequenceEditor.draw_handler_add( 17 | draw_axes, args, 'PREVIEW', 'POST_PIXEL') 18 | self.choose_axis = True 19 | self.pos_clic = self.mouse_pos 20 | if value == 'RELEASE' : 21 | self.choose_axis = False 22 | if self.pos_clic == self.mouse_pos: 23 | self.axis_x = self.axis_y = True 24 | if self.handle_axes: 25 | bpy.types.SpaceSequenceEditor.draw_handler_remove( 26 | self.handle_axes, 'PREVIEW') 27 | self.handle_axes = None 28 | if self.choose_axis: 29 | vec_axis_z = Vector((0, 0, 1)) 30 | vec_axis_x = Vector((1, 0, 0)) 31 | vec_axis_x.rotate(Quaternion(vec_axis_z, math.radians(angle))) 32 | vec_axis_x = vec_axis_x.to_2d() 33 | vec_axis_y = Vector((0, 1, 0)) 34 | vec_axis_y.rotate(Quaternion(vec_axis_z, math.radians(angle))) 35 | vec_axis_y = vec_axis_y.to_2d() 36 | 37 | ang_x = math.degrees(vec_axis_x.angle(self.mouse_pos - self.center_area)) 38 | ang_y = math.degrees(vec_axis_y.angle(self.mouse_pos - self.center_area)) 39 | 40 | if ang_x > 90: 41 | ang_x = 180 - ang_x 42 | if ang_y > 90: 43 | ang_y = 180 - ang_y 44 | 45 | if ang_x < ang_y: 46 | self.axis_x = True 47 | self.axis_y = False 48 | else : 49 | self.axis_x = False 50 | self.axis_y = True 51 | -------------------------------------------------------------------------------- /operators/utils/geometry/__init__.py: -------------------------------------------------------------------------------- 1 | from .get_group_box import get_group_box 2 | from .get_pos_x import get_pos_x 3 | from .get_pos_y import get_pos_y 4 | from .get_post_rot_bbox import get_post_rot_bbox 5 | from .get_preview_offset import get_preview_offset 6 | from .get_res_factor import get_res_factor 7 | from .get_strip_box import get_strip_box 8 | from .get_strip_corners import get_strip_corners 9 | from .get_transform_box import get_transform_box 10 | from .mouse_to_res import mouse_to_res 11 | from .reposition_strip import reposition_strip 12 | from .reposition_transform_strip import reposition_transform_strip 13 | from .rotate_point import rotate_point 14 | from .set_pos_x import set_pos_x 15 | from .set_pos_y import set_pos_y -------------------------------------------------------------------------------- /operators/utils/geometry/get_group_box.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from operator import attrgetter 4 | 5 | from .get_strip_box import get_strip_box 6 | from .get_transform_box import get_transform_box 7 | from .get_strip_corners import get_strip_corners 8 | 9 | 10 | def get_group_box(strips): 11 | """ 12 | Get the bounding box of a group of strips 13 | 14 | Parameters 15 | ---------- 16 | strips: list of bpy.types.Sequence 17 | 18 | Returns 19 | ------- 20 | box: list of int 21 | The bounding box of all the strips ie: left, right, bottom, top 22 | """ 23 | scene = bpy.context.scene 24 | boxes = [] 25 | 26 | nontransforms = [] 27 | for i in range(len(strips)): 28 | if strips[i].type != "TRANSFORM" and strips[i].type != "SOUND": 29 | nontransforms.append(strips[i]) 30 | 31 | for strip in nontransforms: 32 | boxes.append(get_strip_box(strip)) 33 | 34 | transforms = [] 35 | for i in range(len(strips)): 36 | if strips[i].type == "TRANSFORM": 37 | transforms.append(strips[i]) 38 | 39 | for strip in transforms: 40 | bl, tl, tr, br = get_strip_corners(strip) 41 | vectors = [bl, tl, tr, br] 42 | 43 | left = min(vectors, key=attrgetter('x')).x 44 | right = max(vectors, key=attrgetter('x')).x 45 | 46 | bottom = min(vectors, key=attrgetter('y')).y 47 | top = max(vectors, key=attrgetter('y')).y 48 | 49 | boxes.append([left, right, bottom, top]) 50 | 51 | res_x = scene.render.resolution_x 52 | res_y = scene.render.resolution_y 53 | 54 | min_left = res_x 55 | max_right = 0 56 | min_bottom = res_y 57 | max_top = 0 58 | 59 | for box in boxes: 60 | if box[0] < min_left: 61 | min_left = box[0] 62 | if box[1] > max_right: 63 | max_right = box[1] 64 | if box[2] < min_bottom: 65 | min_bottom = box[2] 66 | if box[3] > max_top: 67 | max_top = box[3] 68 | 69 | return [min_left, max_right, min_bottom, max_top] 70 | -------------------------------------------------------------------------------- /operators/utils/geometry/get_pos_x.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def get_pos_x(strip): 5 | """ 6 | Get the X position of a transform strip. 7 | 8 | Note that a transform strip's position may be defined as a 9 | percentage or a number of pixes. 10 | 11 | 12 | :param strip: A transform strip (bpy.types.Sequence) 13 | Returns 14 | :pos: An X position in pixels (int) 15 | """ 16 | res_x = bpy.context.scene.render.resolution_x 17 | 18 | if strip.translation_unit == 'PERCENT': 19 | pos = strip.translate_start_x * res_x / 100 20 | 21 | else: 22 | pos = strip.translate_start_x 23 | 24 | return pos 25 | -------------------------------------------------------------------------------- /operators/utils/geometry/get_pos_y.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def get_pos_y(strip): 5 | """ 6 | Get the Y position of a transform strip. Note that a transform 7 | strip's position may be defined as a percentage or a number of 8 | pixes. 9 | 10 | Args 11 | :strip: A transform strip (bpy.types.Sequence) 12 | Returns 13 | :pos: A Y position in pixels (int) 14 | """ 15 | res_y = bpy.context.scene.render.resolution_y 16 | 17 | if strip.translation_unit == 'PERCENT': 18 | pos = strip.translate_start_y * res_y / 100 19 | 20 | else: 21 | pos = strip.translate_start_y 22 | 23 | return pos 24 | -------------------------------------------------------------------------------- /operators/utils/geometry/get_post_rot_bbox.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def get_post_rot_bbox(left, right, bottom, top, rotation): 5 | """ 6 | Return bounding box of a rotated rectangle. 7 | 8 | Args 9 | :left: left horizontal position of the unrotated rectangle 10 | (float) 11 | :right: right horizontal position of the unrotated rectangle 12 | (float) 13 | :bottom: bottom vertical position of the unrotated rectangle 14 | (float) 15 | :top: top vertical position of the unrotated rectangle 16 | (float) 17 | :rotation: rotation of the rectangle in radians (float) 18 | Returns 19 | :b_left: minimum horizontal extremity of the rotated rectangle 20 | :b_right: maximum horizontal extremity of the rotated rectangle 21 | :b_bottom: minimum vertical extremity of the rotated rectangle 22 | :b_top: maximum vertical extremity of the rotated rectangle 23 | """ 24 | 25 | rotation = abs(rotation) 26 | width = right - left 27 | height = top - bottom 28 | 29 | b_width = (math.sin(rotation) * height) + (math.cos(rotation) * width) 30 | b_height = (math.sin(rotation) * width) + (math.cos(rotation) * height) 31 | 32 | diff_x = (b_width - width) / 2 33 | b_left = left - diff_x 34 | b_right = right + diff_x 35 | 36 | diff_y = (b_height - height) / 2 37 | b_bottom = bottom - diff_y 38 | b_top = top + diff_y 39 | 40 | return b_left, b_right, b_bottom, b_top 41 | -------------------------------------------------------------------------------- /operators/utils/geometry/get_preview_offset.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .get_res_factor import get_res_factor 4 | 5 | def get_preview_offset(): 6 | """ 7 | Get the number of pixels (x and y) that the preview is offset in the 8 | preview window as well as other related information. 9 | 10 | Returns 11 | :offset_x: The horizontal offset of the preview window 12 | :offset_y: The vertical offset of the preview window 13 | :fac: The resolution factor for the scene 14 | :preview_zoom: The zoom level of the preview window 15 | """ 16 | context = bpy.context 17 | fac = get_res_factor() 18 | 19 | width = context.region.width 20 | height = context.region.height 21 | 22 | rv1 = context.region.view2d.region_to_view(0, 0) 23 | rv2 = context.region.view2d.region_to_view(width, height) 24 | 25 | res_x = context.scene.render.resolution_x 26 | res_y = context.scene.render.resolution_y 27 | 28 | preview_zoom = (width / (rv2[0] - rv1[0])) 29 | 30 | offset_x = -int((rv1[0] + ((res_x * fac) / 2)) * preview_zoom) 31 | offset_y = -int((rv1[1] + ((res_y * fac) / 2)) * preview_zoom) 32 | 33 | return offset_x, offset_y, fac, preview_zoom 34 | -------------------------------------------------------------------------------- /operators/utils/geometry/get_res_factor.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def get_res_factor(): 5 | """ 6 | Get the scene resolution percentage as a factor 7 | 8 | Returns 9 | :fac: The resolution factor (float) 10 | """ 11 | fac = 1.0 12 | 13 | prs = bpy.context.space_data.proxy_render_size 14 | res_perc = bpy.context.scene.render.resolution_percentage 15 | 16 | if prs == 'SCENE': 17 | fac = res_perc / 100 18 | 19 | return fac 20 | -------------------------------------------------------------------------------- /operators/utils/geometry/get_strip_box.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def get_strip_box(strip): 5 | """ 6 | Gets the box of a non-transform strip 7 | 8 | Args 9 | :strip: A strip from the vse (bpy.types.Sequence) 10 | Returns 11 | :box: A list comprising the strip's left, right, bottom, top 12 | (list of int) 13 | """ 14 | scene = bpy.context.scene 15 | 16 | res_x = scene.render.resolution_x 17 | res_y = scene.render.resolution_y 18 | 19 | proxy_facs = { 20 | 'NONE': 1.0, 21 | 'SCENE': 1.0, 22 | 'FULL': 1.0, 23 | 'PROXY_100': 1.0, 24 | 'PROXY_75': 0.75, 25 | 'PROXY_50': 0.5, 26 | 'PROXY_25': 0.25 27 | } 28 | 29 | proxy_key = bpy.context.space_data.proxy_render_size 30 | proxy_fac = proxy_facs[proxy_key] 31 | 32 | if not strip.use_translation and not strip.use_crop: 33 | left = 0 34 | right = res_x 35 | bottom = 0 36 | top = res_y 37 | 38 | elif strip.use_crop and not strip.use_translation: 39 | left = 0 40 | right = res_x 41 | bottom = 0 42 | top = res_y 43 | 44 | elif not hasattr(strip, 'elements'): 45 | len_crop_x = res_x 46 | len_crop_y = res_y 47 | 48 | if strip.type == "SCENE": 49 | factor = strip.scene.render.resolution_percentage / 100 50 | len_crop_x = strip.scene.render.resolution_x * factor 51 | len_crop_y = strip.scene.render.resolution_y * factor 52 | if strip.use_crop: 53 | len_crop_x -= (strip.crop.min_x + strip.crop.max_x) 54 | len_crop_y -= (strip.crop.min_y + strip.crop.max_y) 55 | 56 | if len_crop_x < 0: 57 | len_crop_x = 0 58 | if len_crop_y < 0: 59 | len_crop_y = 0 60 | 61 | left = 0 62 | right = res_x 63 | bottom = 0 64 | top = res_y 65 | 66 | if strip.use_translation: 67 | left = strip.transform.offset_x 68 | right = left + len_crop_x 69 | bottom = strip.transform.offset_y 70 | top = strip.transform.offset_y + len_crop_y 71 | 72 | elif strip.use_translation and not strip.use_crop: 73 | left = strip.transform.offset_x 74 | right = left + (strip.elements[0].orig_width / proxy_fac) 75 | bottom = strip.transform.offset_y 76 | top = bottom + (strip.elements[0].orig_height / proxy_fac) 77 | 78 | else: 79 | total_crop_x = strip.crop.min_x + strip.crop.max_x 80 | total_crop_y = strip.crop.min_y + strip.crop.max_y 81 | 82 | len_crop_x = (strip.elements[0].orig_width / proxy_fac) - total_crop_x 83 | len_crop_y = (strip.elements[0].orig_height / proxy_fac) - total_crop_y 84 | 85 | left = strip.transform.offset_x 86 | right = left + len_crop_x 87 | bottom = strip.transform.offset_y 88 | top = bottom + len_crop_y 89 | 90 | box = [left, right, bottom, top] 91 | 92 | return box 93 | -------------------------------------------------------------------------------- /operators/utils/geometry/get_strip_corners.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | 5 | from .get_strip_box import get_strip_box 6 | from .get_transform_box import get_transform_box 7 | from .rotate_point import rotate_point 8 | 9 | 10 | def get_strip_corners(strip): 11 | """ 12 | Get the bottom_left, top_left, top_right, bottom_right corners of a 13 | strip. 14 | 15 | Args 16 | :strip: A strip (bpy.types.Sequence) 17 | Returns 18 | :corners: The positions of the bottom-left, top-left, top-right, 19 | & bottom-right corners (list of mathutils.Vectors) 20 | """ 21 | scene = bpy.context.scene 22 | if strip.type == 'TRANSFORM': 23 | box = get_transform_box(strip) 24 | left, right, bottom, top = box 25 | 26 | width = right - left 27 | height = top - bottom 28 | 29 | origin = Vector([left + (width / 2), bottom + (height / 2)]) 30 | if strip.input_1.use_translation: 31 | off_x = strip.input_1.transform.offset_x 32 | off_y = strip.input_1.transform.offset_y 33 | scl_x = strip.scale_start_x 34 | scl_y = strip.scale_start_y 35 | 36 | origin_x = (width / 2) - (off_x * scl_x) / 2 37 | origin_y = (height / 2) - (off_y * scl_y) / 2 38 | 39 | origin = Vector([left + origin_x, bottom + origin_y]) 40 | 41 | rot = math.radians(strip.rotation_start) 42 | if strip.use_translation or strip.use_crop: 43 | rot = 0 44 | 45 | bottom_left = Vector([left, bottom]) 46 | top_left = Vector([left, top]) 47 | top_right = Vector([right, top]) 48 | bottom_right = Vector([right, bottom]) 49 | 50 | corners = [bottom_left, top_left, top_right, bottom_right] 51 | for i in range(len(corners)): 52 | corners[i] = rotate_point(corners[i], rot, origin) 53 | 54 | bottom_left, top_left, top_right, bottom_right = corners 55 | 56 | else: 57 | 58 | box = get_strip_box(strip) 59 | left, right, bottom, top = box 60 | 61 | bottom_left = Vector([left, bottom]) 62 | top_left = Vector([left, top]) 63 | 64 | top_right = Vector([right, top]) 65 | bottom_right = Vector([right, bottom]) 66 | 67 | #if hasattr(strip, 'elements') and not strip.type == 'IMAGE' and proxy_fac < 1.0: 68 | # top_left = Vector([left, (bottom + (top - bottom)) / proxy_fac]) 69 | # top_right = Vector([(left + (right - left)) / proxy_fac, (bottom + (top - bottom)) / proxy_fac]) 70 | # bottom_right = Vector([(left + (right - left)) / proxy_fac, bottom]) 71 | 72 | res_x = scene.render.resolution_x 73 | res_y = scene.render.resolution_y 74 | 75 | corners = [bottom_left, top_left, top_right, bottom_right] 76 | 77 | if strip.use_flip_x: 78 | for i in range(len(corners)): 79 | corners[i].x = res_x - corners[i].x 80 | 81 | if strip.use_flip_y: 82 | for i in range(len(corners)): 83 | corners[i].y = res_y - corners[i].y 84 | 85 | bottom_left, top_left, top_right, bottom_right = corners 86 | 87 | return [bottom_left, top_left, top_right, bottom_right] 88 | -------------------------------------------------------------------------------- /operators/utils/geometry/get_transform_box.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | 4 | from .get_pos_x import get_pos_x 5 | from .get_pos_y import get_pos_y 6 | from .get_strip_box import get_strip_box 7 | from .get_res_factor import get_res_factor 8 | 9 | 10 | def get_transform_box(strip): 11 | """ 12 | Gets the unrotated left, right, top, bottom of a transform strip 13 | 14 | Args 15 | :strip: A transform strip (bpy.types.Sequence) Returns 16 | :box: The left, right, top, bottom of a transform strip (list 17 | of int) 18 | """ 19 | scene = bpy.context.scene 20 | 21 | left, right, bottom, top = get_strip_box(strip.input_1) 22 | 23 | res_x = scene.render.resolution_x 24 | res_y = scene.render.resolution_y 25 | 26 | strip_in = strip.input_1 27 | if strip_in.use_translation: 28 | left = 0 29 | right = res_x 30 | bottom = 0 31 | top = res_y 32 | 33 | width = right - left 34 | height = top - bottom 35 | 36 | t_pos_x = get_pos_x(strip) 37 | t_pos_y = get_pos_y(strip) 38 | 39 | world_left = left + t_pos_x 40 | world_right = right + t_pos_x 41 | world_bottom = bottom + t_pos_y 42 | world_top = top + t_pos_y 43 | 44 | origin_x = world_left + (width / 2) 45 | origin_y = world_bottom + (height / 2) 46 | 47 | scl_x = strip.scale_start_x 48 | scl_y = strip.scale_start_y 49 | if strip.use_uniform_scale: 50 | scl_x = scl_y = max([scl_x, scl_y]) 51 | 52 | scaled_left = (world_left - origin_x) * scl_x 53 | scaled_bottom = (world_bottom - origin_y) * scl_y 54 | 55 | diff_x = scaled_left - (world_left - origin_x) 56 | diff_y = scaled_bottom - (world_bottom - origin_y) 57 | 58 | left = world_left + diff_x 59 | right = left + (width * scl_x) 60 | bottom = world_bottom + diff_y 61 | top = bottom + (height * scl_y) 62 | 63 | width = right - left 64 | height = top - bottom 65 | 66 | if right - left <= 0 or top - bottom <= 0: 67 | left = right = bottom = top = 0 68 | 69 | box = [left, right, bottom, top] 70 | 71 | return box 72 | -------------------------------------------------------------------------------- /operators/utils/geometry/mouse_to_res.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | 4 | from .get_res_factor import get_res_factor 5 | 6 | 7 | def mouse_to_res(mouse_vec): 8 | """ 9 | Convert mouse click position to pixel position. 10 | 11 | When a user clicks the preview window, the position is given as a 12 | unit of pixels from the bottom left of the entire preview window. 13 | 14 | This function converts that position to a unit of video resolution 15 | begining from the bottom left of the player part of the preview 16 | window 17 | 18 | Args 19 | :mouse_vec: The point at which the mouse click occured 20 | (mathutils.Vector) 21 | Returns 22 | :mouse_res: The point at which the mouse click occured, in 23 | resolution pixels (mathutils.Vector) 24 | """ 25 | scene = bpy.context.scene 26 | 27 | res_x = scene.render.resolution_x 28 | res_y = scene.render.resolution_y 29 | 30 | mouse_x = mouse_vec.x 31 | mouse_y = mouse_vec.y 32 | 33 | fac = get_res_factor() 34 | 35 | pos = bpy.context.region.view2d.region_to_view(mouse_x, mouse_y) 36 | 37 | pos_x = (pos[0] + (res_x * fac / 2)) / fac 38 | pos_y = (pos[1] + (res_y * fac / 2)) / fac 39 | 40 | mouse_res = Vector((pos_x, pos_y)) 41 | 42 | return mouse_res 43 | -------------------------------------------------------------------------------- /operators/utils/geometry/reposition_strip.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .get_strip_box import get_strip_box 3 | 4 | def reposition_strip(strip, group_box): 5 | """ 6 | Reposition a (non-transform) strip. 7 | 8 | After adjusting scene resolution, strip sizes will be 9 | discombobulated. This function will reset a strip's size and 10 | position to how it was relative to the group box prior to the 11 | resolution change. 12 | 13 | Args 14 | :strip: A strip (bpy.types.Sequence) 15 | :group_box: The group bounding box prior to the scene resolution 16 | change. (list of int: ie [left, right, bottom, top]) 17 | """ 18 | scene = bpy.context.scene 19 | 20 | min_left, max_right, min_bottom, max_top = group_box 21 | 22 | total_width = max_right - min_left 23 | total_height = max_top - min_bottom 24 | 25 | res_x = scene.render.resolution_x 26 | res_y = scene.render.resolution_y 27 | 28 | available_width = res_x - total_width 29 | available_height = res_y - total_height 30 | 31 | if strip.use_translation: 32 | strip.transform.offset_x -= min_left 33 | strip.transform.offset_y -= min_bottom 34 | 35 | # While this part works, it makes the strip blurry. Users shouldn't use this technique. 36 | """ 37 | if strip.type == "META": 38 | left, right, bottom, top = get_strip_box(strip) 39 | width = right - left 40 | height = top - bottom 41 | 42 | fac_x = width / res_x 43 | fac_y = height / res_y 44 | 45 | new_crop_left = int(strip.crop.min_x * fac_x) 46 | new_crop_right = int(strip.crop.max_x * fac_x) 47 | new_crop_top = int(strip.crop.max_y * fac_y) 48 | new_crop_bottom = int(strip.crop.min_y * fac_y) 49 | 50 | strip.crop.min_x = new_crop_left 51 | strip.crop.max_x = new_crop_right 52 | strip.crop.min_y = new_crop_bottom 53 | strip.crop.max_y = new_crop_top 54 | 55 | strip.use_float = True 56 | 57 | bpy.ops.sequencer.select_all(action='DESELECT') 58 | scene.sequence_editor.active_strip = strip 59 | bpy.ops.sequencer.effect_strip_add(type="TRANSFORM") 60 | 61 | transform_strip = bpy.context.scene.sequence_editor.active_strip 62 | transform_strip.name = "[TR]-%s" % strip.name 63 | 64 | transform_strip.blend_type = 'ALPHA_OVER' 65 | transform_strip.blend_alpha = strip.blend_alpha 66 | 67 | strip.mute = True 68 | 69 | if strip.use_translation: 70 | transform_strip.scale_start_x = 1.0 71 | transform_strip.scale_start_y = 1.0 72 | 73 | offset_x = strip.transform.offset_x 74 | offset_y = strip.transform.offset_y 75 | 76 | flip_x = 1 77 | if strip.use_flip_x: 78 | flip_x = -1 79 | 80 | flip_y = 1 81 | if strip.use_flip_y: 82 | flip_y = -1 83 | 84 | delta_x = ((width / 2) / fac_x) - (width / 2) 85 | pos_x = offset_x + (width / 2) - (res_x / 2) + delta_x 86 | pos_x *= flip_x 87 | 88 | delta_y = ((height / 2) / fac_y) - (height / 2) 89 | pos_y = offset_y + (height / 2) - (res_y / 2) + delta_y 90 | pos_y *= flip_y 91 | 92 | if transform_strip.translation_unit == 'PERCENT': 93 | pos_x = (pos_x / res_x) * 100 94 | pos_y = (pos_y / res_y) * 100 95 | 96 | transform_strip.translate_start_x = pos_x 97 | transform_strip.translate_start_y = pos_y 98 | 99 | strip.use_translation = False 100 | """ 101 | 102 | if not hasattr(strip, 'elements') and strip.use_crop: 103 | if strip.crop.min_x < available_width: 104 | available_width -= strip.crop.min_x 105 | strip.crop.min_x = 0 106 | else: 107 | strip.crop.min_x -= available_width 108 | available_width = 0 109 | 110 | if strip.crop.min_y < available_height: 111 | available_height -= strip.crop.min_y 112 | strip.crop.min_y = 0 113 | else: 114 | strip.crop.min_y -= available_height 115 | available_height = 0 116 | 117 | strip.crop.max_x -= available_width 118 | strip.crop.max_y -= available_height 119 | -------------------------------------------------------------------------------- /operators/utils/geometry/reposition_transform_strip.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from operator import attrgetter 3 | import math 4 | 5 | from .get_transform_box import get_transform_box 6 | from .get_strip_box import get_strip_box 7 | from .get_post_rot_bbox import get_post_rot_bbox 8 | from .get_strip_corners import get_strip_corners 9 | 10 | 11 | def reposition_transform_strip(strip, group_box): 12 | """ 13 | Reposition a transform strip. 14 | 15 | After adjusting scene resolution, strip sizes will be 16 | discombobulated. This function will reset a strip's size and 17 | position to how it was relative to the group box prior to the 18 | resolution change. 19 | 20 | Args 21 | :strip: A transform strip (bpy.types.Sequence) 22 | :group_box: The group bounding box prior to the scene resolution 23 | change. (list of int: ie [left, right, bottom, top]) 24 | """ 25 | scene = bpy.context.scene 26 | 27 | min_left, max_right, min_bottom, max_top = group_box 28 | 29 | total_width = max_right - min_left 30 | total_height = max_top - min_bottom 31 | 32 | res_x = scene.render.resolution_x 33 | res_y = scene.render.resolution_y 34 | 35 | min_left, max_right, min_bottom, max_top = group_box 36 | 37 | left, right, bottom, top = get_transform_box(strip) 38 | 39 | width = right - left 40 | height = top - bottom 41 | 42 | rot = math.radians(strip.rotation_start) 43 | 44 | bl, tl, tr, br = get_strip_corners(strip) 45 | vectors = [bl, tl, tr, br] 46 | b_left = min(vectors, key=attrgetter('x')).x 47 | b_right = max(vectors, key=attrgetter('x')).x 48 | 49 | b_bottom = min(vectors, key=attrgetter('y')).y 50 | b_top = max(vectors, key=attrgetter('y')).y 51 | 52 | primary_offset_x = b_left - min_left 53 | primary_offset_y = b_bottom - min_bottom 54 | 55 | b_width = b_right - b_left 56 | b_height = b_top - b_bottom 57 | 58 | scale_ratio_x = total_width / res_x 59 | scale_ratio_y = total_height / res_y 60 | 61 | current_width = width * scale_ratio_x 62 | current_height = height * scale_ratio_y 63 | 64 | current_left = left * scale_ratio_x 65 | current_bottom = bottom * scale_ratio_y 66 | 67 | current_right = current_left + current_width 68 | current_top = current_bottom + current_height 69 | 70 | box = get_post_rot_bbox( 71 | current_left, current_right, current_bottom, current_top, rot) 72 | current_b_left = box[0] 73 | current_b_right = box[1] 74 | current_b_bottom = box[2] 75 | current_b_top = box[3] 76 | 77 | current_b_width = current_b_right - current_b_left 78 | current_b_height = current_b_top - current_b_bottom 79 | 80 | collapse_offset_x = -current_b_left 81 | collapse_offset_y = -current_b_bottom 82 | 83 | scale_offset_x = (b_width - current_b_width) / 2 84 | scale_offset_y = (b_height - current_b_height) / 2 85 | 86 | offset_x, null, offset_y, null = get_strip_box(strip) 87 | 88 | if strip.use_translation: 89 | strip.transform.offset_x = 0 90 | strip.transform.offset_y = 0 91 | 92 | if strip.translation_unit == 'PERCENT': 93 | primary_offset_x = (primary_offset_x / total_width) * 100 94 | primary_offset_y = (primary_offset_y / total_height) * 100 95 | 96 | collapse_offset_x = (collapse_offset_x / total_width) * 100 97 | collapse_offset_y = (collapse_offset_y / total_height) * 100 98 | 99 | scale_offset_x = (scale_offset_x / total_width) * 100 100 | scale_offset_y = (scale_offset_y / total_height) * 100 101 | 102 | offset_x = (offset_x / res_x) * 100 103 | offset_y = (offset_y / res_y) * 100 104 | 105 | combo_offset_x = sum([primary_offset_x, collapse_offset_x, 106 | scale_offset_x, offset_x]) 107 | combo_offset_y = sum([primary_offset_y, collapse_offset_y, 108 | scale_offset_y, offset_y]) 109 | 110 | strip.translate_start_x += combo_offset_x 111 | strip.translate_start_y += combo_offset_y 112 | 113 | strip.scale_start_x = strip.scale_start_x / scale_ratio_x 114 | strip.scale_start_y = strip.scale_start_y / scale_ratio_y 115 | -------------------------------------------------------------------------------- /operators/utils/geometry/rotate_point.py: -------------------------------------------------------------------------------- 1 | import math 2 | from mathutils import Vector 3 | 4 | 5 | def rotate_point(point, angle, origin=Vector([0, 0])): 6 | """ 7 | Rotate a point around an origin and return it's new position 8 | 9 | Args 10 | :point: The point that will be rotated (mathutils.Vector) 11 | Returns 12 | :rotated_vector: A point that has been rotated about an origin 13 | (mathutils.Vector) 14 | """ 15 | 16 | pos = point - origin 17 | 18 | sin = math.sin(angle) 19 | cos = math.cos(angle) 20 | 21 | pos_x = (pos.x * cos) - (pos.y * sin) 22 | pos_y = (pos.x * sin) + (pos.y * cos) 23 | 24 | rotated_vector = origin + Vector([pos_x, pos_y]) 25 | 26 | return rotated_vector 27 | -------------------------------------------------------------------------------- /operators/utils/geometry/set_pos_x.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | def set_pos_x(strip, pos): 4 | """ 5 | Set the X position of a transform strip, accounting for whether or 6 | not the translation unit is set to PERCENT 7 | 8 | Args 9 | :strip: The transform strip (bpy.types.Sequence) 10 | :pos: The X position the strip should be moved to, in pixels 11 | (int) 12 | """ 13 | res_x = bpy.context.scene.render.resolution_x 14 | 15 | if strip.translation_unit == 'PERCENT': 16 | pos = (pos * 100) / res_x 17 | 18 | return pos 19 | -------------------------------------------------------------------------------- /operators/utils/geometry/set_pos_y.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | def set_pos_y(strip, pos): 4 | """ 5 | Set the Y position of a transform strip, accounting for whether or 6 | not the translation unit is set to PERCENT 7 | 8 | Args 9 | :strip: The transform strip (bpy.types.Sequence) 10 | :pos: The X position the strip should be moved to, in pixels 11 | (int) 12 | """ 13 | res_y = bpy.context.scene.render.resolution_y 14 | 15 | if strip.translation_unit == 'PERCENT': 16 | pos = pos * 100 / res_y 17 | 18 | return pos 19 | -------------------------------------------------------------------------------- /operators/utils/process_input.py: -------------------------------------------------------------------------------- 1 | def process_input(self, key, value): 2 | """ 3 | Get the movement value inputted by the user. 4 | 5 | For example, after pressing r to rotate, the user can input a 6 | rotation amount (in degrees) by pressing numbers on the keyboard 7 | """ 8 | numbers = { 9 | 'NUMPAD_0' : '0', 10 | 'NUMPAD_1' : '1', 11 | 'NUMPAD_2' : '2', 12 | 'NUMPAD_3' : '3', 13 | 'NUMPAD_4' : '4', 14 | 'NUMPAD_5' : '5', 15 | 'NUMPAD_6' : '6', 16 | 'NUMPAD_7' : '7', 17 | 'NUMPAD_8' : '8', 18 | 'NUMPAD_9' : '9', 19 | 'ZERO' : '0', 20 | 'ONE' : '1', 21 | 'TWO' : '2', 22 | 'THREE' : '3', 23 | 'FOUR' : '4', 24 | 'FIVE' : '5', 25 | 'SIX' : '6', 26 | 'SEVEN' : '7', 27 | 'EIGHT' : '8', 28 | 'NINE' : '9', 29 | 'PERIOD' : '.', 30 | 'NUMPAD_PERIOD' : '.', 31 | 'MINUS' : '-', 32 | 'NUMPAD_MINUS' : '-', 33 | 'BACK_SPACE' : '', 34 | } 35 | if key in numbers.keys() and value == 'PRESS' and not key == 'BACK_SPACE': 36 | self.key_val += numbers[key] 37 | 38 | elif key == 'BACK_SPACE' and value == 'PRESS' and len(self.key_val) > 0: 39 | self.key_val = self.key_val[0:-1] 40 | 41 | if len(self.key_val) > 1 and self.key_val.endswith('-'): 42 | self.key_val = '-' + self.key_val[0:-1] 43 | 44 | if self.key_val.count('-') > 1: 45 | if self.key_val.count('-') % 2 == 0: 46 | self.key_val = self.key_val.replace('-', '') 47 | else: 48 | self.key_val = '-' + self.key_val.replace('-', '') 49 | -------------------------------------------------------------------------------- /operators/utils/selection/__init__.py: -------------------------------------------------------------------------------- 1 | from .ensure_transforms import ensure_transforms 2 | from .get_highest_transform import get_highest_transform 3 | from .get_input_tree import get_input_tree 4 | from .get_nontransforms import get_nontransforms 5 | from .get_transforms import get_transforms 6 | from .get_visible_strips import get_visible_strips -------------------------------------------------------------------------------- /operators/utils/selection/ensure_transforms.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .get_highest_transform import get_highest_transform 3 | 4 | 5 | def ensure_transforms(): 6 | """ 7 | Check each selected strip. If it is not a transform strip, use 8 | bpy.ops.vse_transform_tools.add_transform() to add a transform to 9 | that strip. 10 | 11 | Returns 12 | :final_selected: A list of all the transform strips related to 13 | the current selected strips, both those 14 | pre-existing and those added by this function. 15 | (list of bpy.types.Sequence) 16 | """ 17 | context = bpy.context 18 | scene = context.scene 19 | 20 | selected = bpy.context.selected_sequences 21 | transforms = [] 22 | for strip in selected: 23 | transform_strip = get_highest_transform(strip) 24 | if transform_strip not in transforms: 25 | transforms.append(transform_strip) 26 | 27 | for strip in selected: 28 | if not strip in transforms: 29 | strip.select = False 30 | 31 | for strip in transforms: 32 | strip.select = True 33 | 34 | new_active = get_highest_transform( 35 | scene.sequence_editor.active_strip) 36 | 37 | scene.sequence_editor.active_strip = new_active 38 | 39 | final_selected = [] 40 | for strip in transforms: 41 | bpy.ops.sequencer.select_all(action="DESELECT") 42 | 43 | if not strip.type == "TRANSFORM": 44 | strip.select = True 45 | bpy.ops.vse_transform_tools.add_transform() 46 | active = scene.sequence_editor.active_strip 47 | final_selected.append(active) 48 | 49 | else: 50 | final_selected.append(strip) 51 | 52 | return final_selected 53 | 54 | -------------------------------------------------------------------------------- /operators/utils/selection/get_highest_transform.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .get_input_tree import get_input_tree 3 | 4 | 5 | def get_highest_transform(strip): 6 | """ 7 | Return the highest transform strip in a modifier hierarchy 8 | 9 | Args 10 | :strip: A strip (bpy.types.Sequence) 11 | """ 12 | scene = bpy.context.scene 13 | 14 | all_sequences = list(sorted(scene.sequence_editor.sequences_all, 15 | key=lambda x: x.channel)) 16 | 17 | all_sequences.reverse() 18 | 19 | checked_strips = [] 20 | 21 | for seq in all_sequences: 22 | if seq not in checked_strips: 23 | tree = get_input_tree(seq) 24 | 25 | if strip in tree: 26 | for branch in tree: 27 | if branch.type == "TRANSFORM": 28 | return branch 29 | 30 | elif branch == strip: 31 | return seq 32 | else: 33 | checked_strips.extend(tree) 34 | -------------------------------------------------------------------------------- /operators/utils/selection/get_input_tree.py: -------------------------------------------------------------------------------- 1 | def get_input_tree(strip): 2 | """ 3 | Recursively gather a list of the strip and all subsequent input_1 4 | and input_2 strips in it's heirarchy. 5 | 6 | Arg 7 | :strip: A strip (bpy.types.Sequence) 8 | Return 9 | :inputs: A list of strips from an input heirarchy (list of 10 | bpy.types.Sequence) 11 | """ 12 | inputs = [strip] 13 | 14 | if hasattr(strip, 'input_1'): 15 | inputs.extend(get_input_tree(strip.input_1)) 16 | if hasattr(strip, 'input_2'): 17 | inputs.extend(get_input_tree(strip.input_2)) 18 | 19 | return inputs 20 | -------------------------------------------------------------------------------- /operators/utils/selection/get_nontransforms.py: -------------------------------------------------------------------------------- 1 | def get_nontransforms(strips): 2 | """ 3 | Get a list of the non-transform strips from the given list of 4 | strips. 5 | """ 6 | nontransforms = [] 7 | for i in range(len(strips)): 8 | if strips[i].type != "TRANSFORM" and strips[i].type != "SOUND": 9 | nontransforms.append(strips[i]) 10 | 11 | return nontransforms 12 | 13 | -------------------------------------------------------------------------------- /operators/utils/selection/get_transforms.py: -------------------------------------------------------------------------------- 1 | def get_transforms(strips): 2 | """ 3 | Get a list of the selected transform strips 4 | """ 5 | transforms = [] 6 | for i in range(len(strips)): 7 | if strips[i].type == "TRANSFORM": 8 | transforms.append(strips[i]) 9 | 10 | return transforms 11 | -------------------------------------------------------------------------------- /operators/utils/selection/get_visible_strips.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def get_visible_strips(): 5 | """ 6 | Get a list of the strips currently visible in the preview window 7 | 8 | Returns 9 | :strips: A list of visible strips (list of bpy.types.Sequence) 10 | """ 11 | scene = bpy.context.scene 12 | sequence_editor = scene.sequence_editor 13 | 14 | if len(scene.sequence_editor.meta_stack) > 0: 15 | strips = list(sequence_editor.meta_stack[-1].sequences) 16 | else: 17 | strips = list(scene.sequence_editor.sequences) 18 | 19 | rejects = [] 20 | for strip in strips: 21 | if strip.frame_final_start > scene.frame_current: 22 | rejects.append(strip) 23 | elif strip.frame_final_end <= scene.frame_current: 24 | rejects.append(strip) 25 | elif strip.type == 'SOUND': 26 | rejects.append(strip) 27 | elif hasattr(strip, 'input_1'): 28 | rejects.append(strip.input_1) 29 | elif hasattr(strip, 'input_2'): 30 | rejects.append(strip.input_2) 31 | 32 | filtered = [] 33 | for strip in strips: 34 | if strip not in rejects: 35 | filtered.append(strip) 36 | 37 | strips = sorted(filtered, key=lambda strip: strip.channel) 38 | rejects = [] 39 | 40 | blocked_visibility = False 41 | for strip in reversed(strips): 42 | if blocked_visibility: 43 | rejects.append(strip) 44 | if strip.blend_type in ['CROSS', 'REPLACE'] and not strip.mute and not strip.type == 'SOUND': 45 | blocked_visibility = True 46 | 47 | output = [] 48 | for strip in strips: 49 | if strip not in rejects: 50 | output.append(strip) 51 | 52 | return output 53 | 54 | 55 | --------------------------------------------------------------------------------