Bladeren bron

New Feature: Selection Syncing

This commit introduces a new feature: Syncing selection between node selection
and viewport/scene selection
Currently it has the following limitations:
 - only does xForm nodes
 - does NOT sync viewport to node graph
 - doesn't select groups' internals
 - requires a parsed tree in memory
 - only works for stuff in the current node-tree path

 This implementation is not exactly the one I will be using in production, but it is a
 useful proof of concept. I'll switch this out with a bidirectional dict that keeps track
 of node <==> object associations and stores in a scene property.
Joseph Brandenburg 1 maand geleden
bovenliggende
commit
f460256de5
4 gewijzigde bestanden met toevoegingen van 206 en 5 verwijderingen
  1. 13 1
      __init__.py
  2. 4 1
      menu_classes.py
  3. 32 3
      preferences.py
  4. 157 0
      timers.py

+ 13 - 1
__init__.py

@@ -371,6 +371,18 @@ def autoload_components(filename):
             bl_addon_object.preferences.MetaArmatureDefaultCollection)):
         context.scene.collection.children.unlink(armature_collection)
 
+# This has to be done after file load because the file load clears the timers
+@persistent
+def begin_selection_sync_timer(filename):
+    # And here I will register any timers used by Mantis.
+    from .timers import timer_update_selection_sync, ENABLED, SELECTION_POLL_RATE
+    from .preferences import get_bl_addon_object
+    bl_mantis_addon = get_bl_addon_object()
+    if bl_mantis_addon and bl_mantis_addon.preferences:
+        ENABLED = bl_mantis_addon.preferences.EnableSelectionSync
+        SELECTION_POLL_RATE = bl_mantis_addon.preferences.SelectionSyncPollRate
+        bpy.app.timers.register(timer_update_selection_sync)
+
 # I'll need to do some fiddling here when it comes time to try
 #   and make rig definitions animatable.
 @persistent
@@ -425,6 +437,7 @@ def register():
     # add the handlers
     bpy.app.handlers.depsgraph_update_pre.insert(0, update_handler)
     bpy.app.handlers.depsgraph_update_post.insert(0, execute_handler)
+    bpy.app.handlers.load_post.insert(0, begin_selection_sync_timer)
     bpy.app.handlers.load_post.insert(0, autoload_components)
     bpy.app.handlers.load_post.insert(0, version_update_handler)
     bpy.app.handlers.animation_playback_pre.insert(0, on_animation_playback_pre_handler)
@@ -434,7 +447,6 @@ def register():
     # but I am a good citizen! so my addon won't mess up yours! probably...
 
 
-
 def unregister():
     bpy.types.NODE_MT_context_menu.remove(node_context_menu_draw)
     bpy.types.NODE_MT_add.remove(node_add_menu_draw)

+ 4 - 1
menu_classes.py

@@ -125,4 +125,7 @@ class MantisActiveTreePanel(Panel):
         layout.operator("mantis.execute_node_tree")
         layout.operator("mantis.force_display_update", text='Force Display Update')
         layout.operator("mantis.import_from_component_library")
-        
+        #
+        from .preferences import get_bl_addon_object
+        bl_mantis_addon = get_bl_addon_object()
+        layout.prop(bl_mantis_addon.preferences, "EnableSelectionSync", text='Sync Selection',)

+ 32 - 3
preferences.py

@@ -59,9 +59,25 @@ def widget_library_get(self):
 def widget_library_idiot_test(self, value):
     self.widget_library_path = filepath_idiot_test(value)
 
+
 class MantisPreferences(bpy.types.AddonPreferences):
     bl_idname = __package__
 
+    def callback_enable_selection_state(self, context):
+        from . import timers
+        timers.ENABLED = self.EnableSelectionSync
+        if timers.ENABLED == True:
+            bpy.app.timers.register(timers.timer_update_selection_sync)
+        else:
+            try:
+                bpy.app.timers.unregister(timers.timer_update_selection_sync)
+            except ValueError:
+                pass # It's not registered anymore. that's OK
+
+    def callback_sync_selection_poll_rate(self, context):
+        from . import timers
+        timers.SELECTION_POLL_RATE = self.SelectionSyncPollRate
+        
     # JSONprefix: bpy.props.StringProperty(
     #     name = "Prefix code file",
     #     subtype = 'FILE_PATH',
