From 35806185367c9f8c2da5e183f87699d54a4603ed Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Wed, 21 Apr 2021 12:38:52 +0200 Subject: [PATCH 01/33] After Effects export: export object hide_render as AE opacity --- io_export_after_effects.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index bca17f7..f13a706 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -487,6 +487,9 @@ def write_jsx_file(file, data, selection, include_animation, 'scale': '', 'scale_static': '', 'scale_anim': False, + 'opacity': '', + 'opacity_static': '', + 'opacity_anim': False, } # Create structure for images @@ -502,6 +505,9 @@ def write_jsx_file(file, data, selection, include_animation, 'scale': '', 'scale_static': '', 'scale_anim': False, + 'opacity': '', + 'opacity_static': '', + 'opacity_anim': False, 'filepath': '', } @@ -529,6 +535,9 @@ def write_jsx_file(file, data, selection, include_animation, 'orientation': '', 'orientation_static': '', 'orientation_anim': False, + 'opacity': '', + 'opacity_static': '', + 'opacity_anim': False, } # Create structure for nulls @@ -708,6 +717,7 @@ def write_jsx_file(file, data, selection, include_animation, scale = '[%f,%f,%f],' % (ae_transform[6], ae_transform[7] * data['width'] / data['height'], ae_transform[8]) + opacity = '0.0,' if obj.hide_render else '100.0,' js_solid = js_data['solids'][name_ae] js_solid['color'] = get_plane_color(obj) js_solid['width'] = data['width'] @@ -715,6 +725,7 @@ def write_jsx_file(file, data, selection, include_animation, js_solid['position'] += position js_solid['orientation'] += orientation js_solid['scale'] += scale + js_solid['opacity'] += opacity # Check if properties change values compared to previous frame # If property don't change through out the whole animation, # keyframes won't be added @@ -725,9 +736,12 @@ def write_jsx_file(file, data, selection, include_animation, js_solid['orientation_anim'] = True if scale != js_solid['scale_static']: js_solid['scale_anim'] = True + if opacity != js_solid['opacity_static']: + js_solid['opacity_anim'] = True js_solid['position_static'] = position js_solid['orientation_static'] = orientation js_solid['scale_static'] = scale + js_solid['opacity_static'] = opacity # Keyframes for all lights. if include_selected_objects: @@ -748,11 +762,13 @@ def write_jsx_file(file, data, selection, include_animation, ae_transform[5]) energy = '[%f],' % (obj.data.energy * 100.0) color = '[%f,%f,%f],' % (color[0], color[1], color[2]) + opacity = '0.0,' if obj.hide_render else '100.0,' js_light = js_data['lights'][name_ae] js_light['position'] += position js_light['orientation'] += orientation js_light['intensity'] += energy js_light['Color'] += color + js_light['opacity'] += opacity # Check if properties change values compared to previous frame # If property don't change through out the whole animation, # keyframes won't be added @@ -765,10 +781,13 @@ def write_jsx_file(file, data, selection, include_animation, js_light['intensity_anim'] = True if color != js_light['Color_static']: js_light['Color_anim'] = True + if opacity != js_light['opacity_static']: + js_light['opacity_anim'] = True js_light['position_static'] = position js_light['orientation_static'] = orientation js_light['intensity_static'] = energy js_light['Color_static'] = color + js_light['opacity_static'] = opacity if type == 'SPOT': cone_angle = '[%f],' % (degrees(obj.data.spot_size)) cone_feather = '[%f],' % (obj.data.spot_blend * 100.0) @@ -842,10 +861,12 @@ def write_jsx_file(file, data, selection, include_animation, ae_transform[7] / ratio_to_comp * image_width / image_height, ae_transform[8]) + opacity = '0.0,' if obj.hide_render else '100.0,' js_image = js_data['images'][name_ae] js_image['position'] += position js_image['orientation'] += orientation js_image['scale'] += scale + js_image['opacity'] += opacity # Check if properties change values compared to previous frame # If property don't change through out the whole animation, # keyframes won't be added @@ -856,9 +877,12 @@ def write_jsx_file(file, data, selection, include_animation, js_image['orientation_anim'] = True if scale != js_image['scale_static']: js_image['scale_anim'] = True + if opacity != js_image['opacity_static']: + js_image['opacity_anim'] = True js_image['position_static'] = position js_image['orientation_static'] = orientation js_image['scale_static'] = scale + js_image['opacity_static'] = opacity js_image['filepath'] = get_image_filepath(obj) # keyframes for all object bundles. Not ready yet. @@ -946,7 +970,7 @@ def write_jsx_file(file, data, selection, include_animation, jsx_file.write( '%s.source.name = "%s";\n' % (name_ae, name_ae)) # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "scale"): + for prop in ("position", "orientation", "scale", "opacity"): if include_animation and obj[prop + '_anim']: jsx_file.write( '%s.property("%s").setValuesAtTimes([%s],[%s]);\n' @@ -971,7 +995,7 @@ def write_jsx_file(file, data, selection, include_animation, jsx_file.write( '%s.source.name = "%s";\n' % (name_ae, name_ae)) # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "scale"): + for prop in ("position", "orientation", "scale", "opacity"): if include_animation and obj[prop + '_anim']: jsx_file.write( '%s.property("%s").setValuesAtTimes([%s],[%s]);\n' @@ -993,7 +1017,7 @@ def write_jsx_file(file, data, selection, include_animation, '%s.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' % name_ae) # Set values of properties, add keyframes only where needed - props = ["position", "orientation", "intensity", "Color"] + props = ["position", "orientation", "intensity", "Color", "opacity"] if obj['type'] == 'SPOT': props.extend(("Cone Angle", "Cone Feather")) for prop in props: -- 2.30.2 From 97ff1244c52d61846ca449abb8232221f764ab3d Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Wed, 28 Apr 2021 15:02:34 +0200 Subject: [PATCH 02/33] After Effects export: group images and solids into a folder --- io_export_after_effects.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index f13a706..582e518 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -920,9 +920,11 @@ def write_jsx_file(file, data, selection, include_animation, '\nvar newComp = app.project.items.addComp(compName, %i, %i, %f, %f, %f);' % (data['width'], data['height'], data['aspect'], data['duration'], data['fps'])) - jsx_file.write('\nnewComp.displayStartTime = %f;\n\n\n' + jsx_file.write('\nnewComp.displayStartTime = %f;\n\n' % ((data['start'] + 1.0) / data['fps'])) + jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n') + # Create camera bundles (nulls) jsx_file.write('// ************** CAMERA 3D MARKERS **************\n\n') for name_ae, obj in js_data['bundles_cam'].items(): @@ -965,10 +967,9 @@ def write_jsx_file(file, data, selection, include_animation, obj['width'], obj['height'], 1.0)) - jsx_file.write( - '%s.threeDLayer = true;\n' % name_ae) - jsx_file.write( - '%s.source.name = "%s";\n' % (name_ae, name_ae)) + jsx_file.write('%s.source.name = "%s";\n' % (name_ae, name_ae)) + jsx_file.write('%s.source.parentFolder = footageFolder;\n' % (name_ae)) + jsx_file.write('%s.threeDLayer = true;\n' % name_ae) # Set values of properties, add keyframes only where needed for prop in ("position", "orientation", "scale", "opacity"): if include_animation and obj[prop + '_anim']: @@ -986,8 +987,9 @@ def write_jsx_file(file, data, selection, include_animation, jsx_file.write('// ************** IMAGES **************\n\n') for name_ae, obj in js_data['images'].items(): jsx_file.write( - 'var newFootage = app.project.importFile(new ImportOptions(File("%s")))\n' + 'var newFootage = app.project.importFile(new ImportOptions(File("%s")));\n' % (obj['filepath'])) + jsx_file.write('newFootage.parentFolder = footageFolder;\n') jsx_file.write( 'var %s = newComp.layers.add(newFootage);\n' % (name_ae)) jsx_file.write( -- 2.30.2 From 001ab4aa5bc291b1950e072cb9f107eed3e863b6 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Mon, 14 Jun 2021 16:23:45 +0200 Subject: [PATCH 03/33] After Effects export: use f-strings for formatting This allows a greater float precision, needed by AE for accuracy. Noticed this when trying to export a shot starting at frame 101 at 24 fps: >>> "%f" % (101 / 24) '4.208333', not enough precision for AE >>> f"{101/24}" '4.208333333333333', good enough for AE --- io_export_after_effects.py | 185 +++++++++++++------------------------ 1 file changed, 66 insertions(+), 119 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 582e518..8d73d19 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -23,7 +23,7 @@ bl_info = { "description": "Export cameras, selected objects & camera solution " "3D Markers to Adobe After Effects CS3 and above", "author": "Bartek Skorupa", - "version": (0, 0, 69), + "version": (0, 0, 70), "blender": (2, 80, 0), "location": "File > Export > Adobe After Effects (.jsx)", "warning": "", @@ -154,7 +154,7 @@ def get_plane_color(obj): wrapper = node_shader_utils.PrincipledBSDFWrapper(obj.active_material) color = Color(wrapper.base_color[:3]) + wrapper.emission_color - return '[%f,%f,%f]' % (color[0], color[1], color[2]) + return str(list(color)) def is_plane(obj): @@ -601,7 +601,7 @@ def write_jsx_file(file, data, selection, include_animation, ae_transform = (convert_transform_matrix( matrix, data['width'], data['height'], data['aspect'], False, ae_size)) - js_data['bundles_cam'][name_ae]['position'] += ('[%f,%f,%f],' % (ae_transform[0], ae_transform[1], ae_transform[2])) + js_data['bundles_cam'][name_ae]['position'] += f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' # Get all keyframes for each object and store in dico if include_animation: @@ -613,7 +613,7 @@ def write_jsx_file(file, data, selection, include_animation, data['scn'].frame_set(frame) # Get time for this loop - js_data['times'] += '%f,' % ((frame - data['start']) / data['fps']) + js_data['times'] += str((frame - data['start']) / data['fps']) + ',' # Keyframes for active camera/cameras if include_active_cam and data['active_cam_frames'] != []: @@ -632,11 +632,10 @@ def write_jsx_file(file, data, selection, include_animation, zoom = convert_lens(active_cam, data['width'], data['height'], data['aspect']) # Store all values in dico - position = '[%f,%f,%f],' % (ae_transform[0], ae_transform[1], - ae_transform[2]) - orientation = '[%f,%f,%f],' % (ae_transform[3], ae_transform[4], - ae_transform[5]) - zoom = '%f,' % (zoom) + + position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' + orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' + zoom = str(zoom) + ',' js_camera = js_data['cameras'][name_ae] js_camera['position'] += position js_camera['orientation'] += orientation @@ -669,13 +668,9 @@ def write_jsx_file(file, data, selection, include_animation, zoom = convert_lens(obj, data['width'], data['height'], data['aspect']) # Store all values in dico - position = '[%f,%f,%f],' % (ae_transform[0], - ae_transform[1], - ae_transform[2]) - orientation = '[%f,%f,%f],' % (ae_transform[3], - ae_transform[4], - ae_transform[5]) - zoom = '%f,' % (zoom) + position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' + orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' + zoom = str(zoom) + ',' js_camera = js_data['cameras'][name_ae] js_camera['position'] += position js_camera['orientation'] += orientation @@ -707,16 +702,9 @@ def write_jsx_file(file, data, selection, include_animation, plane_matrix, data['width'], data['height'], data['aspect'], True, ae_size) # Store all values in dico - position = '[%f,%f,%f],' % (ae_transform[0], - ae_transform[1], - ae_transform[2]) - orientation = '[%f,%f,%f],' % (ae_transform[3], - ae_transform[4], - ae_transform[5]) - # plane_width, plane_height, _ = plane_matrix.to_scale() - scale = '[%f,%f,%f],' % (ae_transform[6], - ae_transform[7] * data['width'] / data['height'], - ae_transform[8]) + position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' + orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' + scale = f'[{ae_transform[6]},{ae_transform[7] * data["width"] / data["height"]},{ae_transform[8]}],' opacity = '0.0,' if obj.hide_render else '100.0,' js_solid = js_data['solids'][name_ae] js_solid['color'] = get_plane_color(obj) @@ -755,13 +743,10 @@ def write_jsx_file(file, data, selection, include_animation, data['aspect'], True, ae_size) color = obj.data.color # Store all values in dico - position = '[%f,%f,%f],' % (ae_transform[0], ae_transform[1], - ae_transform[2]) - orientation = '[%f,%f,%f],' % (ae_transform[3], - ae_transform[4], - ae_transform[5]) - energy = '[%f],' % (obj.data.energy * 100.0) - color = '[%f,%f,%f],' % (color[0], color[1], color[2]) + position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' + orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' + energy = f'[{obj.data.energy * 100.0}],' + color = f'[{color[0]},{color[1]},{color[2]}],' opacity = '0.0,' if obj.hide_render else '100.0,' js_light = js_data['lights'][name_ae] js_light['position'] += position @@ -789,8 +774,8 @@ def write_jsx_file(file, data, selection, include_animation, js_light['Color_static'] = color js_light['opacity_static'] = opacity if type == 'SPOT': - cone_angle = '[%f],' % (degrees(obj.data.spot_size)) - cone_feather = '[%f],' % (obj.data.spot_blend * 100.0) + cone_angle = f'[{degrees(obj.data.spot_size)}],' + cone_feather = f'[obj.data.spot_blend * 100.0],' js_light['Cone Angle'] += cone_angle js_light['Cone Feather'] += cone_feather # Check if properties change values compared to previous frame @@ -812,12 +797,9 @@ def write_jsx_file(file, data, selection, include_animation, # Convert obj transform properties to AE space ae_transform = convert_transform_matrix(obj.matrix_world.copy(), data['width'], data['height'], data['aspect'], True, ae_size) # Store all values in dico - position = '[%f,%f,%f],' % (ae_transform[0], ae_transform[1], - ae_transform[2]) - orientation = '[%f,%f,%f],' % (ae_transform[3], ae_transform[4], - ae_transform[5]) - scale = '[%f,%f,%f],' % (ae_transform[6], ae_transform[7], - ae_transform[8]) + position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' + orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' + scale = f'[{ae_transform[6]},{ae_transform[7]},{ae_transform[8]}],' js_null = js_data['nulls'][name_ae] js_null['position'] += position js_null['orientation'] += orientation @@ -849,18 +831,11 @@ def write_jsx_file(file, data, selection, include_animation, plane_matrix, data['width'], data['height'], data['aspect'], True, ae_size) # Store all values in dico - position = '[%f,%f,%f],' % (ae_transform[0], - ae_transform[1], - ae_transform[2]) - orientation = '[%f,%f,%f],' % (ae_transform[3], - ae_transform[4], - ae_transform[5]) + position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' + orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' image_width, image_height = get_image_size(obj) ratio_to_comp = image_width / data['width'] - scale = '[%f,%f,%f],' % (ae_transform[6] / ratio_to_comp, - ae_transform[7] / ratio_to_comp - * image_width / image_height, - ae_transform[8]) + scale = f'[{ae_transform[6] / ratio_to_comp},{ae_transform[7] / ratio_to_comp * image_width / image_height},{ae_transform[8]}],' opacity = '0.0,' if obj.hide_render else '100.0,' js_image = js_data['images'][name_ae] js_image['position'] += position @@ -897,13 +872,13 @@ def write_jsx_file(file, data, selection, include_animation, jsx_file.write('#target AfterEffects\n\n') # Script's header jsx_file.write('/**************************************\n') - jsx_file.write('Scene : %s\n' % data['scn'].name) - jsx_file.write('Resolution : %i x %i\n' % (data['width'], data['height'])) - jsx_file.write('Duration : %f\n' % (data['duration'])) - jsx_file.write('FPS : %f\n' % (data['fps'])) - jsx_file.write('Date : %s\n' % datetime.datetime.now()) - jsx_file.write('Exported with io_export_after_effects.py\n') - jsx_file.write('**************************************/\n\n\n\n') + jsx_file.write(f'Scene : {data["scn"].name}\n') + jsx_file.write(f'Resolution : {data["width"]} x {data["height"]}\n') + jsx_file.write(f'Duration : {data["duration"]}\n') + jsx_file.write(f'FPS : {data["fps"]}\n') + jsx_file.write(f'Date : {datetime.datetime.now()}\n') + jsx_file.write(f'Exported with io_export_after_effects.py\n') + jsx_file.write(f'**************************************/\n\n\n\n') # Wrap in function jsx_file.write("function compFromBlender(){\n") @@ -913,26 +888,23 @@ def write_jsx_file(file, data, selection, include_animation, os.path.splitext(os.path.basename(bpy.data.filepath))[0]) else: comp_name = "BlendComp" - jsx_file.write('\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","%s","Composition\'s Name");\n' % comp_name) + jsx_file.write(f'\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","{comp_name}","Composition\'s Name");\n') jsx_file.write('if (compName){') # Continue only if comp name is given. If not - terminate jsx_file.write( - '\nvar newComp = app.project.items.addComp(compName, %i, %i, %f, %f, %f);' - % (data['width'], data['height'], data['aspect'], - data['duration'], data['fps'])) - jsx_file.write('\nnewComp.displayStartTime = %f;\n\n' - % ((data['start'] + 1.0) / data['fps'])) + f'\nvar newComp = app.project.items.addComp(compName, {data["width"]}, ' + f'{data["height"]}, {data["aspect"]}, {data["duration"]}, {data["fps"]});') + jsx_file.write(f"\nnewComp.displayStartTime = {(data['start']) / data['fps']};\n\n") jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n') # Create camera bundles (nulls) jsx_file.write('// ************** CAMERA 3D MARKERS **************\n\n') for name_ae, obj in js_data['bundles_cam'].items(): - jsx_file.write('var %s = newComp.layers.addNull();\n' % (name_ae)) - jsx_file.write('%s.threeDLayer = true;\n' % name_ae) - jsx_file.write('%s.source.name = "%s";\n' % (name_ae, name_ae)) - jsx_file.write('%s.property("position").setValue(%s);\n\n' - % (name_ae, obj['position'])) + jsx_file.write(f'var {name_ae} = newComp.layers.addNull();\n') + jsx_file.write(f'{name_ae}.threeDLayer = true;\n') + jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') + jsx_file.write(f'{name_ae}.property("position").setValue({obj["position"]});\n\n') jsx_file.write('\n') # Create object bundles (not ready yet) @@ -940,19 +912,17 @@ def write_jsx_file(file, data, selection, include_animation, # Create objects (nulls) jsx_file.write('// ************** OBJECTS **************\n\n') for name_ae, obj in js_data['nulls'].items(): - jsx_file.write('var %s = newComp.layers.addNull();\n' % (name_ae)) - jsx_file.write('%s.threeDLayer = true;\n' % name_ae) - jsx_file.write('%s.source.name = "%s";\n' % (name_ae, name_ae)) + jsx_file.write(f'var {name_ae} = newComp.layers.addNull();\n') + jsx_file.write(f'{name_ae}.threeDLayer = true;\n') + jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') # Set values of properties, add keyframes only where needed for prop in ("position", "orientation", "scale"): if include_animation and obj[prop + '_anim']: jsx_file.write( - '%s.property("%s").setValuesAtTimes([%s],[%s]);\n' - % (name_ae, prop, js_data['times'], obj[prop])) + f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') else: jsx_file.write( - '%s.property("%s").setValue(%s);\n' - % (name_ae, prop, obj[prop + '_static'])) + f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') jsx_file.write('\n') jsx_file.write('\n') @@ -960,26 +930,18 @@ def write_jsx_file(file, data, selection, include_animation, jsx_file.write('// ************** SOLIDS **************\n\n') for name_ae, obj in js_data['solids'].items(): jsx_file.write( - 'var %s = newComp.layers.addSolid(%s,"%s",%i,%i,%f);\n' % ( - name_ae, - obj['color'], - name_ae, - obj['width'], - obj['height'], - 1.0)) - jsx_file.write('%s.source.name = "%s";\n' % (name_ae, name_ae)) - jsx_file.write('%s.source.parentFolder = footageFolder;\n' % (name_ae)) - jsx_file.write('%s.threeDLayer = true;\n' % name_ae) + f'var {name_ae} = newComp.layers.addSolid({obj["color"]},"{name_ae}",{obj["width"]},{obj["height"]},1.0);\n') + jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') + jsx_file.write(f'{name_ae}.source.parentFolder = footageFolder;\n') + jsx_file.write(f'{name_ae}.threeDLayer = true;\n') # Set values of properties, add keyframes only where needed for prop in ("position", "orientation", "scale", "opacity"): if include_animation and obj[prop + '_anim']: jsx_file.write( - '%s.property("%s").setValuesAtTimes([%s],[%s]);\n' - % (name_ae, prop, js_data['times'], obj[prop])) + f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') else: jsx_file.write( - '%s.property("%s").setValue(%s);\n' - % (name_ae, prop, obj[prop + '_static'])) + f'{name_ae}.property("{prop}").setValue([{obj[prop + "_static"]});\n') jsx_file.write('\n') jsx_file.write('\n') @@ -987,25 +949,19 @@ def write_jsx_file(file, data, selection, include_animation, jsx_file.write('// ************** IMAGES **************\n\n') for name_ae, obj in js_data['images'].items(): jsx_file.write( - 'var newFootage = app.project.importFile(new ImportOptions(File("%s")));\n' - % (obj['filepath'])) + f'var newFootage = app.project.importFile(new ImportOptions(File("{obj["filepath"]}")));\n') jsx_file.write('newFootage.parentFolder = footageFolder;\n') jsx_file.write( - 'var %s = newComp.layers.add(newFootage);\n' % (name_ae)) - jsx_file.write( - '%s.threeDLayer = true;\n' % name_ae) - jsx_file.write( - '%s.source.name = "%s";\n' % (name_ae, name_ae)) + f'var {name_ae} = newComp.layers.add(newFootage);\n') + jsx_file.write(f'{name_ae}.threeDLayer = true;\n') + jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') # Set values of properties, add keyframes only where needed for prop in ("position", "orientation", "scale", "opacity"): if include_animation and obj[prop + '_anim']: jsx_file.write( - '%s.property("%s").setValuesAtTimes([%s],[%s]);\n' - % (name_ae, prop, js_data['times'], obj[prop])) + f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') else: - jsx_file.write( - '%s.property("%s").setValue(%s);\n' - % (name_ae, prop, obj[prop + '_static'])) + jsx_file.write(f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') jsx_file.write('\n') jsx_file.write('\n') @@ -1013,11 +969,9 @@ def write_jsx_file(file, data, selection, include_animation, jsx_file.write('// ************** LIGHTS **************\n\n') for name_ae, obj in js_data['lights'].items(): jsx_file.write( - 'var %s = newComp.layers.addLight("%s", [0.0, 0.0]);\n' - % (name_ae, name_ae)) + f'var {name_ae} = newComp.layers.addLight("{name_ae}", [0.0, 0.0]);\n') jsx_file.write( - '%s.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' - % name_ae) + f'{name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n') # Set values of properties, add keyframes only where needed props = ["position", "orientation", "intensity", "Color", "opacity"] if obj['type'] == 'SPOT': @@ -1025,12 +979,10 @@ def write_jsx_file(file, data, selection, include_animation, for prop in props: if include_animation and obj[prop + '_anim']: jsx_file.write( - '%s.property("%s").setValuesAtTimes([%s],[%s]);\n' - % (name_ae, prop, js_data['times'], obj[prop])) + f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') else: jsx_file.write( - '%s.property("%s").setValue(%s);\n' - % (name_ae, prop, obj[prop + '_static'])) + f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') jsx_file.write('\n') jsx_file.write('\n') @@ -1039,22 +991,17 @@ def write_jsx_file(file, data, selection, include_animation, for name_ae, obj in js_data['cameras'].items(): # More than one camera can be selected jsx_file.write( - 'var %s = newComp.layers.addCamera("%s",[0,0]);\n' - % (name_ae, name_ae)) + f'var {name_ae} = newComp.layers.addCamera("{name_ae}",[0,0]);\n') jsx_file.write( - '%s.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' - % name_ae) + f'{name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n') # Set values of properties, add keyframes only where needed for prop in ("position", "orientation", "zoom"): if include_animation and obj[prop + '_anim']: jsx_file.write( - '%s.property("%s").setValuesAtTimes([%s],[%s]);\n' - % (name_ae, prop, js_data['times'], obj[prop])) + f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') else: - jsx_file.write( - '%s.property("%s").setValue(%s);\n' - % (name_ae, prop, obj[prop + '_static'])) + jsx_file.write(f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') jsx_file.write('\n') jsx_file.write('\n') -- 2.30.2 From afb79c2611c0ffe6a09cd35d899c6e2bad73d59b Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Tue, 15 Jun 2021 10:27:31 +0200 Subject: [PATCH 04/33] After Effects export: refactor object types into classes Use classes for each object type, which include data and animation collection, and script generation, instead of long and similar functions for each of these steps. Also: - add option to export solids; - do not systematically prefix all AE object names with an underscore, as this is not so useful and can be frustrating in comps. --- io_export_after_effects.py | 994 ++++++++++++++----------------------- 1 file changed, 360 insertions(+), 634 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 8d73d19..4861de9 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -22,8 +22,8 @@ bl_info = { "name": "Export: Adobe After Effects (.jsx)", "description": "Export cameras, selected objects & camera solution " "3D Markers to Adobe After Effects CS3 and above", - "author": "Bartek Skorupa", - "version": (0, 0, 70), + "author": "Bartek Skorupa, Damien Picard (@pioverfour)", + "version": (0, 1, 0), "blender": (2, 80, 0), "location": "File > Export > Adobe After Effects (.jsx)", "warning": "", @@ -61,7 +61,7 @@ def get_comp_data(context): 'end': end, 'duration': (end - start + 1.0) / fps, 'active_cam_frames': active_cam_frames, - 'curframe': scene.frame_current, + 'frame_current': scene.frame_current, } @@ -97,40 +97,285 @@ def get_active_cam_for_each_frame(scene, start, end): return(active_cam_frames) -def get_selected(context): - """Create manageable list of selected objects""" - cameras = [] # List of selected cameras - solids = [] # List of selected meshes exported as AE solids - images = [] # List of selected meshes exported as AE AV layers - lights = [] # List of selected lights exported as AE lights - nulls = [] # List of selected objects except cameras (will be used to create nulls in AE) - obs = context.selected_objects +class ObjectExport(): + """Base exporter class - for ob in obs: - if ob.type == 'CAMERA': - cameras.append(ob) + Collects data about an object and outputs the proper JSX script for AE. + """ + def __init__(self, obj): + self.obj = obj + self.name_ae = convert_name(self.obj.name) + self.keyframes = {} - elif is_image_plane(ob): - images.append(ob) + def get_prop_keyframe(self, context, prop_name, value, time): + """Set keyframe for given property""" + prop_keys = self.keyframes.setdefault(prop_name, []) + if not len(prop_keys) or value != prop_keys[-1][1]: + prop_keys.append((time, value)) - elif is_plane(ob): - solids.append(ob) + def get_keyframe(self, context, data, time, ae_size): + """Store animation for the current frame""" + ae_transform = convert_transform_matrix(self.obj.matrix_world, + data['width'], data['height'], + data['aspect'], True, ae_size) - elif ob.type == 'LIGHT': - lights.append(ob) + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'scale', ae_transform[6:9], time) + def get_obj_script(self, include_animation): + """Get the JSX script for the object""" + return self.get_type_script() + self.get_prop_script(include_animation) + self.get_post_script() + + def get_type_script(self): + """Get the basic part of the JSX script""" + type_script = f'var {self.name_ae} = newComp.layers.addNull();\n' + type_script += f'{self.name_ae}.threeDLayer = true;\n' + type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' + return type_script + + def get_prop_script(self, include_animation): + """Get the part of the JSX script encoding animation""" + prop_script = "" + + # Set values of properties, add keyframes only where needed + for prop, keys in self.keyframes.items(): + if include_animation and len(keys) > 1: + times = ",".join((str(k[0]) for k in keys)) + values = ",".join((str(k[1]) for k in keys)).replace(" ", "") + prop_script += ( + f'{self.name_ae}.property("{prop}").setValuesAtTimes([{times}],[{values}]);\n') + else: + value = str(keys[0][1]).replace(" ", "") + prop_script += ( + f'{self.name_ae}.property("{prop}").setValue({value});\n') + prop_script += '\n' + + return prop_script + + def get_post_script(self): + """This is only used in lights as a post-treatment after animation""" + return "" + +class CameraExport(ObjectExport): + def get_keyframe(self, context, data, time, ae_size): + ae_transform = convert_transform_matrix(self.obj.matrix_world, + data['width'], data['height'], + data['aspect'], True, ae_size) + zoom = convert_lens(self.obj, data['width'], data['height'], + data['aspect']) + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'zoom', zoom, time) + + def get_type_script(self): + type_script = f'var {self.name_ae} = newComp.layers.addCamera("{self.name_ae}",[0,0]);\n' + type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' + return type_script + + +class LightExport(ObjectExport): + def get_keyframe(self, context, data, time, ae_size): + ae_transform = convert_transform_matrix(self.obj.matrix_world, + data['width'], data['height'], + data['aspect'], True, ae_size) + self.type = self.obj.data.type + color = list(self.obj.data.color) + intensity = self.obj.data.energy * 10.0 + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + if self.type in {'SPOT', 'SUN'}: + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'intensity', intensity, time) + self.get_prop_keyframe(context, 'Color', color, time) + if self.type == 'SPOT': + cone_angle = degrees(self.obj.data.spot_size) + self.get_prop_keyframe(context, 'Cone Angle', cone_angle, time) + cone_feather = self.obj.data.spot_blend * 100.0 + self.get_prop_keyframe(context, 'Cone Feather', cone_feather, time) + + def get_type_script(self): + type_script = f'var {self.name_ae} = newComp.layers.addLight("{self.name_ae}", [0.0, 0.0]);\n' + type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' + type_script += f'{self.name_ae}.lightType = LightType.SPOT;\n' + return type_script + + def get_post_script(self): + """Set light type _after_ the orientation, otherwise the property is hidden in AE...""" + if self.obj.data.type == 'SUN': + post_script = f'{self.name_ae}.lightType = LightType.PARALLEL;\n' + elif self.obj.data.type == 'SPOT': + post_script = f'{self.name_ae}.lightType = LightType.SPOT;\n' else: - nulls.append(ob) + post_script = f'{self.name_ae}.lightType = LightType.POINT;\n' + return post_script - selection = { - 'cameras': cameras, - 'images': images, - 'solids': solids, - 'lights': lights, - 'nulls': nulls, - } - return selection +class ImageExport(ObjectExport): + def get_keyframe(self, context, data, time, ae_size): + # Convert obj transform properties to AE space + plane_matrix = get_image_plane_matrix(self.obj) + # Scale plane to account for AE's transforms + plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4) + + ae_transform = convert_transform_matrix(plane_matrix, data['width'], + data['height'], data['aspect'], + True, ae_size) + opacity = 0.0 if self.obj.hide_render else 100.0 + + if not hasattr(self, 'filepath'): + self.filepath = get_image_filepath(self.obj) + + image_width, image_height = get_image_size(self.obj) + ratio_to_comp = image_width / data['width'] + scale = ae_transform[6:9] + scale[0] /= ratio_to_comp + scale[1] = scale[1] / ratio_to_comp * image_width / image_height + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'scale', scale, time) + self.get_prop_keyframe(context, 'opacity', opacity, time) + + def get_type_script(self): + type_script = f'var newFootage = app.project.importFile(new ImportOptions(File("{self.filepath}")));\n' + type_script += 'newFootage.parentFolder = footageFolder;\n' + type_script += f'var {self.name_ae} = newComp.layers.add(newFootage);\n' + type_script += f'{self.name_ae}.threeDLayer = true;\n' + type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' + return type_script + + +class SolidExport(ObjectExport): + def get_keyframe(self, context, data, time, ae_size): + # Convert obj transform properties to AE space + plane_matrix = get_plane_matrix(self.obj) + # Scale plane to account for AE's transforms + plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4) + + ae_transform = convert_transform_matrix(plane_matrix, data['width'], + data['height'], data['aspect'], + True, ae_size) + opacity = 0.0 if self.obj.hide_render else 100.0 + if not hasattr(self, 'color'): + self.color = get_plane_color(self.obj) + if not hasattr(self, 'width'): + self.width = data['width'] + if not hasattr(self, 'height'): + self.height = data['height'] + + scale = ae_transform[6:9] + scale[1] *= data['width'] / data['height'] + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'scale', scale, time) + self.get_prop_keyframe(context, 'opacity', opacity, time) + + def get_type_script(self): + type_script = f'var {self.name_ae} = newComp.layers.addSolid({self.color},"{self.name_ae}",{self.width},{self.height},1.0);\n' + type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' + type_script += f'{self.name_ae}.source.parentFolder = footageFolder;\n' + type_script += f'{self.name_ae}.threeDLayer = true;\n' + return type_script + + +class CamBundleExport(ObjectExport): + def __init__(self, obj, track): + self.obj = obj + self.track = track + self.name_ae = convert_name(f'{obj.name}__{track.name}') + self.keyframes = {} + + def get_keyframe(self, context, data, time, ae_size): + # Bundles are in camera space. + # Transpose to world space + matrix = Matrix.Translation(self.obj.matrix_basis + @ self.track.bundle) + # Convert the position into AE space + ae_transform = convert_transform_matrix(matrix, data['width'], + data['height'], + data['aspect'], False, + ae_size) + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + + def get_type_script(self): + type_script = f'var {self.name_ae} = newComp.layers.addNull();\n' + type_script += f'{self.name_ae}.threeDLayer = true;\n' + type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' + return type_script + + +def get_camera_bundles(scene, camera): + cam_bundles = [] + + for constraint in camera.constraints: + if constraint.type == 'CAMERA_SOLVER': + # Which movie clip does it use + if constraint.use_active_clip: + clip = scene.active_clip + else: + clip = constraint.clip + + # Go through each tracking point + for track in clip.tracking.tracks: + # Does this tracking point have a bundle + # (has its 3D position been solved) + if track.has_bundle: + cam_bundles.append(CamBundleExport(camera, track)) + + return cam_bundles + + +def get_selected(context, include_active_cam, include_selected_cams, + include_selected_objects, include_cam_bundles, + include_image_planes, include_solids): + """Create manageable list of selected objects""" + cameras = [] + solids = [] # Meshes exported as AE solids + images = [] # Meshes exported as AE AV layers + lights = [] # Lights exported as AE lights + cam_bundles = [] # Camera trackers exported as AE nulls + nulls = [] # Remaining objects exported as AE nulls + + if context.scene.camera is not None: + if include_active_cam: + cameras.append(CameraExport(context.scene.camera)) + if include_cam_bundles: + cam_bundles.extend(get_camera_bundles(context.scene, context.scene.camera)) + + for obj in context.selected_objects: + if obj.type == 'CAMERA': + if (include_active_cam + and obj is context.scene.camera): + # Ignore active camera if already selected + continue + else: + if include_selected_cams: + cameras.append(CameraExport(obj)) + if include_cam_bundles: + cam_bundles.extend(get_camera_bundles(context.scene, obj)) + + elif include_image_planes and is_image_plane(obj): + images.append(ImageExport(obj)) + + elif include_solids and is_plane(obj): + solids.append(SolidExport(obj)) + + elif include_selected_objects: + if obj.type == 'LIGHT': + lights.append(LightExport(obj)) + else: + nulls.append(ObjectExport(obj)) + + return {'cameras': cameras, + 'images': images, + 'solids': solids, + 'lights': lights, + 'nulls': nulls, + 'cam_bundles': cam_bundles} def get_first_material(obj): @@ -276,14 +521,8 @@ def get_image_plane_matrix(obj): def convert_name(name): """Convert names of objects to avoid errors in AE""" - name = "_" + name - ''' - # Digits are not allowed at beginning of AE vars names. - # This section is commented, as "_" is added at beginning of names anyway. - # Placeholder for this name modification is left so that it's not ignored if needed - if name[0].isdigit(): + if not name[0].isalpha(): name = "_" + name - ''' name = bpy.path.clean_name(name) name = name.replace("-", "_") @@ -300,7 +539,7 @@ def convert_transform_matrix(matrix, width, height, aspect, scale_mat = Matrix.Scale(width, 4) - # Get blender transform data for ob + # Get blender transform data for object b_loc = matrix.to_translation() b_rot = matrix.to_euler('ZYX') # ZYX euler matches AE's orientation and allows to use x_rot_correction b_scale = matrix.to_scale() @@ -319,7 +558,7 @@ def convert_transform_matrix(matrix, width, height, aspect, ry = -degrees(b_rot.y) # AE's Y orientation = -blender's Y rotation if 'ZYX' euler rz = -degrees(b_rot.z) # AE's Z orientation = -blender's Z rotation if 'ZYX' euler if x_rot_correction: - # In Blender, ob of zero rotation lays on floor. + # In Blender, object of zero rotation lays on floor. # In AE, layer of zero orientation "stands" rx -= 90.0 # Convert scale to AE scale. ae_size is a global multiplier. @@ -327,7 +566,8 @@ def convert_transform_matrix(matrix, width, height, aspect, sy = b_scale.y * ae_size sz = b_scale.z * ae_size - return x, y, z, rx, ry, rz, sx, sy, sz + return [x, y, z, rx, ry, rz, sx, sy, sz] + # Get camera's lens and convert to AE's "zoom" value in pixels # this function will be called for every camera for every frame @@ -387,7 +627,6 @@ def convert_transform_matrix(matrix, width, height, aspect, # aspect compensation is needed, so final formula is: # zoom = lens * dimension / sensor * aspect - def convert_lens(camera, width, height, aspect): if camera.data.sensor_fit == 'VERTICAL': sensor = camera.data.sensor_height @@ -411,612 +650,86 @@ def convert_lens(camera, width, height, aspect): # return matrix -def write_jsx_file(file, data, selection, include_animation, - include_active_cam, include_selected_cams, - include_selected_objects, include_cam_bundles, - include_image_planes, ae_size): +def write_jsx_file(context, file, data, selection, include_animation, ae_size): """jsx script for AE creation""" - print("\n---------------------------\n- Export to After Effects -\n---------------------------") + print("\n---------------------------\n" + "- Export to After Effects -\n" + "---------------------------") + # Store the current frame to restore it at the end of export - curframe = data['curframe'] - # Create array which will contain all keyframes values - js_data = { - 'times': '', - 'cameras': {}, - 'images': {}, - 'solids': {}, - 'lights': {}, - 'nulls': {}, - 'bundles_cam': {}, - 'bundles_ob': {}, # not ready yet - } - - # Create structure for active camera/cameras - active_cam_name = '' - if include_active_cam and data['active_cam_frames']: - # Check if more than one active cam exists - # (True if active cams set by markers) - if len(data['active_cam_frames']) == 1: - # Take name of the only active camera in scene - name_ae = convert_name(data['active_cam_frames'][0].name) - else: - name_ae = 'Active_Camera' - # Store name to be used when creating keyframes for active cam - active_cam_name = name_ae - js_data['cameras'][name_ae] = { - 'position': '', - 'position_static': '', - 'position_anim': False, - 'orientation': '', - 'orientation_static': '', - 'orientation_anim': False, - 'zoom': '', - 'zoom_static': '', - 'zoom_anim': False, - } - - # Create camera structure for selected cameras - if include_selected_cams: - for obj in selection['cameras']: - # More than one camera can be selected - if convert_name(obj.name) != active_cam_name: - name_ae = convert_name(obj.name) - js_data['cameras'][name_ae] = { - 'position': '', - 'position_static': '', - 'position_anim': False, - 'orientation': '', - 'orientation_static': '', - 'orientation_anim': False, - 'zoom': '', - 'zoom_static': '', - 'zoom_anim': False, - } - - # Create structure for solids - for obj in selection['solids']: - name_ae = convert_name(obj.name) - js_data['solids'][name_ae] = { - 'position': '', - 'position_static': '', - 'position_anim': False, - 'orientation': '', - 'orientation_static': '', - 'orientation_anim': False, - 'scale': '', - 'scale_static': '', - 'scale_anim': False, - 'opacity': '', - 'opacity_static': '', - 'opacity_anim': False, - } - - # Create structure for images - for obj in selection['images']: - name_ae = convert_name(obj.name) - js_data['images'][name_ae] = { - 'position': '', - 'position_static': '', - 'position_anim': False, - 'orientation': '', - 'orientation_static': '', - 'orientation_anim': False, - 'scale': '', - 'scale_static': '', - 'scale_anim': False, - 'opacity': '', - 'opacity_static': '', - 'opacity_anim': False, - 'filepath': '', - } - - # Create structure for lights - for obj in selection['lights']: - if include_selected_objects: - name_ae = obj.data.type + convert_name(obj.name) - js_data['lights'][name_ae] = { - 'type': obj.data.type, - 'intensity': '', - 'intensity_static': '', - 'intensity_anim': False, - 'Cone Angle': '', - 'Cone Angle_static': '', - 'Cone Angle_anim': False, - 'Cone Feather': '', - 'Cone Feather_static': '', - 'Cone Feather_anim': False, - 'Color': '', - 'Color_static': '', - 'Color_anim': False, - 'position': '', - 'position_static': '', - 'position_anim': False, - 'orientation': '', - 'orientation_static': '', - 'orientation_anim': False, - 'opacity': '', - 'opacity_static': '', - 'opacity_anim': False, - } - - # Create structure for nulls - # nulls representing blender's obs except cameras, lights and solids - for obj in selection['nulls']: - if include_selected_objects: - name_ae = convert_name(obj.name) - js_data['nulls'][name_ae] = { - 'position': '', - 'position_static': '', - 'position_anim': False, - 'orientation': '', - 'orientation_static': '', - 'orientation_anim': False, - 'scale': '', - 'scale_static': '', - 'scale_anim': False, - } - - # Create structure for cam bundles including positions - # (cam bundles don't move) - if include_cam_bundles: - # Go through each selected camera and active cameras - selected_cams = [] - active_cams = [] - if include_active_cam: - active_cams = data['active_cam_frames'] - if include_selected_cams: - for cam in selection['cameras']: - selected_cams.append(cam) - # List of cameras that will be checked for 'CAMERA SOLVER' - cams = list(set.union(set(selected_cams), set(active_cams))) - - for cam in cams: - # Go through each constraints of this camera - for constraint in cam.constraints: - # Does the camera have a Camera Solver constraint - if constraint.type == 'CAMERA_SOLVER': - # Which movie clip does it use - if constraint.use_active_clip: - clip = data['scn'].active_clip - else: - clip = constraint.clip - - # Go through each tracking point - for track in clip.tracking.tracks: - # Does this tracking point have a bundle - # (has its 3D position been solved) - if track.has_bundle: - # Get the name of the tracker - name_ae = convert_name(str(cam.name) + '__' + - str(track.name)) - js_data['bundles_cam'][name_ae] = { - 'position': '', - } - # Bundles are in camera space. - # Transpose to world space - matrix = Matrix.Translation(cam.matrix_basis.copy() - @ track.bundle) - # Convert the position into AE space - ae_transform = (convert_transform_matrix( - matrix, data['width'], data['height'], - data['aspect'], False, ae_size)) - js_data['bundles_cam'][name_ae]['position'] += f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' + frame_current = data['frame_current'] # Get all keyframes for each object and store in dico if include_animation: end = data['end'] + 1 else: end = data['start'] + 1 + for frame in range(data['start'], end): - print("working on frame: " + str(frame)) + print("Working on frame: " + str(frame)) data['scn'].frame_set(frame) # Get time for this loop - js_data['times'] += str((frame - data['start']) / data['fps']) + ',' + time = (frame - data['start']) / data['fps'] - # Keyframes for active camera/cameras - if include_active_cam and data['active_cam_frames'] != []: - if len(data['active_cam_frames']) == 1: - cur_cam_index = 0 - else: - cur_cam_index = frame - data['start'] - active_cam = data['active_cam_frames'][cur_cam_index] - # Get cam name - name_ae = active_cam_name - # Convert cam transform properties to AE space - ae_transform = (convert_transform_matrix( - active_cam.matrix_world.copy(), data['width'], data['height'], - data['aspect'], True, ae_size)) - # Convert Blender's lens to AE's zoom in pixels - zoom = convert_lens(active_cam, data['width'], data['height'], - data['aspect']) - # Store all values in dico - - position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - zoom = str(zoom) + ',' - js_camera = js_data['cameras'][name_ae] - js_camera['position'] += position - js_camera['orientation'] += orientation - js_camera['zoom'] += zoom - # Check if properties change values compared to previous frame - # If property don't change through out the whole animation, - # keyframes won't be added - if frame != data['start']: - if position != js_camera['position_static']: - js_camera['position_anim'] = True - if orientation != js_camera['orientation_static']: - js_camera['orientation_anim'] = True - if zoom != js_camera['zoom_static']: - js_camera['zoom_anim'] = True - js_camera['position_static'] = position - js_camera['orientation_static'] = orientation - js_camera['zoom_static'] = zoom - - # Keyframes for selected cameras - if include_selected_cams: - for obj in selection['cameras']: - if convert_name(obj.name) != active_cam_name: - # Get cam name - name_ae = convert_name(obj.name) - # Convert cam transform properties to AE space - ae_transform = convert_transform_matrix( - obj.matrix_world.copy(), data['width'], - data['height'], data['aspect'], True, ae_size) - # Convert Blender's lens to AE's zoom in pixels - zoom = convert_lens(obj, data['width'], data['height'], - data['aspect']) - # Store all values in dico - position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - zoom = str(zoom) + ',' - js_camera = js_data['cameras'][name_ae] - js_camera['position'] += position - js_camera['orientation'] += orientation - js_camera['zoom'] += zoom - # Check if properties change values compared to previous frame - # If property don't change through out the whole animation, - # keyframes won't be added - if frame != data['start']: - if position != js_camera['position_static']: - js_camera['position_anim'] = True - if orientation != js_camera['orientation_static']: - js_camera['orientation_anim'] = True - if zoom != js_camera['zoom_static']: - js_camera['zoom_anim'] = True - js_camera['position_static'] = position - js_camera['orientation_static'] = orientation - js_camera['zoom_static'] = zoom - - # Keyframes for all solids. - if include_selected_objects: - for obj in selection['solids']: - # Get object name - name_ae = convert_name(obj.name) - # Convert obj transform properties to AE space - plane_matrix = get_plane_matrix(obj) - # Scale plane to account for AE's transforms - plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4) - ae_transform = convert_transform_matrix( - plane_matrix, data['width'], data['height'], - data['aspect'], True, ae_size) - # Store all values in dico - position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - scale = f'[{ae_transform[6]},{ae_transform[7] * data["width"] / data["height"]},{ae_transform[8]}],' - opacity = '0.0,' if obj.hide_render else '100.0,' - js_solid = js_data['solids'][name_ae] - js_solid['color'] = get_plane_color(obj) - js_solid['width'] = data['width'] - js_solid['height'] = data['height'] - js_solid['position'] += position - js_solid['orientation'] += orientation - js_solid['scale'] += scale - js_solid['opacity'] += opacity - # Check if properties change values compared to previous frame - # If property don't change through out the whole animation, - # keyframes won't be added - if frame != data['start']: - if position != js_solid['position_static']: - js_solid['position_anim'] = True - if orientation != js_solid['orientation_static']: - js_solid['orientation_anim'] = True - if scale != js_solid['scale_static']: - js_solid['scale_anim'] = True - if opacity != js_solid['opacity_static']: - js_solid['opacity_anim'] = True - js_solid['position_static'] = position - js_solid['orientation_static'] = orientation - js_solid['scale_static'] = scale - js_solid['opacity_static'] = opacity - - # Keyframes for all lights. - if include_selected_objects: - for obj in selection['lights']: - # Get object name - name_ae = obj.data.type + convert_name(obj.name) - type = obj.data.type - # Convert ob transform properties to AE space - ae_transform = convert_transform_matrix( - obj.matrix_world.copy(), data['width'], data['height'], - data['aspect'], True, ae_size) - color = obj.data.color - # Store all values in dico - position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - energy = f'[{obj.data.energy * 100.0}],' - color = f'[{color[0]},{color[1]},{color[2]}],' - opacity = '0.0,' if obj.hide_render else '100.0,' - js_light = js_data['lights'][name_ae] - js_light['position'] += position - js_light['orientation'] += orientation - js_light['intensity'] += energy - js_light['Color'] += color - js_light['opacity'] += opacity - # Check if properties change values compared to previous frame - # If property don't change through out the whole animation, - # keyframes won't be added - if frame != data['start']: - if position != js_light['position_static']: - js_light['position_anim'] = True - if orientation != js_light['orientation_static']: - js_light['orientation_anim'] = True - if energy != js_light['intensity_static']: - js_light['intensity_anim'] = True - if color != js_light['Color_static']: - js_light['Color_anim'] = True - if opacity != js_light['opacity_static']: - js_light['opacity_anim'] = True - js_light['position_static'] = position - js_light['orientation_static'] = orientation - js_light['intensity_static'] = energy - js_light['Color_static'] = color - js_light['opacity_static'] = opacity - if type == 'SPOT': - cone_angle = f'[{degrees(obj.data.spot_size)}],' - cone_feather = f'[obj.data.spot_blend * 100.0],' - js_light['Cone Angle'] += cone_angle - js_light['Cone Feather'] += cone_feather - # Check if properties change values compared to previous frame - # If property don't change through out the whole animation, - # keyframes won't be added - if frame != data['start']: - if cone_angle != js_light['Cone Angle_static']: - js_light['Cone Angle_anim'] = True - if cone_feather != js_light['Cone Feather_static']: - js_light['Cone Feather_anim'] = True - js_light['Cone Angle_static'] = cone_angle - js_light['Cone Feather_static'] = cone_feather - - # Keyframes for all nulls - if include_selected_objects: - for obj in selection['nulls']: - # Get object name - name_ae = convert_name(obj.name) - # Convert obj transform properties to AE space - ae_transform = convert_transform_matrix(obj.matrix_world.copy(), data['width'], data['height'], data['aspect'], True, ae_size) - # Store all values in dico - position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - scale = f'[{ae_transform[6]},{ae_transform[7]},{ae_transform[8]}],' - js_null = js_data['nulls'][name_ae] - js_null['position'] += position - js_null['orientation'] += orientation - js_null['scale'] += scale - # Check if properties change values compared to previous frame - # If property don't change through out the whole animation, - # keyframes won't be added - if frame != data['start']: - if position != js_null['position_static']: - js_null['position_anim'] = True - if orientation != js_null['orientation_static']: - js_null['orientation_anim'] = True - if scale != js_null['scale_static']: - js_null['scale_anim'] = True - js_null['position_static'] = position - js_null['orientation_static'] = orientation - js_null['scale_static'] = scale - - # Keyframes for all images - if include_image_planes: - for obj in selection['images']: - # Get object name - name_ae = convert_name(obj.name) - # Convert obj transform properties to AE space - plane_matrix = get_image_plane_matrix(obj) - # Scale plane to account for AE's transforms - plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4) - ae_transform = convert_transform_matrix( - plane_matrix, data['width'], data['height'], - data['aspect'], True, ae_size) - # Store all values in dico - position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - image_width, image_height = get_image_size(obj) - ratio_to_comp = image_width / data['width'] - scale = f'[{ae_transform[6] / ratio_to_comp},{ae_transform[7] / ratio_to_comp * image_width / image_height},{ae_transform[8]}],' - opacity = '0.0,' if obj.hide_render else '100.0,' - js_image = js_data['images'][name_ae] - js_image['position'] += position - js_image['orientation'] += orientation - js_image['scale'] += scale - js_image['opacity'] += opacity - # Check if properties change values compared to previous frame - # If property don't change through out the whole animation, - # keyframes won't be added - if frame != data['start']: - if position != js_image['position_static']: - js_image['position_anim'] = True - if orientation != js_image['orientation_static']: - js_image['orientation_anim'] = True - if scale != js_image['scale_static']: - js_image['scale_anim'] = True - if opacity != js_image['opacity_static']: - js_image['opacity_anim'] = True - js_image['position_static'] = position - js_image['orientation_static'] = orientation - js_image['scale_static'] = scale - js_image['opacity_static'] = opacity - js_image['filepath'] = get_image_filepath(obj) - - # keyframes for all object bundles. Not ready yet. - # - # - # + for obj_type in selection.values(): + for obj in obj_type: + obj.get_keyframe(context, data, time, ae_size) # ---- write JSX file - jsx_file = open(file, 'w') + with open(file, 'w') as jsx_file: - # Make the jsx executable in After Effects (enable double click on jsx) - jsx_file.write('#target AfterEffects\n\n') - # Script's header - jsx_file.write('/**************************************\n') - jsx_file.write(f'Scene : {data["scn"].name}\n') - jsx_file.write(f'Resolution : {data["width"]} x {data["height"]}\n') - jsx_file.write(f'Duration : {data["duration"]}\n') - jsx_file.write(f'FPS : {data["fps"]}\n') - jsx_file.write(f'Date : {datetime.datetime.now()}\n') - jsx_file.write(f'Exported with io_export_after_effects.py\n') - jsx_file.write(f'**************************************/\n\n\n\n') + # Make the jsx executable in After Effects (enable double click on jsx) + jsx_file.write('#target AfterEffects\n\n') + # Script's header + jsx_file.write('/**************************************\n') + jsx_file.write(f'Scene : {data["scn"].name}\n') + jsx_file.write(f'Resolution : {data["width"]} x {data["height"]}\n') + jsx_file.write(f'Duration : {data["duration"]}\n') + jsx_file.write(f'FPS : {data["fps"]}\n') + jsx_file.write(f'Date : {datetime.datetime.now()}\n') + jsx_file.write(f'Exported with io_export_after_effects.py\n') + jsx_file.write(f'**************************************/\n\n\n\n') - # Wrap in function - jsx_file.write("function compFromBlender(){\n") - # Create new comp - if bpy.data.filepath: - comp_name = convert_name( - os.path.splitext(os.path.basename(bpy.data.filepath))[0]) - else: - comp_name = "BlendComp" - jsx_file.write(f'\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","{comp_name}","Composition\'s Name");\n') - jsx_file.write('if (compName){') - # Continue only if comp name is given. If not - terminate - jsx_file.write( - f'\nvar newComp = app.project.items.addComp(compName, {data["width"]}, ' - f'{data["height"]}, {data["aspect"]}, {data["duration"]}, {data["fps"]});') - jsx_file.write(f"\nnewComp.displayStartTime = {(data['start']) / data['fps']};\n\n") + # Wrap in function + jsx_file.write("function compFromBlender(){\n") - jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n') - - # Create camera bundles (nulls) - jsx_file.write('// ************** CAMERA 3D MARKERS **************\n\n') - for name_ae, obj in js_data['bundles_cam'].items(): - jsx_file.write(f'var {name_ae} = newComp.layers.addNull();\n') - jsx_file.write(f'{name_ae}.threeDLayer = true;\n') - jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') - jsx_file.write(f'{name_ae}.property("position").setValue({obj["position"]});\n\n') - jsx_file.write('\n') - - # Create object bundles (not ready yet) - - # Create objects (nulls) - jsx_file.write('// ************** OBJECTS **************\n\n') - for name_ae, obj in js_data['nulls'].items(): - jsx_file.write(f'var {name_ae} = newComp.layers.addNull();\n') - jsx_file.write(f'{name_ae}.threeDLayer = true;\n') - jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') - # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "scale"): - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write( - f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') - jsx_file.write('\n') - jsx_file.write('\n') - - # Create solids - jsx_file.write('// ************** SOLIDS **************\n\n') - for name_ae, obj in js_data['solids'].items(): + # Create new comp + if bpy.data.filepath: + comp_name = convert_name( + os.path.splitext(os.path.basename(bpy.data.filepath))[0]) + else: + comp_name = "BlendComp" + jsx_file.write(f'\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","{comp_name}","Composition\'s Name");\n') + jsx_file.write('if (compName){') + # Continue only if comp name is given. If not - terminate jsx_file.write( - f'var {name_ae} = newComp.layers.addSolid({obj["color"]},"{name_ae}",{obj["width"]},{obj["height"]},1.0);\n') - jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') - jsx_file.write(f'{name_ae}.source.parentFolder = footageFolder;\n') - jsx_file.write(f'{name_ae}.threeDLayer = true;\n') - # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "scale", "opacity"): - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write( - f'{name_ae}.property("{prop}").setValue([{obj[prop + "_static"]});\n') - jsx_file.write('\n') - jsx_file.write('\n') + f'\nvar newComp = app.project.items.addComp(compName, {data["width"]}, ' + f'{data["height"]}, {data["aspect"]}, {data["duration"]}, {data["fps"]});') + jsx_file.write(f"\nnewComp.displayStartTime = {(data['start']) / data['fps']};\n\n") - # Create images - jsx_file.write('// ************** IMAGES **************\n\n') - for name_ae, obj in js_data['images'].items(): - jsx_file.write( - f'var newFootage = app.project.importFile(new ImportOptions(File("{obj["filepath"]}")));\n') - jsx_file.write('newFootage.parentFolder = footageFolder;\n') - jsx_file.write( - f'var {name_ae} = newComp.layers.add(newFootage);\n') - jsx_file.write(f'{name_ae}.threeDLayer = true;\n') - jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') - # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "scale", "opacity"): - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write(f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') - jsx_file.write('\n') - jsx_file.write('\n') + jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n') - # Create lights - jsx_file.write('// ************** LIGHTS **************\n\n') - for name_ae, obj in js_data['lights'].items(): - jsx_file.write( - f'var {name_ae} = newComp.layers.addLight("{name_ae}", [0.0, 0.0]);\n') - jsx_file.write( - f'{name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n') - # Set values of properties, add keyframes only where needed - props = ["position", "orientation", "intensity", "Color", "opacity"] - if obj['type'] == 'SPOT': - props.extend(("Cone Angle", "Cone Feather")) - for prop in props: - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write( - f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') - jsx_file.write('\n') - jsx_file.write('\n') + for obj_type in ('cam_bundles', 'nulls', 'solids', 'images', 'lights', 'cameras'): + if len(selection[obj_type]): + type_name = 'CAMERA 3D MARKERS' if obj_type == 'cam_bundles' else obj_type.upper() + jsx_file.write(f'// ************** {type_name} **************\n\n') + for obj in selection[obj_type]: + jsx_file.write(obj.get_obj_script(include_animation)) + jsx_file.write('\n') - # Create cameras - jsx_file.write('// ************** CAMERAS **************\n\n') - for name_ae, obj in js_data['cameras'].items(): - # More than one camera can be selected - jsx_file.write( - f'var {name_ae} = newComp.layers.addCamera("{name_ae}",[0,0]);\n') - jsx_file.write( - f'{name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n') - - # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "zoom"): - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write(f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') - jsx_file.write('\n') - jsx_file.write('\n') - - # Exit import if no comp name given - jsx_file.write('\n}else{alert ("Exit Import Blender animation data \\nNo Comp name has been chosen","EXIT")};') - # Close function - jsx_file.write("}\n\n\n") - # Execute function. Wrap in "undo group" for easy undoing import process - jsx_file.write('app.beginUndoGroup("Import Blender animation data");\n') - jsx_file.write('compFromBlender();\n') # Execute function - jsx_file.write('app.endUndoGroup();\n\n\n') - jsx_file.close() + # Exit import if no comp name given + jsx_file.write('\n}else{alert ("Exit Import Blender animation data \\nNo Comp name has been chosen","EXIT")};') + # Close function + jsx_file.write("}\n\n\n") + # Execute function. Wrap in "undo group" for easy undoing import process + jsx_file.write('app.beginUndoGroup("Import Blender animation data");\n') + jsx_file.write('compFromBlender();\n') # Execute function + jsx_file.write('app.endUndoGroup();\n\n\n') # Set current frame of animation in blender to state before export - data['scn'].frame_set(curframe) + data['scn'].frame_set(frame_current) ########################################## @@ -1066,6 +779,11 @@ class ExportJsx(bpy.types.Operator, ExportHelper): description="Include image mesh objects", default=True, ) + include_solids: BoolProperty( + name="Solids", + description="Include rectangles as solids", + default=True, + ) # include_ob_bundles = BoolProperty( # name="Objects 3D Markers", # description="Include 3D Markers of Object Motion Solution for selected cameras", @@ -1084,19 +802,24 @@ class ExportJsx(bpy.types.Operator, ExportHelper): box = layout.box() box.label(text='Include Cameras and Objects') - box.prop(self, 'include_active_cam') - box.prop(self, 'include_selected_cams') - box.prop(self, 'include_selected_objects') - box.prop(self, 'include_image_planes') + col = box.column(align=True) + col.prop(self, 'include_active_cam') + col.prop(self, 'include_selected_cams') + col.prop(self, 'include_selected_objects') + col.prop(self, 'include_image_planes') + col.prop(self, 'include_solids') + + box = layout.box() + box.label(text='Include Tracking Data') + box.prop(self, 'include_cam_bundles') +# box.prop(self, 'include_ob_bundles') + box = layout.box() box.prop(self, 'include_animation') + box = layout.box() box.label(text='Transform') box.prop(self, 'ae_size') - box = layout.box() - box.label(text='Include Tracking Data:') - box.prop(self, 'include_cam_bundles') -# box.prop(self, 'include_ob_bundles') @classmethod def poll(cls, context): @@ -1106,11 +829,14 @@ class ExportJsx(bpy.types.Operator, ExportHelper): def execute(self, context): data = get_comp_data(context) - selection = get_selected(context) - write_jsx_file(self.filepath, data, selection, self.include_animation, - self.include_active_cam, self.include_selected_cams, - self.include_selected_objects, self.include_cam_bundles, - self.include_image_planes, self.ae_size) + selection = get_selected(context, self.include_active_cam, + self.include_selected_cams, + self.include_selected_objects, + self.include_cam_bundles, + self.include_image_planes, + self.include_solids) + write_jsx_file(context, self.filepath, data, selection, + self.include_animation, self.ae_size) print("\nExport to After Effects Completed") return {'FINISHED'} -- 2.30.2 From a23408695ad077ee3e03fdfb0a76adeb7a09d45b Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Thu, 24 Jun 2021 13:22:13 +0200 Subject: [PATCH 05/33] After Effects export: simplify FPS --- io_export_after_effects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 4861de9..1fcca15 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -49,7 +49,7 @@ def get_comp_data(context): start = scene.frame_start end = scene.frame_end active_cam_frames = get_active_cam_for_each_frame(scene, start, end) - fps = floor(scene.render.fps / (scene.render.fps_base) * 1000.0) / 1000.0 + fps = scene.render.fps / scene.render.fps_base return { 'scn': scene, -- 2.30.2 From 07352bddf40ef0408b6bfe024355511b36589e0f Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Mon, 9 Aug 2021 16:28:53 +0200 Subject: [PATCH 06/33] After Effects export: fix interpolation when channel stays fixed --- io_export_after_effects.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 1fcca15..0f4bd28 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -110,8 +110,16 @@ class ObjectExport(): def get_prop_keyframe(self, context, prop_name, value, time): """Set keyframe for given property""" prop_keys = self.keyframes.setdefault(prop_name, []) - if not len(prop_keys) or value != prop_keys[-1][1]: - prop_keys.append((time, value)) + if len(prop_keys) == 0: + prop_keys.append([time, value, False]) + return + + if value != prop_keys[-1][1]: + prop_keys.append([time, value, False]) + # Store which keys should hold, that is, which are + # the first in a series of identical values + else: + prop_keys[-1][2] = True def get_keyframe(self, context, data, time, ae_size): """Store animation for the current frame""" @@ -141,10 +149,21 @@ class ObjectExport(): # Set values of properties, add keyframes only where needed for prop, keys in self.keyframes.items(): if include_animation and len(keys) > 1: - times = ",".join((str(k[0]) for k in keys)) - values = ",".join((str(k[1]) for k in keys)).replace(" ", "") + times = ",".join(str(k[0]) for k in keys) + values = ",".join(str(k[1]) for k in keys).replace(" ", "") prop_script += ( f'{self.name_ae}.property("{prop}").setValuesAtTimes([{times}],[{values}]);\n') + + # Set to HOLD the frames after which animation is fixed + # for several frames, to avoid interpolation errors + if any(k[2] for k in keys): + prop_script += ( + f'var hold_frames = {[i + 1 for i, k in enumerate(keys) if k[2]]};\n' + 'for (var i = 0; i < hold_frames.length; i++) {\n' + f' {self.name_ae}.property("{prop}").setInterpolationTypeAtKey(hold_frames[i], KeyframeInterpolationType.HOLD);\n' + '}\n') + + # No animation for this property else: value = str(keys[0][1]).replace(" ", "") prop_script += ( -- 2.30.2 From ab2ec03debcc5cc924163053142146f9b7442b0b Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Thu, 9 Dec 2021 10:18:41 +0100 Subject: [PATCH 07/33] After Effects export: fix image export when scale is 0 --- io_export_after_effects.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 0f4bd28..2b13b57 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -249,8 +249,11 @@ class ImageExport(ObjectExport): image_width, image_height = get_image_size(self.obj) ratio_to_comp = image_width / data['width'] scale = ae_transform[6:9] - scale[0] /= ratio_to_comp - scale[1] = scale[1] / ratio_to_comp * image_width / image_height + if image_height != 0.0: + scale[1] *= image_width / image_height + if ratio_to_comp != 0.0: + scale[0] /= ratio_to_comp + scale[1] /= ratio_to_comp self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) -- 2.30.2 From 5c634a4378dea4104c1455110d442691776c3701 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Thu, 19 May 2022 17:37:14 +0200 Subject: [PATCH 08/33] After Effects export: cleanup - remove "data" dict This dict is only used from the write_jsx_file function, so its content can be stored directly there instead of passed around. --- io_export_after_effects.py | 196 +++++++++++++++++-------------------- 1 file changed, 90 insertions(+), 106 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 2b13b57..dccb56c 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -40,31 +40,6 @@ from math import degrees, floor from mathutils import Matrix, Vector, Color -def get_comp_data(context): - """Create list of static blender's data""" - scene = context.scene - aspect_x = scene.render.pixel_aspect_x - aspect_y = scene.render.pixel_aspect_y - aspect = aspect_x / aspect_y - start = scene.frame_start - end = scene.frame_end - active_cam_frames = get_active_cam_for_each_frame(scene, start, end) - fps = scene.render.fps / scene.render.fps_base - - return { - 'scn': scene, - 'width': scene.render.resolution_x, - 'height': scene.render.resolution_y, - 'aspect': aspect, - 'fps': fps, - 'start': start, - 'end': end, - 'duration': (end - start + 1.0) / fps, - 'active_cam_frames': active_cam_frames, - 'frame_current': scene.frame_current, - } - - def get_active_cam_for_each_frame(scene, start, end): """Create list of active camera for each frame in case active camera is set by markers""" active_cam_frames = [] @@ -107,8 +82,8 @@ class ObjectExport(): self.name_ae = convert_name(self.obj.name) self.keyframes = {} - def get_prop_keyframe(self, context, prop_name, value, time): - """Set keyframe for given property""" + def get_prop_keyframe(self, prop_name, value, time): + """Get keyframe for given property, only if different from previous value""" prop_keys = self.keyframes.setdefault(prop_name, []) if len(prop_keys) == 0: prop_keys.append([time, value, False]) @@ -121,19 +96,19 @@ class ObjectExport(): else: prop_keys[-1][2] = True - def get_keyframe(self, context, data, time, ae_size): + def get_keyframe(self, context, width, height, aspect, time, ae_size): """Store animation for the current frame""" ae_transform = convert_transform_matrix(self.obj.matrix_world, - data['width'], data['height'], - data['aspect'], True, ae_size) + width, height, + aspect, True, ae_size) - self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) - self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) - self.get_prop_keyframe(context, 'scale', ae_transform[6:9], time) + self.get_prop_keyframe('position', ae_transform[0:3], time) + self.get_prop_keyframe('orientation', ae_transform[3:6], time) + self.get_prop_keyframe('scale', ae_transform[6:9], time) def get_obj_script(self, include_animation): """Get the JSX script for the object""" - return self.get_type_script() + self.get_prop_script(include_animation) + self.get_post_script() + return self.get_type_script() + self.get_anim_script(include_animation) + self.get_post_script() def get_type_script(self): """Get the basic part of the JSX script""" @@ -142,22 +117,22 @@ class ObjectExport(): type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' return type_script - def get_prop_script(self, include_animation): + def get_anim_script(self, include_animation): """Get the part of the JSX script encoding animation""" - prop_script = "" + anim_script = "" # Set values of properties, add keyframes only where needed for prop, keys in self.keyframes.items(): if include_animation and len(keys) > 1: times = ",".join(str(k[0]) for k in keys) values = ",".join(str(k[1]) for k in keys).replace(" ", "") - prop_script += ( + anim_script += ( f'{self.name_ae}.property("{prop}").setValuesAtTimes([{times}],[{values}]);\n') # Set to HOLD the frames after which animation is fixed # for several frames, to avoid interpolation errors if any(k[2] for k in keys): - prop_script += ( + anim_script += ( f'var hold_frames = {[i + 1 for i, k in enumerate(keys) if k[2]]};\n' 'for (var i = 0; i < hold_frames.length; i++) {\n' f' {self.name_ae}.property("{prop}").setInterpolationTypeAtKey(hold_frames[i], KeyframeInterpolationType.HOLD);\n' @@ -166,27 +141,28 @@ class ObjectExport(): # No animation for this property else: value = str(keys[0][1]).replace(" ", "") - prop_script += ( + anim_script += ( f'{self.name_ae}.property("{prop}").setValue({value});\n') - prop_script += '\n' - return prop_script + anim_script += '\n' + + return anim_script def get_post_script(self): """This is only used in lights as a post-treatment after animation""" return "" class CameraExport(ObjectExport): - def get_keyframe(self, context, data, time, ae_size): + def get_keyframe(self, context, width, height, aspect, time, ae_size): ae_transform = convert_transform_matrix(self.obj.matrix_world, - data['width'], data['height'], - data['aspect'], True, ae_size) - zoom = convert_lens(self.obj, data['width'], data['height'], - data['aspect']) + width, height, + aspect, True, ae_size) + zoom = convert_lens(self.obj, width, height, + aspect) - self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) - self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) - self.get_prop_keyframe(context, 'zoom', zoom, time) + self.get_prop_keyframe('position', ae_transform[0:3], time) + self.get_prop_keyframe('orientation', ae_transform[3:6], time) + self.get_prop_keyframe('zoom', zoom, time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addCamera("{self.name_ae}",[0,0]);\n' @@ -195,24 +171,24 @@ class CameraExport(ObjectExport): class LightExport(ObjectExport): - def get_keyframe(self, context, data, time, ae_size): + def get_keyframe(self, context, width, height, aspect, time, ae_size): ae_transform = convert_transform_matrix(self.obj.matrix_world, - data['width'], data['height'], - data['aspect'], True, ae_size) + width, height, + aspect, True, ae_size) self.type = self.obj.data.type color = list(self.obj.data.color) intensity = self.obj.data.energy * 10.0 - self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe('position', ae_transform[0:3], time) if self.type in {'SPOT', 'SUN'}: - self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) - self.get_prop_keyframe(context, 'intensity', intensity, time) - self.get_prop_keyframe(context, 'Color', color, time) + self.get_prop_keyframe('orientation', ae_transform[3:6], time) + self.get_prop_keyframe('intensity', intensity, time) + self.get_prop_keyframe('Color', color, time) if self.type == 'SPOT': cone_angle = degrees(self.obj.data.spot_size) - self.get_prop_keyframe(context, 'Cone Angle', cone_angle, time) + self.get_prop_keyframe('Cone Angle', cone_angle, time) cone_feather = self.obj.data.spot_blend * 100.0 - self.get_prop_keyframe(context, 'Cone Feather', cone_feather, time) + self.get_prop_keyframe('Cone Feather', cone_feather, time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addLight("{self.name_ae}", [0.0, 0.0]);\n' @@ -232,14 +208,14 @@ class LightExport(ObjectExport): class ImageExport(ObjectExport): - def get_keyframe(self, context, data, time, ae_size): + def get_keyframe(self, context, width, height, aspect, time, ae_size): # Convert obj transform properties to AE space plane_matrix = get_image_plane_matrix(self.obj) # Scale plane to account for AE's transforms - plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4) + plane_matrix = plane_matrix @ Matrix.Scale(100.0 / width, 4) - ae_transform = convert_transform_matrix(plane_matrix, data['width'], - data['height'], data['aspect'], + ae_transform = convert_transform_matrix(plane_matrix, width, + height, aspect, True, ae_size) opacity = 0.0 if self.obj.hide_render else 100.0 @@ -247,7 +223,7 @@ class ImageExport(ObjectExport): self.filepath = get_image_filepath(self.obj) image_width, image_height = get_image_size(self.obj) - ratio_to_comp = image_width / data['width'] + ratio_to_comp = image_width / width scale = ae_transform[6:9] if image_height != 0.0: scale[1] *= image_width / image_height @@ -255,10 +231,10 @@ class ImageExport(ObjectExport): scale[0] /= ratio_to_comp scale[1] /= ratio_to_comp - self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) - self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) - self.get_prop_keyframe(context, 'scale', scale, time) - self.get_prop_keyframe(context, 'opacity', opacity, time) + self.get_prop_keyframe('position', ae_transform[0:3], time) + self.get_prop_keyframe('orientation', ae_transform[3:6], time) + self.get_prop_keyframe('scale', scale, time) + self.get_prop_keyframe('opacity', opacity, time) def get_type_script(self): type_script = f'var newFootage = app.project.importFile(new ImportOptions(File("{self.filepath}")));\n' @@ -270,30 +246,30 @@ class ImageExport(ObjectExport): class SolidExport(ObjectExport): - def get_keyframe(self, context, data, time, ae_size): + def get_keyframe(self, context, width, height, aspect, time, ae_size): # Convert obj transform properties to AE space plane_matrix = get_plane_matrix(self.obj) # Scale plane to account for AE's transforms - plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4) + plane_matrix = plane_matrix @ Matrix.Scale(100.0 / width, 4) - ae_transform = convert_transform_matrix(plane_matrix, data['width'], - data['height'], data['aspect'], + ae_transform = convert_transform_matrix(plane_matrix, width, + height, aspect, True, ae_size) opacity = 0.0 if self.obj.hide_render else 100.0 if not hasattr(self, 'color'): self.color = get_plane_color(self.obj) if not hasattr(self, 'width'): - self.width = data['width'] + self.width = width if not hasattr(self, 'height'): - self.height = data['height'] + self.height = height scale = ae_transform[6:9] - scale[1] *= data['width'] / data['height'] + scale[1] *= width / height - self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) - self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) - self.get_prop_keyframe(context, 'scale', scale, time) - self.get_prop_keyframe(context, 'opacity', opacity, time) + self.get_prop_keyframe('position', ae_transform[0:3], time) + self.get_prop_keyframe('orientation', ae_transform[3:6], time) + self.get_prop_keyframe('scale', scale, time) + self.get_prop_keyframe('opacity', opacity, time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addSolid({self.color},"{self.name_ae}",{self.width},{self.height},1.0);\n' @@ -310,18 +286,18 @@ class CamBundleExport(ObjectExport): self.name_ae = convert_name(f'{obj.name}__{track.name}') self.keyframes = {} - def get_keyframe(self, context, data, time, ae_size): + def get_keyframe(self, context, width, height, aspect, time, ae_size): # Bundles are in camera space. # Transpose to world space matrix = Matrix.Translation(self.obj.matrix_basis @ self.track.bundle) # Convert the position into AE space - ae_transform = convert_transform_matrix(matrix, data['width'], - data['height'], - data['aspect'], False, + ae_transform = convert_transform_matrix(matrix, width, + height, + aspect, False, ae_size) - self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe('position', ae_transform[0:3], time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addNull();\n' @@ -672,44 +648,52 @@ def convert_lens(camera, width, height, aspect): # return matrix -def write_jsx_file(context, file, data, selection, include_animation, ae_size): +def write_jsx_file(context, file, selection, include_animation, ae_size): """jsx script for AE creation""" print("\n---------------------------\n" "- Export to After Effects -\n" "---------------------------") - # Store the current frame to restore it at the end of export - frame_current = data['frame_current'] - - # Get all keyframes for each object and store in dico + # Create list of static blender data + scene = context.scene + width = scene.render.resolution_x + height = scene.render.resolution_y + aspect_x = scene.render.pixel_aspect_x + aspect_y = scene.render.pixel_aspect_y + aspect = aspect_x / aspect_y if include_animation: - end = data['end'] + 1 + frame_end = scene.frame_end + 1 else: - end = data['start'] + 1 + frame_end = scene.frame_start + 1 + fps = scene.render.fps / scene.render.fps_base + duration = (frame_end - scene.frame_start) / fps - for frame in range(data['start'], end): + # Store the current frame to restore it at the end of export + frame_current = scene.frame_current + + # Get all keyframes for each object + for frame in range(scene.frame_start, frame_end): print("Working on frame: " + str(frame)) - data['scn'].frame_set(frame) + scene.frame_set(frame) # Get time for this loop - time = (frame - data['start']) / data['fps'] + time = (frame - scene.frame_start) / fps for obj_type in selection.values(): for obj in obj_type: - obj.get_keyframe(context, data, time, ae_size) + obj.get_keyframe(context, width, height, aspect, time, ae_size) # ---- write JSX file with open(file, 'w') as jsx_file: - # Make the jsx executable in After Effects (enable double click on jsx) jsx_file.write('#target AfterEffects\n\n') # Script's header jsx_file.write('/**************************************\n') - jsx_file.write(f'Scene : {data["scn"].name}\n') - jsx_file.write(f'Resolution : {data["width"]} x {data["height"]}\n') - jsx_file.write(f'Duration : {data["duration"]}\n') - jsx_file.write(f'FPS : {data["fps"]}\n') + jsx_file.write(f'Scene : {scene.name}\n') + jsx_file.write(f'Resolution : {width} x {height}\n') + jsx_file.write(f'Duration : {duration}\n') + jsx_file.write(f'FPS : {fps}\n') jsx_file.write(f'Date : {datetime.datetime.now()}\n') jsx_file.write(f'Exported with io_export_after_effects.py\n') jsx_file.write(f'**************************************/\n\n\n\n') @@ -727,12 +711,13 @@ def write_jsx_file(context, file, data, selection, include_animation, ae_size): jsx_file.write('if (compName){') # Continue only if comp name is given. If not - terminate jsx_file.write( - f'\nvar newComp = app.project.items.addComp(compName, {data["width"]}, ' - f'{data["height"]}, {data["aspect"]}, {data["duration"]}, {data["fps"]});') - jsx_file.write(f"\nnewComp.displayStartTime = {(data['start']) / data['fps']};\n\n") + f'\nvar newComp = app.project.items.addComp(compName, {width}, ' + f'{height}, {aspect}, {duration}, {fps});') + jsx_file.write(f"\nnewComp.displayStartTime = {scene.frame_start / fps};\n\n") jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n') + # Write each object's creation script for obj_type in ('cam_bundles', 'nulls', 'solids', 'images', 'lights', 'cameras'): if len(selection[obj_type]): type_name = 'CAMERA 3D MARKERS' if obj_type == 'cam_bundles' else obj_type.upper() @@ -750,8 +735,8 @@ def write_jsx_file(context, file, data, selection, include_animation, ae_size): jsx_file.write('compFromBlender();\n') # Execute function jsx_file.write('app.endUndoGroup();\n\n\n') - # Set current frame of animation in blender to state before export - data['scn'].frame_set(frame_current) + # Restore current frame of animation in blender to state before export + scene.frame_set(frame_current) ########################################## @@ -850,14 +835,13 @@ class ExportJsx(bpy.types.Operator, ExportHelper): return selected or camera def execute(self, context): - data = get_comp_data(context) selection = get_selected(context, self.include_active_cam, self.include_selected_cams, self.include_selected_objects, self.include_cam_bundles, self.include_image_planes, self.include_solids) - write_jsx_file(context, self.filepath, data, selection, + write_jsx_file(context, self.filepath, selection, self.include_animation, self.ae_size) print("\nExport to After Effects Completed") return {'FINISHED'} -- 2.30.2 From 14365d76b06367e615b504807890f40e0d8e2ab4 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Thu, 19 May 2022 18:19:22 +0200 Subject: [PATCH 09/33] After Effects export: cleanup - Remove unused import, variable and f-strings - Replace "if not len(sequence)" by "if len(sequence) == 0" - Remove useless else block after continue --- io_export_after_effects.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index dccb56c..5578eaf 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -36,7 +36,7 @@ bl_info = { import bpy import os import datetime -from math import degrees, floor +from math import degrees from mathutils import Matrix, Vector, Color @@ -350,11 +350,10 @@ def get_selected(context, include_active_cam, include_selected_cams, and obj is context.scene.camera): # Ignore active camera if already selected continue - else: - if include_selected_cams: - cameras.append(CameraExport(obj)) - if include_cam_bundles: - cam_bundles.extend(get_camera_bundles(context.scene, obj)) + if include_selected_cams: + cameras.append(CameraExport(obj)) + if include_cam_bundles: + cam_bundles.extend(get_camera_bundles(context.scene, obj)) elif include_image_planes and is_image_plane(obj): images.append(ImageExport(obj)) @@ -444,7 +443,7 @@ def is_image_plane(obj): if not is_plane(obj): return False - if not len(obj.material_slots): + if len(obj.material_slots) == 0: return False mat = get_first_material(obj) @@ -535,8 +534,6 @@ def convert_transform_matrix(matrix, width, height, aspect, This function will be called for every object for every frame """ - scale_mat = Matrix.Scale(width, 4) - # Get blender transform data for object b_loc = matrix.to_translation() b_rot = matrix.to_euler('ZYX') # ZYX euler matches AE's orientation and allows to use x_rot_correction @@ -695,8 +692,8 @@ def write_jsx_file(context, file, selection, include_animation, ae_size): jsx_file.write(f'Duration : {duration}\n') jsx_file.write(f'FPS : {fps}\n') jsx_file.write(f'Date : {datetime.datetime.now()}\n') - jsx_file.write(f'Exported with io_export_after_effects.py\n') - jsx_file.write(f'**************************************/\n\n\n\n') + jsx_file.write('Exported with io_export_after_effects.py\n') + jsx_file.write('**************************************/\n\n\n\n') # Wrap in function jsx_file.write("function compFromBlender(){\n") -- 2.30.2 From bbd906ad6fc55e84f36b5e35336c84473f4a27dc Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Fri, 20 May 2022 14:46:55 +0200 Subject: [PATCH 10/33] After Effects export: export bundle rotations --- io_export_after_effects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 5578eaf..e3b2c43 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -23,7 +23,7 @@ bl_info = { "description": "Export cameras, selected objects & camera solution " "3D Markers to Adobe After Effects CS3 and above", "author": "Bartek Skorupa, Damien Picard (@pioverfour)", - "version": (0, 1, 0), + "version": (0, 1, 1), "blender": (2, 80, 0), "location": "File > Export > Adobe After Effects (.jsx)", "warning": "", @@ -289,15 +289,15 @@ class CamBundleExport(ObjectExport): def get_keyframe(self, context, width, height, aspect, time, ae_size): # Bundles are in camera space. # Transpose to world space - matrix = Matrix.Translation(self.obj.matrix_basis - @ self.track.bundle) + matrix = self.obj.matrix_basis @ Matrix.Translation(self.track.bundle) # Convert the position into AE space ae_transform = convert_transform_matrix(matrix, width, height, - aspect, False, + aspect, True, ae_size) self.get_prop_keyframe('position', ae_transform[0:3], time) + self.get_prop_keyframe('orientation', ae_transform[3:6], time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addNull();\n' -- 2.30.2 From c33079dc0f4ccc0b681ba99382d5d956f322c678 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Fri, 20 May 2022 15:02:12 +0200 Subject: [PATCH 11/33] After Effects export: cleanup - remove unused option Remove x_rot_correction in convert_transform_matrix(), since it's always True --- io_export_after_effects.py | 39 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index e3b2c43..96a5609 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -99,8 +99,7 @@ class ObjectExport(): def get_keyframe(self, context, width, height, aspect, time, ae_size): """Store animation for the current frame""" ae_transform = convert_transform_matrix(self.obj.matrix_world, - width, height, - aspect, True, ae_size) + width, height, aspect, ae_size) self.get_prop_keyframe('position', ae_transform[0:3], time) self.get_prop_keyframe('orientation', ae_transform[3:6], time) @@ -155,8 +154,7 @@ class ObjectExport(): class CameraExport(ObjectExport): def get_keyframe(self, context, width, height, aspect, time, ae_size): ae_transform = convert_transform_matrix(self.obj.matrix_world, - width, height, - aspect, True, ae_size) + width, height, aspect, ae_size) zoom = convert_lens(self.obj, width, height, aspect) @@ -173,8 +171,7 @@ class CameraExport(ObjectExport): class LightExport(ObjectExport): def get_keyframe(self, context, width, height, aspect, time, ae_size): ae_transform = convert_transform_matrix(self.obj.matrix_world, - width, height, - aspect, True, ae_size) + width, height, aspect, ae_size) self.type = self.obj.data.type color = list(self.obj.data.color) intensity = self.obj.data.energy * 10.0 @@ -214,9 +211,8 @@ class ImageExport(ObjectExport): # Scale plane to account for AE's transforms plane_matrix = plane_matrix @ Matrix.Scale(100.0 / width, 4) - ae_transform = convert_transform_matrix(plane_matrix, width, - height, aspect, - True, ae_size) + ae_transform = convert_transform_matrix(plane_matrix, + width, height, aspect, ae_size) opacity = 0.0 if self.obj.hide_render else 100.0 if not hasattr(self, 'filepath'): @@ -252,10 +248,10 @@ class SolidExport(ObjectExport): # Scale plane to account for AE's transforms plane_matrix = plane_matrix @ Matrix.Scale(100.0 / width, 4) - ae_transform = convert_transform_matrix(plane_matrix, width, - height, aspect, - True, ae_size) + ae_transform = convert_transform_matrix(plane_matrix, + width, height, aspect, ae_size) opacity = 0.0 if self.obj.hide_render else 100.0 + if not hasattr(self, 'color'): self.color = get_plane_color(self.obj) if not hasattr(self, 'width'): @@ -291,10 +287,8 @@ class CamBundleExport(ObjectExport): # Transpose to world space matrix = self.obj.matrix_basis @ Matrix.Translation(self.track.bundle) # Convert the position into AE space - ae_transform = convert_transform_matrix(matrix, width, - height, - aspect, True, - ae_size) + ae_transform = convert_transform_matrix(matrix, + width, height, aspect, ae_size) self.get_prop_keyframe('position', ae_transform[0:3], time) self.get_prop_keyframe('orientation', ae_transform[3:6], time) @@ -526,8 +520,7 @@ def convert_name(name): return name -def convert_transform_matrix(matrix, width, height, aspect, - x_rot_correction=False, ae_size=100.0): +def convert_transform_matrix(matrix, width, height, aspect, ae_size=100.0): """Convert from Blender's Location, Rotation and Scale to AE's Position, Rotation/Orientation and Scale @@ -548,14 +541,12 @@ def convert_transform_matrix(matrix, width, height, aspect, z = (b_loc.y * 100.0) * ae_size / 100.0 # Convert rotations to match AE's orientation. - # If not x_rot_correction - rx = degrees(b_rot.x) # AE's X orientation = blender's X rotation if 'ZYX' euler. + # In Blender, object of zero rotation lays on floor. + # In AE, layer of zero orientation "stands", so subtract 90 degrees + rx = degrees(b_rot.x) - 90.0 # AE's X orientation = blender's X rotation if 'ZYX' euler. ry = -degrees(b_rot.y) # AE's Y orientation = -blender's Y rotation if 'ZYX' euler rz = -degrees(b_rot.z) # AE's Z orientation = -blender's Z rotation if 'ZYX' euler - if x_rot_correction: - # In Blender, object of zero rotation lays on floor. - # In AE, layer of zero orientation "stands" - rx -= 90.0 + # Convert scale to AE scale. ae_size is a global multiplier. sx = b_scale.x * ae_size sy = b_scale.y * ae_size -- 2.30.2 From e45c1506171c165a3f442c4fd95637faac69f993 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Thu, 19 May 2022 18:03:48 +0200 Subject: [PATCH 12/33] After Effects export: rewrite function to get cameras for each frame Instead, get frame ranges for each camera. This will allow us to export many animated cameras with appropriate frame ranges. --- io_export_after_effects.py | 48 ++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 96a5609..fef9dba 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -40,36 +40,28 @@ from math import degrees from mathutils import Matrix, Vector, Color -def get_active_cam_for_each_frame(scene, start, end): - """Create list of active camera for each frame in case active camera is set by markers""" - active_cam_frames = [] - sorted_markers = [] - markers = scene.timeline_markers - if markers: - for marker in markers: - if marker.camera: - sorted_markers.append([marker.frame, marker]) - sorted_markers = sorted(sorted_markers) +def get_camera_frame_ranges(scene, start, end): + """Get frame ranges for each marker in the timeline - if sorted_markers: - for frame in range(start, end + 1): - for m, marker in enumerate(sorted_markers): - if marker[0] > frame: - if m != 0: - active_cam_frames.append( - sorted_markers[m - 1][1].camera) - else: - active_cam_frames.append(marker[1].camera) - break - elif m == len(sorted_markers) - 1: - active_cam_frames.append(marker[1].camera) - if not active_cam_frames: - if scene.camera: - # in this case active_cam_frames array will have length of 1. This - # will indicate that there is only one active cam in all frames - active_cam_frames.append(scene.camera) + For this, start at the end of the timeline, + iterate through each camera-bound marker in reverse, + and get the range from this marker to the end of the previous range. + """ + markers = sorted((m for m in scene.timeline_markers if m.camera is not None), + key=lambda m:m.frame, reverse=True) - return(active_cam_frames) + if len(markers) <= 1: + return [[[start, end], scene.camera],] + + camera_frame_ranges = [] + current_frame = end + for m in markers: + if m.frame < current_frame: + camera_frame_ranges.append([[m.frame, current_frame + 1], m.camera]) + current_frame = m.frame - 1 + camera_frame_ranges.reverse() + camera_frame_ranges[0][0][0] = start + return camera_frame_ranges class ObjectExport(): -- 2.30.2 From 61efd17f87b45c3049091127a5619219f9d2a821 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Fri, 20 May 2022 17:51:01 +0200 Subject: [PATCH 13/33] After Effects export: export multiple cameras when using markers In previous versions, when using markers to switch between cameras, a single animated camera would be exported. This would cause motion blur issues when the camera would jump quickly and unexpectedly between two positions. Instead, we now export all marker cameras, but restrict their time range to switch between them. If a camera is used multiple times through a shot, as many cameras with the same name will be exported. --- io_export_after_effects.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index fef9dba..eab2efe 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -23,7 +23,7 @@ bl_info = { "description": "Export cameras, selected objects & camera solution " "3D Markers to Adobe After Effects CS3 and above", "author": "Bartek Skorupa, Damien Picard (@pioverfour)", - "version": (0, 1, 1), + "version": (0, 1, 2), "blender": (2, 80, 0), "location": "File > Export > Adobe After Effects (.jsx)", "warning": "", @@ -144,6 +144,11 @@ class ObjectExport(): return "" class CameraExport(ObjectExport): + def __init__(self, obj, start_time=None, end_time=None): + super().__init__(obj) + self.start_time = start_time + self.end_time = end_time + def get_keyframe(self, context, width, height, aspect, time, ae_size): ae_transform = convert_transform_matrix(self.obj.matrix_world, width, height, aspect, ae_size) @@ -156,6 +161,10 @@ class CameraExport(ObjectExport): def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addCamera("{self.name_ae}",[0,0]);\n' + # Restrict time range when multiple cameras are used (markers) + if self.start_time is not None: + type_script += f'{self.name_ae}.inPoint = {self.start_time};\n' + type_script += f'{self.name_ae}.outPoint = {self.end_time};\n' type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' return type_script @@ -324,17 +333,29 @@ def get_selected(context, include_active_cam, include_selected_cams, cam_bundles = [] # Camera trackers exported as AE nulls nulls = [] # Remaining objects exported as AE nulls + scene = context.scene + fps = scene.render.fps / scene.render.fps_base + if context.scene.camera is not None: if include_active_cam: - cameras.append(CameraExport(context.scene.camera)) - if include_cam_bundles: - cam_bundles.extend(get_camera_bundles(context.scene, context.scene.camera)) + for frame_range, camera in get_camera_frame_ranges( + context.scene, + context.scene.frame_start, context.scene.frame_end): + + if (include_cam_bundles + and camera not in (cam.obj for cam in cameras)): + cam_bundles.extend( + get_camera_bundles(context.scene, camera)) + + cameras.append( + CameraExport(camera, + (frame_range[0] - scene.frame_start) / fps, + (frame_range[1] - scene.frame_start) / fps)) for obj in context.selected_objects: if obj.type == 'CAMERA': - if (include_active_cam - and obj is context.scene.camera): - # Ignore active camera if already selected + # Ignore camera if already selected + if obj in (cam.obj for cam in cameras): continue if include_selected_cams: cameras.append(CameraExport(obj)) -- 2.30.2 From 9406de5f31254c0c608ba8785fd3dcad12dcd7c4 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Fri, 3 Jun 2022 11:52:13 +1000 Subject: [PATCH 14/33] Cleanup: remove comment This is no longer necessary, see: T98554. --- animation_motion_trail.py | 2 -- development_ui_classes.py | 2 -- io_export_after_effects.py | 2 -- io_scene_3ds/__init__.py | 2 -- io_scene_3ds/export_3ds.py | 2 -- io_scene_3ds/import_3ds.py | 2 -- io_scene_open_street_map.py | 2 -- io_vector/__init__.py | 2 -- io_vector/art2polyarea.py | 2 -- io_vector/geom.py | 2 -- io_vector/import_vecfile.py | 2 -- io_vector/model.py | 2 -- io_vector/offset.py | 2 -- io_vector/pdf.py | 2 -- io_vector/svg.py | 2 -- io_vector/triquad.py | 2 -- io_vector/vecfile.py | 2 -- mocap/__init__.py | 2 -- mocap/mocap_constraints.py | 2 -- mocap/mocap_tools.py | 2 -- mocap/retarget.py | 2 -- object_facemap_auto/__init__.py | 2 -- object_facemap_auto/auto_fmap_ops.py | 2 -- object_facemap_auto/auto_fmap_utils.py | 2 -- object_facemap_auto/auto_fmap_widgets.py | 2 -- object_facemap_auto/auto_fmap_widgets_xform.py | 2 -- object_fracture_crack/process/cell_calc.py | 2 -- render_cube_map.py | 2 -- render_to_print.py | 2 -- system_keyboard_svg.py | 2 -- 30 files changed, 60 deletions(-) diff --git a/animation_motion_trail.py b/animation_motion_trail.py index 65be2db..d5b0bba 100644 --- a/animation_motion_trail.py +++ b/animation_motion_trail.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - bl_info = { "name": "Motion Trail", diff --git a/development_ui_classes.py b/development_ui_classes.py index 6516098..24918c0 100644 --- a/development_ui_classes.py +++ b/development_ui_classes.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - bl_info = { "name": "UI Classes Overview", "author": "lijenstina", diff --git a/io_export_after_effects.py b/io_export_after_effects.py index eab2efe..40c208d 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - bl_info = { "name": "Export: Adobe After Effects (.jsx)", "description": "Export cameras, selected objects & camera solution " diff --git a/io_scene_3ds/__init__.py b/io_scene_3ds/__init__.py index 0137dd2..2ccb62e 100644 --- a/io_scene_3ds/__init__.py +++ b/io_scene_3ds/__init__.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - from bpy_extras.io_utils import ( ImportHelper, ExportHelper, diff --git a/io_scene_3ds/export_3ds.py b/io_scene_3ds/export_3ds.py index 0ee332a..dd46b4d 100644 --- a/io_scene_3ds/export_3ds.py +++ b/io_scene_3ds/export_3ds.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - # Script copyright (C) Bob Holcomb # Contributors: Campbell Barton, Bob Holcomb, Richard Lärkäng, Damien McGinnes, Mark Stijnman, Sebastian Sille diff --git a/io_scene_3ds/import_3ds.py b/io_scene_3ds/import_3ds.py index 193f53e..304ca70 100644 --- a/io_scene_3ds/import_3ds.py +++ b/io_scene_3ds/import_3ds.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - # Script copyright (C) Bob Holcomb # Contributors: Bob Holcomb, Richard L?rk?ng, Damien McGinnes, Sebastian Sille # Campbell Barton, Mario Lapin, Dominique Lorre, Andreas Atteneder diff --git a/io_scene_open_street_map.py b/io_scene_open_street_map.py index 204f41a..96136bf 100644 --- a/io_scene_open_street_map.py +++ b/io_scene_open_street_map.py @@ -16,8 +16,6 @@ # # ***** END GPL LICENCE BLOCK ***** -# - bl_info = { "name": "Open Street Map (.osm)", "author": "Michael Anthrax Schlachter, ideasman42, littleneo", diff --git a/io_vector/__init__.py b/io_vector/__init__.py index 6abf2d5..6986b03 100644 --- a/io_vector/__init__.py +++ b/io_vector/__init__.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - bl_info = { "name": "Adobe Illustrator / PDF / SVG", "author": "Howard Trickey", diff --git a/io_vector/art2polyarea.py b/io_vector/art2polyarea.py index b49d3a2..27624e7 100644 --- a/io_vector/art2polyarea.py +++ b/io_vector/art2polyarea.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - """Convert an Art object to a list of PolyArea objects. """ diff --git a/io_vector/geom.py b/io_vector/geom.py index a7eb4fe..c2f3366 100644 --- a/io_vector/geom.py +++ b/io_vector/geom.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - """Geometry classes and operations. Also, vector file representation (Art). """ diff --git a/io_vector/import_vecfile.py b/io_vector/import_vecfile.py index 163eefc..db8674b 100644 --- a/io_vector/import_vecfile.py +++ b/io_vector/import_vecfile.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - """Importing a vector file into Model format. """ diff --git a/io_vector/model.py b/io_vector/model.py index a3eb2aa..a714e83 100644 --- a/io_vector/model.py +++ b/io_vector/model.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - """Manipulations of Models. """ diff --git a/io_vector/offset.py b/io_vector/offset.py index 4e860b6..df04d2f 100644 --- a/io_vector/offset.py +++ b/io_vector/offset.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - """Creating offset polygons inside faces.""" __author__ = "howard.trickey@gmail.com" diff --git a/io_vector/pdf.py b/io_vector/pdf.py index e2e3199..4e8f2f4 100644 --- a/io_vector/pdf.py +++ b/io_vector/pdf.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - """Functions for dealing with PDF files. """ diff --git a/io_vector/svg.py b/io_vector/svg.py index 4a2012b..1ef5443 100644 --- a/io_vector/svg.py +++ b/io_vector/svg.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - """Reading SVG file format. """ diff --git a/io_vector/triquad.py b/io_vector/triquad.py index 88affa8..edced67 100644 --- a/io_vector/triquad.py +++ b/io_vector/triquad.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - from . import geom import math diff --git a/io_vector/vecfile.py b/io_vector/vecfile.py index 808a84e..594255a 100644 --- a/io_vector/vecfile.py +++ b/io_vector/vecfile.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - """Reading various vector file formats. Functions for classifying files, tokenizing, and parsing them. diff --git a/mocap/__init__.py b/mocap/__init__.py index 710db64..fecbf8c 100644 --- a/mocap/__init__.py +++ b/mocap/__init__.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - bl_info = { "name": "Motion Capture Tools", "author": "Benjy Cook", diff --git a/mocap/mocap_constraints.py b/mocap/mocap_constraints.py index 3d3e4a6..6d4a34c 100644 --- a/mocap/mocap_constraints.py +++ b/mocap/mocap_constraints.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - import bpy from mathutils import Vector from bpy_extras import anim_utils diff --git a/mocap/mocap_tools.py b/mocap/mocap_tools.py index cc85529..04a4261 100644 --- a/mocap/mocap_tools.py +++ b/mocap/mocap_tools.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - from math import sqrt, radians, floor, ceil import bpy import time diff --git a/mocap/retarget.py b/mocap/retarget.py index 4fa5d2c..5c963c0 100644 --- a/mocap/retarget.py +++ b/mocap/retarget.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - import bpy from mathutils import Vector, Matrix from math import radians diff --git a/object_facemap_auto/__init__.py b/object_facemap_auto/__init__.py index 0a309bc..1085633 100644 --- a/object_facemap_auto/__init__.py +++ b/object_facemap_auto/__init__.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - bl_info = { "name": "Auto Face Map Widgets", "author": "Campbell Barton", diff --git a/object_facemap_auto/auto_fmap_ops.py b/object_facemap_auto/auto_fmap_ops.py index df2434f..4ed3aef 100644 --- a/object_facemap_auto/auto_fmap_ops.py +++ b/object_facemap_auto/auto_fmap_ops.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - import bpy from bpy.types import ( Operator, diff --git a/object_facemap_auto/auto_fmap_utils.py b/object_facemap_auto/auto_fmap_utils.py index 0a02907..ba825c0 100644 --- a/object_facemap_auto/auto_fmap_utils.py +++ b/object_facemap_auto/auto_fmap_utils.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - # Use so we can develop modules without reloading the add-on. diff --git a/object_facemap_auto/auto_fmap_widgets.py b/object_facemap_auto/auto_fmap_widgets.py index 6522eb4..24ddf10 100644 --- a/object_facemap_auto/auto_fmap_widgets.py +++ b/object_facemap_auto/auto_fmap_widgets.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - ''' Face map manipulator: diff --git a/object_facemap_auto/auto_fmap_widgets_xform.py b/object_facemap_auto/auto_fmap_widgets_xform.py index 71b99cf..5b4ae1f 100644 --- a/object_facemap_auto/auto_fmap_widgets_xform.py +++ b/object_facemap_auto/auto_fmap_widgets_xform.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - import bpy import math diff --git a/object_fracture_crack/process/cell_calc.py b/object_fracture_crack/process/cell_calc.py index 2e47eaf..87fba05 100644 --- a/object_fracture_crack/process/cell_calc.py +++ b/object_fracture_crack/process/cell_calc.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - # Script copyright (C) Blender Foundation 2012 diff --git a/render_cube_map.py b/render_cube_map.py index 06417ba..363bc02 100644 --- a/render_cube_map.py +++ b/render_cube_map.py @@ -16,8 +16,6 @@ # # ======================= END GPL LICENSE BLOCK ======================== -# - # ######################################## # Render Cube Map # diff --git a/render_to_print.py b/render_to_print.py index 52d2fd5..fdc54cd 100644 --- a/render_to_print.py +++ b/render_to_print.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - bl_info = { "name": "Render to Print", "author": "Marco Crippa , Dealga McArdle, zebus3d", diff --git a/system_keyboard_svg.py b/system_keyboard_svg.py index bf6ab61..db14916 100644 --- a/system_keyboard_svg.py +++ b/system_keyboard_svg.py @@ -16,8 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# - # this script creates Keyboard layout images of the current keyboard configuration. # first implementation done by jbakker # version 0.2 - file manager directory on export, modified the SVG layout (lijenstina) -- 2.30.2 From 95107484d076bc965239942e857c83433bfa86d7 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Tue, 7 Jun 2022 21:12:54 +1000 Subject: [PATCH 15/33] Cleanup: remove comment (no longer needed) --- mesh_show_vgroup_weights.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mesh_show_vgroup_weights.py b/mesh_show_vgroup_weights.py index f16adcd..ca603d1 100644 --- a/mesh_show_vgroup_weights.py +++ b/mesh_show_vgroup_weights.py @@ -17,8 +17,6 @@ # # ***** END GPL LICENCE BLOCK ***** -# (Thanks to CodemanX on IRC) - bl_info = { "name": "Show Vertex Groups/Weights", "author": "Jason van Gumster (Fweeb), Bartius Crouch, CoDEmanX", -- 2.30.2 From 8fdbc5a71b2e8ab7dd1967077c829d6de7c84e06 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Fri, 16 Sep 2022 20:14:21 +0200 Subject: [PATCH 16/33] Storypencil: New Storyboard add-on This add-on allows to handle the drawing of storyboards using VSE and grease pencil. https://developer.blender.org/T100665 --- storypencil/__init__.py | 217 +++++++++ storypencil/dopesheet_overlay.py | 180 +++++++ storypencil/render.py | 281 +++++++++++ storypencil/scene_tools.py | 173 +++++++ storypencil/synchro.py | 790 +++++++++++++++++++++++++++++++ storypencil/ui.py | 211 +++++++++ storypencil/utils.py | 110 +++++ 7 files changed, 1962 insertions(+) create mode 100644 storypencil/__init__.py create mode 100644 storypencil/dopesheet_overlay.py create mode 100644 storypencil/render.py create mode 100644 storypencil/scene_tools.py create mode 100644 storypencil/synchro.py create mode 100644 storypencil/ui.py create mode 100644 storypencil/utils.py diff --git a/storypencil/__init__.py b/storypencil/__init__.py new file mode 100644 index 0000000..86b93b1 --- /dev/null +++ b/storypencil/__init__.py @@ -0,0 +1,217 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +# ---------------------------------------------- +# Define Addon info +# ---------------------------------------------- +bl_info = { + "name": "Storypencil - Storyboard Tools", + "description": "Storyboard tools", + "author": "Antonio Vazquez, Matias Mendiola, Daniel Martinez Lara, Rodrigo Blaas", + "version": (0, 1, 1), + "blender": (3, 3, 0), + "location": "", + "warning": "", + "category": "Sequencer", +} + +# ---------------------------------------------- +# Import modules +# ---------------------------------------------- +if "bpy" in locals(): + import importlib + + importlib.reload(utils) + importlib.reload(synchro) + importlib.reload(dopesheet_overlay) + importlib.reload(scene_tools) + importlib.reload(render) + importlib.reload(ui) +else: + from . import utils + from . import synchro + from . import dopesheet_overlay + from . import scene_tools + from . import render + from . import ui + +import bpy +from bpy.types import ( + Scene, + WindowManager, + WorkSpace, +) +from bpy.props import ( + BoolProperty, + IntProperty, + PointerProperty, + StringProperty, + EnumProperty, +) + +# -------------------------------------------------------------- +# Register all operators, props and panels +# -------------------------------------------------------------- +classes = ( + synchro.STORYPENCIL_PG_Settings, + scene_tools.STORYPENCIL_OT_Setup, + scene_tools.STORYPENCIL_OT_NewScene, + synchro.STORYPENCIL_OT_WindowBringFront, + synchro.STORYPENCIL_OT_WindowCloseOperator, + synchro.STORYPENCIL_OT_SyncToggleSlave, + synchro.STORYPENCIL_OT_SetSyncMainOperator, + synchro.STORYPENCIL_OT_AddSlaveWindowOperator, + synchro.STORYPENCIL_OT_Switch, + render.STORYPENCIL_OT_RenderAction, + ui.STORYPENCIL_PT_Settings, + ui.STORYPENCIL_PT_SettingsNew, + ui.STORYPENCIL_PT_RenderPanel, + ui.STORYPENCIL_PT_General, + ui.STORYPENCIL_MT_extra_options, +) + + +def save_mode(self, context): + wm = context.window_manager + wm['storypencil_use_new_window'] = context.scene.storypencil_use_new_window + # Close all secondary windows + if context.scene.storypencil_use_new_window is False: + c = context.copy() + for win in context.window_manager.windows: + # Don't close actual window + if win == context.window: + continue + win_id = str(win.as_pointer()) + if win_id != wm.storypencil_settings.main_window_id and win.parent is None: + c["window"] = win + bpy.ops.wm.window_close(c) + + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + + Scene.storypencil_scene_duration = IntProperty( + name="Scene Duration", + description="Default Duration for new Scene", + default=48, + min=1, + soft_max=250, + ) + + Scene.storypencil_use_new_window = BoolProperty(name="Open in new window", + description="Use secondary main window to edit scenes", + default=False, + update=save_mode) + + Scene.storypencil_main_workspace = PointerProperty(type=WorkSpace, + description="Main Workspace used for editing Storyboard") + Scene.storypencil_main_scene = PointerProperty(type=Scene, + description="Main Scene used for editing Storyboard") + Scene.storypencil_edit_workspace = PointerProperty(type=WorkSpace, + description="Workspace used for changing drawings") + + Scene.storypencil_base_scene = PointerProperty(type=Scene, + description="Base Scene used for creating new scenes") + + Scene.storypencil_render_render_path = StringProperty(name="Output Path", subtype='FILE_PATH', maxlen=256, + description="Directory/name to save files") + + Scene.storypencil_name_prefix = StringProperty(name="Scene Name Prefix", maxlen=20, default="") + + Scene.storypencil_name_suffix = StringProperty(name="Scene Name Suffix", maxlen=20, default="") + + Scene.storypencil_render_onlyselected = BoolProperty(name="Render only Selected Strips", + description="Render only the selected strips", + default=True) + + Scene.storypencil_render_channel = IntProperty(name="Channel", + description="Channel to set the new rendered video", + default=5, min=1, max=128) + + Scene.storypencil_add_render_strip = BoolProperty(name="Import Rendered Strips", + description="Add a Strip with the render", + default=True) + + Scene.storypencil_render_step = IntProperty(name="Image Steps", + description="Minimum frames number to generate images between keyframes (0 to disable)", + default=0, min=0, max=128) + + Scene.storypencil_render_numbering = EnumProperty(name="Image Numbering", + items=( + ('1', "Frame", "Use real frame number"), + ('2', "Consecutive", "Use sequential numbering"), + ), + description="Defines how frame is named") + + Scene.storypencil_add_render_byfolder = BoolProperty(name="Folder by Strip", + description="Create a separated folder for each strip", + default=True) + + WindowManager.storypencil_settings = PointerProperty( + type=synchro.STORYPENCIL_PG_Settings, + name="Storypencil settings", + description="Storypencil tool settings", + ) + + # Append Handlers + bpy.app.handlers.frame_change_post.clear() + bpy.app.handlers.frame_change_post.append(synchro.on_frame_changed) + bpy.app.handlers.load_post.append(synchro.sync_autoconfig) + + bpy.context.window_manager.storypencil_settings.active = False + bpy.context.window_manager.storypencil_settings.main_window_id = "" + bpy.context.window_manager.storypencil_settings.slave_windows_ids = "" + + # UI integration in dopesheet header + bpy.types.DOPESHEET_HT_header.append(synchro.draw_sync_header) + dopesheet_overlay.register() + + synchro.sync_autoconfig() + + # UI integration in VSE header + bpy.types.SEQUENCER_HT_header.remove(synchro.draw_sync_sequencer_header) + bpy.types.SEQUENCER_HT_header.append(synchro.draw_sync_sequencer_header) + + bpy.types.SEQUENCER_MT_add.append(scene_tools.draw_new_scene) + bpy.types.VIEW3D_MT_draw_gpencil.append(scene_tools.setup_storyboard) + + +def unregister(): + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + + # Remove Handlers + if bpy.app.handlers.frame_change_post: + bpy.app.handlers.frame_change_post.remove(synchro.on_frame_changed) + bpy.app.handlers.load_post.remove(synchro.sync_autoconfig) + + # remove UI integration + bpy.types.DOPESHEET_HT_header.remove(synchro.draw_sync_header) + dopesheet_overlay.unregister() + bpy.types.SEQUENCER_HT_header.remove(synchro.draw_sync_sequencer_header) + + bpy.types.SEQUENCER_MT_add.remove(scene_tools.draw_new_scene) + bpy.types.VIEW3D_MT_draw_gpencil.remove(scene_tools.setup_storyboard) + + del Scene.storypencil_scene_duration + del WindowManager.storypencil_settings + + del Scene.storypencil_base_scene + del Scene.storypencil_main_workspace + del Scene.storypencil_main_scene + del Scene.storypencil_edit_workspace + + del Scene.storypencil_render_render_path + del Scene.storypencil_name_prefix + del Scene.storypencil_name_suffix + del Scene.storypencil_render_onlyselected + del Scene.storypencil_render_channel + del Scene.storypencil_render_step + del Scene.storypencil_add_render_strip + del Scene.storypencil_render_numbering + del Scene.storypencil_add_render_byfolder + +if __name__ == '__main__': + register() diff --git a/storypencil/dopesheet_overlay.py b/storypencil/dopesheet_overlay.py new file mode 100644 index 0000000..63f3cf3 --- /dev/null +++ b/storypencil/dopesheet_overlay.py @@ -0,0 +1,180 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import typing + +import bpy +import bgl +import gpu +from gpu_extras.batch import batch_for_shader + +from .utils import (redraw_all_areas_by_type) +from .synchro import (is_slave_window, window_id, get_main_strip) + +Int3 = typing.Tuple[int, int, int] + +Float2 = typing.Tuple[float, float] +Float3 = typing.Tuple[float, float, float] +Float4 = typing.Tuple[float, float, float, float] + + +class LineDrawer: + def __init__(self): + self._format = gpu.types.GPUVertFormat() + self._pos_id = self._format.attr_add( + id="pos", comp_type="F32", len=2, fetch_mode="FLOAT" + ) + self._color_id = self._format.attr_add( + id="color", comp_type="F32", len=4, fetch_mode="FLOAT" + ) + + self.shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') + + def draw( + self, + coords: typing.List[Float2], + indices: typing.List[Int3], + color: Float4, + ): + if not coords: + return + + bgl.glEnable(bgl.GL_BLEND) + + self.shader.uniform_float("color", color) + + batch = batch_for_shader(self.shader, 'TRIS', {"pos": coords}, indices=indices) + batch.program_set(self.shader) + batch.draw() + + bgl.glDisable(bgl.GL_BLEND) + + +def get_scene_strip_in_out(strip): + """ Return the in and out keyframe of the given strip in the scene time reference""" + shot_in = strip.scene.frame_start + strip.frame_offset_start + shot_out = shot_in + strip.frame_final_duration - 1 + return (shot_in, shot_out) + + +def draw_callback_px(line_drawer: LineDrawer): + context = bpy.context + region = context.region + + wm = context.window_manager + + if ( + not wm.storypencil_settings.active + or not wm.storypencil_settings.show_main_strip_range + or not is_slave_window(wm, window_id(context.window)) + ): + return + + # get main strip driving the sync + strip = get_main_strip(wm) + + if not strip or strip.scene != context.scene: + return + + xwin1, ywin1 = region.view2d.region_to_view(0, 0) + one_pixel_further_x = region.view2d.region_to_view(1, 1)[0] + pixel_size_x = one_pixel_further_x - xwin1 + rect_width = 1 + + shot_in, shot_out = get_scene_strip_in_out(strip) + key_coords_in = [ + ( + shot_in - rect_width * pixel_size_x, + ywin1, + ), + ( + shot_in + rect_width * pixel_size_x, + ywin1, + ), + ( + shot_in + rect_width * pixel_size_x, + ywin1 + context.region.height, + ), + ( + shot_in - rect_width * pixel_size_x, + ywin1 + context.region.height, + ), + ] + + key_coords_out = [ + ( + shot_out - rect_width * pixel_size_x, + ywin1, + ), + ( + shot_out + rect_width * pixel_size_x, + ywin1, + ), + ( + shot_out + rect_width * pixel_size_x, + ywin1 + context.region.height, + ), + ( + shot_out - rect_width * pixel_size_x, + ywin1 + context.region.height, + ), + ] + + indices = [(0, 1, 2), (2, 0, 3)] + # Draw the IN frame in green + # hack: in certain cases, opengl draw state is invalid for the first drawn item + # resulting in a non-colored line + # => draw it a first time with a null alpha, so that the second one is drawn correctly + line_drawer.draw(key_coords_in, indices, (0, 0, 0, 0)) + line_drawer.draw(key_coords_in, indices, (0.3, 0.99, 0.4, 0.5)) + # Draw the OUT frame un red + line_drawer.draw(key_coords_out, indices, (0.99, 0.3, 0.4, 0.5)) + + +def tag_redraw_all_dopesheets(): + redraw_all_areas_by_type(bpy.context, 'DOPESHEET') + + +# This is a list so it can be changed instead of set +# if it is only changed, it does not have to be declared as a global everywhere +cb_handle = [] + + +def callback_enable(): + if cb_handle: + return + + # Doing GPU stuff in the background crashes Blender, so let's not. + if bpy.app.background: + return + + line_drawer = LineDrawer() + # POST_VIEW allow to work in time coordinate (1 unit = 1 frame) + cb_handle[:] = ( + bpy.types.SpaceDopeSheetEditor.draw_handler_add( + draw_callback_px, (line_drawer,), 'WINDOW', 'POST_VIEW' + ), + ) + + tag_redraw_all_dopesheets() + + +def callback_disable(): + if not cb_handle: + return + + try: + bpy.types.SpaceDopeSheetEditor.draw_handler_remove(cb_handle[0], 'WINDOW') + except ValueError: + # Thrown when already removed. + pass + cb_handle.clear() + + tag_redraw_all_dopesheets() + + +def register(): + callback_enable() + + +def unregister(): + callback_disable() diff --git a/storypencil/render.py b/storypencil/render.py new file mode 100644 index 0000000..6f4a136 --- /dev/null +++ b/storypencil/render.py @@ -0,0 +1,281 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import bpy +import os +import shutil +import sys + +from datetime import datetime +from bpy.types import Operator +from .utils import get_keyframe_list + +# ------------------------------------------------------ +# Button: Render VSE +# ------------------------------------------------------ + + +class STORYPENCIL_OT_RenderAction(Operator): + bl_idname = "storypencil.render_vse" + bl_label = "Render Strips" + bl_description = "Render VSE strips" + + # Extension by FFMPEG container type + video_ext = { + "MPEG1": ".mpg", + "MPEG2": ".dvd", + "MPEG4": ".mp4", + "AVI": ".avi", + "QUICKTIME": ".mov", + "DV": ".dv", + "OGG": ".ogv", + "MKV": ".mkv", + "FLASH": ".flv", + "WEBM": ".webm" + } + # Extension by image format + image_ext = { + "BMP": ".bmp", + "IRIS": ".rgb", + "PNG": ".png", + "JPEG": ".jpg", + "JPEG2000": ".jp2", + "TARGA": ".tga", + "TARGA_RAW": ".tga", + "CINEON": ".cin", + "DPX": ".dpx", + "OPEN_EXR_MULTILAYER": ".exr", + "OPEN_EXR": ".exr", + "HDR": ".hdr", + "TIFF": ".tif", + "WEBP": ".webp" + } + + # -------------------------------------------------------------------- + # Format an int adding 4 zero padding + # -------------------------------------------------------------------- + def format_to4(self, value): + return f"{value:04}" + + # -------------------------------------------------------------------- + # Add frames every N frames + # -------------------------------------------------------------------- + def add_missing_frames(self, sq, step, keyframe_list): + missing = [] + lk = len(keyframe_list) + if lk == 0: + return + + # Add mid frames + if step > 0: + for i in range(0, lk - 1): + dist = keyframe_list[i + 1] - keyframe_list[i] + if dist > step: + delta = int(dist / step) + e = 1 + for x in range(1, delta): + missing.append(keyframe_list[i] + (step * e)) + e += 1 + + keyframe_list.extend(missing) + keyframe_list.sort() + + # ------------------------------ + # Execute + # ------------------------------ + def execute(self, context): + scene = bpy.context.scene + image_settings = scene.render.image_settings + is_video_output = image_settings.file_format in { + 'FFMPEG', 'AVI_JPEG', 'AVI_RAW'} + step = scene.storypencil_render_step + + sequences = scene.sequence_editor.sequences_all + prv_start = scene.frame_start + prv_end = scene.frame_end + prv_frame = bpy.context.scene.frame_current + + prv_path = scene.render.filepath + prv_format = image_settings.file_format + prv_use_file_extension = scene.render.use_file_extension + prv_ffmpeg_format = scene.render.ffmpeg.format + rootpath = scene.storypencil_render_render_path + only_selected = scene.storypencil_render_onlyselected + channel = scene.storypencil_render_channel + + context.window.cursor_set('WAIT') + + # Create list of selected strips because the selection is changed when adding new strips + Strips = [] + for sq in sequences: + if sq.type == 'SCENE': + if only_selected is False or sq.select is True: + Strips.append(sq) + + # Sort strips + Strips = sorted(Strips, key=lambda strip: strip.frame_start) + + # For video, clear BL_proxy folder because sometimes the video + # is not rendered as expected if this folder has data. + # This ensure the output video is correct. + if is_video_output: + proxy_folder = os.path.join(rootpath, "BL_proxy") + if os.path.exists(proxy_folder): + for filename in os.listdir(proxy_folder): + file_path = os.path.join(proxy_folder, filename) + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): + shutil.rmtree(file_path) + except Exception as e: + print('Failed to delete %s. Reason: %s' % + (file_path, e)) + + try: + Videos = [] + Sheets = [] + # Read all strips and render the output + for sq in Strips: + strip_name = sq.name + strip_scene = sq.scene + scene.frame_start = int(sq.frame_start + sq.frame_offset_start) + scene.frame_end = int(scene.frame_start + sq.frame_final_duration - 1) # Image + if is_video_output is False: + # Get list of any keyframe + strip_start = sq.frame_offset_start + if strip_start < strip_scene.frame_start: + strip_start = strip_scene.frame_start + + strip_end = strip_start + sq.frame_final_duration - 1 + keyframe_list = get_keyframe_list( + strip_scene, strip_start, strip_end) + self.add_missing_frames(sq, step, keyframe_list) + + scene.render.use_file_extension = True + foldername = strip_name + if scene.storypencil_add_render_byfolder is True: + root_folder = os.path.join(rootpath, foldername) + else: + root_folder = rootpath + + frame_nrr = 0 + print("Render:" + strip_name + "/" + strip_scene.name) + print("Image From:", strip_start, "To", strip_end) + for key in range(int(strip_start), int(strip_end) + 1): + if key not in keyframe_list: + continue + + keyframe = key + sq.frame_start + if scene.use_preview_range: + if keyframe < scene.frame_preview_start: + continue + if keyframe > scene.frame_preview_end: + break + else: + if keyframe < scene.frame_start: + continue + if keyframe > scene.frame_end: + break + # For frame name use only the number + if scene.storypencil_render_numbering == '1': + # Real + framename = strip_name + '.' + self.format_to4(key) + else: + # Consecutive + frame_nrr += 1 + framename = strip_name + '.' + \ + self.format_to4(frame_nrr) + + filepath = os.path.join(root_folder, framename) + + sheet = os.path.realpath(filepath) + sheet = bpy.path.ensure_ext( + sheet, self.image_ext[image_settings.file_format]) + Sheets.append([sheet, keyframe]) + + scene.render.filepath = filepath + + # Render Frame + scene.frame_set(int(keyframe - 1.0), subframe=0.0) + bpy.ops.render.render( + animation=False, write_still=True) + + # Add strip with the corresponding length + if scene.storypencil_add_render_strip: + frame_start = sq.frame_start + key - 1 + index = keyframe_list.index(key) + if index < len(keyframe_list) - 1: + key_next = keyframe_list[index + 1] + frame_end = frame_start + (key_next - key) + else: + frame_end = scene.frame_end + 1 + + if index == 0 and frame_start > scene.frame_start: + frame_start = scene.frame_start + + if frame_end < frame_start: + frame_end = frame_start + image_ext = self.image_ext[image_settings.file_format] + bpy.ops.sequencer.image_strip_add(directory=root_folder, + files=[ + {"name": framename + image_ext}], + frame_start=int(frame_start), + frame_end=int(frame_end), + channel=channel) + else: + print("Render:" + strip_name + "/" + strip_scene.name) + print("Video From:", scene.frame_start, + "To", scene.frame_end) + # Video + filepath = os.path.join(rootpath, strip_name) + + if image_settings.file_format == 'FFMPEG': + ext = self.video_ext[scene.render.ffmpeg.format] + else: + ext = '.avi' + + if not filepath.endswith(ext): + filepath += ext + + scene.render.use_file_extension = False + scene.render.filepath = filepath + + # Render Animation + bpy.ops.render.render(animation=True) + + # Add video to add strip later + if scene.storypencil_add_render_strip: + Videos.append( + [filepath, sq.frame_start + sq.frame_offset_start]) + + # Add pending video Strips + for vid in Videos: + bpy.ops.sequencer.movie_strip_add(filepath=vid[0], + frame_start=int(vid[1]), + channel=channel) + + scene.frame_start = prv_start + scene.frame_end = prv_end + scene.render.use_file_extension = prv_use_file_extension + image_settings.file_format = prv_format + scene.render.ffmpeg.format = prv_ffmpeg_format + + scene.render.filepath = prv_path + scene.frame_set(int(prv_frame)) + + context.window.cursor_set('DEFAULT') + + return {'FINISHED'} + + except: + print("Unexpected error:" + str(sys.exc_info())) + self.report({'ERROR'}, "Unable to render") + scene.frame_start = prv_start + scene.frame_end = prv_end + scene.render.use_file_extension = prv_use_file_extension + image_settings.file_format = prv_format + + scene.render.filepath = prv_path + scene.frame_set(int(prv_frame)) + context.window.cursor_set('DEFAULT') + return {'FINISHED'} diff --git a/storypencil/scene_tools.py b/storypencil/scene_tools.py new file mode 100644 index 0000000..ffc6d18 --- /dev/null +++ b/storypencil/scene_tools.py @@ -0,0 +1,173 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import bpy +import os + +from bpy.types import ( + Operator, +) + + +# ------------------------------------------------------------- +# Add a new scene and set to new strip +# +# ------------------------------------------------------------- +class STORYPENCIL_OT_NewScene(Operator): + bl_idname = "storypencil.new_scene" + bl_label = "New Scene" + bl_description = "Create a new scene base on template scene" + bl_options = {'REGISTER', 'UNDO'} + + scene_name: bpy.props.StringProperty(default="Scene") + + # ------------------------------ + # Poll + # ------------------------------ + @classmethod + def poll(cls, context): + scene = context.scene + scene_base = scene.storypencil_base_scene + if scene_base is not None and scene_base.name in bpy.data.scenes: + return True + + return False + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + col = layout.column() + col.prop(self, "scene_name", text="Scene Name") + + def format_to3(self, value): + return f"{value:03}" + + # ------------------------------ + # Execute button action + # ------------------------------ + def execute(self, context): + scene_prv = context.scene + cfra_prv = scene_prv.frame_current + scene_base = scene_prv.storypencil_base_scene + + # Set context to base scene and duplicate + context.window.scene = scene_base + bpy.ops.scene.new(type='FULL_COPY') + scene_new = context.window.scene + new_name = scene_prv.storypencil_name_prefix + \ + self.scene_name + scene_prv.storypencil_name_suffix + id = 0 + while new_name in bpy.data.scenes: + id += 1 + new_name = scene_prv.storypencil_name_prefix + self.scene_name + \ + scene_prv.storypencil_name_suffix + '.' + self.format_to3(id) + + scene_new.name = new_name + # Set duration of new scene + scene_new.frame_end = scene_new.frame_start + \ + scene_prv.storypencil_scene_duration - 1 + + # Back to original scene + context.window.scene = scene_prv + scene_prv.frame_current = cfra_prv + bpy.ops.sequencer.scene_strip_add( + frame_start=cfra_prv, scene=scene_new.name) + + scene_new.update_tag() + scene_prv.update_tag() + + return {"FINISHED"} + + +def draw_new_scene(self, context): + """Add menu options.""" + + self.layout.operator_context = 'INVOKE_REGION_WIN' + row = self.layout.row(align=True) + row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New Base Scene") + + +def setup_storyboard(self, context): + """Add Setup menu option.""" + # For security, check if this is the default template. + is_gpencil = context.active_object and context.active_object.name == 'Stroke' + if is_gpencil and context.workspace.name in ('2D Animation', '2D Full Canvas') and context.scene.name == 'Scene': + if "Video Editing" not in bpy.data.workspaces: + row = self.layout.row(align=True) + row.separator() + row = self.layout.row(align=True) + row.operator(STORYPENCIL_OT_Setup.bl_idname, + text="Setup Storyboard Session") + + +# ------------------------------------------------------------- +# Setup all environment +# +# ------------------------------------------------------------- +class STORYPENCIL_OT_Setup(Operator): + bl_idname = "storypencil.setup" + bl_label = "Setup" + bl_description = "Configure all settings for a storyboard session" + bl_options = {'REGISTER', 'UNDO'} + + # ------------------------------ + # Poll + # ------------------------------ + @classmethod + def poll(cls, context): + return True + + def get_workspace(self, type): + for wrk in bpy.data.workspaces: + if wrk.name == type: + return wrk + + return None + + # ------------------------------ + # Execute button action + # ------------------------------ + def execute(self, context): + scene_base = context.scene + # Create Workspace + templatepath = None + if "Video Editing" not in bpy.data.workspaces: + template_path = None + for path in bpy.utils.app_template_paths(): + template_path = path + + filepath = os.path.join( + template_path, "Video_Editing", "startup.blend") + bpy.ops.workspace.append_activate( + idname="Video Editing", filepath=filepath) + # Create New scene + bpy.ops.scene.new() + scene_edit = context.scene + scene_edit.name = 'Edit' + # Rename original base scene + scene_base.name = 'Base' + # Setup Edit scene settings + scene_edit.storypencil_main_workspace = self.get_workspace( + "Video Editing") + scene_edit.storypencil_main_scene = scene_edit + scene_edit.storypencil_base_scene = scene_base + scene_edit.storypencil_edit_workspace = self.get_workspace( + "2D Animation") + + # Add a new strip (need set the area context) + context.window.scene = scene_edit + area_prv = context.area.ui_type + context.area.ui_type = 'SEQUENCE_EDITOR' + prv_frame = scene_edit.frame_current + + scene_edit.frame_current = scene_edit.frame_start + bpy.ops.storypencil.new_scene() + + context.area.ui_type = area_prv + scene_edit.frame_current = prv_frame + + scene_edit.update_tag() + bpy.ops.sequencer.reload() + + return {"FINISHED"} diff --git a/storypencil/synchro.py b/storypencil/synchro.py new file mode 100644 index 0000000..86a762a --- /dev/null +++ b/storypencil/synchro.py @@ -0,0 +1,790 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +from typing import List, Sequence, Tuple + +import bpy +import functools +import os +from bpy.app.handlers import persistent + +from bpy.types import ( + Context, + MetaSequence, + Operator, + PropertyGroup, + SceneSequence, + Window, + WindowManager, +) +from bpy.props import ( + BoolProperty, + IntProperty, + StringProperty, +) +from .scene_tools import STORYPENCIL_OT_NewScene +from .render import STORYPENCIL_OT_RenderAction + +def window_id(window: Window) -> str: + """ Get Window's ID. + + :param window: the Window to consider + :return: the Window's ID + """ + return str(window.as_pointer()) + + +def get_window_from_id(wm: WindowManager, win_id: str) -> Window: + """Get a Window object from its ID (serialized ptr). + + :param wm: a WindowManager holding Windows + :param win_id: the ID of the Window to get + :return: the Window matching the given ID, None otherwise + """ + return next((w for w in wm.windows if w and window_id(w) == win_id), None) + + +def get_main_windows_list(wm: WindowManager) -> Sequence[Window]: + """Get all the Main Windows held by the given WindowManager `wm`""" + return [w for w in wm.windows if w and w.parent is None] + + +def join_win_ids(ids: List[str]) -> str: + """Join Windows IDs in a single string""" + return ";".join(ids) + + +def split_win_ids(ids: str) -> List[str]: + """Split a Windows IDs string into individual IDs""" + return ids.split(";") + + +class STORYPENCIL_OT_SetSyncMainOperator(Operator): + bl_idname = "storypencil.sync_set_main" + bl_label = "Set as Sync Main" + bl_description = "Set this Window as main for Synchronization" + bl_options = {'INTERNAL'} + + win_id: bpy.props.StringProperty( + name="Window ID", + default="", + options=set(), + description="Main window ID", + ) + + def copy_settings(self, main_window, slave_window): + if main_window is None or slave_window is None: + return + slave_window.scene.storypencil_main_workspace = main_window.scene.storypencil_main_workspace + slave_window.scene.storypencil_main_scene = main_window.scene.storypencil_main_scene + slave_window.scene.storypencil_edit_workspace = main_window.scene.storypencil_edit_workspace + + def execute(self, context): + options = context.window_manager.storypencil_settings + options.main_window_id = self.win_id + wm = bpy.context.window_manager + scene = context.scene + wm['storypencil_use_new_window'] = scene.storypencil_use_new_window + + main_windows = get_main_windows_list(wm) + main_window = get_main_window(wm) + slave_window = get_slave_window(wm) + # Active sync + options.active = True + if slave_window is None: + # Open a new window + if len(main_windows) < 2: + bpy.ops.storypencil.create_slave_window() + slave_window = get_slave_window(wm) + self.copy_settings(get_main_window(wm), slave_window) + return {'FINISHED'} + else: + # Reuse the existing window + slave_window = get_not_main_window(wm) + else: + # Open new slave + if len(main_windows) < 2: + bpy.ops.storypencil.create_slave_window() + slave_window = get_slave_window(wm) + self.copy_settings(get_main_window(wm), slave_window) + return {'FINISHED'} + else: + # Reuse the existing window + slave_window = get_not_main_window(wm) + + if slave_window: + enable_slave_window(wm, window_id(slave_window)) + win_id = window_id(slave_window) + self.copy_settings(get_main_window(wm), slave_window) + bpy.ops.storypencil.sync_window_bring_front(win_id=win_id) + + return {'FINISHED'} + + +class STORYPENCIL_OT_AddSlaveWindowOperator(Operator): + bl_idname = "storypencil.create_slave_window" + bl_label = "Create Slave Window" + bl_description = "Create a Slave Main Window and enable Synchronization" + bl_options = {'INTERNAL'} + + def execute(self, context): + # store existing windows + windows = set(context.window_manager.windows[:]) + bpy.ops.wm.window_new_main() + # get newly created window by comparing to previous list + new_window = (set(context.window_manager.windows[:]) - windows).pop() + # activate sync system and enable sync for this window + toggle_slave_window(context.window_manager, window_id(new_window)) + context.window_manager.storypencil_settings.active = True + # trigger initial synchronization to open the current Sequence's Scene + on_frame_changed() + # Configure the new window + self.configure_new_slave_window(context, new_window) + + return {'FINISHED'} + + def configure_new_slave_window(self, context, new_window): + wrk_name = context.scene.storypencil_edit_workspace.name + override_context = context.copy() + override_context["window"] = new_window + # Open the 2D workspace + blendpath = os.path.dirname(bpy.app.binary_path) + version = bpy.app.version + version_full = str(version[0]) + '.' + str(version[1]) + template = os.path.join("scripts", "startup", + "bl_app_templates_system") + template = os.path.join(template, wrk_name, "startup.blend") + template_path = os.path.join(blendpath, version_full, template) + # Check if workspace exist and add it if missing + for wk in bpy.data.workspaces: + if wk.name == wrk_name: + new_window.workspace = wk + return + bpy.ops.workspace.append_activate( + override_context, idname=wk_name, filepath=template_path) + + +class STORYPENCIL_OT_WindowBringFront(Operator): + bl_idname = "storypencil.sync_window_bring_front" + bl_label = "Bring Window Front" + bl_description = "Bring a Window to Front" + bl_options = {'INTERNAL'} + + win_id: bpy.props.StringProperty() + + def execute(self, context): + c = context.copy() + win = get_window_from_id(context.window_manager, self.win_id) + if not win: + return {'CANCELLED'} + c["window"] = win + bpy.ops.wm.window_fullscreen_toggle(c) + bpy.ops.wm.window_fullscreen_toggle(c) + return {'FINISHED'} + + +class STORYPENCIL_OT_WindowCloseOperator(Operator): + bl_idname = "storypencil.close_slave_window" + bl_label = "Close Window" + bl_description = "Close a specific Window" + bl_options = {'INTERNAL'} + + win_id: bpy.props.StringProperty() + + def execute(self, context): + c = context.copy() + win = get_window_from_id(context.window_manager, self.win_id) + if not win: + return {'CANCELLED'} + c["window"] = win + bpy.ops.wm.window_close(c) + return {'FINISHED'} + + +def validate_sync(window_manager: WindowManager) -> bool: + """ + Ensure synchronization system is functional, with a valid main window. + Disable it otherwise and return the system status. + """ + if not window_manager.storypencil_settings.active: + return False + if not get_window_from_id(window_manager, window_manager.storypencil_settings.main_window_id): + window_manager.storypencil_settings.active = False + return window_manager.storypencil_settings.active + + +def get_slave_window_indices(wm: WindowManager) -> List[str]: + """Get slave Windows indices as a list of IDs + + :param wm: the WindowManager to consider + :return: the list of slave Windows IDs + """ + return split_win_ids(wm.storypencil_settings.slave_windows_ids) + + +def is_slave_window(window_manager: WindowManager, win_id: str) -> bool: + """Return wether the Window identified by 'win_id' is a slave window. + + :return: whether this Window is a sync slave + """ + return win_id in get_slave_window_indices(window_manager) + + +def enable_slave_window(wm: WindowManager, win_id: str): + """Enable the slave status of a Window. + + :param wm: the WindowManager instance + :param win_id: the id of the window + """ + slave_indices = get_slave_window_indices(wm) + win_id_str = win_id + # Delete old indice if exist + if win_id_str in slave_indices: + slave_indices.remove(win_id_str) + + # Add indice + slave_indices.append(win_id_str) + + # rebuild the whole list of valid slave windows + slave_indices = [ + idx for idx in slave_indices if get_window_from_id(wm, idx)] + + wm.storypencil_settings.slave_windows_ids = join_win_ids(slave_indices) + + +def toggle_slave_window(wm: WindowManager, win_id: str): + """Toggle the slave status of a Window. + + :param wm: the WindowManager instance + :param win_id: the id of the window + """ + slave_indices = get_slave_window_indices(wm) + win_id_str = win_id + if win_id_str in slave_indices: + slave_indices.remove(win_id_str) + else: + slave_indices.append(win_id_str) + + # rebuild the whole list of valid slave windows + slave_indices = [ + idx for idx in slave_indices if get_window_from_id(wm, idx)] + + wm.storypencil_settings.slave_windows_ids = join_win_ids(slave_indices) + + +def get_main_window(wm: WindowManager) -> Window: + """Get the Window used to drive the synchronization system + + :param wm: the WindowManager instance + :returns: the main Window or None + """ + return get_window_from_id(wm=wm, win_id=wm.storypencil_settings.main_window_id) + + +def get_slave_window(wm: WindowManager) -> Window: + """Get the first slave Window + + :param wm: the WindowManager instance + :returns: the Window or None + """ + for w in wm.windows: + win_id = window_id(w) + if is_slave_window(wm, win_id): + return w + + return None + + +def get_not_main_window(wm: WindowManager) -> Window: + """Get the first not main Window + + :param wm: the WindowManager instance + :returns: the Window or None + """ + for w in wm.windows: + win_id = window_id(w) + if win_id != wm.storypencil_settings.main_window_id: + return w + + return None + + +def get_main_strip(wm: WindowManager) -> SceneSequence: + """Get Scene Strip at current time in Main window + + :param wm: the WindowManager instance + :returns: the Strip at current time or None + """ + main_window = get_main_window(wm=wm) + if not main_window or not main_window.scene.sequence_editor: + return None + seq_editor = main_window.scene.sequence_editor + return seq_editor.sequences.get(wm.storypencil_settings.main_strip_name, None) + + +class STORYPENCIL_OT_SyncToggleSlave(Operator): + bl_idname = "storypencil.sync_toggle_slave" + bl_label = "Toggle Slave Window Status" + bl_description = "Enable/Disable synchronization for a specific Window" + bl_options = {'INTERNAL'} + + win_id: bpy.props.StringProperty(name="Window Index") + + def execute(self, context): + wm = context.window_manager + toggle_slave_window(wm, self.win_id) + return {'FINISHED'} + + +def get_sequences_at_frame( + frame: int, + sequences: Sequence[Sequence]) -> Sequence[bpy.types.Sequence]: + """ Get all sequencer strips at given frame. + + :param frame: the frame to consider + """ + return [s for s in sequences if frame >= s.frame_start + s.frame_offset_start and + frame < s.frame_start + s.frame_offset_start + s.frame_final_duration] + + +def get_sequence_at_frame( + frame: int, + sequences: Sequence[bpy.types.Sequence] = None, + skip_muted: bool = True, +) -> Tuple[bpy.types.Sequence, int]: + """ + Get the higher sequence strip in channels stack at current frame. + Recursively enters scene sequences and returns the original frame in the + returned strip's time referential. + + :param frame: the frame to consider + :param skip_muted: skip muted strips + :returns: the sequence strip and the frame in strip's time referential + """ + + strips = get_sequences_at_frame(frame, sequences or bpy.context.sequences) + + # exclude muted strips + if skip_muted: + strips = [strip for strip in strips if not strip.mute] + + if not strips: + return None, frame + + # Remove strip not scene type. Switch is only with Scenes + for strip in strips: + if strip.type != 'SCENE': + strips.remove(strip) + + # consider higher strip in stack + strip = sorted(strips, key=lambda x: x.channel)[-1] + # go deeper when current strip is a MetaSequence + if isinstance(strip, MetaSequence): + return get_sequence_at_frame(frame, strip.sequences, skip_muted) + if isinstance(strip, SceneSequence): + # apply time offset to get in sequence's referential + frame = frame - strip.frame_start + strip.scene.frame_start + # enter scene's sequencer if used as input + if strip.scene_input == 'SEQUENCER': + return get_sequence_at_frame(frame, strip.scene.sequence_editor.sequences) + return strip, frame + + +def set_scene_frame(scene, frame, force_update_main=False): + """ + Set `scene` frame_current to `frame` if different. + + :param scene: the scene to update + :param frame: the frame value + :param force_update_main: whether to force the update of main scene + """ + options = bpy.context.window_manager.storypencil_settings + if scene.frame_current != frame: + scene.frame_current = frame + scene.frame_set(int(frame)) + if force_update_main: + update_sync( + bpy.context, bpy.context.window_manager.storypencil_settings.main_window_id) + + +def setup_window_from_scene_strip(window: Window, strip: SceneSequence): + """Change the Scene and camera of `window` based on `strip`. + + :param window: [description] + :param scene_strip: [description] + """ + if window.scene != strip.scene: + window.scene = strip.scene + if strip.scene_camera and strip.scene_camera != window.scene.camera: + strip.scene.camera = strip.scene_camera + + +@persistent +def on_frame_changed(*args): + """ + React to current frame changes and synchronize slave windows. + """ + # ensure context is fully initialized, i.e not '_RestrictData + if not isinstance(bpy.context, Context): + return + + # happens in some cases (not sure why) + if not bpy.context.window: + return + + wm = bpy.context.window_manager + + # early return if synchro is disabled / not available + if not validate_sync(wm) or len(bpy.data.scenes) < 2: + return + + # get current window id + update_sync(bpy.context) + + +def update_sync(context: Context, win_id=None): + """ Update synchronized Windows based on the current `context`. + + :param context: the context + :param win_id: specify a window id (context.window is used otherwise) + """ + wm = context.window_manager + + if not win_id: + win_id = window_id(context.window) + + main_scene = get_window_from_id( + wm, wm.storypencil_settings.main_window_id).scene + if not main_scene.sequence_editor: + return + + # return if scene's sequence editor has no sequences + sequences = main_scene.sequence_editor.sequences + if not sequences: + return + + # bidirectionnal sync: change main time from slave window + if ( + wm.storypencil_settings.bidirectional + and win_id != wm.storypencil_settings.main_window_id + and is_slave_window(wm, win_id) + ): + # get strip under time cursor in main window + strip, old_frame = get_sequence_at_frame( + main_scene.frame_current, + sequences=sequences + ) + # only do bidirectional sync if slave window matches the strip at current time in main + if not isinstance(strip, SceneSequence) or strip.scene != context.scene: + return + + # calculate offset + frame_offset = context.scene.frame_current - old_frame + if frame_offset == 0: + return + + new_main_frame = main_scene.frame_current + frame_offset + update_main_time = True + # check if a valid scene strip is available under new frame before changing main time + f_start = strip.frame_start + strip.frame_offset_start + f_end = f_start + strip.frame_final_duration + if new_main_frame < f_start or new_main_frame >= f_end: + new_strip, _ = get_sequence_at_frame( + new_main_frame, + main_scene.sequence_editor.sequences, + ) + update_main_time = isinstance(new_strip, SceneSequence) + if update_main_time: + # update main time change in the next event loop + force the sync system update + # because Blender won't trigger a frame_changed event to avoid infinite recursion + bpy.app.timers.register( + functools.partial(set_scene_frame, main_scene, + new_main_frame, True) + ) + + return + + # return if current window is not main window + if win_id != wm.storypencil_settings.main_window_id: + return + + slave_windows = [ + get_window_from_id(wm, win_id) + for win_id + in get_slave_window_indices(wm) + if win_id and win_id != wm.storypencil_settings.main_window_id + ] + + # only work with at least 2 windows + if not slave_windows: + return + + seq, frame = get_sequence_at_frame(main_scene.frame_current, sequences) + + # return if no sequence at current time or not a scene strip + if not isinstance(seq, SceneSequence) or not seq.scene: + wm.storypencil_settings.main_strip_name = "" + return + + wm.storypencil_settings.main_strip_name = seq.name + # change the scene on slave windows + # warning: only one window's scene can be changed in this event loop, + # otherwise it may crashes Blender randomly + for idx, win in enumerate(slave_windows): + if not win: + continue + # change first slave window immediately + if idx == 0: + setup_window_from_scene_strip(win, seq) + else: + # trigger change in next event loop for other windows + bpy.app.timers.register( + functools.partial(setup_window_from_scene_strip, win, seq) + ) + + set_scene_frame(seq.scene, frame) + + +def sync_all_windows(wm: WindowManager): + """Enable synchronization on all main windows held by `wm`.""" + wm.storypencil_settings.slave_windows_ids = join_win_ids([ + window_id(w) + for w + in get_main_windows_list(wm) + ]) + + +@persistent +def sync_autoconfig(*args): + """Autoconfigure synchronization system. + If a window contains a VSE area on a scene with a valid sequence_editor, + makes it main window and enable synchronization on all other main windows. + """ + main_windows = get_main_windows_list(bpy.context.window_manager) + # don't try to go any further if only one main window + if len(main_windows) < 2: + return + + # look for a main window with a valid sequence editor + main = next( + ( + win + for win in main_windows + if win.scene.sequence_editor + and any(area.type == 'SEQUENCE_EDITOR' for area in win.screen.areas) + ), + None + ) + # if any, set as main and activate sync on all other windows + if main: + bpy.context.window_manager.storypencil_settings.main_window_id = window_id( + main) + sync_all_windows(bpy.context.window_manager) + bpy.context.window_manager.storypencil_settings.active = True + + +def sync_active_update(self, context): + """ Update function for WindowManager.storypencil_settings.active. """ + # ensure main window is valid, using current context's window if none is set + if ( + self.active + and ( + not self.main_window_id + or not get_window_from_id(context.window_manager, self.main_window_id) + ) + ): + self.main_window_id = window_id(context.window) + # automatically sync all other windows if nothing was previously set + if not self.slave_windows_ids: + sync_all_windows(context.window_manager) + + on_frame_changed() + + +def draw_sync_header(self, context): + """Draw Window sync tools header.""" + + wm = context.window_manager + self.layout.separator() + if wm.get('storypencil_use_new_window') is not None: + new_window = wm['storypencil_use_new_window'] + else: + new_window = False + + if not new_window: + if context.scene.storypencil_main_workspace: + if context.scene.storypencil_main_workspace.name != context.workspace.name: + if context.area.ui_type == 'DOPESHEET': + row = self.layout.row(align=True) + row.operator(STORYPENCIL_OT_Switch.bl_idname, + text="Back To VSE") + + +def draw_sync_sequencer_header(self, context): + """Draw Window sync tools header.""" + if context.space_data.view_type != 'SEQUENCER': + return + + wm = context.window_manager + layout = self.layout + layout.separator() + row = layout.row(align=True) + row.label(text="Scenes:") + if context.scene.storypencil_use_new_window: + row.operator(STORYPENCIL_OT_SetSyncMainOperator.bl_idname, text="Edit") + else: + row.operator(STORYPENCIL_OT_Switch.bl_idname, text="Edit") + + row.menu("STORYPENCIL_MT_extra_options", icon='DOWNARROW_HLT', text="") + + row.separator() + layout.operator_context = 'INVOKE_REGION_WIN' + row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New") + + layout.operator_context = 'INVOKE_DEFAULT' + row.separator(factor=0.5) + row.operator(STORYPENCIL_OT_RenderAction.bl_idname, text="Render") + + +class STORYPENCIL_PG_Settings(PropertyGroup): + """ + PropertyGroup with storypencil settings. + """ + active: BoolProperty( + name="Synchronize", + description=( + "Automatically open current Sequence's Scene in other " + "Main Windows and activate Time Synchronization"), + default=False, + update=sync_active_update + ) + + bidirectional: BoolProperty( + name="Bi-directional", + description="Enable bi-directional sync to drive Main time from synced Slave Windows", + default=False, + ) + + main_window_id: StringProperty( + name="Main Window ID", + description="ID of the window driving the Synchronization", + default="", + ) + + slave_windows_ids: StringProperty( + name="Slave Windows", + description="Serialized Slave Window Indices", + default="", + ) + + active_window_index: IntProperty( + name="Active Window Index", + description="Index for using Window Manager's windows in a UIList", + default=0 + ) + + main_strip_name: StringProperty( + name="Main Strip Name", + description="Scene Strip at current time in the Main window", + default="", + ) + + show_main_strip_range: BoolProperty( + name="Show Main Strip Range in Slave Windows", + description="Draw main Strip's in/out markers in synchronized slave Windows", + default=True, + ) + + +# ------------------------------------------------------------- +# Switch manually between Main and Edit Scene and Layout +# +# ------------------------------------------------------------- +class STORYPENCIL_OT_Switch(Operator): + bl_idname = "storypencil.switch" + bl_label = "Switch" + bl_description = "Switch workspace" + bl_options = {'REGISTER', 'UNDO'} + + # Get active strip + def act_strip(self, context): + scene = context.scene + sequences = scene.sequence_editor.sequences + if not sequences: + return None + # Get strip under time cursor + strip, old_frame = get_sequence_at_frame( + scene.frame_current, sequences=sequences) + return strip + + # ------------------------------ + # Poll + # ------------------------------ + @classmethod + def poll(cls, context): + scene = context.scene + if scene.storypencil_main_workspace is None or scene.storypencil_main_scene is None: + return False + if scene.storypencil_edit_workspace is None: + return False + + return True + + # ------------------------------ + # Execute button action + # ------------------------------ + def execute(self, context): + wm = context.window_manager + scene = context.scene + wm['storypencil_use_new_window'] = scene.storypencil_use_new_window + + # Switch to Main + if scene.storypencil_main_workspace.name != context.workspace.name: + cfra_prv = scene.frame_current + prv_pin = None + if scene.storypencil_main_workspace is not None: + if scene.storypencil_main_workspace.use_pin_scene: + scene.storypencil_main_workspace.use_pin_scene = False + + context.window.workspace = scene.storypencil_main_workspace + + if scene.storypencil_main_scene is not None: + context.window.scene = scene.storypencil_main_scene + strip = self.act_strip(context) + if strip: + context.window.scene.frame_current = int(cfra_prv + strip.frame_start) - 1 + + bpy.ops.sequencer.reload() + else: + # Switch to Edit + strip = self.act_strip(context) + # save camera + if strip is not None and strip.type == "SCENE": + # Save data + strip.scene.storypencil_main_workspace = scene.storypencil_main_workspace + strip.scene.storypencil_main_scene = scene.storypencil_main_scene + strip.scene.storypencil_edit_workspace = scene.storypencil_edit_workspace + + # Set workspace and Scene + cfra_prv = scene.frame_current + if scene.storypencil_edit_workspace.use_pin_scene: + scene.storypencil_edit_workspace.use_pin_scene = False + + context.window.workspace = scene.storypencil_edit_workspace + context.window.workspace.update_tag() + + context.window.scene = strip.scene + active_frame = cfra_prv - strip.frame_start + 1 + if active_frame < strip.scene.frame_start: + active_frame = strip.scene.frame_start + context.window.scene.frame_current = int(active_frame) + + # Set camera + if strip.scene_input == 'CAMERA': + for screen in bpy.data.screens: + for area in screen.areas: + if area.type == 'VIEW_3D': + # select camera as view + if strip and strip.scene.camera is not None: + area.spaces.active.region_3d.view_perspective = 'CAMERA' + + return {"FINISHED"} diff --git a/storypencil/ui.py b/storypencil/ui.py new file mode 100644 index 0000000..4973b4d --- /dev/null +++ b/storypencil/ui.py @@ -0,0 +1,211 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import bpy + +from bpy.types import ( + Menu, + Panel, +) + +from .synchro import get_main_window, validate_sync, window_id + + +class STORYPENCIL_MT_extra_options(Menu): + bl_label = "Scene Settings" + + def draw(self, context): + layout = self.layout + wm = bpy.context.window_manager + scene = context.scene + layout.prop(scene, "storypencil_use_new_window") + + # If no main window nothing else to do + if not get_main_window(wm): + return + + win_id = window_id(context.window) + row = self.layout.row(align=True) + if not validate_sync(window_manager=wm) or win_id == wm.storypencil_settings.main_window_id: + row = layout.row() + row.prop(wm.storypencil_settings, "active", + text="Timeline Synchronization") + row.active = scene.storypencil_use_new_window + + row = layout.row() + row.prop(wm.storypencil_settings, + "show_main_strip_range", text="Show Strip Range") + row.active = scene.storypencil_use_new_window + + +# ------------------------------------------------------ +# Defines UI panel +# ------------------------------------------------------ +# ------------------------------------------------------------------ +# Define panel class for manual switch parameters. +# ------------------------------------------------------------------ +class STORYPENCIL_PT_Settings(Panel): + bl_idname = "STORYPENCIL_PT_Settings" + bl_label = "Settings" + bl_space_type = 'SEQUENCE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Storypencil' + + @classmethod + def poll(cls, context): + if context.space_data.view_type != 'SEQUENCER': + return False + + return True + + # ------------------------------ + # Draw UI + # ------------------------------ + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + +class STORYPENCIL_PT_General(Panel): + bl_idname = "STORYPENCIL_PT_General" + bl_label = "General" + bl_space_type = 'SEQUENCE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Storypencil' + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "STORYPENCIL_PT_Settings" + + @classmethod + def poll(cls, context): + if context.space_data.view_type != 'SEQUENCER': + return False + + return True + + # ------------------------------ + # Draw UI + # ------------------------------ + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + scene = context.scene + + setup_ready = scene.storypencil_main_workspace is not None + row = layout.row() + row.alert = not setup_ready + row.prop(scene, "storypencil_main_workspace", text="VSE Workspace") + + row = layout.row() + if scene.storypencil_main_scene is None: + row.alert = True + row.prop(scene, "storypencil_main_scene", text="VSE Scene") + + layout.separator() + + row = layout.row() + if scene.storypencil_main_workspace and scene.storypencil_edit_workspace: + if scene.storypencil_main_workspace.name == scene.storypencil_edit_workspace.name: + row.alert = True + if scene.storypencil_edit_workspace is None: + row.alert = True + row.prop(scene, "storypencil_edit_workspace", text="Drawing Workspace") + + +class STORYPENCIL_PT_RenderPanel(Panel): + bl_label = "Render Strips" + bl_space_type = 'SEQUENCE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Storypencil' + bl_parent_id = "STORYPENCIL_PT_Settings" + + @classmethod + def poll(cls, context): + if context.space_data.view_type != 'SEQUENCER': + return False + + return True + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + scene = context.scene + settings = scene.render.image_settings + + is_video = settings.file_format in {'FFMPEG', 'AVI_JPEG', 'AVI_RAW'} + row = layout.row() + if scene.storypencil_render_render_path is None: + row.alert = True + row.prop(scene, "storypencil_render_render_path") + + row = layout.row() + row.prop(scene, "storypencil_render_onlyselected") + + row = layout.row() + row.prop(scene.render.image_settings, "file_format") + + if settings.file_format == 'FFMPEG': + row = layout.row() + row.prop(scene.render.ffmpeg, "format") + + row = layout.row() + row.enabled = is_video + row.prop(scene.render.ffmpeg, "audio_codec") + + row = layout.row() + row.prop(scene, "storypencil_add_render_strip") + + row = layout.row() + row.enabled = scene.storypencil_add_render_strip + row.prop(scene, "storypencil_render_channel") + + if not is_video: + row = layout.row() + row.prop(scene, "storypencil_render_step") + + row = layout.row() + row.prop(scene, "storypencil_render_numbering") + + row = layout.row() + row.prop(scene, "storypencil_add_render_byfolder") + + +# ------------------------------------------------------------------ +# Define panel class for new base scene creation. +# ------------------------------------------------------------------ +class STORYPENCIL_PT_SettingsNew(Panel): + bl_idname = "STORYPENCIL_PT_SettingsNew" + bl_label = "New Scenes" + bl_space_type = 'SEQUENCE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Storypencil' + bl_parent_id = "STORYPENCIL_PT_Settings" + + @classmethod + def poll(cls, context): + if context.space_data.view_type != 'SEQUENCER': + return False + + return True + + # ------------------------------ + # Draw UI + # ------------------------------ + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + scene = context.scene + row = layout.row() + row.prop(scene, "storypencil_name_prefix", text="Name Prefix") + row = layout.row() + row.prop(scene, "storypencil_name_suffix", text="Name Suffix") + row = layout.row() + row.prop(scene, "storypencil_scene_duration", text="Frames") + + row = layout.row() + if scene.storypencil_base_scene is None: + row.alert = True + row.prop(scene, "storypencil_base_scene", text="Base Scene") diff --git a/storypencil/utils.py b/storypencil/utils.py new file mode 100644 index 0000000..9357d1c --- /dev/null +++ b/storypencil/utils.py @@ -0,0 +1,110 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import bpy +import math + + +def redraw_areas_by_type(window, area_type, region_type='WINDOW'): + """Redraw `window`'s areas matching the given `area_type` and optionnal `region_type`.""" + for area in window.screen.areas: + if area.type == area_type: + for region in area.regions: + if region.type == region_type: + region.tag_redraw() + + +def redraw_all_areas_by_type(context, area_type, region_type='WINDOW'): + """Redraw areas in all windows matching the given `area_type` and optionnal `region_type`.""" + for window in context.window_manager.windows: + redraw_areas_by_type(window, area_type, region_type) + + +def get_selected_keyframes(context): + """Get list of selected keyframes for any object in the scene. """ + keys = [] + + for ob in context.scene.objects: + if ob.type == 'GPENCIL': + for gpl in ob.data.layers: + for gpf in gpl.frames: + if gpf.select: + keys.append(gpf.frame_number) + + elif ob.animation_data is not None and ob.animation_data.action is not None: + action = ob.animation_data.action + for fcu in action.fcurves: + for kp in fcu.keyframe_points: + if kp.select_control_point: + keys.append(int(kp.co[0])) + + keys.sort() + unique_keys = list(set(keys)) + return unique_keys + + +def find_collections_recursive(root, collections=None): + # Initialize the result once + if collections is None: + collections = [] + + def recurse(parent, result): + result.append(parent) + # Look over children at next level + for child in parent.children: + recurse(child, result) + + recurse(root, collections) + + return collections + + +def get_keyframe_list(scene, frame_start, frame_end): + """Get list of frames for any gpencil object in the scene and meshes. """ + keys = [] + root = scene.view_layers[0].layer_collection + collections = find_collections_recursive(root) + + for laycol in collections: + if laycol.exclude is True or laycol.collection.hide_render is True: + continue + for ob in laycol.collection.objects: + if ob.hide_render: + continue + if ob.type == 'GPENCIL': + for gpl in ob.data.layers: + if gpl.hide: + continue + for gpf in gpl.frames: + if frame_start <= gpf.frame_number <= frame_end: + keys.append(gpf.frame_number) + + # Animation at object level + if ob.animation_data is not None and ob.animation_data.action is not None: + action = ob.animation_data.action + for fcu in action.fcurves: + for kp in fcu.keyframe_points: + if frame_start <= int(kp.co[0]) <= frame_end: + keys.append(int(kp.co[0])) + + # Animation at datablock level + if ob.type != 'GPENCIL': + data = ob.data + if data and data.animation_data is not None and data.animation_data.action is not None: + action = data.animation_data.action + for fcu in action.fcurves: + for kp in fcu.keyframe_points: + if frame_start <= int(kp.co[0]) <= frame_end: + keys.append(int(kp.co[0])) + + # Scene Markers + for m in scene.timeline_markers: + if frame_start <= m.frame <= frame_end and m.camera is not None: + keys.append(int(m.frame)) + + # If no animation or markers, must add first frame + if len(keys) == 0: + keys.append(int(frame_start)) + + unique_keys = list(set(keys)) + unique_keys.sort() + return unique_keys -- 2.30.2 From 7e86118607df4ffdb87f1726031302916a1fee9e Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Mon, 19 Sep 2022 15:59:18 +0200 Subject: [PATCH 17/33] Storypencil: Remove BGL module This was deprecated Also, fixed version number --- storypencil/__init__.py | 2 +- storypencil/dopesheet_overlay.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/storypencil/__init__.py b/storypencil/__init__.py index 86b93b1..4cf4616 100644 --- a/storypencil/__init__.py +++ b/storypencil/__init__.py @@ -7,7 +7,7 @@ bl_info = { "name": "Storypencil - Storyboard Tools", "description": "Storyboard tools", "author": "Antonio Vazquez, Matias Mendiola, Daniel Martinez Lara, Rodrigo Blaas", - "version": (0, 1, 1), + "version": (1, 1, 0), "blender": (3, 3, 0), "location": "", "warning": "", diff --git a/storypencil/dopesheet_overlay.py b/storypencil/dopesheet_overlay.py index 63f3cf3..754b8e5 100644 --- a/storypencil/dopesheet_overlay.py +++ b/storypencil/dopesheet_overlay.py @@ -3,7 +3,6 @@ import typing import bpy -import bgl import gpu from gpu_extras.batch import batch_for_shader @@ -38,7 +37,7 @@ class LineDrawer: if not coords: return - bgl.glEnable(bgl.GL_BLEND) + gpu.state.blend_set('ALPHA') self.shader.uniform_float("color", color) @@ -46,7 +45,7 @@ class LineDrawer: batch.program_set(self.shader) batch.draw() - bgl.glDisable(bgl.GL_BLEND) + gpu.state.blend_set('NONE') def get_scene_strip_in_out(strip): -- 2.30.2 From c43c0b2bcf08c34d933c3b56f096c9a23c8eff68 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Tue, 20 Sep 2022 20:05:54 +0200 Subject: [PATCH 18/33] Storypencil: Fix conversion error using frame_current Need an int --- storypencil/synchro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storypencil/synchro.py b/storypencil/synchro.py index 86a762a..839de76 100644 --- a/storypencil/synchro.py +++ b/storypencil/synchro.py @@ -399,7 +399,7 @@ def set_scene_frame(scene, frame, force_update_main=False): """ options = bpy.context.window_manager.storypencil_settings if scene.frame_current != frame: - scene.frame_current = frame + scene.frame_current = int(frame) scene.frame_set(int(frame)) if force_update_main: update_sync( -- 2.30.2 From 52f926b784a1ddd0ab38a2cf8fe0f57ba381955f Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Thu, 10 Nov 2022 22:48:09 +0100 Subject: [PATCH 19/33] Storypencil: Rename Base Scene to Template Scene in UI This makes the name more consistent. --- storypencil/__init__.py | 2 +- storypencil/scene_tools.py | 2 +- storypencil/ui.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/storypencil/__init__.py b/storypencil/__init__.py index 4cf4616..e468201 100644 --- a/storypencil/__init__.py +++ b/storypencil/__init__.py @@ -112,7 +112,7 @@ def register(): description="Workspace used for changing drawings") Scene.storypencil_base_scene = PointerProperty(type=Scene, - description="Base Scene used for creating new scenes") + description="Template Scene used for creating new scenes") Scene.storypencil_render_render_path = StringProperty(name="Output Path", subtype='FILE_PATH', maxlen=256, description="Directory/name to save files") diff --git a/storypencil/scene_tools.py b/storypencil/scene_tools.py index ffc6d18..d3bf037 100644 --- a/storypencil/scene_tools.py +++ b/storypencil/scene_tools.py @@ -85,7 +85,7 @@ def draw_new_scene(self, context): self.layout.operator_context = 'INVOKE_REGION_WIN' row = self.layout.row(align=True) - row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New Base Scene") + row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New Template Scene") def setup_storyboard(self, context): diff --git a/storypencil/ui.py b/storypencil/ui.py index 4973b4d..cc34fbb 100644 --- a/storypencil/ui.py +++ b/storypencil/ui.py @@ -208,4 +208,4 @@ class STORYPENCIL_PT_SettingsNew(Panel): row = layout.row() if scene.storypencil_base_scene is None: row.alert = True - row.prop(scene, "storypencil_base_scene", text="Base Scene") + row.prop(scene, "storypencil_base_scene", text="Template Scene") -- 2.30.2 From 80ebc172347a27bfb34b8dafcf4ae3951d01bd2f Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Thu, 10 Nov 2022 22:48:09 +0100 Subject: [PATCH 20/33] Storypencil: Rename Base Scene to Template Scene in UI This makes the name more consistent. --- storypencil/__init__.py | 2 +- storypencil/scene_tools.py | 2 +- storypencil/ui.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/storypencil/__init__.py b/storypencil/__init__.py index 4cf4616..e468201 100644 --- a/storypencil/__init__.py +++ b/storypencil/__init__.py @@ -112,7 +112,7 @@ def register(): description="Workspace used for changing drawings") Scene.storypencil_base_scene = PointerProperty(type=Scene, - description="Base Scene used for creating new scenes") + description="Template Scene used for creating new scenes") Scene.storypencil_render_render_path = StringProperty(name="Output Path", subtype='FILE_PATH', maxlen=256, description="Directory/name to save files") diff --git a/storypencil/scene_tools.py b/storypencil/scene_tools.py index ffc6d18..d3bf037 100644 --- a/storypencil/scene_tools.py +++ b/storypencil/scene_tools.py @@ -85,7 +85,7 @@ def draw_new_scene(self, context): self.layout.operator_context = 'INVOKE_REGION_WIN' row = self.layout.row(align=True) - row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New Base Scene") + row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New Template Scene") def setup_storyboard(self, context): diff --git a/storypencil/ui.py b/storypencil/ui.py index 4973b4d..cc34fbb 100644 --- a/storypencil/ui.py +++ b/storypencil/ui.py @@ -208,4 +208,4 @@ class STORYPENCIL_PT_SettingsNew(Panel): row = layout.row() if scene.storypencil_base_scene is None: row.alert = True - row.prop(scene, "storypencil_base_scene", text="Base Scene") + row.prop(scene, "storypencil_base_scene", text="Template Scene") -- 2.30.2 From 69b1305f4b74fbd7e847acc6a5566755b9803d78 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Thu, 17 Nov 2022 17:03:01 +0100 Subject: [PATCH 21/33] Storypencil: Do bidirectional synchronization The time line in the child window was not affecting the main window. --- storypencil/synchro.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/storypencil/synchro.py b/storypencil/synchro.py index 839de76..81b47b9 100644 --- a/storypencil/synchro.py +++ b/storypencil/synchro.py @@ -464,8 +464,7 @@ def update_sync(context: Context, win_id=None): # bidirectionnal sync: change main time from slave window if ( - wm.storypencil_settings.bidirectional - and win_id != wm.storypencil_settings.main_window_id + win_id != wm.storypencil_settings.main_window_id and is_slave_window(wm, win_id) ): # get strip under time cursor in main window @@ -658,12 +657,6 @@ class STORYPENCIL_PG_Settings(PropertyGroup): update=sync_active_update ) - bidirectional: BoolProperty( - name="Bi-directional", - description="Enable bi-directional sync to drive Main time from synced Slave Windows", - default=False, - ) - main_window_id: StringProperty( name="Main Window ID", description="ID of the window driving the Synchronization", -- 2.30.2 From 19e8c6bbf0dc048206c770c337e28c6c4c47c828 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Tue, 22 Nov 2022 20:27:50 +0100 Subject: [PATCH 22/33] Storypencil: Rename "slave" to "secondary" Old name was not used today. --- storypencil/__init__.py | 6 +- storypencil/dopesheet_overlay.py | 4 +- storypencil/synchro.py | 160 +++++++++++++++---------------- 3 files changed, 85 insertions(+), 85 deletions(-) diff --git a/storypencil/__init__.py b/storypencil/__init__.py index e468201..b8f4536 100644 --- a/storypencil/__init__.py +++ b/storypencil/__init__.py @@ -57,9 +57,9 @@ classes = ( scene_tools.STORYPENCIL_OT_NewScene, synchro.STORYPENCIL_OT_WindowBringFront, synchro.STORYPENCIL_OT_WindowCloseOperator, - synchro.STORYPENCIL_OT_SyncToggleSlave, + synchro.STORYPENCIL_OT_SyncToggleSecondary, synchro.STORYPENCIL_OT_SetSyncMainOperator, - synchro.STORYPENCIL_OT_AddSlaveWindowOperator, + synchro.STORYPENCIL_OT_AddSecondaryWindowOperator, synchro.STORYPENCIL_OT_Switch, render.STORYPENCIL_OT_RenderAction, ui.STORYPENCIL_PT_Settings, @@ -161,7 +161,7 @@ def register(): bpy.context.window_manager.storypencil_settings.active = False bpy.context.window_manager.storypencil_settings.main_window_id = "" - bpy.context.window_manager.storypencil_settings.slave_windows_ids = "" + bpy.context.window_manager.storypencil_settings.secondary_windows_ids = "" # UI integration in dopesheet header bpy.types.DOPESHEET_HT_header.append(synchro.draw_sync_header) diff --git a/storypencil/dopesheet_overlay.py b/storypencil/dopesheet_overlay.py index 754b8e5..2c00d8e 100644 --- a/storypencil/dopesheet_overlay.py +++ b/storypencil/dopesheet_overlay.py @@ -7,7 +7,7 @@ import gpu from gpu_extras.batch import batch_for_shader from .utils import (redraw_all_areas_by_type) -from .synchro import (is_slave_window, window_id, get_main_strip) +from .synchro import (is_secondary_window, window_id, get_main_strip) Int3 = typing.Tuple[int, int, int] @@ -64,7 +64,7 @@ def draw_callback_px(line_drawer: LineDrawer): if ( not wm.storypencil_settings.active or not wm.storypencil_settings.show_main_strip_range - or not is_slave_window(wm, window_id(context.window)) + or not is_secondary_window(wm, window_id(context.window)) ): return diff --git a/storypencil/synchro.py b/storypencil/synchro.py index 81b47b9..ecdf4cd 100644 --- a/storypencil/synchro.py +++ b/storypencil/synchro.py @@ -71,12 +71,12 @@ class STORYPENCIL_OT_SetSyncMainOperator(Operator): description="Main window ID", ) - def copy_settings(self, main_window, slave_window): - if main_window is None or slave_window is None: + def copy_settings(self, main_window, secondary_window): + if main_window is None or secondary_window is None: return - slave_window.scene.storypencil_main_workspace = main_window.scene.storypencil_main_workspace - slave_window.scene.storypencil_main_scene = main_window.scene.storypencil_main_scene - slave_window.scene.storypencil_edit_workspace = main_window.scene.storypencil_edit_workspace + secondary_window.scene.storypencil_main_workspace = main_window.scene.storypencil_main_workspace + secondary_window.scene.storypencil_main_scene = main_window.scene.storypencil_main_scene + secondary_window.scene.storypencil_edit_workspace = main_window.scene.storypencil_edit_workspace def execute(self, context): options = context.window_manager.storypencil_settings @@ -87,43 +87,43 @@ class STORYPENCIL_OT_SetSyncMainOperator(Operator): main_windows = get_main_windows_list(wm) main_window = get_main_window(wm) - slave_window = get_slave_window(wm) + secondary_window = get_secondary_window(wm) # Active sync options.active = True - if slave_window is None: + if secondary_window is None: # Open a new window if len(main_windows) < 2: - bpy.ops.storypencil.create_slave_window() - slave_window = get_slave_window(wm) - self.copy_settings(get_main_window(wm), slave_window) + bpy.ops.storypencil.create_secondary_window() + secondary_window = get_secondary_window(wm) + self.copy_settings(get_main_window(wm), secondary_window) return {'FINISHED'} else: # Reuse the existing window - slave_window = get_not_main_window(wm) + secondary_window = get_not_main_window(wm) else: - # Open new slave + # Open new secondary if len(main_windows) < 2: - bpy.ops.storypencil.create_slave_window() - slave_window = get_slave_window(wm) - self.copy_settings(get_main_window(wm), slave_window) + bpy.ops.storypencil.create_secondary_window() + secondary_window = get_secondary_window(wm) + self.copy_settings(get_main_window(wm), secondary_window) return {'FINISHED'} else: # Reuse the existing window - slave_window = get_not_main_window(wm) + secondary_window = get_not_main_window(wm) - if slave_window: - enable_slave_window(wm, window_id(slave_window)) - win_id = window_id(slave_window) - self.copy_settings(get_main_window(wm), slave_window) + if secondary_window: + enable_secondary_window(wm, window_id(secondary_window)) + win_id = window_id(secondary_window) + self.copy_settings(get_main_window(wm), secondary_window) bpy.ops.storypencil.sync_window_bring_front(win_id=win_id) return {'FINISHED'} -class STORYPENCIL_OT_AddSlaveWindowOperator(Operator): - bl_idname = "storypencil.create_slave_window" - bl_label = "Create Slave Window" - bl_description = "Create a Slave Main Window and enable Synchronization" +class STORYPENCIL_OT_AddSecondaryWindowOperator(Operator): + bl_idname = "storypencil.create_secondary_window" + bl_label = "Create Secondary Window" + bl_description = "Create a Secondary Main Window and enable Synchronization" bl_options = {'INTERNAL'} def execute(self, context): @@ -133,16 +133,16 @@ class STORYPENCIL_OT_AddSlaveWindowOperator(Operator): # get newly created window by comparing to previous list new_window = (set(context.window_manager.windows[:]) - windows).pop() # activate sync system and enable sync for this window - toggle_slave_window(context.window_manager, window_id(new_window)) + toggle_secondary_window(context.window_manager, window_id(new_window)) context.window_manager.storypencil_settings.active = True # trigger initial synchronization to open the current Sequence's Scene on_frame_changed() # Configure the new window - self.configure_new_slave_window(context, new_window) + self.configure_new_secondary_window(context, new_window) return {'FINISHED'} - def configure_new_slave_window(self, context, new_window): + def configure_new_secondary_window(self, context, new_window): wrk_name = context.scene.storypencil_edit_workspace.name override_context = context.copy() override_context["window"] = new_window @@ -183,7 +183,7 @@ class STORYPENCIL_OT_WindowBringFront(Operator): class STORYPENCIL_OT_WindowCloseOperator(Operator): - bl_idname = "storypencil.close_slave_window" + bl_idname = "storypencil.close_secondary_window" bl_label = "Close Window" bl_description = "Close a specific Window" bl_options = {'INTERNAL'} @@ -212,63 +212,63 @@ def validate_sync(window_manager: WindowManager) -> bool: return window_manager.storypencil_settings.active -def get_slave_window_indices(wm: WindowManager) -> List[str]: - """Get slave Windows indices as a list of IDs +def get_secondary_window_indices(wm: WindowManager) -> List[str]: + """Get secondary Windows indices as a list of IDs :param wm: the WindowManager to consider - :return: the list of slave Windows IDs + :return: the list of secondary Windows IDs """ - return split_win_ids(wm.storypencil_settings.slave_windows_ids) + return split_win_ids(wm.storypencil_settings.secondary_windows_ids) -def is_slave_window(window_manager: WindowManager, win_id: str) -> bool: - """Return wether the Window identified by 'win_id' is a slave window. +def is_secondary_window(window_manager: WindowManager, win_id: str) -> bool: + """Return wether the Window identified by 'win_id' is a secondary window. - :return: whether this Window is a sync slave + :return: whether this Window is a sync secondary """ - return win_id in get_slave_window_indices(window_manager) + return win_id in get_secondary_window_indices(window_manager) -def enable_slave_window(wm: WindowManager, win_id: str): - """Enable the slave status of a Window. +def enable_secondary_window(wm: WindowManager, win_id: str): + """Enable the secondary status of a Window. :param wm: the WindowManager instance :param win_id: the id of the window """ - slave_indices = get_slave_window_indices(wm) + secondary_indices = get_secondary_window_indices(wm) win_id_str = win_id # Delete old indice if exist - if win_id_str in slave_indices: - slave_indices.remove(win_id_str) + if win_id_str in secondary_indices: + secondary_indices.remove(win_id_str) # Add indice - slave_indices.append(win_id_str) + secondary_indices.append(win_id_str) - # rebuild the whole list of valid slave windows - slave_indices = [ - idx for idx in slave_indices if get_window_from_id(wm, idx)] + # rebuild the whole list of valid secondary windows + secondary_indices = [ + idx for idx in secondary_indices if get_window_from_id(wm, idx)] - wm.storypencil_settings.slave_windows_ids = join_win_ids(slave_indices) + wm.storypencil_settings.secondary_windows_ids = join_win_ids(secondary_indices) -def toggle_slave_window(wm: WindowManager, win_id: str): - """Toggle the slave status of a Window. +def toggle_secondary_window(wm: WindowManager, win_id: str): + """Toggle the secondary status of a Window. :param wm: the WindowManager instance :param win_id: the id of the window """ - slave_indices = get_slave_window_indices(wm) + secondary_indices = get_secondary_window_indices(wm) win_id_str = win_id - if win_id_str in slave_indices: - slave_indices.remove(win_id_str) + if win_id_str in secondary_indices: + secondary_indices.remove(win_id_str) else: - slave_indices.append(win_id_str) + secondary_indices.append(win_id_str) - # rebuild the whole list of valid slave windows - slave_indices = [ - idx for idx in slave_indices if get_window_from_id(wm, idx)] + # rebuild the whole list of valid secondary windows + secondary_indices = [ + idx for idx in secondary_indices if get_window_from_id(wm, idx)] - wm.storypencil_settings.slave_windows_ids = join_win_ids(slave_indices) + wm.storypencil_settings.secondary_windows_ids = join_win_ids(secondary_indices) def get_main_window(wm: WindowManager) -> Window: @@ -280,15 +280,15 @@ def get_main_window(wm: WindowManager) -> Window: return get_window_from_id(wm=wm, win_id=wm.storypencil_settings.main_window_id) -def get_slave_window(wm: WindowManager) -> Window: - """Get the first slave Window +def get_secondary_window(wm: WindowManager) -> Window: + """Get the first secondary Window :param wm: the WindowManager instance :returns: the Window or None """ for w in wm.windows: win_id = window_id(w) - if is_slave_window(wm, win_id): + if is_secondary_window(wm, win_id): return w return None @@ -321,9 +321,9 @@ def get_main_strip(wm: WindowManager) -> SceneSequence: return seq_editor.sequences.get(wm.storypencil_settings.main_strip_name, None) -class STORYPENCIL_OT_SyncToggleSlave(Operator): - bl_idname = "storypencil.sync_toggle_slave" - bl_label = "Toggle Slave Window Status" +class STORYPENCIL_OT_SyncToggleSecondary(Operator): + bl_idname = "storypencil.sync_toggle_secondary" + bl_label = "Toggle Secondary Window Status" bl_description = "Enable/Disable synchronization for a specific Window" bl_options = {'INTERNAL'} @@ -331,7 +331,7 @@ class STORYPENCIL_OT_SyncToggleSlave(Operator): def execute(self, context): wm = context.window_manager - toggle_slave_window(wm, self.win_id) + toggle_secondary_window(wm, self.win_id) return {'FINISHED'} @@ -421,7 +421,7 @@ def setup_window_from_scene_strip(window: Window, strip: SceneSequence): @persistent def on_frame_changed(*args): """ - React to current frame changes and synchronize slave windows. + React to current frame changes and synchronize secondary windows. """ # ensure context is fully initialized, i.e not '_RestrictData if not isinstance(bpy.context, Context): @@ -462,17 +462,17 @@ def update_sync(context: Context, win_id=None): if not sequences: return - # bidirectionnal sync: change main time from slave window + # bidirectionnal sync: change main time from secondary window if ( win_id != wm.storypencil_settings.main_window_id - and is_slave_window(wm, win_id) + and is_secondary_window(wm, win_id) ): # get strip under time cursor in main window strip, old_frame = get_sequence_at_frame( main_scene.frame_current, sequences=sequences ) - # only do bidirectional sync if slave window matches the strip at current time in main + # only do bidirectional sync if secondary window matches the strip at current time in main if not isinstance(strip, SceneSequence) or strip.scene != context.scene: return @@ -506,15 +506,15 @@ def update_sync(context: Context, win_id=None): if win_id != wm.storypencil_settings.main_window_id: return - slave_windows = [ + secondary_windows = [ get_window_from_id(wm, win_id) for win_id - in get_slave_window_indices(wm) + in get_secondary_window_indices(wm) if win_id and win_id != wm.storypencil_settings.main_window_id ] # only work with at least 2 windows - if not slave_windows: + if not secondary_windows: return seq, frame = get_sequence_at_frame(main_scene.frame_current, sequences) @@ -525,13 +525,13 @@ def update_sync(context: Context, win_id=None): return wm.storypencil_settings.main_strip_name = seq.name - # change the scene on slave windows + # change the scene on secondary windows # warning: only one window's scene can be changed in this event loop, # otherwise it may crashes Blender randomly - for idx, win in enumerate(slave_windows): + for idx, win in enumerate(secondary_windows): if not win: continue - # change first slave window immediately + # change first secondary window immediately if idx == 0: setup_window_from_scene_strip(win, seq) else: @@ -545,7 +545,7 @@ def update_sync(context: Context, win_id=None): def sync_all_windows(wm: WindowManager): """Enable synchronization on all main windows held by `wm`.""" - wm.storypencil_settings.slave_windows_ids = join_win_ids([ + wm.storypencil_settings.secondary_windows_ids = join_win_ids([ window_id(w) for w in get_main_windows_list(wm) @@ -593,7 +593,7 @@ def sync_active_update(self, context): ): self.main_window_id = window_id(context.window) # automatically sync all other windows if nothing was previously set - if not self.slave_windows_ids: + if not self.secondary_windows_ids: sync_all_windows(context.window_manager) on_frame_changed() @@ -663,9 +663,9 @@ class STORYPENCIL_PG_Settings(PropertyGroup): default="", ) - slave_windows_ids: StringProperty( - name="Slave Windows", - description="Serialized Slave Window Indices", + secondary_windows_ids: StringProperty( + name="Secondary Windows", + description="Serialized Secondary Window Indices", default="", ) @@ -682,8 +682,8 @@ class STORYPENCIL_PG_Settings(PropertyGroup): ) show_main_strip_range: BoolProperty( - name="Show Main Strip Range in Slave Windows", - description="Draw main Strip's in/out markers in synchronized slave Windows", + name="Show Main Strip Range in Secondary Windows", + description="Draw main Strip's in/out markers in synchronized secondary Windows", default=True, ) -- 2.30.2 From d116731b99378a1e37ed6bce1c06f6cf6bac91b2 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Fri, 25 Nov 2022 16:40:28 +0100 Subject: [PATCH 23/33] Storypencil: Add new shotcut to switch After receiving some feedback, we have added this shortcut: In VSE window: `Tab` to switch to Edit mode --- storypencil/__init__.py | 24 +++++++++++++++++++++++- storypencil/synchro.py | 15 +++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/storypencil/__init__.py b/storypencil/__init__.py index b8f4536..1abd026 100644 --- a/storypencil/__init__.py +++ b/storypencil/__init__.py @@ -7,7 +7,7 @@ bl_info = { "name": "Storypencil - Storyboard Tools", "description": "Storyboard tools", "author": "Antonio Vazquez, Matias Mendiola, Daniel Martinez Lara, Rodrigo Blaas", - "version": (1, 1, 0), + "version": (1, 1, 1), "blender": (3, 3, 0), "location": "", "warning": "", @@ -61,6 +61,7 @@ classes = ( synchro.STORYPENCIL_OT_SetSyncMainOperator, synchro.STORYPENCIL_OT_AddSecondaryWindowOperator, synchro.STORYPENCIL_OT_Switch, + synchro.STORYPENCIL_OT_TabSwitch, render.STORYPENCIL_OT_RenderAction, ui.STORYPENCIL_PT_Settings, ui.STORYPENCIL_PT_SettingsNew, @@ -86,10 +87,29 @@ def save_mode(self, context): bpy.ops.wm.window_close(c) +addon_keymaps = [] +def register_keymaps(): + addon = bpy.context.window_manager.keyconfigs.addon + km = addon.keymaps.new(name="Sequencer", space_type="SEQUENCE_EDITOR") + kmi = km.keymap_items.new( + idname="storypencil.tabswitch", + type="TAB", + value="PRESS", + shift=False, ctrl=False, alt = False, oskey=False, + ) + + addon_keymaps.append((km, kmi)) + +def unregister_keymaps(): + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + addon_keymaps.clear() + def register(): from bpy.utils import register_class for cls in classes: register_class(cls) + register_keymaps() Scene.storypencil_scene_duration = IntProperty( name="Scene Duration", @@ -178,6 +198,8 @@ def register(): def unregister(): + unregister_keymaps() + from bpy.utils import unregister_class for cls in reversed(classes): unregister_class(cls) diff --git a/storypencil/synchro.py b/storypencil/synchro.py index ecdf4cd..a089411 100644 --- a/storypencil/synchro.py +++ b/storypencil/synchro.py @@ -781,3 +781,18 @@ class STORYPENCIL_OT_Switch(Operator): area.spaces.active.region_3d.view_perspective = 'CAMERA' return {"FINISHED"} + + +class STORYPENCIL_OT_TabSwitch(Operator): + bl_idname = "storypencil.tabswitch" + bl_label = "Switch using tab key" + bl_description = "Wrapper used to handle the Tab key to switch" + bl_options = {'INTERNAL'} + + def execute(self, context): + if context.scene.storypencil_use_new_window: + bpy.ops.storypencil.sync_set_main('INVOKE_DEFAULT', True) + else: + bpy.ops.storypencil.switch('INVOKE_DEFAULT', True) + + return {'FINISHED'} -- 2.30.2 From 96143b1a8b037ea3c81f065f557025db9fe1ace3 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Fri, 25 Nov 2022 17:04:42 +0100 Subject: [PATCH 24/33] Storypencil: Remove deprecated context function --- storypencil/synchro.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/storypencil/synchro.py b/storypencil/synchro.py index a089411..94ebe02 100644 --- a/storypencil/synchro.py +++ b/storypencil/synchro.py @@ -144,8 +144,6 @@ class STORYPENCIL_OT_AddSecondaryWindowOperator(Operator): def configure_new_secondary_window(self, context, new_window): wrk_name = context.scene.storypencil_edit_workspace.name - override_context = context.copy() - override_context["window"] = new_window # Open the 2D workspace blendpath = os.path.dirname(bpy.app.binary_path) version = bpy.app.version @@ -159,8 +157,8 @@ class STORYPENCIL_OT_AddSecondaryWindowOperator(Operator): if wk.name == wrk_name: new_window.workspace = wk return - bpy.ops.workspace.append_activate( - override_context, idname=wk_name, filepath=template_path) + with context.temp_override(window=new_window): + bpy.ops.workspace.append_activate(context, idname=wk_name, filepath=template_path) class STORYPENCIL_OT_WindowBringFront(Operator): @@ -172,13 +170,12 @@ class STORYPENCIL_OT_WindowBringFront(Operator): win_id: bpy.props.StringProperty() def execute(self, context): - c = context.copy() win = get_window_from_id(context.window_manager, self.win_id) if not win: return {'CANCELLED'} - c["window"] = win - bpy.ops.wm.window_fullscreen_toggle(c) - bpy.ops.wm.window_fullscreen_toggle(c) + with context.temp_override(window=win): + bpy.ops.wm.window_fullscreen_toggle() + bpy.ops.wm.window_fullscreen_toggle() return {'FINISHED'} @@ -191,12 +188,11 @@ class STORYPENCIL_OT_WindowCloseOperator(Operator): win_id: bpy.props.StringProperty() def execute(self, context): - c = context.copy() win = get_window_from_id(context.window_manager, self.win_id) if not win: return {'CANCELLED'} - c["window"] = win - bpy.ops.wm.window_close(c) + with context.temp_override(window=win): + bpy.ops.wm.window_close() return {'FINISHED'} -- 2.30.2 From bdcfdd47ec3451822b21d1cff2ea2db751093c9a Mon Sep 17 00:00:00 2001 From: Thomas Dinges Date: Fri, 9 Dec 2022 10:00:52 +0100 Subject: [PATCH 25/33] Addons: Remove storypencil from contrib, this has been moved to official. --- storypencil/__init__.py | 239 ---------- storypencil/dopesheet_overlay.py | 179 ------- storypencil/render.py | 281 ----------- storypencil/scene_tools.py | 173 ------- storypencil/synchro.py | 794 ------------------------------- storypencil/ui.py | 211 -------- storypencil/utils.py | 110 ----- 7 files changed, 1987 deletions(-) delete mode 100644 storypencil/__init__.py delete mode 100644 storypencil/dopesheet_overlay.py delete mode 100644 storypencil/render.py delete mode 100644 storypencil/scene_tools.py delete mode 100644 storypencil/synchro.py delete mode 100644 storypencil/ui.py delete mode 100644 storypencil/utils.py diff --git a/storypencil/__init__.py b/storypencil/__init__.py deleted file mode 100644 index 1abd026..0000000 --- a/storypencil/__init__.py +++ /dev/null @@ -1,239 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -# ---------------------------------------------- -# Define Addon info -# ---------------------------------------------- -bl_info = { - "name": "Storypencil - Storyboard Tools", - "description": "Storyboard tools", - "author": "Antonio Vazquez, Matias Mendiola, Daniel Martinez Lara, Rodrigo Blaas", - "version": (1, 1, 1), - "blender": (3, 3, 0), - "location": "", - "warning": "", - "category": "Sequencer", -} - -# ---------------------------------------------- -# Import modules -# ---------------------------------------------- -if "bpy" in locals(): - import importlib - - importlib.reload(utils) - importlib.reload(synchro) - importlib.reload(dopesheet_overlay) - importlib.reload(scene_tools) - importlib.reload(render) - importlib.reload(ui) -else: - from . import utils - from . import synchro - from . import dopesheet_overlay - from . import scene_tools - from . import render - from . import ui - -import bpy -from bpy.types import ( - Scene, - WindowManager, - WorkSpace, -) -from bpy.props import ( - BoolProperty, - IntProperty, - PointerProperty, - StringProperty, - EnumProperty, -) - -# -------------------------------------------------------------- -# Register all operators, props and panels -# -------------------------------------------------------------- -classes = ( - synchro.STORYPENCIL_PG_Settings, - scene_tools.STORYPENCIL_OT_Setup, - scene_tools.STORYPENCIL_OT_NewScene, - synchro.STORYPENCIL_OT_WindowBringFront, - synchro.STORYPENCIL_OT_WindowCloseOperator, - synchro.STORYPENCIL_OT_SyncToggleSecondary, - synchro.STORYPENCIL_OT_SetSyncMainOperator, - synchro.STORYPENCIL_OT_AddSecondaryWindowOperator, - synchro.STORYPENCIL_OT_Switch, - synchro.STORYPENCIL_OT_TabSwitch, - render.STORYPENCIL_OT_RenderAction, - ui.STORYPENCIL_PT_Settings, - ui.STORYPENCIL_PT_SettingsNew, - ui.STORYPENCIL_PT_RenderPanel, - ui.STORYPENCIL_PT_General, - ui.STORYPENCIL_MT_extra_options, -) - - -def save_mode(self, context): - wm = context.window_manager - wm['storypencil_use_new_window'] = context.scene.storypencil_use_new_window - # Close all secondary windows - if context.scene.storypencil_use_new_window is False: - c = context.copy() - for win in context.window_manager.windows: - # Don't close actual window - if win == context.window: - continue - win_id = str(win.as_pointer()) - if win_id != wm.storypencil_settings.main_window_id and win.parent is None: - c["window"] = win - bpy.ops.wm.window_close(c) - - -addon_keymaps = [] -def register_keymaps(): - addon = bpy.context.window_manager.keyconfigs.addon - km = addon.keymaps.new(name="Sequencer", space_type="SEQUENCE_EDITOR") - kmi = km.keymap_items.new( - idname="storypencil.tabswitch", - type="TAB", - value="PRESS", - shift=False, ctrl=False, alt = False, oskey=False, - ) - - addon_keymaps.append((km, kmi)) - -def unregister_keymaps(): - for km, kmi in addon_keymaps: - km.keymap_items.remove(kmi) - addon_keymaps.clear() - -def register(): - from bpy.utils import register_class - for cls in classes: - register_class(cls) - register_keymaps() - - Scene.storypencil_scene_duration = IntProperty( - name="Scene Duration", - description="Default Duration for new Scene", - default=48, - min=1, - soft_max=250, - ) - - Scene.storypencil_use_new_window = BoolProperty(name="Open in new window", - description="Use secondary main window to edit scenes", - default=False, - update=save_mode) - - Scene.storypencil_main_workspace = PointerProperty(type=WorkSpace, - description="Main Workspace used for editing Storyboard") - Scene.storypencil_main_scene = PointerProperty(type=Scene, - description="Main Scene used for editing Storyboard") - Scene.storypencil_edit_workspace = PointerProperty(type=WorkSpace, - description="Workspace used for changing drawings") - - Scene.storypencil_base_scene = PointerProperty(type=Scene, - description="Template Scene used for creating new scenes") - - Scene.storypencil_render_render_path = StringProperty(name="Output Path", subtype='FILE_PATH', maxlen=256, - description="Directory/name to save files") - - Scene.storypencil_name_prefix = StringProperty(name="Scene Name Prefix", maxlen=20, default="") - - Scene.storypencil_name_suffix = StringProperty(name="Scene Name Suffix", maxlen=20, default="") - - Scene.storypencil_render_onlyselected = BoolProperty(name="Render only Selected Strips", - description="Render only the selected strips", - default=True) - - Scene.storypencil_render_channel = IntProperty(name="Channel", - description="Channel to set the new rendered video", - default=5, min=1, max=128) - - Scene.storypencil_add_render_strip = BoolProperty(name="Import Rendered Strips", - description="Add a Strip with the render", - default=True) - - Scene.storypencil_render_step = IntProperty(name="Image Steps", - description="Minimum frames number to generate images between keyframes (0 to disable)", - default=0, min=0, max=128) - - Scene.storypencil_render_numbering = EnumProperty(name="Image Numbering", - items=( - ('1', "Frame", "Use real frame number"), - ('2', "Consecutive", "Use sequential numbering"), - ), - description="Defines how frame is named") - - Scene.storypencil_add_render_byfolder = BoolProperty(name="Folder by Strip", - description="Create a separated folder for each strip", - default=True) - - WindowManager.storypencil_settings = PointerProperty( - type=synchro.STORYPENCIL_PG_Settings, - name="Storypencil settings", - description="Storypencil tool settings", - ) - - # Append Handlers - bpy.app.handlers.frame_change_post.clear() - bpy.app.handlers.frame_change_post.append(synchro.on_frame_changed) - bpy.app.handlers.load_post.append(synchro.sync_autoconfig) - - bpy.context.window_manager.storypencil_settings.active = False - bpy.context.window_manager.storypencil_settings.main_window_id = "" - bpy.context.window_manager.storypencil_settings.secondary_windows_ids = "" - - # UI integration in dopesheet header - bpy.types.DOPESHEET_HT_header.append(synchro.draw_sync_header) - dopesheet_overlay.register() - - synchro.sync_autoconfig() - - # UI integration in VSE header - bpy.types.SEQUENCER_HT_header.remove(synchro.draw_sync_sequencer_header) - bpy.types.SEQUENCER_HT_header.append(synchro.draw_sync_sequencer_header) - - bpy.types.SEQUENCER_MT_add.append(scene_tools.draw_new_scene) - bpy.types.VIEW3D_MT_draw_gpencil.append(scene_tools.setup_storyboard) - - -def unregister(): - unregister_keymaps() - - from bpy.utils import unregister_class - for cls in reversed(classes): - unregister_class(cls) - - # Remove Handlers - if bpy.app.handlers.frame_change_post: - bpy.app.handlers.frame_change_post.remove(synchro.on_frame_changed) - bpy.app.handlers.load_post.remove(synchro.sync_autoconfig) - - # remove UI integration - bpy.types.DOPESHEET_HT_header.remove(synchro.draw_sync_header) - dopesheet_overlay.unregister() - bpy.types.SEQUENCER_HT_header.remove(synchro.draw_sync_sequencer_header) - - bpy.types.SEQUENCER_MT_add.remove(scene_tools.draw_new_scene) - bpy.types.VIEW3D_MT_draw_gpencil.remove(scene_tools.setup_storyboard) - - del Scene.storypencil_scene_duration - del WindowManager.storypencil_settings - - del Scene.storypencil_base_scene - del Scene.storypencil_main_workspace - del Scene.storypencil_main_scene - del Scene.storypencil_edit_workspace - - del Scene.storypencil_render_render_path - del Scene.storypencil_name_prefix - del Scene.storypencil_name_suffix - del Scene.storypencil_render_onlyselected - del Scene.storypencil_render_channel - del Scene.storypencil_render_step - del Scene.storypencil_add_render_strip - del Scene.storypencil_render_numbering - del Scene.storypencil_add_render_byfolder - -if __name__ == '__main__': - register() diff --git a/storypencil/dopesheet_overlay.py b/storypencil/dopesheet_overlay.py deleted file mode 100644 index 2c00d8e..0000000 --- a/storypencil/dopesheet_overlay.py +++ /dev/null @@ -1,179 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -import typing - -import bpy -import gpu -from gpu_extras.batch import batch_for_shader - -from .utils import (redraw_all_areas_by_type) -from .synchro import (is_secondary_window, window_id, get_main_strip) - -Int3 = typing.Tuple[int, int, int] - -Float2 = typing.Tuple[float, float] -Float3 = typing.Tuple[float, float, float] -Float4 = typing.Tuple[float, float, float, float] - - -class LineDrawer: - def __init__(self): - self._format = gpu.types.GPUVertFormat() - self._pos_id = self._format.attr_add( - id="pos", comp_type="F32", len=2, fetch_mode="FLOAT" - ) - self._color_id = self._format.attr_add( - id="color", comp_type="F32", len=4, fetch_mode="FLOAT" - ) - - self.shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') - - def draw( - self, - coords: typing.List[Float2], - indices: typing.List[Int3], - color: Float4, - ): - if not coords: - return - - gpu.state.blend_set('ALPHA') - - self.shader.uniform_float("color", color) - - batch = batch_for_shader(self.shader, 'TRIS', {"pos": coords}, indices=indices) - batch.program_set(self.shader) - batch.draw() - - gpu.state.blend_set('NONE') - - -def get_scene_strip_in_out(strip): - """ Return the in and out keyframe of the given strip in the scene time reference""" - shot_in = strip.scene.frame_start + strip.frame_offset_start - shot_out = shot_in + strip.frame_final_duration - 1 - return (shot_in, shot_out) - - -def draw_callback_px(line_drawer: LineDrawer): - context = bpy.context - region = context.region - - wm = context.window_manager - - if ( - not wm.storypencil_settings.active - or not wm.storypencil_settings.show_main_strip_range - or not is_secondary_window(wm, window_id(context.window)) - ): - return - - # get main strip driving the sync - strip = get_main_strip(wm) - - if not strip or strip.scene != context.scene: - return - - xwin1, ywin1 = region.view2d.region_to_view(0, 0) - one_pixel_further_x = region.view2d.region_to_view(1, 1)[0] - pixel_size_x = one_pixel_further_x - xwin1 - rect_width = 1 - - shot_in, shot_out = get_scene_strip_in_out(strip) - key_coords_in = [ - ( - shot_in - rect_width * pixel_size_x, - ywin1, - ), - ( - shot_in + rect_width * pixel_size_x, - ywin1, - ), - ( - shot_in + rect_width * pixel_size_x, - ywin1 + context.region.height, - ), - ( - shot_in - rect_width * pixel_size_x, - ywin1 + context.region.height, - ), - ] - - key_coords_out = [ - ( - shot_out - rect_width * pixel_size_x, - ywin1, - ), - ( - shot_out + rect_width * pixel_size_x, - ywin1, - ), - ( - shot_out + rect_width * pixel_size_x, - ywin1 + context.region.height, - ), - ( - shot_out - rect_width * pixel_size_x, - ywin1 + context.region.height, - ), - ] - - indices = [(0, 1, 2), (2, 0, 3)] - # Draw the IN frame in green - # hack: in certain cases, opengl draw state is invalid for the first drawn item - # resulting in a non-colored line - # => draw it a first time with a null alpha, so that the second one is drawn correctly - line_drawer.draw(key_coords_in, indices, (0, 0, 0, 0)) - line_drawer.draw(key_coords_in, indices, (0.3, 0.99, 0.4, 0.5)) - # Draw the OUT frame un red - line_drawer.draw(key_coords_out, indices, (0.99, 0.3, 0.4, 0.5)) - - -def tag_redraw_all_dopesheets(): - redraw_all_areas_by_type(bpy.context, 'DOPESHEET') - - -# This is a list so it can be changed instead of set -# if it is only changed, it does not have to be declared as a global everywhere -cb_handle = [] - - -def callback_enable(): - if cb_handle: - return - - # Doing GPU stuff in the background crashes Blender, so let's not. - if bpy.app.background: - return - - line_drawer = LineDrawer() - # POST_VIEW allow to work in time coordinate (1 unit = 1 frame) - cb_handle[:] = ( - bpy.types.SpaceDopeSheetEditor.draw_handler_add( - draw_callback_px, (line_drawer,), 'WINDOW', 'POST_VIEW' - ), - ) - - tag_redraw_all_dopesheets() - - -def callback_disable(): - if not cb_handle: - return - - try: - bpy.types.SpaceDopeSheetEditor.draw_handler_remove(cb_handle[0], 'WINDOW') - except ValueError: - # Thrown when already removed. - pass - cb_handle.clear() - - tag_redraw_all_dopesheets() - - -def register(): - callback_enable() - - -def unregister(): - callback_disable() diff --git a/storypencil/render.py b/storypencil/render.py deleted file mode 100644 index 6f4a136..0000000 --- a/storypencil/render.py +++ /dev/null @@ -1,281 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -import bpy -import os -import shutil -import sys - -from datetime import datetime -from bpy.types import Operator -from .utils import get_keyframe_list - -# ------------------------------------------------------ -# Button: Render VSE -# ------------------------------------------------------ - - -class STORYPENCIL_OT_RenderAction(Operator): - bl_idname = "storypencil.render_vse" - bl_label = "Render Strips" - bl_description = "Render VSE strips" - - # Extension by FFMPEG container type - video_ext = { - "MPEG1": ".mpg", - "MPEG2": ".dvd", - "MPEG4": ".mp4", - "AVI": ".avi", - "QUICKTIME": ".mov", - "DV": ".dv", - "OGG": ".ogv", - "MKV": ".mkv", - "FLASH": ".flv", - "WEBM": ".webm" - } - # Extension by image format - image_ext = { - "BMP": ".bmp", - "IRIS": ".rgb", - "PNG": ".png", - "JPEG": ".jpg", - "JPEG2000": ".jp2", - "TARGA": ".tga", - "TARGA_RAW": ".tga", - "CINEON": ".cin", - "DPX": ".dpx", - "OPEN_EXR_MULTILAYER": ".exr", - "OPEN_EXR": ".exr", - "HDR": ".hdr", - "TIFF": ".tif", - "WEBP": ".webp" - } - - # -------------------------------------------------------------------- - # Format an int adding 4 zero padding - # -------------------------------------------------------------------- - def format_to4(self, value): - return f"{value:04}" - - # -------------------------------------------------------------------- - # Add frames every N frames - # -------------------------------------------------------------------- - def add_missing_frames(self, sq, step, keyframe_list): - missing = [] - lk = len(keyframe_list) - if lk == 0: - return - - # Add mid frames - if step > 0: - for i in range(0, lk - 1): - dist = keyframe_list[i + 1] - keyframe_list[i] - if dist > step: - delta = int(dist / step) - e = 1 - for x in range(1, delta): - missing.append(keyframe_list[i] + (step * e)) - e += 1 - - keyframe_list.extend(missing) - keyframe_list.sort() - - # ------------------------------ - # Execute - # ------------------------------ - def execute(self, context): - scene = bpy.context.scene - image_settings = scene.render.image_settings - is_video_output = image_settings.file_format in { - 'FFMPEG', 'AVI_JPEG', 'AVI_RAW'} - step = scene.storypencil_render_step - - sequences = scene.sequence_editor.sequences_all - prv_start = scene.frame_start - prv_end = scene.frame_end - prv_frame = bpy.context.scene.frame_current - - prv_path = scene.render.filepath - prv_format = image_settings.file_format - prv_use_file_extension = scene.render.use_file_extension - prv_ffmpeg_format = scene.render.ffmpeg.format - rootpath = scene.storypencil_render_render_path - only_selected = scene.storypencil_render_onlyselected - channel = scene.storypencil_render_channel - - context.window.cursor_set('WAIT') - - # Create list of selected strips because the selection is changed when adding new strips - Strips = [] - for sq in sequences: - if sq.type == 'SCENE': - if only_selected is False or sq.select is True: - Strips.append(sq) - - # Sort strips - Strips = sorted(Strips, key=lambda strip: strip.frame_start) - - # For video, clear BL_proxy folder because sometimes the video - # is not rendered as expected if this folder has data. - # This ensure the output video is correct. - if is_video_output: - proxy_folder = os.path.join(rootpath, "BL_proxy") - if os.path.exists(proxy_folder): - for filename in os.listdir(proxy_folder): - file_path = os.path.join(proxy_folder, filename) - try: - if os.path.isfile(file_path) or os.path.islink(file_path): - os.unlink(file_path) - elif os.path.isdir(file_path): - shutil.rmtree(file_path) - except Exception as e: - print('Failed to delete %s. Reason: %s' % - (file_path, e)) - - try: - Videos = [] - Sheets = [] - # Read all strips and render the output - for sq in Strips: - strip_name = sq.name - strip_scene = sq.scene - scene.frame_start = int(sq.frame_start + sq.frame_offset_start) - scene.frame_end = int(scene.frame_start + sq.frame_final_duration - 1) # Image - if is_video_output is False: - # Get list of any keyframe - strip_start = sq.frame_offset_start - if strip_start < strip_scene.frame_start: - strip_start = strip_scene.frame_start - - strip_end = strip_start + sq.frame_final_duration - 1 - keyframe_list = get_keyframe_list( - strip_scene, strip_start, strip_end) - self.add_missing_frames(sq, step, keyframe_list) - - scene.render.use_file_extension = True - foldername = strip_name - if scene.storypencil_add_render_byfolder is True: - root_folder = os.path.join(rootpath, foldername) - else: - root_folder = rootpath - - frame_nrr = 0 - print("Render:" + strip_name + "/" + strip_scene.name) - print("Image From:", strip_start, "To", strip_end) - for key in range(int(strip_start), int(strip_end) + 1): - if key not in keyframe_list: - continue - - keyframe = key + sq.frame_start - if scene.use_preview_range: - if keyframe < scene.frame_preview_start: - continue - if keyframe > scene.frame_preview_end: - break - else: - if keyframe < scene.frame_start: - continue - if keyframe > scene.frame_end: - break - # For frame name use only the number - if scene.storypencil_render_numbering == '1': - # Real - framename = strip_name + '.' + self.format_to4(key) - else: - # Consecutive - frame_nrr += 1 - framename = strip_name + '.' + \ - self.format_to4(frame_nrr) - - filepath = os.path.join(root_folder, framename) - - sheet = os.path.realpath(filepath) - sheet = bpy.path.ensure_ext( - sheet, self.image_ext[image_settings.file_format]) - Sheets.append([sheet, keyframe]) - - scene.render.filepath = filepath - - # Render Frame - scene.frame_set(int(keyframe - 1.0), subframe=0.0) - bpy.ops.render.render( - animation=False, write_still=True) - - # Add strip with the corresponding length - if scene.storypencil_add_render_strip: - frame_start = sq.frame_start + key - 1 - index = keyframe_list.index(key) - if index < len(keyframe_list) - 1: - key_next = keyframe_list[index + 1] - frame_end = frame_start + (key_next - key) - else: - frame_end = scene.frame_end + 1 - - if index == 0 and frame_start > scene.frame_start: - frame_start = scene.frame_start - - if frame_end < frame_start: - frame_end = frame_start - image_ext = self.image_ext[image_settings.file_format] - bpy.ops.sequencer.image_strip_add(directory=root_folder, - files=[ - {"name": framename + image_ext}], - frame_start=int(frame_start), - frame_end=int(frame_end), - channel=channel) - else: - print("Render:" + strip_name + "/" + strip_scene.name) - print("Video From:", scene.frame_start, - "To", scene.frame_end) - # Video - filepath = os.path.join(rootpath, strip_name) - - if image_settings.file_format == 'FFMPEG': - ext = self.video_ext[scene.render.ffmpeg.format] - else: - ext = '.avi' - - if not filepath.endswith(ext): - filepath += ext - - scene.render.use_file_extension = False - scene.render.filepath = filepath - - # Render Animation - bpy.ops.render.render(animation=True) - - # Add video to add strip later - if scene.storypencil_add_render_strip: - Videos.append( - [filepath, sq.frame_start + sq.frame_offset_start]) - - # Add pending video Strips - for vid in Videos: - bpy.ops.sequencer.movie_strip_add(filepath=vid[0], - frame_start=int(vid[1]), - channel=channel) - - scene.frame_start = prv_start - scene.frame_end = prv_end - scene.render.use_file_extension = prv_use_file_extension - image_settings.file_format = prv_format - scene.render.ffmpeg.format = prv_ffmpeg_format - - scene.render.filepath = prv_path - scene.frame_set(int(prv_frame)) - - context.window.cursor_set('DEFAULT') - - return {'FINISHED'} - - except: - print("Unexpected error:" + str(sys.exc_info())) - self.report({'ERROR'}, "Unable to render") - scene.frame_start = prv_start - scene.frame_end = prv_end - scene.render.use_file_extension = prv_use_file_extension - image_settings.file_format = prv_format - - scene.render.filepath = prv_path - scene.frame_set(int(prv_frame)) - context.window.cursor_set('DEFAULT') - return {'FINISHED'} diff --git a/storypencil/scene_tools.py b/storypencil/scene_tools.py deleted file mode 100644 index d3bf037..0000000 --- a/storypencil/scene_tools.py +++ /dev/null @@ -1,173 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -import bpy -import os - -from bpy.types import ( - Operator, -) - - -# ------------------------------------------------------------- -# Add a new scene and set to new strip -# -# ------------------------------------------------------------- -class STORYPENCIL_OT_NewScene(Operator): - bl_idname = "storypencil.new_scene" - bl_label = "New Scene" - bl_description = "Create a new scene base on template scene" - bl_options = {'REGISTER', 'UNDO'} - - scene_name: bpy.props.StringProperty(default="Scene") - - # ------------------------------ - # Poll - # ------------------------------ - @classmethod - def poll(cls, context): - scene = context.scene - scene_base = scene.storypencil_base_scene - if scene_base is not None and scene_base.name in bpy.data.scenes: - return True - - return False - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - layout = self.layout - col = layout.column() - col.prop(self, "scene_name", text="Scene Name") - - def format_to3(self, value): - return f"{value:03}" - - # ------------------------------ - # Execute button action - # ------------------------------ - def execute(self, context): - scene_prv = context.scene - cfra_prv = scene_prv.frame_current - scene_base = scene_prv.storypencil_base_scene - - # Set context to base scene and duplicate - context.window.scene = scene_base - bpy.ops.scene.new(type='FULL_COPY') - scene_new = context.window.scene - new_name = scene_prv.storypencil_name_prefix + \ - self.scene_name + scene_prv.storypencil_name_suffix - id = 0 - while new_name in bpy.data.scenes: - id += 1 - new_name = scene_prv.storypencil_name_prefix + self.scene_name + \ - scene_prv.storypencil_name_suffix + '.' + self.format_to3(id) - - scene_new.name = new_name - # Set duration of new scene - scene_new.frame_end = scene_new.frame_start + \ - scene_prv.storypencil_scene_duration - 1 - - # Back to original scene - context.window.scene = scene_prv - scene_prv.frame_current = cfra_prv - bpy.ops.sequencer.scene_strip_add( - frame_start=cfra_prv, scene=scene_new.name) - - scene_new.update_tag() - scene_prv.update_tag() - - return {"FINISHED"} - - -def draw_new_scene(self, context): - """Add menu options.""" - - self.layout.operator_context = 'INVOKE_REGION_WIN' - row = self.layout.row(align=True) - row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New Template Scene") - - -def setup_storyboard(self, context): - """Add Setup menu option.""" - # For security, check if this is the default template. - is_gpencil = context.active_object and context.active_object.name == 'Stroke' - if is_gpencil and context.workspace.name in ('2D Animation', '2D Full Canvas') and context.scene.name == 'Scene': - if "Video Editing" not in bpy.data.workspaces: - row = self.layout.row(align=True) - row.separator() - row = self.layout.row(align=True) - row.operator(STORYPENCIL_OT_Setup.bl_idname, - text="Setup Storyboard Session") - - -# ------------------------------------------------------------- -# Setup all environment -# -# ------------------------------------------------------------- -class STORYPENCIL_OT_Setup(Operator): - bl_idname = "storypencil.setup" - bl_label = "Setup" - bl_description = "Configure all settings for a storyboard session" - bl_options = {'REGISTER', 'UNDO'} - - # ------------------------------ - # Poll - # ------------------------------ - @classmethod - def poll(cls, context): - return True - - def get_workspace(self, type): - for wrk in bpy.data.workspaces: - if wrk.name == type: - return wrk - - return None - - # ------------------------------ - # Execute button action - # ------------------------------ - def execute(self, context): - scene_base = context.scene - # Create Workspace - templatepath = None - if "Video Editing" not in bpy.data.workspaces: - template_path = None - for path in bpy.utils.app_template_paths(): - template_path = path - - filepath = os.path.join( - template_path, "Video_Editing", "startup.blend") - bpy.ops.workspace.append_activate( - idname="Video Editing", filepath=filepath) - # Create New scene - bpy.ops.scene.new() - scene_edit = context.scene - scene_edit.name = 'Edit' - # Rename original base scene - scene_base.name = 'Base' - # Setup Edit scene settings - scene_edit.storypencil_main_workspace = self.get_workspace( - "Video Editing") - scene_edit.storypencil_main_scene = scene_edit - scene_edit.storypencil_base_scene = scene_base - scene_edit.storypencil_edit_workspace = self.get_workspace( - "2D Animation") - - # Add a new strip (need set the area context) - context.window.scene = scene_edit - area_prv = context.area.ui_type - context.area.ui_type = 'SEQUENCE_EDITOR' - prv_frame = scene_edit.frame_current - - scene_edit.frame_current = scene_edit.frame_start - bpy.ops.storypencil.new_scene() - - context.area.ui_type = area_prv - scene_edit.frame_current = prv_frame - - scene_edit.update_tag() - bpy.ops.sequencer.reload() - - return {"FINISHED"} diff --git a/storypencil/synchro.py b/storypencil/synchro.py deleted file mode 100644 index 94ebe02..0000000 --- a/storypencil/synchro.py +++ /dev/null @@ -1,794 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -from typing import List, Sequence, Tuple - -import bpy -import functools -import os -from bpy.app.handlers import persistent - -from bpy.types import ( - Context, - MetaSequence, - Operator, - PropertyGroup, - SceneSequence, - Window, - WindowManager, -) -from bpy.props import ( - BoolProperty, - IntProperty, - StringProperty, -) -from .scene_tools import STORYPENCIL_OT_NewScene -from .render import STORYPENCIL_OT_RenderAction - -def window_id(window: Window) -> str: - """ Get Window's ID. - - :param window: the Window to consider - :return: the Window's ID - """ - return str(window.as_pointer()) - - -def get_window_from_id(wm: WindowManager, win_id: str) -> Window: - """Get a Window object from its ID (serialized ptr). - - :param wm: a WindowManager holding Windows - :param win_id: the ID of the Window to get - :return: the Window matching the given ID, None otherwise - """ - return next((w for w in wm.windows if w and window_id(w) == win_id), None) - - -def get_main_windows_list(wm: WindowManager) -> Sequence[Window]: - """Get all the Main Windows held by the given WindowManager `wm`""" - return [w for w in wm.windows if w and w.parent is None] - - -def join_win_ids(ids: List[str]) -> str: - """Join Windows IDs in a single string""" - return ";".join(ids) - - -def split_win_ids(ids: str) -> List[str]: - """Split a Windows IDs string into individual IDs""" - return ids.split(";") - - -class STORYPENCIL_OT_SetSyncMainOperator(Operator): - bl_idname = "storypencil.sync_set_main" - bl_label = "Set as Sync Main" - bl_description = "Set this Window as main for Synchronization" - bl_options = {'INTERNAL'} - - win_id: bpy.props.StringProperty( - name="Window ID", - default="", - options=set(), - description="Main window ID", - ) - - def copy_settings(self, main_window, secondary_window): - if main_window is None or secondary_window is None: - return - secondary_window.scene.storypencil_main_workspace = main_window.scene.storypencil_main_workspace - secondary_window.scene.storypencil_main_scene = main_window.scene.storypencil_main_scene - secondary_window.scene.storypencil_edit_workspace = main_window.scene.storypencil_edit_workspace - - def execute(self, context): - options = context.window_manager.storypencil_settings - options.main_window_id = self.win_id - wm = bpy.context.window_manager - scene = context.scene - wm['storypencil_use_new_window'] = scene.storypencil_use_new_window - - main_windows = get_main_windows_list(wm) - main_window = get_main_window(wm) - secondary_window = get_secondary_window(wm) - # Active sync - options.active = True - if secondary_window is None: - # Open a new window - if len(main_windows) < 2: - bpy.ops.storypencil.create_secondary_window() - secondary_window = get_secondary_window(wm) - self.copy_settings(get_main_window(wm), secondary_window) - return {'FINISHED'} - else: - # Reuse the existing window - secondary_window = get_not_main_window(wm) - else: - # Open new secondary - if len(main_windows) < 2: - bpy.ops.storypencil.create_secondary_window() - secondary_window = get_secondary_window(wm) - self.copy_settings(get_main_window(wm), secondary_window) - return {'FINISHED'} - else: - # Reuse the existing window - secondary_window = get_not_main_window(wm) - - if secondary_window: - enable_secondary_window(wm, window_id(secondary_window)) - win_id = window_id(secondary_window) - self.copy_settings(get_main_window(wm), secondary_window) - bpy.ops.storypencil.sync_window_bring_front(win_id=win_id) - - return {'FINISHED'} - - -class STORYPENCIL_OT_AddSecondaryWindowOperator(Operator): - bl_idname = "storypencil.create_secondary_window" - bl_label = "Create Secondary Window" - bl_description = "Create a Secondary Main Window and enable Synchronization" - bl_options = {'INTERNAL'} - - def execute(self, context): - # store existing windows - windows = set(context.window_manager.windows[:]) - bpy.ops.wm.window_new_main() - # get newly created window by comparing to previous list - new_window = (set(context.window_manager.windows[:]) - windows).pop() - # activate sync system and enable sync for this window - toggle_secondary_window(context.window_manager, window_id(new_window)) - context.window_manager.storypencil_settings.active = True - # trigger initial synchronization to open the current Sequence's Scene - on_frame_changed() - # Configure the new window - self.configure_new_secondary_window(context, new_window) - - return {'FINISHED'} - - def configure_new_secondary_window(self, context, new_window): - wrk_name = context.scene.storypencil_edit_workspace.name - # Open the 2D workspace - blendpath = os.path.dirname(bpy.app.binary_path) - version = bpy.app.version - version_full = str(version[0]) + '.' + str(version[1]) - template = os.path.join("scripts", "startup", - "bl_app_templates_system") - template = os.path.join(template, wrk_name, "startup.blend") - template_path = os.path.join(blendpath, version_full, template) - # Check if workspace exist and add it if missing - for wk in bpy.data.workspaces: - if wk.name == wrk_name: - new_window.workspace = wk - return - with context.temp_override(window=new_window): - bpy.ops.workspace.append_activate(context, idname=wk_name, filepath=template_path) - - -class STORYPENCIL_OT_WindowBringFront(Operator): - bl_idname = "storypencil.sync_window_bring_front" - bl_label = "Bring Window Front" - bl_description = "Bring a Window to Front" - bl_options = {'INTERNAL'} - - win_id: bpy.props.StringProperty() - - def execute(self, context): - win = get_window_from_id(context.window_manager, self.win_id) - if not win: - return {'CANCELLED'} - with context.temp_override(window=win): - bpy.ops.wm.window_fullscreen_toggle() - bpy.ops.wm.window_fullscreen_toggle() - return {'FINISHED'} - - -class STORYPENCIL_OT_WindowCloseOperator(Operator): - bl_idname = "storypencil.close_secondary_window" - bl_label = "Close Window" - bl_description = "Close a specific Window" - bl_options = {'INTERNAL'} - - win_id: bpy.props.StringProperty() - - def execute(self, context): - win = get_window_from_id(context.window_manager, self.win_id) - if not win: - return {'CANCELLED'} - with context.temp_override(window=win): - bpy.ops.wm.window_close() - return {'FINISHED'} - - -def validate_sync(window_manager: WindowManager) -> bool: - """ - Ensure synchronization system is functional, with a valid main window. - Disable it otherwise and return the system status. - """ - if not window_manager.storypencil_settings.active: - return False - if not get_window_from_id(window_manager, window_manager.storypencil_settings.main_window_id): - window_manager.storypencil_settings.active = False - return window_manager.storypencil_settings.active - - -def get_secondary_window_indices(wm: WindowManager) -> List[str]: - """Get secondary Windows indices as a list of IDs - - :param wm: the WindowManager to consider - :return: the list of secondary Windows IDs - """ - return split_win_ids(wm.storypencil_settings.secondary_windows_ids) - - -def is_secondary_window(window_manager: WindowManager, win_id: str) -> bool: - """Return wether the Window identified by 'win_id' is a secondary window. - - :return: whether this Window is a sync secondary - """ - return win_id in get_secondary_window_indices(window_manager) - - -def enable_secondary_window(wm: WindowManager, win_id: str): - """Enable the secondary status of a Window. - - :param wm: the WindowManager instance - :param win_id: the id of the window - """ - secondary_indices = get_secondary_window_indices(wm) - win_id_str = win_id - # Delete old indice if exist - if win_id_str in secondary_indices: - secondary_indices.remove(win_id_str) - - # Add indice - secondary_indices.append(win_id_str) - - # rebuild the whole list of valid secondary windows - secondary_indices = [ - idx for idx in secondary_indices if get_window_from_id(wm, idx)] - - wm.storypencil_settings.secondary_windows_ids = join_win_ids(secondary_indices) - - -def toggle_secondary_window(wm: WindowManager, win_id: str): - """Toggle the secondary status of a Window. - - :param wm: the WindowManager instance - :param win_id: the id of the window - """ - secondary_indices = get_secondary_window_indices(wm) - win_id_str = win_id - if win_id_str in secondary_indices: - secondary_indices.remove(win_id_str) - else: - secondary_indices.append(win_id_str) - - # rebuild the whole list of valid secondary windows - secondary_indices = [ - idx for idx in secondary_indices if get_window_from_id(wm, idx)] - - wm.storypencil_settings.secondary_windows_ids = join_win_ids(secondary_indices) - - -def get_main_window(wm: WindowManager) -> Window: - """Get the Window used to drive the synchronization system - - :param wm: the WindowManager instance - :returns: the main Window or None - """ - return get_window_from_id(wm=wm, win_id=wm.storypencil_settings.main_window_id) - - -def get_secondary_window(wm: WindowManager) -> Window: - """Get the first secondary Window - - :param wm: the WindowManager instance - :returns: the Window or None - """ - for w in wm.windows: - win_id = window_id(w) - if is_secondary_window(wm, win_id): - return w - - return None - - -def get_not_main_window(wm: WindowManager) -> Window: - """Get the first not main Window - - :param wm: the WindowManager instance - :returns: the Window or None - """ - for w in wm.windows: - win_id = window_id(w) - if win_id != wm.storypencil_settings.main_window_id: - return w - - return None - - -def get_main_strip(wm: WindowManager) -> SceneSequence: - """Get Scene Strip at current time in Main window - - :param wm: the WindowManager instance - :returns: the Strip at current time or None - """ - main_window = get_main_window(wm=wm) - if not main_window or not main_window.scene.sequence_editor: - return None - seq_editor = main_window.scene.sequence_editor - return seq_editor.sequences.get(wm.storypencil_settings.main_strip_name, None) - - -class STORYPENCIL_OT_SyncToggleSecondary(Operator): - bl_idname = "storypencil.sync_toggle_secondary" - bl_label = "Toggle Secondary Window Status" - bl_description = "Enable/Disable synchronization for a specific Window" - bl_options = {'INTERNAL'} - - win_id: bpy.props.StringProperty(name="Window Index") - - def execute(self, context): - wm = context.window_manager - toggle_secondary_window(wm, self.win_id) - return {'FINISHED'} - - -def get_sequences_at_frame( - frame: int, - sequences: Sequence[Sequence]) -> Sequence[bpy.types.Sequence]: - """ Get all sequencer strips at given frame. - - :param frame: the frame to consider - """ - return [s for s in sequences if frame >= s.frame_start + s.frame_offset_start and - frame < s.frame_start + s.frame_offset_start + s.frame_final_duration] - - -def get_sequence_at_frame( - frame: int, - sequences: Sequence[bpy.types.Sequence] = None, - skip_muted: bool = True, -) -> Tuple[bpy.types.Sequence, int]: - """ - Get the higher sequence strip in channels stack at current frame. - Recursively enters scene sequences and returns the original frame in the - returned strip's time referential. - - :param frame: the frame to consider - :param skip_muted: skip muted strips - :returns: the sequence strip and the frame in strip's time referential - """ - - strips = get_sequences_at_frame(frame, sequences or bpy.context.sequences) - - # exclude muted strips - if skip_muted: - strips = [strip for strip in strips if not strip.mute] - - if not strips: - return None, frame - - # Remove strip not scene type. Switch is only with Scenes - for strip in strips: - if strip.type != 'SCENE': - strips.remove(strip) - - # consider higher strip in stack - strip = sorted(strips, key=lambda x: x.channel)[-1] - # go deeper when current strip is a MetaSequence - if isinstance(strip, MetaSequence): - return get_sequence_at_frame(frame, strip.sequences, skip_muted) - if isinstance(strip, SceneSequence): - # apply time offset to get in sequence's referential - frame = frame - strip.frame_start + strip.scene.frame_start - # enter scene's sequencer if used as input - if strip.scene_input == 'SEQUENCER': - return get_sequence_at_frame(frame, strip.scene.sequence_editor.sequences) - return strip, frame - - -def set_scene_frame(scene, frame, force_update_main=False): - """ - Set `scene` frame_current to `frame` if different. - - :param scene: the scene to update - :param frame: the frame value - :param force_update_main: whether to force the update of main scene - """ - options = bpy.context.window_manager.storypencil_settings - if scene.frame_current != frame: - scene.frame_current = int(frame) - scene.frame_set(int(frame)) - if force_update_main: - update_sync( - bpy.context, bpy.context.window_manager.storypencil_settings.main_window_id) - - -def setup_window_from_scene_strip(window: Window, strip: SceneSequence): - """Change the Scene and camera of `window` based on `strip`. - - :param window: [description] - :param scene_strip: [description] - """ - if window.scene != strip.scene: - window.scene = strip.scene - if strip.scene_camera and strip.scene_camera != window.scene.camera: - strip.scene.camera = strip.scene_camera - - -@persistent -def on_frame_changed(*args): - """ - React to current frame changes and synchronize secondary windows. - """ - # ensure context is fully initialized, i.e not '_RestrictData - if not isinstance(bpy.context, Context): - return - - # happens in some cases (not sure why) - if not bpy.context.window: - return - - wm = bpy.context.window_manager - - # early return if synchro is disabled / not available - if not validate_sync(wm) or len(bpy.data.scenes) < 2: - return - - # get current window id - update_sync(bpy.context) - - -def update_sync(context: Context, win_id=None): - """ Update synchronized Windows based on the current `context`. - - :param context: the context - :param win_id: specify a window id (context.window is used otherwise) - """ - wm = context.window_manager - - if not win_id: - win_id = window_id(context.window) - - main_scene = get_window_from_id( - wm, wm.storypencil_settings.main_window_id).scene - if not main_scene.sequence_editor: - return - - # return if scene's sequence editor has no sequences - sequences = main_scene.sequence_editor.sequences - if not sequences: - return - - # bidirectionnal sync: change main time from secondary window - if ( - win_id != wm.storypencil_settings.main_window_id - and is_secondary_window(wm, win_id) - ): - # get strip under time cursor in main window - strip, old_frame = get_sequence_at_frame( - main_scene.frame_current, - sequences=sequences - ) - # only do bidirectional sync if secondary window matches the strip at current time in main - if not isinstance(strip, SceneSequence) or strip.scene != context.scene: - return - - # calculate offset - frame_offset = context.scene.frame_current - old_frame - if frame_offset == 0: - return - - new_main_frame = main_scene.frame_current + frame_offset - update_main_time = True - # check if a valid scene strip is available under new frame before changing main time - f_start = strip.frame_start + strip.frame_offset_start - f_end = f_start + strip.frame_final_duration - if new_main_frame < f_start or new_main_frame >= f_end: - new_strip, _ = get_sequence_at_frame( - new_main_frame, - main_scene.sequence_editor.sequences, - ) - update_main_time = isinstance(new_strip, SceneSequence) - if update_main_time: - # update main time change in the next event loop + force the sync system update - # because Blender won't trigger a frame_changed event to avoid infinite recursion - bpy.app.timers.register( - functools.partial(set_scene_frame, main_scene, - new_main_frame, True) - ) - - return - - # return if current window is not main window - if win_id != wm.storypencil_settings.main_window_id: - return - - secondary_windows = [ - get_window_from_id(wm, win_id) - for win_id - in get_secondary_window_indices(wm) - if win_id and win_id != wm.storypencil_settings.main_window_id - ] - - # only work with at least 2 windows - if not secondary_windows: - return - - seq, frame = get_sequence_at_frame(main_scene.frame_current, sequences) - - # return if no sequence at current time or not a scene strip - if not isinstance(seq, SceneSequence) or not seq.scene: - wm.storypencil_settings.main_strip_name = "" - return - - wm.storypencil_settings.main_strip_name = seq.name - # change the scene on secondary windows - # warning: only one window's scene can be changed in this event loop, - # otherwise it may crashes Blender randomly - for idx, win in enumerate(secondary_windows): - if not win: - continue - # change first secondary window immediately - if idx == 0: - setup_window_from_scene_strip(win, seq) - else: - # trigger change in next event loop for other windows - bpy.app.timers.register( - functools.partial(setup_window_from_scene_strip, win, seq) - ) - - set_scene_frame(seq.scene, frame) - - -def sync_all_windows(wm: WindowManager): - """Enable synchronization on all main windows held by `wm`.""" - wm.storypencil_settings.secondary_windows_ids = join_win_ids([ - window_id(w) - for w - in get_main_windows_list(wm) - ]) - - -@persistent -def sync_autoconfig(*args): - """Autoconfigure synchronization system. - If a window contains a VSE area on a scene with a valid sequence_editor, - makes it main window and enable synchronization on all other main windows. - """ - main_windows = get_main_windows_list(bpy.context.window_manager) - # don't try to go any further if only one main window - if len(main_windows) < 2: - return - - # look for a main window with a valid sequence editor - main = next( - ( - win - for win in main_windows - if win.scene.sequence_editor - and any(area.type == 'SEQUENCE_EDITOR' for area in win.screen.areas) - ), - None - ) - # if any, set as main and activate sync on all other windows - if main: - bpy.context.window_manager.storypencil_settings.main_window_id = window_id( - main) - sync_all_windows(bpy.context.window_manager) - bpy.context.window_manager.storypencil_settings.active = True - - -def sync_active_update(self, context): - """ Update function for WindowManager.storypencil_settings.active. """ - # ensure main window is valid, using current context's window if none is set - if ( - self.active - and ( - not self.main_window_id - or not get_window_from_id(context.window_manager, self.main_window_id) - ) - ): - self.main_window_id = window_id(context.window) - # automatically sync all other windows if nothing was previously set - if not self.secondary_windows_ids: - sync_all_windows(context.window_manager) - - on_frame_changed() - - -def draw_sync_header(self, context): - """Draw Window sync tools header.""" - - wm = context.window_manager - self.layout.separator() - if wm.get('storypencil_use_new_window') is not None: - new_window = wm['storypencil_use_new_window'] - else: - new_window = False - - if not new_window: - if context.scene.storypencil_main_workspace: - if context.scene.storypencil_main_workspace.name != context.workspace.name: - if context.area.ui_type == 'DOPESHEET': - row = self.layout.row(align=True) - row.operator(STORYPENCIL_OT_Switch.bl_idname, - text="Back To VSE") - - -def draw_sync_sequencer_header(self, context): - """Draw Window sync tools header.""" - if context.space_data.view_type != 'SEQUENCER': - return - - wm = context.window_manager - layout = self.layout - layout.separator() - row = layout.row(align=True) - row.label(text="Scenes:") - if context.scene.storypencil_use_new_window: - row.operator(STORYPENCIL_OT_SetSyncMainOperator.bl_idname, text="Edit") - else: - row.operator(STORYPENCIL_OT_Switch.bl_idname, text="Edit") - - row.menu("STORYPENCIL_MT_extra_options", icon='DOWNARROW_HLT', text="") - - row.separator() - layout.operator_context = 'INVOKE_REGION_WIN' - row.operator(STORYPENCIL_OT_NewScene.bl_idname, text="New") - - layout.operator_context = 'INVOKE_DEFAULT' - row.separator(factor=0.5) - row.operator(STORYPENCIL_OT_RenderAction.bl_idname, text="Render") - - -class STORYPENCIL_PG_Settings(PropertyGroup): - """ - PropertyGroup with storypencil settings. - """ - active: BoolProperty( - name="Synchronize", - description=( - "Automatically open current Sequence's Scene in other " - "Main Windows and activate Time Synchronization"), - default=False, - update=sync_active_update - ) - - main_window_id: StringProperty( - name="Main Window ID", - description="ID of the window driving the Synchronization", - default="", - ) - - secondary_windows_ids: StringProperty( - name="Secondary Windows", - description="Serialized Secondary Window Indices", - default="", - ) - - active_window_index: IntProperty( - name="Active Window Index", - description="Index for using Window Manager's windows in a UIList", - default=0 - ) - - main_strip_name: StringProperty( - name="Main Strip Name", - description="Scene Strip at current time in the Main window", - default="", - ) - - show_main_strip_range: BoolProperty( - name="Show Main Strip Range in Secondary Windows", - description="Draw main Strip's in/out markers in synchronized secondary Windows", - default=True, - ) - - -# ------------------------------------------------------------- -# Switch manually between Main and Edit Scene and Layout -# -# ------------------------------------------------------------- -class STORYPENCIL_OT_Switch(Operator): - bl_idname = "storypencil.switch" - bl_label = "Switch" - bl_description = "Switch workspace" - bl_options = {'REGISTER', 'UNDO'} - - # Get active strip - def act_strip(self, context): - scene = context.scene - sequences = scene.sequence_editor.sequences - if not sequences: - return None - # Get strip under time cursor - strip, old_frame = get_sequence_at_frame( - scene.frame_current, sequences=sequences) - return strip - - # ------------------------------ - # Poll - # ------------------------------ - @classmethod - def poll(cls, context): - scene = context.scene - if scene.storypencil_main_workspace is None or scene.storypencil_main_scene is None: - return False - if scene.storypencil_edit_workspace is None: - return False - - return True - - # ------------------------------ - # Execute button action - # ------------------------------ - def execute(self, context): - wm = context.window_manager - scene = context.scene - wm['storypencil_use_new_window'] = scene.storypencil_use_new_window - - # Switch to Main - if scene.storypencil_main_workspace.name != context.workspace.name: - cfra_prv = scene.frame_current - prv_pin = None - if scene.storypencil_main_workspace is not None: - if scene.storypencil_main_workspace.use_pin_scene: - scene.storypencil_main_workspace.use_pin_scene = False - - context.window.workspace = scene.storypencil_main_workspace - - if scene.storypencil_main_scene is not None: - context.window.scene = scene.storypencil_main_scene - strip = self.act_strip(context) - if strip: - context.window.scene.frame_current = int(cfra_prv + strip.frame_start) - 1 - - bpy.ops.sequencer.reload() - else: - # Switch to Edit - strip = self.act_strip(context) - # save camera - if strip is not None and strip.type == "SCENE": - # Save data - strip.scene.storypencil_main_workspace = scene.storypencil_main_workspace - strip.scene.storypencil_main_scene = scene.storypencil_main_scene - strip.scene.storypencil_edit_workspace = scene.storypencil_edit_workspace - - # Set workspace and Scene - cfra_prv = scene.frame_current - if scene.storypencil_edit_workspace.use_pin_scene: - scene.storypencil_edit_workspace.use_pin_scene = False - - context.window.workspace = scene.storypencil_edit_workspace - context.window.workspace.update_tag() - - context.window.scene = strip.scene - active_frame = cfra_prv - strip.frame_start + 1 - if active_frame < strip.scene.frame_start: - active_frame = strip.scene.frame_start - context.window.scene.frame_current = int(active_frame) - - # Set camera - if strip.scene_input == 'CAMERA': - for screen in bpy.data.screens: - for area in screen.areas: - if area.type == 'VIEW_3D': - # select camera as view - if strip and strip.scene.camera is not None: - area.spaces.active.region_3d.view_perspective = 'CAMERA' - - return {"FINISHED"} - - -class STORYPENCIL_OT_TabSwitch(Operator): - bl_idname = "storypencil.tabswitch" - bl_label = "Switch using tab key" - bl_description = "Wrapper used to handle the Tab key to switch" - bl_options = {'INTERNAL'} - - def execute(self, context): - if context.scene.storypencil_use_new_window: - bpy.ops.storypencil.sync_set_main('INVOKE_DEFAULT', True) - else: - bpy.ops.storypencil.switch('INVOKE_DEFAULT', True) - - return {'FINISHED'} diff --git a/storypencil/ui.py b/storypencil/ui.py deleted file mode 100644 index cc34fbb..0000000 --- a/storypencil/ui.py +++ /dev/null @@ -1,211 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -import bpy - -from bpy.types import ( - Menu, - Panel, -) - -from .synchro import get_main_window, validate_sync, window_id - - -class STORYPENCIL_MT_extra_options(Menu): - bl_label = "Scene Settings" - - def draw(self, context): - layout = self.layout - wm = bpy.context.window_manager - scene = context.scene - layout.prop(scene, "storypencil_use_new_window") - - # If no main window nothing else to do - if not get_main_window(wm): - return - - win_id = window_id(context.window) - row = self.layout.row(align=True) - if not validate_sync(window_manager=wm) or win_id == wm.storypencil_settings.main_window_id: - row = layout.row() - row.prop(wm.storypencil_settings, "active", - text="Timeline Synchronization") - row.active = scene.storypencil_use_new_window - - row = layout.row() - row.prop(wm.storypencil_settings, - "show_main_strip_range", text="Show Strip Range") - row.active = scene.storypencil_use_new_window - - -# ------------------------------------------------------ -# Defines UI panel -# ------------------------------------------------------ -# ------------------------------------------------------------------ -# Define panel class for manual switch parameters. -# ------------------------------------------------------------------ -class STORYPENCIL_PT_Settings(Panel): - bl_idname = "STORYPENCIL_PT_Settings" - bl_label = "Settings" - bl_space_type = 'SEQUENCE_EDITOR' - bl_region_type = 'UI' - bl_category = 'Storypencil' - - @classmethod - def poll(cls, context): - if context.space_data.view_type != 'SEQUENCER': - return False - - return True - - # ------------------------------ - # Draw UI - # ------------------------------ - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - - -class STORYPENCIL_PT_General(Panel): - bl_idname = "STORYPENCIL_PT_General" - bl_label = "General" - bl_space_type = 'SEQUENCE_EDITOR' - bl_region_type = 'UI' - bl_category = 'Storypencil' - bl_options = {'DEFAULT_CLOSED'} - bl_parent_id = "STORYPENCIL_PT_Settings" - - @classmethod - def poll(cls, context): - if context.space_data.view_type != 'SEQUENCER': - return False - - return True - - # ------------------------------ - # Draw UI - # ------------------------------ - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - scene = context.scene - - setup_ready = scene.storypencil_main_workspace is not None - row = layout.row() - row.alert = not setup_ready - row.prop(scene, "storypencil_main_workspace", text="VSE Workspace") - - row = layout.row() - if scene.storypencil_main_scene is None: - row.alert = True - row.prop(scene, "storypencil_main_scene", text="VSE Scene") - - layout.separator() - - row = layout.row() - if scene.storypencil_main_workspace and scene.storypencil_edit_workspace: - if scene.storypencil_main_workspace.name == scene.storypencil_edit_workspace.name: - row.alert = True - if scene.storypencil_edit_workspace is None: - row.alert = True - row.prop(scene, "storypencil_edit_workspace", text="Drawing Workspace") - - -class STORYPENCIL_PT_RenderPanel(Panel): - bl_label = "Render Strips" - bl_space_type = 'SEQUENCE_EDITOR' - bl_region_type = 'UI' - bl_category = 'Storypencil' - bl_parent_id = "STORYPENCIL_PT_Settings" - - @classmethod - def poll(cls, context): - if context.space_data.view_type != 'SEQUENCER': - return False - - return True - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - - scene = context.scene - settings = scene.render.image_settings - - is_video = settings.file_format in {'FFMPEG', 'AVI_JPEG', 'AVI_RAW'} - row = layout.row() - if scene.storypencil_render_render_path is None: - row.alert = True - row.prop(scene, "storypencil_render_render_path") - - row = layout.row() - row.prop(scene, "storypencil_render_onlyselected") - - row = layout.row() - row.prop(scene.render.image_settings, "file_format") - - if settings.file_format == 'FFMPEG': - row = layout.row() - row.prop(scene.render.ffmpeg, "format") - - row = layout.row() - row.enabled = is_video - row.prop(scene.render.ffmpeg, "audio_codec") - - row = layout.row() - row.prop(scene, "storypencil_add_render_strip") - - row = layout.row() - row.enabled = scene.storypencil_add_render_strip - row.prop(scene, "storypencil_render_channel") - - if not is_video: - row = layout.row() - row.prop(scene, "storypencil_render_step") - - row = layout.row() - row.prop(scene, "storypencil_render_numbering") - - row = layout.row() - row.prop(scene, "storypencil_add_render_byfolder") - - -# ------------------------------------------------------------------ -# Define panel class for new base scene creation. -# ------------------------------------------------------------------ -class STORYPENCIL_PT_SettingsNew(Panel): - bl_idname = "STORYPENCIL_PT_SettingsNew" - bl_label = "New Scenes" - bl_space_type = 'SEQUENCE_EDITOR' - bl_region_type = 'UI' - bl_category = 'Storypencil' - bl_parent_id = "STORYPENCIL_PT_Settings" - - @classmethod - def poll(cls, context): - if context.space_data.view_type != 'SEQUENCER': - return False - - return True - - # ------------------------------ - # Draw UI - # ------------------------------ - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - scene = context.scene - row = layout.row() - row.prop(scene, "storypencil_name_prefix", text="Name Prefix") - row = layout.row() - row.prop(scene, "storypencil_name_suffix", text="Name Suffix") - row = layout.row() - row.prop(scene, "storypencil_scene_duration", text="Frames") - - row = layout.row() - if scene.storypencil_base_scene is None: - row.alert = True - row.prop(scene, "storypencil_base_scene", text="Template Scene") diff --git a/storypencil/utils.py b/storypencil/utils.py deleted file mode 100644 index 9357d1c..0000000 --- a/storypencil/utils.py +++ /dev/null @@ -1,110 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -import bpy -import math - - -def redraw_areas_by_type(window, area_type, region_type='WINDOW'): - """Redraw `window`'s areas matching the given `area_type` and optionnal `region_type`.""" - for area in window.screen.areas: - if area.type == area_type: - for region in area.regions: - if region.type == region_type: - region.tag_redraw() - - -def redraw_all_areas_by_type(context, area_type, region_type='WINDOW'): - """Redraw areas in all windows matching the given `area_type` and optionnal `region_type`.""" - for window in context.window_manager.windows: - redraw_areas_by_type(window, area_type, region_type) - - -def get_selected_keyframes(context): - """Get list of selected keyframes for any object in the scene. """ - keys = [] - - for ob in context.scene.objects: - if ob.type == 'GPENCIL': - for gpl in ob.data.layers: - for gpf in gpl.frames: - if gpf.select: - keys.append(gpf.frame_number) - - elif ob.animation_data is not None and ob.animation_data.action is not None: - action = ob.animation_data.action - for fcu in action.fcurves: - for kp in fcu.keyframe_points: - if kp.select_control_point: - keys.append(int(kp.co[0])) - - keys.sort() - unique_keys = list(set(keys)) - return unique_keys - - -def find_collections_recursive(root, collections=None): - # Initialize the result once - if collections is None: - collections = [] - - def recurse(parent, result): - result.append(parent) - # Look over children at next level - for child in parent.children: - recurse(child, result) - - recurse(root, collections) - - return collections - - -def get_keyframe_list(scene, frame_start, frame_end): - """Get list of frames for any gpencil object in the scene and meshes. """ - keys = [] - root = scene.view_layers[0].layer_collection - collections = find_collections_recursive(root) - - for laycol in collections: - if laycol.exclude is True or laycol.collection.hide_render is True: - continue - for ob in laycol.collection.objects: - if ob.hide_render: - continue - if ob.type == 'GPENCIL': - for gpl in ob.data.layers: - if gpl.hide: - continue - for gpf in gpl.frames: - if frame_start <= gpf.frame_number <= frame_end: - keys.append(gpf.frame_number) - - # Animation at object level - if ob.animation_data is not None and ob.animation_data.action is not None: - action = ob.animation_data.action - for fcu in action.fcurves: - for kp in fcu.keyframe_points: - if frame_start <= int(kp.co[0]) <= frame_end: - keys.append(int(kp.co[0])) - - # Animation at datablock level - if ob.type != 'GPENCIL': - data = ob.data - if data and data.animation_data is not None and data.animation_data.action is not None: - action = data.animation_data.action - for fcu in action.fcurves: - for kp in fcu.keyframe_points: - if frame_start <= int(kp.co[0]) <= frame_end: - keys.append(int(kp.co[0])) - - # Scene Markers - for m in scene.timeline_markers: - if frame_start <= m.frame <= frame_end and m.camera is not None: - keys.append(int(m.frame)) - - # If no animation or markers, must add first frame - if len(keys) == 0: - keys.append(int(frame_start)) - - unique_keys = list(set(keys)) - unique_keys.sort() - return unique_keys -- 2.30.2 From 0f72f6c85c3743a9072273acb6a8a34b1cf1064b Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Fri, 6 Jan 2023 16:16:35 +0100 Subject: [PATCH 26/33] After Effects export: fix crash when trying to export some meshes If a mesh consisted in a single rectangle but it wasn't properly unwrapped (with each vert at a corner), the object was identified as an image, but could then not be exported as one. Consider such objects as nulls instead. --- io_export_after_effects.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 40c208d..0db40e8 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -21,7 +21,7 @@ bl_info = { "description": "Export cameras, selected objects & camera solution " "3D Markers to Adobe After Effects CS3 and above", "author": "Bartek Skorupa, Damien Picard (@pioverfour)", - "version": (0, 1, 2), + "version": (0, 1, 3), "blender": (2, 80, 0), "location": "File > Export > Adobe After Effects (.jsx)", "warning": "", @@ -444,6 +444,7 @@ def is_image_plane(obj): - The mesh is a plane - The mesh has exactly one material - There is only one image in this material node tree + - The rectangle is UV unwrapped and its UV is occupying the whole space """ if not is_plane(obj): return False @@ -459,9 +460,13 @@ def is_image_plane(obj): if img is None: return False - if len(obj.data.vertices) == 4: - return True + if len(obj.data.vertices) != 4: + return False + if not get_image_plane_matrix(obj): + return False + + return True def get_image_filepath(obj): mat = get_first_material(obj) @@ -499,6 +504,7 @@ def get_image_plane_matrix(obj): This will only work if uvs occupy all space, to get bounds """ + p0, px, py = None, None, None for p_i, p in enumerate(obj.data.uv_layers.active.data): if p.uv == Vector((0, 0)): p0 = p_i @@ -507,6 +513,9 @@ def get_image_plane_matrix(obj): elif p.uv == Vector((0, 1)): py = p_i + if None in (p0, px, py): + return False + verts = obj.data.vertices loops = obj.data.loops -- 2.30.2 From 9d538629bb8a425991c7d10a49bab1ba0788c18f Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Wed, 18 Jan 2023 18:45:44 +0100 Subject: [PATCH 27/33] Gitea: add pull request template --- .gitea/pull_request_template.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .gitea/pull_request_template.yaml diff --git a/.gitea/pull_request_template.yaml b/.gitea/pull_request_template.yaml new file mode 100644 index 0000000..49ccff3 --- /dev/null +++ b/.gitea/pull_request_template.yaml @@ -0,0 +1,22 @@ +name: Pull Request +about: Contribute to add-ons bundled with Blender +body: + - type: markdown + attributes: + value: | + ### Instructions + + * [Contributing a new add-on](https://wiki.blender.org/wiki/Process/Addons) + * [Contributing code](https://wiki.blender.org/index.php/Dev:Doc/Process/Contributing_Code) + * [Effective code review](https://wiki.blender.org/index.php/Dev:Doc/Tools/Code_Review) + + By submitting code here, you agree that the code is (compatible with) GNU GPL v2 or later. + + - type: textarea + id: body + attributes: + label: "Description" + value: | + Description of the problem that is addressed in the patch. + + Description of the proposed solution and its implementation. -- 2.30.2 From 74a85bed96115401ead7d9920118b5c17fc9f14d Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Sat, 28 Jan 2023 18:14:59 +0100 Subject: [PATCH 28/33] Gitea: fix pull request template so commit body can be set as description --- .gitea/pull_request_template.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitea/pull_request_template.yaml b/.gitea/pull_request_template.yaml index 49ccff3..7ca55da 100644 --- a/.gitea/pull_request_template.yaml +++ b/.gitea/pull_request_template.yaml @@ -16,7 +16,4 @@ body: id: body attributes: label: "Description" - value: | - Description of the problem that is addressed in the patch. - - Description of the proposed solution and its implementation. + hide_label: true -- 2.30.2 From 14ab9273409ea0231d08ba6e86fdc73d4e459e99 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Mon, 30 Jan 2023 23:55:27 +0100 Subject: [PATCH 29/33] Gitea: add merge message templates To add the pull request # at the end of the commit. --- .gitea/default_merge_message/REBASE_TEMPLATE.md | 5 +++++ .gitea/default_merge_message/SQUASH_TEMPLATE.md | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 .gitea/default_merge_message/REBASE_TEMPLATE.md create mode 100644 .gitea/default_merge_message/SQUASH_TEMPLATE.md diff --git a/.gitea/default_merge_message/REBASE_TEMPLATE.md b/.gitea/default_merge_message/REBASE_TEMPLATE.md new file mode 100644 index 0000000..87a0937 --- /dev/null +++ b/.gitea/default_merge_message/REBASE_TEMPLATE.md @@ -0,0 +1,5 @@ +${CommitTitle} + +${CommitBody} + +Pull Request #${PullRequestIndex} diff --git a/.gitea/default_merge_message/SQUASH_TEMPLATE.md b/.gitea/default_merge_message/SQUASH_TEMPLATE.md new file mode 100644 index 0000000..36123d4 --- /dev/null +++ b/.gitea/default_merge_message/SQUASH_TEMPLATE.md @@ -0,0 +1,3 @@ +${PullRequestTitle} + +Pull Request #${PullRequestIndex} -- 2.30.2 From 043c220958ba97538b11cce0f72dfb01b0477639 Mon Sep 17 00:00:00 2001 From: Sergey Sharybin Date: Tue, 7 Feb 2023 14:19:21 +0100 Subject: [PATCH 30/33] Update references to the new projects platform and main branch --- .github/pull_request_template.md | 2 +- .github/stale.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 93a2956..eeceb52 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,5 @@ This repository is only used as a mirror of git.blender.org. Blender development happens on -https://developer.blender.org. +https://projects.blender.org. To get started with contributing code, please see: https://wiki.blender.org/wiki/Process/Contributing_Code diff --git a/.github/stale.yml b/.github/stale.yml index 9c563f2..b954d3f 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -16,7 +16,7 @@ staleLabel: stale closeComment: > This issue has been automatically closed, because this repository is only used as a mirror of git.blender.org. Blender development happens on - developer.blender.org. + projects.blender.org. To get started contributing code, please read: https://wiki.blender.org/wiki/Process/Contributing_Code -- 2.30.2 From 65ff08e325d54a58b47fb3219ec7dbf417f20f18 Mon Sep 17 00:00:00 2001 From: Sergey Sharybin Date: Tue, 7 Feb 2023 14:46:24 +0100 Subject: [PATCH 31/33] GitHub mirror: change Git URLs to point projects.blender.org instead of git.blender.org --- .github/pull_request_template.md | 3 +-- .github/stale.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index eeceb52..4b8e99e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,4 @@ -This repository is only used as a mirror of git.blender.org. Blender development happens on -https://projects.blender.org. +This repository is only used as a mirror. Blender development happens on projects.blender.org. To get started with contributing code, please see: https://wiki.blender.org/wiki/Process/Contributing_Code diff --git a/.github/stale.yml b/.github/stale.yml index b954d3f..db14bfd 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -15,8 +15,7 @@ staleLabel: stale # Comment to post when closing a stale Issue or Pull Request. closeComment: > This issue has been automatically closed, because this repository is only - used as a mirror of git.blender.org. Blender development happens on - projects.blender.org. + used as a mirror. Blender development happens on projects.blender.org. To get started contributing code, please read: https://wiki.blender.org/wiki/Process/Contributing_Code -- 2.30.2 From 8f4599c4fb6ab2386f2498814c203fd676b2ccf2 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Wed, 15 Feb 2023 12:33:22 +0100 Subject: [PATCH 32/33] Cleanup: delete .arcconfig This no longer works without developer.blender.org --- .arcconfig | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .arcconfig diff --git a/.arcconfig b/.arcconfig deleted file mode 100644 index b5d479d..0000000 --- a/.arcconfig +++ /dev/null @@ -1,7 +0,0 @@ -{ - "project_id" : "Blender Addons Contrib", - "conduit_uri" : "https://developer.blender.org/", - "phabricator.uri" : "https://developer.blender.org/", - "git.default-relative-commit" : "origin/master", - "arc.land.update.default" : "rebase" -} -- 2.30.2 From 7c685d3b9e703f74b90e854a097701d442859f36 Mon Sep 17 00:00:00 2001 From: Sebastian Sille Date: Fri, 17 Feb 2023 21:21:48 +0100 Subject: [PATCH 33/33] io_scene_3ds: Update for Blender 3.x Pull Request #1 --- io_scene_3ds/__init__.py | 4 ++-- io_scene_3ds/export_3ds.py | 29 ++++++++++++++--------------- io_scene_3ds/import_3ds.py | 27 ++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/io_scene_3ds/__init__.py b/io_scene_3ds/__init__.py index 2ccb62e..3912ac1 100644 --- a/io_scene_3ds/__init__.py +++ b/io_scene_3ds/__init__.py @@ -32,8 +32,8 @@ import bpy bl_info = { "name": "Autodesk 3DS format", "author": "Bob Holcomb, Campbell Barton, Andreas Atteneder, Sebastian Schrand", - "version": (2, 1, 0), - "blender": (2, 82, 0), + "version": (2, 2, 0), + "blender": (3, 0, 0), "location": "File > Import", "description": "Import 3DS, meshes, uvs, materials, textures, " "cameras & lamps", diff --git a/io_scene_3ds/export_3ds.py b/io_scene_3ds/export_3ds.py index dd46b4d..2a22d74 100644 --- a/io_scene_3ds/export_3ds.py +++ b/io_scene_3ds/export_3ds.py @@ -645,8 +645,8 @@ def make_material_chunk(material, image): name_str = material.name if material else "None" - if image: - name_str += image.name + #if image: + # name_str += image.name name.add_variable("name", _3ds_string(sane_name(name_str))) material_chunk.add_subchunk(name) @@ -670,6 +670,7 @@ def make_material_chunk(material, image): material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, wrap.specular)) material_chunk.add_subchunk(make_percent_subchunk(MATSHIN3, wrap.metallic)) material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - wrap.alpha)) + material_chunk.add_subchunk(make_percent_subchunk(MATSELFILPCT, wrap.emission_strength)) material_chunk.add_subchunk(shading) if wrap.base_color_texture: @@ -704,13 +705,10 @@ def make_material_chunk(material, image): normal = [wrap.normalmap_texture] bump = wrap.normalmap_strength b_pct = min(bump, 1) - bumpval = min(999, (bump * 100)) # 3ds max bump = 999 - strength = _3ds_chunk(MAT_BUMP_PERCENT) - strength.add_variable("bump_pct", _3ds_ushort(int(bumpval))) matmap = make_material_texture_chunk(MAT_BUMPMAP, normal, b_pct) if matmap: material_chunk.add_subchunk(matmap) - material_chunk.add_subchunk(strength) + material_chunk.add_subchunk(make_percent_subchunk(MAT_BUMP_PERCENT, b_pct)) if wrap.roughness_texture: roughness = [wrap.roughness_texture] @@ -720,7 +718,7 @@ def make_material_chunk(material, image): material_chunk.add_subchunk(matmap) if wrap.emission_color_texture: - e_pct = sum(wrap.emission_color[:]) * .25 + e_pct = wrap.emission_strength emission = [wrap.emission_color_texture] matmap = make_material_texture_chunk(MAT_SELFIMAP, emission, e_pct) if matmap: @@ -907,8 +905,8 @@ def make_faces_chunk(tri_list, mesh, materialDict): context_face_array = unique_mats[ma, img][1] except: name_str = ma if ma else "None" - if img: - name_str += img + #if img: + # name_str += img context_face_array = _3ds_array() unique_mats[ma, img] = _3ds_string(sane_name(name_str)), context_face_array @@ -1169,7 +1167,7 @@ def save(operator, ): import time - from bpy_extras.io_utils import create_derived_objects, free_derived_objects + #from bpy_extras.io_utils import create_derived_objects, free_derived_objects """Save the Blender scene to a 3ds file.""" @@ -1185,7 +1183,7 @@ def save(operator, scene = context.scene layer = context.view_layer - #depsgraph = context.evaluated_depsgraph_get() + depsgraph = context.evaluated_depsgraph_get() # Initialize the main chunk (primary): primary = _3ds_chunk(PRIMARY) @@ -1233,7 +1231,9 @@ def save(operator, for ob in objects: # get derived objects - free, derived = create_derived_objects(scene, ob) + #free, derived = create_derived_objects(scene, ob) + derived_dict = bpy_extras.io_utils.create_derived_objects(depsgraph, [ob]) + derived = derived_dict.get(ob) if derived is None: continue @@ -1242,7 +1242,6 @@ def save(operator, if ob.type not in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}: continue - #ob_derived_eval = ob_derived.evaluated_get(depsgraph) try: data = ob_derived.to_mesh() except: @@ -1286,8 +1285,8 @@ def save(operator, # ob_derived_eval.to_mesh_clear() - if free: - free_derived_objects(ob) + #if free: + # free_derived_objects(ob) # Make material chunks for all materials used in the meshes: for ma_image in materialDict.values(): diff --git a/io_scene_3ds/import_3ds.py b/io_scene_3ds/import_3ds.py index 304ca70..8dfa475 100644 --- a/io_scene_3ds/import_3ds.py +++ b/io_scene_3ds/import_3ds.py @@ -63,6 +63,7 @@ MAT_SHIN2 = 0xA041 # Shininess of the object/material (percent) MAT_SHIN3 = 0xA042 # Reflection of the object/material (percent) MAT_TRANSPARENCY = 0xA050 # Transparency value of material (percent) MAT_SELF_ILLUM = 0xA080 # Self Illumination value of material +MATSELFILPCT = 0xA084 # Self illumination strength (percent) MAT_WIRE = 0xA085 # Only render's wireframe MAT_TEXTURE_MAP = 0xA200 # This is a header for a new texture map @@ -465,6 +466,7 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, IMAGE_SE pct = 50 contextWrapper.emission_color = contextMaterial.line_color[:3] + contextWrapper.emission_strength = contextMaterial.line_priority / 100 contextWrapper.base_color = contextMaterial.diffuse_color[:3] contextWrapper.specular = contextMaterial.specular_intensity contextWrapper.roughness = contextMaterial.roughness @@ -667,6 +669,18 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, IMAGE_SE print("Cannot read material transparency") new_chunk.bytes_read += temp_chunk.bytes_read + elif new_chunk.ID == MATSELFILPCT: + read_chunk(file, temp_chunk) + if temp_chunk.ID == PERCENTAGE_SHORT: + temp_data = file.read(SZ_U_SHORT) + temp_chunk.bytes_read += SZ_U_SHORT + contextMaterial.line_priority = int(struct.unpack('H', temp_data)[0]) + elif temp_chunk.ID == PERCENTAGE_FLOAT: + temp_data = file.read(SZ_FLOAT) + temp_chunk.bytes_read += SZ_FLOAT + contextMaterial.line_priority = (float(struct.unpack('f', temp_data)[0]) * 100) + new_chunk.bytes_read += temp_chunk.bytes_read + elif new_chunk.ID == MAT_TEXTURE_MAP: read_texture(new_chunk, temp_chunk, "Diffuse", "COLOR") @@ -684,11 +698,18 @@ def process_next_chunk(context, file, previous_chunk, imported_objects, IMAGE_SE read_texture(new_chunk, temp_chunk, "Bump", "NORMAL") elif new_chunk.ID == MAT_BUMP_PERCENT: - temp_data = file.read(SZ_U_SHORT) - new_chunk.bytes_read += SZ_U_SHORT - contextWrapper.normalmap_strength = (float(struct.unpack('