From 1d2165ad3ce9a5a5f385b194e1a320199e1e1ce5 Mon Sep 17 00:00:00 2001 From: julien blervaque Date: Fri, 26 Aug 2022 15:54:17 +0200 Subject: [PATCH 01/27] added retimer filter stb shots --- shotmanager/properties/props.py | 35 ++++++++- .../retimer/retimer_applyto_settings.py | 30 ++++++++ shotmanager/retimer/retimer_applyto_ui.py | 51 +++++++++++-- shotmanager/retimer/retimer_operators.py | 21 ++++-- shotmanager/retimer/retimer_props.py | 7 ++ shotmanager/retimer/retimer_ui.py | 4 +- shotmanager/utils/utils.py | 14 ++++ shotmanager/utils/utils_storyboard.py | 45 +++++++++++ shotmanager/utils/utils_ui.py | 75 +++++++++++++++++-- 9 files changed, 259 insertions(+), 23 deletions(-) create mode 100644 shotmanager/utils/utils_storyboard.py diff --git a/shotmanager/properties/props.py b/shotmanager/properties/props.py index feda7845..a32c9d00 100644 --- a/shotmanager/properties/props.py +++ b/shotmanager/properties/props.py @@ -1739,7 +1739,7 @@ def _get_greasePencil_layersModeB(self): pass val = self.get("greasePencil_layersModeB", 0) print(f" gpencil.name: {gpencil.name}, val:{val}") - gpSettings = self.stb_frameTemplate.getEditedGPByName(gpencil.name) + # gpSettings = self.stb_frameTemplate.getEditedGPByName(gpencil.name) # if gpSettings is not None: # # Create a lookup-dict for the object layers # # layers_dict = {layer.info: i for i, layer in enumerate(gpencil.data.layers)} @@ -3765,6 +3765,39 @@ def getFirstShotIndexAfterFrame(self, frameIndex, ignoreDisabled=False): return firstShotInd + ############################################# + # shot cameras + ############################################# + + def getCameras(self, fromAllTakes=False, ignoreDisabled=False, takeIndex=-1, onlyShotsOfType=None): + """Return the list of all the valid cameras used by the shots of the specified take + Cameras are present only 1 time in the returned list + Args: + fromAllTakes: If True, then takeIndex is ignored + onlyShotOfType: Can be None, "STORYBOARD" or "PREVIZ" + """ + takeInd = ( + self.getCurrentTakeIndex() + if -1 == takeIndex + else (takeIndex if 0 <= takeIndex and takeIndex < len(self.getTakes()) else -1) + ) + if -1 == takeInd: + return None + + takes = self.takes if fromAllTakes else [self.takes[takeInd]] + + shotCameras = list() + for take in takes: + for shot in take.shots: + if shot.enabled or ignoreDisabled: + if onlyShotsOfType is None or onlyShotsOfType == shot.shotType: + if shot.isCameraValid(): + cam = shot.camera + if cam not in shotCameras: + shotCameras.append(cam) + + return shotCameras + def getShotsUsingCamera(self, cam, ignoreDisabled=False, takeIndex=-1): """Return the list of all the shots used by the specified camera in the specified take""" takeInd = ( diff --git a/shotmanager/retimer/retimer_applyto_settings.py b/shotmanager/retimer/retimer_applyto_settings.py index a2a751b8..bfe8e5a8 100644 --- a/shotmanager/retimer/retimer_applyto_settings.py +++ b/shotmanager/retimer/retimer_applyto_settings.py @@ -155,3 +155,33 @@ def initialize(self, applyToMode): if "LEGACY" == applyToMode: self.id = applyToMode self.name = "Apply to Legacy Preset" + + def getQuickHelp(self, topic): + """Args: + topic: Can be APPLYTO_STORYBOARDSHOTS""" + + docPath = "https://ubisoft-shotmanager.readthedocs.io/en/latest/feature-toggles/retimer.html" + + if "APPLYTO_STORYBOARDSHOTS" == topic: + title = "Storyboard Shots" + text = "Except if you have some very specific needs, it is usually not necessary to" + text += "\napply the Scene Retiming to the Storyboard shots since they do not" + text += "\nreally depends on the scene content." + # TODO wkip add doc anchor to each path + docPath += "" + # elif "INSERT_BEFORE" == topic: + # text += "" + # elif "INSERT_AFTER" == topic: + # text += "" + # elif "DELETE_RANGE" == topic: + # text += "" + # elif "RESCALE" == topic: + # text += "" + # elif "CLEAR_ANIM" == topic: + # text += "" + else: + title = "description" + text = "text" + + tooltip = "Quick tips about " + title + return (tooltip, title, text, docPath) diff --git a/shotmanager/retimer/retimer_applyto_ui.py b/shotmanager/retimer/retimer_applyto_ui.py index 71934092..6715985b 100644 --- a/shotmanager/retimer/retimer_applyto_ui.py +++ b/shotmanager/retimer/retimer_applyto_ui.py @@ -20,13 +20,14 @@ """ -from shotmanager.utils.utils_ui import propertyColumn +from shotmanager.utils.utils_ui import propertyColumn, quickTooltip from shotmanager.config import config -def drawApplyTo(context, retimerSettings, layout): +def drawApplyTo(context, retimerProps, layout): prefs = config.getShotManagerPrefs() + retimerSettings = retimerProps.getCurrentApplyToSettings() propCol = propertyColumn(layout, padding_left=2, align=False) # layout = layout.box() @@ -48,12 +49,48 @@ def drawApplyTo(context, retimerSettings, layout): propCol.separator(factor=0.5) if "SCENE" == retimerSettings.id: - propCol.label(text="Retime is applied to EVERYTHING in the scene, except:") + propCol.label(text="Retiming is applied to EVERYTHING in the scene, except:") - messagesCol = propertyColumn(propCol, padding_left=4, padding_bottom=2, scale_y=0.8) - messagesCol.label(text="- Storyboard Shots: Their temporality is not related to the scene") + messagesCol = propertyColumn(propCol, padding_left=4, padding_bottom=1, scale_y=0.8) messagesCol.label(text="- Compositor content: Currently not supported by the Retimer") messagesCol.label(text="- NLA Editor content: Currently not supported by the Retimer") + # messagesCol.label(text="- Storyboard Shots: Their temporality is not related to the scene") + messagesCol.label(text="- Unchecked entities below:") + + entitiesCol = propertyColumn(propCol, padding_left=6, padding_bottom=0, scale_y=1) + + stbRow = entitiesCol.row() + stbRow.prop( + retimerSettings, + "applyToStoryboardShots", + text="Storyboard Shots", + ) + # text="Storyboard Shots: Their temporality is not related to the scene", + + # doesnt work, need an enum + # stbRow.prop_with_popover( + # retimerSettings, "applyToStoryboardShots", panel="UAS_PT_SM_quicktooltip", text="tototo", icon="INFO" + # ) + + stbRowRight = stbRow.row() + stbRowRight.alert = retimerSettings.applyToStoryboardShots + + quickHelpInfo = retimerProps.getQuickHelp("APPLYTO_STORYBOARDSHOTS") + # doc_op = stbRowRight.operator("shotmanager.open_documentation_url", text="", icon="INFO", emboss=False) + # doc_op.path = quickHelpInfo[3] + # tooltipStr = quickHelpInfo[1] + # tooltipStr += f"\n{quickHelpInfo[2]}" + # tooltipStr += f"\n\nOpen Shot Manager Retimer online documentation:\n {doc_op.path}" + # doc_op.tooltip = tooltipStr + + # quickTooltip(stbRowRight, "patate", title="Storyboard Shots", alert=retimerSettings.applyToStoryboardShots) + quickTooltip( + stbRowRight, quickHelpInfo[2], title=quickHelpInfo[1], alert=retimerSettings.applyToStoryboardShots + ) + + entitiesCol.separator(factor=0.5) + entitiesCol.prop(prefs, "applyToTimeCursor", text="Time Cursor") + entitiesCol.prop(prefs, "applyToSceneRange", text="Scene Range") if "SELECTED_OBJECTS" == retimerSettings.id: split = propCol.split(factor=0.326) @@ -115,9 +152,9 @@ def drawApplyTo(context, retimerSettings, layout): # time cursor and range ########################## - if "SCENE" == retimerSettings.id or "LEGACY" == retimerSettings.id or config.devDebug: + if "LEGACY" == retimerSettings.id or config.devDebug: box = propCol.box() - box.alert = "SCENE" != retimerSettings.id + # box.alert = "SCENE" != retimerSettings.id col = box.column() diff --git a/shotmanager/retimer/retimer_operators.py b/shotmanager/retimer/retimer_operators.py index 898c666c..fa4c720b 100644 --- a/shotmanager/retimer/retimer_operators.py +++ b/shotmanager/retimer/retimer_operators.py @@ -25,6 +25,7 @@ from . import retimer +from shotmanager.utils.utils_storyboard import getStoryboardObjects from shotmanager.config import sm_logging _logger = sm_logging.getLogger(__name__) @@ -103,9 +104,15 @@ def execute(self, context): retimerApplyToSettings = context.scene.UAS_shot_manager_props.retimer.getCurrentApplyToSettings() if retimerApplyToSettings.onlyOnSelection: - obj_list = context.selected_objects + sceneObjs = [obj for obj in context.selected_objects] else: - obj_list = context.scene.objects + sceneObjs = [obj for obj in context.scene.objects] + + if not retimerApplyToSettings.applyToStoryboardShots: + stbObjs = getStoryboardObjects(context.scene) + for obj in stbObjs: + if obj in sceneObjs: + sceneObjs.remove(obj) # startFrame = retimeEngine.start_frame # endFrame = retimeEngine.end_frame @@ -138,7 +145,7 @@ def execute(self, context): retimeEngine, offsetMode, retimerApplyToSettings, - obj_list, + sceneObjs, farRefPoint + 1, abs(retimeEngine.offset_duration), retimeEngine.gap, @@ -159,7 +166,7 @@ def execute(self, context): retimeEngine, "INSERT", retimerApplyToSettings, - obj_list, + sceneObjs, start_excl + 1, retimeEngine.insert_duration, retimeEngine.gap, @@ -179,7 +186,7 @@ def execute(self, context): retimeEngine, "DELETE", retimerApplyToSettings, - obj_list, + sceneObjs, start_excl + 1, duration_incl, True, @@ -199,7 +206,7 @@ def execute(self, context): retimeEngine, retimeEngine.mode, retimerApplyToSettings, - obj_list, + sceneObjs, start_excl, duration_incl, True, @@ -219,7 +226,7 @@ def execute(self, context): retimeEngine, retimeEngine.mode, retimerApplyToSettings, - obj_list, + sceneObjs, start_excl + 1, duration_incl, False, diff --git a/shotmanager/retimer/retimer_props.py b/shotmanager/retimer/retimer_props.py index 32e22db9..32c841fb 100644 --- a/shotmanager/retimer/retimer_props.py +++ b/shotmanager/retimer/retimer_props.py @@ -105,6 +105,13 @@ def createRenderSettings(self): self.applyToSettingsSelectedObjects.initialize("SELECTED_OBJECTS") self.applyToSettingsLegacy.initialize("LEGACY") + def getQuickHelp(self, topic): + if -1 != topic.find("APPLYTO_"): + retimerSettings = self.getCurrentApplyToSettings() + return retimerSettings.getQuickHelp(topic) + else: + return self.retimeEngine.getQuickHelp(topic) + _classes = ( UAS_Retimer_ApplyToSettings, diff --git a/shotmanager/retimer/retimer_ui.py b/shotmanager/retimer/retimer_ui.py index b1a6334c..fa132869 100644 --- a/shotmanager/retimer/retimer_ui.py +++ b/shotmanager/retimer/retimer_ui.py @@ -306,9 +306,7 @@ def _get_retime_frames_as_range(start, end): split.prop(retimerProps, "applyTo", text="") if prefs.retimer_applyTo_expanded: - retimerApplyToSettings = retimerProps.getCurrentApplyToSettings() - - drawApplyTo(context, retimerApplyToSettings, box) + drawApplyTo(context, retimerProps, box) # apply button ################ ############################### diff --git a/shotmanager/utils/utils.py b/shotmanager/utils/utils.py index 78a6d99e..359cf9f3 100644 --- a/shotmanager/utils/utils.py +++ b/shotmanager/utils/utils.py @@ -694,6 +694,20 @@ def getSceneVSE(vsm_sceneName, createVseTab=False): # Objects ################### +def getChildrenHierarchy(parentObject): + """Return a list with all the children - recursive - of the specified object""" + + allChildren = list() + + def _getChildrenHierarchyRec(parentObject): + for child in parentObject.children: + allChildren.append(child) + _getChildrenHierarchyRec(child) + + _getChildrenHierarchyRec(parentObject) + + return allChildren + def duplicateObject(sourceObject, newName=None, duplicateHierarchy=False): """Duplicate (deepcopy) an object and place it in the same collection diff --git a/shotmanager/utils/utils_storyboard.py b/shotmanager/utils/utils_storyboard.py new file mode 100644 index 00000000..f3a136fd --- /dev/null +++ b/shotmanager/utils/utils_storyboard.py @@ -0,0 +1,45 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Functions to manipulate Shot Manager storyboard entities +""" + +from . import utils + +# from shotmanager.config import config +from shotmanager.config import sm_logging + +_logger = sm_logging.getLogger(__name__) + + +def getStoryboardObjects(scene): + """Return a list of all the objects of the scene that are belonging to storyboard shots + This includes the shots cameras and all their hierarchy (empty, gp, children of all kinds), + this for all takes + """ + props = scene.UAS_shot_manager_props + + shotCameras = props.getCameras(fromAllTakes=True, ignoreDisabled=False, onlyShotsOfType="STORYBOARD") + allStbObjects = list() + allStbObjects.extend(shotCameras) + + for cam in shotCameras: + camChildren = utils.getChildrenHierarchy(cam) + allStbObjects.extend(camChildren) + + return allStbObjects diff --git a/shotmanager/utils/utils_ui.py b/shotmanager/utils/utils_ui.py index 2c6d23d9..96c2b30f 100644 --- a/shotmanager/utils/utils_ui.py +++ b/shotmanager/utils/utils_ui.py @@ -26,7 +26,7 @@ import subprocess import bpy -from bpy.types import Operator +from bpy.types import Operator, Panel from bpy.props import StringProperty # for file browser: @@ -126,14 +126,16 @@ def propertyColumn( return propCol -def labelBold(layout, text): - """Draw a label that is (slightly) brighter than the standard one""" +def labelBold(layout, text, scale_x=0.8, emboss=False): + """Draw a label that is (slightly) brighter than the standard one + Args: + emboss: used for debug""" # prefs = config.getShotManagerPrefs() row = layout.row(align=True) row.alignment = "LEFT" - row.scale_x = 0.8 + row.scale_x = scale_x # row.prop(prefs, "emptyBool", text=text, emboss=True, icon_only=True) - row.operator("uas.empty_operator", text=text, emboss=False, depress=False) + row.operator("uas.empty_operator", text=text, emboss=emboss, depress=False) def collapsable_panel( @@ -175,6 +177,67 @@ def collapsable_panel( return row +class UAS_PT_SM_QuickToolip(Panel): + bl_idname = "UAS_PT_SM_quicktooltip" + bl_label = "Shot Manager - Quick Tooltip" + bl_space_type = "VIEW_3D" + bl_region_type = "HEADER" + bl_ui_units_x = 20 + + title: StringProperty(default="") + message: StringProperty(default="") + path: StringProperty() + + @classmethod + def setTitle(self, text): + self.title = text + + # self.bl_ui_units_x = 190 + + @classmethod + def setMessage(self, text): + self.message = text + + def draw(self, context): + layout = self.layout + + if "" != self.title: + labelBold(layout, self.title, scale_x=1.0) + + messages = self.message.split("\n") + propsCol = propertyColumn(layout, padding_left=0, padding_bottom=0, scale_y=0.8) + + for s in messages: + propsCol.label(text=s) + + +def quickTooltip(layout, message, title="", text="", icon="INFO", alert=False): + """Draw a quick tooltip component + A message can be drawn on several lines when containing the separator \n + """ + + UAS_PT_SM_QuickToolip.setTitle(title) + UAS_PT_SM_QuickToolip.setMessage(message) + + # get the panel to change its unit_x + popPanelCls = getattr(bpy.types, "UAS_PT_SM_quicktooltip") + # # print(popPanelCls) + # popPanelCls.bl_ui_units_x = 155 + # # print(popPanelCls.bl_ui_units_x) + # popPanelCls.bl_label = "toto" + + row = layout.row() + + # doesn't work on the popover component :S + row.alert = alert + + popIcon = "COLORSET_01_VEC" if alert else icon + + # row.label(text="ttt") + + row.popover(panel="UAS_PT_SM_quicktooltip", text=text, icon=popIcon) + + ################### # Open doc and explorers ################### @@ -410,7 +473,9 @@ def execute(self, context): _classes = ( + UAS_PT_SM_QuickToolip, UAS_ShotManager_OpenExplorer, + # UAS_SM_QuickTooltip, UAS_SM_Open_Documentation_Url, UAS_ShotManager_OpenFileBrowser, UAS_ShotManager_OT_Querybox, From a9cc8211c032b4b34ca9382a2fde5d5d7b7d7021 Mon Sep 17 00:00:00 2001 From: julien blervaque Date: Sat, 27 Aug 2022 09:10:32 +0200 Subject: [PATCH 02/27] preparing linking --- shotmanager/addon_prefs/addon_prefs.py | 11 ++ shotmanager/config/config.py | 5 +- shotmanager/operators/shots.py | 6 + .../interact_shots_stack/shots_stack.py | 1 + .../interact_shots_stack/shots_stack_prefs.py | 2 + .../widgets/shots_stack_widget.py | 5 + shotmanager/prefs/__init__.py | 3 + shotmanager/prefs/prefs.py | 110 ------------ shotmanager/prefs/prefs_shots_display.py | 157 ++++++++++++++++++ shotmanager/properties/shot.py | 19 ++- shotmanager/utils/utils_editors_dopesheet.py | 5 +- 11 files changed, 210 insertions(+), 114 deletions(-) create mode 100644 shotmanager/prefs/prefs_shots_display.py diff --git a/shotmanager/addon_prefs/addon_prefs.py b/shotmanager/addon_prefs/addon_prefs.py index 839b1dff..821d7133 100644 --- a/shotmanager/addon_prefs/addon_prefs.py +++ b/shotmanager/addon_prefs/addon_prefs.py @@ -456,6 +456,11 @@ def _set_stb_global_visibility(self, value): description="Automatically zoom the timeline content to frame the shot when the current shot is changed.\n(Add-on preference)", default=False, ) + current_shot_select_stb_frame: BoolProperty( + name="Select Storyboard Frame of the Current Short", + description="Automatically select the storyboard frame (= grease pencil) of the shot when the current shot is changed.\n(Add-on preference)", + default=True, + ) # current_shot_changes_edited_frame_in_stb: BoolProperty( # name="Set selected shot to edited", # description="When a shot is selected in the shot list, in Storyboard layout mode, and another one is being edited, then" @@ -930,6 +935,12 @@ def _update_display_shtStack_toolbar(self, context): update=_update_display_shtStack_toolbar, default=True, ) + + shtStack_link_stb_clips_to_keys: BoolProperty( + name="Link Storyboard Clips to Keyframes", + description="Link the Storyboard shot clips ", + default=True, + ) shtStack_opacity: FloatProperty( name="Shots Stack Opacity", diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index efa79ac3..aeef40a8 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -43,7 +43,7 @@ def initGlobalVariables(): devDebug = False # change this value to force debug at start time - devDebug = False + devDebug = True global devDebug_lastRedrawTime devDebug_lastRedrawTime = -1 @@ -101,6 +101,9 @@ def initGlobalVariables(): global gModulePath gModulePath = None + global gShotsStack_ui_scale + gShotsStack_ui_scale = 1.25 + # dependencies ############# global STAMP_INFO_MIN_VERSION STAMP_INFO_MIN_VERSION = ("1.3.5", 1003005) diff --git a/shotmanager/operators/shots.py b/shotmanager/operators/shots.py index f70023a8..0144bf6e 100644 --- a/shotmanager/operators/shots.py +++ b/shotmanager/operators/shots.py @@ -219,6 +219,12 @@ def _updateEditors(changeTime=True, zoom_mode=""): context, shot.start, shot.end, changeTime=prefs.current_shot_changes_current_time_to_start ) + if prefs.current_shot_select_stb_frame: + if "STORYBOARD" == shot.shotType: + gpChild = shot.getStoryboardFrame() + if gpChild: + utils.select_object(gpChild) + # change shot if not self.event_ctrl: props.setCurrentShotByIndex( diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py index 7bccbc94..7e2453e0 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py @@ -201,6 +201,7 @@ def modal(self, context, event): props = context.scene.UAS_shot_manager_props prefs = config.getShotManagerPrefs() + event_handled = False # _logger.debug_ext(f"uas_shot_manager.interactive_shots_stack Modal", col="PURPLE") # if event.type not in ["TIMER", "MOUSEMOVE"]: diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py index c1bc07a3..245727dd 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py @@ -42,6 +42,8 @@ def draw_settings(context, layout): text="Compact Shots Display (= decrease visual stack height)", ) + propCol.prop(prefs, "shtStack_link_stb_clips_to_keys") + # def draw_settings_in_menu(self, context): # """Used in Shot Manager Feature Toggles panel diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py index 2010622a..7f330c8b 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py @@ -68,6 +68,7 @@ def __init__(self, target_area=None): self.manipulated_clip = None self.manipulated_clip_handle = None + self.manipulated_clip_objs = None self.prev_mouse_x = 0 self.prev_mouse_y = 0 @@ -464,6 +465,7 @@ def validateAction(self): self.manipulated_clip.highlight = False self.manipulated_clip = None self.manipulated_clip_handle = None + self.manipulated_clip_objs = None def cancelAction(self): # TODO restore the initial @@ -472,6 +474,7 @@ def cancelAction(self): self.manipulated_clip.highlight = False self.manipulated_clip = None self.manipulated_clip_handle = None + self.manipulated_clip_objs = None def handle_event(self, context, event, region): """Return True if the event is handled for BL_UI_ShotStack""" @@ -537,6 +540,8 @@ def handle_event(self, context, event, region): # active clip ################## self.manipulated_clip = uiShot self.manipulated_clip_handle = manipulated_clip_handle + # if uiShot.shot.isStoryboardType() + # self.manipulated_clip_objs = getChildren self.mouseFrame = int(region.view2d.region_to_view(event.mouse_x - region.x, 0)[0]) self.previousMouseFrame = self.mouseFrame diff --git a/shotmanager/prefs/__init__.py b/shotmanager/prefs/__init__.py index e4c63362..de9d95c8 100644 --- a/shotmanager/prefs/__init__.py +++ b/shotmanager/prefs/__init__.py @@ -28,6 +28,7 @@ from . import prefs_overlay_tools from . import prefs_project from . import prefs_sequence +from . import prefs_shots_display from . import prefs_tools from . import prefs @@ -48,6 +49,7 @@ def register(): prefs_overlay_tools.register() prefs_project.register() prefs_sequence.register() + prefs_shots_display.register() prefs_tools.register() prefs.register() @@ -63,6 +65,7 @@ def unregister(): prefs.unregister() prefs_tools.unregister() + prefs_shots_display.unregister() prefs_sequence.unregister() prefs_project.unregister() prefs_overlay_tools.unregister() diff --git a/shotmanager/prefs/prefs.py b/shotmanager/prefs/prefs.py index 85b28dad..6cb9db93 100644 --- a/shotmanager/prefs/prefs.py +++ b/shotmanager/prefs/prefs.py @@ -86,115 +86,6 @@ def draw(self, context): col.label(text="PlaceHolder") -class UAS_ShotManager_Shots_Prefs(Operator): - bl_idname = "uas_shot_manager.shots_prefs" - bl_label = "Shots Display Settings" - bl_description = "Display the Shots Settings panel\nfor the Shot Manager instanced in this scene" - bl_options = {"INTERNAL"} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self, width=480) - - def draw(self, context): - props = context.scene.UAS_shot_manager_props - prefs = config.getShotManagerPrefs() - - layout = self.layout - - layout.alert = True - layout.label(text="Any change is effective immediately") - layout.alert = False - - # Shot List - ############## - layout.label(text="Shot List:") - box = layout.box() - box.use_property_decorate = False - - # main column, to allow title offset - maincol = box.column() - row = maincol.row() - row.separator(factor=2) - row.label(text="Display:") - - # empty spacer column - row = maincol.row() - col = row.column() - col.scale_x = 0.4 - col.label(text=" ") - col = row.column() - - col.separator(factor=0.5) - col.use_property_split = False - col.prop(props, "display_selectbut_in_shotlist", text="Display Camera Select Button") - col.prop(props, "display_enabled_in_shotlist", text="Display Enabled State") - col.prop(props, "display_getsetcurrentframe_in_shotlist", text="Display Get/Set current Frame Buttons") - - col.prop(props, "display_cameraBG_in_shotlist") - col.prop(props, "display_greasepencil_in_shotlist") - - col.separator(factor=1.7) - col.prop(props, "highlight_all_shot_frames", text="Highlight Framing Values When Equal to Current Time") - col.prop(props, "display_duration_after_time_range", text="Display Shot Duration After Time Range") - col.prop(props, "use_camera_color", text="Use Camera Color for Shots ") - - col.use_property_split = False - col.separator(factor=1.7) - row = col.row() - row.label(text="Display Shot Properties Mode:") - row.prop(props, "current_shot_properties_mode", text="") - row.separator() - - # User Prefs at addon level - ############### - - box.separator(factor=0.5) - # main column, to allow title offset - maincol = box.column() - row = maincol.row() - row.separator(factor=2) - row.label(text="When Current Shot Is Changed: (Settings stored in the Add-on Preferences):") - - # empty spacer column - row = maincol.row() - col = row.column() - col.scale_x = 0.28 - col.label(text=" ") - col = row.column() - - # col.separator(factor=1.0) - # col.label(text="Time Change:") - # col.label(text="User Preferenes (in Preference Add-on Window):") - col.separator(factor=0.5) - col.use_property_split = False - col.prop( - prefs, - "current_shot_changes_current_time_to_start", - text="Set Current Frame To Shot Start", - ) - col.prop( - prefs, - "current_shot_changes_time_range", - text="Set Scene Animation Range To Shot Range", - ) - col.prop( - prefs, - "current_shot_changes_time_zoom", - text="Zoom Timeline Content To Frame The Current Shot", - ) - - # propsCol.prop( - # prefs, - # "current_shot_changes_edited_frame_in_stb", - # text="Storyboard Shots List: Set Selected Shot to Edited One", - # ) - - layout.separator(factor=2) - - def execute(self, context): - return {"FINISHED"} - - # class UAS_PT_ShotManager_Render_StampInfoProperties(Panel): # bl_label = "Stamp Info Properties" # bl_idname = "UAS_PT_Shot_Manager_Pref_StampInfoPrefs" @@ -231,7 +122,6 @@ def execute(self, context): _classes = ( UAS_ShotManager_General_Prefs, # UAS_PT_ShotManagerPref_General, - UAS_ShotManager_Shots_Prefs, # UAS_PT_ShotManager_Render_StampInfoProperties, ) diff --git a/shotmanager/prefs/prefs_shots_display.py b/shotmanager/prefs/prefs_shots_display.py new file mode 100644 index 00000000..72691046 --- /dev/null +++ b/shotmanager/prefs/prefs_shots_display.py @@ -0,0 +1,157 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Shot template list preferences +""" + +import bpy +from bpy.types import Operator + +from shotmanager.utils.utils_ui import propertyColumn +from shotmanager.config import config + +############# +# Preferences +############# + + +class UAS_ShotManager_Shots_Prefs(Operator): + bl_idname = "uas_shot_manager.shots_prefs" + bl_label = "Shots Display Settings" + bl_description = "Display the Shots Settings panel\nfor the Shot Manager instanced in this scene" + bl_options = {"INTERNAL"} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=480) + + def draw(self, context): + props = context.scene.UAS_shot_manager_props + prefs = config.getShotManagerPrefs() + + layout = self.layout + + layout.alert = True + layout.label(text="Any change is effective immediately") + layout.alert = False + + # Shot List + ############## + layout.label(text="Shot List:") + box = layout.box() + box.use_property_decorate = False + + # main column, to allow title offset + maincol = box.column() + row = maincol.row() + row.separator(factor=2) + row.label(text="Display:") + + # empty spacer column + row = maincol.row() + col = row.column() + col.scale_x = 0.4 + col.label(text=" ") + col = row.column() + + col.separator(factor=0.5) + col.use_property_split = False + col.prop(props, "display_selectbut_in_shotlist", text="Display Camera Select Button") + col.prop(props, "display_enabled_in_shotlist", text="Display Enabled State") + col.prop(props, "display_getsetcurrentframe_in_shotlist", text="Display Get/Set current Frame Buttons") + + col.prop(props, "display_cameraBG_in_shotlist") + col.prop(props, "display_greasepencil_in_shotlist") + + col.separator(factor=1.7) + col.prop(props, "highlight_all_shot_frames", text="Highlight Framing Values When Equal to Current Time") + col.prop(props, "display_duration_after_time_range", text="Display Shot Duration After Time Range") + col.prop(props, "use_camera_color", text="Use Camera Color for Shots ") + + col.use_property_split = False + col.separator(factor=1.7) + row = col.row() + row.label(text="Display Shot Properties Mode:") + row.prop(props, "current_shot_properties_mode", text="") + row.separator() + + # User Prefs at addon level + ############### + + box.separator(factor=0.5) + # main column, to allow title offset + maincol = box.column() + row = maincol.row() + row.separator(factor=2) + row.label(text="When Current Shot Is Changed: (Settings stored in the Add-on Preferences):") + + # col.scale_x = 0.28 + + propsCol = propertyColumn(maincol, padding_left=7) + + # col.separator(factor=1.0) + # col.label(text="Time Change:") + # col.label(text="User Preferenes (in Preference Add-on Window):") + # col.use_property_split = False + propsCol.prop( + prefs, + "current_shot_changes_current_time_to_start", + text="Set Current Frame To Shot Start", + ) + propsCol.prop( + prefs, + "current_shot_changes_time_range", + text="Set Scene Animation Range To Shot Range", + ) + propsCol.prop( + prefs, + "current_shot_changes_time_zoom", + text="Zoom Timeline Content To Frame The Current Shot", + ) + + # propsCol.prop( + # prefs, + # "current_shot_changes_edited_frame_in_stb", + # text="Storyboard Shots List: Set Selected Shot to Edited One", + # ) + + propsCol.separator(factor=0.5) + propsCol.label(text="Storyboard Shots:") + propsCol.prop( + prefs, + "current_shot_select_stb_frame", + text="Select Storyboard Frame of the Current Short", + ) + + layout.separator(factor=2) + + def execute(self, context): + return {"FINISHED"} + + +_classes = (UAS_ShotManager_Shots_Prefs,) + + +def register(): + + for cls in _classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in reversed(_classes): + bpy.utils.unregister_class(cls) diff --git a/shotmanager/properties/shot.py b/shotmanager/properties/shot.py index 5181b6cd..4aa5e211 100644 --- a/shotmanager/properties/shot.py +++ b/shotmanager/properties/shot.py @@ -663,7 +663,7 @@ def hasGreasePencil(self): return False # wkip to update with the gp list - def getGreasePencilObject(self, mode): + def getGreasePencilObject(self, mode="STORYBOARD"): """Set the grease pencil object of the specified mode associated to the camera. Return the created - or corresponding if one existed - grease pencil object, or None if the camera was invalid Args: @@ -756,6 +756,23 @@ def _ClearParent(child): self.shotType = "PREVIZ" + ############# + # storyboard + ############# + + def isStoryboardType(self): + return 'S' == self.shotType[0] + + # wkip to update with the gp list + def getStoryboardFrame(self): + """Set the grease pencil object of the specified mode associated to the camera. + Return the created - or corresponding if one existed - grease pencil object, or None if the camera was invalid + Args: + mode: can be "STORYBOARD" + """ + return self.getGreasePencilObject(mode="STORYBOARD") + + ############# # notes ##### ############# diff --git a/shotmanager/utils/utils_editors_dopesheet.py b/shotmanager/utils/utils_editors_dopesheet.py index cd8450c1..23ca3a4f 100644 --- a/shotmanager/utils/utils_editors_dopesheet.py +++ b/shotmanager/utils/utils_editors_dopesheet.py @@ -34,6 +34,7 @@ import bpy from shotmanager.utils.utils import clamp +from shotmanager.config import config def getRegionFrameRange(context, targetArea, inViewUnits=True): @@ -92,14 +93,14 @@ def getPrefsUIScale(): def getRulerHeight(): """Return the height in pixels of the time ruler of a dopesheet""" - RULER_HEIGHT = 23 + RULER_HEIGHT = 23 * config.gShotsStack_ui_scale # RULER_HEIGHT = 28 # on laptop return RULER_HEIGHT * getPrefsUIScale() def getLaneHeight(): """Return the height of a lane in pixels""" - LANE_HEIGHT = 18 + LANE_HEIGHT = 18 * config.gShotsStack_ui_scale # LANE_HEIGHT = 18.5 # LANE_HEIGHT = 22.5 # on laptop return LANE_HEIGHT * getPrefsUIScale() From 9785013607c7570bfdeb80418e253fcbd3cb15ba Mon Sep 17 00:00:00 2001 From: julien blervaque Date: Sat, 27 Aug 2022 21:26:42 +0200 Subject: [PATCH 03/27] Removed old ShotStack and rename widget --- .../doc_interac_shots_stack.md | 2 +- .../interact_shots_stack/shots_stack.py | 4 +- .../widgets/shot_clip_widget.py | 336 ------------------ .../widgets/shots_stack_widget.py | 260 +------------- 4 files changed, 9 insertions(+), 593 deletions(-) delete mode 100644 shotmanager/overlay_tools/interact_shots_stack/widgets/shot_clip_widget.py diff --git a/shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md b/shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md index dcd6329d..0bad7e81 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md +++ b/shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md @@ -4,7 +4,7 @@ | | |_ shots_stack_widget - Main graphic widget: holds the events on the widget and its components, - (BL_UI_ShotStack) as well as the draw functions + (ShotStackWidget) as well as the draw functions | | | diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py index 7e2453e0..8d36035c 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py @@ -20,7 +20,7 @@ """ -from .widgets.shots_stack_widget import BL_UI_ShotStack +from .widgets.shots_stack_widget import ShotStackWidget import bpy from bpy.types import Operator @@ -127,7 +127,7 @@ def invoke(self, context, event): _logger.debug_ext("Canceled op uas_shot_manager.interactive_shots_stack area", col="PURPLE") return {"CANCELLED"} else: - self.init_widgets(context, [BL_UI_ShotStack(target_area=self.target_area)]) + self.init_widgets(context, [ShotStackWidget(target_area=self.target_area)]) args = (self, context, self.widgets) self.register_handlers(args, context) diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shot_clip_widget.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shot_clip_widget.py deleted file mode 100644 index 0f7e246b..00000000 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shot_clip_widget.py +++ /dev/null @@ -1,336 +0,0 @@ -# GPLv3 License -# -# Copyright (C) 2021 Ubisoft -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -UI in BGL for the Interactive Shots Stack overlay tool -""" - -import bpy -import bgl -import blf -import gpu -from mathutils import Vector - -from shotmanager.utils.utils import color_to_sRGB, color_is_dark, lighten_color -from shotmanager.gpu.gpu_2d.class_Mesh2D import build_rectangle_mesh -from ..shots_stack_bgl import ( - get_lane_origin_y, - get_lane_height, - get_prefs_ui_scale, -) - -# from shotmanager.config import config -from shotmanager.config import sm_logging - -_logger = sm_logging.getLogger(__name__) - -UNIFORM_SHADER_2D = gpu.shader.from_builtin("2D_UNIFORM_COLOR") - - -class BL_UI_ShotClip: - def __init__(self, context, shot_index): - """ - shot_index is the index of the shot in the whole take list - """ - self.context = context - - self.origin = None - self.width = 0 - self.font_size = 12 - - self.active_handle = None - self.active_clip_over = False - self.highlight = False - self.mouseover = False - - # self.prev_mouse_x = 0 - # self.prev_mouse_y = 0 - self.prev_click = 0 - - self.clip_mesh = None - self.contour_mesh = None - self.contourCurrent_mesh = None - self.start_interaction_mesh = None - self.end_interaction_mesh = None - - self.color_currentShot_border = (0.92, 0.55, 0.18, 0.99) - self.color_currentShot_border_mix = (0.94, 0.3, 0.1, 0.99) - - self._shot_index = shot_index - self._name_color_light = (0.9, 0.9, 0.9, 1) - self._name_color_dark = (0.07, 0.07, 0.07, 1) - self._name_color_disabled = (0.6, 0.6, 0.6, 1) - - self._shot_color = (0.8, 0.3, 0.3, 1.0) - self._shot_color_disabled = (0.1, 0.1, 0.1, 0.5) - - # self.color_selectedShot_border = (0.9, 0.9, 0.2, 0.99) - # self.color_selectedShot_border = (0.2, 0.2, 0.2, 0.99) # dark gray - self.color_selectedShot_border = (0.95, 0.95, 0.95, 0.9) # white - - # self.update() - - def update(self, lane): - props = bpy.context.scene.UAS_shot_manager_props - shots = props.get_shots() - shot = shots[self._shot_index] - - self.lane = lane - self.width = shot.end - shot.start + 1 - self.origin = Vector([shot.start, get_lane_origin_y(self.lane)]) - self.clip_mesh = build_rectangle_mesh(self.origin, self.width, self.height) - self.start_interaction_mesh = build_rectangle_mesh(self.origin, 1, self.height) - self.end_interaction_mesh = build_rectangle_mesh(self.origin + Vector([self.width - 1, 0]), 1, self.height) - self.contour_mesh = build_rectangle_mesh(self.origin, self.width, self.height, True) - self.contourCurrent_mesh = build_rectangle_mesh(self.origin, self.width, self.height, True) - # self.contourCurrent_mesh = build_rectangle_mesh( - # Vector([self.origin.x - 1, self.origin.y - 1]), self.width + 2, self.height + 2, True - # ) - - @property - def height(self): - return get_lane_height() - - @property - def shot_index(self): - return self._shot_index - - @shot_index.setter - def shot_index(self, value): - self._shot_index = value - - @property - def shot_color(self): - return self._shot_color - - @shot_color.setter - def shot_color(self, value): - self._shot_color = (value[0], value[1], value[2], 0.5) - - def draw(self): - context = self.context - props = context.scene.UAS_shot_manager_props - shots = props.get_shots() - shot = shots[self._shot_index] - - bgl.glEnable(bgl.GL_BLEND) - UNIFORM_SHADER_2D.bind() - - self.shot_color = shot.color - color = color_to_sRGB(self.shot_color) - - if not shot.enabled: - color = self._shot_color_disabled - # color = (0.15, 0.15, 0.15, 0.5) - - if self.highlight: - # _logger.debug_ext("highlight Shot in draw", col="RED", tag="SHOTSTACK_EVENT") - color = (0.9, 0.9, 0.9, 0.5) - if self.mouseover: - # _logger.debug_ext(f"mouseover Shot in draw {shot.name}", col="RED", tag="SHOTSTACK_EVENT") - color = lighten_color(color, 0.2) - - UNIFORM_SHADER_2D.uniform_float("color", color) - self.clip_mesh.draw(UNIFORM_SHADER_2D, context.region) - - # handles highlight - if self.active_clip_over and self.highlight and 0 != self.active_handle: - # left handle - if -1 == self.active_handle: - self.end_interaction_mesh.draw(UNIFORM_SHADER_2D, context.region) - color = (0.9, 0.0, 0.0, 0.5) - UNIFORM_SHADER_2D.uniform_float("color", color) - self.start_interaction_mesh.draw(UNIFORM_SHADER_2D, context.region) - # right handle - elif 1 == self.active_handle: - self.start_interaction_mesh.draw(UNIFORM_SHADER_2D, context.region) - color = (0.9, 0.0, 0.0, 0.5) - UNIFORM_SHADER_2D.uniform_float("color", color) - self.end_interaction_mesh.draw(UNIFORM_SHADER_2D, context.region) - else: - self.start_interaction_mesh.draw(UNIFORM_SHADER_2D, context.region) - self.end_interaction_mesh.draw(UNIFORM_SHADER_2D, context.region) - - # current_shot = props.getCurrentShot() - # selected_shot = props.getSelectedShot() - current_shot_ind = props.getCurrentShotIndex() - selected_shot_ind = props.getSelectedShotIndex() - - # current shot - # if current_shot != -1 and self.name == current_shot.name: - if self.shot_index == current_shot_ind: - UNIFORM_SHADER_2D.uniform_float("color", self.color_currentShot_border) - self.contourCurrent_mesh.lineThickness = 4 if current_shot_ind == selected_shot_ind else 2 - self.contourCurrent_mesh.draw(UNIFORM_SHADER_2D, context.region, "LINES") - - # selected shot - # if current_shot != -1 and self.name == selected_shot.name: - if self.shot_index == selected_shot_ind: - UNIFORM_SHADER_2D.uniform_float("color", self.color_selectedShot_border) - self.contour_mesh.lineThickness = 1 if current_shot_ind == selected_shot_ind else 2 - self.contour_mesh.draw(UNIFORM_SHADER_2D, context.region, "LINES") - - # draw shot name - ########################## - bgl.glDisable(bgl.GL_BLEND) - - if shot.enabled: - if color_is_dark(color, 0.4): - blf.color(0, *self._name_color_light) - else: - blf.color(0, *self._name_color_dark) - else: - blf.color(0, *self._name_color_disabled) - - blf.size(0, round(self.font_size * get_prefs_ui_scale()), 72) - textPos_y = self.origin.y + 6 * get_prefs_ui_scale() - textPos_y = self.origin.y + get_lane_height() * 0.2 * get_prefs_ui_scale() - blf.position(0, *context.region.view2d.view_to_region(self.origin.x + 1.4, textPos_y), 0) - blf.draw(0, shot.name) - - def get_clip_handle(self, x, y): - """ - Return the handle of the clip the mouse is on: -1 for start, 0 for move, 1 for end. None otherwise - """ - props = bpy.context.scene.UAS_shot_manager_props - shots = props.get_shots() - shot = shots[self._shot_index] - - if shot.start <= x < shot.end + 1 and self.origin.y <= y < self.origin.y + self.height: - # Test order is important for the case of start and end are the same. We want to prioritize moving the end. - if shot.end <= x < shot.end + 1: - return 1 - elif shot.start <= x < shot.start + 1: - return -1 - else: - return 0 - - return None - - # TODO: wkip undo doesn't work here !!! - def handle_mouse_interaction(self, handle, mouse_disp): - """ - handle: if handle == -1: left clip handle (start) - if handle == 0: clip (start and end) - if handle == 1: right clip handle (end) - """ - # from shotmanager.properties.shot import UAS_ShotManager_Shot - - # bpy.ops.ed.undo_push(message=f"Set Shot Start...") - - props = bpy.context.scene.UAS_shot_manager_props - shots = props.get_shots() - shot = shots[self._shot_index] - # !! we have to be sure we work on the selected shot !!! - if handle == 1: - shot.end += mouse_disp - elif handle == -1: - shot.start += mouse_disp - # bpy.ops.uas_shot_manager.set_shot_start(newStart=self.start + mouse_disp) - else: - # Very important, don't use properties for changing both start and ends. Depending of the amount of displacement duration can change. - if shot.durationLocked: - if mouse_disp > 0: - shot.end += mouse_disp - # shot.start += mouse_disp - else: - shot.start += mouse_disp - # shot.end += mouse_disp - else: - if mouse_disp > 0: - shot.end += mouse_disp - shot.start += mouse_disp - else: - shot.start += mouse_disp - shot.end += mouse_disp - - def handle_event(self, context, event, region): - """Return True if the event is handled for BL_UI_ShotStack - - Notes: - - self.mouseover is not working perfectly since the over color is left on the last - overed shot when the mouse is out of every shots. This is because when leaving shots - the event is not returned as being handled. - """ - - event_handled = False - - # ######## - # # REMOVED - # ######## - - # context = self.context - # props = context.scene.UAS_shot_manager_props - # shots = props.get_shots() - # shot = shots[self._shot_index] - - # # self.mouseover = True - - # # if event.type not in ["MOUSEMOVE", "INBETWEEN_MOUSEMOVE", "TIMER"]: - # _logger.debug_ext(f" *** event in BL_UI_Shot: {event.type}", col="GREEN", tag="SHOTSTACK_EVENT") - - # mouse_x, mouse_y = region.view2d.region_to_view(event.mouse_x - region.x, event.mouse_y - region.y) - # active_clip_region = self.get_clip_handle(mouse_x, mouse_y) - - # # _logger.debug_ext(f"over Shot {shot.name} active_clip_region: {active_clip_region}", col="RED") - # if active_clip_region is not None: - - # # mouse over ################# - # # NOTE: mouseover works but is not used (= desactivated in draw function) because it has to be associated - # # with a redraw when no events are handle, which is hardware greedy (moreover reactive components are not - # # in the philosophy of Blender) - # self.mouseover = True - - # if event.type in ["MOUSEMOVE", "INBETWEEN_MOUSEMOVE"]: - - # # _logger.debug_ext(f"over Shot {shot.name}", col="RED") - # # self.mouseover = True - # self.highlight = True - # # config.gShotsStackInfos["active_clip_index"] = i - # # config.gShotsStackInfos["active_clip_region"] = active_clip_region - # # self.active_clip = clip - # # self.active_clip_region = active_clip_region - # # props = context.scene.UAS_shot_manager_props - # # props.setSelectedShotByIndex(self.shot_index) - - # # event_handled = True - # # else: - # # self.mouseover = False - # # pass - # pass - - # elif event.type == "LEFTMOUSE": - # if event.value == "PRESS": - # props.setSelectedShotByIndex(self.shot_index) - - # # double click - # counter = time.perf_counter() - # print(f"pref clic: {self.prev_click}") - # if counter - self.prev_click < 0.3: # Double click. - # props.setCurrentShotByIndex(self.shot_index, changeTime=False) - # mouse_frame = int(region.view2d.region_to_view(event.mouse_x - region.x, 0)[0]) - # context.scene.frame_current = mouse_frame - # # bpy.ops.uas_shot_manager.set_current_shot(index=self.shot_index) - # self.prev_click = counter - # event_handled = True - - # else: - # self.highlight = False - # self.mouseover = False - # pass - - return event_handled diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py index 7f330c8b..09a97a8b 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py @@ -22,22 +22,20 @@ from collections import defaultdict import os -import time from mathutils import Vector -import bpy import bgl import gpu from shotmanager.overlay_tools.interact_shots_stack.widgets.shots_stack_clip_component import ShotClipComponent -from .shot_clip_widget import BL_UI_ShotClip from ..shots_stack_bgl import get_lane_origin_y from shotmanager.utils import utils_editors_dopesheet from shotmanager.utils.utils import color_to_linear -from shotmanager.gpu.gpu_2d.class_Mesh2D import Mesh2D, build_rectangle_mesh +# from shotmanager.gpu.gpu_2d.class_Mesh2D import Mesh2D +from shotmanager.gpu.gpu_2d.class_Mesh2D import build_rectangle_mesh from shotmanager.gpu.gpu_2d.class_QuadObject import QuadObject from shotmanager.gpu.gpu_2d.class_Component2D import Component2D from shotmanager.gpu.gpu_2d.class_Text2D import Text2D @@ -53,12 +51,11 @@ UNIFORM_SHADER_2D = gpu.shader.from_builtin("2D_UNIFORM_COLOR") -class BL_UI_ShotStack: +class ShotStackWidget: def __init__(self, target_area=None): prefs = config.getShotManagerPrefs() self.useDebugComponents = False - self.use_shots_old_way = False self.context = None self.target_area = target_area @@ -335,81 +332,6 @@ def drawShots_compactMode(self, preDrawOnly=False): # draw quad for current shot over the result self.drawCurrentShotDecoration(shotCompoCurrent, preDrawOnly=preDrawOnly) - def draw_shots_old_way(self): - props = self.context.scene.UAS_shot_manager_props - shots = props.get_shots() - ui_shots_previous = [] - - def _getUIShotFromShotIndex(shot_index): - """Return the instance of BL_UI_ShotClip in ui_shots_previous that uses the - specified shot index or None if not found""" - for s in ui_shots_previous: - if shot_index == s.shot_index: - return s - return None - - # create an array of tupples (ind, shot) to keep the association between the shot and its position - shotTupples = [] - for i, shot in enumerate(shots): - shotTupples.append((i, shot)) - - ui_shots_previous = self.ui_shots.copy() - self.ui_shots.clear() - # print(f"num items in: self.ui_shots: {len(self.ui_shots)}, ui_shots_previous: {len(ui_shots_previous)}") - - if props.interactShotsStack_displayInCompactMode: - shotTupplesSorted = sorted( - shotTupples, - key=lambda shotTupple: shotTupple[1].start, - ) - # print(f"Tupples sorted: {shotTupplesSorted}") - shots_from_lane = defaultdict(list) - - for ind, shotTupple in enumerate(shotTupplesSorted): - shot = shotTupple[1] - if not props.interactShotsStack_displayDisabledShots and not shot.enabled: - continue - lane = 0 - if ind > 0: - for ln, shots_in_lane in shots_from_lane.items(): - for s in shots_in_lane: - if s.start <= shot.start <= s.end: - break - else: - lane = ln - break - else: - if len(shots_from_lane): - lane = max(shots_from_lane) + 1 # No free lane, make a new one. - else: - # lane = ln - pass - shots_from_lane[lane].append(shot) - - s = _getUIShotFromShotIndex(shotTupple[0]) - if s is None: - s = BL_UI_ShotClip(self.context, shotTupple[0]) - s.update(lane + 10) - self.ui_shots.append(s) - s.draw() - else: - shots = props.get_shots() - lane = -1 - for i, shot in enumerate(shots): - if not props.interactShotsStack_displayDisabledShots and not shot.enabled: - continue - lane += 1 - - s = _getUIShotFromShotIndex(i) - if s is None: - s = BL_UI_ShotClip(self.context, i) - # debug: - s.update(lane + 10) - # s.update(lane) - - self.ui_shots.append(s) - s.draw() - def draw(self, preDrawOnly=False): if self.target_area is not None and self.context.area != self.target_area: return @@ -456,9 +378,6 @@ def draw(self, preDrawOnly=False): else: self.drawShots(preDrawOnly=preDrawOnly) - if self.use_shots_old_way: - self.draw_shots_old_way() - def validateAction(self): _logger.debug_ext("Validating Shot Stack action", col="GREEN", tag="SHOTSTACK_EVENT") if self.manipulated_clip: @@ -477,16 +396,16 @@ def cancelAction(self): self.manipulated_clip_objs = None def handle_event(self, context, event, region): - """Return True if the event is handled for BL_UI_ShotStack""" + """Return True if the event is handled for ShotStackWidget""" prefs = config.getShotManagerPrefs() - # _logger.debug_ext("*** handle event for BL_UI_ShotStack", col="GREEN", tag="SHOTSTACK_EVENT") + # _logger.debug_ext("*** handle event for ShotStackWidget", col="GREEN", tag="SHOTSTACK_EVENT") if not context.window_manager.UAS_shot_manager_toggle_shots_stack_interaction: return False event_handled = False # if event.type not in ["MOUSEMOVE", "INBETWEEN_MOUSEMOVE", "TIMER"]: - # _logger.debug_ext(f" *** event in BL_UI_ShotStack: {event.type}", col="GREEN", tag="SHOTSTACK_EVENT") + # _logger.debug_ext(f" *** event in ShotStackWidget: {event.type}", col="GREEN", tag="SHOTSTACK_EVENT") context = self.context props = context.scene.UAS_shot_manager_props @@ -505,173 +424,6 @@ def handle_event(self, context, event, region): if event_handled: break - if self.use_shots_old_way: - # if True and not event_handled: - if "PRESS" == event.value and event.type in ("RIGHTMOUSE", "ESC", "WINDOW_DEACTIVATE"): - self.cancelAction() - event_handled = True - else: - for uiShot in self.ui_shots: - # if uiShot.handle_event(context, event, region): - # event_handled = True - # break - manipulated_clip_handle = uiShot.get_clip_handle(mouse_x, mouse_y) - uiShot.mouseover = False - - if manipulated_clip_handle is not None: - - # mouse over ################# - # NOTE: mouseover works but is not used (= desactivated in draw function) because it has to be associated - # with a redraw when no events are handle, which is hardware greedy (moreover reactive components are not - # in the philosophy of Blender) - - # self.previousDrawWasInAClip = True - currentDrawIsInAClip = True - uiShot.mouseover = True - # event_handled = True - # config.gRedrawShotStack = True - - if event.type == "LEFTMOUSE": - if event.value == "PRESS": - prefs.shot_selected_from_shots_stack__flag = True - props.setSelectedShotByIndex(uiShot.shot_index) - prefs.shot_selected_from_shots_stack__flag = False - - # active clip ################## - self.manipulated_clip = uiShot - self.manipulated_clip_handle = manipulated_clip_handle - # if uiShot.shot.isStoryboardType() - # self.manipulated_clip_objs = getChildren - self.mouseFrame = int(region.view2d.region_to_view(event.mouse_x - region.x, 0)[0]) - self.previousMouseFrame = self.mouseFrame - - # double click ################# - counter = time.perf_counter() - print(f"pref clic: {uiShot.prev_click}") - if counter - uiShot.prev_click < 0.3: # Double click. - # props.setCurrentShotByIndex(uiShot.shot_index, changeTime=False) - mouse_frame = int(region.view2d.region_to_view(event.mouse_x - region.x, 0)[0]) - # context.scene.frame_current = mouse_frame - bpy.ops.uas_shot_manager.set_current_shot( - index=uiShot.shot_index, - calledFromShotStack=True, - event_ctrl=event.ctrl, - event_alt=event.alt, - event_shift=event.shift, - ) - - uiShot.prev_click = counter - event_handled = True - - elif event.value == "RELEASE": - # bpy.ops.ed.undo_push(message=f"Change Shot...") - # self.manipulated_clip = None - # self.manipulated_clip_handle = None - print("Tutu Release") - self.cancelAction() - # event_handled = False - - elif event.type in ["MOUSEMOVE", "INBETWEEN_MOUSEMOVE"]: - # _logger.debug_ext(f"In MouseMouve 01", col="RED", tag="SHOTSTACK_EVENT") - pass - # uiShot.highlight = True - - # #_mouseMove() - # if event.value == "PRESS": - # # _logger.debug_ext(f" key pressed", col="BLUE", tag="SHOTSTACK_EVENT") - # if self.manipulated_clip: - # mouse_frame = int(region.view2d.region_to_view(event.mouse_x - region.x, 0)[0]) - # prev_mouse_frame = int(region.view2d.region_to_view(self.prev_mouse_x, 0)[0]) - # self.manipulated_clip.handle_mouse_interaction( - # self.manipulated_clip_handle, mouse_frame - prev_mouse_frame - # ) - # # self.manipulated_clip.update() - # if self.manipulated_clip_handle != 0: - # self.frame_under_mouse = mouse_frame - # event_handled = True - # elif event.value == "RELEASE": - # # _logger.debug_ext(f" key released", col="BLUE", tag="SHOTSTACK_EVENT") - # if self.manipulated_clip: - # self.manipulated_clip.highlight = False - # self.manipulated_clip = None - # self.frame_under_mouse = None - # event_handled = True - - else: - # events out of the shot clips - if event.type == "LEFTMOUSE": - if event.value == "RELEASE": - # bpy.ops.ed.undo_push(message=f"Change Shot...") - # uiShot.highlight = False - # self.manipulated_clip = None - # self.manipulated_clip_handle = None - - # note that this is called probably too many times due to - # the fact that the event can occur on another component - # This can probably be cleaned - _logger.debug_ext("tata Release") - self.cancelAction() - event_handled = True - - # if self.previousDrawWasInAClip: - # config.gRedrawShotStack = True - # if not event_handled: - # self.previousDrawWasInAClip = False - - if event.type in ["MOUSEMOVE", "INBETWEEN_MOUSEMOVE"]: - # _logger.debug_ext(f" In MouseMouve 02", col="PURPLE", tag="SHOTSTACK_EVENT") - - # uiShot.highlight = True - # _mouseMove() - - if True or event.value == "PRESS": - - # _logger.debug_ext(f" move key pressed", col="BLUE", tag="SHOTSTACK_EVENT") - if self.manipulated_clip: - # _logger.debug_ext( - # f" move key pressed on manipulated clip", col="BLUE", tag="SHOTSTACK_EVENT" - # ) - self.manipulated_clip.highlight = True - - mouse_frame = int(region.view2d.region_to_view(event.mouse_x - region.x, 0)[0]) - prev_mouse_frame = int(region.view2d.region_to_view(self.prev_mouse_x, 0)[0]) - if mouse_frame != self.mouseFrame or prev_mouse_frame != self.previousMouseFrame: - self.manipulated_clip.handle_mouse_interaction( - self.manipulated_clip_handle, mouse_frame - prev_mouse_frame - ) - self.mouseFrame = mouse_frame - self.previousMouseFrame = prev_mouse_frame - - # _logger.debug_ext( - # f" mouse frame: {mouse_frame}, prev_mouse_frame: {prev_mouse_frame}", - # col="BLUE", - # tag="SHOTSTACK_EVENT", - # ) - - # self.manipulated_clip.update() - if self.manipulated_clip_handle != 0: - self.frame_under_mouse = mouse_frame - event_handled = True - # elif event.value == "RELEASE": - # # _logger.debug_ext(f" key released", col="BLUE", tag="SHOTSTACK_EVENT") - # if self.manipulated_clip: - # self.manipulated_clip.highlight = False - # self.manipulated_clip = None - # self.frame_under_mouse = None - # event_handled = True - - else: - uiShot.highlight = False - - # do a draw when the mouse leave a clip - if self.previousDrawWasInAClip and not currentDrawIsInAClip: - _logger.debug_ext(" LEave clip", col="BLUE", tag="SHOTSTACK_EVENT") - config.gRedrawShotStack = True - # self.previousDrawWasInAClip = False - self.previousDrawWasInAClip = currentDrawIsInAClip - - # uiShot.mouseover = False - # debug if not event_handled: if self.useDebugComponents: From 9729abcc08bb3e71d80c594ed416a17a7100e26e Mon Sep 17 00:00:00 2001 From: julien blervaque Date: Sat, 27 Aug 2022 21:33:14 +0200 Subject: [PATCH 04/27] claning old shot stack 02 --- .../gpu/gpu_2d/class_InteractiveComponent.py | 7 ------- .../interact_shots_stack/shots_stack.py | 2 +- .../widgets/shots_stack_widget.py | 16 ++-------------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/shotmanager/gpu/gpu_2d/class_InteractiveComponent.py b/shotmanager/gpu/gpu_2d/class_InteractiveComponent.py index be6bbcde..e271d5b7 100644 --- a/shotmanager/gpu/gpu_2d/class_InteractiveComponent.py +++ b/shotmanager/gpu/gpu_2d/class_InteractiveComponent.py @@ -286,9 +286,6 @@ def _handle_event_custom(self, context, event, region): elif event.value == "RELEASE": # bpy.ops.ed.undo_push(message=f"Change Shot...") - # self.manipulated_clip = None - # self.manipulated_clip_handle = None - # print("Titi") if self.isManipulated: self.cancelAction(context) event_handled = True @@ -310,10 +307,6 @@ def _handle_event_custom(self, context, event, region): # tag="SHOTSTACK_EVENT", # ) - # self.manipulated_clip.update() - - # if self.manipulated_clip_handle != 0: - # self.frame_under_mouse = mouse_frame if self.isManipulated: self.frame_under_mouse = mouse_frame event_handled = True diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py index 8d36035c..f670dfb6 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py @@ -170,7 +170,7 @@ def handle_widget_events(self, context, event): # events canceling the action # for widget in self.widgets: - # if widget.manipulated_clip: + ## if widget.manipulated_clip: # removed # if ( # (event.type == "LEFTMOUSE" and event.value == "RELEASE") # or (event.type == "RIGHTMOUSE" and event.value == "RELEASE") diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py index 09a97a8b..f40f8a2e 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py @@ -63,10 +63,6 @@ def __init__(self, target_area=None): self.ui_shots = list() self.shotComponents = [] - self.manipulated_clip = None - self.manipulated_clip_handle = None - self.manipulated_clip_objs = None - self.prev_mouse_x = 0 self.prev_mouse_y = 0 self.frame_under_mouse = -1 @@ -380,20 +376,12 @@ def draw(self, preDrawOnly=False): def validateAction(self): _logger.debug_ext("Validating Shot Stack action", col="GREEN", tag="SHOTSTACK_EVENT") - if self.manipulated_clip: - self.manipulated_clip.highlight = False - self.manipulated_clip = None - self.manipulated_clip_handle = None - self.manipulated_clip_objs = None + pass def cancelAction(self): # TODO restore the initial _logger.debug_ext("Canceling Shot Stack action 22", col="ORANGE", tag="SHOTSTACK_EVENT") - if self.manipulated_clip: - self.manipulated_clip.highlight = False - self.manipulated_clip = None - self.manipulated_clip_handle = None - self.manipulated_clip_objs = None + pass def handle_event(self, context, event, region): """Return True if the event is handled for ShotStackWidget""" From 15efd50db9be26336bb681544259b4446f305864 Mon Sep 17 00:00:00 2001 From: julien blervaque Date: Mon, 29 Aug 2022 11:54:18 +0200 Subject: [PATCH 05/27] wip scale stb clip --- shotmanager/retimer/retimer.py | 65 +++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/shotmanager/retimer/retimer.py b/shotmanager/retimer/retimer.py index 640e7acc..69f6544e 100644 --- a/shotmanager/retimer/retimer.py +++ b/shotmanager/retimer/retimer.py @@ -781,8 +781,7 @@ def remove_time(sed, start_frame, end_frame, remove_gap): def retimeScene( context, - retimerProps, - mode: str, + retimeMode: str, retimerApplyToSettings, objects, start_incl: int, @@ -793,14 +792,6 @@ def retimeScene( ): """Apply the time change for each type of entities - For the following lines keep in mind that: - - retimerProps.insert_duration is inclusive - - retimerProps.start_frame is EXCLUSIVE (in other words it is NOT modified) - - retimerProps.end_frame is EXCLUSIVE (in other words is the first frame to be offset) - - But retimeScene() requires INCLUSIVE range of time for the modifications (= all the frames - created or deleted, not the moved ones). - Args: start_incl (int): The included start frame duration_incl (int): The range of retime frames (new or deleted) @@ -808,11 +799,21 @@ def retimeScene( prefs = config.getShotManagerPrefs() scene = context.scene end_incl = start_incl + duration_incl - 1 + current_frame = scene.frame_current print( - f" - retimeScene(): {retimerProps.mode}, start_incl: {start_incl}, end_incl: {end_incl}, duration_incl: {duration_incl}" + f" - retimeScene(): {retimeMode}, start_incl: {start_incl}, end_incl: {end_incl}, duration_incl: {duration_incl}" ) + mode = retimeMode + if "GLOBAL_OFFSET" == retimeMode: + if 0 < duration_incl: + mode = "INSERT" + else: + mode = "DELETE" + duration_incl = -1 * duration_incl + end_incl = start_incl + duration_incl - 1 + # print("Retiming scene: , factor: ", mode, factor) retime_args = (mode, start_incl, end_incl, join_gap, factor, pivot) # print("retime_args: ", retime_args) @@ -911,7 +912,7 @@ def retimeScene( retime_markers(scene, *retime_args) # anim range - def compute_retimed_frame(frame_value, mode, start_incl, end_incl, duration_incl, pivot, factor): + def _compute_retimed_frame(frame_value, mode, start_incl, end_incl, duration_incl, pivot, factor): new_frame_value = frame_value if "INSERT" == mode: @@ -931,16 +932,19 @@ def compute_retimed_frame(frame_value, mode, start_incl, end_incl, duration_incl return new_frame_value # anim range - if prefs.applyToSceneRange and "CLEAR_ANIM" != mode: - new_range_start = compute_retimed_frame( + if retimerApplyToSettings.applyToSceneRange and "CLEAR_ANIM" != mode: + + new_range_start = _compute_retimed_frame( scene.frame_start, mode, start_incl, end_incl, duration_incl, pivot, factor ) - new_range_end = compute_retimed_frame(scene.frame_end, mode, start_incl, end_incl, duration_incl, pivot, factor) - new_range_preview_start = compute_retimed_frame( + new_range_end = _compute_retimed_frame( + scene.frame_end, mode, start_incl, end_incl, duration_incl, pivot, factor + ) + new_range_preview_start = _compute_retimed_frame( scene.frame_preview_start, mode, start_incl, end_incl, duration_incl, pivot, factor ) - new_range_preview_end = compute_retimed_frame( + new_range_preview_end = _compute_retimed_frame( scene.frame_preview_end, mode, start_incl, end_incl, duration_incl, pivot, factor ) @@ -963,20 +967,23 @@ def compute_retimed_frame(frame_value, mode, start_incl, end_incl, duration_incl scene.frame_preview_end = max(new_range_preview_end, 0) # time cursor - if prefs.applyToTimeCursor and "CLEAR_ANIM" != mode: - new_current_frame = max( - compute_retimed_frame(scene.frame_current, mode, start_incl, end_incl, duration_incl, pivot, factor), 0 + if retimerApplyToSettings.applyToTimeCursor and "CLEAR_ANIM" != mode: + new_current_frame = _compute_retimed_frame( + current_frame, mode, start_incl, end_incl, duration_incl, pivot, factor ) + if scene.use_preview_range: + rangeStart = scene.frame_preview_start + rangeEnd = scene.frame_preview_end + else: + rangeStart = scene.frame_start + rangeEnd = scene.frame_end + new_current_frame = max(new_current_frame, rangeStart) + new_current_frame = min(new_current_frame, rangeEnd) scene.frame_set(new_current_frame) - bpy.data.actions.remove(action_tmp) + # NOTE: should be kept but returns an error message in the log: + # ERROR (bke.lib_id_delete): C:\Users\blender\git\blender-v320\blender.git\source\blender\blenkernel\intern\lib_id_delete.c:344 + # id_delete: Deleting ACRetimer_TmpAction which still has 1 users (including 0 'extra' shallow users) + # bpy.data.actions.remove(action_tmp) return () - - -# to do: -# - faire marcher le rescale -# - verif shots -# - shape keys -# - vse -# - finir fonction generic From 6b3b0b05a41ee7b470319a6bff582c0e7418a8d1 Mon Sep 17 00:00:00 2001 From: julien blervaque Date: Mon, 29 Aug 2022 11:54:24 +0200 Subject: [PATCH 06/27] wip stb scale clip linking --- shotmanager/__init__.py | 6 +- shotmanager/addon_prefs/addon_prefs.py | 22 +++--- shotmanager/config/config.py | 8 +- shotmanager/otio/imports.py | 2 +- .../widgets/shots_stack_clip_component.py | 35 +++++++-- .../widgets/shots_stack_handle_component.py | 25 ++++++- .../widgets/shots_stack_widget.py | 4 +- shotmanager/properties/shot.py | 25 ++++++- .../retimer/retimer_applyto_settings.py | 50 +++++++++++-- shotmanager/retimer/retimer_applyto_ui.py | 74 +++++++++---------- shotmanager/retimer/retimer_operators.py | 22 ++---- shotmanager/retimer/retimer_retime_engine.py | 2 + shotmanager/retimer/retimer_ui_applyto.py | 66 ----------------- shotmanager/utils/utils_storyboard.py | 2 + 14 files changed, 185 insertions(+), 158 deletions(-) delete mode 100644 shotmanager/retimer/retimer_ui_applyto.py diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index 03bc6c91..a22fcdae 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -20,7 +20,7 @@ """ import bpy -from bpy.props import BoolProperty, IntProperty, FloatProperty +from bpy.props import BoolProperty, IntProperty, FloatProperty, PointerProperty from .config import config @@ -48,6 +48,8 @@ from . import prefs from . import retimer from .retimer import retimer_ui +from .retimer.retimer_applyto_settings import UAS_Retimer_ApplyToSettings + from . import rendering from .rendering import rendering_ui @@ -319,6 +321,8 @@ def _update_UAS_shot_manager_identify_dopesheets(self, context): options=set(), ) + bpy.types.WindowManager.UAS_shot_manager_shots_stack_retimerApplyTo = PointerProperty(type=UAS_Retimer_ApplyToSettings) + if config.devDebug: print(f"\n ------ Ubisoft Shot Manager debug: {config.devDebug} ------- ") diff --git a/shotmanager/addon_prefs/addon_prefs.py b/shotmanager/addon_prefs/addon_prefs.py index 821d7133..ce0e03e0 100644 --- a/shotmanager/addon_prefs/addon_prefs.py +++ b/shotmanager/addon_prefs/addon_prefs.py @@ -839,16 +839,16 @@ def _update_layersListDropdown(self, context): description="Display the Retimer sub-panel in the Shot Manager panel.\n\n(saved in the add-on preferences)", default=True, ) - applyToTimeCursor: BoolProperty( - name="Apply to Time Cursor", - description="Apply retime operation to the time cursor.\n\n(saved in the add-on preferences)", - default=True, - ) - applyToSceneRange: BoolProperty( - name="Apply to Scene Range", - description="Apply retime operation to the animation start and end of the scene.\n\n(saved in the add-on preferences)", - default=True, - ) + # applyToTimeCursor: BoolProperty( + # name="Apply to Time Cursor", + # description="Apply retime operation to the time cursor.\n\n(saved in the add-on preferences)", + # default=True, + # ) + # applyToSceneRange: BoolProperty( + # name="Apply to Scene Range", + # description="Apply retime operation to the animation start and end of the scene.\n\n(saved in the add-on preferences)", + # default=True, + # ) ######################################################################## # additional tools ### @@ -935,7 +935,7 @@ def _update_display_shtStack_toolbar(self, context): update=_update_display_shtStack_toolbar, default=True, ) - + shtStack_link_stb_clips_to_keys: BoolProperty( name="Link Storyboard Clips to Keyframes", description="Link the Storyboard shot clips ", diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index aeef40a8..622fef77 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -148,19 +148,19 @@ def getLoggingTags(): tags["REG"] = False tags["UNREG"] = False - tags["INIT_AND_DATA"] = True + tags["INIT_AND_DATA"] = False tags["SHOTS_PLAY_MODE"] = True tags["RENDER"] = False tags["LAYOUT"] = False - tags["RETIMER"] = True + tags["RETIMER"] = False tags["EDIT_IO"] = True tags["TIMELINE_EVENT"] = False - tags["SHOTSTACK_EVENT"] = False - tags["EVENT"] = False + tags["SHOTSTACK_EVENT"] = True + tags["EVENT"] = True # info tags tags["RENDERTIME"] = False diff --git a/shotmanager/otio/imports.py b/shotmanager/otio/imports.py index 972e2602..397f31b3 100644 --- a/shotmanager/otio/imports.py +++ b/shotmanager/otio/imports.py @@ -670,7 +670,7 @@ def _addNewShot( # add media as camera background if useMediaAsCameraBG: - print("Import Otio clip.media_reference.target_url: ", clip.media_reference.target_url) + # print("Import Otio clip.media_reference.target_url: ", clip.media_reference.target_url) # media_path = Path(utils.file_path_from_url(clip.media_reference.target_url)) # print("Import Otio media_path: ", media_path) # if not media_path.exists(): diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py index 78bdc014..6ce55a47 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py @@ -31,6 +31,8 @@ from .shots_stack_handle_component import ShotHandleComponent +from shotmanager.retimer.retimer import retimeScene + # from shotmanager.gpu.gpu_2d.gpu_2d import loadImageAsTexture from shotmanager.utils import utils @@ -94,6 +96,10 @@ def __init__(self, targetArea, posY=2, shot=None): self._name_color_dark = (0.07, 0.07, 0.07, 1) self._name_color_disabled = (0.8, 0.8, 0.8, 1) + # manipulation + # filled when isManipulated changes + self.manipulatedChildren = None + # text component ######### self.textComponent = Text2D( posXIsInRegionCS=True, posYIsInRegionCS=True, posY=0, alignment="MID_LEFT", parent=self @@ -378,19 +384,18 @@ def _on_manipulated_changed(self, context, isManipulated): function is called """ if isManipulated: - # _logger.debug_ext("component2D handle_events set manipulated true", col="PURPLE", tag="EVENT" + _logger.debug_ext("component2D handle_events set manipulated True", col="PURPLE", tag="EVENT") + if self.shot.isStoryboardType(): + self.manipulatedChildren = self.shot.getStoryboardChildren() pass else: + _logger.debug_ext("component2D handle_events set manipulated False", col="PURPLE", tag="EVENT") + self.manipulatedChildren = None pass # override of InteractiveComponent def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): - """wkip note: delta_frames is in frames but may need to be in pixels in some cases - handle: if handle == -1: left clip handle (start) - if handle == 0: clip (start and end) - if handle == 1: right clip handle (end) - """ - + """wkip note: delta_frames is in frames but may need to be in pixels in some cases""" # Very important, don't use properties for changing both start and ends. Depending of the amount of displacement duration can change. if self.shot.durationLocked: if mouse_delta_frames > 0: @@ -407,6 +412,22 @@ def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): self.shot.start += mouse_delta_frames self.shot.end += mouse_delta_frames + if self.manipulatedChildren is not None: + retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo + retimerApplyToSettings.initialize("STORYBOARD_CLIP") + + # if offset_duration > 0 we insert time from a point far in negative time + # if offset_duration < 0 we delete time from a point very far in negative time + farRefPoint = -100000 + retimeScene( + context, + "GLOBAL_OFFSET", + retimerApplyToSettings, + self.manipulatedChildren, + farRefPoint + 1, + mouse_delta_frames, + ) + # to override by inheriting classes def _on_doublecliked(self, context, event, region): props = context.scene.UAS_shot_manager_props diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py index bf24f1e1..53fc355b 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py @@ -25,6 +25,7 @@ from shotmanager.utils.utils_python import clamp from shotmanager.utils.utils import color_to_sRGB, lighten_color, set_color_alpha, alpha_to_linear +from shotmanager.retimer.retimer import retimeScene from shotmanager.config import config from shotmanager.config import sm_logging @@ -59,9 +60,12 @@ def __init__(self, targetArea, posY=2, width=32, alignment="BOTTOM_LEFT", parent self.zOrder = -1.0 self.isStart = isStart - self.color = self.shot.color + # manipulation + # filled when isManipulated changes + self.manipulatedChildren = None + # green or orange self.color_highlight = (0.2, 0.7, 0.2, 1) if self.isStart else (0.7, 0.3, 0.0, 1) self.color_highlight_durationLocked = (0.14, 0.28, 0.99, 1) @@ -153,8 +157,11 @@ def _on_manipulated_changed(self, context, isManipulated): # we use this to set the color of the clip as for when manipulated if isManipulated: self.parent.isManipulatedByAnotherComponent = True + if self.shot.isStoryboardType(): + self.manipulatedChildren = self.shot.getStoryboardChildren() else: self.parent.isManipulatedByAnotherComponent = False + self.manipulatedChildren = None # override of InteractiveComponent def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): @@ -163,5 +170,21 @@ def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): if self.isStart: self.shot.start += mouse_delta_frames # bpy.ops.uas_shot_manager.set_shot_start(newStart=self.start + mouse_delta_frames) + + if self.manipulatedChildren is not None: + retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo + retimerApplyToSettings.initialize("STORYBOARD_CLIP") + # retimeScene( + # context, + # "RESCALE", + # retimerApplyToSettings, + # self.manipulatedChildren, + # farRefPoint + 1, + # mouse_delta_frames, + # True, + # retimeEngine.factor, + # start_excl, + # ) + else: self.shot.end += mouse_delta_frames diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py index f40f8a2e..03e6f9bf 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py @@ -368,7 +368,6 @@ def draw(self, preDrawOnly=False): # return - # print("draw shot stack") if props.interactShotsStack_displayInCompactMode: self.drawShots_compactMode(preDrawOnly=preDrawOnly) else: @@ -395,13 +394,12 @@ def handle_event(self, context, event, region): # if event.type not in ["MOUSEMOVE", "INBETWEEN_MOUSEMOVE", "TIMER"]: # _logger.debug_ext(f" *** event in ShotStackWidget: {event.type}", col="GREEN", tag="SHOTSTACK_EVENT") + # NOTE: context is different. Normal? context = self.context props = context.scene.UAS_shot_manager_props mouse_x, mouse_y = region.view2d.region_to_view(event.mouse_x - region.x, event.mouse_y - region.y) - currentDrawIsInAClip = False - if event.type not in ["TIMER"]: _logger.debug_ext(f"event: type: {event.type}, value: {event.value}", col="GREEN", tag="SHOTSTACK_EVENT") diff --git a/shotmanager/properties/shot.py b/shotmanager/properties/shot.py index 4aa5e211..6ef09ed6 100644 --- a/shotmanager/properties/shot.py +++ b/shotmanager/properties/shot.py @@ -761,9 +761,9 @@ def _ClearParent(child): ############# def isStoryboardType(self): - return 'S' == self.shotType[0] + return "S" == self.shotType[0] - # wkip to update with the gp list + # TODO: wkip to update with the gp list def getStoryboardFrame(self): """Set the grease pencil object of the specified mode associated to the camera. Return the created - or corresponding if one existed - grease pencil object, or None if the camera was invalid @@ -772,6 +772,27 @@ def getStoryboardFrame(self): """ return self.getGreasePencilObject(mode="STORYBOARD") + # TODO: wkip check that the empty is the one of the storyboard frame + def getStoryboardEmptyChild(self): + """Return the Empty object used as the parent of the storyboard frame + Note: This doesn't depend on the shot type since camera shots can also have a storyboard frame""" + emptyChild = None + if self.isCameraValid(): + emptyChild = utils_greasepencil.get_greasepencil_child(self.camera, childType="EMPTY") + return emptyChild + + # wkip to update with the gp list + def getStoryboardChildren(self): + """If the shot has a valid camera: return the list of all the children of the camera associated + to the Storyboard Frame + Return None otherwise + Note: This doesn't depend on the shot type since camera shots can also have a storyboard frame""" + stbChildren = None + # if self.isStoryboardType(): + emptyChild = self.getStoryboardEmptyChild() + if emptyChild is not None: + stbChildren = utils.getChildrenHierarchy(emptyChild) + return stbChildren ############# # notes ##### diff --git a/shotmanager/retimer/retimer_applyto_settings.py b/shotmanager/retimer/retimer_applyto_settings.py index bfe8e5a8..a17e6697 100644 --- a/shotmanager/retimer/retimer_applyto_settings.py +++ b/shotmanager/retimer/retimer_applyto_settings.py @@ -93,19 +93,24 @@ class UAS_Retimer_ApplyToSettings(PropertyGroup): ) # moved to add-on preferences - # applyToTimeCursor: BoolProperty( - # name="Apply to Time Cursor", description="Apply retime operation to the time cursor", default=True, - # ) - # applyToSceneRange: BoolProperty( - # name="Apply to Scene Range", - # description="Apply retime operation to the animation start and end of the scene", - # default=True, - # ) + applyToTimeCursor: BoolProperty( + name="Apply to Time Cursor", + description="Apply retime operation to the time cursor", + default=True, + options=set(), + ) + applyToSceneRange: BoolProperty( + name="Apply to Scene Range", + description="Apply retime operation to the animation start and end of the scene", + default=True, + options=set(), + ) def initialize(self, applyToMode): """ Args: applyToMode: the mode of the applyTo settings. Can be SCENE, SELECTED_OBJECTS, LEGACY + Internaly if can also be: STORYBOARD_CLIP """ _logger.debug_ext(f"initialize Retimer ApplyTo Settings {applyToMode}", col="GREEN", tag="RETIMER") @@ -131,6 +136,9 @@ def initialize(self, applyToMode): self.applyToVSE = True + self.applyToTimeCursor = True + self.applyToSceneRange = True + # Selected objects if "SELECTED_OBJECTS" == applyToMode: self.id = applyToMode @@ -151,6 +159,32 @@ def initialize(self, applyToMode): self.applyToVSE = False + self.applyToTimeCursor = False + self.applyToSceneRange = False + + # Storyboard clip - fos shot stack clip manipulations + ######################## + if "STORYBOARD_CLIP" == applyToMode: + self.id = applyToMode + self.name = "Apply to Storyboard Clips" + + self.onlyOnSelection = False + self.includeLockAnim = True + + self.applyToObjects = True + self.applyToShapeKeys = True + self.applytToGreasePencil = True + + self.applyToMarkers = False + + self.applyToCameraShots = False + self.applyToStoryboardShots = False + + self.applyToVSE = False + + self.applyToTimeCursor = False + self.applyToSceneRange = False + # Legacy if "LEGACY" == applyToMode: self.id = applyToMode diff --git a/shotmanager/retimer/retimer_applyto_ui.py b/shotmanager/retimer/retimer_applyto_ui.py index 6715985b..dda34eb3 100644 --- a/shotmanager/retimer/retimer_applyto_ui.py +++ b/shotmanager/retimer/retimer_applyto_ui.py @@ -27,19 +27,19 @@ def drawApplyTo(context, retimerProps, layout): prefs = config.getShotManagerPrefs() - retimerSettings = retimerProps.getCurrentApplyToSettings() + retimerApplyToSettings = retimerProps.getCurrentApplyToSettings() propCol = propertyColumn(layout, padding_left=2, align=False) # layout = layout.box() # layout = propCol - if config.devDebug or "DEFAULT" == retimerSettings.id: + if config.devDebug or "DEFAULT" == retimerApplyToSettings.id: settingsNameRow = propCol.row() - if "DEFAULT" != retimerSettings.id: + if "DEFAULT" != retimerApplyToSettings.id: settingsNameRow.label(text="Settings:") settingsNameRow = settingsNameRow.row() - settingsNameRow.label(text=f"{retimerSettings.name}") + settingsNameRow.label(text=f"{retimerApplyToSettings.name}") else: settingsNameRow.alert = True settingsNameRow.label(text="*** Invalid Settings: Default ***") @@ -48,7 +48,7 @@ def drawApplyTo(context, retimerProps, layout): propCol.separator(factor=0.5) - if "SCENE" == retimerSettings.id: + if "SCENE" == retimerApplyToSettings.id: propCol.label(text="Retiming is applied to EVERYTHING in the scene, except:") messagesCol = propertyColumn(propCol, padding_left=4, padding_bottom=1, scale_y=0.8) @@ -61,7 +61,7 @@ def drawApplyTo(context, retimerProps, layout): stbRow = entitiesCol.row() stbRow.prop( - retimerSettings, + retimerApplyToSettings, "applyToStoryboardShots", text="Storyboard Shots", ) @@ -69,11 +69,11 @@ def drawApplyTo(context, retimerProps, layout): # doesnt work, need an enum # stbRow.prop_with_popover( - # retimerSettings, "applyToStoryboardShots", panel="UAS_PT_SM_quicktooltip", text="tototo", icon="INFO" + # retimerApplyToSettings, "applyToStoryboardShots", panel="UAS_PT_SM_quicktooltip", text="tototo", icon="INFO" # ) stbRowRight = stbRow.row() - stbRowRight.alert = retimerSettings.applyToStoryboardShots + stbRowRight.alert = retimerApplyToSettings.applyToStoryboardShots quickHelpInfo = retimerProps.getQuickHelp("APPLYTO_STORYBOARDSHOTS") # doc_op = stbRowRight.operator("shotmanager.open_documentation_url", text="", icon="INFO", emboss=False) @@ -83,82 +83,82 @@ def drawApplyTo(context, retimerProps, layout): # tooltipStr += f"\n\nOpen Shot Manager Retimer online documentation:\n {doc_op.path}" # doc_op.tooltip = tooltipStr - # quickTooltip(stbRowRight, "patate", title="Storyboard Shots", alert=retimerSettings.applyToStoryboardShots) + # quickTooltip(stbRowRight, "patate", title="Storyboard Shots", alert=retimerApplyToSettings.applyToStoryboardShots) quickTooltip( - stbRowRight, quickHelpInfo[2], title=quickHelpInfo[1], alert=retimerSettings.applyToStoryboardShots + stbRowRight, quickHelpInfo[2], title=quickHelpInfo[1], alert=retimerApplyToSettings.applyToStoryboardShots ) entitiesCol.separator(factor=0.5) - entitiesCol.prop(prefs, "applyToTimeCursor", text="Time Cursor") - entitiesCol.prop(prefs, "applyToSceneRange", text="Scene Range") + entitiesCol.prop(retimerApplyToSettings, "applyToTimeCursor", text="Time Cursor") + entitiesCol.prop(retimerApplyToSettings, "applyToSceneRange", text="Scene Range") - if "SELECTED_OBJECTS" == retimerSettings.id: + if "SELECTED_OBJECTS" == retimerApplyToSettings.id: split = propCol.split(factor=0.326) row = split.row(align=True) row.separator(factor=0.7) - row.prop(retimerSettings, "onlyOnSelection", text="Selection Only") + row.prop(retimerApplyToSettings, "onlyOnSelection", text="Selection Only") row = split.row(align=True) - row.prop(retimerSettings, "includeLockAnim", text="Include Locked Anim") + row.prop(retimerApplyToSettings, "includeLockAnim", text="Include Locked Anim") box = propCol.box() col = box.column() row = col.row(align=True) - row.prop(retimerSettings, "applyToObjects") - row.prop(retimerSettings, "applyToShapeKeys") - row.prop(retimerSettings, "applytToGreasePencil") + row.prop(retimerApplyToSettings, "applyToObjects") + row.prop(retimerApplyToSettings, "applyToShapeKeys") + row.prop(retimerApplyToSettings, "applytToGreasePencil") row = col.row(align=True) - row.prop(retimerSettings, "applyToMarkers") + row.prop(retimerApplyToSettings, "applyToMarkers") - if "LEGACY" == retimerSettings.id or config.devDebug: + if "LEGACY" == retimerApplyToSettings.id or config.devDebug: propRow = propCol.row() - propRow.alert = config.devDebug and "LEGACY" != retimerSettings.id + propRow.alert = config.devDebug and "LEGACY" != retimerApplyToSettings.id - if config.devDebug and "LEGACY" != retimerSettings.id: + if config.devDebug and "LEGACY" != retimerApplyToSettings.id: propRow.label(text="Debug Infos:") - if "SELECTED_OBJECTS" != retimerSettings.id: + if "SELECTED_OBJECTS" != retimerApplyToSettings.id: split = propRow.split(factor=0.326) row = split.row(align=True) row.separator(factor=0.7) - row.prop(retimerSettings, "onlyOnSelection", text="Selection Only") + row.prop(retimerApplyToSettings, "onlyOnSelection", text="Selection Only") row = split.row(align=True) - row.prop(retimerSettings, "includeLockAnim", text="Include Locked Anim") + row.prop(retimerApplyToSettings, "includeLockAnim", text="Include Locked Anim") propRow = propCol.row() - propRow.alert = config.devDebug and "LEGACY" != retimerSettings.id + propRow.alert = config.devDebug and "LEGACY" != retimerApplyToSettings.id box = propRow.box() col = box.column() - if "SELECTED_OBJECTS" != retimerSettings.id: + if "SELECTED_OBJECTS" != retimerApplyToSettings.id: row = col.row(align=True) - row.prop(retimerSettings, "applyToObjects") - row.prop(retimerSettings, "applyToShapeKeys") - row.prop(retimerSettings, "applytToGreasePencil") + row.prop(retimerApplyToSettings, "applyToObjects") + row.prop(retimerApplyToSettings, "applyToShapeKeys") + row.prop(retimerApplyToSettings, "applytToGreasePencil") row = col.row(align=True) - row.prop(retimerSettings, "applyToMarkers") + row.prop(retimerApplyToSettings, "applyToMarkers") row = col.row(align=True) row.scale_y = 0.3 row = col.row(align=True) - row.prop(retimerSettings, "applyToCameraShots") - row.prop(retimerSettings, "applyToVSE") + row.prop(retimerApplyToSettings, "applyToCameraShots") + row.prop(retimerApplyToSettings, "applyToVSE") row.label(text="") # time cursor and range ########################## - if "LEGACY" == retimerSettings.id or config.devDebug: + if "LEGACY" == retimerApplyToSettings.id or config.devDebug: box = propCol.box() - # box.alert = "SCENE" != retimerSettings.id + # box.alert = "SCENE" != retimerApplyToSettings.id col = box.column() row = col.row(align=True) - row.prop(prefs, "applyToTimeCursor", text="Time Cursor") - row.prop(prefs, "applyToSceneRange", text="Scene Range") + row.prop(retimerApplyToSettings, "applyToTimeCursor", text="Time Cursor") + row.prop(retimerApplyToSettings, "applyToSceneRange", text="Scene Range") # row.label(text="") diff --git a/shotmanager/retimer/retimer_operators.py b/shotmanager/retimer/retimer_operators.py index fa4c720b..bf5978e5 100644 --- a/shotmanager/retimer/retimer_operators.py +++ b/shotmanager/retimer/retimer_operators.py @@ -133,24 +133,16 @@ def execute(self, context): # if offset_duration > 0 we insert time from a point far in negative time # if offset_duration < 0 we delete time from a point very far in negative time - farRefPoint = -10000 - - if 0 < retimeEngine.offset_duration: - offsetMode = "INSERT" - else: - offsetMode = "DELETE" + farRefPoint = -100000 retimer.retimeScene( context, - retimeEngine, - offsetMode, + "GLOBAL_OFFSET", retimerApplyToSettings, sceneObjs, farRefPoint + 1, - abs(retimeEngine.offset_duration), + retimeEngine.offset_duration, retimeEngine.gap, - 1.0, - retimeEngine.pivot, ) elif -1 < retimeEngine.mode.find("INSERT"): @@ -163,7 +155,6 @@ def execute(self, context): ) retimer.retimeScene( context, - retimeEngine, "INSERT", retimerApplyToSettings, sceneObjs, @@ -183,7 +174,6 @@ def execute(self, context): ) retimer.retimeScene( context, - retimeEngine, "DELETE", retimerApplyToSettings, sceneObjs, @@ -203,8 +193,7 @@ def execute(self, context): ) retimer.retimeScene( context, - retimeEngine, - retimeEngine.mode, + "RESCALE", retimerApplyToSettings, sceneObjs, start_excl, @@ -223,8 +212,7 @@ def execute(self, context): ) retimer.retimeScene( context, - retimeEngine, - retimeEngine.mode, + "CLEAR_ANIM", retimerApplyToSettings, sceneObjs, start_excl + 1, diff --git a/shotmanager/retimer/retimer_retime_engine.py b/shotmanager/retimer/retimer_retime_engine.py index c37debd6..247b14a8 100644 --- a/shotmanager/retimer/retimer_retime_engine.py +++ b/shotmanager/retimer/retimer_retime_engine.py @@ -115,8 +115,10 @@ def list_retime_modes(self, context): mode: EnumProperty( name="Time Mode", + description="Can be GLOBAL_OFFSET, INSERT_BEFORE, INSERT_AFTER, DELETE_RANGE, RESCALE, CLEAR_ANIM", items=list_retime_modes, # default="GLOBAL_OFFSET", + default=0, options=set(), ) diff --git a/shotmanager/retimer/retimer_ui_applyto.py b/shotmanager/retimer/retimer_ui_applyto.py deleted file mode 100644 index ce352b58..00000000 --- a/shotmanager/retimer/retimer_ui_applyto.py +++ /dev/null @@ -1,66 +0,0 @@ -# GPLv3 License -# -# Copyright (C) 2021 Ubisoft -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Retimer UI -""" - -import bpy - -from shotmanager.utils.utils_ui import collapsable_panel, propertyColumn, separatorLine - -from shotmanager.config import config - - -def drawApplyTo(context, layout): - retimerProps = context.scene.UAS_shot_manager_props.retimer - prefs = config.getShotManagerPrefs() - - propCol = propertyColumn(layout, padding_left=2, align=False) - # layout = layout.box() - layout = propCol - - split = layout.split(factor=0.326) - row = split.row(align=True) - row.separator(factor=0.7) - row.prop(retimerProps, "onlyOnSelection", text="Selection Only") - row = split.row(align=True) - row.prop(retimerProps, "includeLockAnim", text="Include Locked Anim") - - box = layout.box() - col = box.column() - - row = col.row(align=True) - row.prop(retimerProps, "applyToObjects") - row.prop(retimerProps, "applyToShapeKeys") - row.prop(retimerProps, "applytToGreasePencil") - - row = col.row(align=True) - row.scale_y = 0.3 - - row = col.row(align=True) - row.prop(retimerProps, "applyToCameraShots") - row.prop(retimerProps, "applyToVSE") - row.label(text="") - - box = layout.box() - col = box.column() - - row = col.row(align=True) - row.prop(prefs, "applyToTimeCursor", text="Time Cursor") - row.prop(prefs, "applyToSceneRange", text="Scene Range") - # row.label(text="") diff --git a/shotmanager/utils/utils_storyboard.py b/shotmanager/utils/utils_storyboard.py index f3a136fd..9076f0ec 100644 --- a/shotmanager/utils/utils_storyboard.py +++ b/shotmanager/utils/utils_storyboard.py @@ -31,6 +31,8 @@ def getStoryboardObjects(scene): """Return a list of all the objects of the scene that are belonging to storyboard shots This includes the shots cameras and all their hierarchy (empty, gp, children of all kinds), this for all takes + + See also shot.getStoryboardChildren() """ props = scene.UAS_shot_manager_props From 0147426892386e16c2aa7ffc52e7d21e57cd9741 Mon Sep 17 00:00:00 2001 From: julien blervaque Date: Thu, 1 Sep 2022 10:40:38 +0200 Subject: [PATCH 07/27] wip retimer changes --- resources/_to_delete_/retimer.py | 1006 +++++++++++++++++ .../widgets/shots_stack_handle_component.py | 6 +- shotmanager/retimer/retimer.py | 181 +-- 3 files changed, 1115 insertions(+), 78 deletions(-) create mode 100644 resources/_to_delete_/retimer.py diff --git a/resources/_to_delete_/retimer.py b/resources/_to_delete_/retimer.py new file mode 100644 index 00000000..b8568cfe --- /dev/null +++ b/resources/_to_delete_/retimer.py @@ -0,0 +1,1006 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Retimer functions +""" + +import bpy + +from shotmanager.utils.utils_markers import sortMarkers + +from shotmanager.config import config +from shotmanager.config import sm_logging + +_logger = sm_logging.getLogger(__name__) + +# FCurve +################################################ + + +class FCurve: + def __init__(self, fcurve): + self.fcurve = fcurve + + def get_key_coordinates(self, index): + return self.fcurve.keyframe_points[index].co + + def set_key_coordinates(self, index, coordinates): + self.fcurve.keyframe_points[index].co = coordinates + + def get_key_index_at_frame(self, frame): + """Return the index of the key at the specified frame, -1 if no key found""" + for i in range(len(self.fcurve.keyframe_points)): + if frame == self.fcurve.keyframe_points[i].co[0]: + return i + return -1 + + def handles(self, index): + return self.fcurve.keyframe_points[index].handle_left, self.fcurve.keyframe_points[index].handle_right + + # Seems to be passed by reference... + # def set_handles ( self, index, value ): + # self.fcurve.keyframe_points[ index ].handle_left, self.fcurve.keyframe_points[ index ].handle_right = value + + def insert_frame(self, coordinates): + kf = self.fcurve.keyframe_points.insert(coordinates[0], coordinates[1]) + + def remove_frames(self, start_incl, end_incl, remove_gap=False): + to_remove = list() + + for k in self.fcurve.keyframe_points: + # print(f" FCurve.remove_frames start_incl:{start_incl}, k.co[0]: {k.co[0]}, end_incl: {end_incl}") + if start_incl <= k.co[0] <= end_incl: + to_remove.append(k) + + # wkip to delete + # if remove_gap and False: + # # the handle values of the start and end must be stored before deletion otherwise they are modified + # start_incl_key_index = self.get_key_index_at_frame(start_incl - 1) + # print(f" rr start_incl_key_index {start_incl_key_index}") + # start_incl_key_right_handle_right = None + # if -1 != start_incl_key_index: + # start_incl_key_right_handle_right = self.fcurve.keyframe_points[start_incl_key_index].handle_right + # print( + # f" rr02 start_incl_key_right_handle_right {start_incl_key_right_handle_right}, type: {self.fcurve.keyframe_points[start_incl_key_index].handle_right_type}" + # ) + # end_incl_key = self.get_key_index_at_frame(start_incl) + + for k in reversed(to_remove): + self.fcurve.keyframe_points.remove(k) + + # wkip to delete + # if remove_gap and False: + # print("Fixing 00") + # # restore handles + # if -1 != start_incl_key_index and start_incl_key_right_handle_right is not None: + # print("Fixing") + # self.fcurve.keyframe_points[start_incl_key_index].handle_right[0] = start_incl_key_right_handle_right[0] + # print(f" rr03 start_incl_key_right_handle_right {start_incl_key_right_handle_right}") + # start_incl_key_right_handle_right02 = self.fcurve.keyframe_points[start_incl_key_index].handle_right + # print(f" rr04 start_incl_key_right_handle_right02 {start_incl_key_right_handle_right02}") + + if remove_gap: + _offset_frames(self, end_incl, start_incl - end_incl - 1) + + def __len__(self): + return len(self.fcurve.keyframe_points) + + +# def _offset_frames(fcurve: FCurve, start_incl, offset): +# for i in range(len(fcurve)): +# key_time, value = fcurve.get_key_coordinates(i) +# if start_incl <= key_time: +# fcurve.set_key_coordinates(i, (key_time + offset, value)) +# left_handle, right_handle = fcurve.handles(i) +# left_handle[0] += offset +# right_handle[0] += offset + +# GPFCurve +################################################ + +# not used!!! +# class GPFCurve(FCurve): +# def get_key_coordinates(self, index): +# return self.fcurve.frames[index].frame_number, 0 + +# def set_key_coordinates(self, index, coordinates): +# self.fcurve.frames[index].frame_number = coordinates[0] + +# def handles(self, index): +# return [0, 0], [0, 0] + +# def set_handles(self, index, value): +# pass + +# def insert_frame(self, coordinates): +# pass + +# def remove_frames(self, start, end): +# to_remove = list() + +# for k in self.fcurve.frames: +# if start <= k.frame_number <= end: +# to_remove.append(k) + +# for k in reversed(to_remove): +# self.fcurve.frames.remove(k) + +# def __len__(self): +# return len(self.fcurve.frames) + + +# Functions +################################################ + + +def computeNewFrameValue(frame_value, mode, start_incl=0, end_incl=0, pivot=0, factor=1.0, roundToNearestFrame=True): + """Return the value of the time (in frames) after the retiming + Return None if the new time value is not available (deleted time for example) + It supports floating time frames + """ + + new_frame_value = frame_value + # offset is also the duration_incl + offset = end_incl - start_incl + 1 + + if mode == "INSERT": + if start_incl <= frame_value: + return frame_value + offset + + elif mode == "DELETE" or mode == "CLEAR_ANIM": + if start_incl <= frame_value: + if frame_value <= end_incl: + # or return new_frame_value = start_incl, as in _compute_retimed_frame??? + new_frame_value = None + else: + # offset = end_incl - start_incl + 1 + new_frame_value = frame_value - offset + + elif mode == "RESCALE": + new_frame_value = rescale_frame(frame_value, start_incl, end_incl, pivot, factor) + + # if start_incl <= frame_value: + # if frame_value <= end_incl: + # return None + # else: + # offset = (end_incl - start_incl - 1) * factor - end_incl + start_incl - 1 + # # _logger.debug_ext(f" In computeNewFrameValue() Rescale mode: offset: {offset}") + # return frame_value + offset + + elif mode == "FREEZE": + pass + elif mode == "CLEAR_ANIM": + pass + + if roundToNearestFrame: + new_frame_value = round(new_frame_value) + return new_frame_value + + +def compute_offset(frame_value, pivot, factor): + """Compute the new value of frame_value when scaled from the pivot and by the given factor""" + duration_to_pivot = frame_value - pivot + return round(duration_to_pivot * factor) # + pivot + + +def rescale_frame(frame_value, start_incl, end_incl, pivot, factor): + """Compute the new value of frame_value when scaled from the pivot and by the given factor + in the specified range + Note: this works only for frames, not floating points + """ + new_frame_value = frame_value + duration_to_pivot = frame_value - pivot + # round(duration_to_pivot * factor) + pivot + + if start_incl <= frame_value: + if end_incl < frame_value: + duration_to_pivot = end_incl + 1 - pivot + new_frame_value = round(duration_to_pivot * factor) + pivot + frame_value - end_incl - 1 + else: + duration_to_pivot = frame_value - pivot + new_frame_value = round(duration_to_pivot * factor) + pivot + + return new_frame_value + + +# TODO wkip generic function to finish +# Adding an option to offset - or to keep fix - the values out of the range +# Adding an optional range min and range max + +# def rescale_value(value: float, start_range: float, end_range: float, origin: float, factor: float, round_result=False): +# """Compute the new value of frame_value when scaled from the origin and by the given factor +# in the specified range +# *** works with any floating value, not just frame. Required for key handle values +# """ +# new_value = value +# distance_to_origin = value - origin +# round(duration_to_pivot * factor) + origin + +# if start_range < value: +# if end_range < value: +# distance_to_origin = end_range + 1 - origin +# new_value = round(distance_to_origin * factor) + origin + value - end_range - 1 +# else: +# distance_to_origin = value - origin +# new_value = round(distance_to_origin * factor) + origin + +# new_value = if round_result else +# return new_value + + +def _stretch_frames(fcurve: FCurve, start_incl, end_incl, factor, pivot_value, clamp): + # First pass. + if clamp: + remove_pre_start = list() + remove_post_end = list() + for i in range(len(fcurve)): + coordinates = fcurve.get_key_coordinates(i) + dist_from_pivot = coordinates[0] - pivot_value + if start_incl >= round(pivot_value + dist_from_pivot * factor): + remove_pre_start.append(coordinates[0]) + elif round(pivot_value + dist_from_pivot * factor) >= end_incl: + remove_post_end.append(coordinates[0]) + + if remove_pre_start: + fcurve.remove_frames(min(remove_pre_start), max(remove_pre_start), False) + + if remove_post_end: + fcurve.remove_frames(min(remove_post_end), max(remove_post_end), False) + else: + if factor > 1: + _offset_frames( + fcurve, end_incl + 1, compute_offset(end_incl + 1, pivot_value, factor) - (end_incl - start_incl + 1) + ) + # print(f" rescale offset 01: {compute_offset(end_incl + 1, pivot_value, factor)}") + + # wkip delete existing keys when factor < 1.0 + # bpy.ops.action.clean(threshold=0.001, channels=False) + for i in range(len(fcurve)): + coordinates = fcurve.get_key_coordinates(i) + if start_incl <= coordinates[0] <= end_incl: + handles = fcurve.handles(i) + offset = compute_offset(coordinates[0], pivot_value, factor) # - start_incl + 1 + # fcurve.set_key_coordinates(i, (coordinates[0] + offset, coordinates[1])) + fcurve.set_key_coordinates(i, (pivot_value + offset, coordinates[1])) + handles[0][0] = pivot_value + compute_offset(handles[0][0], pivot_value, factor) + handles[1][0] = pivot_value + compute_offset(handles[1][0], pivot_value, factor) + + if factor < 1.0: + _offset_frames( + fcurve, end_incl + 1, compute_offset(end_incl + 1, pivot_value, factor) - (end_incl - start_incl + 1) + ) + + +def _offset_frames(fcurve: FCurve, start_incl, offset): + for i in range(len(fcurve)): + key_time, value = fcurve.get_key_coordinates(i) + if start_incl <= key_time: + fcurve.set_key_coordinates(i, (key_time + offset, value)) + left_handle, right_handle = fcurve.handles(i) + left_handle[0] += offset + right_handle[0] += offset + + +def _offset_GPframes(layer, start_incl, offset): + """Move the layer frames that are AT THE SAME TIME or later than the reference frame""" + # print(f"layer:{layer.info}") + for f in layer.frames: + if start_incl <= f.frame_number: + f.frame_number += offset + + +def retime_GPframes(layer, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): + """Retime "frames" (= each drawing of a Grease Pencil object)""" + offset = end_incl - start_incl + 1 + + if mode == "INSERT": + _offset_GPframes(layer, start_incl, offset) + + elif mode == "DELETE" or mode == "CLEAR_ANIM": + # delete frames + if 0 < len(layer.frames): + f_ind = 0 + while f_ind < len(layer.frames): + if start_incl <= layer.frames[f_ind].frame_number <= end_incl: + # we suppose frames are sorted according to increasing time + layer.frames.remove(layer.frames[f_ind]) + # print(f" *** deleting frame {f_ind}, end_incl= {end_incl}") + else: + f_ind += 1 + + # remove empty gap + if mode == "DELETE": + _offset_GPframes(layer, end_incl, -offset) + pass + # if 0 < len(layer.frames): + # for f in layer.frames: + # if end_incl < f.frame_number: + # f.frame_number -= end_incl - start_incl + + elif mode == "RESCALE": + # push out of range frames later in time + if factor > 1.0: + # wkip sur du +1 on end frame???? + _offset_GPframes( + layer, end_incl + 1, compute_offset(end_incl + 1, pivot, factor) - (end_incl - start_incl + 1) + ) + + # scale range + for f in layer.frames: + if start_incl <= f.frame_number <= end_incl: + offset = compute_offset(f.frame_number, pivot, factor) + f.frame_number = offset + pivot + + # pull out of range frames sooner in time + if factor < 1.0: + _offset_GPframes( + layer, end_incl + 1, compute_offset(end_incl + 1, pivot, factor) - (end_incl - start_incl + 1) + ) + + return () + + +def retime_frames(fcurve: FCurve, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): + + if mode == "INSERT": + _offset_frames(fcurve, start_incl, end_incl - start_incl + 1) + + elif mode == "DELETE" or mode == "CLEAR_ANIM": + fcurve.remove_frames(start_incl, end_incl, remove_gap) + + elif mode == "RESCALE": + _stretch_frames(fcurve, start_incl, end_incl, factor, pivot, False) + + elif mode == "FREEZE": + for i in range(len(fcurve)): + key_time, value = fcurve.get_key_coordinates(i) + new_keys = list() + if key_time == start_incl: + new_keys.append((key_time, value)) + new_keys.append((key_time + end_incl - start_incl, value)) + + if key_time >= start_incl: + fcurve.set_key_coordinates(i, (key_time + end_incl - start_incl, value)) + + left_handle, right_handle = fcurve.get_handles(i) + left_handle[0] += end_incl - start_incl + right_handle[0] += end_incl - start_incl + fcurve.set_handles(i, (left_handle, right_handle)) + + for v in new_keys: + fcurve.insert_frame(v) + + +# wkip warining here start_frame is EXCLUSIF - To change!! +# end frame is inclusive +def retime_shot(shot, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): + + start_frame = start_incl - 1 + end_frame = end_incl # + 1 + if mode == "INSERT": + offset = end_frame - start_frame + + if shot.durationLocked: + if start_frame < shot.start: + shot.start += offset + elif start_frame < shot.end: + shot.durationLocked = False + shot.end += offset + shot.durationLocked = True + else: + # important to offset end first!! + if start_frame < shot.end: + shot.end += offset + if start_frame < shot.start: + shot.start += offset + + elif mode == "FREEZE": + pass + + elif mode == "DELETE": + + # # the removal lets a 1 frame space, not an overlap of start by end!! + # # if start and end are in the range then we create a 1 frame shot + # if start_incl - 1 <= shot.start and shot.end <= end_frame: + # shot.start = start_incl - 1 + # shot.end = end_frame + + # # shot is before, nothing happens + # elif shot.start < start_incl - 1 and shot.end < start_incl - 1: + # pass + + # # shot is after, we offset + # elif end_frame <= shot.start and shot.end <= end_frame: + # offset = end_frame - start_incl - 1 + # shot.start -= offset + # shot.end -= offset + + # else: + + # offset = end_frame - start_incl - 1 + offset = end_incl - start_incl + 1 + # print(f" In retime_shot() Delete mode: offset: {offset}") + + if shot.durationLocked: + if shot.start <= start_incl - 1: + if shot.end <= start_incl - 1: + pass + elif shot.end <= end_frame: + shot.durationLocked = False + shot.end = start_incl - 1 # goes to a non deleted part + shot.durationLocked = True + else: + shot.durationLocked = False + shot.end -= offset + shot.durationLocked = True + + elif start_incl - 1 < shot.start and shot.start < end_frame: + if shot.end <= end_frame: + shot.durationLocked = False + shot.start = start_incl - 1 + shot.end = start_incl - 1 + shot.durationLocked = True + shot.enabled = False + else: + shot.durationLocked = False + shot.start = start_incl - 1 + shot.end -= offset + shot.durationLocked = True + + else: + shot.start -= offset + + else: + if shot.start <= start_incl - 1: + if shot.end <= start_incl - 1: + pass + elif shot.end <= end_frame: + shot.end = start_incl - 1 # goes to a non deleted part + else: + shot.end -= offset + + elif start_incl - 1 < shot.start and shot.start < end_frame: + shot.start = start_incl - 1 + + if shot.end <= end_frame: + shot.end = start_incl - 1 + shot.enabled = False + else: + shot.end -= offset + + else: + shot.start -= offset + shot.end -= offset + + elif mode == "RESCALE": + offset = (end_frame - start_frame) * factor - end_frame + start_frame + _logger.debug_ext(f" In retime_shot() Rescale mode: offset: {offset}") + + if shot.durationLocked: + if offset > 0: + # important to offset END first!! + # shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + # shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + if end_frame < shot.end: + if end_frame < shot.start: + # shot.start += offset + shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + elif start_frame < shot.start and shot.start <= end_frame: + shot.durationLocked = False + # shot.end += offset + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + # shot.start = (shot.start - pivot) * factor + pivot + shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + shot.durationLocked = True + else: + shot.durationLocked = False + # shot.end += offset + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + shot.durationLocked = True + + elif start_frame < shot.end and shot.end <= end_frame: + if start_frame < shot.start and shot.start <= end_frame: + shot.durationLocked = False + # shot.end = (shot.end - pivot) * factor + pivot + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + # shot.start = (shot.start - pivot) * factor + pivot + shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + shot.durationLocked = True + else: + shot.durationLocked = False + # shot.end = (shot.end - pivot) * factor + pivot + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + shot.durationLocked = True + + else: + # important to offset START first!! + if end_frame < shot.start: + # shot.start += offset + shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + elif start_frame < shot.start and shot.start <= end_frame: + if end_frame < shot.end: + shot.durationLocked = False + # shot.start = ( + # (shot.start - pivot) * factor + pivot + 0.005 + # ) # approximation to make sure the rounded value is done to the upper value + shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + # shot.end += offset + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + shot.durationLocked = True + else: + shot.durationLocked = False + # shot.start = ( + # (shot.start - pivot) * factor + pivot + 0.005 + # ) # approximation to make sure the rounded value is done to the upper value + shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + # shot.end = ( + # (shot.end - pivot) * factor + pivot + 0.005 + # ) # approximation to make sure the rounded value is done to the upper value + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + shot.durationLocked = True + + else: + if end_frame < shot.end: + shot.durationLocked = False + # shot.end += offset + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + shot.durationLocked = True + elif start_frame < shot.end and shot.end <= end_frame: + shot.durationLocked = False + # shot.end = ( + # (shot.end - pivot) * factor + pivot + 0.005 + # ) # approximation to make sure the rounded value is done to the upper value + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + shot.durationLocked = True + + else: + if offset > 0: + # important to offset END first!! + print( + f" In retime_shot() Rescale mode: offset > 0: {offset}, shot: {shot.name}, s:{shot.start}, e:{shot.end}" + ) + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + print( + f" In retime_shot() Rescale mode: offset > 0: {offset}, shot: {shot.name}, ns:{shot.start}, ne:{shot.end}\n" + ) + + else: + shot.start = rescale_frame(shot.start, start_incl, end_incl, pivot, factor) + shot.end = rescale_frame(shot.end + 1, start_incl, end_incl, pivot, factor) - 1 + # # important to offset START first!! + # if end_frame < shot.start: + # shot.start += offset + # elif start_frame < shot.start and shot.start <= end_frame: + # shot.start = ( + # (shot.start - pivot) * factor + pivot + 0.005 + # ) # approximation to make sure the rounded value is done to the upper value + + # if end_frame < shot.end: + # shot.end += offset + # elif start_frame < shot.end and shot.end <= end_frame: + # shot.end = ( + # (shot.end - pivot) * factor + pivot + 0.005 + # ) # approximation to make sure the rounded value is done to the upper value + + elif mode == "RESCALE_OLD": + # offset = (end_frame - start_frame) * (factor - 1) + offset = (end_frame - start_frame) * factor - end_frame + start_frame + + print(f" In retime_shot() Rescale mode: offset: {offset}") + + if shot.durationLocked: + if offset > 0: + # important to offset END first!! + if end_frame < shot.end: + if end_frame < shot.start: + shot.start += offset + elif start_frame < shot.start and shot.start <= end_frame: + shot.durationLocked = False + shot.end += offset + shot.start = (shot.start - pivot) * factor + pivot + shot.durationLocked = True + else: + shot.durationLocked = False + shot.end += offset + shot.durationLocked = True + + elif start_frame < shot.end and shot.end <= end_frame: + if start_frame < shot.start and shot.start <= end_frame: + shot.durationLocked = False + shot.end = (shot.end - pivot) * factor + pivot + shot.start = (shot.start - pivot) * factor + pivot + shot.durationLocked = True + else: + shot.durationLocked = False + shot.end = (shot.end - pivot) * factor + pivot + shot.durationLocked = True + + else: + pass + + else: + # important to offset START first!! + if end_frame < shot.start: + shot.start += offset + elif start_frame < shot.start and shot.start <= end_frame: + if end_frame < shot.end: + shot.durationLocked = False + shot.start = ( + (shot.start - pivot) * factor + pivot + 0.005 + ) # approximation to make sure the rounded value is done to the upper value + shot.end += offset + shot.durationLocked = True + else: + shot.durationLocked = False + shot.start = ( + (shot.start - pivot) * factor + pivot + 0.005 + ) # approximation to make sure the rounded value is done to the upper value + shot.end = ( + (shot.end - pivot) * factor + pivot + 0.005 + ) # approximation to make sure the rounded value is done to the upper value + shot.durationLocked = True + + else: + if end_frame < shot.end: + shot.durationLocked = False + shot.end += offset + shot.durationLocked = True + elif start_frame < shot.end and shot.end <= end_frame: + shot.durationLocked = False + shot.end = ( + (shot.end - pivot) * factor + pivot + 0.005 + ) # approximation to make sure the rounded value is done to the upper value + shot.durationLocked = True + else: + pass + + else: + if offset > 0: + # important to offset END first!! + if end_frame < shot.end: + shot.end += offset + elif start_frame < shot.end and shot.end <= end_frame: + shot.end = (shot.end + 1 - pivot) * factor + pivot - 1 + else: + pass + + if end_frame < shot.start: + shot.start += offset + elif start_frame < shot.start and shot.start <= end_frame: + shot.start = (shot.start - pivot) * factor + pivot + else: + pass + + else: + # important to offset START first!! + if end_frame < shot.start: + shot.start += offset + elif start_frame < shot.start and shot.start <= end_frame: + shot.start = ( + (shot.start - pivot) * factor + pivot + 0.005 + ) # approximation to make sure the rounded value is done to the upper value + else: + pass + + if end_frame < shot.end: + shot.end += offset + elif start_frame < shot.end and shot.end <= end_frame: + shot.end = ( + (shot.end - pivot) * factor + pivot + 0.005 + ) # approximation to make sure the rounded value is done to the upper value + else: + pass + + elif mode == "CLEAR_ANIM": + pass + + +def retime_markers(scene, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): + # NOTE: there can be several markers per frame!!! + + if mode == "RESCALE": + offset = (end_incl - start_incl - 1) * factor - end_incl + start_incl - 1 + _logger.debug_ext(f" In retime_markers() Rescale mode: offset: {offset}") + + markers = sortMarkers(scene.timeline_markers) + if len(markers): + for m in markers: + newFrameVal = computeNewFrameValue(m.frame, mode, start_incl, end_incl, pivot, factor) + if newFrameVal is None: + # delete marker + scene.timeline_markers.remove(m) + else: + m.frame = newFrameVal + + +def retime_vse(scene, mode, start_frame, end_frame, remove_gap=True): + def insert_time(sed, start_frame, end_frame): + # This will be a two pass process since we will use operators to cut the clips. + offset = end_frame - start_frame + sequences = list() + for sequence in sed.sequences: + sequence.select = False + sequences.append(sequence) + + sequences.sort(key=lambda s: s.frame_start, reverse=True) + + # First pass is about move start frame of the clip if they are behind the start_frame and cutting the clips which contains start_frame. + for seq in sequences: + if seq.frame_final_start < start_frame < seq.frame_final_end: + seq.select = True + bpy.ops.sequencer.split(frame=start_frame) + seq.select = False + elif seq.frame_final_start >= start_frame: + seq.frame_start += offset + + # Second pass is about offseting clips which just have been cut. They are identified by seq.frame_start + seq.frame_offset_start == start_frame + for seq in list(sed.sequences): + if seq.frame_final_start == start_frame: + seq.frame_start += offset + + def remove_time(sed, start_frame, end_frame, remove_gap): + for s in sed.sequences: + s.select = False + + for s in list(sed.sequences): + if s.frame_final_start < start_frame < s.frame_final_end: + s.select = True + if s.frame_final_start < end_frame < s.frame_final_end: + s.select = True + + bpy.ops.sequencer.split(frame=start_frame) + bpy.ops.sequencer.split(frame=end_frame) + for s in sed.sequences: + s.select = False + + for s in list(sed.sequences): + if start_frame <= s.frame_final_start <= end_frame and start_frame <= s.frame_final_end <= end_frame: + sed.sequences.remove(s) + + if remove_gap: + insert_time(sed, end_frame, start_frame) + + sed = scene.sequence_editor + if sed is None: + return + + if mode == "INSERT": + insert_time(sed, start_frame, end_frame) + + elif mode == "DELETE": + remove_time(sed, start_frame, end_frame, remove_gap) + + +def retimeScene( + context, + retimeMode: str, + retimerApplyToSettings, + objects, + start_incl: float, + duration_incl: float, + join_gap=True, + factor=1.0, + pivot=0, +): + """Apply the time change for each type of entities + + Args: + start_incl (int): The included start frame + duration_incl (int): The range of retime frames (new or deleted) + """ + prefs = config.getShotManagerPrefs() + scene = context.scene + + current_frame = scene.frame_current + + mode = retimeMode + if "GLOBAL_OFFSET" == retimeMode: + if 0 < duration_incl: + mode = "INSERT" + else: + mode = "DELETE" + duration_incl = -1 * duration_incl + + end_incl = start_incl + duration_incl - 1 + + print( + f" - retimeScene(): {retimeMode}, start_incl: {start_incl}, end_incl: {end_incl}, duration_incl: {duration_incl}" + ) + + # print("Retiming scene: , factor: ", mode, factor) + retime_args = (mode, start_incl, end_incl, join_gap, factor, pivot) + # print("retime_args: ", retime_args) + + actions_done = set() # Actions can be linked so we must make sure to only retime them once + action_tmp = bpy.data.actions.new("Retimer_TmpAction") + + for obj in objects: + # print(f"Retiming object named: {obj.name}") + + # Standard object keyframes + if retimerApplyToSettings.applyToObjects: + if obj.type != "GPENCIL": + if obj.animation_data is not None: + action = obj.animation_data.action + if action is not None and action not in actions_done: + # wkip can we have animated properties that are not actions? + for fcurve in action.fcurves: + if not fcurve.lock or retimerApplyToSettings.includeLockAnim: + retime_frames(FCurve(fcurve), *retime_args) + actions_done.add(action) + + # Shape keys + if retimerApplyToSettings.applyToShapeKeys: + if ( + obj.type == "MESH" + and obj.data.shape_keys is not None + and obj.data.shape_keys.animation_data is not None + ): + action = obj.data.shape_keys.animation_data.action + if action is not None and action not in actions_done: + for fcurve in action.fcurves: + if not fcurve.lock or retimerApplyToSettings.includeLockAnim: + retime_frames(FCurve(fcurve), *retime_args) + actions_done.add(action) + + # Grease pencil + if retimerApplyToSettings.applytToGreasePencil: + # retime_args = (mode, start_incl, end_incl, join_gap, factor, pivot) + + if obj.type == "GPENCIL": + action_tmp_added = False + + if obj.animation_data is None: + obj.animation_data_create() + + if obj.animation_data is not None: + # when a stroke has no transform animation it has no action and because of a bug + # the stroke frames are not updated. As a turnaround we force an action and + # remove it afterward + if obj.animation_data.action is None: + + obj.animation_data.action = action_tmp + action_tmp_added = True + + action = obj.animation_data.action + if action is not None and action not in actions_done: + for fcurve in action.fcurves: + if not fcurve.lock or retimerApplyToSettings.includeLockAnim: + retime_frames(FCurve(fcurve), *retime_args) + if not action_tmp_added: + actions_done.add(action) + + for layer in obj.data.layers: + # print(f"Treating GP object: {obj.name} layer: {layer}") + if not layer.lock or retimerApplyToSettings.includeLockAnim: + retime_GPframes(layer, *retime_args) + + if action_tmp_added: + obj.animation_data.action = None + + # Force an update on the actions (cause bug. Other approach would be to save the file and reload it) + if obj.animation_data is not None: + if obj.animation_data.action is not None: + action_backup = obj.animation_data.action + obj.animation_data.action = None + obj.animation_data.action = action_backup + + # VSE + # no operation for CLEAR_ANIM + if "CLEAR_ANIM" != mode: + if retimerApplyToSettings.applyToVSE: + retime_vse(scene, mode, start_incl, end_incl) + + # Shots + if retimerApplyToSettings.applyToCameraShots: + props = scene.UAS_shot_manager_props + shotList = props.getShotsList(ignoreDisabled=False) + + if "CLEAR_ANIM" != mode: + for shot in shotList: + retime_shot(shot, *retime_args) + + # markers + if retimerApplyToSettings.applyToMarkers: + retime_markers(scene, *retime_args) + + # anim range + # NOTE: end_incl = start_incl + duration_incl - 1 <=> duration_incl = end_incl - start_incl + 1 + # def _compute_retimed_frame(frame_value, mode, start_incl, end_incl, duration_incl, pivot, factor): + # new_frame_value = frame_value + + # if "INSERT" == mode: + # if start_incl <= frame_value: + # new_frame_value = frame_value + duration_incl + # elif "DELETE" == mode: + # if start_incl <= frame_value: + # if end_incl < frame_value: + # new_frame_value = frame_value - duration_incl + # else: + # new_frame_value = start_incl + # elif "RESCALE" == mode: + # new_frame_value = rescale_frame(frame_value, start_incl, end_incl, pivot, factor) + + # # no operation for CLEAR_ANIM + + # return new_frame_value + + # anim range + + if retimerApplyToSettings.applyToSceneRange and "CLEAR_ANIM" != mode: + + # new_range_start = _compute_retimed_frame( + # scene.frame_start, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + # new_range_end = _compute_retimed_frame( + # scene.frame_end, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + # new_range_preview_start = _compute_retimed_frame( + # scene.frame_preview_start, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + # new_range_preview_end = _compute_retimed_frame( + # scene.frame_preview_end, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + new_range_start = computeNewFrameValue(scene.frame_start, mode, start_incl, end_incl, pivot, factor) + new_range_end = computeNewFrameValue(scene.frame_end, mode, start_incl, end_incl, pivot, factor) + new_range_preview_start = computeNewFrameValue( + scene.frame_preview_start, mode, start_incl, end_incl, pivot, factor + ) + new_range_preview_end = computeNewFrameValue(scene.frame_preview_end, mode, start_incl, end_incl, pivot, factor) + + # extension of the animation range end is wanted in these cases + if "INSERT" == mode: + if scene.frame_start == start_incl: + new_range_start = start_incl + if scene.frame_preview_start == start_incl: + new_range_preview_start = start_incl + + if scene.frame_end == start_incl: + new_range_end = end_incl + 1 + if scene.frame_end == start_incl: + new_range_preview_end = end_incl + 1 + + # print(f"\n scene range: new start_incl: {new_range_start}, new end_incl: {new_range_end}") + scene.frame_start = max(new_range_start, 0) + scene.frame_end = max(new_range_end, 0) + scene.frame_preview_start = max(new_range_preview_start, 0) + scene.frame_preview_end = max(new_range_preview_end, 0) + + # time cursor + if retimerApplyToSettings.applyToTimeCursor and "CLEAR_ANIM" != mode: + # new_current_frame = _compute_retimed_frame( + # current_frame, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + new_current_frame = computeNewFrameValue(current_frame, mode, start_incl, end_incl, pivot, factor) + if scene.use_preview_range: + rangeStart = scene.frame_preview_start + rangeEnd = scene.frame_preview_end + else: + rangeStart = scene.frame_start + rangeEnd = scene.frame_end + new_current_frame = max(new_current_frame, rangeStart) + new_current_frame = min(new_current_frame, rangeEnd) + scene.frame_set(new_current_frame) + + # NOTE: should be kept but returns an error message in the log: + # ERROR (bke.lib_id_delete): C:\Users\blender\git\blender-v320\blender.git\source\blender\blenkernel\intern\lib_id_delete.c:344 + # id_delete: Deleting ACRetimer_TmpAction which still has 1 users (including 0 'extra' shallow users) + # bpy.data.actions.remove(action_tmp) + + return () diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py index 53fc355b..66bf59a9 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py @@ -65,6 +65,7 @@ def __init__(self, targetArea, posY=2, width=32, alignment="BOTTOM_LEFT", parent # manipulation # filled when isManipulated changes self.manipulatedChildren = None + self.manipulationBeginningFrame = None # green or orange self.color_highlight = (0.2, 0.7, 0.2, 1) if self.isStart else (0.7, 0.3, 0.0, 1) @@ -159,9 +160,11 @@ def _on_manipulated_changed(self, context, isManipulated): self.parent.isManipulatedByAnotherComponent = True if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() + self.manipulationBeginningFrame = context.scene.frame_current else: self.parent.isManipulatedByAnotherComponent = False self.manipulatedChildren = None + self.manipulationBeginningFrame = None # override of InteractiveComponent def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): @@ -174,12 +177,13 @@ def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): if self.manipulatedChildren is not None: retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo retimerApplyToSettings.initialize("STORYBOARD_CLIP") + # retimeScene( # context, # "RESCALE", # retimerApplyToSettings, # self.manipulatedChildren, - # farRefPoint + 1, + # self.shot.end, # mouse_delta_frames, # True, # retimeEngine.factor, diff --git a/shotmanager/retimer/retimer.py b/shotmanager/retimer/retimer.py index 69f6544e..e25338f5 100644 --- a/shotmanager/retimer/retimer.py +++ b/shotmanager/retimer/retimer.py @@ -148,20 +148,68 @@ def __len__(self): ################################################ -def compute_offset(frame_value, pivot, factor): +def computeNewFrameValue(frame_value, mode, start_incl=0, end_incl=0, pivot=0, factor=1.0, roundToNearestFrame=True): + """Return the value of the time (in frames) after the retiming + Return None if the new time value is not available (deleted time for example) + It supports floating time frames. + ARgs: + roundToNearestFrame: round to nearest frame + """ + + new_frame_value = frame_value + # offset is also the duration_incl + offset = end_incl - start_incl + 1 + + if mode == "INSERT": + if start_incl <= frame_value: + return frame_value + offset + + elif mode == "DELETE" or mode == "CLEAR_ANIM": + if start_incl <= frame_value: + if frame_value <= end_incl: + # or return new_frame_value = start_incl, as in _compute_retimed_frame??? + new_frame_value = None + else: + # offset = end_incl - start_incl + 1 + new_frame_value = frame_value - offset + + elif mode == "RESCALE": + new_frame_value = rescale_frame(frame_value, start_incl, end_incl, pivot, factor, roundToNearestFrame=False) + + # if start_incl <= frame_value: + # if frame_value <= end_incl: + # return None + # else: + # offset = (end_incl - start_incl - 1) * factor - end_incl + start_incl - 1 + # # _logger.debug_ext(f" In computeNewFrameValue() Rescale mode: offset: {offset}") + # return frame_value + offset + + elif mode == "FREEZE": + pass + elif mode == "CLEAR_ANIM": + pass + + if roundToNearestFrame: + new_frame_value = round(new_frame_value) + return new_frame_value + + +def compute_offset(frame_value, pivot, factor, roundToNearestFrame=True): """Compute the new value of frame_value when scaled from the pivot and by the given factor""" duration_to_pivot = frame_value - pivot - return round(duration_to_pivot * factor) # + pivot + offset = duration_to_pivot * factor + return round(offset) if roundToNearestFrame else offset + # return round(duration_to_pivot * factor) # + pivot -def rescale_frame(frame_value, start_incl, end_incl, pivot, factor): +def rescale_frame(frame_value, start_incl, end_incl, pivot, factor, roundToNearestFrame=True): """Compute the new value of frame_value when scaled from the pivot and by the given factor in the specified range Note: this works only for frames, not floating points """ new_frame_value = frame_value duration_to_pivot = frame_value - pivot - round(duration_to_pivot * factor) + pivot + # round(duration_to_pivot * factor) + pivot if start_incl <= frame_value: if end_incl < frame_value: @@ -667,42 +715,6 @@ def retime_shot(shot, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1. pass -def computeNewTimeValue(frameVal, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): - """Return the value of the time (in frames) after the retiming - Return None if the new time value is not available (deleted time for example)""" - - newFrameVal = frameVal - - if mode == "INSERT": - offset = end_incl - start_incl + 1 - if start_incl <= frameVal: - return frameVal + offset - - elif mode == "DELETE" or mode == "CLEAR_ANIM": - if start_incl <= frameVal: - if frameVal <= end_incl: - return None - else: - offset = end_incl - start_incl + 1 - return frameVal - offset - - elif mode == "RESCALE": - newFrameVal = rescale_frame(frameVal, start_incl, end_incl, pivot, factor) - return newFrameVal - # if start_incl <= frameVal: - # if frameVal <= end_incl: - # return None - # else: - # offset = (end_incl - start_incl - 1) * factor - end_incl + start_incl - 1 - # # _logger.debug_ext(f" In computeNewTimeValue() Rescale mode: offset: {offset}") - # return frameVal + offset - - elif mode == "FREEZE": - pass - - return newFrameVal - - def retime_markers(scene, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): # NOTE: there can be several markers per frame!!! @@ -713,7 +725,7 @@ def retime_markers(scene, mode, start_incl=0, end_incl=0, remove_gap=True, facto markers = sortMarkers(scene.timeline_markers) if len(markers): for m in markers: - newFrameVal = computeNewTimeValue(m.frame, mode, start_incl, end_incl, remove_gap, factor, pivot) + newFrameVal = computeNewFrameValue(m.frame, mode, start_incl, end_incl, pivot, factor) if newFrameVal is None: # delete marker scene.timeline_markers.remove(m) @@ -784,7 +796,7 @@ def retimeScene( retimeMode: str, retimerApplyToSettings, objects, - start_incl: int, + start_incl: float, duration_incl: float, join_gap=True, factor=1.0, @@ -798,12 +810,8 @@ def retimeScene( """ prefs = config.getShotManagerPrefs() scene = context.scene - end_incl = start_incl + duration_incl - 1 - current_frame = scene.frame_current - print( - f" - retimeScene(): {retimeMode}, start_incl: {start_incl}, end_incl: {end_incl}, duration_incl: {duration_incl}" - ) + current_frame = scene.frame_current mode = retimeMode if "GLOBAL_OFFSET" == retimeMode: @@ -812,7 +820,12 @@ def retimeScene( else: mode = "DELETE" duration_incl = -1 * duration_incl - end_incl = start_incl + duration_incl - 1 + + end_incl = start_incl + duration_incl - 1 + + print( + f" - retimeScene(): {retimeMode}, start_incl: {start_incl}, end_incl: {end_incl}, duration_incl: {duration_incl}" + ) # print("Retiming scene: , factor: ", mode, factor) retime_args = (mode, start_incl, end_incl, join_gap, factor, pivot) @@ -912,40 +925,53 @@ def retimeScene( retime_markers(scene, *retime_args) # anim range - def _compute_retimed_frame(frame_value, mode, start_incl, end_incl, duration_incl, pivot, factor): - new_frame_value = frame_value - - if "INSERT" == mode: - if start_incl <= frame_value: - new_frame_value = frame_value + duration_incl - elif "DELETE" == mode: - if start_incl <= frame_value: - if end_incl < frame_value: - new_frame_value = frame_value - duration_incl - else: - new_frame_value = start_incl - elif "RESCALE" == mode: - new_frame_value = rescale_frame(frame_value, start_incl, end_incl, pivot, factor) - - # no operation for CLEAR_ANIM - - return new_frame_value + # NOTE: end_incl = start_incl + duration_incl - 1 <=> duration_incl = end_incl - start_incl + 1 + # def _compute_retimed_frame(frame_value, mode, start_incl, end_incl, duration_incl, pivot, factor): + # new_frame_value = frame_value + + # if "INSERT" == mode: + # if start_incl <= frame_value: + # new_frame_value = frame_value + duration_incl + # elif "DELETE" == mode: + # if start_incl <= frame_value: + # if end_incl < frame_value: + # new_frame_value = frame_value - duration_incl + # else: + # new_frame_value = start_incl + # elif "RESCALE" == mode: + # new_frame_value = rescale_frame(frame_value, start_incl, end_incl, pivot, factor) + + # # no operation for CLEAR_ANIM + + # return new_frame_value # anim range if retimerApplyToSettings.applyToSceneRange and "CLEAR_ANIM" != mode: - new_range_start = _compute_retimed_frame( - scene.frame_start, mode, start_incl, end_incl, duration_incl, pivot, factor + # new_range_start = _compute_retimed_frame( + # scene.frame_start, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + # new_range_end = _compute_retimed_frame( + # scene.frame_end, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + # new_range_preview_start = _compute_retimed_frame( + # scene.frame_preview_start, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + # new_range_preview_end = _compute_retimed_frame( + # scene.frame_preview_end, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + new_range_start = computeNewFrameValue( + scene.frame_start, mode, start_incl, end_incl, pivot, factor, roundToNearestFrame=True ) - new_range_end = _compute_retimed_frame( - scene.frame_end, mode, start_incl, end_incl, duration_incl, pivot, factor + new_range_end = computeNewFrameValue( + scene.frame_end, mode, start_incl, end_incl, pivot, factor, roundToNearestFrame=True ) - new_range_preview_start = _compute_retimed_frame( - scene.frame_preview_start, mode, start_incl, end_incl, duration_incl, pivot, factor + new_range_preview_start = computeNewFrameValue( + scene.frame_preview_start, mode, start_incl, end_incl, pivot, factor, roundToNearestFrame=True ) - new_range_preview_end = _compute_retimed_frame( - scene.frame_preview_end, mode, start_incl, end_incl, duration_incl, pivot, factor + new_range_preview_end = computeNewFrameValue( + scene.frame_preview_end, mode, start_incl, end_incl, pivot, factor, roundToNearestFrame=True ) # extension of the animation range end is wanted in these cases @@ -968,9 +994,10 @@ def _compute_retimed_frame(frame_value, mode, start_incl, end_incl, duration_inc # time cursor if retimerApplyToSettings.applyToTimeCursor and "CLEAR_ANIM" != mode: - new_current_frame = _compute_retimed_frame( - current_frame, mode, start_incl, end_incl, duration_incl, pivot, factor - ) + # new_current_frame = _compute_retimed_frame( + # current_frame, mode, start_incl, end_incl, duration_incl, pivot, factor + # ) + new_current_frame = computeNewFrameValue(current_frame, mode, start_incl, end_incl, pivot, factor) if scene.use_preview_range: rangeStart = scene.frame_preview_start rangeEnd = scene.frame_preview_end From c3716b2cb079a70aee0b0f24fc624c45cb100682 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Mon, 12 Sep 2022 18:54:41 +0200 Subject: [PATCH 08/27] feat: on Win setting to get the screen res --- CHANGELOG.md | 5 ++ resources/wkzipaddon/wkzipaddon.py | 4 +- shotmanager/__init__.py | 10 +-- shotmanager/addon_prefs/addon_prefs.py | 62 ++++++++++++++++--- shotmanager/config/config.py | 7 +-- shotmanager/operators/general.py | 6 +- .../interact_shots_stack/shots_stack_prefs.py | 9 +++ shotmanager/prefs/prefs_features.py | 2 +- shotmanager/utils/utils_editors_dopesheet.py | 6 +- 9 files changed, 89 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb2e9f4e..0970e079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +----- +## 2.0.226 (2022-09-12) +### Shots Stack UI +- Added a user preference to automaticaly detect the sceen display factor (Windows only) + ----- ## 2.0.224 (2022-08-24) **Pre-Release** diff --git a/resources/wkzipaddon/wkzipaddon.py b/resources/wkzipaddon/wkzipaddon.py index 670be88f..e01f322e 100644 --- a/resources/wkzipaddon/wkzipaddon.py +++ b/resources/wkzipaddon/wkzipaddon.py @@ -27,8 +27,8 @@ def main(): # to use for debug in VSC: # pathArr = [ - # "self", - # "Z:\\EvalSofts\\Blender\\DevPython\\WkZipAddon\\WkSamples\\myAddon_Addon\\myaddon", + # "self", + # "Z:\\EvalSofts\\Blender\\DevPython\\WkZipAddon\\WkSamples\\myAddon_Addon\\myaddon", # ] # "Z:\\EvalSofts\\Blender\\DevPython\\WkZipAddon\\WkSamples\\toto.py", diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index a22fcdae..b1677799 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,12 +84,12 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 0, 224), + "version": (2, 0, 226), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", - # "warning": "BETA Version", - "warning": "Pre-Release", + "warning": "BETA Version", + # "warning": "Pre-Release", "category": "Ubisoft", } @@ -321,7 +321,9 @@ def _update_UAS_shot_manager_identify_dopesheets(self, context): options=set(), ) - bpy.types.WindowManager.UAS_shot_manager_shots_stack_retimerApplyTo = PointerProperty(type=UAS_Retimer_ApplyToSettings) + bpy.types.WindowManager.UAS_shot_manager_shots_stack_retimerApplyTo = PointerProperty( + type=UAS_Retimer_ApplyToSettings + ) if config.devDebug: print(f"\n ------ Ubisoft Shot Manager debug: {config.devDebug} ------- ") diff --git a/shotmanager/addon_prefs/addon_prefs.py b/shotmanager/addon_prefs/addon_prefs.py index ce0e03e0..c3c95a85 100644 --- a/shotmanager/addon_prefs/addon_prefs.py +++ b/shotmanager/addon_prefs/addon_prefs.py @@ -19,6 +19,9 @@ add-on global preferences """ +import platform +import ctypes + import bpy from bpy.types import AddonPreferences from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty, FloatProperty, PointerProperty @@ -794,7 +797,7 @@ def _update_layersListDropdown(self, context): ) ######################################################################## - # Overlay tools + # overlay tools ######################################################################## # tools disabled during play @@ -826,7 +829,7 @@ def _update_layersListDropdown(self, context): ) ######################################################################## - # Retimer + # retimer ######################################################################## retimer_applyTo_expanded: BoolProperty( @@ -855,7 +858,7 @@ def _update_layersListDropdown(self, context): ######################################################################## ################################### - # Sequence Timeline ############### + # sequence timeline ############### ################################### # displayed when toggle overlays button is on @@ -886,7 +889,7 @@ def _update_toggle_overlays_turnOn_sequenceTimeline(self, context): ) ################################### - # Interactive Shots Stack ######### + # interactive shots stack ######### ################################### # displayed when toggle overlays button is on @@ -951,8 +954,53 @@ def _update_display_shtStack_toolbar(self, context): default=0.7, ) + def _update_shtStack_screen_display_factor_mode(self, context): + # read also: + # https://stackoverflow.com/questions/53889520/getting-screen-pixels-taking-into-account-the-scale-factor + if "Windows" == platform.system(): + if "AUTO" == self.shtStack_screen_display_factor_mode: + self.shtStack_screen_display_factor = ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100 + elif "100" == self.shtStack_screen_display_factor_mode: + self.shtStack_screen_display_factor = 1.0 + elif "125" == self.shtStack_screen_display_factor_mode: + self.shtStack_screen_display_factor = 1.25 + elif "150" == self.shtStack_screen_display_factor_mode: + self.shtStack_screen_display_factor = 1.5 + elif "175" == self.shtStack_screen_display_factor_mode: + self.shtStack_screen_display_factor = 1.75 + else: + self.shtStack_screen_display_factor = 1.0 + + shtStack_screen_display_factor_mode: EnumProperty( + name="Windows Screen Factor Mode", + description=( + "*** Windows Only ***" + "Set the scale factor for the display of the Shots Stack so that the shot clips match the line size of the Dopesheet editor." + "In Windows this parameter corresponds to the Display Scale Percentage. Usually it should be let to Auto, unless you" + "use 2 screens with different display factors" + ), + items=( + ("AUTO", "Auto", "Screen resolution factor is automatically detected"), + ("100", "100%", "(default)"), + ("125", "125%", ""), + ("150", "150%", ""), + ("175", "175%", ""), + ), + update=_update_shtStack_screen_display_factor_mode, + default="AUTO", + ) + + shtStack_screen_display_factor: FloatProperty( + name="Windows Screen Factor Value", + description="Hidden value", + min=1.0, + max=1.75, + step=0.25, + default=1.0, + ) + ################################### - # Camera HUD ###################### + # camera hud ###################### ################################### cameraHUD_shotNameSize: IntProperty( @@ -970,7 +1018,7 @@ def _update_display_shtStack_toolbar(self, context): ) ################################### - # Frame Range tool ################ + # frame range tool ################ ################################### def _update_display_frame_range_tool(self, context): @@ -987,7 +1035,7 @@ def _update_display_frame_range_tool(self, context): ) ################################### - # Markers Nav Bar Tool ############ + # markers nav bar tool ############ ################################### def _update_display_markersNavBar_tool(self, context): diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index 622fef77..1afdd92e 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -101,12 +101,9 @@ def initGlobalVariables(): global gModulePath gModulePath = None - global gShotsStack_ui_scale - gShotsStack_ui_scale = 1.25 - # dependencies ############# - global STAMP_INFO_MIN_VERSION - STAMP_INFO_MIN_VERSION = ("1.3.5", 1003005) + # global STAMP_INFO_MIN_VERSION + # STAMP_INFO_MIN_VERSION = ("1.3.5", 1003005) # otio ############# global gImportOpenTimelineIO diff --git a/shotmanager/operators/general.py b/shotmanager/operators/general.py index f8b611dd..8ab1eb1c 100644 --- a/shotmanager/operators/general.py +++ b/shotmanager/operators/general.py @@ -64,7 +64,11 @@ def poll(cls, context): # _logger.debug_ext(f"uas_shot_manager.display_overlay_tools Poll", col="PURPLE") return len(context.scene.UAS_shot_manager_props.get_shots()) - def invoke(self, context, event): + def execute(self, context): + prefs = config.getShotManagerPrefs() + # we force the update of the prefs display factor value + prefs.shtStack_screen_display_factor_mode = prefs.shtStack_screen_display_factor_mode + # _logger.debug_ext(f"uas_shot_manager.display_overlay_tools Invoke", col="PURPLE") context.window_manager.UAS_shot_manager_display_overlay_tools = ( not context.window_manager.UAS_shot_manager_display_overlay_tools diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py index 245727dd..b7c93f21 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py @@ -19,6 +19,8 @@ Settings panel for the Interactive Shots Stack overlay tool """ +import platform + import bpy from bpy.types import Operator @@ -44,6 +46,13 @@ def draw_settings(context, layout): propCol.prop(prefs, "shtStack_link_stb_clips_to_keys") + diplFactRow = propCol.row(align=False) + diplFactRow.enabled = "Windows" == platform.system() + diplFactRow.label(text="Windows Only - Display Screen Factor:") + diplFactSubRow = diplFactRow.row(align=True) + diplFactSubRow.ui_units_x = 4 + diplFactSubRow.prop(prefs, "shtStack_screen_display_factor_mode", text="") + # def draw_settings_in_menu(self, context): # """Used in Shot Manager Feature Toggles panel diff --git a/shotmanager/prefs/prefs_features.py b/shotmanager/prefs/prefs_features.py index 6c3dc3fe..1a772b7e 100644 --- a/shotmanager/prefs/prefs_features.py +++ b/shotmanager/prefs/prefs_features.py @@ -464,7 +464,7 @@ def _draw_separator_row(layout, factor=0.9): icon="NLA_PUSHDOWN", depress=prefs.toggle_overlays_turnOn_interactiveShotsStack, ) - subrow.label(text="Interaction Shots Stack") + subrow.label(text="Interactive Shots Stack") subrowright = subrow.row() subrowright.alignment = "RIGHT" diff --git a/shotmanager/utils/utils_editors_dopesheet.py b/shotmanager/utils/utils_editors_dopesheet.py index 23ca3a4f..9990541f 100644 --- a/shotmanager/utils/utils_editors_dopesheet.py +++ b/shotmanager/utils/utils_editors_dopesheet.py @@ -93,14 +93,16 @@ def getPrefsUIScale(): def getRulerHeight(): """Return the height in pixels of the time ruler of a dopesheet""" - RULER_HEIGHT = 23 * config.gShotsStack_ui_scale + prefs = config.getShotManagerPrefs() + RULER_HEIGHT = 23 * prefs.shtStack_screen_display_factor # RULER_HEIGHT = 28 # on laptop return RULER_HEIGHT * getPrefsUIScale() def getLaneHeight(): """Return the height of a lane in pixels""" - LANE_HEIGHT = 18 * config.gShotsStack_ui_scale + prefs = config.getShotManagerPrefs() + LANE_HEIGHT = 18 * prefs.shtStack_screen_display_factor # LANE_HEIGHT = 18.5 # LANE_HEIGHT = 22.5 # on laptop return LANE_HEIGHT * getPrefsUIScale() From ef9b6647c80e2eca77f09446734e10afd1c6389a Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Tue, 13 Sep 2022 09:03:40 +0200 Subject: [PATCH 09/27] Fix retimer as floating values --- CHANGELOG.md | 4 ++ shotmanager/__init__.py | 4 +- shotmanager/config/config.py | 4 +- shotmanager/debug/sm_debug_ui.py | 57 ++++++++++++++++++- .../gpu/gpu_2d/class_InteractiveComponent.py | 22 +++---- .../widgets/shots_stack_clip_component.py | 11 ++-- .../widgets/shots_stack_handle_component.py | 43 +++++++------- shotmanager/retimer/retimer.py | 12 +++- 8 files changed, 114 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0970e079..2fd0788f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +----- +## 2.1.001 (2022-09-12) +### Shots Stack UI + ----- ## 2.0.226 (2022-09-12) ### Shots Stack UI diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index b1677799..2a775ce3 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 0, 226), + "version": (2, 1, 1), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", @@ -329,7 +329,7 @@ def _update_UAS_shot_manager_identify_dopesheets(self, context): print(f"\n ------ Ubisoft Shot Manager debug: {config.devDebug} ------- ") addon_prefs_inst = config.getShotManagerPrefs() - addon_prefs_inst.displaySMDebugPanel = False + addon_prefs_inst.displaySMDebugPanel = True # _props = bpy.context.scene.UAS_shot_manager_props # # currentLayout = props.getCurrentLayout() diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index 1afdd92e..e6bedcfe 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -142,8 +142,8 @@ def getLoggingTags(): # debug tags tags["DEPRECATED"] = False - tags["REG"] = False - tags["UNREG"] = False + tags["REG"] = True + tags["UNREG"] = True tags["INIT_AND_DATA"] = False diff --git a/shotmanager/debug/sm_debug_ui.py b/shotmanager/debug/sm_debug_ui.py index 61bce5d5..ba529dcc 100644 --- a/shotmanager/debug/sm_debug_ui.py +++ b/shotmanager/debug/sm_debug_ui.py @@ -93,7 +93,8 @@ def draw(self, context): # https://blender.stackexchange.com/questions/64129/get-blender-scripts-path # x = bpy.utils.script_path_user() # bpy.utils.script_paths() returns the list of script folders - filePath = utils.getAddonsFolder() + # filePath = utils.getAddonsFolder() + filePath = utils.getPythonPackagesFolder() row.operator( "uas_shot_manager.open_explorer", text="Open add-ons Folder", icon_value=iconExplorer.icon_id ).path = filePath @@ -157,6 +158,60 @@ def draw(self, context): layout.separator() + self.drawDebugAnim(layout) + + def drawDebugAnim(self, layout): + def _getSelectedKeysInDopesheet(): + keys = [] + for area in bpy.context.screen.areas: # loop through areas + if area.type == "DOPESHEET_EDITOR": # find the dopesheet + dopesheet = area.spaces[0] + # print(dopesheet.type) + action = dopesheet.action + if action: + for fcurve in action.fcurves: + for p in fcurve.keyframe_points: + # print(p.co[0], p.select_control_point) + keys.append(p) + break + return keys + + def _getSelectedKeysOfSelObj(): + # https://blender.stackexchange.com/questions/28005/how-do-i-know-if-i-have-a-selected-keyframe-using-python + keys = [] + obj = bpy.context.object + + if not obj: + return keys + + if "GPENCIL" == obj.type: + gpencil = obj + for gpLayer in gpencil.data.layers: + for kf in gpLayer.frames: + if kf.select: + keys.append([kf.frame_number]) + + else: + if obj.animation_data: + action = obj.animation_data.action + + for fcurve in action.fcurves: + for p in fcurve.keyframe_points: + if p.select_control_point: + # print(p.co[0], p.select_control_point) + keys.append([p.co[0]]) + return keys + + keys = _getSelectedKeysOfSelObj() + layout.label(text=f"Selected keys: {len(keys)}") + if len(keys): + for i, k in enumerate(keys): + if i < 3: + layout.label(text=f" key at fr. {k[0]}") + else: + layout.label(text=f"... and {len(keys) - 3} other keys") + break + _classes = (UAS_PT_Shot_Manager_Debug,) diff --git a/shotmanager/gpu/gpu_2d/class_InteractiveComponent.py b/shotmanager/gpu/gpu_2d/class_InteractiveComponent.py index e271d5b7..c7ee5a24 100644 --- a/shotmanager/gpu/gpu_2d/class_InteractiveComponent.py +++ b/shotmanager/gpu/gpu_2d/class_InteractiveComponent.py @@ -80,7 +80,7 @@ def isHighlighted(self): @isHighlighted.setter def isHighlighted(self, value): self._isHighlighted = value - self._on_highlighted_changed(self.context, value) + self._on_highlighted_changed(self.context, None, value) @property def isSelected(self): @@ -89,7 +89,7 @@ def isSelected(self): @isSelected.setter def isSelected(self, value): self._isSelected = value - self._on_selected_changed(self.context, value) + self._on_selected_changed(self.context, None, value) @property def isManipulated(self): @@ -98,7 +98,7 @@ def isManipulated(self): @isManipulated.setter def isManipulated(self, value): self._isManipulated = value - self._on_manipulated_changed(self.context, value) + self._on_manipulated_changed(self.context, None, value) ################################################################# # functions ######## @@ -159,11 +159,11 @@ def _event_highlight(self, context, event, region): if not self.isHighlighted: self.isHighlighted = True # _logger.debug_ext("component2D handle_events set highlighte true", col="PURPLE", tag="EVENT") - # self._on_highlighted_changed(context, self.isHighlighted) + # self._on_highlighted_changed(context, event, self.isHighlighted) else: if self.isHighlighted: self.isHighlighted = False - # self._on_highlighted_changed(context, self.isHighlighted) + # self._on_highlighted_changed(context, event, self.isHighlighted) # # to override by inheriting classes # def _handle_event_custom(self, context, event, region): @@ -262,7 +262,7 @@ def _handle_event_custom(self, context, event, region): if not self.isSelected: self.isSelected = True # _logger.debug_ext("component2D handle_events set selected true", col="PURPLE", tag="EVENT") - # self._on_selected_changed(context, self.isSelected) + # self._on_selected_changed(context, event, self.isSelected) # manipulation ################# self.isManipulated = True @@ -297,7 +297,7 @@ def _handle_event_custom(self, context, event, region): mouse_frame = int(region.view2d.region_to_view(event.mouse_x - region.x, 0)[0]) prev_mouse_frame = int(region.view2d.region_to_view(self.prev_mouse_x, 0)[0]) if mouse_frame != self.mouseFrame or prev_mouse_frame != self.previousMouseFrame: - self._on_manipulated_mouse_moved(context, mouse_delta_frames=mouse_frame - prev_mouse_frame) + self._on_manipulated_mouse_moved(context, event, mouse_delta_frames=mouse_frame - prev_mouse_frame) self.mouseFrame = mouse_frame self.previousMouseFrame = prev_mouse_frame @@ -339,7 +339,7 @@ def cancelAction(self, context): # self._on_manipulated_changed(context, self.isManipulated) # to override by inheriting classes - def _on_highlighted_changed(self, context, isHighlighted): + def _on_highlighted_changed(self, context, event, isHighlighted): """isHighlighted has the same value than self.isHighlighted, which is set right before this function is called """ @@ -350,7 +350,7 @@ def _on_highlighted_changed(self, context, isHighlighted): pass # to override by inheriting classes - def _on_selected_changed(self, context, isSelected): + def _on_selected_changed(self, context, event, isSelected): """isSelected has the same value than self.isSelected, which is set right before this function is called """ @@ -361,7 +361,7 @@ def _on_selected_changed(self, context, isSelected): pass # to override by inheriting classes - def _on_manipulated_changed(self, context, isManipulated): + def _on_manipulated_changed(self, context, event, isManipulated): """isManipulated has the same value than self.isManipulated, which is set right before this function is called """ @@ -372,7 +372,7 @@ def _on_manipulated_changed(self, context, isManipulated): pass # to override by inheriting classes - def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): + def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): """wkip note: mouse_delta_frames is in frames but may need to be in pixels in some cases""" if mouse_delta_frames: # _logger.debug_ext("component2D handle_events set manipulated true", col="PURPLE", tag="EVENT" diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py index 6ce55a47..391abf3b 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py @@ -357,7 +357,7 @@ def draw(self, shader=None, region=None, draw_types="TRIS", cap_lines=False, pre ###################################################################### # override of InteractiveComponent - def _on_highlighted_changed(self, context, isHighlighted): + def _on_highlighted_changed(self, context, event, isHighlighted): """isHighlighted has the same value than self.isHighlighted, which is set right before this function is called """ @@ -368,7 +368,7 @@ def _on_highlighted_changed(self, context, isHighlighted): config.gRedrawShotStack = True # override of InteractiveComponent - def _on_selected_changed(self, context, isSelected): + def _on_selected_changed(self, context, event, isSelected): if isSelected: _logger.debug_ext("\n\nClip isSelected set to True", col="RED") props = context.scene.UAS_shot_manager_props @@ -379,7 +379,7 @@ def _on_selected_changed(self, context, isSelected): prefs.shot_selected_from_shots_stack__flag = False # override of InteractiveComponent - def _on_manipulated_changed(self, context, isManipulated): + def _on_manipulated_changed(self, context, event, isManipulated): """isManipulated has the same value than self.isManipulated, which is set right before this function is called """ @@ -394,7 +394,7 @@ def _on_manipulated_changed(self, context, isManipulated): pass # override of InteractiveComponent - def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): + def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): """wkip note: delta_frames is in frames but may need to be in pixels in some cases""" # Very important, don't use properties for changing both start and ends. Depending of the amount of displacement duration can change. if self.shot.durationLocked: @@ -412,7 +412,8 @@ def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): self.shot.start += mouse_delta_frames self.shot.end += mouse_delta_frames - if self.manipulatedChildren is not None: + prefs = config.getShotManagerPrefs() + if prefs.shtStack_link_stb_clips_to_keys and self.manipulatedChildren is not None: retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo retimerApplyToSettings.initialize("STORYBOARD_CLIP") diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py index 66bf59a9..484b6c9c 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py @@ -131,7 +131,7 @@ def draw(self, shader=None, region=None, draw_types="TRIS", cap_lines=False, pre ################################################################# # override of InteractiveComponent - def _on_highlighted_changed(self, context, isHighlighted): + def _on_highlighted_changed(self, context, event, isHighlighted): """isHighlighted has the same value than self.isHighlighted, which is set right before this function is called """ @@ -142,7 +142,7 @@ def _on_highlighted_changed(self, context, isHighlighted): config.gRedrawShotStack = True # override of InteractiveComponent - def _on_selected_changed(self, context, isSelected): + def _on_selected_changed(self, context, event, isSelected): """isSelected has the same value than self.isSelected, which is set right before this function is called """ @@ -151,7 +151,7 @@ def _on_selected_changed(self, context, isSelected): self.isSelected = False # override of InteractiveComponent - def _on_manipulated_changed(self, context, isManipulated): + def _on_manipulated_changed(self, context, event, isManipulated): """isManipulated has the same value than self.isManipulated, which is set right before this function is called """ @@ -167,28 +167,33 @@ def _on_manipulated_changed(self, context, isManipulated): self.manipulationBeginningFrame = None # override of InteractiveComponent - def _on_manipulated_mouse_moved(self, context, mouse_delta_frames=0): + def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): """wkip note: delta_frames is in frames but may need to be in pixels in some cases""" # !! we have to be sure we work on the selected shot !!! if self.isStart: + prevShotStart = self.shot.start self.shot.start += mouse_delta_frames # bpy.ops.uas_shot_manager.set_shot_start(newStart=self.start + mouse_delta_frames) - if self.manipulatedChildren is not None: - retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo - retimerApplyToSettings.initialize("STORYBOARD_CLIP") - - # retimeScene( - # context, - # "RESCALE", - # retimerApplyToSettings, - # self.manipulatedChildren, - # self.shot.end, - # mouse_delta_frames, - # True, - # retimeEngine.factor, - # start_excl, - # ) + prefs = config.getShotManagerPrefs() + if prefs.shtStack_link_stb_clips_to_keys and event.ctrl: + if self.manipulatedChildren is not None: + retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo + retimerApplyToSettings.initialize("STORYBOARD_CLIP") + + retimeFactor = (self.shot.end - self.shot.start) / (self.shot.end - prevShotStart) + # retimeFactor = 0.5 + retimeScene( + context, + "RESCALE", + retimerApplyToSettings, + self.manipulatedChildren, + -10000, + 90000, + True, + retimeFactor, + self.shot.end, + ) else: self.shot.end += mouse_delta_frames diff --git a/shotmanager/retimer/retimer.py b/shotmanager/retimer/retimer.py index e25338f5..c5f40851 100644 --- a/shotmanager/retimer/retimer.py +++ b/shotmanager/retimer/retimer.py @@ -715,7 +715,9 @@ def retime_shot(shot, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1. pass -def retime_markers(scene, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): +def retime_markers( + scene, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0, roundToNearestFrame=True +): # NOTE: there can be several markers per frame!!! if mode == "RESCALE": @@ -725,7 +727,9 @@ def retime_markers(scene, mode, start_incl=0, end_incl=0, remove_gap=True, facto markers = sortMarkers(scene.timeline_markers) if len(markers): for m in markers: - newFrameVal = computeNewFrameValue(m.frame, mode, start_incl, end_incl, pivot, factor) + newFrameVal = computeNewFrameValue( + m.frame, mode, start_incl, end_incl, pivot, factor, roundToNearestFrame=True + ) if newFrameVal is None: # delete marker scene.timeline_markers.remove(m) @@ -997,7 +1001,9 @@ def retimeScene( # new_current_frame = _compute_retimed_frame( # current_frame, mode, start_incl, end_incl, duration_incl, pivot, factor # ) - new_current_frame = computeNewFrameValue(current_frame, mode, start_incl, end_incl, pivot, factor) + new_current_frame = computeNewFrameValue( + current_frame, mode, start_incl, end_incl, pivot, factor, roundToNearestFrame=True + ) if scene.use_preview_range: rangeStart = scene.frame_preview_start rangeEnd = scene.frame_preview_end From 96c729d61c04c32cfbf0773f48821f1b7dda71c0 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Tue, 13 Sep 2022 14:21:28 +0200 Subject: [PATCH 10/27] feat: improved keymap for shot navigation --- CHANGELOG.md | 5 +- shotmanager/__init__.py | 1 + shotmanager/addon_prefs/addon_prefs.py | 28 ++- shotmanager/addon_prefs/addon_prefs_ui.py | 20 +- shotmanager/config/config.py | 6 +- .../greasepencil/greasepencil_operators.py | 2 +- shotmanager/keymaps/__init__.py | 17 +- shotmanager/keymaps/general_keymaps.py | 205 ++-------------- shotmanager/keymaps/playbar_keymaps.py | 225 ++++++++++++++++++ .../keymaps/playbar_wrappers_operators.py | 129 ++++++++++ shotmanager/keymaps/storyboard_keymaps.py | 23 +- shotmanager/operators/general.py | 6 +- shotmanager/operators/playbar.py | 2 +- 13 files changed, 459 insertions(+), 210 deletions(-) create mode 100644 shotmanager/keymaps/playbar_keymaps.py create mode 100644 shotmanager/keymaps/playbar_wrappers_operators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd0788f..2b5313c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ----- ## 2.1.001 (2022-09-12) -### Shots Stack UI +### Keymaps +- Separated key mappings per category +- Added a Preferences parameter to toggle the vertical arrows used to navigate from shot to shot +- Made the up arrow go to next shots by default instead of previous ones ----- ## 2.0.226 (2022-09-12) diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index 2a775ce3..84ffa198 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -109,6 +109,7 @@ def register(): if config.devDebug: _logger.setLevel("DEBUG") # CRITICAL ERROR WARNING INFO DEBUG NOTSET + _logger.tags = config.getLoggingTags() logger_level = f"Logger level: {sm_logging.getLevelName()}" versionTupple = utils.display_addon_registered_version("Ubisoft Shot Manager", more_info=logger_level) diff --git a/shotmanager/addon_prefs/addon_prefs.py b/shotmanager/addon_prefs/addon_prefs.py index c3c95a85..5e28b43a 100644 --- a/shotmanager/addon_prefs/addon_prefs.py +++ b/shotmanager/addon_prefs/addon_prefs.py @@ -38,6 +38,8 @@ from shotmanager.tools.frame_range.frame_range_operators import display_frame_range_in_editor from shotmanager.tools.markers_nav_bar.markers_nav_bar import display_markersNavBar_in_editor +from shotmanager.keymaps import playbar_keymaps + from shotmanager.config import config from shotmanager.config import sm_logging @@ -439,6 +441,10 @@ def _set_stb_global_visibility(self, value): name="Expand Stamp Info Preferences", default=False, ) + addonPrefs_keymapping_expanded: BoolProperty( + name="Expand Key Mapping Preferences", + default=False, + ) addonPrefs_debug_expanded: BoolProperty( name="Expand Debug Preferences", default=False, @@ -1516,7 +1522,7 @@ def _set_addShot_end(self, value): ################## #################### - # Playblast + # playblast #################### playblastFileName: StringProperty(name="Temporary Playblast File", default="toto.mp4") @@ -1536,11 +1542,29 @@ def _set_addShot_end(self, value): ) ################################################################################## - # Draw + # draw ################################################################################## def draw(self, context): draw_addon_prefs(self, context) + ################################################################################## + # key mapping + ################################################################################## + + def _update_kmap_shots_nav_invert_direction(self, context): + playbar_keymaps.unregisterKeymaps() + playbar_keymaps.registerKeymaps() + + kmap_shots_nav_invert_direction: BoolProperty( + name="Invert Shots Navigation Direction", + description=( + "Invert Up and Down arrows to navigate between shots boundaries." + "\nBy default Up goes to Next shots and Down to Previous shots" + ), + update=_update_kmap_shots_nav_invert_direction, + default=False, + ) + _classes = (UAS_ShotManager_AddonPrefs,) diff --git a/shotmanager/addon_prefs/addon_prefs_ui.py b/shotmanager/addon_prefs/addon_prefs_ui.py index 549b9396..2c5128d2 100644 --- a/shotmanager/addon_prefs/addon_prefs_ui.py +++ b/shotmanager/addon_prefs/addon_prefs_ui.py @@ -21,7 +21,7 @@ from shotmanager.config import config from shotmanager.ui.dependencies_ui import drawDependencies -from shotmanager.utils.utils_ui import collapsable_panel +from shotmanager.utils.utils_ui import collapsable_panel, propertyColumn from shotmanager.prefs.prefs_features import draw_features_prefs @@ -34,6 +34,7 @@ def draw_addon_prefs(self, context): layout = self.layout layout = layout.column(align=False) + padding_left = 4 # Dependencies ############### @@ -63,6 +64,10 @@ def draw_addon_prefs(self, context): ############### drawStampInfo(context, self, layout) + # Key Mapping + ############### + drawKeyMapping(context, self, layout, padding_left) + # Tools ############### # box = layout.box() @@ -252,6 +257,19 @@ def drawFeatures(context, prefs, layout): draw_features_prefs("ADDON_PREFS", box) +def drawKeyMapping(context, prefs, layout, padding_left): + box = layout.box() + title = "Key Mapping" + collapsable_panel(box, prefs, "addonPrefs_keymapping_expanded", text=title) + if prefs.addonPrefs_keymapping_expanded: + propCol = propertyColumn(box, padding_left=padding_left, padding_bottom=0.2, scale_y=0.8) + propCol.label(text="Key mappings are located in the Keymap section of this Preferences panel.") + propCol.label(text='They can be listed by typing "Ubisoft Shot Mng" in the Keympap Search field.') + + propCol = propertyColumn(box, padding_left=padding_left) + propCol.prop(prefs, "kmap_shots_nav_invert_direction") + + def drawDevAndDebug(context, prefs, layout): box = layout.box() collapsable_panel(box, prefs, "addonPrefs_debug_expanded", text="Dev and Debug") diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index e6bedcfe..735a3d99 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -87,6 +87,10 @@ def initGlobalVariables(): # keymaps ########## global gAddonKeymaps gAddonKeymaps = [] + global gAddonKeymaps_shotsNav + gAddonKeymaps_shotsNav = [] + global gAddonKeymaps_storyboard + gAddonKeymaps_storyboard = [] # interactive shots stack ############ global tmpTimelineModalRect @@ -145,7 +149,7 @@ def getLoggingTags(): tags["REG"] = True tags["UNREG"] = True - tags["INIT_AND_DATA"] = False + tags["INIT_AND_DATA"] = True tags["SHOTS_PLAY_MODE"] = True diff --git a/shotmanager/features/greasepencil/greasepencil_operators.py b/shotmanager/features/greasepencil/greasepencil_operators.py index c3dd69ae..fa48b728 100644 --- a/shotmanager/features/greasepencil/greasepencil_operators.py +++ b/shotmanager/features/greasepencil/greasepencil_operators.py @@ -840,7 +840,7 @@ def execute(self, context): class UAS_ShotManager_GreasePencil_NavigateInKeyFrames(Operator): bl_idname = "uas_shot_manager.greasepencil_navigateinkeyframes" - bl_label = "Shot Manager - Navigate in grease pencil key frames" + bl_label = "Ubisoft Shot Mng - Navigate in grease pencil key frames" bl_description = "Jump from key frame to key frame on the specified grease pencil layer" bl_options = {"INTERNAL", "UNDO"} diff --git a/shotmanager/keymaps/__init__.py b/shotmanager/keymaps/__init__.py index ed7c4785..c079b5e9 100644 --- a/shotmanager/keymaps/__init__.py +++ b/shotmanager/keymaps/__init__.py @@ -22,10 +22,11 @@ """ from . import general_keymaps +from . import playbar_wrappers_operators +from . import playbar_keymaps from . import storyboard_keymaps from shotmanager import config - from shotmanager.config import sm_logging _logger = sm_logging.getLogger(__name__) @@ -33,7 +34,9 @@ def register(): _logger.debug_ext(" - Registering Keymaps Package", form="REG") + playbar_wrappers_operators.register() general_keymaps.registerKeymaps() + playbar_keymaps.registerKeymaps() storyboard_keymaps.registerKeymaps() @@ -41,6 +44,12 @@ def unregister(): _logger.debug_ext(" - Unregistering Keymaps Package", form="UNREG") # Remove the hotkeys - for km, kmi in config.gAddonKeymaps: - km.keymap_items.remove(kmi) - config.gAddonKeymaps.clear() + # for km, kmi in config.gAddonKeymaps: + # km.keymap_items.remove(kmi) + # config.gAddonKeymaps.clear() + # general_wrappers_operators.unregister() + + storyboard_keymaps.unregisterKeymaps() + playbar_keymaps.unregisterKeymaps() + general_keymaps.unregisterKeymaps() + playbar_wrappers_operators.unregister() diff --git a/shotmanager/keymaps/general_keymaps.py b/shotmanager/keymaps/general_keymaps.py index ebf137e4..6e8438d9 100644 --- a/shotmanager/keymaps/general_keymaps.py +++ b/shotmanager/keymaps/general_keymaps.py @@ -24,13 +24,14 @@ import bpy from shotmanager import config - from shotmanager.config import sm_logging _logger = sm_logging.getLogger(__name__) def registerKeymaps(): + keymaps = config.gAddonKeymaps + # Add the hotkey wm = bpy.context.window_manager kc = wm.keyconfigs.addon @@ -45,217 +46,41 @@ def registerKeymaps(): # VIEW_3D works also for timeline km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") kmi = km.keymap_items.new("uas_shot_manager.shots_play_mode", type="SPACE", value="PRESS", alt=True) - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") kmi = km.keymap_items.new("uas_shot_manager.shots_play_mode", type="SPACE", value="PRESS", alt=True) - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") kmi = km.keymap_items.new("uas_shot_manager.shots_play_mode", type="SPACE", value="PRESS", alt=True) - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) # NOTE: Does not exist anymore on 2.83+, use VIEW_3D instead # km = wm.keyconfigs.addon.keymaps.new(name="Timeline", space_type="TIMELINE") # kmi = km.keymap_items.new("uas_shot_manager.shots_play_mode", type="SPACE", value="PRESS", alt=True) - # config.gAddonKeymaps.append((km, kmi)) + # keymaps.append((km, kmi)) # display_overlay_tools operator ############################### km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") kmi = km.keymap_items.new("uas_shot_manager.display_overlay_tools", type="NONE", value="PRESS") - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) # km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") # kmi = km.keymap_items.new("uas_shot_manager.display_overlay_tools", type="NONE", value="PRESS") - # config.gAddonKeymaps.append((km, kmi)) + # keymaps.append((km, kmi)) # km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") # kmi = km.keymap_items.new("uas_shot_manager.display_overlay_tools", type="NONE", value="PRESS") - # config.gAddonKeymaps.append((km, kmi)) - - ############################### - # Navigate between shots - ############################### - - # previous shot - ############################### - - usePreviousShot = True - if usePreviousShot: - - # START ############## - - # VIEW_3D works also for timeline - km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", ctrl=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "START" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", ctrl=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "START" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", ctrl=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "START" - config.gAddonKeymaps.append((km, kmi)) - - # END ############## - - # VIEW_3D works also for timeline - km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", alt=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "END" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", alt=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "END" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", alt=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "END" - config.gAddonKeymaps.append((km, kmi)) - - # ANY ############## - - # VIEW_3D works also for timeline - km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", ctrl=True, alt=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "ANY" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", ctrl=True, alt=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "ANY" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="UP_ARROW", value="PRESS", ctrl=True, alt=True - ) - kmi.properties.navigDirection = "PREVIOUS" - kmi.properties.boundaryMode = "ANY" - config.gAddonKeymaps.append((km, kmi)) - - # next shot - ############################### - - useNextShot = True - if useNextShot: - - # START ############## - - # VIEW_3D works also for timeline - km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="DOWN_ARROW", value="PRESS", ctrl=True - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "START" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="DOWN_ARROW", value="PRESS", ctrl=True - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "START" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="DOWN_ARROW", value="PRESS", ctrl=True - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "START" - config.gAddonKeymaps.append((km, kmi)) - - # END ############## - - # VIEW_3D works also for timeline - km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", - type="DOWN_ARROW", - value="PRESS", - alt=True, - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "END" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="DOWN_ARROW", value="PRESS", alt=True - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "END" - config.gAddonKeymaps.append((km, kmi)) - - km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="DOWN_ARROW", value="PRESS", alt=True - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "END" - config.gAddonKeymaps.append((km, kmi)) - - # ANY ############## + # keymaps.append((km, kmi)) - # VIEW_3D works also for timeline - km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="DOWN_ARROW", value="PRESS", ctrl=True, alt=True - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "ANY" - config.gAddonKeymaps.append((km, kmi)) - km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", type="DOWN_ARROW", value="PRESS", ctrl=True, alt=True - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "ANY" - config.gAddonKeymaps.append((km, kmi)) +def unregisterKeymaps(): + keymaps = config.gAddonKeymaps - km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") - kmi = km.keymap_items.new( - "uas_shot_manager.playbar_gotoshotboundary", - type="DOWN_ARROW", - value="PRESS", - ctrl=True, - alt=True, - ) - kmi.properties.navigDirection = "NEXT" - kmi.properties.boundaryMode = "ANY" - config.gAddonKeymaps.append((km, kmi)) + # Remove the hotkeys + for km, kmi in keymaps: + km.keymap_items.remove(kmi) + keymaps.clear() diff --git a/shotmanager/keymaps/playbar_keymaps.py b/shotmanager/keymaps/playbar_keymaps.py new file mode 100644 index 00000000..5073fa8d --- /dev/null +++ b/shotmanager/keymaps/playbar_keymaps.py @@ -0,0 +1,225 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Key mappings for all the operators of the add-on + +Eg here: https://blender.stackexchange.com/questions/196483/create-keyboard-shortcut-for-an-operator-using-python +""" + +import bpy + +from shotmanager import config +from shotmanager.config import sm_logging + +_logger = sm_logging.getLogger(__name__) + +############################### +# Navigate between shots +############################### + + +def registerKeymaps(): + prefs = config.getShotManagerPrefs() + keymaps = config.gAddonKeymaps_shotsNav + + if prefs.kmap_shots_nav_invert_direction: + arrowKeyPrev = "UP_ARROW" + arrowKeyNext = "DOWN_ARROW" + else: + arrowKeyPrev = "DOWN_ARROW" + arrowKeyNext = "UP_ARROW" + + # Add the hotkey + wm = bpy.context.window_manager + kc = wm.keyconfigs.addon + if kc: + # previous shot + ############################### + + usePreviousShot = True + if usePreviousShot: + + # START ############## + + # VIEW_3D works also for timeline + km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousstart", type=arrowKeyPrev, value="PRESS", ctrl=True + ) + # kmi.properties.navigDirection = "PREVIOUS" + # kmi.properties.boundaryMode = "START" + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousstart", type=arrowKeyPrev, value="PRESS", ctrl=True + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousstart", type=arrowKeyPrev, value="PRESS", ctrl=True + ) + keymaps.append((km, kmi)) + + # END ############## + + # VIEW_3D works also for timeline + km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousend", type=arrowKeyPrev, value="PRESS", alt=True + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousend", type=arrowKeyPrev, value="PRESS", alt=True + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousend", type=arrowKeyPrev, value="PRESS", alt=True + ) + keymaps.append((km, kmi)) + + # ANY ############## + + # VIEW_3D works also for timeline + km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousany", + type=arrowKeyPrev, + value="PRESS", + ctrl=True, + alt=True, + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousany", + type=arrowKeyPrev, + value="PRESS", + ctrl=True, + alt=True, + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_previousany", + type=arrowKeyPrev, + value="PRESS", + ctrl=True, + alt=True, + ) + keymaps.append((km, kmi)) + + # next shot + ############################### + + useNextShot = True + if useNextShot: + + # START ############## + + # VIEW_3D works also for timeline + km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextstart", type=arrowKeyNext, value="PRESS", ctrl=True + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextstart", type=arrowKeyNext, value="PRESS", ctrl=True + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextstart", type=arrowKeyNext, value="PRESS", ctrl=True + ) + keymaps.append((km, kmi)) + + # END ############## + + # VIEW_3D works also for timeline + km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextend", + type=arrowKeyNext, + value="PRESS", + alt=True, + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextend", type=arrowKeyNext, value="PRESS", alt=True + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextend", type=arrowKeyNext, value="PRESS", alt=True + ) + keymaps.append((km, kmi)) + + # ANY ############## + + # VIEW_3D works also for timeline + km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextany", + type=arrowKeyNext, + value="PRESS", + ctrl=True, + alt=True, + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextany", + type=arrowKeyNext, + value="PRESS", + ctrl=True, + alt=True, + ) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") + kmi = km.keymap_items.new( + "uas_shot_manager.playbar_gotoshotboundary_nextany", + type=arrowKeyNext, + value="PRESS", + ctrl=True, + alt=True, + ) + keymaps.append((km, kmi)) + + +def unregisterKeymaps(): + keymaps = config.gAddonKeymaps_shotsNav + + # Remove the hotkeys + for km, kmi in keymaps: + km.keymap_items.remove(kmi) + keymaps.clear() diff --git a/shotmanager/keymaps/playbar_wrappers_operators.py b/shotmanager/keymaps/playbar_wrappers_operators.py new file mode 100644 index 00000000..afe03ef8 --- /dev/null +++ b/shotmanager/keymaps/playbar_wrappers_operators.py @@ -0,0 +1,129 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Wrapper operators for the key mappings of the playbar. +There wrappers allow a clearer identification of the shortcuts in the Keymaps Preferences panel +""" + +import bpy + +from bpy.types import Operator +from bpy.props import BoolProperty, StringProperty + +from shotmanager import config +from shotmanager.config import sm_logging + +_logger = sm_logging.getLogger(__name__) + + +class UAS_ShotManager_Playbar_GoToShotBoundary_PreviousStart(Operator): + bl_idname = "uas_shot_manager.playbar_gotoshotboundary_previousstart" + bl_label = "Ubisoft Shot Mng - Go to Previous Shot Start" + bl_description = "Go to the start of previous shot" + bl_options = {"INTERNAL"} + + description: StringProperty(name="Description", default="Go to the start of previous shot") + + def execute(self, context): + bpy.ops.uas_shot_manager.playbar_gotoshotboundary(navigDirection="PREVIOUS", boundaryMode="START") + return {"FINISHED"} + + +class UAS_ShotManager_Playbar_GoToShotBoundary_PreviousEnd(Operator): + bl_idname = "uas_shot_manager.playbar_gotoshotboundary_previousend" + bl_label = "Ubisoft Shot Mng - Go to Previous Shot End" + bl_description = "Go to the end of previous shot" + bl_options = {"INTERNAL"} + + description: StringProperty(name="Description", default="Go to the end of previous shot") + + def execute(self, context): + bpy.ops.uas_shot_manager.playbar_gotoshotboundary(navigDirection="PREVIOUS", boundaryMode="END") + return {"FINISHED"} + + +class UAS_ShotManager_Playbar_GoToShotBoundary_PreviousAny(Operator): + bl_idname = "uas_shot_manager.playbar_gotoshotboundary_previousany" + bl_label = "Ubisoft Shot Mng - Go to Previous Shot Boundary" + bl_description = "Go to the previous boundary of previous shot" + bl_options = {"INTERNAL"} + + description: StringProperty(name="Description", default="Go to the previous boundary of previous shot") + + def execute(self, context): + bpy.ops.uas_shot_manager.playbar_gotoshotboundary(navigDirection="PREVIOUS", boundaryMode="ANY") + return {"FINISHED"} + + +class UAS_ShotManager_Playbar_GoToShotBoundary_NextStart(Operator): + bl_idname = "uas_shot_manager.playbar_gotoshotboundary_nextstart" + bl_label = "Ubisoft Shot Mng - Go to Next Shot Start" + bl_description = "Go to the start of next shot" + bl_options = {"INTERNAL"} + + description: StringProperty(name="Description", default="Go to the start of next shot") + + def execute(self, context): + bpy.ops.uas_shot_manager.playbar_gotoshotboundary(navigDirection="NEXT", boundaryMode="START") + return {"FINISHED"} + + +class UAS_ShotManager_Playbar_GoToShotBoundary_NextEnd(Operator): + bl_idname = "uas_shot_manager.playbar_gotoshotboundary_nextend" + bl_label = "Ubisoft Shot Mng - Go to Next Shot End" + bl_description = "Go to the end of next shot" + bl_options = {"INTERNAL"} + + description: StringProperty(name="Description", default="Go to the end of next shot") + + def execute(self, context): + bpy.ops.uas_shot_manager.playbar_gotoshotboundary(navigDirection="NEXT", boundaryMode="END") + return {"FINISHED"} + + +class UAS_ShotManager_Playbar_GoToShotBoundary_NextAny(Operator): + bl_idname = "uas_shot_manager.playbar_gotoshotboundary_nextany" + bl_label = "Ubisoft Shot Mng - Go to Next Shot Boundary" + bl_description = "Go to the next boundary of next shot" + bl_options = {"INTERNAL"} + + description: StringProperty(name="Description", default="Go to the next boundary of next shot") + + def execute(self, context): + bpy.ops.uas_shot_manager.playbar_gotoshotboundary(navigDirection="NEXT", boundaryMode="ANY") + return {"FINISHED"} + + +_classes = ( + UAS_ShotManager_Playbar_GoToShotBoundary_PreviousStart, + UAS_ShotManager_Playbar_GoToShotBoundary_PreviousEnd, + UAS_ShotManager_Playbar_GoToShotBoundary_PreviousAny, + UAS_ShotManager_Playbar_GoToShotBoundary_NextStart, + UAS_ShotManager_Playbar_GoToShotBoundary_NextEnd, + UAS_ShotManager_Playbar_GoToShotBoundary_NextAny, +) + + +def register(): + for cls in _classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in reversed(_classes): + bpy.utils.unregister_class(cls) diff --git a/shotmanager/keymaps/storyboard_keymaps.py b/shotmanager/keymaps/storyboard_keymaps.py index 6d3b7f04..ba5f0b32 100644 --- a/shotmanager/keymaps/storyboard_keymaps.py +++ b/shotmanager/keymaps/storyboard_keymaps.py @@ -31,6 +31,8 @@ def registerKeymaps(): + keymaps = config.gAddonKeymaps_storyboard + # Add the hotkey wm = bpy.context.window_manager kc = wm.keyconfigs.addon @@ -49,21 +51,21 @@ def registerKeymaps(): "uas_shot_manager.greasepencil_navigateinkeyframes", type="LEFT_ARROW", value="PRESS", ctrl=True ) kmi.properties.navigDirection = "PREVIOUS" - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") kmi = km.keymap_items.new( "uas_shot_manager.greasepencil_navigateinkeyframes", type="LEFT_ARROW", value="PRESS", ctrl=True ) kmi.properties.navigDirection = "PREVIOUS" - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") kmi = km.keymap_items.new( "uas_shot_manager.greasepencil_navigateinkeyframes", type="LEFT_ARROW", value="PRESS", ctrl=True ) kmi.properties.navigDirection = "PREVIOUS" - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) # next key frame ############################### @@ -73,18 +75,27 @@ def registerKeymaps(): "uas_shot_manager.greasepencil_navigateinkeyframes", type="RIGHT_ARROW", value="PRESS", ctrl=True ) kmi.properties.navigDirection = "NEXT" - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") kmi = km.keymap_items.new( "uas_shot_manager.greasepencil_navigateinkeyframes", type="RIGHT_ARROW", value="PRESS", ctrl=True ) kmi.properties.navigDirection = "NEXT" - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") kmi = km.keymap_items.new( "uas_shot_manager.greasepencil_navigateinkeyframes", type="RIGHT_ARROW", value="PRESS", ctrl=True ) kmi.properties.navigDirection = "NEXT" - config.gAddonKeymaps.append((km, kmi)) + keymaps.append((km, kmi)) + + +def unregisterKeymaps(): + keymaps = config.gAddonKeymaps_storyboard + + # Remove the hotkeys + for km, kmi in keymaps: + km.keymap_items.remove(kmi) + keymaps.clear() diff --git a/shotmanager/operators/general.py b/shotmanager/operators/general.py index 8ab1eb1c..f879de67 100644 --- a/shotmanager/operators/general.py +++ b/shotmanager/operators/general.py @@ -41,7 +41,7 @@ class UAS_ShotManager_OT_ShotsPlayMode(Operator): bl_idname = "uas_shot_manager.shots_play_mode" - bl_label = "Shot Manager - Toggle Shots Play Mode" + bl_label = "Ubisoft Shot Mng - Toggle Shots Play Mode" bl_description = "Enable / disable the Shots Play Mode" bl_options = {"INTERNAL"} @@ -55,7 +55,7 @@ def invoke(self, context, event): class UAS_ShotManager_OT_DisplayOverlayTools(Operator): bl_idname = "uas_shot_manager.display_overlay_tools" - bl_label = "Shot Manager - Toggle Overlay Tools Display" + bl_label = "Ubisoft Shot Mng - Toggle Overlay Tools Display" bl_description = "Show or hide the Sequence Timeline, Interactive Shots Stack and some other tools" bl_options = {"INTERNAL"} @@ -83,7 +83,7 @@ class UAS_ShotManager_OT_DisplayDisabledShotsInOverlays(Operator): # bl_description = "Display Disabled Shots in Overlay Tools" bl_options = {"INTERNAL"} - def invoke(self, context, event): + def execute(self, context): props = context.scene.UAS_shot_manager_props val = not props.interactShotsStack_displayDisabledShots diff --git a/shotmanager/operators/playbar.py b/shotmanager/operators/playbar.py index 6cc3cfbd..d0de3364 100644 --- a/shotmanager/operators/playbar.py +++ b/shotmanager/operators/playbar.py @@ -74,7 +74,7 @@ def execute(self, context): class UAS_ShotManager_Playbar_GoToShotBoundary(Operator): bl_idname = "uas_shot_manager.playbar_gotoshotboundary" - bl_label = "Shot Manager - Navigate on shots boundaries" + bl_label = "Ubisoft Shot Mng - Navigate on shots boundaries" bl_description = "Go from start to end of each shot" bl_options = {"INTERNAL", "UNDO"} From 78de91c1f8c124b18ed944dfef6dbd360e826515 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Tue, 13 Sep 2022 15:02:28 +0200 Subject: [PATCH 11/27] Release of Beta v2.1.1 --- CHANGELOG.md | 4 ++++ shotmanager/__init__.py | 2 +- shotmanager/addon_prefs/addon_prefs.py | 11 ++++++++++ shotmanager/config/config.py | 2 +- .../interact_shots_stack/shots_stack_bgl.py | 13 ++++-------- .../interact_shots_stack/shots_stack_prefs.py | 4 ++++ .../widgets/shots_stack_widget.py | 6 ++++-- .../workspace_info/workspace_info.py | 2 +- shotmanager/utils/utils_editors_dopesheet.py | 20 +++++++++++-------- shotmanager/utils/utils_ui.py | 1 + 10 files changed, 43 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b5313c8..5732c2bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ - Added a Preferences parameter to toggle the vertical arrows used to navigate from shot to shot - Made the up arrow go to next shots by default instead of previous ones +### Shots Stack UI +- Added a Preferences setting to make the shots stack starts at the specified lane +- Set the first lane to 1 instead of 0 to see the keys of the Summany lane + ----- ## 2.0.226 (2022-09-12) ### Shots Stack UI diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index 84ffa198..d7a9b6eb 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -330,7 +330,7 @@ def _update_UAS_shot_manager_identify_dopesheets(self, context): print(f"\n ------ Ubisoft Shot Manager debug: {config.devDebug} ------- ") addon_prefs_inst = config.getShotManagerPrefs() - addon_prefs_inst.displaySMDebugPanel = True + addon_prefs_inst.displaySMDebugPanel = False # _props = bpy.context.scene.UAS_shot_manager_props # # currentLayout = props.getCurrentLayout() diff --git a/shotmanager/addon_prefs/addon_prefs.py b/shotmanager/addon_prefs/addon_prefs.py index 5e28b43a..4c3c2de5 100644 --- a/shotmanager/addon_prefs/addon_prefs.py +++ b/shotmanager/addon_prefs/addon_prefs.py @@ -960,6 +960,17 @@ def _update_display_shtStack_toolbar(self, context): default=0.7, ) + shtStack_firstLineIndex: IntProperty( + name="Shots Stack Fisrt Row", + description=( + "Set the line at which the first shot of the stack is placed." + "\nDefault is 1 in order to let the keys of the Summary line visible" + ), + min=0, + max=10, + default=1, + ) + def _update_shtStack_screen_display_factor_mode(self, context): # read also: # https://stackoverflow.com/questions/53889520/getting-screen-pixels-taking-into-account-the-scale-factor diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index 735a3d99..b2027c19 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -43,7 +43,7 @@ def initGlobalVariables(): devDebug = False # change this value to force debug at start time - devDebug = True + devDebug = False global devDebug_lastRedrawTime devDebug_lastRedrawTime = -1 diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_bgl.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_bgl.py index 41830d94..d4974a54 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_bgl.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_bgl.py @@ -28,6 +28,7 @@ from mathutils import Vector from shotmanager.utils import utils +from shotmanager.utils import utils_editors_dopesheet from shotmanager.overlay_tools.workspace_info.workspace_info import draw_typo_2d from shotmanager.config import config @@ -65,15 +66,9 @@ def get_lane_origin_y(lane): """Return the offset to put under the timeline ruler""" RULER_HEIGHT = 52 RULER_HEIGHT = 40 - return math.floor(-1.0 * get_lane_height() * lane - (RULER_HEIGHT * bpy.context.preferences.view.ui_scale)) - - -# deprecated - use getLaneHeight -def get_lane_height(): - """Return the offset to put under the timeline ruler""" - LANE_HEIGHT = 22.5 - LANE_HEIGHT = 18 - return LANE_HEIGHT * get_prefs_ui_scale() + return math.floor( + -1.0 * utils_editors_dopesheet.getLaneHeight() * lane - (RULER_HEIGHT * bpy.context.preferences.view.ui_scale) + ) ############################################################################################################## diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py index b7c93f21..f0db4a52 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py @@ -44,6 +44,10 @@ def draw_settings(context, layout): text="Compact Shots Display (= decrease visual stack height)", ) + firstLineRow = propCol.row(align=False) + # firstLineRow.use_property_split = True + firstLineRow.prop(prefs, "shtStack_firstLineIndex") + propCol.prop(prefs, "shtStack_link_stb_clips_to_keys") diplFactRow = propCol.row(align=False) diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py index 03e6f9bf..e2a62a17 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py @@ -243,6 +243,7 @@ def drawCurrentShotDecoration(self, shotCompoCurrent, preDrawOnly=False): def drawShots(self, preDrawOnly=False): props = self.context.scene.UAS_shot_manager_props + prefs = config.getShotManagerPrefs() self.rebuildShotComponents() currentShotInd = props.getCurrentShotIndex() @@ -250,7 +251,7 @@ def drawShots(self, preDrawOnly=False): debug_maxShots = 5000 # 6 - lane = 1 + lane = 1 + prefs.shtStack_firstLineIndex shotCompoCurrent = None for i, shotCompo in enumerate(self.shotComponents): shotCompo.isCurrent = i == currentShotInd @@ -281,6 +282,7 @@ def drawShots(self, preDrawOnly=False): def drawShots_compactMode(self, preDrawOnly=False): # return props = self.context.scene.UAS_shot_manager_props + prefs = config.getShotManagerPrefs() self.rebuildShotComponents() currentShot = props.getCurrentShot() @@ -295,7 +297,7 @@ def drawShots_compactMode(self, preDrawOnly=False): if not props.interactShotsStack_displayDisabledShots and not shotCompo.shot.enabled: shotCompo.isVisible = False continue - lane = 1 + lane = 1 + prefs.shtStack_firstLineIndex if i > 0: for ln, shots_in_lane in shots_from_lane.items(): for s in shots_in_lane: diff --git a/shotmanager/overlay_tools/workspace_info/workspace_info.py b/shotmanager/overlay_tools/workspace_info/workspace_info.py index 1356672f..547a35b7 100644 --- a/shotmanager/overlay_tools/workspace_info/workspace_info.py +++ b/shotmanager/overlay_tools/workspace_info/workspace_info.py @@ -211,7 +211,7 @@ def draw_callback__dopesheet_size(self, context, callingArea): # blf.size(0, round(self.font_size * get_prefs_ui_scale()), 72) blf.size(0, 12, 72) # textPos_y = self.origin.y + 6 * get_prefs_ui_scale() - # textPos_y = self.origin.y + get_lane_height() * 0.2 * get_prefs_ui_scale() + # textPos_y = self.origin.y + utils_editors_dopesheet.getLaneHeight() * 0.2 # blf.position(0, *context.region.view2d.view_to_region(self.origin.x + 1.4, textPos_y), 0) offset_y = 20 diff --git a/shotmanager/utils/utils_editors_dopesheet.py b/shotmanager/utils/utils_editors_dopesheet.py index 9990541f..ad520ac4 100644 --- a/shotmanager/utils/utils_editors_dopesheet.py +++ b/shotmanager/utils/utils_editors_dopesheet.py @@ -91,21 +91,25 @@ def getPrefsUIScale(): return bpy.context.preferences.view.ui_scale -def getRulerHeight(): +def getRulerHeight(firstLaneIndex=0): """Return the height in pixels of the time ruler of a dopesheet""" prefs = config.getShotManagerPrefs() - RULER_HEIGHT = 23 * prefs.shtStack_screen_display_factor - # RULER_HEIGHT = 28 # on laptop - return RULER_HEIGHT * getPrefsUIScale() + RULER_HEIGHT = 23 + # RULER_HEIGHT = 28 # on laptop at display 125% + rulerHeight = ( + RULER_HEIGHT * prefs.shtStack_screen_display_factor + ) * getPrefsUIScale() + firstLaneIndex * getLaneHeight() + # rulerHeight = rulerHeight * getPrefsUIScale() + return rulerHeight def getLaneHeight(): """Return the height of a lane in pixels""" prefs = config.getShotManagerPrefs() - LANE_HEIGHT = 18 * prefs.shtStack_screen_display_factor - # LANE_HEIGHT = 18.5 - # LANE_HEIGHT = 22.5 # on laptop - return LANE_HEIGHT * getPrefsUIScale() + LANE_HEIGHT = 18 + # LANE_HEIGHT = 22.5 # on laptop at display 125% + laneHeight = LANE_HEIGHT * prefs.shtStack_screen_display_factor + return laneHeight * getPrefsUIScale() # same as pixel to lane diff --git a/shotmanager/utils/utils_ui.py b/shotmanager/utils/utils_ui.py index 96c2b30f..b087c723 100644 --- a/shotmanager/utils/utils_ui.py +++ b/shotmanager/utils/utils_ui.py @@ -97,6 +97,7 @@ def propertyColumn( ): """Return a column to add components in a more compact way that the standart layout""" propRow = layout.row(align=True) + # propRow.use_property_split = True if 0 < padding_left: leftRow = propRow.row(align=True) From 77dba262798721f66b4847e5ffbe41b0ba5178a4 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Tue, 13 Sep 2022 15:04:24 +0200 Subject: [PATCH 12/27] beta added in log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5732c2bd..71497fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ----- ## 2.1.001 (2022-09-12) +**Beta Release** ### Keymaps - Separated key mappings per category - Added a Preferences parameter to toggle the vertical arrows used to navigate from shot to shot From 0259f61db51f543ba03627050370706afa0eeaf2 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Tue, 13 Sep 2022 17:44:56 +0200 Subject: [PATCH 13/27] fix: stb shotType was added for new stb frames --- CHANGELOG.md | 4 ++++ shotmanager/__init__.py | 2 +- .../features/greasepencil/greasepencil_props.py | 3 ++- shotmanager/properties/props.py | 3 ++- shotmanager/properties/shot.py | 14 +++++++++++++- shotmanager/ui/sm_shots_ui_storyboard_layout.py | 4 ++-- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71497fbb..404dd11f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +----- +## 2.1.002 (2022-09-12) +- Fixed the type of shot that was set to Storyboard when a storyboard frame was added + ----- ## 2.1.001 (2022-09-12) **Beta Release** diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index d7a9b6eb..ed925fa6 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 1, 1), + "version": (2, 1, 2), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", diff --git a/shotmanager/features/greasepencil/greasepencil_props.py b/shotmanager/features/greasepencil/greasepencil_props.py index ef75fd7b..ec7623f4 100644 --- a/shotmanager/features/greasepencil/greasepencil_props.py +++ b/shotmanager/features/greasepencil/greasepencil_props.py @@ -79,7 +79,8 @@ def initialize(self, parentShot, mode): gpObj = gp.createStoryboarFrameGP( gpName, framePreset, parentCamera=self.parentCamera, location=[0, 0, -0.5] ) - parentShot.shotType = mode + # No!! + # parentShot.shotType = mode self.canvasOpacity = prefs.storyboard_default_canvasOpacity self.distanceFromOrigin = prefs.storyboard_default_distanceFromOrigin diff --git a/shotmanager/properties/props.py b/shotmanager/properties/props.py index a32c9d00..ce6b7fce 100644 --- a/shotmanager/properties/props.py +++ b/shotmanager/properties/props.py @@ -2983,7 +2983,8 @@ def addShot( newShotInd = atValidIndex if addGreasePencilStoryboard: - newShot.addGreasePencil(mode="STORYBOARD") + # newShot.addGreasePencil(mode=shotType) + newShot.addStoryboardFrame() # update the current take if needed if takeInd == currentTakeInd: diff --git a/shotmanager/properties/shot.py b/shotmanager/properties/shot.py index 6ef09ed6..5c712813 100644 --- a/shotmanager/properties/shot.py +++ b/shotmanager/properties/shot.py @@ -624,7 +624,10 @@ def addGreasePencil(self, mode="STORYBOARD"): if "STORYBOARD" == mode: gpObj = gp.createStoryboarFrameGP(gpName, framePreset, parentCamera=self.camera, location=[0, 0, -0.5]) - self.shotType = "STORYBOARD" + + # No !!! + # self.shotType = "STORYBOARD" + gpProps.updateGreasePencil() return (gpProps, gpObj) @@ -760,6 +763,15 @@ def _ClearParent(child): # storyboard ############# + def addStoryboardFrame(self): + """Add a grase pencil object to the shot as well as a Storyboard frame preset + The type of the shot is NOT changed. + Return a tupple with the grease pencil properties and the created object.""" + + # the mode used here is to specify the kind of grease pencil object to create + # it is not related to the shot type since any shot type can have a storyboard frame + return self.addGreasePencil(mode="STORYBOARD") + def isStoryboardType(self): return "S" == self.shotType[0] diff --git a/shotmanager/ui/sm_shots_ui_storyboard_layout.py b/shotmanager/ui/sm_shots_ui_storyboard_layout.py index 4d2565f6..7f98480a 100644 --- a/shotmanager/ui/sm_shots_ui_storyboard_layout.py +++ b/shotmanager/ui/sm_shots_ui_storyboard_layout.py @@ -47,8 +47,8 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn display_getsetcurrentframe_in_shotlist = False currentIconIsOrange = True - orange = "_Orange" if currentIconIsOrange else "" - cam = f"Cam{orange}" if current_shot_index == index else "" + # orange = "_Orange" if currentIconIsOrange else "" + # cam = f"Cam{orange}" if current_shot_index == index else "" currentFrame = context.scene.frame_current # check if the camera still exists in the scene From 6ddf4ada767260ba3f9500d76337dc36306bafd5 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Tue, 13 Sep 2022 22:03:09 +0200 Subject: [PATCH 14/27] fix: regression in camera binding convert --- CHANGELOG.md | 5 +++++ shotmanager/__init__.py | 2 +- shotmanager/config/dev_notes.txt | 6 ++++++ shotmanager/operators/shots.py | 3 ++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 404dd11f..a8a9d8f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +----- +## 2.1.003 (2022-09-12) +- Fixed regression bug on Convert Camera Binding + + ----- ## 2.1.002 (2022-09-12) - Fixed the type of shot that was set to Storyboard when a storyboard frame was added diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index ed925fa6..95689a72 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 1, 2), + "version": (2, 1, 3), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", diff --git a/shotmanager/config/dev_notes.txt b/shotmanager/config/dev_notes.txt index e6c63628..5e8b61b2 100644 --- a/shotmanager/config/dev_notes.txt +++ b/shotmanager/config/dev_notes.txt @@ -22,6 +22,12 @@ Mark Down language memo: https://github.com/tchapi/markdown-cheatsheet/blob/mast propsCol = mainRow.column(align=True) +To do: +clear anim on stb cameras +pbs paul +play with the stb + + Pb icons lot loaded: diff --git a/shotmanager/operators/shots.py b/shotmanager/operators/shots.py index 0144bf6e..29fb252a 100644 --- a/shotmanager/operators/shots.py +++ b/shotmanager/operators/shots.py @@ -29,6 +29,7 @@ import json from shotmanager.utils import utils +from shotmanager.utils import utils_markers from shotmanager.utils.utils_time import zoom_dopesheet_view_to_range from shotmanager.config import config @@ -1107,7 +1108,7 @@ def convertMarkersFromCameraBindingToShots(scene): if m.camera is not None: boundMarkers.append(m) - boundMarkers = utils.sortMarkers(boundMarkers) + boundMarkers = utils_markers.sortMarkers(boundMarkers) # display sorted markers # for m in boundMarkers: From 84489eb32e5e514c38ddf938a172eae665fbf48c Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Tue, 13 Sep 2022 23:24:36 +0200 Subject: [PATCH 15/27] fix: unlock transforms when detadhing a gp --- CHANGELOG.md | 2 +- shotmanager/operators/shots.py | 5 +++++ shotmanager/properties/shot.py | 1 + shotmanager/utils/utils.py | 15 ++++++++++++++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8a9d8f1..1b946bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ----- ## 2.1.003 (2022-09-12) - Fixed regression bug on Convert Camera Binding - +- Fixed the detached grease pencil transform lock state ----- ## 2.1.002 (2022-09-12) diff --git a/shotmanager/operators/shots.py b/shotmanager/operators/shots.py index 29fb252a..1c3e309c 100644 --- a/shotmanager/operators/shots.py +++ b/shotmanager/operators/shots.py @@ -191,9 +191,14 @@ def execute(self, context): prefs = config.getShotManagerPrefs() shot = props.getShotByIndex(self.index) + if not shot: + _logger.error_ext(f"Set Current Shot Operator exec: shot is None. index: {self.index}") # _logger.debug_ext("Set Current Shot Operator exec: ", col="RED") def _updateEditors(changeTime=True, zoom_mode=""): + if shot is None: + return + # change time range to match shot range if prefs.current_shot_changes_time_range: if scene.use_preview_range: diff --git a/shotmanager/properties/shot.py b/shotmanager/properties/shot.py index 5c712813..5cd260dd 100644 --- a/shotmanager/properties/shot.py +++ b/shotmanager/properties/shot.py @@ -752,6 +752,7 @@ def _ClearParent(child): # unparent: bpy.ops.object.parent_clear(type='CLEAR') _ClearParent(gp_child) + utils.lockTransforms(gp_child, lock=False) utils.clearTransformAnimation(gp_child) if 0 == gp_child.name.find("Cam_"): diff --git a/shotmanager/utils/utils.py b/shotmanager/utils/utils.py index 359cf9f3..c4defe10 100644 --- a/shotmanager/utils/utils.py +++ b/shotmanager/utils/utils.py @@ -694,11 +694,12 @@ def getSceneVSE(vsm_sceneName, createVseTab=False): # Objects ################### + def getChildrenHierarchy(parentObject): """Return a list with all the children - recursive - of the specified object""" allChildren = list() - + def _getChildrenHierarchyRec(parentObject): for child in parentObject.children: allChildren.append(child) @@ -1185,6 +1186,18 @@ def _clearCurve(action, type, axis): # bpy.ops.object.scale_clear() +def lockTransforms(obj, *, lock=True): + obj.lock_location[0] = lock + obj.lock_location[1] = lock + obj.lock_location[2] = lock + obj.lock_rotation[0] = lock + obj.lock_rotation[1] = lock + obj.lock_rotation[2] = lock + obj.lock_scale[0] = lock + obj.lock_scale[1] = lock + obj.lock_scale[2] = lock + + ################### # Color ################### From f2095b08d8d83cd07cff35a1f84f468b96f92199 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Wed, 14 Sep 2022 00:17:41 +0200 Subject: [PATCH 16/27] Added function to get the canvas frame when missing --- CHANGELOG.md | 1 + .../greasepencil/greasepencil_props.py | 6 +++++- shotmanager/properties/props.py | 13 +++++++++---- shotmanager/utils/utils_greasepencil.py | 19 ++++++++++++------- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b946bbe..42a7028c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.1.003 (2022-09-12) - Fixed regression bug on Convert Camera Binding - Fixed the detached grease pencil transform lock state +- Added a function to get the canvas frame when missing ----- ## 2.1.002 (2022-09-12) diff --git a/shotmanager/features/greasepencil/greasepencil_props.py b/shotmanager/features/greasepencil/greasepencil_props.py index ec7623f4..ac5e43cf 100644 --- a/shotmanager/features/greasepencil/greasepencil_props.py +++ b/shotmanager/features/greasepencil/greasepencil_props.py @@ -196,7 +196,7 @@ def copyPropertiesFrom(self, sourceGpProps): self.updateGreasePencil() - def updateCanvas(self): + def updateCanvas(self, rebuildIfMissing=True): props = bpy.context.scene.UAS_shot_manager_props # res = props.getRenderResolution() @@ -216,6 +216,10 @@ def updateCanvas(self): canvasLayer = self.getCanvasLayer() if canvasLayer is not None: + + if not len(canvasLayer.frames) and rebuildIfMissing: + utils_greasepencil.create_grease_pencil_canvas_frame(canvasLayer) + if len(canvasLayer.frames): gpFrame = canvasLayer.frames[0] if len(gpFrame.strokes): diff --git a/shotmanager/properties/props.py b/shotmanager/properties/props.py index ce6b7fce..617a2421 100644 --- a/shotmanager/properties/props.py +++ b/shotmanager/properties/props.py @@ -1320,12 +1320,17 @@ def updateStoryboardGrid(self): Only cameras related to storyboard frames, and not shots, are affected""" grid = self.stb_frameTemplate.frameGrid - frameList = self.getStoryboardFramesList(ignoreDisabled=False) + stbShots = self.getStoryboardShots(ignoreDisabled=False) - grid.updateStoryboardGrid(frameList) + # clear camera animation + for shot in stbShots: + if shot.isCameraValid(): + utils.clearTransformAnimation(shot.camera) + + grid.updateStoryboardGrid(stbShots) - def getStoryboardFramesList(self, ignoreDisabled=False, takeIndex=-1): - """Return a list of the shots that are flagged as storyboard frames""" + def getStoryboardShots(self, ignoreDisabled=False, takeIndex=-1): + """Return a list of the shots that are of the type storyboard shot""" takeInd = ( self.getCurrentTakeIndex() if -1 == takeIndex diff --git a/shotmanager/utils/utils_greasepencil.py b/shotmanager/utils/utils_greasepencil.py index 8130724d..ee1c34f1 100644 --- a/shotmanager/utils/utils_greasepencil.py +++ b/shotmanager/utils/utils_greasepencil.py @@ -222,6 +222,17 @@ def add_grease_pencil_canvas_layer( gpencil, gpencil_layer_name=gpencil_layer_name, clear_layer=clear_layer, order=order ) + gpStroke = create_grease_pencil_canvas_frame(gpencil_layer) + gpStroke.material_index = mat_index + + fitCanvasToFrustum(gpStroke, camera) + + gpencil_layer.lock = True + + return gpencil_layer + + +def create_grease_pencil_canvas_frame(gpencil_layer): keyFrame = addLayerKeyFrameAtTime(gpencil_layer, 0) zDistance = 0.0 # -5 @@ -230,13 +241,7 @@ def add_grease_pencil_canvas_layer( ptTopLeft = (-0.5, -0.5, zDistance) ptBottomRight = (0.5, 0.5, zDistance) gpStroke = draw_canvas_rect(keyFrame, ptTopLeft, ptBottomRight) - gpStroke.material_index = mat_index - - fitCanvasToFrustum(gpStroke, camera) - - gpencil_layer.lock = True - - return gpencil_layer + return gpStroke def fitGreasePencilToFrustum(camera, distance=None): From 1564fe5b12ff2604f7abfccc64846ad496a24a53 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Fri, 16 Sep 2022 00:11:29 +0200 Subject: [PATCH 17/27] wip: keyframe manips in the shots stack --- CHANGELOG.md | 9 + doc/README.md | 19 +- shotmanager/__init__.py | 4 +- shotmanager/addon_prefs/addon_prefs.py | 10 +- shotmanager/config/config.py | 2 +- shotmanager/config/sm_logging.py | 23 +- shotmanager/gpu/gpu_2d/class_Component2D.py | 22 +- shotmanager/gpu/gpu_2d/class_Mesh2D.py | 2 +- shotmanager/gpu/gpu_2d/class_Object2D.py | 10 +- shotmanager/gpu/gpu_2d/class_QuadObject.py | 22 +- shotmanager/gpu/gpu_2d/class_Text2D.py | 4 +- .../gpu/gpu_2d/doc_gpu_2d_components.md | 35 ++- .../gpu_2d/samples/dopesheet_gpu_sample.py | 289 ++++++++++++++++++ .../samples/dopesheet_gpu_sample_widget.py | 289 ++++++++++++++++++ .../interact_shots_stack/__init__.py | 7 + .../doc_interac_shots_stack.md | 8 + .../interact_shots_stack/shots_stack_prefs.py | 20 +- .../shots_stack_toolbar.py | 17 ++ .../widgets/shots_stack_clip_component.py | 39 ++- .../widgets/shots_stack_handle_component.py | 78 +++-- .../widgets/shots_stack_info_component.py | 247 +++++++++++++++ .../widgets/shots_stack_widget.py | 29 +- shotmanager/properties/props.py | 3 +- shotmanager/retimer/retimer.py | 3 +- shotmanager/retimer/retimer_operators.py | 86 +++--- 25 files changed, 1127 insertions(+), 150 deletions(-) create mode 100644 shotmanager/gpu/gpu_2d/samples/dopesheet_gpu_sample.py create mode 100644 shotmanager/gpu/gpu_2d/samples/dopesheet_gpu_sample_widget.py create mode 100644 shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_info_component.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a7028c..e07520e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +----- +## 2.1.004 (2022-09-14) +### Interactive Shots Stack +- Added an info component +- Added mode and scale keyframe changes when a shot clip is manipulated + +### Code +- Added a sample widget to the gpu components library + ----- ## 2.1.003 (2022-09-12) - Fixed regression bug on Convert Camera Binding diff --git a/doc/README.md b/doc/README.md index a4d7eb1a..a74d3aa9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,7 +1,24 @@ # Contributing -Shot Manager is a production tool. At the moment it is not yet opened to external contributions. Any feedback is very welcome though. +Shot Manager is a production tool. At the moment it is not yet completely opened to external contributions. Any feedback is very welcome though. + ## Documentation The technical documentation is currently being written. + +In particular some focus is being done to a UI components structure based on BGL and GPU and used to create advanced +UI components in the various editors of Blender. +As an example, the Interactive Shots Stack and the Sequence Timeline and based on it. +Read more about it here: [gpu 2D library](../shotmanager/gpu/gpu_2d/doc_gpu_2d_components.md) + +### Interactive Shots Stack +[Shots Stack notes](../shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md) + + +## Configuration of the development environment + +The development is made with Visual Studio Code. + +The requirements dev file is here: [requirements-dev.txt](../requirements-dev.txt] + diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index 95689a72..b64d2a2e 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 1, 3), + "version": (2, 1, 4), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", @@ -111,6 +111,8 @@ def register(): _logger.tags = config.getLoggingTags() logger_level = f"Logger level: {sm_logging.getLevelName()}" + sm_logging.loggerFormatTest(message="Logger test message") + versionTupple = utils.display_addon_registered_version("Ubisoft Shot Manager", more_info=logger_level) from .overlay_tools.workspace_info import workspace_info diff --git a/shotmanager/addon_prefs/addon_prefs.py b/shotmanager/addon_prefs/addon_prefs.py index 4c3c2de5..2f009559 100644 --- a/shotmanager/addon_prefs/addon_prefs.py +++ b/shotmanager/addon_prefs/addon_prefs.py @@ -945,9 +945,15 @@ def _update_display_shtStack_toolbar(self, context): default=True, ) + shtStack_display_info_widget: BoolProperty( + name="Display Information Widget", + description="Display the Information Widget with the modifier tips in the Shots Stack", + default=True, + ) + shtStack_link_stb_clips_to_keys: BoolProperty( - name="Link Storyboard Clips to Keyframes", - description="Link the Storyboard shot clips ", + name="Link Storyboard Frame Content to Storyboard Clips", + description="Link the Storyboard Frame content and the animation keys to the manipulated Storyboard shot clip", default=True, ) diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index b2027c19..735a3d99 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -43,7 +43,7 @@ def initGlobalVariables(): devDebug = False # change this value to force debug at start time - devDebug = False + devDebug = True global devDebug_lastRedrawTime devDebug_lastRedrawTime = -1 diff --git a/shotmanager/config/sm_logging.py b/shotmanager/config/sm_logging.py index 5b9e0fc2..6847629c 100644 --- a/shotmanager/config/sm_logging.py +++ b/shotmanager/config/sm_logging.py @@ -135,7 +135,9 @@ def _getFormatter(self, col="", form="DEFAULT"): elif "INFO" == form: if "" == col: color = self._colors["DEFAULT"] - f = Formatter(color + "{message:<140}" + _ENDCOLOR, style="{") + # f = Formatter(color + "{message:<140}" + _ENDCOLOR, style="{") + f = Formatter(color + "{message}" + _ENDCOLOR, style="{") + f.append_origin = False elif "WARNING" == form: if "" == col: color = self._colors["ORANGE"] @@ -244,6 +246,8 @@ def print_ext(self, msg, col="DEFAULT", tag=None, display=True): class Formatter(logging.Formatter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.append_origin = kwargs.get("append_origin", True) + # print(f"Formatter init: self.append_origin: {self.append_origin}") def format(self, record: logging.LogRecord): """ @@ -253,16 +257,17 @@ def format(self, record: logging.LogRecord): """ s = super().format(record) - MODULE_PATH = Path(__file__).parent.parent - # print("MODULE_PATH:" + str(MODULE_PATH)) - pathname = Path(record.pathname).relative_to(MODULE_PATH) + if self.append_origin: + MODULE_PATH = Path(__file__).parent.parent + # print("MODULE_PATH:" + str(MODULE_PATH)) + pathname = Path(record.pathname).relative_to(MODULE_PATH) - # display the full relative path - # s += f" [{os.curdir}{os.sep}{pathname}:{record.lineno}]" + # display the full relative path + # s += f" [{os.curdir}{os.sep}{pathname}:{record.lineno}]" - # display only the file name - filename = pathname.stem - s += f" [{filename}:{record.lineno}]" + # display only the file name + filename = pathname.stem + s += f" [{filename}:{record.lineno}]" return s diff --git a/shotmanager/gpu/gpu_2d/class_Component2D.py b/shotmanager/gpu/gpu_2d/class_Component2D.py index 33a3c97e..fdc30ab0 100644 --- a/shotmanager/gpu/gpu_2d/class_Component2D.py +++ b/shotmanager/gpu/gpu_2d/class_Component2D.py @@ -56,20 +56,20 @@ def __init__( ): InteractiveComponent.__init__( self, - targetArea, + targetArea=targetArea, ) QuadObject.__init__( self, - posXIsInRegionCS, - posX, - posYIsInRegionCS, - posY, - widthIsInRegionCS, - width, - heightIsInRegionCS, - height, - alignment, - alignmentToRegion, + posXIsInRegionCS=posXIsInRegionCS, + posX=posX, + posYIsInRegionCS=posYIsInRegionCS, + posY=posY, + widthIsInRegionCS=widthIsInRegionCS, + width=width, + heightIsInRegionCS=heightIsInRegionCS, + height=height, + alignment=alignment, + alignmentToRegion=alignmentToRegion, parent=parent, ) diff --git a/shotmanager/gpu/gpu_2d/class_Mesh2D.py b/shotmanager/gpu/gpu_2d/class_Mesh2D.py index c0dba462..d2f677e1 100644 --- a/shotmanager/gpu/gpu_2d/class_Mesh2D.py +++ b/shotmanager/gpu/gpu_2d/class_Mesh2D.py @@ -38,7 +38,7 @@ class Mesh2D: - def __init__(self, vertices=None, indices=None, texcoords=None): + def __init__(self, *, vertices=None, indices=None, texcoords=None): self._vertices = list() if vertices is None else vertices self._indices = list() if indices is None else indices self._indicesLine = list() diff --git a/shotmanager/gpu/gpu_2d/class_Object2D.py b/shotmanager/gpu/gpu_2d/class_Object2D.py index 1341ba56..da455756 100644 --- a/shotmanager/gpu/gpu_2d/class_Object2D.py +++ b/shotmanager/gpu/gpu_2d/class_Object2D.py @@ -80,7 +80,7 @@ class Object2D: width: The width of the object. Its unit is in pixels if widthIsInRegionCS is True, in frames otherwise - heightIsInRegionCS: If True the height is in the View coordinate system of the region, height is then in pixels + heightIsInRegionCS: If True the height is in the View coordinate system of the region, height is then in pixels Height of the object will NOT change even if the region scale on y is changed (eg: not dependent on lines scale) If False the height is in the Region coordinate system, in number of lines. @@ -92,16 +92,20 @@ class Object2D: height: The height of the object. Its unit is in pixels if heightIsInRegionCS is True, in lines otherwise - alignment: aligment of the object to its origin + alignment: aligment of the object to its origin. It can be seen as being the placement of the pivot relatively + to the component bounding box. can be TOP_LEFT, TOP_MID, TOP_RIGHT, MID_RIGHT, BOTTOM_RIGHT, BOTTOM_MID, BOTTOM_LEFT (default), MID_LEFT, MID_MID - alignmentToRegion: aligment of the object to its parent region + alignmentToRegion: aligment of the object to the REGION in which it belongs to (and not to its parent component). + *** When this component has to be displayed in its parent component coordinate system then alignmentToRegion + must be ignored and let to default *** can be TOP_LEFT, TOP_MID, TOP_RIGHT, MID_RIGHT, BOTTOM_RIGHT, BOTTOM_MID, BOTTOM_LEFT (default), MID_LEFT, MID_MID """ def __init__( self, + *, posXIsInRegionCS=True, posX=30, posYIsInRegionCS=True, diff --git a/shotmanager/gpu/gpu_2d/class_QuadObject.py b/shotmanager/gpu/gpu_2d/class_QuadObject.py index 6bebf0c4..0cc20469 100644 --- a/shotmanager/gpu/gpu_2d/class_QuadObject.py +++ b/shotmanager/gpu/gpu_2d/class_QuadObject.py @@ -40,6 +40,7 @@ class QuadObject(Object2D, Mesh2D): def __init__( self, + *, posXIsInRegionCS=True, posX=30, posYIsInRegionCS=True, @@ -55,16 +56,16 @@ def __init__( ): Object2D.__init__( self, - posXIsInRegionCS, - posX, - posYIsInRegionCS, - posY, - widthIsInRegionCS, - width, - heightIsInRegionCS, - height, - alignment, - alignmentToRegion, + posXIsInRegionCS=posXIsInRegionCS, + posX=posX, + posYIsInRegionCS=posYIsInRegionCS, + posY=posY, + widthIsInRegionCS=widthIsInRegionCS, + width=width, + heightIsInRegionCS=heightIsInRegionCS, + height=height, + alignment=alignment, + alignmentToRegion=alignmentToRegion, parent=parent, ) Mesh2D.__init__(self) @@ -336,6 +337,7 @@ def draw(self, shader=None, region=None, draw_types="TRIS", cap_lines=False, pre if False: draw_bBox(self._bBox, color=(0, 1, 1, 1)) draw_bBox(self._clamped_bBox) + # draw tripod if False: draw_tripod(self._pivot_posX, self._pivot_posY, 20) diff --git a/shotmanager/gpu/gpu_2d/class_Text2D.py b/shotmanager/gpu/gpu_2d/class_Text2D.py index 73689e91..5de28e91 100644 --- a/shotmanager/gpu/gpu_2d/class_Text2D.py +++ b/shotmanager/gpu/gpu_2d/class_Text2D.py @@ -42,8 +42,9 @@ class Text2D(Object2D): def __init__( self, + *, posXIsInRegionCS=True, - posX=30, + posX=0, posYIsInRegionCS=True, posY=0, alignment="BOTTOM_LEFT", @@ -90,7 +91,6 @@ def _getFontSizeForGlDraw(self): def _drawText(self, shader=None, region=None, pX=10, pY=10): # bgl.glDisable(bgl.GL_BLEND) - blf.enable(0, blf.CLIPPING) # blf.clipping(0, *self._clamped_bBox) margin = 20 diff --git a/shotmanager/gpu/gpu_2d/doc_gpu_2d_components.md b/shotmanager/gpu/gpu_2d/doc_gpu_2d_components.md index 82683d1d..cde33a0b 100644 --- a/shotmanager/gpu/gpu_2d/doc_gpu_2d_components.md +++ b/shotmanager/gpu/gpu_2d/doc_gpu_2d_components.md @@ -3,11 +3,11 @@ Mesh2D - draw simple quad (fill, line) \ \ - \ Object2D - pos, alignment, children - \ / \ + \ Object2D - pos, alignment, children + \ / \ - no draw functions ! \ / \ - \ / text2D - \ / + \ / \ + \ / Text2D \ / \ / QuadObject @@ -20,6 +20,12 @@ \ / Component2D +## Alignment: + + Instances of classes inheriting from Object2D have a transformation (only position is supported at the moment) + that is dependent on the alignment of their pivot in regard to their bounding box and on their alignment + to the parent. See [Object2D] description for more details. + ## Draw: @@ -36,7 +42,19 @@ | |_ [Component2D].draw() - + Children components are recursively drawn from parent to each ends of the hierarchy. + To do so: + - Do NOT explicitely call the draw() function of a child in the parent draw() function + - Either: + - Call the parent class (not the parent component!) draw() function, which will draw + the component as expected and will also collect the children and draw them + wkNote: add a drawChildren() somewhere? + - Or, in case the component draw() function has been customized and the draw() of the + parent class is not relevant, then explicitely call the children draw() functions + with such code: + sortedChildren = self.getChildren(sortedByIncreasingZ=True) + for child in sortedChildren: + child.draw(None, region) ## Event: @@ -100,5 +118,10 @@ # To do: - - simplify the arguments of the draw() function + - simplify the arguments of the draw() function, and in particular place the region arg first + - add an alignmentToParent to [Object2D] + - add a display / visible flag to hide the component + - add a disable + + \ No newline at end of file diff --git a/shotmanager/gpu/gpu_2d/samples/dopesheet_gpu_sample.py b/shotmanager/gpu/gpu_2d/samples/dopesheet_gpu_sample.py new file mode 100644 index 00000000..2e0bd5e0 --- /dev/null +++ b/shotmanager/gpu/gpu_2d/samples/dopesheet_gpu_sample.py @@ -0,0 +1,289 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Example of use of the gpu package applued to the dopesheet region +""" + + +from .dopesheet_gpu_sample_widget import DopesheetGpuSampleWidget + +import bpy +from bpy.types import Operator +from bpy.props import BoolProperty + +from shotmanager.utils.utils_ogl import get_region_at_xy + + +from shotmanager.config import config +from shotmanager.config import sm_logging + +_logger = sm_logging.getLogger(__name__) + + +# wkip really used??? +def initialize_gDopesheetGpuSampleInfos(): + return { + "prev_mouse_x": 0, + "prev_mouse_y": 0, + "frame_under_mouse": -1, + "active_clip_index": -1, + "active_clip_region": None, + "clips": list(), + } + + +############################################################################################################## +############################################################################################################## + + +class UAS_Gpu_DopesheetGpuSample(Operator): + bl_idname = "uas_gpu.dopesheet_gpu_sample" + bl_label = "Draw gpu sample in timeline" + bl_options = {"REGISTER", "INTERNAL"} + # bl_options = {"UNDO"} + # !!! Important note: Do not set undo here: it doesn't work and it will be in conflic with the + # calls to bpy.ops.ed.undo_push in the code !!! + + def __init__(self): + self.draw_handle = None + self.draw_event = None + + self.target_area = None + self.target_area_index = -1 + + self.prev_mouse_x = 0 + self.prev_mouse_y = 0 + self.prev_click = 0 + + # self.active_clip = None + # self.active_clip_region = None + + self.widgets = [] + + def init_widgets(self, context, widgets): + self.widgets = widgets + for widget in self.widgets: + widget.init(context) + + def __del__(self): + # config.gRedrawShotStack = False + pass + + ################################### + # invoke + ################################### + + def invoke(self, context, event): + _logger.debug_ext("Invoke op dopesheet_gpu_sample", col="PURPLE") + + # if ignoreWidget(context): + # _logger.debug_ext("Canceled op uas_gpu.dopesheet_gpu_sample", col="PURPLE") + # context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample = False + # return {"CANCELLED"} + + # context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample = True + + props = context.scene.UAS_shot_manager_props + # # config.gRedrawShotStack = False + + self.target_area_index = props.getTargetDopesheetIndex(context, only_valid=True) + self.target_area = props.getValidTargetDopesheet(context) + + # if self.target_area is None: + # _logger.debug_ext("Canceled op uas_gpu.dopesheet_gpu_sample area", col="PURPLE") + # return {"CANCELLED"} + # else: + # self.init_widgets(context, [DopesheetGpuSampleWidget(target_area=self.target_area)]) + self.init_widgets(context, [DopesheetGpuSampleWidget(target_area=self.target_area)]) + + args = (self, context, self.widgets) + self.register_handlers(args, context) + + context.window_manager.modal_handler_add(self) + + # # if config.gShotsStackInfos is None: + # # config.gShotsStackInfos = initialize_gDopesheetGpuSampleInfos() + + # redraw all + for area in context.screen.areas: + area.tag_redraw() + # context.scene.frame_current = context.scene.frame_current + + return {"RUNNING_MODAL"} + + ################################### + # register handles + ################################### + + def register_handlers(self, args, context): + self.draw_handle = bpy.types.SpaceDopeSheetEditor.draw_handler_add( + self.draw_callback_px, args, "WINDOW", "POST_PIXEL" + ) + + def unregister_handlers(self, context): + bpy.types.SpaceDopeSheetEditor.draw_handler_remove(self.draw_handle, "WINDOW") + self.draw_handle = None + self.draw_event = None + + def handle_widget_events(self, context, event): + """handle event for dopesheet_gpu_sample operator""" + # _logger.debug_ext(" handle_widget_events", col="PURPLE", tag="SHOTSTACK_EVENT") + + event_handled = False + region, area = get_region_at_xy(context, event.mouse_x, event.mouse_y, "DOPESHEET_EDITOR") + + # get only events in the target area + # wkip: mouse release out of the region have to be taken into account + + # events canceling the action + # for widget in self.widgets: + ## if widget.manipulated_clip: # removed + # if ( + # (event.type == "LEFTMOUSE" and event.value == "RELEASE") + # or (event.type == "RIGHTMOUSE" and event.value == "RELEASE") + # or (event.type == "ESC" and event.value == "RELEASE") + # ): + # widget.cancelAction(context) + # event_handled = True + + # events doing the action + if not event_handled: + if self.target_area is not None and area == self.target_area: + if region: + # if ignoreWidget(bpy.context): + # return False + # else: + for widget in self.widgets: + if widget.handle_event(context, event, region): + event_handled = True + break + + return event_handled + + ################################### + # modal + ################################### + + def modal(self, context, event): + # props = context.scene.UAS_shot_manager_props + # prefs = config.getShotManagerPrefs() + + event_handled = False + # _logger.debug_ext(f"uas_shot_manager.dopesheet_gpu_sample Modal", col="PURPLE") + + # if event.type not in ["TIMER", "MOUSEMOVE"]: + # print(f"Modal, event type: {event.type}") + + # if config.gShotsStackInfos is None: + # config.gShotsStackInfos = initialize_gDopesheetGpuSampleInfos() + + if not context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample: + _logger.debug_ext("dopesheet_gpu_sample In Modal - for Cancel", col="PURPLE") + self.unregister_handlers(context) + + # redraw all + for area in context.screen.areas: + area.tag_redraw() + # context.scene.frame_current = context.scene.frame_current + + # context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample = False + # return {"CANCELLED"} + return {"FINISHED"} + + # temp to remove + return {"PASS_THROUGH"} + + # _logger.debug_ext(f" context.area: {context.area}, self.target_area: {self.target_area}", col="YELLOW") + if context.area: + # _logger.debug_ext(" context.area", col="YELLOW") + # if ignoreWidget(context): + # _logger.debug_ext(" ignore widget in dopesheet_gpu_sample", col="PURPLE") + + # # self.unregister_handlers(context) + # # for area in context.screen.areas: + # # # if area.type == "DOPESHEET_EDITOR" and area == self.target_area: + # # area.tag_redraw() + # # print("Ignioring...") + # return {"PASS_THROUGH"} + + event_handled = self.handle_widget_events(context, event) + if event_handled: + config.gRedrawShotStack = True + # return {"RUNNING_MODAL"} + + # bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1) + + config.gMouseScreenPos[0] = event.mouse_x + config.gMouseScreenPos[1] = event.mouse_y + + debug_forceRedraw = config.gShotsStack_forceRedraw_debug + + if config.gRedrawShotStack or event_handled or debug_forceRedraw: + for area in context.screen.areas: + area.tag_redraw() + config.gRedrawShotStack = False + config.gRedrawShotStack_preDrawOnly = False + + if event_handled: + return {"RUNNING_MODAL"} + + # if debug_forceRedraw: + # return {"RUNNING_MODAL"} + + return {"PASS_THROUGH"} + + def cancel(self, context): + self.unregister_handlers(context) + + def draw_callback_px(self, op, context, widgets): + """Draw handler to paint onto the screen""" + # print( + # f"*** context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample: {context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample}" + # ) + # if not context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample: + # return + + # if ignoreWidget(context): + # return + + for widget in widgets: + widget.draw(preDrawOnly=False) + + +_classes = (UAS_Gpu_DopesheetGpuSample,) + + +def register(): + for cls in _classes: + bpy.utils.register_class(cls) + + def _update_UAS_shot_manager__display_dopesheet_gpu_sample(self, context): + if context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample: + bpy.ops.uas_gpu.dopesheet_gpu_sample("INVOKE_DEFAULT") + + bpy.types.WindowManager.UAS_shot_manager_display_dopesheet_gpu_sample = BoolProperty( + name="Show Dopesheet Gpu Sample", + description="Toggle the display of the dopesheet gpu sample in the Dopesheet editor", + update=_update_UAS_shot_manager__display_dopesheet_gpu_sample, + default=False, + ) + + +def unregister(): + for cls in reversed(_classes): + bpy.utils.unregister_class(cls) diff --git a/shotmanager/gpu/gpu_2d/samples/dopesheet_gpu_sample_widget.py b/shotmanager/gpu/gpu_2d/samples/dopesheet_gpu_sample_widget.py new file mode 100644 index 00000000..f02c38dc --- /dev/null +++ b/shotmanager/gpu/gpu_2d/samples/dopesheet_gpu_sample_widget.py @@ -0,0 +1,289 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +UI in BGL for the Interactive Shots Stack overlay tool +""" + +from collections import defaultdict + +import os +from mathutils import Vector + +import bgl +import gpu + + +from shotmanager.overlay_tools.interact_shots_stack.shots_stack_bgl import get_lane_origin_y + +from shotmanager.utils import utils_editors_dopesheet +from shotmanager.utils.utils import color_to_linear + +# from shotmanager.gpu.gpu_2d.class_Mesh2D import Mesh2D +from shotmanager.gpu.gpu_2d.class_Mesh2D import build_rectangle_mesh +from shotmanager.gpu.gpu_2d.class_QuadObject import QuadObject +from shotmanager.gpu.gpu_2d.class_Component2D import Component2D +from shotmanager.gpu.gpu_2d.class_Text2D import Text2D + +from shotmanager.overlay_tools.workspace_info import workspace_info + + +from shotmanager.config import config +from shotmanager.config import sm_logging + +_logger = sm_logging.getLogger(__name__) + +UNIFORM_SHADER_2D = gpu.shader.from_builtin("2D_UNIFORM_COLOR") + + +class DopesheetGpuSampleWidget: + def __init__(self, target_area=None): + prefs = config.getShotManagerPrefs() + + self.useDebugComponents = True + + self.context = None + self.target_area = target_area + + self.prev_mouse_x = 0 + self.prev_mouse_y = 0 + self.frame_under_mouse = -1 + + self.mouseFrame = -1 + self.previousMouseFrame = -1 + + self.previousDrawWasInAClip = False + + self.debug_mesh = None + self.debug_quadObject = None + self.debug_quadObject_Ruler = None + self.debug_quadObject_test = None + + self.color_currentShot_border = color_to_linear((0.92, 0.55, 0.18, 0.99)) + # self.color_currentShot_border = (1, 0, 0, 1) + self.color_currentShot_border_mix = (0.94, 0.3, 0.1, 0.99) + + # prefs settings ################### + self.opacity = prefs.shtStack_opacity + self.color_text = (0.0, 0.0, 0.0, 1) + + self.numRedraws = 0 + + def init(self, context): + self.context = context + + self.currentShotBorder = QuadObject( + posXIsInRegionCS=False, + posYIsInRegionCS=False, + widthIsInRegionCS=False, + heightIsInRegionCS=False, + alignment="BOTTOM_LEFT", + alignmentToRegion="TOP_LEFT", + ) + self.currentShotBorder.hasFill = False + self.currentShotBorder.hasLine = True + self.currentShotBorder.colorLine = self.color_currentShot_border_mix + self.currentShotBorder.lineThickness = 2 + self.currentShotBorder.isVisible = False + # self.currentShotBorder.lineOffsetPerEdge = [0, -1, -1, 0] + + # wip not super clean + # config.gRedrawShotStack = True + # config.gRedrawShotStack_preDrawOnly = True + + if self.useDebugComponents: + ############################################### + # debug + ############################################### + # height = 20 + # lane = 3 + # startframe = 120 + # numFrames = 15 + # origin = Vector([startframe, get_lane_origin_y(lane)]) + # self.debug_mesh = build_rectangle_mesh(origin, numFrames, height) + + # this quad is supposed to cover the time ruler all the time to check the top clipping zone + # BOTTOM_LEFT TOP_LEFT + self.debug_quadObject_Ruler = QuadObject( + posXIsInRegionCS=True, + posX=0, + posYIsInRegionCS=True, + posY=0, + widthIsInRegionCS=True, + width=1900, + heightIsInRegionCS=True, + height=utils_editors_dopesheet.getRulerHeight(), + alignment="TOP_LEFT", + alignmentToRegion="TOP_LEFT", + displayOverRuler=True, + ) + self.debug_quadObject_Ruler.color = (0.4, 0.0, 0.0, 0.5) + + self.debug_quadObject_test = QuadObject( + posXIsInRegionCS=False, + posX=20, + posYIsInRegionCS=False, + posY=1, + widthIsInRegionCS=False, + width=40, + heightIsInRegionCS=False, + height=10, + alignment="TOP_LEFT", + alignmentToRegion="TOP_LEFT", + ) + self.debug_quadObject_test.color = (0.0, 0.4, 0.0, 0.5) + imgFile = os.path.join(os.path.dirname(__file__), "../../../icons/ShotMan_EnabledCurrentCam.png") + self.debug_quadObject_test.setImageFromFile(imgFile) + self.debug_quadObject_test.hasTexture = True + + self.debug_quadObject = QuadObject( + posXIsInRegionCS=True, + posX=60, + posYIsInRegionCS=True, + posY=90, + widthIsInRegionCS=False, + width=25, + heightIsInRegionCS=True, + height=70, + alignment="TOP_LEFT", + alignmentToRegion="BOTTOM_RIGHT", + ) + + self.debug_component2D = Component2D( + self.target_area, + posXIsInRegionCS=False, + posX=15, + posYIsInRegionCS=False, + posY=3, + widthIsInRegionCS=False, + width=20, + heightIsInRegionCS=False, + height=2, + alignment="BOTTOM_LEFT", + alignmentToRegion="BOTTOM_LEFT", + ) + self.debug_component2D.color = (0.7, 0.5, 0.6, 0.6) + self.debug_component2D.hasLine = True + self.debug_component2D.colorLine = (0.7, 0.8, 0.8, 0.9) + self.debug_component2D.lineThickness = 3 + + self.debug_Text2D = Text2D( + posXIsInRegionCS=False, + posX=25, + posYIsInRegionCS=False, + posY=6, + alignment="BOTTOM_LEFT", + alignmentToRegion="BOTTOM_LEFT", + text="MyText", + fontSize=20, + ) + + self.redrawCounter_Text2D = Text2D( + posX=100, + posY=20, + alignment="BOTTOM_RIGHT", + alignmentToRegion="BOTTOM_RIGHT", + text="Num redraws: -", + fontSize=16, + ) + + def draw(self, preDrawOnly=False): + if self.target_area is not None and self.context.area != self.target_area: + return + + # Debug - red rectangle #################### + if self.useDebugComponents: + height = 20 + lane = 5 + startframe = 160 + numFrames = 15 + origin = Vector([startframe, get_lane_origin_y(lane)]) + self.debug_mesh = build_rectangle_mesh(origin, numFrames, height) + + bgl.glEnable(bgl.GL_BLEND) + UNIFORM_SHADER_2D.bind() + color = (0.9, 0.0, 0.0, 0.9) + UNIFORM_SHADER_2D.uniform_float("color", color) + + # self.debug_mesh.draw(UNIFORM_SHADER_2D, self.context.region) + + # Quad object + ############################### + + # self.debug_quadObject_Ruler.draw(None, self.context.region) + # self.debug_quadObject_test.draw(None, self.context.region) + # self.debug_quadObject.draw(None, self.context.region) + # self.debug_component2D.draw(None, self.context.region) + # self.debug_Text2D.draw(None, self.context.region) + + # draw text + ############################### + + # workspace_info.draw_callback__dopesheet_size(self, self.context, self.target_area) + # workspace_info.draw_callback__dopesheet_mouse_pos(self, self.context, self.target_area) + # workspace_info.draw_callback__dopesheet_lane_numbers(self, self.context, self.target_area) + + # redraw counter + ############################### + self.numRedraws += 1 + self.redrawCounter_Text2D.text = f"Num redraws: {self.numRedraws}" + self.redrawCounter_Text2D.draw(None, self.context.region) + + def validateAction(self): + _logger.debug_ext("Validating Shot Stack action", col="GREEN", tag="SHOTSTACK_EVENT") + pass + + def cancelAction(self): + # TODO restore the initial + _logger.debug_ext("Canceling Shot Stack action 22", col="ORANGE", tag="SHOTSTACK_EVENT") + pass + + def handle_event(self, context, event, region): + """Return True if the event is handled for DopesheetGpuSampleWidget""" + # props = context.scene.UAS_shot_manager_props + # prefs = config.getShotManagerPrefs() + + # _logger.debug_ext("*** handle event for DopesheetGpuSampleWidget", col="GREEN", tag="SHOTSTACK_EVENT") + if not context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample: + return False + + event_handled = False + # if event.type not in ["MOUSEMOVE", "INBETWEEN_MOUSEMOVE", "TIMER"]: + # _logger.debug_ext(f" *** event in DopesheetGpuSampleWidget: {event.type}", col="GREEN", tag="SHOTSTACK_EVENT") + + # NOTE: context is different. Normal? + context = self.context + + mouse_x, mouse_y = region.view2d.region_to_view(event.mouse_x - region.x, event.mouse_y - region.y) + + # if event.ctrl: + # self.infoComponent.setText("Ctrl") + # else: + # self.infoComponent.setText("4") + + if event.type not in ["TIMER"]: + _logger.debug_ext(f"event: type: {event.type}, value: {event.value}", col="GREEN", tag="SHOTSTACK_EVENT") + + # debug + if not event_handled: + if self.useDebugComponents: + event_handled = self.debug_component2D.handle_event(context, event) + + self.prev_mouse_x = event.mouse_x - region.x + self.prev_mouse_y = event.mouse_y - region.y + + return event_handled diff --git a/shotmanager/overlay_tools/interact_shots_stack/__init__.py b/shotmanager/overlay_tools/interact_shots_stack/__init__.py index c8f6bf3d..fef6df54 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/__init__.py +++ b/shotmanager/overlay_tools/interact_shots_stack/__init__.py @@ -29,6 +29,9 @@ # from . import shots_stack_bgl +# debug +from shotmanager.gpu.gpu_2d.samples import dopesheet_gpu_sample + from shotmanager.config import config from shotmanager.config import sm_logging @@ -48,6 +51,8 @@ def register(): shots_stack.register() # shots_stack_bgl.register() + dopesheet_gpu_sample.register() + # # # def _update_UAS_shot_manager__useInteracShotsStack(self, context): # # # # toggle_overlay_tools_display(context) # # # print(f"\nFrom Update fct: Toggle Interactive Shots Stack: {self.UAS_shot_manager__useInteracShotsStack}") @@ -69,6 +74,8 @@ def register(): def unregister(): _logger.debug_ext(" - Unregistering Interactive Shots Stack Package", form="UNREG") + dopesheet_gpu_sample.unregister() + # shots_stack_toolbar.unregister() shots_stack_toolbar.display_shots_stack_toolbar_in_editor(False) diff --git a/shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md b/shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md index 0bad7e81..4976bdc2 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md +++ b/shotmanager/overlay_tools/interact_shots_stack/doc_interac_shots_stack.md @@ -21,6 +21,14 @@ ## See classes inheritance here: [GPU 2D Components](../../gpu/gpu_2d/doc_gpu_2d_components) +## Todebug: + +To display debug messages and information in opengl over the dopesheets use the functions draw_callback__dopesheet_lane_numbers and so +in workspace_info.py. +Just place them in a draw function: + workspace_info.draw_callback__dopesheet_mouse_pos(self, self.context, self.target_area) + + ## To do: - make current requires a TRIPLE click... diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py index f0db4a52..7a013a4c 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_prefs.py @@ -43,6 +43,7 @@ def draw_settings(context, layout): "interactShotsStack_displayInCompactMode", text="Compact Shots Display (= decrease visual stack height)", ) + propCol.prop(prefs, "shtStack_display_info_widget") firstLineRow = propCol.row(align=False) # firstLineRow.use_property_split = True @@ -120,14 +121,13 @@ def draw(self, context): mainRow = layout.row(align=True) mainRow.separator(factor=0.7) - col = mainRow.column(align=True) - col.separator(factor=0.7) - mainRow.separator(factor=0.7) + + propCol = propertyColumn(mainRow) # target dopesheet ####################### - targetrow = col.row(align=True) + targetrow = propCol.row(align=True) # activeindrow.scale_x = 0.4 targetrow.separator(factor=4) @@ -153,7 +153,7 @@ def draw(self, context): # seq timeline ####################### - targetrow = col.row(align=True) + targetrow = propCol.row(align=True) targetrow.operator( "uas_shot_manager.toggle_seq_timeline_with_overlay_tools", text="", @@ -165,13 +165,13 @@ def draw(self, context): # shots stack ####################### - row = col.row(align=True) - row.label(text="Shots Stack:") + propCol.separator(factor=1) + propCol.label(text="Shots Stack:") - row = col.row(align=False) - row.prop(prefs, "shtStack_opacity", text="Shots Stack Opacity", slider=True) + propCol.prop(prefs, "shtStack_opacity", text="Shots Stack Opacity", slider=True) + propCol.prop(prefs, "shtStack_display_info_widget") - col.separator(factor=0.7) + propCol.separator(factor=0.7) def execute(self, context): return {"FINISHED"} diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_toolbar.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_toolbar.py index 36aa667e..0a5b1c88 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_toolbar.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_toolbar.py @@ -69,6 +69,23 @@ def draw_shots_stack_toolbar_in_editor(self, context): row.separator(factor=0.8) + if config.devDebug: + # row.operator( + # "uas_gpu.dopesheet_gpu_sample", + # text="GPU", + # depress=context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample, + # # icon_value=icon.icon_id, + # ) + row.prop( + context.window_manager, + "UAS_shot_manager_display_dopesheet_gpu_sample", + text="GPU", + # depress=context.window_manager.UAS_shot_manager_display_dopesheet_gpu_sample, + # icon_value=icon.icon_id, + ) + + row.separator(factor=0.8) + row.operator( "uas_shot_manager.toggle_shots_stack_with_overlay_tools", text="", diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py index 391abf3b..171a91d4 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py @@ -52,10 +52,10 @@ class ShotClipComponent(Component2D): """Shot clip component""" - def __init__(self, targetArea, posY=2, shot=None): + def __init__(self, targetArea, posY=2, shot=None, shotsStack=None): Component2D.__init__( self, - targetArea, + targetArea=targetArea, posXIsInRegionCS=False, posX=10, posYIsInRegionCS=False, @@ -68,6 +68,8 @@ def __init__(self, targetArea, posY=2, shot=None): alignmentToRegion="TOP_LEFT", ) + self.shotsStackWidget = shotsStack + # preferences ############ self.pref_handleWidth = int(getLaneHeight() * 0.5) self.pref_distanceFromParentOrigin = self.pref_handleWidth * 1.5 @@ -153,6 +155,7 @@ def __init__(self, targetArea, posY=2, shot=None): parent=self, shot=self.shot, isStart=True, + shotsStack=self.shotsStackWidget, ) # end handle component ######## @@ -164,6 +167,7 @@ def __init__(self, targetArea, posY=2, shot=None): parent=self, shot=self.shot, isStart=False, + shotsStack=self.shotsStackWidget, ) ################################################################# @@ -385,11 +389,13 @@ def _on_manipulated_changed(self, context, event, isManipulated): """ if isManipulated: _logger.debug_ext("component2D handle_events set manipulated True", col="PURPLE", tag="EVENT") + self.shotsStackWidget.manipulatedComponent = self if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() pass else: _logger.debug_ext("component2D handle_events set manipulated False", col="PURPLE", tag="EVENT") + self.shotsStackWidget.manipulatedComponent = None self.manipulatedChildren = None pass @@ -414,20 +420,21 @@ def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): prefs = config.getShotManagerPrefs() if prefs.shtStack_link_stb_clips_to_keys and self.manipulatedChildren is not None: - retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo - retimerApplyToSettings.initialize("STORYBOARD_CLIP") - - # if offset_duration > 0 we insert time from a point far in negative time - # if offset_duration < 0 we delete time from a point very far in negative time - farRefPoint = -100000 - retimeScene( - context, - "GLOBAL_OFFSET", - retimerApplyToSettings, - self.manipulatedChildren, - farRefPoint + 1, - mouse_delta_frames, - ) + if not (not event.ctrl and event.alt and not event.shift): + retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo + retimerApplyToSettings.initialize("STORYBOARD_CLIP") + + # if offset_duration > 0 we insert time from a point far in negative time + # if offset_duration < 0 we delete time from a point very far in negative time + farRefPoint = -100000 + retimeScene( + context=context, + retimeMode="GLOBAL_OFFSET", + retimerApplyToSettings=retimerApplyToSettings, + objects=self.manipulatedChildren, + start_incl=farRefPoint + 1, + duration_incl=mouse_delta_frames, + ) # to override by inheriting classes def _on_doublecliked(self, context, event, region): diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py index 484b6c9c..196e0ea9 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py @@ -38,7 +38,17 @@ class ShotHandleComponent(Component2D): """Handle for shot clip component""" - def __init__(self, targetArea, posY=2, width=32, alignment="BOTTOM_LEFT", parent=None, shot=None, isStart=True): + def __init__( + self, + targetArea, + posY=2, + width=32, + alignment="BOTTOM_LEFT", + parent=None, + shot=None, + isStart=True, + shotsStack=None, + ): Component2D.__init__( self, targetArea, @@ -55,6 +65,8 @@ def __init__(self, targetArea, posY=2, width=32, alignment="BOTTOM_LEFT", parent parent=parent, ) + self.shotsStackWidget = shotsStack + # shot ################### self.shot = shot self.zOrder = -1.0 @@ -65,7 +77,7 @@ def __init__(self, targetArea, posY=2, width=32, alignment="BOTTOM_LEFT", parent # manipulation # filled when isManipulated changes self.manipulatedChildren = None - self.manipulationBeginningFrame = None + self.manipulationBeginingFrame = None # green or orange self.color_highlight = (0.2, 0.7, 0.2, 1) if self.isStart else (0.7, 0.3, 0.0, 1) @@ -156,44 +168,54 @@ def _on_manipulated_changed(self, context, event, isManipulated): function is called """ # we use this to set the color of the clip as for when manipulated + self.manipulatedChildren = None + if isManipulated: + self.shotsStackWidget.manipulatedComponent = self self.parent.isManipulatedByAnotherComponent = True + self.manipulationBeginingFrame = context.scene.frame_current if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() - self.manipulationBeginningFrame = context.scene.frame_current + else: + if self.shot.isCameraValid(): + self.manipulatedChildren = [self.shot.camera] else: + self.shotsStackWidget.manipulatedComponent = None self.parent.isManipulatedByAnotherComponent = False - self.manipulatedChildren = None - self.manipulationBeginningFrame = None + self.manipulationBeginingFrame = None # override of InteractiveComponent def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): """wkip note: delta_frames is in frames but may need to be in pixels in some cases""" # !! we have to be sure we work on the selected shot !!! + prevShotStart = self.shot.start + prevShotEnd = self.shot.end + if self.isStart: - prevShotStart = self.shot.start self.shot.start += mouse_delta_frames - # bpy.ops.uas_shot_manager.set_shot_start(newStart=self.start + mouse_delta_frames) - - prefs = config.getShotManagerPrefs() - if prefs.shtStack_link_stb_clips_to_keys and event.ctrl: - if self.manipulatedChildren is not None: - retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo - retimerApplyToSettings.initialize("STORYBOARD_CLIP") - - retimeFactor = (self.shot.end - self.shot.start) / (self.shot.end - prevShotStart) - # retimeFactor = 0.5 - retimeScene( - context, - "RESCALE", - retimerApplyToSettings, - self.manipulatedChildren, - -10000, - 90000, - True, - retimeFactor, - self.shot.end, - ) - + pivot = self.shot.end else: self.shot.end += mouse_delta_frames + pivot = self.shot.start + + # bpy.ops.uas_shot_manager.set_shot_start(newStart=self.start + mouse_delta_frames) + + prefs = config.getShotManagerPrefs() + if prefs.shtStack_link_stb_clips_to_keys and self.manipulatedChildren is not None: + if not event.ctrl and not event.alt and event.shift: + retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo + retimerApplyToSettings.initialize("STORYBOARD_CLIP") + + retimeFactor = (self.shot.end - self.shot.start) / (prevShotEnd - prevShotStart) + # retimeFactor = 0.5 + retimeScene( + context=context, + retimeMode="RESCALE", + retimerApplyToSettings=retimerApplyToSettings, + objects=self.manipulatedChildren, + start_incl=-10000, + duration_incl=90000, + join_gap=True, + factor=retimeFactor, + pivot=pivot, + ) diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_info_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_info_component.py new file mode 100644 index 00000000..d1ac2129 --- /dev/null +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_info_component.py @@ -0,0 +1,247 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +UI in BGL for the Interactive Shots Stack overlay tool +""" + + +import os + +import bpy +import gpu +import blf + +from shotmanager.gpu.gpu_2d.class_QuadObject import QuadObject +from shotmanager.gpu.gpu_2d.class_Text2D import Text2D + +# from shotmanager.gpu.gpu_2d.class_Component2D import Component2D + + +# from shotmanager.gpu.gpu_2d.gpu_2d import loadImageAsTexture + +from shotmanager.utils import utils +from shotmanager.utils.utils_python import clamp +from shotmanager.utils.utils import color_to_sRGB, lighten_color, set_color_alpha, alpha_to_linear, color_to_linear + + +from shotmanager.config import config +from shotmanager.config import sm_logging + +_logger = sm_logging.getLogger(__name__) + +UNIFORM_SHADER_2D = gpu.shader.from_builtin("2D_UNIFORM_COLOR") + + +class InfoComponent(QuadObject): + """Info component""" + + def __init__(self, shotsStack=None): + QuadObject.__init__( + self, + posXIsInRegionCS=True, + posX=10, + posYIsInRegionCS=True, + posY=18, + widthIsInRegionCS=True, + width=252, + heightIsInRegionCS=True, + height=35, + alignment="BOTTOM_LEFT", + alignmentToRegion="BOTTOM_LEFT", + ) + + self.shotsStackWidget = shotsStack + + self.useDebugComponents = True + + self.text = "" + self.modifierKeyPressed = False + self.ctrlAction = False + self.altAction = False + self.shiftAction = False + self.show = True + + self.fontSize = 11 + + def init(self, context): + self.color = (0.4, 0.4, 0.4, 0.5) + self.textTitleColor = (0.9, 0.9, 0.9, 0.94) + self.textColor = (0.7, 0.7, 0.7, 0.9) + self.keyModifierPressedColor = (0.9, 0.9, 0.0, 0.9) + + self.textLineTitle = Text2D( + text="", + fontSize=self.fontSize, + parent=self, + ) + self.textLineTitle.color = self.textTitleColor + + self.textLine01 = Text2D( + text="", + fontSize=self.fontSize, + parent=self, + ) + + self.textLine02 = Text2D( + text="", + fontSize=self.fontSize, + parent=self, + ) + + if self.useDebugComponents: + self.debugInfosQuad = QuadObject( + posXIsInRegionCS=True, + posX=280, + posYIsInRegionCS=True, + posY=18, + widthIsInRegionCS=True, + width=260, + heightIsInRegionCS=True, + height=20, + alignment="BOTTOM_LEFT", + alignmentToRegion="BOTTOM_RIGHT", + ) + self.debugInfosQuad.color = (0.6, 0.4, 0.4, 0.5) + + self.debugTextLine01 = Text2D( + text="Debug infos", + posX=5, + posY=5, + fontSize=self.fontSize, + parent=self.debugInfosQuad, + ) + + ################################################################# + + # def setText(self, text): + # if text != self.text: + # self.text = text + # self.updateDisplayedInfo() + # config.gRedrawShotStack = True + + # def setModifierKeyState(self, modifierKeyPressed): + # if modifierKeyPressed != self.modifierKeyPressed: + # self.modifierKeyPressed = modifierKeyPressed + # self.updateDisplayedInfo() + # config.gRedrawShotStack = True + + def updateFromEvent(self, event): + # newCtrlAction = event.ctrl and not event.alt and not event.shift + newAltAction = not event.ctrl and event.alt and not event.shift + newShiftAction = not event.ctrl and not event.alt and event.shift + + if newAltAction != self.altAction or newShiftAction != self.shiftAction: + self.altAction = newAltAction + self.shiftAction = newShiftAction + # self.updateDisplayedInfo() + config.gRedrawShotStack = True + + def updateDisplayedInfo(self): + props = bpy.context.scene.UAS_shot_manager_props + selectedShot = props.getSelectedShot() + manipulatedCompoType = type(self.shotsStackWidget.manipulatedComponent).__name__ + + if selectedShot: + self.show = True + self.textLine01.color = self.textColor + self.textLine02.color = self.textColor + + if "PREVIZ" == selectedShot.shotType: + self.height = 50 + self.textLineTitle.text = "Camera Shot" + self.textLine01.text = "+ Shift on Handles: Scale camera keys" + self.textLine02.text = "+ Shift on clip: Move camera keys" + + if self.shiftAction: + if "ShotHandleComponent" == manipulatedCompoType: + self.textLine01.color = self.keyModifierPressedColor + self.textLine01.text = "Shift pressed: Scaling camera keys" + elif "ShotClipComponent" == manipulatedCompoType: + self.textLine02.color = self.keyModifierPressedColor + self.textLine02.text = "Shift pressed: Moving camera keys" + else: + self.textLine01.color = self.keyModifierPressedColor + self.textLine02.color = self.keyModifierPressedColor + else: + self.height = 50 + self.textLineTitle.text = "Storyboard Shot" + self.textLine01.text = "+ Shift on Handles: Scale storyboard frame keys" + self.textLine02.text = "+ Alt: Sleep shot range only" + + if self.shiftAction: + self.textLine01.color = self.keyModifierPressedColor + if "ShotHandleComponent" == manipulatedCompoType: + self.textLine01.text = "Shift pressed: Scaling storyboard frame keys" + + if self.altAction: + self.textLine02.color = self.keyModifierPressedColor + if "ShotClipComponent" == manipulatedCompoType: + self.textLine02.text = "Alt pressed: Sleeping shot range only" + + else: + # self.textLineTitle.text = "-" + self.show = False + + ################################################################# + + # drawing ########## + + ################################################################# + + # override QuadObject + def draw(self, shader=None, region=None, draw_types="TRIS", cap_lines=False, preDrawOnly=False): + prefs = config.getShotManagerPrefs() + if not prefs.shtStack_display_info_widget: + return + + leftOffset = 6 + + self.updateDisplayedInfo() + vOffset = self.height - self.textLine01.fontSize - 2 + + self.textLineTitle.posX = leftOffset + self.textLineTitle.posY = vOffset + + vOffset -= 15 + self.textLine01.posX = leftOffset + self.textLine01.posY = vOffset + + vOffset -= 15 + self.textLine02.posX = leftOffset + self.textLine02.posY = vOffset + + # self.draw_infos(region) + + # def draw_infos(self, region): + # blf.color(0, 0.9, 0.9, 0.9, 0.9) + # blf.size(0, 12, 72) + # offset_y = 80 + + # txtStr = f"Mouse pos: Screen: x: " + + # blf.position(0, 60, offset_y, 0) + # blf.draw(0, txtStr) + + if self.useDebugComponents: + self.debugTextLine01.text = f"Manip Compo: {type(self.shotsStackWidget.manipulatedComponent).__name__}" + + self.debugInfosQuad.draw(None, region) + + if self.show: + # children such as the text2D are drawn in QuadObject + QuadObject.draw(self, None, region) diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py index e2a62a17..3ed62073 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_widget.py @@ -27,7 +27,6 @@ import bgl import gpu -from shotmanager.overlay_tools.interact_shots_stack.widgets.shots_stack_clip_component import ShotClipComponent from ..shots_stack_bgl import get_lane_origin_y @@ -42,6 +41,8 @@ from shotmanager.overlay_tools.workspace_info import workspace_info +from shotmanager.overlay_tools.interact_shots_stack.widgets.shots_stack_clip_component import ShotClipComponent +from shotmanager.overlay_tools.interact_shots_stack.widgets.shots_stack_info_component import InfoComponent from shotmanager.config import config from shotmanager.config import sm_logging @@ -62,6 +63,7 @@ def __init__(self, target_area=None): self.ui_shots = list() self.shotComponents = [] + self.infoComponent = None self.prev_mouse_x = 0 self.prev_mouse_y = 0 @@ -71,6 +73,7 @@ def __init__(self, target_area=None): self.previousMouseFrame = -1 self.previousDrawWasInAClip = False + self.manipulatedComponent = None self.debug_mesh = None self.debug_quadObject = None @@ -109,6 +112,9 @@ def init(self, context): config.gRedrawShotStack = True config.gRedrawShotStack_preDrawOnly = True + self.infoComponent = InfoComponent(shotsStack=self) + self.infoComponent.init(context) + if self.useDebugComponents: ############################################### # debug @@ -222,7 +228,7 @@ def rebuildShotComponents(self, forceRebuild=False): lane = 1 for _i, shot in enumerate(shots): - shotCompo = ShotClipComponent(self.target_area, posY=lane, shot=shot) + shotCompo = ShotClipComponent(self.target_area, posY=lane, shot=shot, shotsStack=self) shotCompo.opacity = self.opacity shotCompo.color_text = self.color_text @@ -370,6 +376,8 @@ def draw(self, preDrawOnly=False): # return + self.infoComponent.draw(None, self.context.region) + if props.interactShotsStack_displayInCompactMode: self.drawShots_compactMode(preDrawOnly=preDrawOnly) else: @@ -386,7 +394,8 @@ def cancelAction(self): def handle_event(self, context, event, region): """Return True if the event is handled for ShotStackWidget""" - prefs = config.getShotManagerPrefs() + # props = context.scene.UAS_shot_manager_props + # prefs = config.getShotManagerPrefs() # _logger.debug_ext("*** handle event for ShotStackWidget", col="GREEN", tag="SHOTSTACK_EVENT") if not context.window_manager.UAS_shot_manager_toggle_shots_stack_interaction: @@ -398,10 +407,22 @@ def handle_event(self, context, event, region): # NOTE: context is different. Normal? context = self.context - props = context.scene.UAS_shot_manager_props mouse_x, mouse_y = region.view2d.region_to_view(event.mouse_x - region.x, event.mouse_y - region.y) + # update info component + self.infoComponent.updateFromEvent(event) + # if event.shift: + # # self.infoComponent.posY = 80 + # # self.infoComponent.textLine01.posX = 40 + # self.infoComponent.setText("Ctrl") + # self.infoComponent.setModifierKeyState(True) + # else: + # # self.infoComponent.posY = 20 + # # self.infoComponent.textLine01.posX = 10 + # self.infoComponent.setText("4") + # self.infoComponent.setModifierKeyState(False) + if event.type not in ["TIMER"]: _logger.debug_ext(f"event: type: {event.type}, value: {event.value}", col="GREEN", tag="SHOTSTACK_EVENT") diff --git a/shotmanager/properties/props.py b/shotmanager/properties/props.py index 617a2421..601e6f33 100644 --- a/shotmanager/properties/props.py +++ b/shotmanager/properties/props.py @@ -2075,7 +2075,8 @@ def _update_selected_shot_index(self, context): if self.selected_shot_index_call_update__flag: props = context.scene.UAS_shot_manager_props prefs = config.getShotManagerPrefs() - _logger.debug_ext(f"\n*** selected_shot_index. New state: {self.selected_shot_index}") + if -1 != self.selected_shot_index: + _logger.debug_ext(f"\n*** selected_shot_index. New state: {self.selected_shot_index}") setCurrentShot = False # if False: diff --git a/shotmanager/retimer/retimer.py b/shotmanager/retimer/retimer.py index c5f40851..d197682e 100644 --- a/shotmanager/retimer/retimer.py +++ b/shotmanager/retimer/retimer.py @@ -796,6 +796,7 @@ def remove_time(sed, start_frame, end_frame, remove_gap): def retimeScene( + *, context, retimeMode: str, retimerApplyToSettings, @@ -812,7 +813,7 @@ def retimeScene( start_incl (int): The included start frame duration_incl (int): The range of retime frames (new or deleted) """ - prefs = config.getShotManagerPrefs() + # prefs = config.getShotManagerPrefs() scene = context.scene current_frame = scene.frame_current diff --git a/shotmanager/retimer/retimer_operators.py b/shotmanager/retimer/retimer_operators.py index bf5978e5..d618aa2a 100644 --- a/shotmanager/retimer/retimer_operators.py +++ b/shotmanager/retimer/retimer_operators.py @@ -136,13 +136,13 @@ def execute(self, context): farRefPoint = -100000 retimer.retimeScene( - context, - "GLOBAL_OFFSET", - retimerApplyToSettings, - sceneObjs, - farRefPoint + 1, - retimeEngine.offset_duration, - retimeEngine.gap, + context=context, + retimeMode="GLOBAL_OFFSET", + retimerApplyToSettings=retimerApplyToSettings, + objects=sceneObjs, + start_incl=farRefPoint + 1, + duration_incl=retimeEngine.offset_duration, + join_gap=retimeEngine.gap, ) elif -1 < retimeEngine.mode.find("INSERT"): @@ -154,15 +154,15 @@ def execute(self, context): f"\nRetimer - Inserting time: new created frames: [{start_excl + 1} .. {end_excl - 1}], duration: {retimeEngine.insert_duration}" ) retimer.retimeScene( - context, - "INSERT", - retimerApplyToSettings, - sceneObjs, - start_excl + 1, - retimeEngine.insert_duration, - retimeEngine.gap, - 1.0, - retimeEngine.pivot, + context=context, + retimeMode="INSERT", + retimerApplyToSettings=retimerApplyToSettings, + objects=sceneObjs, + start_incl=start_excl + 1, + duration_incl=retimeEngine.insert_duration, + join_gap=retimeEngine.gap, + factor=1.0, + pivot=retimeEngine.pivot, ) elif -1 < retimeEngine.mode.find("DELETE"): @@ -173,15 +173,15 @@ def execute(self, context): f"\nRetimer - Deleting time: deleted frames: [{start_excl + 1} .. {end_excl - 1}], duration: {duration_incl}" ) retimer.retimeScene( - context, - "DELETE", - retimerApplyToSettings, - sceneObjs, - start_excl + 1, - duration_incl, - True, - 1.0, - retimeEngine.pivot, + context=context, + retimeMode="DELETE", + retimerApplyToSettings=retimerApplyToSettings, + objects=sceneObjs, + start_incl=start_excl + 1, + duration_incl=duration_incl, + join_gap=True, + factor=1.0, + pivot=retimeEngine.pivot, ) elif "RESCALE" == retimeEngine.mode: # *** Warning: due to the nature of the time operation the duration is not computed as for Delete Time *** @@ -192,15 +192,15 @@ def execute(self, context): f"\nRetimer - Rescaling time: modified frames: [{start_excl} .. {end_excl - 1}], duration: {duration_incl}" ) retimer.retimeScene( - context, - "RESCALE", - retimerApplyToSettings, - sceneObjs, - start_excl, - duration_incl, - True, - retimeEngine.factor, - start_excl, + context=context, + retimeMode="RESCALE", + retimerApplyToSettings=retimerApplyToSettings, + objects=sceneObjs, + start_incl=start_excl, + duration_incl=duration_incl, + join_gap=True, + factor=retimeEngine.factor, + pivot=start_excl, ) elif "CLEAR_ANIM" == retimeEngine.mode: @@ -211,15 +211,15 @@ def execute(self, context): f"\nRetimer - Deleting animation: cleared frames: [{start_excl + 1} .. {end_excl - 1}], duration:{duration_incl}" ) retimer.retimeScene( - context, - "CLEAR_ANIM", - retimerApplyToSettings, - sceneObjs, - start_excl + 1, - duration_incl, - False, - retimeEngine.factor, - retimeEngine.pivot, + context=context, + retimeMode="CLEAR_ANIM", + retimerApplyToSettings=retimerApplyToSettings, + objects=sceneObjs, + start_incl=start_excl + 1, + duration_incl=duration_incl, + join_gap=False, + factor=retimeEngine.factor, + pivot=retimeEngine.pivot, ) else: print(f"*** Retimer failed: No Retimer mode named {retimeEngine.mode} ***") From 23af19cf39ac63365a85e88f49cee4ed2474df5a Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Thu, 22 Sep 2022 23:47:58 +0200 Subject: [PATCH 18/27] wip retimer - shift on cam shots ok --- resources/_to_delete_/retimer.py | 2 +- shotmanager/__init__.py | 2 +- shotmanager/config/config.py | 11 +- shotmanager/debug/sm_debug_ui.py | 31 +- .../widgets/shots_stack_clip_component.py | 23 +- .../widgets/shots_stack_handle_component.py | 23 +- shotmanager/retimer/retimer.py | 363 +++++++++++++----- .../retimer/retimer_applyto_settings.py | 65 +++- shotmanager/retimer/retimer_applyto_ui.py | 25 +- shotmanager/retimer/retimer_operators.py | 4 +- 10 files changed, 401 insertions(+), 148 deletions(-) diff --git a/resources/_to_delete_/retimer.py b/resources/_to_delete_/retimer.py index b8568cfe..ee9f756a 100644 --- a/resources/_to_delete_/retimer.py +++ b/resources/_to_delete_/retimer.py @@ -908,7 +908,7 @@ def retimeScene( retime_vse(scene, mode, start_incl, end_incl) # Shots - if retimerApplyToSettings.applyToCameraShots: + if retimerApplyToSettings.applyToCameraShotRanges: props = scene.UAS_shot_manager_props shotList = props.getShotsList(ignoreDisabled=False) diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index b64d2a2e..fa4bf2a9 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -332,7 +332,7 @@ def _update_UAS_shot_manager_identify_dopesheets(self, context): print(f"\n ------ Ubisoft Shot Manager debug: {config.devDebug} ------- ") addon_prefs_inst = config.getShotManagerPrefs() - addon_prefs_inst.displaySMDebugPanel = False + addon_prefs_inst.displaySMDebugPanel = config.devDebug_displayDebugPanel # _props = bpy.context.scene.UAS_shot_manager_props # # currentLayout = props.getCurrentLayout() diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index 735a3d99..72ada62d 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -45,6 +45,9 @@ def initGlobalVariables(): # change this value to force debug at start time devDebug = True + global devDebug_displayDebugPanel + devDebug_displayDebugPanel = True + global devDebug_lastRedrawTime devDebug_lastRedrawTime = -1 @@ -149,9 +152,9 @@ def getLoggingTags(): tags["REG"] = True tags["UNREG"] = True - tags["INIT_AND_DATA"] = True + tags["INIT_AND_DATA"] = False - tags["SHOTS_PLAY_MODE"] = True + tags["SHOTS_PLAY_MODE"] = False tags["RENDER"] = False tags["LAYOUT"] = False @@ -160,8 +163,8 @@ def getLoggingTags(): tags["EDIT_IO"] = True tags["TIMELINE_EVENT"] = False - tags["SHOTSTACK_EVENT"] = True - tags["EVENT"] = True + tags["SHOTSTACK_EVENT"] = False + tags["EVENT"] = False # info tags tags["RENDERTIME"] = False diff --git a/shotmanager/debug/sm_debug_ui.py b/shotmanager/debug/sm_debug_ui.py index ba529dcc..74215338 100644 --- a/shotmanager/debug/sm_debug_ui.py +++ b/shotmanager/debug/sm_debug_ui.py @@ -189,17 +189,28 @@ def _getSelectedKeysOfSelObj(): for gpLayer in gpencil.data.layers: for kf in gpLayer.frames: if kf.select: - keys.append([kf.frame_number]) + keys.append([kf.frame_number, 0]) else: + # transformation anim if obj.animation_data: action = obj.animation_data.action + if action: + for fcurve in action.fcurves: + for p in fcurve.keyframe_points: + if p.select_control_point: + # print(p.co[0], p.select_control_point) + keys.append(p) - for fcurve in action.fcurves: - for p in fcurve.keyframe_points: - if p.select_control_point: - # print(p.co[0], p.select_control_point) - keys.append([p.co[0]]) + # data anim + if obj.data.animation_data: + action = obj.data.animation_data.action + if action: + for fcurve in action.fcurves: + for p in fcurve.keyframe_points: + if p.select_control_point: + # print(p.co[0], p.select_control_point) + keys.append(p) return keys keys = _getSelectedKeysOfSelObj() @@ -207,7 +218,13 @@ def _getSelectedKeysOfSelObj(): if len(keys): for i, k in enumerate(keys): if i < 3: - layout.label(text=f" key at fr. {k[0]}") + # try: + layout.label( + text=f" key at fr. {k.co[0]:0.2f}, val: {k.co[1]:0.2f}, left h: {k.handle_left}, right h: {k.handle_right}" + ) + # except Exception: + # layout.label(text=f" GP key at fr. {k}") + else: layout.label(text=f"... and {len(keys) - 3} other keys") break diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py index 171a91d4..cfc39880 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py @@ -387,17 +387,20 @@ def _on_manipulated_changed(self, context, event, isManipulated): """isManipulated has the same value than self.isManipulated, which is set right before this function is called """ + + self.manipulatedChildren = None + if isManipulated: _logger.debug_ext("component2D handle_events set manipulated True", col="PURPLE", tag="EVENT") self.shotsStackWidget.manipulatedComponent = self if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() - pass + else: + if self.shot.isCameraValid(): + self.manipulatedChildren = [self.shot.camera] else: _logger.debug_ext("component2D handle_events set manipulated False", col="PURPLE", tag="EVENT") self.shotsStackWidget.manipulatedComponent = None - self.manipulatedChildren = None - pass # override of InteractiveComponent def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): @@ -420,10 +423,18 @@ def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): prefs = config.getShotManagerPrefs() if prefs.shtStack_link_stb_clips_to_keys and self.manipulatedChildren is not None: - if not (not event.ctrl and event.alt and not event.shift): - retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo - retimerApplyToSettings.initialize("STORYBOARD_CLIP") + retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo + + offsetShotContent = False + if self.shot.isStoryboardType(): + offsetShotContent = not (not event.ctrl and event.alt and not event.shift) + retimerApplyToSettings.initialize("STB_SHOT_CLIP") + else: + offsetShotContent = not event.ctrl and not event.alt and event.shift + retimerApplyToSettings.initialize("PVZ_SHOT_CLIP") + + if offsetShotContent: # if offset_duration > 0 we insert time from a point far in negative time # if offset_duration < 0 we delete time from a point very far in negative time farRefPoint = -100000 diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py index 196e0ea9..3086403a 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py @@ -174,6 +174,7 @@ def _on_manipulated_changed(self, context, event, isManipulated): self.shotsStackWidget.manipulatedComponent = self self.parent.isManipulatedByAnotherComponent = True self.manipulationBeginingFrame = context.scene.frame_current + if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() else: @@ -202,20 +203,32 @@ def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): prefs = config.getShotManagerPrefs() if prefs.shtStack_link_stb_clips_to_keys and self.manipulatedChildren is not None: - if not event.ctrl and not event.alt and event.shift: - retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo - retimerApplyToSettings.initialize("STORYBOARD_CLIP") + + retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo + + scaleShotContent = False + if self.shot.isStoryboardType(): + scaleShotContent = not event.ctrl and not event.alt and event.shift + retimerApplyToSettings.initialize("STB_SHOT_CLIP") + else: + scaleShotContent = not event.ctrl and not event.alt and event.shift + retimerApplyToSettings.initialize("PVZ_SHOT_CLIP") + + if scaleShotContent: + # do NOT snap on frames during scaling transformation otherwhise frames will be lost because merged at the same time ! + retimerApplyToSettings.snapKeysToFrames = False retimeFactor = (self.shot.end - self.shot.start) / (prevShotEnd - prevShotStart) - # retimeFactor = 0.5 retimeScene( context=context, retimeMode="RESCALE", retimerApplyToSettings=retimerApplyToSettings, objects=self.manipulatedChildren, start_incl=-10000, - duration_incl=90000, + duration_incl=900000, join_gap=True, factor=retimeFactor, pivot=pivot, + keysBeforeRangeMode="RESCALE", + keysAfterRangeMode="RESCALE", ) diff --git a/shotmanager/retimer/retimer.py b/shotmanager/retimer/retimer.py index d197682e..f15d52cc 100644 --- a/shotmanager/retimer/retimer.py +++ b/shotmanager/retimer/retimer.py @@ -23,7 +23,7 @@ from shotmanager.utils.utils_markers import sortMarkers -from shotmanager.config import config +# from shotmanager.config import config from shotmanager.config import sm_logging _logger = sm_logging.getLogger(__name__) @@ -56,10 +56,11 @@ def handles(self, index): # def set_handles ( self, index, value ): # self.fcurve.keyframe_points[ index ].handle_left, self.fcurve.keyframe_points[ index ].handle_right = value - def insert_frame(self, coordinates): - kf = self.fcurve.keyframe_points.insert(coordinates[0], coordinates[1]) + def insert_frame(self, coordinates, roundToNearestFrame=True): + frame_val = round(coordinates[0]) if roundToNearestFrame else coordinate[0] + self.fcurve.keyframe_points.insert(frame_val, coordinates[1]) - def remove_frames(self, start_incl, end_incl, remove_gap=False): + def remove_frames(self, start_incl, end_incl, remove_gap=False, roundToNearestFrame=True): to_remove = list() for k in self.fcurve.keyframe_points: @@ -95,13 +96,13 @@ def remove_frames(self, start_incl, end_incl, remove_gap=False): # print(f" rr04 start_incl_key_right_handle_right02 {start_incl_key_right_handle_right02}") if remove_gap: - _offset_frames(self, end_incl, start_incl - end_incl - 1) + _offset_fCurve_frames(self, end_incl, start_incl - end_incl - 1, roundToNearestFrame) def __len__(self): return len(self.fcurve.keyframe_points) -# def _offset_frames(fcurve: FCurve, start_incl, offset): +# def _offset_fCurve_frames(fcurve: FCurve, start_incl, offset): # for i in range(len(fcurve)): # key_time, value = fcurve.get_key_coordinates(i) # if start_incl <= key_time: @@ -152,8 +153,8 @@ def computeNewFrameValue(frame_value, mode, start_incl=0, end_incl=0, pivot=0, f """Return the value of the time (in frames) after the retiming Return None if the new time value is not available (deleted time for example) It supports floating time frames. - ARgs: - roundToNearestFrame: round to nearest frame + Args: + roundToNearestFrame: round the frame time value """ new_frame_value = frame_value @@ -195,11 +196,23 @@ def computeNewFrameValue(frame_value, mode, start_incl=0, end_incl=0, pivot=0, f def compute_offset(frame_value, pivot, factor, roundToNearestFrame=True): + """Compute the offset value to add to frame_value to scale it from the pivot and by the given factor. + If frame_value > pivot then the computed offset is negative. + Return the offset""" + duration_to_pivot = pivot - frame_value + new_duration_to_pivot = duration_to_pivot * factor + offset = duration_to_pivot - new_duration_to_pivot + offset = round(offset) if roundToNearestFrame else offset + return offset + + +def apply_offset(frame_value, pivot, factor, roundToNearestFrame=True): """Compute the new value of frame_value when scaled from the pivot and by the given factor""" duration_to_pivot = frame_value - pivot offset = duration_to_pivot * factor - return round(offset) if roundToNearestFrame else offset - # return round(duration_to_pivot * factor) # + pivot + new_frame_value = frame_value + offset + new_frame_value = round(new_frame_value) if roundToNearestFrame else new_frame_value + return new_frame_value def rescale_frame(frame_value, start_incl, end_incl, pivot, factor, roundToNearestFrame=True): @@ -246,8 +259,28 @@ def rescale_frame(frame_value, start_incl, end_incl, pivot, factor, roundToNeare # new_value = if round_result else # return new_value +########################################################################## +# fcurve +########################################################################## + -def _stretch_frames(fcurve: FCurve, start_incl, end_incl, factor, pivot_value, clamp): +def _rescale_fCurve_frames( + *, + fcurve: FCurve, + start_incl, + end_incl, + factor, + pivot, + clamp=False, + roundToNearestFrame=True, + keysBeforeRangeMode="DO_NOTHING", + keysAfterRangeMode="DO_NOTHING", +): + """ + Args: + keysBeforeRangeMode: Action to do on keys located before the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" + keysAfterRangeMode: Action to do on keys located after the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" + """ # First pass. if clamp: remove_pre_start = list() @@ -265,41 +298,173 @@ def _stretch_frames(fcurve: FCurve, start_incl, end_incl, factor, pivot_value, c if remove_post_end: fcurve.remove_frames(min(remove_post_end), max(remove_post_end), False) - else: - if factor > 1: - _offset_frames( - fcurve, end_incl + 1, compute_offset(end_incl + 1, pivot_value, factor) - (end_incl - start_incl + 1) - ) - # print(f" rescale offset 01: {compute_offset(end_incl + 1, pivot_value, factor)}") - # wkip delete existing keys when factor < 1.0 + # else: + # if factor > 1: + # _offset_fCurve_frames( + # fcurve, + # end_incl + 1, + # compute_offset(end_incl + 1, pivot, factor, roundToNearestFrame=False) + # - (end_incl - start_incl + 1), + # roundToNearestFrame, + # ) + # # print(f" rescale offset 01: {compute_offset(end_incl + 1, pivot, factor)}") + + # TODO: wkip delete existing or ducplicated keys when factor < 1.0 + # bpy.ops.action.clean(threshold=0.001, channels=False) for i in range(len(fcurve)): - coordinates = fcurve.get_key_coordinates(i) - if start_incl <= coordinates[0] <= end_incl: - handles = fcurve.handles(i) - offset = compute_offset(coordinates[0], pivot_value, factor) # - start_incl + 1 - # fcurve.set_key_coordinates(i, (coordinates[0] + offset, coordinates[1])) - fcurve.set_key_coordinates(i, (pivot_value + offset, coordinates[1])) - handles[0][0] = pivot_value + compute_offset(handles[0][0], pivot_value, factor) - handles[1][0] = pivot_value + compute_offset(handles[1][0], pivot_value, factor) - - if factor < 1.0: - _offset_frames( - fcurve, end_incl + 1, compute_offset(end_incl + 1, pivot_value, factor) - (end_incl - start_incl + 1) - ) + key_time, key_value = fcurve.get_key_coordinates(i) + changeMode = "RESCALE" + if key_time < start_incl: + changeMode = keysBeforeRangeMode + elif end_incl < key_time: + changeMode = keysAfterRangeMode + + # if start_incl <= key_time <= end_incl: + + if "RESCALE" == changeMode: + offset = compute_offset(key_time, pivot, factor, roundToNearestFrame=False) # - start_incl + 1 + new_key_time = key_time + offset + if roundToNearestFrame: + new_key_time = round(new_key_time) + fcurve.set_key_coordinates(i, (new_key_time, key_value)) + + # handle coordinates are absolute, not changing when the key position changes + # we want to apply the scaling on the x axis of the handles that corresponds to the + # final move of the key (so including the rounding, if applicated) + # To do so we then compute back the corresponding scale factor + + # offset_with_rounded_added_offset = new_key_time - key_time + + left_handle, right_handle = fcurve.handles(i) + if key_time != pivot: + factor_for_rounded_offset = (new_key_time - pivot) / (key_time - pivot) + + left_handle[0] += compute_offset( + left_handle[0], pivot, factor_for_rounded_offset, roundToNearestFrame=False + ) + right_handle[0] += compute_offset( + right_handle[0], pivot, factor_for_rounded_offset, roundToNearestFrame=False + ) + + # since the handle is on the pivot it is not affected by the scaling... but at least + # one of its handles is (if the pivot is one of the boundaries of the range), if not + # both, if the pivot is inbetween + else: + if start_incl < left_handle[0]: + left_handle[0] += compute_offset(left_handle[0], pivot, factor, roundToNearestFrame=False) + if right_handle[0] < end_incl: + right_handle[0] += compute_offset(right_handle[0], pivot, factor, roundToNearestFrame=False) + + elif "OFFSET" == changeMode: + if key_time < start_incl: + offset = compute_offset(start_incl, pivot, factor, roundToNearestFrame=False) + elif end_incl < key_time: + offset = compute_offset(end_incl, pivot, factor, roundToNearestFrame=False) + + new_key_time = key_time + offset + if roundToNearestFrame: + new_key_time = round(new_key_time) + fcurve.set_key_coordinates(i, (new_key_time, key_value)) + + offset_with_rounded_added_offset = new_key_time - key_time + left_handle, right_handle = fcurve.handles(i) + left_handle[0] += offset_with_rounded_added_offset + right_handle[0] += offset_with_rounded_added_offset + + # if factor < 1.0: + # _offset_fCurve_frames( + # fcurve, + # end_incl + 1, + # compute_offset(end_incl + 1, pivot, factor) - (end_incl - start_incl + 1), + # roundToNearestFrame, + # ) -def _offset_frames(fcurve: FCurve, start_incl, offset): +def _offset_fCurve_frames(fcurve: FCurve, start_incl, offset, roundToNearestFrame): for i in range(len(fcurve)): key_time, value = fcurve.get_key_coordinates(i) if start_incl <= key_time: - fcurve.set_key_coordinates(i, (key_time + offset, value)) + # key_time = key_time + offset + # if roundToNearestFrame: + # key_time = round(key_time) + # fcurve.set_key_coordinates(i, (key_time, value)) + # left_handle, right_handle = fcurve.handles(i) + # left_handle[0] += offset + # right_handle[0] += offset + + new_key_time = key_time + offset + if roundToNearestFrame: + new_key_time = round(new_key_time) + + offset_with_rounded_added_offset = new_key_time - key_time left_handle, right_handle = fcurve.handles(i) - left_handle[0] += offset - right_handle[0] += offset + left_handle[0] += offset_with_rounded_added_offset + right_handle[0] += offset_with_rounded_added_offset + fcurve.set_key_coordinates(i, (new_key_time, value)) + + +def retime_fCurve_frames( + fcurve: FCurve, + mode, + start_incl=0, + end_incl=0, + remove_gap=True, + factor=1.0, + pivot=0, + roundToNearestFrame=True, + keysBeforeRangeMode="DO_NOTHING", + keysAfterRangeMode="DO_NOTHING", +): + """ + Args: + keysBeforeRangeMode: Action to do on keys located before the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" + keysAfterRangeMode: Action to do on keys located after the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" + """ + + if mode == "INSERT": + _offset_fCurve_frames(fcurve, start_incl, end_incl - start_incl + 1, roundToNearestFrame) + + elif mode == "DELETE" or mode == "CLEAR_ANIM": + fcurve.remove_frames(start_incl, end_incl, remove_gap) + elif mode == "RESCALE": + _rescale_fCurve_frames( + fcurve=fcurve, + start_incl=start_incl, + end_incl=end_incl, + factor=factor, + pivot=pivot, + clamp=False, + roundToNearestFrame=roundToNearestFrame, + keysBeforeRangeMode=keysBeforeRangeMode, + keysAfterRangeMode=keysAfterRangeMode, + ) + elif mode == "FREEZE": + for i in range(len(fcurve)): + key_time, value = fcurve.get_key_coordinates(i) + new_keys = list() + if key_time == start_incl: + new_keys.append((key_time, value)) + new_keys.append((key_time + end_incl - start_incl, value)) + + if key_time >= start_incl: + fcurve.set_key_coordinates(i, (key_time + end_incl - start_incl, value)) + + left_handle, right_handle = fcurve.get_handles(i) + left_handle[0] += end_incl - start_incl + right_handle[0] += end_incl - start_incl + fcurve.set_handles(i, (left_handle, right_handle)) + + for v in new_keys: + fcurve.insert_frame(v, roundToNearestFrame) + + +########################################################################## +# grease pencil +########################################################################## def _offset_GPframes(layer, start_incl, offset): """Move the layer frames that are AT THE SAME TIME or later than the reference frame""" # print(f"layer:{layer.info}") @@ -308,7 +473,9 @@ def _offset_GPframes(layer, start_incl, offset): f.frame_number += offset -def retime_GPframes(layer, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): +def retime_GPframes( + layer, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0, roundToNearestFrame=True +): """Retime "frames" (= each drawing of a Grease Pencil object)""" offset = end_incl - start_incl + 1 @@ -359,37 +526,9 @@ def retime_GPframes(layer, mode, start_incl=0, end_incl=0, remove_gap=True, fact return () -def retime_frames(fcurve: FCurve, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): - - if mode == "INSERT": - _offset_frames(fcurve, start_incl, end_incl - start_incl + 1) - - elif mode == "DELETE" or mode == "CLEAR_ANIM": - fcurve.remove_frames(start_incl, end_incl, remove_gap) - - elif mode == "RESCALE": - _stretch_frames(fcurve, start_incl, end_incl, factor, pivot, False) - - elif mode == "FREEZE": - for i in range(len(fcurve)): - key_time, value = fcurve.get_key_coordinates(i) - new_keys = list() - if key_time == start_incl: - new_keys.append((key_time, value)) - new_keys.append((key_time + end_incl - start_incl, value)) - - if key_time >= start_incl: - fcurve.set_key_coordinates(i, (key_time + end_incl - start_incl, value)) - - left_handle, right_handle = fcurve.get_handles(i) - left_handle[0] += end_incl - start_incl - right_handle[0] += end_incl - start_incl - fcurve.set_handles(i, (left_handle, right_handle)) - - for v in new_keys: - fcurve.insert_frame(v) - - +########################################################################## +# shot range +########################################################################## # wkip warining here start_frame is EXCLUSIF - To change!! # end frame is inclusive def retime_shot(shot, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0): @@ -806,12 +945,16 @@ def retimeScene( join_gap=True, factor=1.0, pivot=0, + keysBeforeRangeMode="DO_NOTHING", + keysAfterRangeMode="DO_NOTHING", ): """Apply the time change for each type of entities - Args: + retimeMode: Can be GLOBAL_OFFSET, INSERT_BEFORE, INSERT_AFTER, DELETE_RANGE, RESCALE, CLEAR_ANIM", start_incl (int): The included start frame duration_incl (int): The range of retime frames (new or deleted) + keysBeforeRangeMode: Action to do on keys located before the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" + keysAfterRangeMode: Action to do on keys located after the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" """ # prefs = config.getShotManagerPrefs() scene = context.scene @@ -833,42 +976,63 @@ def retimeScene( ) # print("Retiming scene: , factor: ", mode, factor) + roundToNearestFrame = retimerApplyToSettings.snapKeysToFrames retime_args = (mode, start_incl, end_incl, join_gap, factor, pivot) # print("retime_args: ", retime_args) - actions_done = set() # Actions can be linked so we must make sure to only retime them once + # Actions can be linked so we must make sure to only retime them once + actions_done = set() action_tmp = bpy.data.actions.new("Retimer_TmpAction") + def _retimeFcurve(action): + if action is not None and action not in actions_done: + # wkip can we have animated properties that are not actions? + for fcurve in action.fcurves: + if not fcurve.lock or retimerApplyToSettings.includeLockAnim: + retime_fCurve_frames( + FCurve(fcurve), *retime_args, roundToNearestFrame, keysBeforeRangeMode, keysAfterRangeMode + ) + actions_done.add(action) + _logger.debug_ext(f"_retimerFcurve: actions_done len: {len(actions_done)}") + + sceneMaterials = [] for obj in objects: # print(f"Retiming object named: {obj.name}") - # Standard object keyframes + # standard object keyframes if retimerApplyToSettings.applyToObjects: if obj.type != "GPENCIL": if obj.animation_data is not None: - action = obj.animation_data.action - if action is not None and action not in actions_done: - # wkip can we have animated properties that are not actions? - for fcurve in action.fcurves: - if not fcurve.lock or retimerApplyToSettings.includeLockAnim: - retime_frames(FCurve(fcurve), *retime_args) - actions_done.add(action) - - # Shape keys - if retimerApplyToSettings.applyToShapeKeys: - if ( - obj.type == "MESH" - and obj.data.shape_keys is not None - and obj.data.shape_keys.animation_data is not None - ): - action = obj.data.shape_keys.animation_data.action - if action is not None and action not in actions_done: - for fcurve in action.fcurves: - if not fcurve.lock or retimerApplyToSettings.includeLockAnim: - retime_frames(FCurve(fcurve), *retime_args) - actions_done.add(action) - - # Grease pencil + _retimeFcurve(obj.animation_data.action) + + # data animation + if obj.data is not None and obj.data.animation_data is not None: + _retimeFcurve(obj.data.animation_data.action) + + # shape keys + if retimerApplyToSettings.applyToShapeKeys: + if ( + obj.type == "MESH" + and obj.data.shape_keys is not None + and obj.data.shape_keys.animation_data is not None + ): + _retimeFcurve(obj.data.shape_keys.animation_data.action) + + # animated materials + for matSlot in obj.material_slots: + if matSlot is not None: + mat = matSlot.material + if mat not in sceneMaterials: + sceneMaterials.append(mat) + if mat.animation_data is not None and mat.animation_data.action is not None: + _retimeFcurve(mat.animation_data.action) + if ( + mat.node_tree.animation_data is not None + and mat.node_tree.animation_data.action is not None + ): + _retimeFcurve(mat.node_tree.animation_data.action) + + # grease pencil if retimerApplyToSettings.applytToGreasePencil: # retime_args = (mode, start_incl, end_incl, join_gap, factor, pivot) @@ -883,7 +1047,6 @@ def retimeScene( # the stroke frames are not updated. As a turnaround we force an action and # remove it afterward if obj.animation_data.action is None: - obj.animation_data.action = action_tmp action_tmp_added = True @@ -891,19 +1054,19 @@ def retimeScene( if action is not None and action not in actions_done: for fcurve in action.fcurves: if not fcurve.lock or retimerApplyToSettings.includeLockAnim: - retime_frames(FCurve(fcurve), *retime_args) + retime_fCurve_frames(FCurve(fcurve), *retime_args, roundToNearestFrame) if not action_tmp_added: actions_done.add(action) for layer in obj.data.layers: # print(f"Treating GP object: {obj.name} layer: {layer}") if not layer.lock or retimerApplyToSettings.includeLockAnim: - retime_GPframes(layer, *retime_args) + retime_GPframes(layer, *retime_args, roundToNearestFrame) if action_tmp_added: obj.animation_data.action = None - # Force an update on the actions (cause bug. Other approach would be to save the file and reload it) + # force an update on the actions (cause bug. Other approach would be to save the file and reload it) if obj.animation_data is not None: if obj.animation_data.action is not None: action_backup = obj.animation_data.action @@ -916,8 +1079,8 @@ def retimeScene( if retimerApplyToSettings.applyToVSE: retime_vse(scene, mode, start_incl, end_incl) - # Shots - if retimerApplyToSettings.applyToCameraShots: + # Shot ranges + if retimerApplyToSettings.applyToCameraShotRanges: props = scene.UAS_shot_manager_props shotList = props.getShotsList(ignoreDisabled=False) @@ -927,7 +1090,7 @@ def retimeScene( # markers if retimerApplyToSettings.applyToMarkers: - retime_markers(scene, *retime_args) + retime_markers(scene, *retime_args, roundToNearestFrame) # anim range # NOTE: end_incl = start_incl + duration_incl - 1 <=> duration_incl = end_incl - start_incl + 1 diff --git a/shotmanager/retimer/retimer_applyto_settings.py b/shotmanager/retimer/retimer_applyto_settings.py index a17e6697..f9a85c87 100644 --- a/shotmanager/retimer/retimer_applyto_settings.py +++ b/shotmanager/retimer/retimer_applyto_settings.py @@ -45,6 +45,12 @@ class UAS_Retimer_ApplyToSettings(PropertyGroup): default=True, options=set(), ) + snapKeysToFrames: BoolProperty( + name="Snap Keys to Frames", + description="Snap the retime keys to the nearest frame value", + default=True, + options=set(), + ) applyToObjects: BoolProperty( name="Objects", @@ -72,15 +78,15 @@ class UAS_Retimer_ApplyToSettings(PropertyGroup): options=set(), ) - applyToCameraShots: BoolProperty( - name="Shots", - description="Apply time change to the range of the shots of type Camera (*** NOT to Storyboard shots ! ***", + applyToCameraShotRanges: BoolProperty( + name="Camera Shot Ranges", + description="Apply time change to the range (but not the shot content) of the shots of type Camera (*** NOT to Storyboard shots ! ***", default=True, options=set(), ) - applyToStoryboardShots: BoolProperty( - name="Shots", - description="Apply time change to the range of the shots of type Storyboard (*** NOT to Camera shots ! ***", + applyToStoryboardShotRanges: BoolProperty( + name="Storyboard Shot Ranges", + description="Apply time change to the range (but not the shot content) of the shots of type Storyboard (*** NOT to Camera shots ! ***", default=False, options=set(), ) @@ -110,7 +116,7 @@ def initialize(self, applyToMode): """ Args: applyToMode: the mode of the applyTo settings. Can be SCENE, SELECTED_OBJECTS, LEGACY - Internaly if can also be: STORYBOARD_CLIP + Internaly if can also be: STB_SHOT_CLIP """ _logger.debug_ext(f"initialize Retimer ApplyTo Settings {applyToMode}", col="GREEN", tag="RETIMER") @@ -124,6 +130,7 @@ def initialize(self, applyToMode): self.onlyOnSelection = False self.includeLockAnim = True + self.snapKeysToFrames = True self.applyToObjects = True self.applyToShapeKeys = True @@ -131,8 +138,8 @@ def initialize(self, applyToMode): self.applyToMarkers = True - self.applyToCameraShots = True - self.applyToStoryboardShots = False + self.applyToCameraShotRanges = True + self.applyToStoryboardShotRanges = False self.applyToVSE = True @@ -146,6 +153,7 @@ def initialize(self, applyToMode): self.onlyOnSelection = False self.includeLockAnim = True + self.snapKeysToFrames = True # NOTE: a camera from a shot is an object belonging to the scene self.applyToObjects = True @@ -154,22 +162,47 @@ def initialize(self, applyToMode): self.applyToMarkers = False - self.applyToCameraShots = False - self.applyToStoryboardShots = False + self.applyToCameraShotRanges = False + self.applyToStoryboardShotRanges = False + + self.applyToVSE = False + + self.applyToTimeCursor = False + self.applyToSceneRange = False + + # Storyboard shot clip - for shot stack clip manipulations + ######################## + if "STB_SHOT_CLIP" == applyToMode: + self.id = applyToMode + self.name = "Apply to Storyboard Shot Clips" + + self.onlyOnSelection = False + self.includeLockAnim = True + self.snapKeysToFrames = True + + self.applyToObjects = True + self.applyToShapeKeys = True + self.applytToGreasePencil = True + + self.applyToMarkers = False + + self.applyToCameraShotRanges = False + self.applyToStoryboardShotRanges = False self.applyToVSE = False self.applyToTimeCursor = False self.applyToSceneRange = False - # Storyboard clip - fos shot stack clip manipulations + # Camera (or previz) shot clip - for shot stack clip manipulations ######################## - if "STORYBOARD_CLIP" == applyToMode: + if "PVZ_SHOT_CLIP" == applyToMode: self.id = applyToMode - self.name = "Apply to Storyboard Clips" + self.name = "Apply to Camera Shot Clips" self.onlyOnSelection = False self.includeLockAnim = True + self.snapKeysToFrames = True self.applyToObjects = True self.applyToShapeKeys = True @@ -177,8 +210,8 @@ def initialize(self, applyToMode): self.applyToMarkers = False - self.applyToCameraShots = False - self.applyToStoryboardShots = False + self.applyToCameraShotRanges = False + self.applyToStoryboardShotRanges = False self.applyToVSE = False diff --git a/shotmanager/retimer/retimer_applyto_ui.py b/shotmanager/retimer/retimer_applyto_ui.py index dda34eb3..f5520859 100644 --- a/shotmanager/retimer/retimer_applyto_ui.py +++ b/shotmanager/retimer/retimer_applyto_ui.py @@ -26,7 +26,7 @@ def drawApplyTo(context, retimerProps, layout): - prefs = config.getShotManagerPrefs() + # prefs = config.getShotManagerPrefs() retimerApplyToSettings = retimerProps.getCurrentApplyToSettings() propCol = propertyColumn(layout, padding_left=2, align=False) @@ -62,18 +62,18 @@ def drawApplyTo(context, retimerProps, layout): stbRow = entitiesCol.row() stbRow.prop( retimerApplyToSettings, - "applyToStoryboardShots", + "applyToStoryboardShotRanges", text="Storyboard Shots", ) # text="Storyboard Shots: Their temporality is not related to the scene", # doesnt work, need an enum # stbRow.prop_with_popover( - # retimerApplyToSettings, "applyToStoryboardShots", panel="UAS_PT_SM_quicktooltip", text="tototo", icon="INFO" + # retimerApplyToSettings, "applyToStoryboardShotRanges", panel="UAS_PT_SM_quicktooltip", text="tototo", icon="INFO" # ) stbRowRight = stbRow.row() - stbRowRight.alert = retimerApplyToSettings.applyToStoryboardShots + stbRowRight.alert = retimerApplyToSettings.applyToStoryboardShotRanges quickHelpInfo = retimerProps.getQuickHelp("APPLYTO_STORYBOARDSHOTS") # doc_op = stbRowRight.operator("shotmanager.open_documentation_url", text="", icon="INFO", emboss=False) @@ -83,9 +83,12 @@ def drawApplyTo(context, retimerProps, layout): # tooltipStr += f"\n\nOpen Shot Manager Retimer online documentation:\n {doc_op.path}" # doc_op.tooltip = tooltipStr - # quickTooltip(stbRowRight, "patate", title="Storyboard Shots", alert=retimerApplyToSettings.applyToStoryboardShots) + # quickTooltip(stbRowRight, "patate", title="Storyboard Shots", alert=retimerApplyToSettings.applyToStoryboardShotRanges) quickTooltip( - stbRowRight, quickHelpInfo[2], title=quickHelpInfo[1], alert=retimerApplyToSettings.applyToStoryboardShots + stbRowRight, + quickHelpInfo[2], + title=quickHelpInfo[1], + alert=retimerApplyToSettings.applyToStoryboardShotRanges, ) entitiesCol.separator(factor=0.5) @@ -100,6 +103,10 @@ def drawApplyTo(context, retimerProps, layout): row = split.row(align=True) row.prop(retimerApplyToSettings, "includeLockAnim", text="Include Locked Anim") + row = propCol.row(align=True) + row.separator(factor=0.7) + row.prop(retimerApplyToSettings, "snapKeysToFrames", text="Snap Keys to Frames") + box = propCol.box() col = box.column() @@ -127,6 +134,10 @@ def drawApplyTo(context, retimerProps, layout): row = split.row(align=True) row.prop(retimerApplyToSettings, "includeLockAnim", text="Include Locked Anim") + row = propCol.row(align=True) + row.separator(factor=0.7) + row.prop(retimerApplyToSettings, "snapKeysToFrames", text="Snap Keys to Frames") + propRow = propCol.row() propRow.alert = config.devDebug and "LEGACY" != retimerApplyToSettings.id @@ -146,7 +157,7 @@ def drawApplyTo(context, retimerProps, layout): row.scale_y = 0.3 row = col.row(align=True) - row.prop(retimerApplyToSettings, "applyToCameraShots") + row.prop(retimerApplyToSettings, "applyToCameraShotRanges") row.prop(retimerApplyToSettings, "applyToVSE") row.label(text="") diff --git a/shotmanager/retimer/retimer_operators.py b/shotmanager/retimer/retimer_operators.py index d618aa2a..156084b4 100644 --- a/shotmanager/retimer/retimer_operators.py +++ b/shotmanager/retimer/retimer_operators.py @@ -108,7 +108,7 @@ def execute(self, context): else: sceneObjs = [obj for obj in context.scene.objects] - if not retimerApplyToSettings.applyToStoryboardShots: + if not retimerApplyToSettings.applyToStoryboardShotRanges: stbObjs = getStoryboardObjects(context.scene) for obj in stbObjs: if obj in sceneObjs: @@ -201,6 +201,8 @@ def execute(self, context): join_gap=True, factor=retimeEngine.factor, pivot=start_excl, + keysBeforeRangeMode="DO_NOTHING", + keysAfterRangeMode="OFFSET", ) elif "CLEAR_ANIM" == retimeEngine.mode: From e4bf7f11863e9d7df5f42b7d2932a3a1c304645e Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Fri, 23 Sep 2022 23:55:17 +0200 Subject: [PATCH 19/27] wip scaling on stb clips and cameras --- shotmanager/debug/sm_debug_ui.py | 14 +- .../widgets/shots_stack_handle_component.py | 12 +- shotmanager/retimer/retimer.py | 186 ++++++++++++------ 3 files changed, 144 insertions(+), 68 deletions(-) diff --git a/shotmanager/debug/sm_debug_ui.py b/shotmanager/debug/sm_debug_ui.py index 74215338..8a1e8200 100644 --- a/shotmanager/debug/sm_debug_ui.py +++ b/shotmanager/debug/sm_debug_ui.py @@ -184,6 +184,7 @@ def _getSelectedKeysOfSelObj(): if not obj: return keys + # C.object.data.layers[8].frames[0].frame_number if "GPENCIL" == obj.type: gpencil = obj for gpLayer in gpencil.data.layers: @@ -218,12 +219,13 @@ def _getSelectedKeysOfSelObj(): if len(keys): for i, k in enumerate(keys): if i < 3: - # try: - layout.label( - text=f" key at fr. {k.co[0]:0.2f}, val: {k.co[1]:0.2f}, left h: {k.handle_left}, right h: {k.handle_right}" - ) - # except Exception: - # layout.label(text=f" GP key at fr. {k}") + try: + layout.label( + text=f" key at fr. {k.co[0]:0.2f}, val: {k.co[1]:0.2f}, left h: {k.handle_left}, right h: {k.handle_right}" + ) + except Exception: + # GP frames + layout.label(text=f" GP key at fr. {k}") else: layout.label(text=f"... and {len(keys) - 3} other keys") diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py index 3086403a..1853d02b 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py @@ -195,9 +195,15 @@ def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): if self.isStart: self.shot.start += mouse_delta_frames pivot = self.shot.end + start_incl = prevShotStart + end_incl = self.shot.end + duration_incl = end_incl - start_incl + 1 else: self.shot.end += mouse_delta_frames pivot = self.shot.start + start_incl = self.shot.start + end_incl = prevShotEnd + duration_incl = end_incl - start_incl + 1 # bpy.ops.uas_shot_manager.set_shot_start(newStart=self.start + mouse_delta_frames) @@ -224,8 +230,10 @@ def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): retimeMode="RESCALE", retimerApplyToSettings=retimerApplyToSettings, objects=self.manipulatedChildren, - start_incl=-10000, - duration_incl=900000, + # start_incl=-10000, + # duration_incl=900000, + start_incl=start_incl, + duration_incl=duration_incl, join_gap=True, factor=retimeFactor, pivot=pivot, diff --git a/shotmanager/retimer/retimer.py b/shotmanager/retimer/retimer.py index f15d52cc..8eb1242f 100644 --- a/shotmanager/retimer/retimer.py +++ b/shotmanager/retimer/retimer.py @@ -264,6 +264,29 @@ def rescale_frame(frame_value, start_incl, end_incl, pivot, factor, roundToNeare ########################################################################## +def _offset_fCurve_frames(fcurve: FCurve, start_incl, offset, roundToNearestFrame): + for i in range(len(fcurve)): + key_time, value = fcurve.get_key_coordinates(i) + if start_incl <= key_time: + # key_time = key_time + offset + # if roundToNearestFrame: + # key_time = round(key_time) + # fcurve.set_key_coordinates(i, (key_time, value)) + # left_handle, right_handle = fcurve.handles(i) + # left_handle[0] += offset + # right_handle[0] += offset + + new_key_time = key_time + offset + if roundToNearestFrame: + new_key_time = round(new_key_time) + + offset_with_rounded_added_offset = new_key_time - key_time + left_handle, right_handle = fcurve.handles(i) + left_handle[0] += offset_with_rounded_added_offset + right_handle[0] += offset_with_rounded_added_offset + fcurve.set_key_coordinates(i, (new_key_time, value)) + + def _rescale_fCurve_frames( *, fcurve: FCurve, @@ -287,10 +310,10 @@ def _rescale_fCurve_frames( remove_post_end = list() for i in range(len(fcurve)): coordinates = fcurve.get_key_coordinates(i) - dist_from_pivot = coordinates[0] - pivot_value - if start_incl >= round(pivot_value + dist_from_pivot * factor): + dist_from_pivot = coordinates[0] - pivot + if start_incl >= round(pivot + dist_from_pivot * factor): remove_pre_start.append(coordinates[0]) - elif round(pivot_value + dist_from_pivot * factor) >= end_incl: + elif round(pivot + dist_from_pivot * factor) >= end_incl: remove_post_end.append(coordinates[0]) if remove_pre_start: @@ -321,8 +344,6 @@ def _rescale_fCurve_frames( elif end_incl < key_time: changeMode = keysAfterRangeMode - # if start_incl <= key_time <= end_incl: - if "RESCALE" == changeMode: offset = compute_offset(key_time, pivot, factor, roundToNearestFrame=False) # - start_incl + 1 new_key_time = key_time + offset @@ -382,29 +403,6 @@ def _rescale_fCurve_frames( # ) -def _offset_fCurve_frames(fcurve: FCurve, start_incl, offset, roundToNearestFrame): - for i in range(len(fcurve)): - key_time, value = fcurve.get_key_coordinates(i) - if start_incl <= key_time: - # key_time = key_time + offset - # if roundToNearestFrame: - # key_time = round(key_time) - # fcurve.set_key_coordinates(i, (key_time, value)) - # left_handle, right_handle = fcurve.handles(i) - # left_handle[0] += offset - # right_handle[0] += offset - - new_key_time = key_time + offset - if roundToNearestFrame: - new_key_time = round(new_key_time) - - offset_with_rounded_added_offset = new_key_time - key_time - left_handle, right_handle = fcurve.handles(i) - left_handle[0] += offset_with_rounded_added_offset - right_handle[0] += offset_with_rounded_added_offset - fcurve.set_key_coordinates(i, (new_key_time, value)) - - def retime_fCurve_frames( fcurve: FCurve, mode, @@ -473,8 +471,61 @@ def _offset_GPframes(layer, start_incl, offset): f.frame_number += offset +def _rescale_GPframes( + *, + layer, + start_incl, + end_incl, + factor, + pivot, + roundToNearestFrame=True, + keysBeforeRangeMode="DO_NOTHING", + keysAfterRangeMode="DO_NOTHING", +): + + for f in layer.frames: + # NOTE: whereas key_time in fcurve is a float, here frameNumber is an int !!! + frame_number_float = f.frame_number + changeMode = "RESCALE" + if frame_number_float < start_incl: + changeMode = keysBeforeRangeMode + elif end_incl < frame_number_float: + changeMode = keysAfterRangeMode + + if "RESCALE" == changeMode: + offset = compute_offset(frame_number_float, pivot, factor, roundToNearestFrame=False) # - start_incl + 1 + new_frame_number_float = frame_number_float + offset + + # rounding has to be done anyway since GP frames are integer + # if roundToNearestFrame: + new_frame_number_float = round(new_frame_number_float) + f.frame_number = new_frame_number_float + + elif "OFFSET" == changeMode: + if frame_number_float < start_incl: + offset = compute_offset(start_incl, pivot, factor, roundToNearestFrame=False) + elif end_incl < frame_number_float: + offset = compute_offset(end_incl, pivot, factor, roundToNearestFrame=False) + + new_frame_number_float = frame_number_float + offset + + # rounding has to be done anyway since GP frames are integer + # if roundToNearestFrame: + new_frame_number_float = round(new_frame_number_float) + f.frame_number = new_frame_number_float + + def retime_GPframes( - layer, mode, start_incl=0, end_incl=0, remove_gap=True, factor=1.0, pivot=0, roundToNearestFrame=True + layer, + mode, + start_incl=0, + end_incl=0, + remove_gap=True, + factor=1.0, + pivot=0, + roundToNearestFrame=True, + keysBeforeRangeMode="DO_NOTHING", + keysAfterRangeMode="DO_NOTHING", ): """Retime "frames" (= each drawing of a Grease Pencil object)""" offset = end_incl - start_incl + 1 @@ -504,24 +555,35 @@ def retime_GPframes( # f.frame_number -= end_incl - start_incl elif mode == "RESCALE": - # push out of range frames later in time - if factor > 1.0: - # wkip sur du +1 on end frame???? - _offset_GPframes( - layer, end_incl + 1, compute_offset(end_incl + 1, pivot, factor) - (end_incl - start_incl + 1) - ) - - # scale range - for f in layer.frames: - if start_incl <= f.frame_number <= end_incl: - offset = compute_offset(f.frame_number, pivot, factor) - f.frame_number = offset + pivot + _rescale_GPframes( + layer=layer, + start_incl=start_incl, + end_incl=end_incl, + factor=factor, + pivot=pivot, + roundToNearestFrame=roundToNearestFrame, + keysBeforeRangeMode=keysBeforeRangeMode, + keysAfterRangeMode=keysAfterRangeMode, + ) - # pull out of range frames sooner in time - if factor < 1.0: - _offset_GPframes( - layer, end_incl + 1, compute_offset(end_incl + 1, pivot, factor) - (end_incl - start_incl + 1) - ) + # # push out of range frames later in time + # if factor > 1.0: + # # wkip sur du +1 on end frame???? + # _offset_GPframes( + # layer, end_incl + 1, compute_offset(end_incl + 1, pivot, factor) - (end_incl - start_incl + 1) + # ) + + # # scale range + # for f in layer.frames: + # if start_incl <= f.frame_number <= end_incl: + # offset = compute_offset(f.frame_number, pivot, factor) + # f.frame_number = offset + pivot + + # # pull out of range frames sooner in time + # if factor < 1.0: + # _offset_GPframes( + # layer, end_incl + 1, compute_offset(end_incl + 1, pivot, factor) - (end_incl - start_incl + 1) + # ) return () @@ -971,9 +1033,8 @@ def retimeScene( end_incl = start_incl + duration_incl - 1 - print( - f" - retimeScene(): {retimeMode}, start_incl: {start_incl}, end_incl: {end_incl}, duration_incl: {duration_incl}" - ) + # duration_incl: {duration_incl} + print(f" - retimeScene(): {retimeMode}, str_inc:{start_incl}, ed_inc:{end_incl}, pivot:{pivot}, factor:{factor}") # print("Retiming scene: , factor: ", mode, factor) roundToNearestFrame = retimerApplyToSettings.snapKeysToFrames @@ -984,7 +1045,7 @@ def retimeScene( actions_done = set() action_tmp = bpy.data.actions.new("Retimer_TmpAction") - def _retimeFcurve(action): + def _retimeFcurve(action, addActionToDone=True): if action is not None and action not in actions_done: # wkip can we have animated properties that are not actions? for fcurve in action.fcurves: @@ -992,8 +1053,10 @@ def _retimeFcurve(action): retime_fCurve_frames( FCurve(fcurve), *retime_args, roundToNearestFrame, keysBeforeRangeMode, keysAfterRangeMode ) - actions_done.add(action) - _logger.debug_ext(f"_retimerFcurve: actions_done len: {len(actions_done)}") + # NOTE: wkip keep the test?? + if addActionToDone: + actions_done.add(action) + # _logger.debug_ext(f"_retimerFcurve: actions_done len: {len(actions_done)}") sceneMaterials = [] for obj in objects: @@ -1050,18 +1113,21 @@ def _retimeFcurve(action): obj.animation_data.action = action_tmp action_tmp_added = True - action = obj.animation_data.action - if action is not None and action not in actions_done: - for fcurve in action.fcurves: - if not fcurve.lock or retimerApplyToSettings.includeLockAnim: - retime_fCurve_frames(FCurve(fcurve), *retime_args, roundToNearestFrame) - if not action_tmp_added: - actions_done.add(action) + _retimeFcurve(obj.animation_data.action, addActionToDone=not action_tmp_added) + # action = obj.animation_data.action + # if action is not None and action not in actions_done: + # for fcurve in action.fcurves: + # if not fcurve.lock or retimerApplyToSettings.includeLockAnim: + # retime_fCurve_frames(FCurve(fcurve), *retime_args, roundToNearestFrame) + # if not action_tmp_added: + # actions_done.add(action) for layer in obj.data.layers: # print(f"Treating GP object: {obj.name} layer: {layer}") if not layer.lock or retimerApplyToSettings.includeLockAnim: - retime_GPframes(layer, *retime_args, roundToNearestFrame) + retime_GPframes( + layer, *retime_args, roundToNearestFrame, keysBeforeRangeMode, keysAfterRangeMode + ) if action_tmp_added: obj.animation_data.action = None From 3af7a84246ba0062614f9dfbb591ccbbcad1a777 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Sat, 24 Sep 2022 00:16:47 +0200 Subject: [PATCH 20/27] wip undo on the manips of shot clips --- shotmanager/config/dev_notes.txt | 5 +++++ .../overlay_tools/interact_shots_stack/shots_stack.py | 1 + .../widgets/shots_stack_clip_component.py | 2 ++ .../widgets/shots_stack_handle_component.py | 3 +++ 4 files changed, 11 insertions(+) diff --git a/shotmanager/config/dev_notes.txt b/shotmanager/config/dev_notes.txt index 5e8b61b2..13a0d6e3 100644 --- a/shotmanager/config/dev_notes.txt +++ b/shotmanager/config/dev_notes.txt @@ -198,3 +198,8 @@ To do dans le support des noms de seq: - faire un test alert qd le nom de la seq est vide - otio import +Avoir un rename de shots batch + +mettre le Snap to Frame +Avoir un warning / clean pour quand les GP frames s'overlappent + diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py index f670dfb6..b51462f8 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack.py @@ -75,6 +75,7 @@ def display_state_changed_intShStack(context): class UAS_ShotManager_InteractiveShotsStack(Operator): bl_idname = "uas_shot_manager.interactive_shots_stack" bl_label = "Draw Interactive Shots Stack in timeline" + # bl_options = {"INTERNAL"} bl_options = {"REGISTER", "INTERNAL"} # bl_options = {"UNDO"} # !!! Important note: Do not set undo here: it doesn't work and it will be in conflic with the diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py index cfc39880..bf356034 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py @@ -395,9 +395,11 @@ def _on_manipulated_changed(self, context, event, isManipulated): self.shotsStackWidget.manipulatedComponent = self if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() + bpy.ops.ed.undo_push() else: if self.shot.isCameraValid(): self.manipulatedChildren = [self.shot.camera] + bpy.ops.ed.undo_push() else: _logger.debug_ext("component2D handle_events set manipulated False", col="PURPLE", tag="EVENT") self.shotsStackWidget.manipulatedComponent = None diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py index 1853d02b..d5e02a49 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py @@ -20,6 +20,7 @@ """ import gpu +import bpy from shotmanager.gpu.gpu_2d.class_Component2D import Component2D @@ -177,9 +178,11 @@ def _on_manipulated_changed(self, context, event, isManipulated): if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() + bpy.ops.ed.undo_push() else: if self.shot.isCameraValid(): self.manipulatedChildren = [self.shot.camera] + bpy.ops.ed.undo_push() else: self.shotsStackWidget.manipulatedComponent = None self.parent.isManipulatedByAnotherComponent = False From d58da8fa7be398579ddaf2499456b11cd79fe159 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Fri, 30 Sep 2022 00:26:08 +0200 Subject: [PATCH 21/27] fixes --- CHANGELOG.md | 19 +- doc/devnotes_addon.md | 30 +++ resources/api_code_samples/api_first_steps.py | 4 +- shotmanager/__init__.py | 11 +- shotmanager/addon_prefs/addon_prefs.py | 132 +++++++++++-- shotmanager/addon_prefs/addon_prefs_ui.py | 173 +++++++++++++++++- shotmanager/config/dev_notes.txt | 8 + .../frame_grid/storyboard_frame_grid_props.py | 27 ++- shotmanager/operators/shots.py | 112 ++++++++---- shotmanager/operators/takes.py | 14 -- .../widgets/shots_stack_clip_component.py | 11 +- .../widgets/shots_stack_handle_component.py | 44 ++++- shotmanager/prefs/prefs_features.py | 87 ++++++--- shotmanager/prefs/prefs_shots_display.py | 66 +------ shotmanager/properties/layout_settings.py | 44 +++-- shotmanager/properties/props.py | 149 ++++++++------- shotmanager/properties/shot.py | 5 +- shotmanager/rendering/rendering_ui.py | 36 +++- shotmanager/retimer/retimer.py | 107 +++++++++-- shotmanager/ui/sm_shots_ui_common.py | 6 +- shotmanager/utils/utils.py | 4 +- 21 files changed, 795 insertions(+), 294 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e07520e3..f05f5003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ ----- -## 2.1.004 (2022-09-14) +## 2.1.20 (2022-09-29) +- Several fixes related to the continuous draw mode +- Fixed storyboard frame that was not duplicated following to a take duplication +- Added shot name in the Render panel, near Current Shot label +- Refactored the Duplicate Shot dialog box and added a checkbox for color variation + +----- +## 2.1.5 (2022-09-27) +- Fixed a crash on Storyboard Grid because of Python 3.10 code + +----- +## 2.1.4 (2022-09-14) ### Interactive Shots Stack - Added an info component - Added mode and scale keyframe changes when a shot clip is manipulated @@ -8,17 +19,17 @@ - Added a sample widget to the gpu components library ----- -## 2.1.003 (2022-09-12) +## 2.1.3 (2022-09-12) - Fixed regression bug on Convert Camera Binding - Fixed the detached grease pencil transform lock state - Added a function to get the canvas frame when missing ----- -## 2.1.002 (2022-09-12) +## 2.1.2 (2022-09-12) - Fixed the type of shot that was set to Storyboard when a storyboard frame was added ----- -## 2.1.001 (2022-09-12) +## 2.1.1 (2022-09-12) **Beta Release** ### Keymaps - Separated key mappings per category diff --git a/doc/devnotes_addon.md b/doc/devnotes_addon.md index 2cacef26..46dcb303 100644 --- a/doc/devnotes_addon.md +++ b/doc/devnotes_addon.md @@ -48,3 +48,33 @@ supported. ### Naming: - Shot: Association of a period of time in the 3D scene, given by a start and a end, and a point of view, given by the camera held by the shot. + + +## Props, the main properties class +Terminology: + We distinguish the "selected shot", which is the one selected in the shots list, from the "current shot", + which is the shot actualy displayed in the viewport and identified in the shots list with an orange icon + in the first column. + + +## Shot select, set current, set to Draw Mode + +The manipulation behaviors are defined in the Shot Manipulations rollout of the add-on Preferences. +They are all part of the add-on preferences settings, so any change is applied to every scenes. + +Key functions and operators to manage those manipulations are: + +### When a shot becomes selected +- *In props.py:* The update() function of selected_shot_index: IntProperty holds all the actions done when + a shot is selected + +- The fact that a shot that has just been selected should be made as the current one is defined by the + function props.selectedShotShouldBecomeCurrent() + +### When a shot becomes current +- *In shots.py:* The operator uas_shot_manager.set_current_shot holds all the actions done when a shot is + made current +- In props.setCurrentShotByIndex() + +### When a shot has to be set to Draw mode +- To set the draw mode of a shot, when in Continuous mode: uas_shot_manager.greasepencil_select_and_draw diff --git a/resources/api_code_samples/api_first_steps.py b/resources/api_code_samples/api_first_steps.py index a226a3b6..7a7cabfa 100644 --- a/resources/api_code_samples/api_first_steps.py +++ b/resources/api_code_samples/api_first_steps.py @@ -26,7 +26,7 @@ ############################# -# Takes manipulation +# Take manipulations ############################# @@ -65,7 +65,7 @@ ############################# -# Shots manipulation +# Shot manipulations ############################# diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index fa4bf2a9..70000a3d 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 1, 4), + "version": (2, 1, 20), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", @@ -343,6 +343,15 @@ def _update_UAS_shot_manager_identify_dopesheets(self, context): # prefs_properties = config.getShotManagerPrefs() # prefs_properties.stb_frameTemplate.initialize(fromPrefs=True) + if not addon_prefs_inst.isPrefsVersionUpToDate(): + addon_prefs_inst.initialize_shot_manager_prefs() + + # not working... + # try: + # bpy.ops.preferences.addon_show(module="shotmanager") + # except Exception: + # print("Fail to update the Preferences panel...") + print("") diff --git a/shotmanager/addon_prefs/addon_prefs.py b/shotmanager/addon_prefs/addon_prefs.py index 2f009559..2455a34d 100644 --- a/shotmanager/addon_prefs/addon_prefs.py +++ b/shotmanager/addon_prefs/addon_prefs.py @@ -69,6 +69,23 @@ class UAS_ShotManager_AddonPrefs(AddonPreferences): # when defining this in a submodule of a python package bl_idname = "shotmanager" + previousInstalledVersion: IntProperty( + description="Internal setting" + "\nStore (as an integer) the version of the release of the add-on that was installed before this release." + "\nIf there was none then the value is 0" + "\nThis version is updated when self.initialize_shot_manager_prefs() is called, which should occur" + "\nwhen the add-on is installed. So after the installation of a new version the value of previousInstalledVersion" + "\nshould be the same as self.version()[1]", + default=0, + ) + + def isPrefsVersionUpToDate(self): + """Used in the __init__() of the add-on to see if a call to initialize_shot_manager_prefs() is required. + This allows the prefs update to be done only when necessary (= when the add-on is updated), and not + at every register of the add-on. + """ + return self.version()[1] == self.previousInstalledVersion + install_failed: BoolProperty( name="Install failed", default=False, @@ -149,9 +166,16 @@ def initialize_shot_manager_prefs(self): else: self.newAvailableVersion = 0 - # layout default values - self.stb_selected_shot_changes_current_shot = False + # *** layouts initialization *** + #################################### + + # storyboard layout default values + ########################## + self.stb_selected_shot_changes_current_shot = True self.stb_selected_shot_in_shots_stack_changes_current_shot = False + self.stb_current_stb_shot_changes_time_zoom = True + self.stb_current_pvz_shot_changes_time_zoom = True + self.stb_display_storyboard_in_properties = True self.stb_display_notes_in_properties = True self.stb_display_cameraBG_in_properties = False @@ -160,8 +184,13 @@ def initialize_shot_manager_prefs(self): self.stb_display_globaleditintegr_in_properties = False self.stb_display_advanced_infos = False + # previz layout default values + ########################## self.pvz_selected_shot_changes_current_shot = False self.pvz_selected_shot_in_shots_stack_changes_current_shot = False + self.pvz_current_stb_shot_changes_time_zoom = True + self.pvz_current_pvz_shot_changes_time_zoom = False + self.pvz_display_storyboard_in_properties = False self.pvz_display_notes_in_properties = False self.pvz_display_cameraBG_in_properties = False @@ -172,6 +201,7 @@ def initialize_shot_manager_prefs(self): # self.display_25D_greasepencil_panel = True + self.previousInstalledVersion = self.version()[1] self.isInitialized = True ######################################################################## @@ -425,6 +455,10 @@ def _set_stb_global_visibility(self, value): name="Expand UI Preferences", default=False, ) + addonPrefs_shotManips_expanded: BoolProperty( + name="Expand Shot Manipulations Preferences", + default=False, + ) addonPrefs_features_expanded: BoolProperty( name="Expand Features Preferences", default=False, @@ -460,16 +494,32 @@ def _set_stb_global_visibility(self, value): description="Set the animation range to match the shot range when the current shot is changed.\n(Add-on preference)", default=False, ) - current_shot_changes_time_zoom: BoolProperty( - name="Zoom Timeline to Shot Range", - description="Automatically zoom the timeline content to frame the shot when the current shot is changed.\n(Add-on preference)", - default=False, + + # split to 2 properties, one for the storyboard layout mode and the other for the previz mode + # current_shot_changes_time_zoom: BoolProperty( + # name="Zoom Timeline to Shot Range", + # description="Automatically zoom the timeline content to frame the shot when the current shot is changed.\n(Add-on preference)", + # default=False, + # ) + + # NOTE: these 2 settings are related to the SHOT TYPE, not to the current layout mode + # changed to current_stb_shot_select_stb_frame and current_pvz_shot_select_stb_frame + # current_shot_select_stb_frame: BoolProperty( + # name="Select Storyboard Frame of the Current Short", + # description="Automatically select the storyboard frame (= grease pencil) of the shot when the current shot is changed.\n(Add-on preference)", + # default=True, + # ) + current_stb_shot_select_stb_frame: BoolProperty( + name="Select Storyboard Frame of the Current Storyboard Short", + description="For shots of type Storyboard Shot: Automatically select the Storyboard Frame (= grease pencil) of the shot when the current shot is changed.\n(Add-on preference)", + default=True, ) - current_shot_select_stb_frame: BoolProperty( - name="Select Storyboard Frame of the Current Short", - description="Automatically select the storyboard frame (= grease pencil) of the shot when the current shot is changed.\n(Add-on preference)", + current_pvz_shot_select_stb_frame: BoolProperty( + name="Select Storyboard Frame of the Current Camera Short", + description="For shots of type Camera Shot: Automatically select the Storyboard Frame (= grease pencil) of the shot when the current shot is changed.\n(Add-on preference)", default=True, ) + # current_shot_changes_edited_frame_in_stb: BoolProperty( # name="Set selected shot to edited", # description="When a shot is selected in the shot list, in Storyboard layout mode, and another one is being edited, then" @@ -602,21 +652,43 @@ def _update_layout_but_previz(self, context): ######################################################################## ######################### - # storyboard + # storyboard LAYOUT + # + # NOTE: + # Settings prefixed by "stb_" are contextual to the STORYBOARD LAYOUT mode (not to the camera type!) + # They are all initialized in the initialize_shot_manager_prefs() function. Search for: *** layouts initialization *** ######################### - # NOTE: when the Continuous Editing mode is on then the selected and current shots are tied anyway + # NOTE: behaviors with (*) are automatically applied when the Continuous Editing mode is used + stb_selected_shot_changes_current_shot: BoolProperty( - name="Set selected shot to current", - description="When a shot is selected in the shot list, in Storyboard layout mode, the shot is also set to be the current one", - default=True, + name="Set Shot Selected in the Shots List to Current (*)", + description="When a shot is selected in the Shot List, in Storyboard layout mode, the shot is also set to be the current one", + default=False, ) stb_selected_shot_in_shots_stack_changes_current_shot: BoolProperty( - name="Set selected shot in Shots Stack to current", + name="Set Shot Selected in the Interactive Shots Stack to Current (*)", description="When a shot is selected in the Interactive Shots Stack, in Storyboard layout mode, the shot is also set to be the current one", default=False, ) + stb_current_stb_shot_changes_time_zoom: BoolProperty( + name="Storyboard Shot: Zoom Timeline to Shot Range", + description="When the current layout is Storyboard: automatically zoom the timeline content to frame the shot when the current shot is changed" + "\nand if the shot type is Storyboard", + default=False, + ) + stb_current_pvz_shot_changes_time_zoom: BoolProperty( + name="Camera Shot: Zoom Timeline to Shot Range", + description="When the current layout is Storyboard: automatically zoom the timeline content to frame the shot when the current shot is changed" + "\nand if the shot type is Camera", + default=False, + ) + + #### + # Following settings are also created in the layout class UAS_ShotManager_LayoutSettings + #### + stb_display_storyboard_in_properties: BoolProperty( name="Storyboard Frames and Grease Pencil Tools", description="Display the storyboard frames properties and tools in the Shot properties panel." @@ -663,24 +735,46 @@ def _update_layout_but_previz(self, context): ######################### # previz + # + # NOTE: + # Settings prefixed by "pvz_" are contextual to the PREVIZ LAYOUT mode (not to the camera type!) + # They are all initialized in the initialize_shot_manager_prefs() function. Search for: *** layouts initialization *** ######################### + # NOTE: behaviors with (*) are automatically applied when the Continuous Editing mode is used + pvz_selected_shot_changes_current_shot: BoolProperty( - name="Set selected shot to current", - description="When a shot is selected in the shot list, in Previz layout mode, the shot is also set to be the current one", + name="Set Shot Selected in the Shots List to Current (*)", + description="When a shot is selected in the Shot List, in Previz layout mode, the shot is also set to be the current one", default=False, ) pvz_selected_shot_in_shots_stack_changes_current_shot: BoolProperty( - name="Set selected shot in Shots Stack to current", + name="Set Shot Selected in the Interactive Shots Stack to Current (*)", description="When a shot is selected in the Interactive Shots Stack, in Previz layout mode, the shot is also set to be the current one", default=False, ) + pvz_current_stb_shot_changes_time_zoom: BoolProperty( + name="Storyboard Shot: Zoom Timeline to Shot Range", + description="When the current layout is Previz: automatically zoom the timeline content to frame the shot when the current shot is changed" + "\nand if the shot type is Storyboard", + default=False, + ) + pvz_current_pvz_shot_changes_time_zoom: BoolProperty( + name="Camera Shot: Zoom Timeline to Shot Range", + description="When the current layout is Previz: automatically zoom the timeline content to frame the shot when the current shot is changed" + "\nand if the shot type is Camera", + default=False, + ) + + #### + # Following settings are also created in the layout class UAS_ShotManager_LayoutSettings + #### pvz_display_storyboard_in_properties: BoolProperty( name="Storyboard Frames and Grease Pencil Tools", description="Display the storyboard frames properties and tools in the Shot properties panel." "\nA storyboard frame is a Grease Pencil drawing surface associated to the camera of each shot", - default=True, + default=False, ) pvz_display_notes_in_properties: BoolProperty( diff --git a/shotmanager/addon_prefs/addon_prefs_ui.py b/shotmanager/addon_prefs/addon_prefs_ui.py index 2c5128d2..f1f09b80 100644 --- a/shotmanager/addon_prefs/addon_prefs_ui.py +++ b/shotmanager/addon_prefs/addon_prefs_ui.py @@ -21,6 +21,7 @@ from shotmanager.config import config from shotmanager.ui.dependencies_ui import drawDependencies +from shotmanager.utils.utils import convertVersionIntToStr from shotmanager.utils.utils_ui import collapsable_panel, propertyColumn from shotmanager.prefs.prefs_features import draw_features_prefs @@ -36,6 +37,35 @@ def draw_addon_prefs(self, context): layout = layout.column(align=False) padding_left = 4 + if config.devDebug: + layout.label( + text=f"Previously Installed Version: {self.previousInstalledVersion} - Current version: {self.version()[1]}" + ) + + if self.previousInstalledVersion > self.version()[1]: + warningRow = layout.row() + warningRow.alert = True + restartRow.alignment = "CENTER" + warningRow.label( + text="*** Warning: The add-on version that has been installed is older than the one that was in place ***" + ) + + restartRow = layout.row() + restartRow.alert = True + restartRow.alignment = "CENTER" + restartRow.label(text="*** Please re-start Blender to finish the update of the add-on Preferences ***") + + elif 0 != self.previousInstalledVersion and self.previousInstalledVersion < self.version()[1]: + restartRow = layout.row() + restartRow.alignment = "CENTER" + restartRow.label( + text=f"-- Version {self.version()[0]} has just been installed over version {convertVersionIntToStr(self.previousInstalledVersion)} --" + ) + restartRow = layout.row() + restartRow.alert = True + restartRow.alignment = "CENTER" + restartRow.label(text="*** Please re-start Blender to finish the update of the add-on Preferences ***") + # Dependencies ############### drawDependencies(context, layout) @@ -44,13 +74,17 @@ def draw_addon_prefs(self, context): ############### drawGeneral(context, self, layout) + # Settings + ############### + drawSettings(context, self, layout) + # Features ############### drawFeatures(context, self, layout) - # Settings + # Shot manipulations ############### - drawSettings(context, self, layout) + drawShotManipulations(context, self, layout) # General UI ############### @@ -186,6 +220,141 @@ def drawSettings(context, prefs, layout): rowRight.prop(prefs, "storyboard_new_shot_duration", slider=True, text="Frames") +def drawShotManipulations(context, prefs, layout): + box = layout.box() + collapsable_panel(box, prefs, "addonPrefs_shotManips_expanded", text="Shot Manipulations") + if prefs.addonPrefs_shotManips_expanded: + + mainCol = propertyColumn(box, padding_left=3) + mainCol.label(text="When Current Shot Is Changed:") + + propsCol = propertyColumn(mainCol, padding_left=4) + + propsCol.prop( + prefs, + "current_shot_changes_current_time_to_start", + text="Set Current Frame To Shot Start", + ) + propsCol.prop( + prefs, + "current_shot_changes_time_range", + text="Set Scene Animation Range To Shot Range", + ) + + propsCol.prop( + prefs, + "current_stb_shot_select_stb_frame", + ) + propsCol.prop( + prefs, + "current_pvz_shot_select_stb_frame", + ) + + propsCol.separator(factor=0.8) + + # in storyboard mode + ########################## + stbLayoutCol = propertyColumn(propsCol, padding_left=0) + + stbLayoutRow = stbLayoutCol.row() + stbLayoutRow.label(text="In Storyboard Layout:") + + stbLayoutPropsCol = propertyColumn(stbLayoutCol, padding_left=3) + + # propsCol.prop( + # prefs, + # "current_shot_changes_edited_frame_in_stb", + # text="Storyboard Shots List: Set Selected Shot to Edited One", + # ) + + # storyboard shots ####### + # propsCol.separator(factor=0.5) + # propsCol.label(text="Storyboard Shots:") + + stbLayoutPropsCol.prop( + prefs, + "stb_selected_shot_changes_current_shot", + ) + stbLayoutPropsCol.prop( + prefs, + "stb_selected_shot_in_shots_stack_changes_current_shot", + ) + + stbLayoutPropsCol.prop( + prefs, + "stb_current_stb_shot_changes_time_zoom", + ) + stbLayoutPropsCol.prop( + prefs, + "stb_current_pvz_shot_changes_time_zoom", + ) + + propsCol.separator(factor=0.8) + + # in previz mode + ########################## + pvzLayoutCol = propertyColumn(propsCol, padding_left=0) + + pvzLayoutRow = pvzLayoutCol.row() + pvzLayoutRow.label(text="In Previz Layout:") + + pvzLayoutPropsCol = propertyColumn(pvzLayoutCol, padding_left=3) + + # pvzLayoutPropsCol.prop( + # prefs, + # "current_stb_shot_select_stb_frame", + # ) + # pvzLayoutPropsCol.prop( + # prefs, + # "current_pvz_shot_select_stb_frame", + # ) + + pvzLayoutPropsCol.prop( + prefs, + "pvz_selected_shot_changes_current_shot", + ) + pvzLayoutPropsCol.prop( + prefs, + "pvz_selected_shot_in_shots_stack_changes_current_shot", + ) + + pvzLayoutPropsCol.prop( + prefs, + "pvz_current_stb_shot_changes_time_zoom", + ) + pvzLayoutPropsCol.prop( + prefs, + "pvz_current_pvz_shot_changes_time_zoom", + ) + + # storyboard shots ####### + # propsCol.separator(factor=0.5) + # propsCol.label(text="Storyboard Shots:") + + # stbPropsCol = propertyColumn(propsCol, padding_left=3) + # stbPropsCol.prop( + # prefs, + # "current_stb_shot_select_stb_frame", + # text="Select Storyboard Frame of the Current Storyboard Short", + # ) + + # # previz shots ########### + # propsCol.separator(factor=0.5) + # propsCol.label(text="Camera Shots:") + + # pvzPropsCol = propertyColumn(propsCol, padding_left=3) + # pvzPropsCol.prop( + # prefs, + # "current_pvz_shot_select_stb_frame", + # text="Select Storyboard Frame of the Current Camera Short", + # ) + + propsCol.separator(factor=0.8) + propsCol.label(text="(*) : Automaticaly activated in Continuous Draw Mode") + + mainCol.separator(factor=0.8) + + def drawGeneralUI(context, prefs, layout): box = layout.box() collapsable_panel(box, prefs, "addonPrefs_ui_expanded", text="UI") diff --git a/shotmanager/config/dev_notes.txt b/shotmanager/config/dev_notes.txt index 13a0d6e3..e9960b65 100644 --- a/shotmanager/config/dev_notes.txt +++ b/shotmanager/config/dev_notes.txt @@ -203,3 +203,11 @@ Avoir un rename de shots batch mettre le Snap to Frame Avoir un warning / clean pour quand les GP frames s'overlappent +timer pour la sel des clips +couleur pour les locked duration +selection du clip qd il est trop petit et qu'on tape dans les handles + +Bug: transform truc from paul + +truc information message contextuel + diff --git a/shotmanager/features/storyboard/frame_grid/storyboard_frame_grid_props.py b/shotmanager/features/storyboard/frame_grid/storyboard_frame_grid_props.py index 79c9335e..76eb0d37 100644 --- a/shotmanager/features/storyboard/frame_grid/storyboard_frame_grid_props.py +++ b/shotmanager/features/storyboard/frame_grid/storyboard_frame_grid_props.py @@ -107,15 +107,24 @@ def updateStoryboardGrid(self, frameList): shot.camera.location[1] = grid.origin[1] # + grid.offset_z * ((i - 1) % grid.numShotsPerRow) # the whole grid has to be re-oriented for this to work... - match grid.orientTowardAxis: - case "X_POSITIVE": - shot.camera.rotation_euler = (radians(90), 0.0, radians(-90)) - case "X_NEGATIVE": - shot.camera.rotation_euler = (radians(90), 0.0, radians(90)) - case "Y_POSITIVE": - shot.camera.rotation_euler = (radians(90), 0.0, 0.0) - case "Y_NEGATIVE": - shot.camera.rotation_euler = (radians(90), 0.0, radians(-180)) + # match grid.orientTowardAxis: + # case "X_POSITIVE": + # shot.camera.rotation_euler = (radians(90), 0.0, radians(-90)) + # case "X_NEGATIVE": + # shot.camera.rotation_euler = (radians(90), 0.0, radians(90)) + # case "Y_POSITIVE": + # shot.camera.rotation_euler = (radians(90), 0.0, 0.0) + # case "Y_NEGATIVE": + # shot.camera.rotation_euler = (radians(90), 0.0, radians(-180)) + + if "X_POSITIVE" == grid.orientTowardAxis: + shot.camera.rotation_euler = (radians(90), 0.0, radians(-90)) + elif "X_NEGATIVE" == grid.orientTowardAxis: + shot.camera.rotation_euler = (radians(90), 0.0, radians(90)) + elif "Y_POSITIVE" == grid.orientTowardAxis: + shot.camera.rotation_euler = (radians(90), 0.0, 0.0) + elif "Y_NEGATIVE" == grid.orientTowardAxis: + shot.camera.rotation_euler = (radians(90), 0.0, radians(-180)) _classes = (UAS_ShotManager_FrameGrid,) diff --git a/shotmanager/operators/shots.py b/shotmanager/operators/shots.py index 1c3e309c..61fa49ad 100644 --- a/shotmanager/operators/shots.py +++ b/shotmanager/operators/shots.py @@ -31,6 +31,8 @@ from shotmanager.utils import utils from shotmanager.utils import utils_markers from shotmanager.utils.utils_time import zoom_dopesheet_view_to_range +from shotmanager.utils.utils_ui import propertyColumn +from shotmanager.utils.utils import slightlyRandomizeColor from shotmanager.config import config from shotmanager.config import sm_logging @@ -188,6 +190,7 @@ def invoke(self, context, event): def execute(self, context): scene = context.scene props = scene.UAS_shot_manager_props + # propsCurrentLayout = props.getCurrentLayout() prefs = config.getShotManagerPrefs() shot = props.getShotByIndex(self.index) @@ -220,16 +223,33 @@ def _updateEditors(changeTime=True, zoom_mode=""): zoom_dopesheet_view_to_range( context, edit_start, edit_end, changeTime=prefs.current_shot_changes_current_time_to_start ) - elif "SHOT" == zoom_mode or prefs.current_shot_changes_time_zoom: - zoom_dopesheet_view_to_range( - context, shot.start, shot.end, changeTime=prefs.current_shot_changes_current_time_to_start - ) + elif "SHOT" == zoom_mode: + zoomView = False + currentLayoutIsStb = "STORYBOARD" == props.currentLayoutMode() + if currentLayoutIsStb: + if "STORYBOARD" == shot.shotType and prefs.stb_current_stb_shot_changes_time_zoom: + zoomView = True + elif "PREVIZ" == shot.shotType and prefs.stb_current_pvz_shot_changes_time_zoom: + zoomView = True + else: + if "STORYBOARD" == shot.shotType and prefs.pvz_current_stb_shot_changes_time_zoom: + zoomView = True + elif "PREVIZ" == shot.shotType and prefs.pvz_current_pvz_shot_changes_time_zoom: + zoomView = True + + if zoomView: + zoom_dopesheet_view_to_range( + context, shot.start, shot.end, changeTime=prefs.current_shot_changes_current_time_to_start + ) - if prefs.current_shot_select_stb_frame: - if "STORYBOARD" == shot.shotType: - gpChild = shot.getStoryboardFrame() - if gpChild: - utils.select_object(gpChild) + if "STORYBOARD" == shot.shotType and prefs.current_stb_shot_select_stb_frame: + gpChild = shot.getStoryboardFrame() + if gpChild: + utils.select_object(gpChild) + elif "PREVIZ" == shot.shotType and prefs.current_pvz_shot_select_stb_frame: + gpChild = shot.getStoryboardFrame() + if gpChild: + utils.select_object(gpChild) # change shot if not self.event_ctrl: @@ -252,6 +272,7 @@ def _updateEditors(changeTime=True, zoom_mode=""): # shot.enabled = not shot.enabled # frame shot range in timeline + ############################### elif self.event_ctrl and not self.event_alt: # if event.alt: # props.setCurrentShotByIndex(self.index, changeTime=False, source_area=context.area) @@ -284,10 +305,10 @@ class UAS_ShotManager_ToggleContinuousGPEditingMode(Operator): """Set the specifed shot as current""" bl_idname = "uas_shot_manager.toggle_continuous_gp_editing_mode" - bl_label = "Continuous GP Editing" + bl_label = "Continuous Draw Mode" bl_description = ( - "When used, the current storyboard frame or shot grease pencil will be switched" - "\nto edit mode if the edit mode is activated on a shot in the scene" + "When used, the Storyboard Frame of each shot set to be the current" + "\none will be automaticaly selected and switched to Draw mode" ) bl_options = {"REGISTER"} @@ -931,11 +952,13 @@ class UAS_ShotManager_ShotDuplicate(Operator): bl_options = {"REGISTER", "UNDO"} name: StringProperty(name="Name") - startAtCurrentTime: BoolProperty(name="Start at Current Frame", default=True) - addToEndOfList: BoolProperty(name="Add at the End of the List") + startAtCurrentTime: BoolProperty(name="Start at Current Frame", default=False) + addToEndOfList: BoolProperty(name="Add at the End of the List", default=False) + duplicateCam: BoolProperty(name="Duplicate Camera", default=True) camName: StringProperty(name="Camera Name") - duplicateStoryboardGP: BoolProperty(name="Duplicate Storyboard Frame", default=True) + useDifferentColor: BoolProperty(name="Slightly Different Color", default=True) + duplicateStoryboardFrame: BoolProperty(name="Duplicate Storyboard Frame", default=True) @classmethod def poll(cls, context): @@ -953,40 +976,53 @@ def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=350) def draw(self, context): + props = context.scene.UAS_shot_manager_props + selectedShot = props.getSelectedShot() + shotIsStoryboard = selectedShot.isStoryboardType() + layout = self.layout # scene = context.scene box = layout.box() - box.separator(factor=0.2) - row = box.row(align=True) - row.label(text="New Shot Name:") + + mainCol = propertyColumn(box, padding_top=0.2, padding_left=0, padding_right=2) + shType = "Storyboard" if shotIsStoryboard else "Camera" + mainCol.label(text=f"New {shType} Shot:") + + propsCol = propertyColumn(mainCol, padding_top=0.8, padding_left=3) + row = propsCol.row(align=True) + row.label(text="Name:") row.scale_x = 1.6 row.prop(self, "name", text="") - box.separator(factor=0.1) - row = box.row(align=True) - row.separator(factor=2.5) - subgrid_flow = row.grid_flow(align=True, row_major=True, columns=1, even_columns=False) - # subgrid_flow.separator( factor=0.5) - # subgrid_flow.use_property_split = True - subgrid_flow.prop(self, "startAtCurrentTime") - subgrid_flow.prop(self, "addToEndOfList") - subgrid_flow.separator(factor=1.0) - subgrid_flow.prop(self, "duplicateCam") - - row = subgrid_flow.row() + # propsCol = propertyColumn(mainCol, padding_top=0.2, padding_left=4) + propsCol.separator(factor=1.0) + + # propsCol.prop(self, "useDifferentColor") + if shotIsStoryboard: + propsCol.prop(self, "startAtCurrentTime") + propsCol.prop(self, "addToEndOfList") + propsCol.separator(factor=1.0) + propsCol.prop(self, "duplicateCam") + + camPropsCol = propertyColumn(propsCol, padding_left=3) + row = camPropsCol.row() row.enabled = self.duplicateCam row.scale_x = 1.6 - row.separator(factor=2.0) row.label(text="New Camera Name:") row.scale_x = 2.4 row.prop(self, "camName", text="") row.separator(factor=0.5) - row = subgrid_flow.row() - row.enabled = self.duplicateCam - row.separator(factor=5) - row.prop(self, "duplicateStoryboardGP") + # propsCol.separator(factor=0.5) + # camSubPropsCol = propertyColumn(camPropsCol, padding_left=3) + if props.use_camera_color: + camPropsCol.prop(self, "useDifferentColor", text="Slightly Change the Camera Identification Color") + + if selectedShot.hasStoryboardFrame(): + row = camPropsCol.row() + row.enabled = self.duplicateCam + row.prop(self, "duplicateStoryboardFrame") box.separator(factor=0.4) @@ -999,7 +1035,7 @@ def execute(self, context): return {"CANCELLED"} newShotInd = len(props.get_shots()) if self.addToEndOfList else selectedShotInd + 1 - copyGreasePencil = self.duplicateCam and self.duplicateStoryboardGP + copyGreasePencil = self.duplicateCam and self.duplicateStoryboardFrame newShot = props.copyShot( selectedShot, atIndex=newShotInd, copyCamera=self.duplicateCam, copyGreasePencil=copyGreasePencil ) @@ -1010,6 +1046,10 @@ def execute(self, context): newShot.start = context.scene.frame_current newShot.end = newShot.start + selectedShot.end - selectedShot.start + if self.useDifferentColor: + if props.use_camera_color: + newShot.camera.color = slightlyRandomizeColor(selectedShot.camera.color, weight=0.55) + # if self.duplicateCam and newShot.camera is not None: # newCam = utils.duplicateObject(newShot.camera) # newCam.name = self.camName diff --git a/shotmanager/operators/takes.py b/shotmanager/operators/takes.py index 4e709fdf..7e7a922e 100644 --- a/shotmanager/operators/takes.py +++ b/shotmanager/operators/takes.py @@ -23,20 +23,6 @@ from bpy.types import Operator from bpy.props import StringProperty, BoolProperty, IntProperty -# import shotmanager.operators.shots as shots - - -# from ..properties import get_takes - - -def _copy_shot(src_shot, dst_shot): - dst_shot.name = src_shot.name - dst_shot.start = src_shot.start - dst_shot.end = src_shot.end - dst_shot.enabled = src_shot.enabled - dst_shot.camera = src_shot.camera - dst_shot.color = src_shot.color - class UAS_ShotManager_TakeAdd(Operator): bl_idname = "uas_shot_manager.take_add" diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py index bf356034..cd9c5b92 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_clip_component.py @@ -395,15 +395,20 @@ def _on_manipulated_changed(self, context, event, isManipulated): self.shotsStackWidget.manipulatedComponent = self if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() - bpy.ops.ed.undo_push() + bpy.ops.ed.undo_push(message="Pre-Modify Shot Clip in the Interactive Shots Stack") else: if self.shot.isCameraValid(): - self.manipulatedChildren = [self.shot.camera] - bpy.ops.ed.undo_push() + self.manipulatedChildren = self.shot.getStoryboardChildren() + if self.manipulatedChildren is None: + self.manipulatedChildren = list() + self.manipulatedChildren.append(self.shot.camera) + bpy.ops.ed.undo_push(message="Pre-Modify Shot Clip in the Interactive Shots Stack") else: _logger.debug_ext("component2D handle_events set manipulated False", col="PURPLE", tag="EVENT") self.shotsStackWidget.manipulatedComponent = None + # bpy.ops.ed.undo_push(message="Modified Shot Clip in the Interactive Shots Stack") + # override of InteractiveComponent def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): """wkip note: delta_frames is in frames but may need to be in pixels in some cases""" diff --git a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py index d5e02a49..a7cee4b4 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py +++ b/shotmanager/overlay_tools/interact_shots_stack/widgets/shots_stack_handle_component.py @@ -79,6 +79,7 @@ def __init__( # filled when isManipulated changes self.manipulatedChildren = None self.manipulationBeginingFrame = None + self.scalingShot = False # green or orange self.color_highlight = (0.2, 0.7, 0.2, 1) if self.isStart else (0.7, 0.3, 0.0, 1) @@ -169,24 +170,56 @@ def _on_manipulated_changed(self, context, event, isManipulated): function is called """ # we use this to set the color of the clip as for when manipulated - self.manipulatedChildren = None if isManipulated: + self.manipulatedChildren = None + self.scalingShot = False self.shotsStackWidget.manipulatedComponent = self self.parent.isManipulatedByAnotherComponent = True self.manipulationBeginingFrame = context.scene.frame_current if self.shot.isStoryboardType(): self.manipulatedChildren = self.shot.getStoryboardChildren() - bpy.ops.ed.undo_push() + bpy.ops.ed.undo_push(message="Pre-Modify Storyboard Shot Clip Handle in the Interactive Shots Stack") else: if self.shot.isCameraValid(): - self.manipulatedChildren = [self.shot.camera] - bpy.ops.ed.undo_push() + self.manipulatedChildren = self.shot.getStoryboardChildren() + if self.manipulatedChildren is None: + self.manipulatedChildren = list() + self.manipulatedChildren.append(self.shot.camera) + bpy.ops.ed.undo_push(message="Pre-Modify Camera Shot Clip Handle in the Interactive Shots Stack") else: + # snap keys to frames + if self.scalingShot: + retimerApplyToSettings = context.window_manager.UAS_shot_manager_shots_stack_retimerApplyTo + + if self.shot.isStoryboardType(): + retimerApplyToSettings.initialize("STB_SHOT_CLIP") + else: + retimerApplyToSettings.initialize("PVZ_SHOT_CLIP") + + start_incl = self.shot.start + duration_incl = self.shot.end - start_incl + 1 + + retimeScene( + context=context, + retimeMode="SNAP", + retimerApplyToSettings=retimerApplyToSettings, + objects=self.manipulatedChildren, + # start_incl=-10000, + # duration_incl=900000, + start_incl=start_incl, + duration_incl=duration_incl, + keysBeforeRangeMode="SNAP", + keysAfterRangeMode="SNAP", + ) + + self.manipulatedChildren = None + self.manipulationBeginingFrame = None self.shotsStackWidget.manipulatedComponent = None self.parent.isManipulatedByAnotherComponent = False - self.manipulationBeginingFrame = None + self.scalingShot = False + # bpy.ops.ed.undo_push(message="Modified Shot Clip Handle in the Interactive Shots Stack") # override of InteractiveComponent def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): @@ -225,6 +258,7 @@ def _on_manipulated_mouse_moved(self, context, event, mouse_delta_frames=0): if scaleShotContent: # do NOT snap on frames during scaling transformation otherwhise frames will be lost because merged at the same time ! + self.scalingShot = True retimerApplyToSettings.snapKeysToFrames = False retimeFactor = (self.shot.end - self.shot.start) / (prevShotEnd - prevShotStart) diff --git a/shotmanager/prefs/prefs_features.py b/shotmanager/prefs/prefs_features.py index 1a772b7e..c6bb13a5 100644 --- a/shotmanager/prefs/prefs_features.py +++ b/shotmanager/prefs/prefs_features.py @@ -173,10 +173,25 @@ def _draw_separator_row(layout, factor=0.9): if "SCENE" == mode: layout.separator(factor=separatorVertTopics) - row = layout.row(align=True) - leftRow = row.row(align=True) - leftRow.label(text="Takes and Shots features to toggle with the selected layout:") + row = layout.row(align=True) + leftRow = row.row(align=True) + leftRow.alignment = "LEFT" + # leftRow.label(text="Takes and Shots features to toggle with the selected layout:") + subLeftRow01 = leftRow.row(align=True) + subLeftRow01.alignment = "RIGHT" + subLeftRow01.label(text="Takes and Shots features") + subLeftRow02 = subLeftRow01.row(align=True) + subLeftRow02.alert = True + subLeftRow02.alignment = "LEFT" + txt = ( + " toggled with the selected layout:" + if "SCENE" == mode + else " to enable by default with the selected layout in new scenes:" + ) + subLeftRow02.label(text=txt) + + if "SCENE" == mode: rightRow = row.row(align=False) rightRow.alignment = "RIGHT" resetOp = rightRow.operator("uas_shotmanager.querybox", text="Reset", icon="LOOP_BACK") @@ -188,8 +203,8 @@ def _draw_separator_row(layout, factor=0.9): rightRow.operator("preferences.addon_show", text="", icon="PREFERENCES").module = "shotmanager" # rightRow.separator() - else: - layout.label(text="Takes and Shots features to enable by default with the selected layout:") + # else: + # layout.label(text="Takes and Shots features to enable by default with the selected layout:") box = layout.box() boxSplit = box.split(factor=0.5) @@ -273,27 +288,47 @@ def _draw_separator_row(layout, factor=0.9): subrow.prop(propsLayout, f"{layoutPrefix}display_advanced_infos", text="", icon="SYNTAX_ON") subrow.label(text="Display Advanced Infos") - ################ - # Selection settings - propsRow = box.row() - propsRow.separator(factor=leftSepFactor) - propsCol = propsRow.column(align=True) - propsCol.label(text="Make the selected shot also the current one when it is selected from:") - row = propsCol.row() - row.separator(factor=3) - subCol = row.column(align=True) - - # NOTE: when the Continuous Editing mode is on then the selected and current shots are tied anyway - subCol.prop( - propsLayout, - f"{layoutPrefix}selected_shot_changes_current_shot", - text="The Shots List", - ) - subCol.prop( - propsLayout, - f"{layoutPrefix}selected_shot_in_shots_stack_changes_current_shot", - text="The Interactive Shots Stack", - ) + # ################ + # # Selection settings + # propsRow = box.row() + # propsRow.separator(factor=leftSepFactor) + # propsCol = propsRow.column(align=True) + # propsCol.label(text="Make the selected shot also the current one when it is selected from:") + # row = propsCol.row() + # row.separator(factor=3) + + # # subCol = row.column(align=True) + # split = row.split(factor=0.35) + + # # NOTE: when the Continuous Editing mode is on then the selected and current shots are tied anyway + # split.prop( + # propsLayout, + # f"{layoutPrefix}selected_shot_changes_current_shot", + # text="The Shots List", + # ) + # split.prop( + # propsLayout, + # f"{layoutPrefix}selected_shot_in_shots_stack_changes_current_shot", + # text="The Interactive Shots Stack", + # ) + + # ################ + # # Timeline zoom settings + # propsRow = box.row() + # propsRow.separator(factor=leftSepFactor) + # propsCol = propsRow.column(align=True) + # propsCol.label(text="When current shot is changed:") + # row = propsCol.row() + # row.separator(factor=3) + + # subCol = row.column(align=True) + + # # NOTE: when the Continuous Editing mode is on then the selected and current shots are tied anyway + # subCol.prop( + # propsLayout, + # f"{layoutPrefix}current_shot_changes_time_zoom", + # text="Zoom Timeline Content to Frame The Current Shot", + # ) ################################################################# # diff --git a/shotmanager/prefs/prefs_shots_display.py b/shotmanager/prefs/prefs_shots_display.py index 72691046..150f5d1d 100644 --- a/shotmanager/prefs/prefs_shots_display.py +++ b/shotmanager/prefs/prefs_shots_display.py @@ -32,8 +32,8 @@ class UAS_ShotManager_Shots_Prefs(Operator): bl_idname = "uas_shot_manager.shots_prefs" - bl_label = "Shots Display Settings" - bl_description = "Display the Shots Settings panel\nfor the Shot Manager instanced in this scene" + bl_label = "Shots Display and Manipulation Settings" + bl_description = "Display the Shots Display and Manipulation Settings panel" bl_options = {"INTERNAL"} def invoke(self, context, event): @@ -49,17 +49,19 @@ def draw(self, context): layout.label(text="Any change is effective immediately") layout.alert = False - # Shot List - ############## - layout.label(text="Shot List:") + # Shot List display + ################ + sceneDisplayRow = layout.row() + sceneDisplayRow.label(text="Shot List Display:") + sceneDisplayRightRow = sceneDisplayRow.row() + sceneDisplayRightRow.alignment = "RIGHT" + sceneDisplayRightRow.label(text="(in Current Scene)") + box = layout.box() box.use_property_decorate = False # main column, to allow title offset maincol = box.column() - row = maincol.row() - row.separator(factor=2) - row.label(text="Display:") # empty spacer column row = maincol.row() @@ -89,54 +91,6 @@ def draw(self, context): row.prop(props, "current_shot_properties_mode", text="") row.separator() - # User Prefs at addon level - ############### - - box.separator(factor=0.5) - # main column, to allow title offset - maincol = box.column() - row = maincol.row() - row.separator(factor=2) - row.label(text="When Current Shot Is Changed: (Settings stored in the Add-on Preferences):") - - # col.scale_x = 0.28 - - propsCol = propertyColumn(maincol, padding_left=7) - - # col.separator(factor=1.0) - # col.label(text="Time Change:") - # col.label(text="User Preferenes (in Preference Add-on Window):") - # col.use_property_split = False - propsCol.prop( - prefs, - "current_shot_changes_current_time_to_start", - text="Set Current Frame To Shot Start", - ) - propsCol.prop( - prefs, - "current_shot_changes_time_range", - text="Set Scene Animation Range To Shot Range", - ) - propsCol.prop( - prefs, - "current_shot_changes_time_zoom", - text="Zoom Timeline Content To Frame The Current Shot", - ) - - # propsCol.prop( - # prefs, - # "current_shot_changes_edited_frame_in_stb", - # text="Storyboard Shots List: Set Selected Shot to Edited One", - # ) - - propsCol.separator(factor=0.5) - propsCol.label(text="Storyboard Shots:") - propsCol.prop( - prefs, - "current_shot_select_stb_frame", - text="Select Storyboard Frame of the Current Short", - ) - layout.separator(factor=2) def execute(self, context): diff --git a/shotmanager/properties/layout_settings.py b/shotmanager/properties/layout_settings.py index e247db7e..e5432aa0 100644 --- a/shotmanager/properties/layout_settings.py +++ b/shotmanager/properties/layout_settings.py @@ -43,16 +43,22 @@ class UAS_ShotManager_LayoutSettings(PropertyGroup): default="PREVIZ", ) - selected_shot_changes_current_shot: BoolProperty( - name="Set selected shot to current", - description="When a shot is selected in the shot list, the shot is also set to be the current one", - default=False, - ) - selected_shot_in_shots_stack_changes_current_shot: BoolProperty( - name="Set selected shot in Shots Stack to current", - description="When a shot is selected in the Interactive Shots Stack, the shot is also set to be the current one", - default=False, - ) + # selected_shot_changes_current_shot: BoolProperty( + # name="Set selected shot to current", + # description="When a shot is selected in the shot list, the shot is also set to be the current one", + # default=False, + # ) + # selected_shot_in_shots_stack_changes_current_shot: BoolProperty( + # name="Set selected shot in Shots Stack to current", + # description="When a shot is selected in the Interactive Shots Stack, the shot is also set to be the current one", + # default=False, + # ) + + # current_shot_changes_time_zoom: BoolProperty( + # name="Zoom Timeline to Shot Range", + # description="Automatically zoom the timeline content to frame the shot when the current shot is changed", + # default=False, + # ) display_storyboard_in_properties: BoolProperty( name="Storyboard Frames and Grease Pencil Tools", @@ -120,10 +126,11 @@ def initialize(self, layoutMode): self.name = "Storyboard" self.layoutMode = "STORYBOARD" - self.selected_shot_changes_current_shot = prefs.stb_selected_shot_changes_current_shot - self.selected_shot_in_shots_stack_changes_current_shot = ( - prefs.stb_selected_shot_in_shots_stack_changes_current_shot - ) + # self.selected_shot_changes_current_shot = prefs.stb_selected_shot_changes_current_shot + # self.selected_shot_in_shots_stack_changes_current_shot = ( + # prefs.stb_selected_shot_in_shots_stack_changes_current_shot + # ) + # self.current_shot_changes_time_zoom = prefs.stb_current_shot_changes_time_zoom self.display_storyboard_in_properties = prefs.stb_display_storyboard_in_properties self.display_notes_in_properties = prefs.stb_display_notes_in_properties self.display_cameraBG_in_properties = prefs.stb_display_cameraBG_in_properties @@ -137,10 +144,11 @@ def initialize(self, layoutMode): self.name = "Previz" self.layoutMode = "PREVIZ" - self.selected_shot_changes_current_shot = prefs.pvz_selected_shot_changes_current_shot - self.selected_shot_in_shots_stack_changes_current_shot = ( - prefs.pvz_selected_shot_in_shots_stack_changes_current_shot - ) + # self.selected_shot_changes_current_shot = prefs.pvz_selected_shot_changes_current_shot + # self.selected_shot_in_shots_stack_changes_current_shot = ( + # prefs.pvz_selected_shot_in_shots_stack_changes_current_shot + # ) + # self.current_shot_changes_time_zoom = prefs.pvz_current_shot_changes_time_zoom self.display_storyboard_in_properties = prefs.pvz_display_storyboard_in_properties self.display_notes_in_properties = prefs.pvz_display_notes_in_properties self.display_cameraBG_in_properties = prefs.pvz_display_cameraBG_in_properties diff --git a/shotmanager/properties/props.py b/shotmanager/properties/props.py index 601e6f33..f822323d 100644 --- a/shotmanager/properties/props.py +++ b/shotmanager/properties/props.py @@ -187,6 +187,7 @@ def initialize_shot_manager(self): # self.parentScene = self.getParentScene() if not prefs.isInitialized: + prefs.initialize_shot_manager_prefs() if self.parentScene is None: @@ -1102,15 +1103,24 @@ def _update_target_viewport_index(self, context): #################### useContinuousGPEditing: BoolProperty( - name="Continuous GP Editing", - description="When used, the current storyboard frame or shot grease pencil will be switched" - "\nto edit mode if the edit mode is activated on a shot in the scene", + name="Continuous Drawing Mode", + description="When used, the Storyboard Frame of each shot set to be the current" + "\none will be automaticaly selected and switched to Draw mode", default=True, ) - def isInContinuousGPEditing(self): - """Call this to get the state of continuous editing instead of directly using useContinuousGPEditing""" - state = self.useContinuousGPEditing and "STORYBOARD" == self.currentLayoutMode() + def isContinuousGPEditingModeActive(self): + """Return True if the context required to enter in a continuous drawing session is valid: the button + has to be visible and checked. + Note that the returned value is not dependent on the fact that a Grease Pencil object is being edited or not. + Query wkip for that. + Call this to get the state of continuous editing instead of directly using useContinuousGPEditing""" + + state = False + currentLayout = self.getCurrentLayout() + if currentLayout: + state = currentLayout.display_storyboard_in_properties and self.useContinuousGPEditing + # state = state and "STORYBOARD" == self.currentLayoutMode() return state def getParentShotFromGpChild(self, obj): @@ -1348,10 +1358,14 @@ def getStoryboardShots(self, ignoreDisabled=False, takeIndex=-1): return shotList - def getEditedGPShot(self, shotType=None): # , takeIndex=-1): - """Return the edited grease pencil parent shot, None if no grease pencil child is currently being edited + def getEditedGPShot(self, shotType=None, onlyInCurrentTake=True): # , takeIndex=-1): + """Return the edited grease pencil parent shot, None if no grease pencil child is currently being edited. + If onlyInCurrentTake is set to True then if the edited shot is not in the current take the returned value + is None. Args: - shotType: can be None, PREVIZ or STORYBOARD""" + shotType: can be None to get the shot no matter which type it is, PREVIZ or STORYBOARD + onlyInCurrentTake: if True, the edited shot is returned only if it belongs to the current shot + """ # takeInd = ( # self.getCurrentTakeIndex() # if -1 == takeIndex @@ -1361,13 +1375,17 @@ def getEditedGPShot(self, shotType=None): # , takeIndex=-1): # return None editedGP = utils_greasepencil.getObjectInSubMode() + + # NOTE: we don't ckeck if the editing mode is Draw, Edit, Sculpt... this in order to + # still navigate between edited objects without changing the mode if editedGP is not None and "GPENCIL" == editedGP.type: parentShot = self.getParentShotFromGpChild(editedGP) if parentShot is not None: - if shotType is None: - return parentShot - elif shotType == parentShot.shotType: - return parentShot + if not onlyInCurrentTake or parentShot.getParentTakeIndex() == self.getCurrentTakeIndex(): + if shotType is None: + return parentShot + elif shotType == parentShot.shotType: + return parentShot return None def getEditedStoryboardFrame(self): # , takeIndex=-1): @@ -1494,6 +1512,9 @@ def getCurrentLayout(self): return self.layouts[self.current_layout_index] return None + def getCurrentLayoutIndex(self): + return self.current_layout_index + def setCurrentLayout(self, layoutMode): """Args: layoutMode: Can be "STORYBOARD or PREVIZ""" @@ -1502,17 +1523,6 @@ def setCurrentLayout(self, layoutMode): self.current_layout_index = ind break - def getLayout(self, layoutMode): - """Args: - layoutMode: Can be "STORYBOARD or PREVIZ""" - for layout in self.layouts: - if layoutMode == layout.layoutMode: - return layout - return None - - def getCurrentLayoutIndex(self): - return self.current_layout_index - def setCurrentLayoutByIndex(self, layoutIndex): """Args: layoutMode: Can be 0 for "STORYBOARD or 1 for PREVIZ""" @@ -1521,6 +1531,14 @@ def setCurrentLayoutByIndex(self, layoutIndex): else: self.current_layout_index = -1 + def getLayout(self, layoutMode): + """Args: + layoutMode: Can be "STORYBOARD or PREVIZ""" + for layout in self.layouts: + if layoutMode == layout.layoutMode: + return layout + return None + def createLayoutSettings(self): _logger.debug_ext("createLayerSettings", col="GREEN", tag="LAYOUT") @@ -2071,48 +2089,52 @@ def _set_useLockCameraView(self, value): selected_shot_index_call_update__flag: BoolProperty(default=True) + def selectedShotShouldBecomeCurrent(self): + """Return True if, according to the Preference settings and the current UI and scene context, + the actualy selected shot should be set as the current one. + Call this function just after the considered shot has been selected.""" + prefs = config.getShotManagerPrefs() + + setCurrentShot = False + # if False: + # if "STORYBOARD" == self.layout_mode: + if "STORYBOARD" == self.currentLayoutMode(): + if prefs.shot_selected_from_shots_stack__flag: + # print(" call from shots stack") + if prefs.stb_selected_shot_in_shots_stack_changes_current_shot: + # print(" sel in shots stack") + setCurrentShot = True + else: + if self.isContinuousGPEditingModeActive(): + setCurrentShot = True + elif prefs.stb_selected_shot_changes_current_shot: + _logger.debug_ext(" Stb _update_selected_shot_index from shot list") + # print("\n*** selected_shot_index. New state: ", self.selected_shot_index) + setCurrentShot = True + # PREVIZ + else: + if prefs.shot_selected_from_shots_stack__flag: + # print(" call from shots stack") + if prefs.pvz_selected_shot_in_shots_stack_changes_current_shot: + # print(" sel in shots stack") + setCurrentShot = True + else: + if self.isContinuousGPEditingModeActive(): + setCurrentShot = True + elif prefs.pvz_selected_shot_changes_current_shot: + _logger.debug_ext(" Pvz _update_selected_shot_index from shot list") + # print("\n*** selected_shot_index. New state: ", self.selected_shot_index) + setCurrentShot = True + + return setCurrentShot + def _update_selected_shot_index(self, context): if self.selected_shot_index_call_update__flag: - props = context.scene.UAS_shot_manager_props - prefs = config.getShotManagerPrefs() if -1 != self.selected_shot_index: _logger.debug_ext(f"\n*** selected_shot_index. New state: {self.selected_shot_index}") - setCurrentShot = False - # if False: - # if "STORYBOARD" == self.layout_mode: - if "STORYBOARD" == self.currentLayoutMode(): - if prefs.shot_selected_from_shots_stack__flag: - # print(" call from shots stack") - if prefs.stb_selected_shot_in_shots_stack_changes_current_shot: - # print(" sel in shots stack") - setCurrentShot = True - else: - # if props.useContinuousGPEditing: - if props.isInContinuousGPEditing(): - setCurrentShot = True - elif prefs.stb_selected_shot_changes_current_shot: - _logger.debug_ext(" Stb _update_selected_shot_index from shot list") - # print("\n*** selected_shot_index. New state: ", self.selected_shot_index) - setCurrentShot = True - # PREVIZ - else: - if prefs.shot_selected_from_shots_stack__flag: - # print(" call from shots stack") - if prefs.pvz_selected_shot_in_shots_stack_changes_current_shot: - # print(" sel in shots stack") - setCurrentShot = True - else: - # if props.useContinuousGPEditing: - if props.isInContinuousGPEditing(): - setCurrentShot = True - elif prefs.pvz_selected_shot_changes_current_shot: - _logger.debug_ext(" Pvz _update_selected_shot_index from shot list") - # print("\n*** selected_shot_index. New state: ", self.selected_shot_index) - setCurrentShot = True - - if setCurrentShot: - bpy.ops.uas_shot_manager.set_current_shot(index=self.selected_shot_index) + if self.selectedShotShouldBecomeCurrent(): + bpy.ops.uas_shot_manager.set_current_shot(index=self.selected_shot_index) selected_shot_index: IntProperty(name="Shot Name", update=_update_selected_shot_index, default=-1) @@ -2517,7 +2539,7 @@ def copyTake(self, take, atIndex=-1, copyCamera=False, ignoreDisabled=False): shots = take.getShotsList(ignoreDisabled=ignoreDisabled) for shot in shots: - self.copyShot(shot, targetTakeIndex=newTakeInd, copyCamera=copyCamera) + self.copyShot(shot, targetTakeIndex=newTakeInd, copyCamera=copyCamera, copyGreasePencil=True) return newTake @@ -3490,8 +3512,7 @@ def setCurrentShotByIndex(self, currentShotIndex, changeTime=None, source_area=N # storyboard ########################## # if "STORYBOARD" == self.layout_mode and prefs.current_shot_changes_edited_frame_in_stb: - # if self.useContinuousGPEditing: - if self.isInContinuousGPEditing(): + if self.isContinuousGPEditingModeActive(): # if self.getEditedStoryboardFrame() is not None: if self.getEditedGPShot() is not None: bpy.ops.uas_shot_manager.greasepencil_select_and_draw( diff --git a/shotmanager/properties/shot.py b/shotmanager/properties/shot.py index 5cd260dd..de8bd0cd 100644 --- a/shotmanager/properties/shot.py +++ b/shotmanager/properties/shot.py @@ -785,6 +785,9 @@ def getStoryboardFrame(self): """ return self.getGreasePencilObject(mode="STORYBOARD") + def hasStoryboardFrame(self): + return self.getGreasePencilObject("STORYBOARD") is not None + # TODO: wkip check that the empty is the one of the storyboard frame def getStoryboardEmptyChild(self): """Return the Empty object used as the parent of the storyboard frame @@ -797,7 +800,7 @@ def getStoryboardEmptyChild(self): # wkip to update with the gp list def getStoryboardChildren(self): """If the shot has a valid camera: return the list of all the children of the camera associated - to the Storyboard Frame + to the Storyboard Frame. The camera is NOT included, but the empty object is. Return None otherwise Note: This doesn't depend on the shot type since camera shots can also have a storyboard frame""" stbChildren = None diff --git a/shotmanager/rendering/rendering_ui.py b/shotmanager/rendering/rendering_ui.py index c7e6a903..b9ac3ee9 100644 --- a/shotmanager/rendering/rendering_ui.py +++ b/shotmanager/rendering/rendering_ui.py @@ -538,8 +538,15 @@ def _separatorRow(layout): # STILL ### if props.displayStillProps: - row = layout.row() - row.label(text="Render Image:") + titleRow = layout.row(align=False) + titleLeftRow = titleRow.row() + titleLeftRow.alignment = "RIGHT" + titleLeftRow.label(text="Render Image:") + # currentShot = props.getCurrentShot() + # txt = currentShot.name if currentShot else "*** No Current Shot ***" + txt = f"Frame {scene.frame_current}" + titleRow.label(text=txt) + box = layout.box() if props.use_project_settings: @@ -588,8 +595,14 @@ def _separatorRow(layout): # ANIMATION ### elif props.displayAnimationProps: - row = layout.row() - row.label(text="Render Current Shot:") + titleRow = layout.row(align=False) + titleLeftRow = titleRow.row() + titleLeftRow.alignment = "RIGHT" + titleLeftRow.label(text="Render Current Shot:") + currentShot = props.getCurrentShot() + txt = currentShot.name if currentShot else "*** No Current Shot ***" + titleRow.label(text=txt) + box = layout.box() if props.use_project_settings: @@ -649,8 +662,9 @@ def _separatorRow(layout): # ALL EDITS ### elif props.displayAllEditsProps: - row = layout.row() - row.label(text="Render All:") + titleRow = layout.row(align=True) + titleRow.separator(factor=0.5) + titleRow.label(text="Render All:") box = layout.box() if props.use_project_settings: @@ -728,8 +742,9 @@ def _separatorRow(layout): # EDIT FILE ### elif props.displayOtioProps: - row = layout.row() - row.label(text="Generate Edit File:") + titleRow = layout.row(align=True) + titleRow.separator(factor=0.5) + titleRow.label(text="Generate Edit File:") box = layout.box() @@ -757,8 +772,9 @@ def _separatorRow(layout): # PLAYBLAST ### elif props.displayPlayblastProps: - row = layout.row() - row.label(text="Playblast:") + titleRow = layout.row(align=True) + titleRow.separator(factor=0.5) + titleRow.label(text="Playblast:") # video tracks available? versionStr = utils.addonVersion("Video Tracks") diff --git a/shotmanager/retimer/retimer.py b/shotmanager/retimer/retimer.py index 8eb1242f..2f97ac53 100644 --- a/shotmanager/retimer/retimer.py +++ b/shotmanager/retimer/retimer.py @@ -36,6 +36,9 @@ class FCurve: def __init__(self, fcurve): self.fcurve = fcurve + def get_key(self, index): + return self.fcurve.keyframe_points[index] + def get_key_coordinates(self, index): return self.fcurve.keyframe_points[index].co @@ -57,7 +60,7 @@ def handles(self, index): # self.fcurve.keyframe_points[ index ].handle_left, self.fcurve.keyframe_points[ index ].handle_right = value def insert_frame(self, coordinates, roundToNearestFrame=True): - frame_val = round(coordinates[0]) if roundToNearestFrame else coordinate[0] + frame_val = round(coordinates[0]) if roundToNearestFrame else coordinates[0] self.fcurve.keyframe_points.insert(frame_val, coordinates[1]) def remove_frames(self, start_incl, end_incl, remove_gap=False, roundToNearestFrame=True): @@ -264,9 +267,27 @@ def rescale_frame(frame_value, start_incl, end_incl, pivot, factor, roundToNeare ########################################################################## +def _offset_fCurve_key(key, offset): + key_time, value = key.co + new_key_time = key_time + offset + key.handle_left[0] += offset + key.handle_right[0] += offset + key.co = (new_key_time, value) + + +def _set_time_fCurve_key(key, new_key_time): + key_time, value = key.co + offset = new_key_time - key_time + key.handle_left[0] += offset + key.handle_right[0] += offset + key.co = (new_key_time, value) + + def _offset_fCurve_frames(fcurve: FCurve, start_incl, offset, roundToNearestFrame): for i in range(len(fcurve)): - key_time, value = fcurve.get_key_coordinates(i) + key = fcurve.get_key(i) + key_time = key.co[0] + # key_time, value = fcurve.get_key_coordinates(i) if start_incl <= key_time: # key_time = key_time + offset # if roundToNearestFrame: @@ -280,11 +301,12 @@ def _offset_fCurve_frames(fcurve: FCurve, start_incl, offset, roundToNearestFram if roundToNearestFrame: new_key_time = round(new_key_time) - offset_with_rounded_added_offset = new_key_time - key_time - left_handle, right_handle = fcurve.handles(i) - left_handle[0] += offset_with_rounded_added_offset - right_handle[0] += offset_with_rounded_added_offset - fcurve.set_key_coordinates(i, (new_key_time, value)) + _set_time_fCurve_key(key, new_key_time) + # offset_with_rounded_added_offset = new_key_time - key_time + # left_handle, right_handle = fcurve.handles(i) + # left_handle[0] += offset_with_rounded_added_offset + # right_handle[0] += offset_with_rounded_added_offset + # fcurve.set_key_coordinates(i, (new_key_time, value)) def _rescale_fCurve_frames( @@ -337,7 +359,9 @@ def _rescale_fCurve_frames( # bpy.ops.action.clean(threshold=0.001, channels=False) for i in range(len(fcurve)): - key_time, key_value = fcurve.get_key_coordinates(i) + key = fcurve.get_key(i) + key_time, key_value = key.co + # key_time, key_value = fcurve.get_key_coordinates(i) changeMode = "RESCALE" if key_time < start_incl: changeMode = keysBeforeRangeMode @@ -387,12 +411,14 @@ def _rescale_fCurve_frames( new_key_time = key_time + offset if roundToNearestFrame: new_key_time = round(new_key_time) - fcurve.set_key_coordinates(i, (new_key_time, key_value)) - offset_with_rounded_added_offset = new_key_time - key_time - left_handle, right_handle = fcurve.handles(i) - left_handle[0] += offset_with_rounded_added_offset - right_handle[0] += offset_with_rounded_added_offset + _set_time_fCurve_key(key, new_key_time) + # fcurve.set_key_coordinates(i, (new_key_time, key_value)) + + # offset_with_rounded_added_offset = new_key_time - key_time + # left_handle, right_handle = fcurve.handles(i) + # left_handle[0] += offset_with_rounded_added_offset + # right_handle[0] += offset_with_rounded_added_offset # if factor < 1.0: # _offset_fCurve_frames( @@ -403,6 +429,37 @@ def _rescale_fCurve_frames( # ) +def _snap_fCurve_frames( + *, + fcurve: FCurve, + start_incl, + end_incl, + keysBeforeRangeMode="DO_NOTHING", + keysAfterRangeMode="DO_NOTHING", +): + """ + Args: + keysBeforeRangeMode: Action to do on keys located before the specified time range. Can be DO_NOTHING, SNAP + keysAfterRangeMode: Action to do on keys located after the specified time range. Can be DO_NOTHING, SNAP + """ + # TODO: wkip delete ducplicated keys !!! + + # bpy.ops.action.clean(threshold=0.001, channels=False) + for i in range(len(fcurve)): + key = fcurve.get_key(i) + key_time, key_value = key.co + # key_time, key_value = fcurve.get_key_coordinates(i) + changeMode = "SNAP" + if key_time < start_incl: + changeMode = keysBeforeRangeMode + elif end_incl < key_time: + changeMode = keysAfterRangeMode + + if "SNAP" == changeMode: + # fcurve.set_key_coordinates(i, (round(key_time), key_value)) + _set_time_fCurve_key(key, round(key_time)) + + def retime_fCurve_frames( fcurve: FCurve, mode, @@ -417,8 +474,11 @@ def retime_fCurve_frames( ): """ Args: - keysBeforeRangeMode: Action to do on keys located before the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" - keysAfterRangeMode: Action to do on keys located after the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" + retimeMode: Can be GLOBAL_OFFSET, INSERT_BEFORE, INSERT_AFTER, DELETE_RANGE, RESCALE, CLEAR_ANIM, SNAP, FREEZE + start_incl (int): The included start frame + duration_incl (int): The range of retime frames (new or deleted) + keysBeforeRangeMode: Action to do on keys located before the specified time range. Can be DO_NOTHING, OFFSET, RESCALE, SNAP + keysAfterRangeMode: Action to do on keys located after the specified time range. Can be DO_NOTHING, OFFSET, RESCALE, SNAP """ if mode == "INSERT": @@ -440,6 +500,15 @@ def retime_fCurve_frames( keysAfterRangeMode=keysAfterRangeMode, ) + elif mode == "SNAP": + _snap_fCurve_frames( + fcurve=fcurve, + start_incl=start_incl, + end_incl=end_incl, + keysBeforeRangeMode=keysBeforeRangeMode, + keysAfterRangeMode=keysAfterRangeMode, + ) + elif mode == "FREEZE": for i in range(len(fcurve)): key_time, value = fcurve.get_key_coordinates(i) @@ -1012,11 +1081,11 @@ def retimeScene( ): """Apply the time change for each type of entities Args: - retimeMode: Can be GLOBAL_OFFSET, INSERT_BEFORE, INSERT_AFTER, DELETE_RANGE, RESCALE, CLEAR_ANIM", + retimeMode: Can be GLOBAL_OFFSET, INSERT_BEFORE, INSERT_AFTER, DELETE_RANGE, RESCALE, CLEAR_ANIM, SNAP, start_incl (int): The included start frame duration_incl (int): The range of retime frames (new or deleted) - keysBeforeRangeMode: Action to do on keys located before the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" - keysAfterRangeMode: Action to do on keys located after the specified time range. Can be "DO_NOTHING", "OFFSET", "RESCALE" + keysBeforeRangeMode: Action to do on keys located before the specified time range. Can be DO_NOTHING, OFFSET, RESCALE, SNAP + keysAfterRangeMode: Action to do on keys located after the specified time range. Can be NOTHING, OFFSET, RESCALE, SNAP """ # prefs = config.getShotManagerPrefs() scene = context.scene @@ -1085,7 +1154,7 @@ def _retimeFcurve(action, addActionToDone=True): for matSlot in obj.material_slots: if matSlot is not None: mat = matSlot.material - if mat not in sceneMaterials: + if mat is not None and mat not in sceneMaterials: sceneMaterials.append(mat) if mat.animation_data is not None and mat.animation_data.action is not None: _retimeFcurve(mat.animation_data.action) diff --git a/shotmanager/ui/sm_shots_ui_common.py b/shotmanager/ui/sm_shots_ui_common.py index 6c330149..82f691eb 100644 --- a/shotmanager/ui/sm_shots_ui_common.py +++ b/shotmanager/ui/sm_shots_ui_common.py @@ -66,8 +66,7 @@ def drawStoryboardRow(layout, props, item, index): row.operator("uas_shot_manager.greasepencil_select_and_draw", text="", icon_value=icon.icon_id).index = index else: # if "STORYBOARD" == props.currentLayoutMode(): - # opMode = "DRAW" if props.useContinuousGPEditing else "SELECT" - opMode = "DRAW" if props.isInContinuousGPEditing() else "SELECT" + opMode = "DRAW" if props.isContinuousGPEditingModeActive() else "SELECT" # if gp == context.active_object and context.active_object.mode == "PAINT_GPENCIL": if gp.mode == "PAINT_GPENCIL": @@ -83,8 +82,7 @@ def drawStoryboardRow(layout, props, item, index): # else: # icon = config.icons_col["ShotManager_CamGPShot_32"] - # if props.useContinuousGPEditing: - if props.isInContinuousGPEditing(): + if props.isContinuousGPEditingModeActive(): icon = "OUTLINER_DATA_GP_LAYER" op = row.operator("uas_shot_manager.greasepencil_select_and_draw", text="", icon=icon) else: diff --git a/shotmanager/utils/utils.py b/shotmanager/utils/utils.py index c4defe10..89be43d7 100644 --- a/shotmanager/utils/utils.py +++ b/shotmanager/utils/utils.py @@ -1206,7 +1206,9 @@ def lockTransforms(obj, *, lock=True): def slightlyRandomizeColor(refColor, weight=0.8): """ refColor is supposed to be linear, returned color is linear too - refColor can be RGB or RGBA. Alpha is not modified. + Args: + refColor: the input color. Can be RGB or RGBA. Alpha is not modified. + weight: value in [0, 1]. The closer weight is to 1, the less the color is changed """ from random import uniform From 5842587c20b83a4c3e4f9cb045ab6c834c10b64b Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Fri, 30 Sep 2022 01:19:52 +0200 Subject: [PATCH 22/27] key map for drawing mode --- CHANGELOG.md | 1 + shotmanager/config/config.py | 2 +- shotmanager/config/dev_notes.txt | 5 ++++ .../greasepencil/greasepencil_operators.py | 4 +++ shotmanager/keymaps/general_keymaps.py | 8 +++++ .../keymaps/playbar_wrappers_operators.py | 2 +- shotmanager/operators/general.py | 29 +++++++++++++++++-- shotmanager/properties/props.py | 2 +- 8 files changed, 48 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f05f5003..96e7fba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Fixed storyboard frame that was not duplicated following to a take duplication - Added shot name in the Render panel, near Current Shot label - Refactored the Duplicate Shot dialog box and added a checkbox for color variation +- Added a key map for activating the draw mode on a current shot ----- ## 2.1.5 (2022-09-27) diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index 72ada62d..4ea4713c 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -43,7 +43,7 @@ def initGlobalVariables(): devDebug = False # change this value to force debug at start time - devDebug = True + devDebug = False global devDebug_displayDebugPanel devDebug_displayDebugPanel = True diff --git a/shotmanager/config/dev_notes.txt b/shotmanager/config/dev_notes.txt index e9960b65..817ebc47 100644 --- a/shotmanager/config/dev_notes.txt +++ b/shotmanager/config/dev_notes.txt @@ -211,3 +211,8 @@ Bug: transform truc from paul truc information message contextuel +rajouter bouton de framing shot a coté de gpu + +shift qui ne marche pas lorsqu'on fait un set courant en continuous mode +finir zoom dans continous + diff --git a/shotmanager/features/greasepencil/greasepencil_operators.py b/shotmanager/features/greasepencil/greasepencil_operators.py index fa48b728..728db42d 100644 --- a/shotmanager/features/greasepencil/greasepencil_operators.py +++ b/shotmanager/features/greasepencil/greasepencil_operators.py @@ -598,7 +598,11 @@ class UAS_ShotManager_GreasePencilSelectAndDraw(Operator): bl_options = {"INTERNAL", "UNDO"} index: IntProperty(default=0) + + # can be SELECT, DRAW mode: StringProperty(default="SELECT") + + # can be DO_NOTHING, SELECT, SELECT_AND_DRAW, ADD_TO_SELECTION action: StringProperty(default="DO_NOTHING") ignoreSetCurrentShot: BoolProperty(default=False) toggleDrawEditing: BoolProperty(default=False) diff --git a/shotmanager/keymaps/general_keymaps.py b/shotmanager/keymaps/general_keymaps.py index 6e8438d9..f21dae89 100644 --- a/shotmanager/keymaps/general_keymaps.py +++ b/shotmanager/keymaps/general_keymaps.py @@ -76,6 +76,14 @@ def registerKeymaps(): # kmi = km.keymap_items.new("uas_shot_manager.display_overlay_tools", type="NONE", value="PRESS") # keymaps.append((km, kmi)) + # toggle storyboard frame draw mode + ############################### + + # VIEW_3D works also for timeline + km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + kmi = km.keymap_items.new("uas_shot_manager.stb_frame_drawing", type="X", value="PRESS", ctrl=True, shift=True) + keymaps.append((km, kmi)) + def unregisterKeymaps(): keymaps = config.gAddonKeymaps diff --git a/shotmanager/keymaps/playbar_wrappers_operators.py b/shotmanager/keymaps/playbar_wrappers_operators.py index afe03ef8..5678ac05 100644 --- a/shotmanager/keymaps/playbar_wrappers_operators.py +++ b/shotmanager/keymaps/playbar_wrappers_operators.py @@ -23,7 +23,7 @@ import bpy from bpy.types import Operator -from bpy.props import BoolProperty, StringProperty +from bpy.props import StringProperty from shotmanager import config from shotmanager.config import sm_logging diff --git a/shotmanager/operators/general.py b/shotmanager/operators/general.py index f879de67..4a260c26 100644 --- a/shotmanager/operators/general.py +++ b/shotmanager/operators/general.py @@ -24,11 +24,11 @@ from bpy.props import BoolProperty, StringProperty, IntProperty from shotmanager.utils import utils +from shotmanager.utils import utils_greasepencil from shotmanager.operators.shots import convertMarkersFromCameraBindingToShots from shotmanager.utils.utils import getSceneVSE, convertVersionIntToStr from shotmanager.utils.utils_markers import clearMarkersFromCameraBinding - from shotmanager.config import config from shotmanager.config import sm_logging @@ -45,7 +45,7 @@ class UAS_ShotManager_OT_ShotsPlayMode(Operator): bl_description = "Enable / disable the Shots Play Mode" bl_options = {"INTERNAL"} - def invoke(self, context, event): + def execute(self, context): context.window_manager.UAS_shot_manager_shots_play_mode = ( not context.window_manager.UAS_shot_manager_shots_play_mode ) @@ -119,6 +119,30 @@ def execute(self, context): return {"FINISHED"} +class UAS_ShotManager_OT_StbFrameDrawing(Operator): + bl_idname = "uas_shot_manager.stb_frame_drawing" + bl_label = "Ubisoft Shot Mng - Toggle Storyboard Frame Draw Mode" + bl_description = "Enable / disable the Storyboard Frame Draw Mode" + bl_options = {"INTERNAL"} + + def execute(self, context): + props = context.scene.UAS_shot_manager_props + + if props.getEditedGPShot() is not None: + utils_greasepencil.switchToObjectMode() + else: + currentShotInd = props.getCurrentShotIndex() + if -1 != currentShotInd and props.isContinuousGPEditingModeActive(): + # context.window_manager.UAS_shot_manager_shots_play_mode = ( + # not context.window_manager.UAS_shot_manager_shots_play_mode + # ) + bpy.ops.uas_shot_manager.greasepencil_select_and_draw( + action="SELECT_AND_DRAW", index=currentShotInd, toggleDrawEditing=True, mode="DRAW" + ) + + return {"FINISHED"} + + ################### # Warning buttons ################### @@ -461,6 +485,7 @@ def execute(self, context): UAS_ShotManager_OT_DisplayDisabledShotsInOverlays, UAS_ShotManager_OT_DisplayOverlayTools, UAS_ShotManager_OT_ChangeLayout, + UAS_ShotManager_OT_StbFrameDrawing, UAS_ShotManager_OT_TurnOffBurnIntoImage, UAS_ShotManager_OT_TurnOffPixelAspect, UAS_ShotManager_OT_SetFpsAsProjectFps, diff --git a/shotmanager/properties/props.py b/shotmanager/properties/props.py index f822323d..831fe800 100644 --- a/shotmanager/properties/props.py +++ b/shotmanager/properties/props.py @@ -1113,7 +1113,7 @@ def isContinuousGPEditingModeActive(self): """Return True if the context required to enter in a continuous drawing session is valid: the button has to be visible and checked. Note that the returned value is not dependent on the fact that a Grease Pencil object is being edited or not. - Query wkip for that. + Query props.getEditedGPShot() for that. Call this to get the state of continuous editing instead of directly using useContinuousGPEditing""" state = False From e4245f3e62fb6b6a322bca1d48a692c1f3b8dba1 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Fri, 30 Sep 2022 15:30:44 +0200 Subject: [PATCH 23/27] UI of Animated Frame Transformation to minimize ambiguities --- CHANGELOG.md | 4 + shotmanager/__init__.py | 2 +- shotmanager/config/dev_notes.txt | 5 + .../features/storyboard/storyboard_ui.py | 108 ++++++++++++------ 4 files changed, 81 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e7fba1..a5afe495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +----- +## 2.1.21 (2022-09-30) +- Revamped the UI of the Animated Frame Transformation to minimize UI ambiguities + ----- ## 2.1.20 (2022-09-29) - Several fixes related to the continuous draw mode diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index 70000a3d..2075b8ba 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 1, 20), + "version": (2, 1, 21), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", diff --git a/shotmanager/config/dev_notes.txt b/shotmanager/config/dev_notes.txt index 817ebc47..4f02c72a 100644 --- a/shotmanager/config/dev_notes.txt +++ b/shotmanager/config/dev_notes.txt @@ -216,3 +216,8 @@ rajouter bouton de framing shot a coté de gpu shift qui ne marche pas lorsqu'on fait un set courant en continuous mode finir zoom dans continous +mettre nom de canvas dont touch + +overlay desactive des qu'on sort du full scerenfull screnn (ctrl alt space) + + diff --git a/shotmanager/features/storyboard/storyboard_ui.py b/shotmanager/features/storyboard/storyboard_ui.py index 7f96d60d..77edda02 100644 --- a/shotmanager/features/storyboard/storyboard_ui.py +++ b/shotmanager/features/storyboard/storyboard_ui.py @@ -22,6 +22,7 @@ # from shotmanager.utils import utils from shotmanager.utils import utils_ui from shotmanager.utils import utils_greasepencil +from shotmanager.utils.utils_ui import propertyColumn from shotmanager.ui import sm_shots_global_settings_ui_cameras from shotmanager.ui import sm_shots_global_settings_ui_overlays @@ -330,36 +331,53 @@ def draw_greasepencil_shot_properties(layout, context, shot): depressedOp=not channelsLocked, ) if prefs.stb_anim_props_expanded: + lockSplitFactor = 0.9 - col.separator(factor=0.5) + def _drawAxisText(propsCol, axisText, warningMessage=False): + ZPropsRow = propsCol.row(align=True) + ZPropsRow.alignment = "RIGHT" + textPropsRow = ZPropsRow.row(align=True) + textPropsRow.alignment = "RIGHT" + textPropsRow.ui_units_x = 4 + if warningMessage: + textPropsRow.alert = True + textPropsRow.label(text="Don't Change !") + else: + textPropsRow.label(text=" ") + + axisPropsRow = ZPropsRow.row(align=True) + axisPropsRow.alignment = "RIGHT" + axisPropsRow.label(text=axisText) + col.separator(factor=0.5) animRow = col.row() animRow.separator(factor=2.0) animBox = animRow.box() - # canvasCol = animBox.column() - # animRow.enabled = not channelsLocked - transformCol = animBox.column() - transformRow = transformCol.row() - transformRow.separator(factor=hSepFactor) - - # transformRow = transformCol.row() - # transformRow.separator(factor=hSepFactor) - # # or prefs.shot_greasepencil_expanded: - - lockSplitFactor = 0.9 - # location ################### + locLocked = not (gp_child.lock_location[0] and gp_child.lock_location[1] and gp_child.lock_location[2]) + transformCol.label(text="Location:") transformRow = transformCol.row() - # transformRow.label(text="Location:") + transformRow.alignment = "CENTER" transformRow.use_property_split = True transformRow.use_property_decorate = True + propsCol = propertyColumn(transformRow) + propsCol.enabled = locLocked - split = transformRow.split(factor=lockSplitFactor) - split.prop(gp_child, "location") - splitRightCol = split.column() + _drawAxisText(propsCol, "X", warningMessage=False) + _drawAxisText(propsCol, "Y", warningMessage=False) + _drawAxisText(propsCol, "Z", warningMessage=True) + + rightRow = transformRow.row() + rightRow.alignment = "RIGHT" + + rightSubRow = rightRow.row() + transfoRow = rightSubRow.row() + transfoRow.enabled = locLocked + transfoRow.prop(gp_child, "location", text="") + splitRightCol = rightSubRow.column() subCol = splitRightCol.column(align=True) draw_lock_but(subCol, gp_child, "LOCK_LOCATION_X") @@ -368,17 +386,28 @@ def draw_greasepencil_shot_properties(layout, context, shot): # rotation ################### - - transformRow = transformCol.row() - transformRow.separator(factor=hSepFactor) - + transformCol.separator(factor=0.5) + rotLocked = not (gp_child.lock_rotation[0] and gp_child.lock_rotation[1] and gp_child.lock_rotation[2]) + transformCol.label(text="Rotation:") transformRow = transformCol.row() + transformRow.alignment = "CENTER" transformRow.use_property_split = True transformRow.use_property_decorate = True + propsCol = propertyColumn(transformRow) + propsCol.enabled = rotLocked - split = transformRow.split(factor=lockSplitFactor) - split.prop(gp_child, "rotation_euler") - splitRightCol = split.column() + _drawAxisText(propsCol, "X", warningMessage=True) + _drawAxisText(propsCol, "Y", warningMessage=True) + _drawAxisText(propsCol, "Z", warningMessage=False) + + rightRow = transformRow.row() + rightRow.alignment = "RIGHT" + + rightSubRow = rightRow.row() + transfoRow = rightSubRow.row() + transfoRow.enabled = rotLocked + transfoRow.prop(gp_child, "rotation_euler", text="") + splitRightCol = rightSubRow.column() subCol = splitRightCol.column(align=True) draw_lock_but(subCol, gp_child, "LOCK_ROTATION_X") @@ -387,29 +416,34 @@ def draw_greasepencil_shot_properties(layout, context, shot): # scale ################### - - transformRow = transformCol.row() - transformRow.separator(factor=hSepFactor) - + transformCol.separator(factor=0.5) + scaleLocked = not (gp_child.lock_scale[0] and gp_child.lock_scale[1] and gp_child.lock_scale[2]) + transformCol.label(text="Scale:") transformRow = transformCol.row() + transformRow.alignment = "CENTER" transformRow.use_property_split = True transformRow.use_property_decorate = True + propsCol = propertyColumn(transformRow) + propsCol.enabled = scaleLocked - split = transformRow.split(factor=lockSplitFactor) - split.prop(gp_child, "scale") - splitRightCol = split.column() + _drawAxisText(propsCol, "X", warningMessage=False) + _drawAxisText(propsCol, "Y", warningMessage=False) + _drawAxisText(propsCol, "Z", warningMessage=False) + + rightRow = transformRow.row() + rightRow.alignment = "RIGHT" + + rightSubRow = rightRow.row() + transfoRow = rightSubRow.row() + transfoRow.enabled = scaleLocked + transfoRow.prop(gp_child, "scale", text="") + splitRightCol = rightSubRow.column() subCol = splitRightCol.column(align=True) draw_lock_but(subCol, gp_child, "LOCK_SCALE_X") draw_lock_but(subCol, gp_child, "LOCK_SCALE_Y") draw_lock_but(subCol, gp_child, "LOCK_SCALE_Z") - # transformRow = col.row() - # transformRow.label(text="test") - # transformRow.use_property_split = True - # transformRow.use_property_decorate = True - # transformRow.prop(gp_child.location, "x") - transformRow = transformCol.row() transformRow.separator(factor=hSepFactor) From 324f29f3bd8aaf292489fbb8d72534460b929c0d Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Sat, 1 Oct 2022 13:46:10 +0200 Subject: [PATCH 24/27] Fix keymap on addonpanel --- shotmanager/__init__.py | 2 +- shotmanager/config/config.py | 2 +- shotmanager/config/dev_notes.txt | 2 -- shotmanager/keymaps/general_keymaps.py | 13 +++++++++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index 2075b8ba..320b2b33 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 1, 21), + "version": (2, 1, 22), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index 4ea4713c..72ada62d 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -43,7 +43,7 @@ def initGlobalVariables(): devDebug = False # change this value to force debug at start time - devDebug = False + devDebug = True global devDebug_displayDebugPanel devDebug_displayDebugPanel = True diff --git a/shotmanager/config/dev_notes.txt b/shotmanager/config/dev_notes.txt index 4f02c72a..9fb5e5a7 100644 --- a/shotmanager/config/dev_notes.txt +++ b/shotmanager/config/dev_notes.txt @@ -207,8 +207,6 @@ timer pour la sel des clips couleur pour les locked duration selection du clip qd il est trop petit et qu'on tape dans les handles -Bug: transform truc from paul - truc information message contextuel rajouter bouton de framing shot a coté de gpu diff --git a/shotmanager/keymaps/general_keymaps.py b/shotmanager/keymaps/general_keymaps.py index f21dae89..3a5cc88e 100644 --- a/shotmanager/keymaps/general_keymaps.py +++ b/shotmanager/keymaps/general_keymaps.py @@ -44,7 +44,8 @@ def registerKeymaps(): ############################### # VIEW_3D works also for timeline - km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + # NOTE: For the keymap to work also over the add-on panel, it has to use the name 3D View Generic, not just 3D View + km = wm.keyconfigs.addon.keymaps.new(name="3D View Generic", space_type="VIEW_3D") kmi = km.keymap_items.new("uas_shot_manager.shots_play_mode", type="SPACE", value="PRESS", alt=True) keymaps.append((km, kmi)) @@ -80,7 +81,15 @@ def registerKeymaps(): ############################### # VIEW_3D works also for timeline - km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") + km = wm.keyconfigs.addon.keymaps.new(name="3D View Generic", space_type="VIEW_3D", tool=False) + kmi = km.keymap_items.new("uas_shot_manager.stb_frame_drawing", type="X", value="PRESS", ctrl=True, shift=True) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") + kmi = km.keymap_items.new("uas_shot_manager.stb_frame_drawing", type="X", value="PRESS", ctrl=True, shift=True) + keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") kmi = km.keymap_items.new("uas_shot_manager.stb_frame_drawing", type="X", value="PRESS", ctrl=True, shift=True) keymaps.append((km, kmi)) From 4ef2286306c5f32e2608e29302c61a60629d147e Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Sat, 1 Oct 2022 21:43:07 +0200 Subject: [PATCH 25/27] New frame time range button --- CHANGELOG.md | 8 ++ shotmanager/__init__.py | 2 +- shotmanager/operators/shots_toolbar.py | 5 +- .../shots_stack_toolbar.py | 3 +- .../frame_range/frame_range_operators.py | 108 ++++++++++++++++-- 5 files changed, 113 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5afe495..04e8647a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +----- +## 2.1.23 (2022-10-01) +- Added a button to set the time range to shot or take range in the Frame Range toolbar + +----- +## 2.1.22 (2022-10-01) +- Fixed keymap for draw mode to work when the mouve is over the add-on panel + ----- ## 2.1.21 (2022-09-30) - Revamped the UI of the Animated Frame Transformation to minimize UI ambiguities diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index 320b2b33..d8933a83 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 1, 22), + "version": (2, 1, 23), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", diff --git a/shotmanager/operators/shots_toolbar.py b/shotmanager/operators/shots_toolbar.py index c4a3f236..3b3d7505 100644 --- a/shotmanager/operators/shots_toolbar.py +++ b/shotmanager/operators/shots_toolbar.py @@ -21,6 +21,7 @@ import bpy from bpy.types import Operator +from bpy.props import FloatProperty from shotmanager.config import config @@ -106,6 +107,8 @@ class UAS_ShotManager_SceneRangeFromTake(Operator): bl_description = "Set scene time range with take range" "\n+ Alt: Set the preview time range" bl_options = {"INTERNAL"} + spacerPercent: FloatProperty(default=5) + def invoke(self, context, event): scene = context.scene props = scene.UAS_shot_manager_props @@ -126,7 +129,7 @@ def invoke(self, context, event): scene.frame_start = currentTake.getMinFrame(ignoreDisabled=False) scene.frame_end = currentTake.getMaxFrame(ignoreDisabled=False) - bpy.ops.uas_shot_manager.frame_time_range(spacerPercent=5) + bpy.ops.uas_shot_manager.frame_time_range(spacerPercent=self.spacerPercent) return {"FINISHED"} diff --git a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_toolbar.py b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_toolbar.py index 0a5b1c88..4944d952 100644 --- a/shotmanager/overlay_tools/interact_shots_stack/shots_stack_toolbar.py +++ b/shotmanager/overlay_tools/interact_shots_stack/shots_stack_toolbar.py @@ -67,9 +67,8 @@ def draw_shots_stack_toolbar_in_editor(self, context): icon_value=icon.icon_id, ) - row.separator(factor=0.8) - if config.devDebug: + row.separator(factor=0.8) # row.operator( # "uas_gpu.dopesheet_gpu_sample", # text="GPU", diff --git a/shotmanager/tools/frame_range/frame_range_operators.py b/shotmanager/tools/frame_range/frame_range_operators.py index 3f28d248..940cc2c6 100644 --- a/shotmanager/tools/frame_range/frame_range_operators.py +++ b/shotmanager/tools/frame_range/frame_range_operators.py @@ -21,7 +21,7 @@ import bpy from bpy.types import Operator -from bpy.props import FloatProperty +from bpy.props import FloatProperty, StringProperty, IntProperty from shotmanager.utils.utils_time import zoom_dopesheet_view_to_range from shotmanager.utils import utils_ui @@ -75,6 +75,8 @@ class UAS_ShotManager_FrameTimeRange(Operator): bl_description = "Change the VSE zoom value to fit the scene time range" bl_options = {"REGISTER"} + start: IntProperty(description="Time start", default=-100000) + end: IntProperty(description="Time end", default=-100000) spacerPercent: FloatProperty( description="Range of time, in percentage, before and after the time range", min=0.0, max=40.0, default=5 ) @@ -83,18 +85,104 @@ class UAS_ShotManager_FrameTimeRange(Operator): def poll(cls, context): return config.getShotManagerPrefs().display_frame_range_tool - # def invoke(self, context, event): - # props = context.scene.UAS_shot_manager_props - - # return {"FINISHED"} - def execute(self, context): - # NOTE: possibility to use the optional parameter changeTime: to prevent current time to be changed + currentFrame = context.scene.frame_current + recenterCurrentFrame = False if context.scene.use_preview_range: - zoom_dopesheet_view_to_range(context, context.scene.frame_preview_start, context.scene.frame_preview_end) + rangeStart = context.scene.frame_preview_start if self.start < -10000 else self.start + rangeEnd = context.scene.frame_preview_end if self.end < -10000 else self.end else: - zoom_dopesheet_view_to_range(context, context.scene.frame_start, context.scene.frame_end) + rangeStart = context.scene.frame_start if self.start < -10000 else self.start + rangeEnd = context.scene.frame_end if self.end < -10000 else self.end + + if not rangeStart <= currentFrame <= rangeEnd: + recenterCurrentFrame = True + + # NOTE: possibility to use the optional parameter changeTime: to prevent current time to be changed + zoom_dopesheet_view_to_range(context, rangeStart, rangeEnd, changeTime=recenterCurrentFrame) utils_ui.redrawAll(context) + + return {"FINISHED"} + + +class UAS_ShotManager_FrameTimeRangeFromShot(Operator): + bl_idname = "uas_shot_manager.frame_time_range_from_shot" + bl_label = "Set scene time range with current SHOT range and zoom on it" + bl_description = ( + "\n+ Ctrl: Zoom on SHOT range without changing time range" + "\n+ Shift: Set scene time range with current TAKE range and zoom on it" + "\n+ Ctrl + Shift: Zoom on TAKE range without changing time range" + "\n+ Alt: Set the preview time range" + ) + bl_options = {"INTERNAL"} + + @classmethod + def poll(cls, context): + return config.getShotManagerPrefs().display_frame_range_tool + + action: StringProperty(default="DO_NOTHING") + spacerPercent: FloatProperty(default=35) + + def invoke(self, context, event): + self.action = "DO_NOTHING" + + # if not event.ctrl and not event.shift and not event.alt: + # self.action = "CURRENT" + # elif not event.ctrl and event.shift and not event.alt: + # self.action = "ALL" + + # this is computed when the operator is called without a specified action + if "DO_NOTHING" == self.action: + if event.ctrl and not event.shift and not event.alt: + self.action = "SHOT" + elif not event.ctrl and not event.shift: + self.action = "SHOT_TIMERANGE" + if event.alt: + self.action += "_PREVIEW" + + elif event.ctrl and event.shift and not event.alt: + self.action = "TAKE" + elif not event.ctrl and event.shift: + self.action = "TAKE_TIMERANGE" + if event.alt: + self.action += "_PREVIEW" + + if "DO_NOTHING" == self.action: + return {"FINISHED"} + return self.execute(context) + + def execute(self, context): + scene = context.scene + props = scene.UAS_shot_manager_props + currentShot = props.getCurrentShot() + + if currentShot: + if "SHOT" in self.action: + rangeStart = currentShot.start + rangeEnd = currentShot.end + bpy.ops.uas_shot_manager.frame_time_range( + start=rangeStart, end=rangeEnd, spacerPercent=self.spacerPercent + ) + elif "TAKE" in self.action: + currentTake = props.getCurrentTake() + if currentTake and 0 < currentTake.getNumShots(ignoreDisabled=True): + rangeStart = currentTake.getMinFrame(ignoreDisabled=False) + rangeEnd = currentTake.getMaxFrame(ignoreDisabled=False) + bpy.ops.uas_shot_manager.frame_time_range( + start=rangeStart, end=rangeEnd, spacerPercent=self.spacerPercent + ) + + if "TIMERANGE" in self.action: + # preview time range + if "PREVIEW" in self.action: + scene.use_preview_range = True + scene.frame_preview_start = rangeStart + scene.frame_preview_end = rangeEnd + else: + scene.use_preview_range = False + scene.frame_start = rangeStart + scene.frame_end = rangeEnd + return {"FINISHED"} @@ -107,6 +195,7 @@ def draw_frame_range_tool_in_editor(self, context): # row.operator("bpy.ops.time.view_all") row.operator("uas_shot_manager.set_time_range_start", text="", icon="TRIA_UP_BAR") row.operator("uas_shot_manager.frame_time_range", text="", icon="CENTER_ONLY") + row.operator("uas_shot_manager.frame_time_range_from_shot", text="", icon="PREVIEW_RANGE") row.operator("uas_shot_manager.set_time_range_end", text="", icon="TRIA_UP_BAR") @@ -121,6 +210,7 @@ def display_frame_range_in_editor(display_value): UAS_ShotManager_SetTimeRangeStart, UAS_ShotManager_SetTimeRangeEnd, UAS_ShotManager_FrameTimeRange, + UAS_ShotManager_FrameTimeRangeFromShot, ) From 7037ab637117ea9c4ddaae9e5897f9ad67f74ccc Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Fri, 7 Oct 2022 17:07:19 +0200 Subject: [PATCH 26/27] API improvements + button eiting --- CHANGELOG.md | 2 + doc/shot_manager_api.md | 17 ++ shotmanager/__init__.py | 2 +- .../api_export_shots_to_markers.py | 47 +++++ .../api/api_code_samples/api_first_steps.py | 185 ++++++++++++++++++ .../api/api_code_samples/api_otio_samples.py | 51 +++++ .../api/api_code_samples/api_rrs_samples.py | 61 ++++++ shotmanager/api/otio.py | 23 ++- shotmanager/api/rrs.py | 24 +-- shotmanager/api/shot.py | 58 +++--- shotmanager/api/shot_manager.py | 140 +++++++------ shotmanager/api/take.py | 31 ++- shotmanager/config/dev_notes.txt | 9 +- .../greasepencil/greasepencil_operators.py | 6 + shotmanager/operators/shots.py | 34 +++- shotmanager/properties/props.py | 10 + shotmanager/ui/sm_ui.py | 23 ++- shotmanager/utils/utils_markers.py | 1 + 18 files changed, 596 insertions(+), 128 deletions(-) create mode 100644 doc/shot_manager_api.md create mode 100644 shotmanager/api/api_code_samples/api_export_shots_to_markers.py create mode 100644 shotmanager/api/api_code_samples/api_first_steps.py create mode 100644 shotmanager/api/api_code_samples/api_otio_samples.py create mode 100644 shotmanager/api/api_code_samples/api_rrs_samples.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e8647a..4629d545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ----- ## 2.1.23 (2022-10-01) - Added a button to set the time range to shot or take range in the Frame Range toolbar +- Improved the Storyborad Frame Editing Mode button +- Improved the API (documentation, samples, code) ----- ## 2.1.22 (2022-10-01) diff --git a/doc/shot_manager_api.md b/doc/shot_manager_api.md new file mode 100644 index 00000000..b0e3fe59 --- /dev/null +++ b/doc/shot_manager_api.md @@ -0,0 +1,17 @@ +Shot Manager Python API +Main concepts +The add-on Shot Manager is made of 2 parts: + +the Shot Manager itself (or SM), located in a 3D workspace and used to define and manipulate all the camera shots in a scene; +the Video Shot Manager (or VSM), located in a Video Sequence Editor workspace, it is dedicated to check the videos and exports (experimental at the moment) +Both parts are instanced located in a scene. There can be only one (or zero) instance of them in a given scene. The VSM needs a SM (and necesseraly have) in its owning scene. + +Shot Manager +The Shot Manager is the main property class. Basically it contains a set of takes (CollectionProperty) and each take has a set of shots (CollectionProperty). + +The functions available in the API are spread in several files, in a logical and object-oriented approach, according to the manipulated entities. They are not classes though, just C-like functions. + +In Shot Manager the UI and functionnal part are - as much as possible - separated. 2 properties are a bit inbetween though: the current shot and the selected shot (they are indices, not pointers to the shot instances). The current shot is more than a UI information since the concerned shot has a special behavior (its camera is the one used by the scene for example). The selected shot refers to the highlighted item in the shots list (which is also the take). Many actions are based on it so it has also to be considered as a functional information. In spite of that the add-on can be completely manipulated without the use of the interface, and takes that are not set as the current one can be changed in exaclty the same way as the current one. + +Video Shot Manager +The VSM is currently not exposed in the API. \ No newline at end of file diff --git a/shotmanager/__init__.py b/shotmanager/__init__.py index d8933a83..35aec1f5 100644 --- a/shotmanager/__init__.py +++ b/shotmanager/__init__.py @@ -84,7 +84,7 @@ "author": "Ubisoft - Julien Blervaque (aka Werwack), Romain Carriquiry Borchiari", "description": "Easily manage shots and cameras in the 3D View and see the resulting edit in real-time", "blender": (3, 1, 0), - "version": (2, 1, 23), + "version": (2, 1, 24), "location": "View3D > Shot Mng", "doc_url": "https://ubisoft-shotmanager.readthedocs.io", "tracker_url": "https://github.com/ubisoft/shotmanager/issues", diff --git a/shotmanager/api/api_code_samples/api_export_shots_to_markers.py b/shotmanager/api/api_code_samples/api_export_shots_to_markers.py new file mode 100644 index 00000000..93d58d02 --- /dev/null +++ b/shotmanager/api/api_code_samples/api_export_shots_to_markers.py @@ -0,0 +1,47 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Shot Manager API Sample - Exporting shots as camera bound markers +""" + +import bpy + +from shotmanager.api import shot_manager +from shotmanager.api import shot +from shotmanager.utils.utils_markers import addMarkerAtFrame + + +# Get the shot manager instance of the scene +sm_props = shot_manager.get_shot_manager(bpy.context.scene) + +# Initialyse shot manager instance to create, amonst other things, a default take +shot_manager.initialize_shot_manager(sm_props) + +# Get the list of all the enabled shots +shots = shot_manager.get_shots_list(sm_props, ignore_disabled=True) +print("\nEnabled shots number: ", len(shots)) +for s in shots: + print(" - ", s.name) + + if shot.is_camera_valid(s): + # marker_name = s.name + marker_name = shot.get_name(s) + m = addMarkerAtFrame(bpy.context.scene, shot.get_start(s), marker_name) + m.camera = shot.get_camera(s) + else: + print(f"*** Shot {s.name} has an invalid camera - No marker created") diff --git a/shotmanager/api/api_code_samples/api_first_steps.py b/shotmanager/api/api_code_samples/api_first_steps.py new file mode 100644 index 00000000..9537a84b --- /dev/null +++ b/shotmanager/api/api_code_samples/api_first_steps.py @@ -0,0 +1,185 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Shot Manager API samples - First steps + +Documentation and general concepts: https://github.com/ubisoft/shotmanager/blob/main/doc/shot_manager_api.md +""" + + +import bpy + +from shotmanager.api import shot_manager +from shotmanager.api import shot +from shotmanager.api import take + + +############################# +# Initialisation +############################# + +# Create an instance of Shot Manager: No need to create it, it is created automatically in each new scene when the add-on is enabled. + +# Get the shot manager instance of the scene +sm_props = shot_manager.get_shot_manager(bpy.context.scene) + +# Initialyse shot manager instance to create, amonst other things, a default take +shot_manager.initialize_shot_manager(sm_props) + +# Possible verification that the parent scene of this instance is the scan +print("Current scene name: ", bpy.context.scene.name) +sm_scene = shot_manager.get_parent_scene(sm_props) +print("SM scene name: ", sm_scene.name) + + +############################# +# Takes manipulation +############################# + + +# Get current take +current_take = shot_manager.get_current_take(sm_props) +print("Current take name: ", current_take.name) +print("Current take name (path compliant): ", take.get_name_path_compliant(current_take)) + +take_index = shot_manager.get_take_index(sm_props, current_take) +print("Current take index: ", take_index) + +# Create another take at the end of the list +my_other_take = shot_manager.add_take(sm_props, at_index=-1, name="My New Take") + +# Get the number of takes in the shot manager +num_takes = len(shot_manager.get_takes(sm_props)) +print("Number of takes: ", num_takes) + +# Get the index of the new take +my_other_take_index = shot_manager.get_take_index(sm_props, my_other_take) +print("New take index: ", my_other_take_index) + +# Make the new take the current one +shot_manager.set_current_take_by_index(sm_props, my_other_take_index) + +# Change take name +current_take = shot_manager.get_current_take(sm_props) +take.set_name(current_take, "My New Take Renamed") + + +# Duplicate take +# wkip to do + +# Delete take +# wkip to do + + +############################# +# Shots manipulation +############################# + + +# Add a new shot to the current take +my_first_shot = shot_manager.add_shot( + sm_props, + at_index=-1, # will be added at the end + take_index=-1, # will be added to the current take (the new renamed one) + name="Hello World Shot", + start=15, # avoid using a short start value before the lenght of the handles (which is 10) + end=50, + camera=None, + color=(0.2, 0.6, 0.8, 1), + enabled=True, +) + +# Get shot name +print("Shot name: ", shot.get_name(my_first_shot)) + +# Rename shot +shot.set_name(my_first_shot, "My First Shot Renamed") +print("Shot name: ", shot.get_name(my_first_shot)) + +# Change shot start and end +# Remember that the end frame is INCLUDED in the range +shot.set_start(my_first_shot, 34) +shot.set_end(my_first_shot, 67) +print("Shot duration:", shot.get_duration(my_first_shot)) + +# Enable or disable the shot +print("Shot enable state:", shot.get_enable_state(my_first_shot)) +shot.set_enable_state(my_first_shot, False) +print("Shot new enable state:", shot.get_enable_state(my_first_shot)) + +# Create 2 cameras +cam = bpy.data.cameras.new("Camera01") +cam_ob = bpy.data.objects.new(cam.name, cam) +bpy.context.collection.objects.link(cam_ob) +bpy.data.cameras[cam.name].lens = 40 +cam_ob.location = (0.0, 0.0, 0.0) + +cam_shot_1 = cam_ob + +cam = bpy.data.cameras.new("Camera02") +cam_ob = bpy.data.objects.new(cam.name, cam) +bpy.context.collection.objects.link(cam_ob) +bpy.data.cameras[cam.name].lens = 50 +cam_ob.location = (2.0, 0.0, 0.0) + +cam_shot_2 = cam_ob + +# Assign the first camera to the shot +shot.set_camera(my_first_shot, cam_shot_1) + +# Create another shot with a camera, before the first one +my_second_shot = shot_manager.add_shot( + sm_props, + at_index=1, + name="My Second Shot", + start=80, + end=98, + camera=cam_shot_2, + color=(0.1, 0.8, 0.3, 1), +) + +# And another one +my_third_shot = shot_manager.add_shot( + sm_props, + at_index=1, + name="My third Shot", + start=62, + end=120, + camera=cam_shot_2, + color=(0.9, 0.2, 0.3, 1), +) + +# Get the number of takes +shots = shot_manager.get_shots(sm_props) +print("\nShots number: ", len(shots)) +for s in shots: + print(" - ", s.name) + +# Make this new shot the current one +shot_manager.set_current_shot(sm_props, my_second_shot) + +# Get the list of all the enabled shots +shots = shot_manager.get_shots_list(sm_props, ignore_disabled=True) +print("\nEnabled shots number: ", len(shots)) +for s in shots: + print(" - ", s.name) + + +# Delete first added shot +my_first_shot = shot_manager.get_shot(sm_props, 1) +shot_manager.remove_shot(sm_props, my_first_shot) diff --git a/shotmanager/api/api_code_samples/api_otio_samples.py b/shotmanager/api/api_code_samples/api_otio_samples.py new file mode 100644 index 00000000..48a72fc1 --- /dev/null +++ b/shotmanager/api/api_code_samples/api_otio_samples.py @@ -0,0 +1,51 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Shot Manager API samples - Otio examples + +Documentation and general concepts: https://github.com/ubisoft/shotmanager/blob/main/doc/shot_manager_api.md +""" + + +import bpy +import os + +from shotmanager.api import shot_manager +from shotmanager.api import otio + +# This should work with any scene where a Shot Manager instance has been instanced and where +# there are several shots +# In case a scene is required you can use this one from the GitLab: +# https://gitlab-ncsa.ubisoft.org/animation-studio/blender/shotmanager-addon/-/blob/master/fixtures/shots_sceneRunningRabbid/shots_sceneRunningRabbid.blend + +# Get the shot manager instance of the scene +sm_props = shot_manager.get_shot_manager(bpy.context.scene) + +# Get current file path +current_file = bpy.data.filepath +current_dir = os.path.dirname(current_file) + +if "" == current_file: + print("*** Save the scene first ***") + +# Export otio file from the specified scene to the directory containing the current file +# If file_name is left to default then the rendered file will be a .xml +print("Exporting Otio File to: ", current_dir) +otio.export_otio( + sm_props, file_path=current_dir, file_name="myOtioFile.otio", add_take_name_to_path=False, take_index=-1 +) diff --git a/shotmanager/api/api_code_samples/api_rrs_samples.py b/shotmanager/api/api_code_samples/api_rrs_samples.py new file mode 100644 index 00000000..2eb5333f --- /dev/null +++ b/shotmanager/api/api_code_samples/api_rrs_samples.py @@ -0,0 +1,61 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Shot Manager API samples - RRS examples + +Documentation and general concepts: https://github.com/ubisoft/shotmanager/blob/main/doc/shot_manager_api.md +""" + +import bpy +import os + +from shotmanager.api import rrs + +# This should work with any scene where a Shot Manager instance has been instanced and where +# there are several shots +# In case a scene is required you can use this one from the GitLab: +# https://gitlab-ncsa.ubisoft.org/animation-studio/blender/shotmanager-addon/-/blob/master/fixtures/shots_sceneRunningRabbid/shots_sceneRunningRabbid.blend + + +# Get current file path +current_file = bpy.data.filepath +current_dir = os.path.dirname(current_file) + +if "" == current_file: + print("*** Save the scene first ***") + + +# Initialize the shot manager of the current scene with the project environment variables +rrs.initialize_for_rrs(override_existing=True, verbose=True) + +# launch the rendering process of the shots from the current take and get a dictionary of +# the rendered and failed files +# The dictionary have the following entries: rendered_files, failed_files, otio_file +files_dico = rrs.publishRRS(current_dir, take_index=-1, verbose=True, use_cache=False) + +print("\nRendered files: ", len(files_dico["rendered_files"])) +for f in files_dico["rendered_files"]: + print(" - ", f) + +print("\nFailed files: ", len(files_dico["failed_files"])) +for f in files_dico["failed_files"]: + print(" - ", f) + +print("\Otio files: ", len(files_dico["otio_file"])) +for f in files_dico["otio_file"]: + print(" - ", f) diff --git a/shotmanager/api/otio.py b/shotmanager/api/otio.py index 62478f22..cebe3ff8 100644 --- a/shotmanager/api/otio.py +++ b/shotmanager/api/otio.py @@ -16,20 +16,29 @@ # along with this program. If not, see . """ -To do: module description here. +Shot Manager API - Interface with Otio """ from shotmanager.utils.utils_os import module_can_be_imported +from shotmanager.properties.props import UAS_ShotManager_Props + from shotmanager.config import sm_logging _logger = sm_logging.getLogger(__name__) -def export_otio(shot_manager, file_path="", file_name="", add_take_name_to_path=False, take_index=-1, fps=-1): - """ Create an OpenTimelineIO XML file for the specified take - Return the file path of the created file - If file_name is left to default then the rendered file will be a .xml +def export_otio( + shot_manager: UAS_ShotManager_Props, + file_path: str = "", + file_name: str = "", + add_take_name_to_path: bool = False, + take_index: int = -1, + fps: float = -1, +): + """Create an OpenTimelineIO XML file for the specified take + Return the file path of the created file + If file_name is left to default then the rendered file will be a .xml """ if module_can_be_imported("shotmanager.otio"): from shotmanager.otio import exports @@ -47,7 +56,3 @@ def export_otio(shot_manager, file_path="", file_name="", add_take_name_to_path= _logger.error("Otio module not available (no OpenTimelineIO)") res = None return res - - -# wkip to do: add import otio - diff --git a/shotmanager/api/rrs.py b/shotmanager/api/rrs.py index 3a1c2239..3166a631 100644 --- a/shotmanager/api/rrs.py +++ b/shotmanager/api/rrs.py @@ -16,7 +16,7 @@ # along with this program. If not, see . """ -To do: module description here. +Shot Manager API - Interface with RRS Publishing """ from shotmanager.scripts.rrs import publish_rrs @@ -37,18 +37,18 @@ def publishRRS( settings_dict=None, ): """ - Arguments: - settings_dict: a dictionary with various custom settings + Arguments: + settings_dict: a dictionary with various custom settings - Return a dictionary with the rendered and the failed file paths - The dictionary have the following entries: - - rendered_files_in_cache: rendered files when cache is used - - failed_files_in_cache: failed files when cache is used - - edl_files_in_cache: edl files when cache is used - - rendered_files: rendered files (either from direct rendering or from copy from cache) - - failed_files: failed files (either from direct rendering or from copy from cache) - - edl_files: edl files - - other_files: json dumped file list + Return a dictionary with the rendered and the failed file paths + The dictionary have the following entries: + - rendered_files_in_cache: rendered files when cache is used + - failed_files_in_cache: failed files when cache is used + - edl_files_in_cache: edl files when cache is used + - rendered_files: rendered files (either from direct rendering or from copy from cache) + - failed_files: failed files (either from direct rendering or from copy from cache) + - edl_files: edl files + - other_files: json dumped file list """ return publish_rrs.publishRRS( prodFilePath, diff --git a/shotmanager/api/shot.py b/shotmanager/api/shot.py index 200e92ae..1a22b6a7 100644 --- a/shotmanager/api/shot.py +++ b/shotmanager/api/shot.py @@ -16,79 +16,89 @@ # along with this program. If not, see . """ -To do: module description here. +Shot Manager API - Interface with shot class """ -def get_shot_manager_owner(shot_instance): - """Return the shot manager properties instance the shot is belonging to. - """ +import bpy +from shotmanager.properties.shot import UAS_ShotManager_Shot + + +def get_shot_manager_owner(shot_instance: UAS_ShotManager_Shot): + """Return the shot manager properties instance the shot is belonging to.""" return shot_instance.shotManager() -def get_name(shot_instance): +def get_name(shot_instance: UAS_ShotManager_Shot) -> str: return shot_instance.name -def set_name(shot_instance, name): - """ Set a unique name to the shot - """ +def set_name(shot_instance: UAS_ShotManager_Shot, name: str): + """Set a unique name to the shot""" shot_instance.name = name -def get_name_path_compliant(shot_instance): +def get_name_path_compliant(shot_instance: UAS_ShotManager_Shot) -> str: return shot_instance.getName_PathCompliant() -def get_start(shot_instance): +def get_start(shot_instance: UAS_ShotManager_Shot) -> float: return shot_instance.start -def set_start(shot_instance, value): +def set_start(shot_instance: UAS_ShotManager_Shot, value: float): shot_instance.start = value -def get_end(shot_instance): +def get_end(shot_instance: UAS_ShotManager_Shot) -> float: return shot_instance.end -def set_end(shot_instance, value): +def set_end(shot_instance: UAS_ShotManager_Shot, value: float): shot_instance.end = value -def get_duration(shot_instance): - """ Returns the shot duration in frames - in Blender - and in Shot Manager - the last frame of the shot is included in the rendered images +def get_duration(shot_instance: UAS_ShotManager_Shot) -> float: + """Returns the shot duration in frames + in Blender - and in Shot Manager - the last frame of the shot is included in the rendered images """ return shot_instance.getDuration() -def get_color(shot_instance): +def get_color(shot_instance: UAS_ShotManager_Shot): return shot_instance.get_color() -def set_color(shot_instance, value): +def set_color(shot_instance: UAS_ShotManager_Shot, value: float): shot_instance.set_color(value) -def get_enable_state(shot_instance): +def get_enable_state(shot_instance: UAS_ShotManager_Shot) -> bool: return shot_instance.enabled -def set_enable_state(shot_instance, value): +def set_enable_state(shot_instance: UAS_ShotManager_Shot, value: float): shot_instance.enabled = value -def get_camera(shot_instance): +def is_camera_valid(shot_instance: UAS_ShotManager_Shot) -> bool: + """Sometimes a pointed camera can seem to work but the camera object doesn't exist anymore in the scene. + Return True if the camera is really there, False otherwise + Note: This doesn't change the camera attribute of the shot + """ + return shot_instance.isCameraValid() + + +def get_camera(shot_instance: UAS_ShotManager_Shot) -> bpy.types.Camera: return shot_instance.camera -def set_camera(shot_instance, camera): +def set_camera(shot_instance: UAS_ShotManager_Shot, camera: bpy.types.Camera): shot_instance.camera = camera -def get_edit_start(shot_instance, scene): +def get_edit_start(shot_instance: UAS_ShotManager_Shot, scene: bpy.types.Scene) -> float: return shot_instance.getEditStart(scene) -def get_edit_end(shot_instance, scene): +def get_edit_end(shot_instance: UAS_ShotManager_Shot, scene: bpy.types.Scene) -> float: return shot_instance.getEditEnd(scene) diff --git a/shotmanager/api/shot_manager.py b/shotmanager/api/shot_manager.py index 0f3699c8..96f55600 100644 --- a/shotmanager/api/shot_manager.py +++ b/shotmanager/api/shot_manager.py @@ -16,25 +16,28 @@ # along with this program. If not, see . """ -Shot Manager API +Shot Manager API - Interface with Shot Manager class """ import bpy +from shotmanager.properties.props import UAS_ShotManager_Props +from shotmanager.properties.take import UAS_ShotManager_Take +from shotmanager.properties.shot import UAS_ShotManager_Shot -def get_shot_manager(scene): +def get_shot_manager(scene: bpy.types.Scene): return scene.UAS_shot_manager_props -def initialize_shot_manager(shot_manager): +def initialize_shot_manager(shot_manager: UAS_ShotManager_Props): shot_manager.initialize_shot_manager() -def get_parent_scene(shot_manager): +def get_parent_scene(shot_manager: UAS_ShotManager_Props) -> bpy.types.Scene: return shot_manager.getParentScene() -def get_warnings(shot_manager): +def get_warnings(shot_manager: UAS_ShotManager_Props): """Check if some warnings are to be mentioned to the user/ A warning message can be on several lines when the separator \n is used. @@ -52,41 +55,41 @@ def get_warnings(shot_manager): ############# -def get_unique_take_name(shot_manager, name): +def get_unique_take_name(shot_manager: UAS_ShotManager_Props, name: str): return shot_manager.getUniqueTakeName(name) -def get_takes(shot_manager): +def get_takes(shot_manager: UAS_ShotManager_Props): return shot_manager.takes -def get_take_by_index(shot_manager, take_index): +def get_take_by_index(shot_manager: UAS_ShotManager_Props, take_index: int): """Return the take corresponding to the specified index""" return shot_manager.getTakeByIndex(take_index) -def get_take_index(shot_manager, take): +def get_take_index(shot_manager: UAS_ShotManager_Props, take: UAS_ShotManager_Take): return shot_manager.getTakeIndex(take) -def get_current_take_index(shot_manager): +def get_current_take_index(shot_manager: UAS_ShotManager_Props): return shot_manager.getCurrentTakeIndex() -def set_current_take_by_index(shot_manager, current_take_index): +def set_current_take_by_index(shot_manager: UAS_ShotManager_Props, current_take_index: int): shot_manager.setCurrentTakeByIndex(current_take_index) -def get_current_take(shot_manager): +def get_current_take(shot_manager: UAS_ShotManager_Props): return shot_manager.getCurrentTake() -def get_current_take_name(shot_manager): +def get_current_take_name(shot_manager: UAS_ShotManager_Props): """Return the name of the current take,""" return shot_manager.getCurrentTakeName() -def add_take(shot_manager, at_index=-1, name="New Take"): +def add_take(shot_manager: UAS_ShotManager_Props, at_index: int = -1, name: str = "New Take"): """Add a new take after the current take if possible or at the end of the take list otherwise Return the newly added take. If it is the only take of the Shot Manager and the project settings are not used then its name will be "Main Take" @@ -94,7 +97,9 @@ def add_take(shot_manager, at_index=-1, name="New Take"): return shot_manager.addTake(atIndex=at_index, name=name) -def copy_take(shot_manager, take, at_index=-1, copy_camera=False): +def copy_take( + shot_manager: UAS_ShotManager_Props, take: UAS_ShotManager_Take, at_index: int = -1, copy_camera: bool = False +): """Copy a take after the current take if possible or at the end of the takes list otherwise Return the newly added take """ @@ -106,20 +111,20 @@ def copy_take(shot_manager, take, at_index=-1, copy_camera=False): #################### -def get_unique_shot_name(shot_manager, name, take_index): +def get_unique_shot_name(shot_manager: UAS_ShotManager_Props, name: str, take_index: int): return shot_manager.getUniqueShotName(shot_manager, name, takeIndex=take_index) def add_shot( - shot_manager, - at_index=-1, - take_index=-1, - name="defaultShot", - start=10, - end=20, - camera=None, - color=(0.2, 0.6, 0.8, 1), - enabled=True, + shot_manager: UAS_ShotManager_Props, + at_index: int = -1, + take_index: int = -1, + name: str = "defaultShot", + start: float = 10, + end: float = 20, + camera: bpy.types.Camera = None, + color: tuple = (0.2, 0.6, 0.8, 1), + enabled: bool = True, ): """Add a new shot after the current shot if possible or at the end of the shot list otherwise (case of an add in a take that is not the current one) @@ -138,7 +143,13 @@ def add_shot( ) -def copy_shot(shot_manager, shot, at_index=-1, target_take_index=-1, copy_camera=False): +def copy_shot( + shot_manager: UAS_ShotManager_Props, + shot: UAS_ShotManager_Shot, + at_index: int = -1, + target_take_index: int = -1, + copy_camera: bool = False, +): """Copy a shot after the current shot if possible or at the end of the shot list otherwise (case of an add in a take that is not the current one) Return the newly added shot @@ -147,51 +158,53 @@ def copy_shot(shot_manager, shot, at_index=-1, target_take_index=-1, copy_camera return shot_manager.copyShot(shot, atIndex=at_index, targetTakeIndex=target_take_index, copyCamera=copy_camera) -def remove_shot(shot_manager, shot): +def remove_shot(shot_manager: UAS_ShotManager_Props, shot: UAS_ShotManager_Shot): shot_manager.removeShot(shot) -def move_shot_to_index(shot_manager, shot, new_index): +def move_shot_to_index(shot_manager: UAS_ShotManager_Props, shot: UAS_ShotManager_Shot, new_index: int): shot_manager.moveShotToIndex(shot, new_index) -def set_current_shot_by_index(shot_manager, current_shot_index): +def set_current_shot_by_index(shot_manager: UAS_ShotManager_Props, current_shot_index: int): """Changing the current shot doesn't affect the selected one""" return shot_manager.setCurrentShotByIndex(current_shot_index) -def set_current_shot(shot_manager, current_shot): +def set_current_shot(shot_manager: UAS_ShotManager_Props, current_shot: UAS_ShotManager_Shot): return shot_manager.setCurrentShot(current_shot) -def get_shot_index(shot_manager, shot, take_index=-1): +def get_shot_index(shot_manager: UAS_ShotManager_Props, shot: UAS_ShotManager_Shot, take_index: int = -1): return shot_manager.getShotIndex(shot, takeIndex=take_index) -def get_shot(shot_manager, shot_index, ignore_disabled=False, take_index=-1): +def get_shot(shot_manager: UAS_ShotManager_Props, shot_index: int, ignore_disabled: bool = False, take_index: int = -1): return shot_manager.getShotByIndex(shot_index, ignoreDisabled=ignore_disabled, takeIndex=take_index) -def get_shot_by_name(shot_manager, shot_name, ignore_disabled=False, takeIndex=-1): +def get_shot_by_name( + shot_manager: UAS_ShotManager_Props, shot_name: str, ignore_disabled: bool = False, takeIndex: int = -1 +): return shot_manager.getShotByName(shot_name, ignoreDisabled=ignore_disabled, takeIndex=takeIndex) -def get_shots(shot_manager, take_index=-1): +def get_shots(shot_manager: UAS_ShotManager_Props, take_index: int = -1): """Return the actual shots array of the specified take""" return shot_manager.get_shots(takeIndex=take_index) -def get_shots_list(shot_manager, ignore_disabled=False, take_index=-1): +def get_shots_list(shot_manager: UAS_ShotManager_Props, ignore_disabled: bool = False, take_index: int = -1): """Return a filtered shots array of the specified take""" return shot_manager.getShotsList(ignoreDisabled=ignore_disabled, takeIndex=take_index) -def get_num_shots(shot_manager, ignore_disabled=False, take_index=-1): +def get_num_shots(shot_manager: UAS_ShotManager_Props, ignore_disabled: bool = False, take_index: int = -1): """Return the number of shots of the specified take""" return shot_manager.getNumShots(ignoreDisabled=ignore_disabled, takeIndex=take_index) -def get_current_shot_index(shot_manager, ignore_disabled=False, take_index=-1): +def get_current_shot_index(shot_manager: UAS_ShotManager_Props, ignore_disabled: bool = False, take_index: int = -1): """Return the index of the current shot in the enabled shot list of the current take Use this function instead of a direct call to shot_manager.current_shot_index @@ -204,11 +217,11 @@ def get_current_shot_index(shot_manager, ignore_disabled=False, take_index=-1): return shot_manager.getCurrentShotIndex(ignoreDisabled=ignore_disabled, takeIndex=take_index) -def get_current_shot(shot_manager): +def get_current_shot(shot_manager: UAS_ShotManager_Props): return shot_manager.getCurrentShot() -def get_first_shot_index(shot_manager, ignore_disabled=False, take_index=-1): +def get_first_shot_index(shot_manager: UAS_ShotManager_Props, ignore_disabled: bool = False, take_index: int = -1): return shot_manager.getFirstShotIndex(ignoreDisabled=ignore_disabled, takeIndex=take_index) @@ -221,31 +234,31 @@ def get_first_shot_index(shot_manager, ignore_disabled=False, take_index=-1): # currentIndexInEnabledList -= 1 # return currentIndexInEnabledList -def get_last_shot_index(shot_manager, ignore_disabled=False, take_index=-1): +def get_last_shot_index(shot_manager: UAS_ShotManager_Props, ignore_disabled: bool = False, take_index: int = -1): return shot_manager.getLastShotIndex(ignoreDisabled=ignore_disabled, takeIndex=take_index) -def get_first_shot(shot_manager, ignore_disabled=False, take_index=-1): +def get_first_shot(shot_manager: UAS_ShotManager_Props, ignore_disabled: bool = False, take_index: int = -1): return shot_manager.getFirstShot(ignoreDisabled=ignore_disabled, takeIndex=take_index) -def get_last_shot(shot_manager, ignore_disabled=False, take_index=-1): +def get_last_shot(shot_manager: UAS_ShotManager_Props, ignore_disabled: bool = False, take_index: int = -1): return shot_manager.getLastShot(ignoreDisabled=ignore_disabled, takeIndex=take_index) # currentShotIndex is given in the WHOLE list of shots (including disabled) # returns the index of the previous enabled shot in the WHOLE list, -1 if none -def get_previous_enabled_shot_index(shot_manager, current_shot_index, take_index=-1): +def get_previous_enabled_shot_index(shot_manager: UAS_ShotManager_Props, current_shot_index: int, take_index: int = -1): return shot_manager.getPreviousEnabledShotIndex(current_shot_index, takeIndex=take_index) # currentShotIndex is given in the WHOLE list of shots (including disabled) # returns the index of the next enabled shot in the WHOLE list, -1 if none -def get_next_enabled_shot_index(shot_manager, current_shot_index, take_index=-1): +def get_next_enabled_shot_index(shot_manager: UAS_ShotManager_Props, current_shot_index: int, take_index: int = -1): return shot_manager.getNextEnabledShotIndex(current_shot_index, takeIndex=take_index) -def delete_shot_camera(shot_manager, shot): +def delete_shot_camera(shot_manager: UAS_ShotManager_Props, shot: UAS_ShotManager_Shot): """Check in all takes if the camera is used by another shot and if not then delete it""" return shot_manager.deleteShotCamera(shot) @@ -255,7 +268,7 @@ def delete_shot_camera(shot_manager, shot): ############### -def get_shots_play_mode(shot_manager): +def get_shots_play_mode(shot_manager: UAS_ShotManager_Props): """Return True if the Shots Play Mode is active, False otherwise Warning: Currently the play mode status is shared between all the scenes of the file, it is not (yet) specific to an instance of shot manager. @@ -263,7 +276,7 @@ def get_shots_play_mode(shot_manager): return bpy.context.window_manager.UAS_shot_manager_shots_play_mode -def set_shots_play_mode(shot_manager, activate): +def set_shots_play_mode(shot_manager: UAS_ShotManager_Props, activate: bool): """Set to True to have the Shots Play Mode active, False otherwise Warning: Currently the play mode status is shared between all the scenes of the file, it is not (yet) specific to an instance of shot manager. @@ -271,7 +284,7 @@ def set_shots_play_mode(shot_manager, activate): bpy.context.window_manager.UAS_shot_manager_shots_play_mode = activate -def go_to_previous_shot(shot_manager, current_frame): +def go_to_previous_shot(shot_manager: UAS_ShotManager_Props, current_frame: float): """ works only on current take behavior of this button: @@ -284,7 +297,7 @@ def go_to_previous_shot(shot_manager, current_frame): # works only on current take -def go_to_next_shot(shot_manager, current_frame): +def go_to_next_shot(shot_manager: UAS_ShotManager_Props, current_frame: float): return shot_manager.goToNextShotBoundary(current_frame) @@ -296,34 +309,40 @@ def go_to_next_shot(shot_manager, current_frame): # - fisrt click: put current time at the end of the previous enabled shot -def go_to_previous_frame(shot_manager, current_frame): +def go_to_previous_frame(shot_manager: UAS_ShotManager_Props, current_frame: float): # print(" ** -- ** goToPreviousFrame") return shot_manager.goToPreviousFrame(current_frame) # works only on current take -def go_to_next_frame(shot_manager, current_frame): +def go_to_next_frame(shot_manager: UAS_ShotManager_Props, current_frame: float): # print(" ** -- ** goToNextShotBoundary") return shot_manager.goToNextFrame(current_frame) # works only on current take -def get_first_shot_index_containing_frame(shot_manager, frame_index, ignore_disabled=False): +def get_first_shot_index_containing_frame( + shot_manager: UAS_ShotManager_Props, frame_index: int, ignore_disabled: bool = False +): """Return the first shot containing the specifed frame, -1 if not found""" return shot_manager.getFirstShotIndexContainingFrame(frame_index, ignoreDisabled=ignore_disabled) # works only on current take -def get_first_shot_index_after_frame(shot_manager, frame_index, ignore_disabled=False): +def get_first_shot_index_after_frame( + shot_manager: UAS_ShotManager_Props, frame_index: int, ignore_disabled: bool = False +): """Return the first shot after the specifed frame (supposing thanks to getFirstShotIndexContainingFrame than frameIndex is not in a shot), -1 if not found """ return shot_manager.getFirstShotIndexAfterFrame(frame_index, ignoreDisabled=ignore_disabled) -def get_shots_using_camera(cam, ignore_disabled=False, take_index=-1): +def get_shots_using_camera( + shot_manager: UAS_ShotManager_Props, cam: bpy.types.Camera, ignore_disabled: bool = False, take_index: int = -1 +): """Return the list of all the shots used by the specified camera in the specified take""" - return shot_manager.getShotsUsingCamera(self, cam, ignoreDisabled=ignore_disabled, takeIndex=take_index) + return shot_manager.getShotsUsingCamera(cam, ignoreDisabled=ignore_disabled, takeIndex=take_index) #################### @@ -331,12 +350,17 @@ def get_shots_using_camera(cam, ignore_disabled=False, take_index=-1): #################### -def get_edit_duration(shot_manager, take_index): +def get_edit_duration(shot_manager: UAS_ShotManager_Props, take_index: int = -1): """Return edit duration in frames""" return shot_manager.getEditDuration(takeIndex=take_index) -def get_edit_time(shot_manager, reference_shot, frame_index_in_3D_time, reference_level="TAKE"): +def get_edit_time( + shot_manager: UAS_ShotManager_Props, + reference_shot: UAS_ShotManager_Shot, + frame_index_in_3D_time: int, + reference_level: str = "TAKE", +): """Return edit current time in frames, -1 if no shots or if current shot is disabled Works on the take from which referenceShot is coming from. Disabled shots are always ignored and considered as not belonging to the edit. @@ -346,7 +370,7 @@ def get_edit_time(shot_manager, reference_shot, frame_index_in_3D_time, referenc return shot_manager.getEditTime(reference_shot, frame_index_in_3D_time, referenceLevel=reference_level) -def get_edit_current_time(shot_manager, reference_level="TAKE"): +def get_edit_current_time(shot_manager: UAS_ShotManager_Props, reference_level: str = "TAKE"): """Return edit current time in frames, -1 if no shots or if current shot is disabled works only on current take reference_level can be "TAKE" or "GLOBAL_EDIT" diff --git a/shotmanager/api/take.py b/shotmanager/api/take.py index 252abadc..dc7e3c24 100644 --- a/shotmanager/api/take.py +++ b/shotmanager/api/take.py @@ -16,37 +16,36 @@ # along with this program. If not, see . """ -To do: module description here. +Shot Manager API - Interface with take class """ -def get_name(take_instance): +import bpy +from shotmanager.properties.take import UAS_ShotManager_Take + + +def get_name(take_instance: UAS_ShotManager_Take) -> str: return take_instance.name -def set_name(take_instance, name): - """ Set a unique name to the take - """ +def set_name(take_instance: UAS_ShotManager_Take, name: str): + """Set a unique name to the take""" take_instance.name = name -def get_name_path_compliant(take_instance): +def get_name_path_compliant(take_instance: UAS_ShotManager_Take) -> str: return take_instance.getName_PathCompliant() -def get_shot_list(take_instance, ignore_disabled=False): - """ Return a filtered copy of the shots associated to this take - """ +def get_shot_list(take_instance: UAS_ShotManager_Take, ignore_disabled: bool = False) -> list: + """Return a filtered copy of the shots associated to this take""" return take_instance.getShotList(ignoreDisabled=ignore_disabled) -def get_num_shots(take_instance, ignore_disabled=False): - """ Return the number of shots of the take - """ +def get_num_shots(take_instance: UAS_ShotManager_Take, ignore_disabled: bool = False) -> int: + """Return the number of shots of the take""" return take_instance.getNumShots(ignoreDisabled=ignore_disabled) -def get_shots_using_camera(take_instance, cam, ignore_disabled=False): - """ Return the list of all the shots used by the specified camera - """ +def get_shots_using_camera(take_instance: UAS_ShotManager_Take, cam: bpy.types.Camera, ignore_disabled: bool = False): + """Return the list of all the shots used by the specified camera""" return take_instance.getShotsUsingCamera(cam, ignoreDisabled=ignore_disabled) - diff --git a/shotmanager/config/dev_notes.txt b/shotmanager/config/dev_notes.txt index 9fb5e5a7..92350449 100644 --- a/shotmanager/config/dev_notes.txt +++ b/shotmanager/config/dev_notes.txt @@ -203,9 +203,12 @@ Avoir un rename de shots batch mettre le Snap to Frame Avoir un warning / clean pour quand les GP frames s'overlappent -timer pour la sel des clips -couleur pour les locked duration -selection du clip qd il est trop petit et qu'on tape dans les handles +Shots stack: + timer pour la sel des clips + couleur pour les locked duration + selection du clip qd il est trop petit et qu'on tape dans les handles + refresh issue pour l'affichage initial + Refresh issue quand on rechoisi la meme target truc information message contextuel diff --git a/shotmanager/features/greasepencil/greasepencil_operators.py b/shotmanager/features/greasepencil/greasepencil_operators.py index 728db42d..ad0a357a 100644 --- a/shotmanager/features/greasepencil/greasepencil_operators.py +++ b/shotmanager/features/greasepencil/greasepencil_operators.py @@ -271,6 +271,8 @@ def execute(self, context): utils_greasepencil.switchToObjectMode() else: utils_greasepencil.switchToDrawMode(context, gp) + props.isEditingStoryboardFrame = True + if props.shotsGlobalSettings.stb_camPOV_forFreeGP: props.getCurrentShot().setCameraToViewport() context.scene.tool_settings.gpencil_stroke_placement_view3d = ( @@ -331,6 +333,8 @@ def execute(self, context): gp.setInkLayerReadyToDraw(gp_child) utils_greasepencil.switchToDrawMode(context, gp_child) + props.isEditingStoryboardFrame = True + # gp_child.select_set(True) # gp_child.hide_select = False # gp_child.hide_viewport = False @@ -705,6 +709,7 @@ def execute(self, context): if editedGP == shot: utils_greasepencil.switchToObjectMode() utils.select_object(gp_child) + props.isEditingStoryboardFrame = False return {"FINISHED"} # get out of GP Draw mode for the edited shot @@ -728,6 +733,7 @@ def execute(self, context): # set ink layer, else topmost layer gp.setInkLayerReadyToDraw(gp_child) utils_greasepencil.switchToDrawMode(context, gp_child) + props.isEditingStoryboardFrame = True # utils.setPropertyPanelContext(context, "DATA") diff --git a/shotmanager/operators/shots.py b/shotmanager/operators/shots.py index 61fa49ad..8e2e22a9 100644 --- a/shotmanager/operators/shots.py +++ b/shotmanager/operators/shots.py @@ -30,6 +30,7 @@ from shotmanager.utils import utils from shotmanager.utils import utils_markers +from shotmanager.utils import utils_greasepencil from shotmanager.utils.utils_time import zoom_dopesheet_view_to_range from shotmanager.utils.utils_ui import propertyColumn from shotmanager.utils.utils import slightlyRandomizeColor @@ -308,15 +309,44 @@ class UAS_ShotManager_ToggleContinuousGPEditingMode(Operator): bl_label = "Continuous Draw Mode" bl_description = ( "When used, the Storyboard Frame of each shot set to be the current" - "\none will be automaticaly selected and switched to Draw mode" + "\none will be automaticaly selected and switched to Draw mode." + "\n+ Alt: Toggle the button between Draw and Select context for the Storyboard Frame" ) bl_options = {"REGISTER"} + # can be: + # - TOGGLE_USE_EDITING: will toggle the context between where the drawing state is available and the selection of the + # storyboard frame is available (basically the action button of each shot in the shot list will be changed). + # *** This does not activate the Drawing mode itself, but it can change it to Object mode in some cases *** + # - TOGGLE_ACTIVATE_EDITING_MODE: When the drawing context is available (one of the 2 states set by the mode above) then + # toggling between edit modes will alternate between Drawing mode activated (even if no stb frame is current) + # and Object mode activated. + action: StringProperty(default="DO_NOTHING") + + def invoke(self, context, event): + if event.alt: + self.action = "TOGGLE_USE_EDITING" + else: + self.action = "TOGGLE_ACTIVATE_EDITING_MODE" + + return self.execute(context) + def execute(self, context): props = context.scene.UAS_shot_manager_props # prefs = config.getShotManagerPrefs() - props.useContinuousGPEditing = not props.useContinuousGPEditing + if "TOGGLE_USE_EDITING" == self.action: + props.useContinuousGPEditing = not props.useContinuousGPEditing + if not props.useContinuousGPEditing: + props.isEditingStoryboardFrame = False + utils_greasepencil.switchToObjectMode() + else: + props.isEditingStoryboardFrame = not props.isEditingStoryboardFrame + if props.isEditingStoryboardFrame: + bpy.ops.uas_shot_manager.stb_frame_drawing() + else: + props.isEditingStoryboardFrame = False + utils_greasepencil.switchToObjectMode() # NOTE: when the Continuous Editing mode is on then the selected and current shots are tied anyway # in the "change selection" code diff --git a/shotmanager/properties/props.py b/shotmanager/properties/props.py index 831fe800..101c9fbb 100644 --- a/shotmanager/properties/props.py +++ b/shotmanager/properties/props.py @@ -1102,6 +1102,7 @@ def _update_target_viewport_index(self, context): # storyboard #################### + # NOTE: call isContinuousGPEditingModeActive() to query the state of this value useContinuousGPEditing: BoolProperty( name="Continuous Drawing Mode", description="When used, the Storyboard Frame of each shot set to be the current" @@ -1109,6 +1110,15 @@ def _update_target_viewport_index(self, context): default=True, ) + isEditingStoryboardFrame: BoolProperty( + name="Editing Storyboard Frame", + description="If True, the storyboard frame of any shot that becomes current will be set to Draw Mode." + "\n*** Do NOT change it directly ***" + "\nThis is changed by the operators uas_shot_manager.toggle_continuous_gp_editing_mode and" + "\nuas_shot_manager.greasepencil_select_and_draw", + default=False, + ) + def isContinuousGPEditingModeActive(self): """Return True if the context required to enter in a continuous drawing session is valid: the button has to be visible and checked. diff --git a/shotmanager/ui/sm_ui.py b/shotmanager/ui/sm_ui.py index baa57bd4..4267e70d 100644 --- a/shotmanager/ui/sm_ui.py +++ b/shotmanager/ui/sm_ui.py @@ -700,11 +700,28 @@ def draw(self, context): subrowedit.operator("uas_shot_manager.enabledisableall", text="", icon=iconCheckBoxes, emboss=False) if currentLayout.display_storyboard_in_properties: - subrowedit.operator( + iconEditingMode = "RESTRICT_SELECT_OFF" + depressState = True + alertState = False + if props.isContinuousGPEditingModeActive(): + iconEditingMode = "GREASEPENCIL" + if props.isEditingStoryboardFrame: + # cannot be set to True if the mode is activated cause button will appear purple instead of red + depressState = False + alertState = True + + # isEditingStoryboardFrame + + # depressState = props.useContinuousGPEditing + # depressState=props.isContinuousGPEditingModeActive() + + subsubrowedit = subrowedit.row(align=True) + subsubrowedit.alert = alertState + subsubrowedit.operator( "uas_shot_manager.toggle_continuous_gp_editing_mode", text="", - icon="GREASEPENCIL", - depress=props.useContinuousGPEditing, + depress=depressState, + icon=iconEditingMode, ) if currentLayout.display_editmode_in_properties: diff --git a/shotmanager/utils/utils_markers.py b/shotmanager/utils/utils_markers.py index 8b35a3f2..0a07a1a0 100644 --- a/shotmanager/utils/utils_markers.py +++ b/shotmanager/utils/utils_markers.py @@ -98,6 +98,7 @@ def addMarkerAtFrame(scene, frame, name): if "" == name: name = f"F_{scene.frame_current}" marker = scene.timeline_markers.new(name, frame=frame) + return marker def deleteMarkerAtFrame(scene, frame): From 34a1a1b902922f6eb028255e416661fbd1136a91 Mon Sep 17 00:00:00 2001 From: "julien.blervaque" Date: Tue, 11 Oct 2022 00:24:23 +0200 Subject: [PATCH 27/27] for beta - v2.1.24B --- CHANGELOG.md | 6 ++ shotmanager/config/config.py | 12 ++-- .../viewport_camera_hud/camera_hud_bgl.py | 2 +- shotmanager/rendering/rendering.md | 53 ++++++++++++++++++ shotmanager/rendering/rendering.py | 29 ++++++---- shotmanager/rendering/rendering_functions.py | 56 +++++++++++++++++++ shotmanager/utils/utils_os.py | 4 +- 7 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 shotmanager/rendering/rendering.md create mode 100644 shotmanager/rendering/rendering_functions.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4629d545..95b8f097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +----- +## 2.1.24 (2022-10-11) +- Added a function applyVideoSettings() in the rendering process to change the rendering file + format in a cleanest way (to be continued) +- Fixed a bug when plablast video appeared all black when called without Project Settings + ----- ## 2.1.23 (2022-10-01) - Added a button to set the time range to shot or take range in the Frame Range toolbar diff --git a/shotmanager/config/config.py b/shotmanager/config/config.py index 72ada62d..7b272afb 100644 --- a/shotmanager/config/config.py +++ b/shotmanager/config/config.py @@ -43,7 +43,7 @@ def initGlobalVariables(): devDebug = False # change this value to force debug at start time - devDebug = True + devDebug = False global devDebug_displayDebugPanel devDebug_displayDebugPanel = True @@ -51,17 +51,19 @@ def initGlobalVariables(): global devDebug_lastRedrawTime devDebug_lastRedrawTime = -1 + global devDebug_ignoreLoggerFormatting + devDebug_ignoreLoggerFormatting = True and devDebug + + # rendering ########## + # keep the intermediate images after rendering (ie the original Blender renderings # and the Stamp Info temporaty images) global devDebug_keepVSEContent devDebug_keepVSEContent = False - global devDebug_ignoreLoggerFormatting - devDebug_ignoreLoggerFormatting = True and devDebug - # installation ############# - # internet/github timeout, in seconds + # internet/github timeout, in seconds global LATEST_VERSION_TIMEOUT LATEST_VERSION_TIMEOUT = 1 diff --git a/shotmanager/overlay_tools/viewport_camera_hud/camera_hud_bgl.py b/shotmanager/overlay_tools/viewport_camera_hud/camera_hud_bgl.py index c3fd14b0..e67a83cf 100644 --- a/shotmanager/overlay_tools/viewport_camera_hud/camera_hud_bgl.py +++ b/shotmanager/overlay_tools/viewport_camera_hud/camera_hud_bgl.py @@ -40,7 +40,7 @@ def draw_shots_names(context): # return # For all camera which have a shot draw on the ui a list of shots associated with it - cameras = [obj for obj in scene.objects if obj.type == "CAMERA"] + cameras = [obj for obj in scene.objects if obj is not None and obj.type == "CAMERA"] for cam in cameras: if cam.type == "CAMERA": # print(f"cam: {cam.name}, cam.visible_get(): {cam.visible_get()}") diff --git a/shotmanager/rendering/rendering.md b/shotmanager/rendering/rendering.md new file mode 100644 index 00000000..32701ba0 --- /dev/null +++ b/shotmanager/rendering/rendering.md @@ -0,0 +1,53 @@ + + + +What happens during... +---------------------- + +Note: Separators in file names can be different in practice according to the settings or Project settings + + +A Take rendering: +================= + + - if Stamp Info is activated: + + - for each enabled shot of the current take: + - the shot is rendered as PNG file in its own temp directory named: + \_intermediate\_.png + - the Stamp Info frame files are rendered in the same temp directory: + \_intermediate\_tmp_StampInfo__.png + - currently a wav file is also rendered + + - in the VSE of a temp scene named Tmp_VSE_RenderScene those 3 renderings are composited and rendered as a .mp4 file in: + \_.mp4 + + This temp scene is then deleted + + + - for the sequence: + - in the VSE of a temp scene named VSE_SequenceRenderScene: + - the video part of the .mp4 of each shot is added to the edit + - the sound part of the .mp4 of each shot is added to the track above + - a video is rendered in: + \_.mp4 + + - if Stamp Info is NOT activated: + + + +A Playblast rendering: +====================== + + **Playblast mode is NOT related to the use of Project Settings** + + - if Stamp Info is activated: + - each enabled shot of the current take is rendered as PNG file in its own temp directory + - for each enabled shot the SI files are rendered in the directory of each shot + + - each shot is rendered as a video? + Or the full seq is based on all the shots? + + - if Stamp Info is NOT activated: + - + diff --git a/shotmanager/rendering/rendering.py b/shotmanager/rendering/rendering.py index 55bb63ba..b182313c 100644 --- a/shotmanager/rendering/rendering.py +++ b/shotmanager/rendering/rendering.py @@ -30,6 +30,7 @@ from shotmanager.config import config from shotmanager.rendering.rendering_stampinfo import setStampInfoSettings, renderStampedInfoForShot +from shotmanager.rendering import rendering_functions from shotmanager.utils import utils from shotmanager.utils import utils_editors_3dview @@ -220,8 +221,10 @@ def _deleteTempFiles(dirPath): # override local variables with project settings if props.use_project_settings: + # wkip needed??? props.applyProjectSettings() - scene.render.image_settings.file_format = props.project_images_output_format + + # scene.render.image_settings.file_format = props.project_images_output_format projectFps = props.project_fps # # # renderResolution = [props.project_resolution_x, props.project_resolution_y] # # # renderResolutionFramed = [props.project_resolution_framed_x, props.project_resolution_framed_y] @@ -296,12 +299,14 @@ def _deleteTempFiles(dirPath): # video settings ################ - # scene.render.image_settings.file_format = "FFMPEG" - # scene.render.ffmpeg.format = "MPEG4" - # scene.render.ffmpeg.constant_rate_factor = "PERC_LOSSLESS" # "PERC_LOSSLESS" - # scene.render.ffmpeg.gopsize = 5 # keyframe interval - # scene.render.ffmpeg.audio_codec = "AAC" - scene.render.use_file_extension = True + rendering_functions.applyVideoSettings(scene, props, "SHOT", renderMode, renderPreset) + # scene.render.image_settings.file_format = props.project_images_output_format + # # scene.render.image_settings.file_format = "FFMPEG" + # # scene.render.ffmpeg.format = "MPEG4" + # # scene.render.ffmpeg.constant_rate_factor = "PERC_LOSSLESS" # "PERC_LOSSLESS" + # # scene.render.ffmpeg.gopsize = 5 # keyframe interval + # # scene.render.ffmpeg.audio_codec = "AAC" + # scene.render.use_file_extension = True # set render quality ####################### @@ -748,8 +753,10 @@ def _deleteTempFiles(dirPath): deleteTempFiles = False else: deleteTempFiles = not renderPreset.keepIntermediateFiles - # ... or not config.devDebug_keepVSEContent - # deleteTempFiles = False + + if deleteTempFiles and config.devDebug and config.devDebug_keepVSEContent: + deleteTempFiles = False + # deleteTempFiles = not config.devDebug_keepVSEContent and not renderPreset.keepIntermediateFiles if deleteTempFiles: _deleteTempFiles(newTempRenderPath) @@ -860,7 +867,9 @@ def _deleteTempFiles(dirPath): deleteTempFiles = False else: deleteTempFiles = not renderPreset.keepIntermediateFiles - # ... or not config.devDebug_keepVSEContent + + if deleteTempFiles and config.devDebug and config.devDebug_keepVSEContent: + deleteTempFiles = False # deleteTempFiles = not config.devDebug_keepVSEContent and not renderPreset.keepIntermediateFiles # deleteTempFiles = False diff --git a/shotmanager/rendering/rendering_functions.py b/shotmanager/rendering/rendering_functions.py new file mode 100644 index 00000000..39423c8f --- /dev/null +++ b/shotmanager/rendering/rendering_functions.py @@ -0,0 +1,56 @@ +# GPLv3 License +# +# Copyright (C) 2021 Ubisoft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Rendering functions +""" + + +def applyVideoSettings(scene, props, mode, renderMode, renderPreset): + """ + Args: + mode: defines at which step the settings are required + SHOT: to render the shot images + SHOT_COMPO: to composite the shot video in the VSE + SEQUENCE: to render the whole sequence in the VSE + """ + + # *** Playblast mode is NOT related to the use of Project Settings *** + + if "SHOT" == mode: + if "PLAYBLAST" == renderMode: + # wkip use jpg + scene.render.image_settings.file_format = "JPEG" + scene.render.image_settings.quality = 85 + + # add the file extention to the rendered file names + scene.render.use_file_extension = True + else: + if props.use_project_settings: + scene.render.image_settings.file_format = props.project_images_output_format + # scene.render.image_settings.file_format = "FFMPEG" + # scene.render.ffmpeg.format = "MPEG4" + # scene.render.ffmpeg.constant_rate_factor = "PERC_LOSSLESS" # "PERC_LOSSLESS" + # scene.render.ffmpeg.gopsize = 5 # keyframe interval + # scene.render.ffmpeg.audio_codec = "AAC" + + scene.render.use_file_extension = True + + else: + scene.render.image_settings.file_format = "PNG" + scene.render.use_file_extension = True + # renderMode = "PROJECT" if renderPreset is None else renderPreset.renderMode diff --git a/shotmanager/utils/utils_os.py b/shotmanager/utils/utils_os.py index 66db5847..6c39d6c0 100644 --- a/shotmanager/utils/utils_os.py +++ b/shotmanager/utils/utils_os.py @@ -57,8 +57,8 @@ def delete_folder(dir_path): print(f"\n*** File locked (by system?): {path_to_file}") try: os.rmdir(dir_path) - except Exception: - print("Cannot delete Dir: ", dir_path) + except OSError as error: + _logger.warning_ext(f"Cannot delete directory: {dir_path}, error:{error}") def internet_on():