@@ -100,13 +116,26 @@ class MantisPreferences(bpy.types.AddonPreferences):
         description = "Location of .rig files to load automatically.",
         subtype = 'FILE_PATH',
         default = os.path.join(dir_path, 'auto_load_components'),)
-
+    EnableSelectionSync:bpy.props.BoolProperty(
+        name = "Enable Selection Sync",
+        description = "Sync selection of nodes and scene objects.",
+        default = True,
+        update = callback_enable_selection_state)
+    SelectionSyncPollRate:bpy.props.FloatProperty(
+        name = "Selection Sync Poll Rate",
+        description = "How often to check for changes in selection.",
+        default = 0.01,
+        update = callback_sync_selection_poll_rate)
+    
     def draw(self, context):
         layout = self.layout
         layout.label(text="Mantis Preferences")
-        layout.prop(self, "WidgetsLibraryFolder", icon='FILE_FOLDER')
+        layout.prop(self, "EnableSelectionSync")
+        layout.prop(self, "SelectionSyncPollRate")
         layout.prop(self, "WidgetDefaultCollection")
-        layout.prop(self, "ComponentsLibraryFolder", icon='FILE_FOLDER')
         layout.prop(self, "CurveDefaultCollection")
         layout.prop(self, "MetaArmatureDefaultCollection")
+        layout.label(text="Paths:")
+        layout.prop(self, "WidgetsLibraryFolder", icon='FILE_FOLDER')
+        layout.prop(self, "ComponentsLibraryFolder", icon='FILE_FOLDER')
         layout.prop(self, "ComponentsAutoLoadFolder", icon='FILE_FOLDER')

+ 157 - 0
timers.py

@@ -0,0 +1,157 @@
+from .utilities import (prRed, prGreen, prPurple, prWhite,
+                              prOrange,
+                              wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                              wrapOrange,)
+
+# the first step is to get selection syncing working
+import bpy
+from bpy import context
+from .base_definitions import get_signature_from_edited_tree 
+
+from bpy.types import Bone, PoseBone
+# just to prevent confusing with Python object:
+from bpy.types import Object as bpy_object
+
+SELECTION_POLL_RATE = 0.01
+ENABLED = True
+cached_selection_state = {}
+
+import bpy
+
+# The following node types represent objects that can be selected.
+WATCH_FOR_SELECTION = [ 'xFormBoneNode', 'xFormArmatureNode',
+        'xFormGeometryObject', 'xFormObjectInstance', 'xFormCurvePin',
+        'xFormGetBone', 'InputExistingGeometryObject']
+
+
+def get_signature_from_space(node, space):
+    sig_path=[None,]
+    for item in space.path[:-1]:
+        sig_path.append(item.node_tree.nodes.active.name)
+    return tuple(sig_path+[node.name])
+
+def get_visible_space_data(space_types={'NODE_EDITOR'}):
+    visible_spaces = []
+    for wm in bpy.data.window_managers:
+        for window in wm.windows:
+            for area in window.screen.areas:
+                if area.type in space_types:
+                    if area.spaces.active:
+                        visible_spaces.append(area.spaces.active)
+                        
+    return visible_spaces
+
+# Example usage: Get all active 3D Viewport space_data objects
+spaces = get_visible_space_data('VIEW_3D')
+
+for space in spaces:
+    print(f"Found 3D Viewport: shading mode = {space.shading.type}")
+
+
+# TODO: now that I have this working...
+#  - Selection State should be tracked and handled on a per-object basis
+#     so Mantis should update selection state one item at a time
+#  - every object should be kept in the cache at all times.
+#  - a deselect event should check only the current tree for empty selection
+#  - but deselect should deselect anything that is tracked by Mantis
+#  - Mantis node <==> UI node relationships should be cached instead of using the
+#     tree cache, and ideally, saved in the Scene object or something
+#  - I'll probably do like a JSON or something for that, maybe a pickle
+#  - anyhow the important thing is I need a bi-directional dictionary so I can
+#     make the selection sync happen both ways
+#  - finally, I want to extend this selection sync to show active constraints and modifiers
+#     and to link up with viewport gizmo tools.
+
+def timer_update_selection_sync():
+    if not ENABLED: return None
+    root_tree = None
+    tree_path = None
+    parsed_tree = None
+    spaces = get_visible_space_data()
+    if not spaces:
+        return SELECTION_POLL_RATE
+
+    selection_state_start = cached_selection_state.copy()
+    selection_state = cached_selection_state.copy()
+    ui_signatures = list(cached_selection_state.keys())
+    current_tree_selected_total = 0
+
+    # we can't get UI context in this function so we have to checc all of them
+    #maybe the tree itself can load the timer when it is opened in the node editor?
+    for space in spaces:
+        # if we are in the node editor, manage node selection
+        if space.type == "NODE_EDITOR":
+            root_tree = space.path[0].node_tree
+            parsed_tree = root_tree.parsed_tree
+            tree_path = [path_item.node_tree for path_item in space.path]
+            if root_tree is None:
+                return SELECTION_POLL_RATE
+            if root_tree.bl_idname != 'MantisTree':
+                return SELECTION_POLL_RATE
+            active_ui_signature = (get_signature_from_space(tree_path[-1].nodes.active, space))
+            for n in tree_path[-1].nodes:
+                if n.bl_idname not in WATCH_FOR_SELECTION: continue
+                ui_signatures.append( ui_signature := get_signature_from_space(n, space))
+                selection_state[ui_signature] = n.select
+                current_tree_selected_total += int(n.select)
+    
+    if parsed_tree is None:
+        return SELECTION_POLL_RATE
+    cached_selection_state.update(selection_state)
+    # We only fire the update if things have changed.
+    if selection_state != selection_state_start:
+        
+        from bpy import context
+        view_layer = context.view_layer
+        deselect = bool(current_tree_selected_total)
+        try:
+            update_scene_selection_from_selection_sync(
+                parsed_tree, ui_signatures, active_ui_signature, view_layer,
+                selection_state, deselect)
+        except Exception as e:
+            prRed (e)
+            return SELECTION_POLL_RATE
+    return SELECTION_POLL_RATE
+
+
+# 1 get the selected nodes
+# 2 find mantis nodes that have the ui_signature of the node
+# 3 get their b-objects and select them
+def update_scene_selection_from_selection_sync(
+        parsed_tree, ui_signatures, active_ui_signature, view_layer,
+        selection_state, deselect ):
+    # first check if this is a deselect event.
+    # if it is, do not change the active object!
+    # that makes the armature switch modes, it's annoying
+    deselect=False
+    if sum(selection_state.values()) == 0:
+        deselect=True
+    prWhite("Updating Scene Selection from Node Graph Selection")
+    for mantis_node in parsed_tree.values():
+        this_one_is_active = False
+        if not hasattr(mantis_node, 'bGetObject'): continue
+        if mantis_node.node_type != 'XFORM': continue
+        if mantis_node.ui_signature in ui_signatures:
+            selected = selection_state[mantis_node.ui_signature]
+            if mantis_node.ui_signature == active_ui_signature:
+                this_one_is_active = True
+            ob = mantis_node.bGetObject()
+            if isinstance(ob, PoseBone):
+                if deselect:
+                    ob.select = False
+                    continue
+                ob.select = selected
+                if this_one_is_active: # set the active bone
+                    ob.id_data.data.bones.active = ob.bone
+                # now do its parent object...
+                if selected:
+                    if ob.id_data.select_get(view_layer=view_layer) == False:
+                        ob.id_data.select_set(True, view_layer=view_layer)
+                    if this_one_is_active:
+                        view_layer.objects.active = ob.id_data
+            elif isinstance(ob, bpy_object):
+                if ob.select_get(view_layer=view_layer) != selected:
+                    ob.select_set(selected, view_layer=view_layer)
+                if this_one_is_active and not deselect:
+                    view_layer.objects.active = ob
+