瀏覽代碼

Add Widget Import Node

This commit is kind of big, a breakdown:
 - adds Addon Preferences  to Mantis
 - adds a preference for Widget Library
 - adds a Widget Node that can import .obj's from the library
 - the widget importer additionally has an axis flip option
Joseph Brandenburg 3 月之前
父節點
當前提交
d1ba063410
共有 9 個文件被更改,包括 299 次插入7 次删除
  1. 1 0
      .gitignore
  2. 3 0
      __init__.py
  3. 70 1
      geometry_node_graphgen.py
  4. 63 0
      misc_nodes.py
  5. 11 1
      misc_nodes_socket_templates.py
  6. 13 0
      misc_nodes_ui.py
  7. 30 0
      preferences.py
  8. 51 5
      socket_definitions.py
  9. 57 0
      utilities.py

+ 1 - 0
.gitignore

@@ -5,3 +5,4 @@ index.html
 index.json
 mantis.zip
 mantis.*.zip
+widgets/*

+ 3 - 0
__init__.py

@@ -43,6 +43,8 @@ while (classLists):
 
 interface_classes = []
 
+from .preferences import MantisPreferences
+classes.append(MantisPreferences)
 
 from os import environ
 if environ.get("ENABLEVIS"):
@@ -75,6 +77,7 @@ input_category=[
             NodeItem("InputStringNode"),
             NodeItem("InputIntNode"),
             NodeItem("InputMatrixNode"),
+            NodeItem("InputWidget"),
             NodeItem("InputExistingGeometryObject"),
             NodeItem("InputExistingGeometryData"),
             NodeItem("UtilityDeclareCollections"),

+ 70 - 1
geometry_node_graphgen.py

@@ -125,4 +125,73 @@ def gen_object_instance_node_group():
     ng.links.new(input=ob_node.outputs["Geometry"], output=out.inputs["Object Instance"])
     inp.location = (-200, 0)
     out.location = ( 200, 0)
-    return ng
+    return ng
+
+def gen_import_obj_node_group():
+    import bpy
+    from bpy import data, types
+    from math import pi as PI
+    tree=bpy.data.node_groups.new("Import OBJ","GeometryNodeTree")
+    tree.is_modifier=True
+    tree.interface.new_socket(name="Path",description="Path to a OBJ file",in_out="INPUT",socket_type="NodeSocketString")
+    tree.interface.new_socket(name="Geometry",description="",in_out="OUTPUT",socket_type="NodeSocketGeometry")
+    Group_Input = tree.nodes.new("NodeGroupInput")
+    Group_Output = tree.nodes.new("NodeGroupOutput")
+    Import_OBJ = tree.nodes.new("GeometryNodeImportOBJ")
+    Realize_Instances = tree.nodes.new("GeometryNodeRealizeInstances")
+    Rotate_Instances = tree.nodes.new("GeometryNodeRotateInstances")
+    Rotate_Instances.inputs[2].default_value=[PI/2,0.0, 0.0] # assume standard axes
+    tree.links.new(Group_Input.outputs[0],Import_OBJ.inputs[0])
+    tree.links.new(Rotate_Instances.outputs[0],Realize_Instances.inputs[0])
+    tree.links.new(Realize_Instances.outputs[0],Group_Output.inputs[0])
+    tree.links.new(Import_OBJ.outputs[0],Rotate_Instances.inputs[0])
+    try:
+        from .utilities import SugiyamaGraph
+        SugiyamaGraph(tree, 4)
+    except: # there should not ever be a user error if this fails
+        pass
+    return tree
+
+def gen_simple_flip_modifier():
+    import bpy
+    from bpy import data, types
+    tree=bpy.data.node_groups.new("Simple Flip","GeometryNodeTree")
+    tree.is_modifier=True
+    tree.interface.new_socket(name="Geometry",description="",in_out="OUTPUT",socket_type="NodeSocketGeometry")
+    tree.interface.new_socket(name="Geometry",description="",in_out="INPUT",socket_type="NodeSocketGeometry")
+    tree.interface.new_socket(name="Flip X",description="",in_out="INPUT",socket_type="NodeSocketBool")
+    tree.interface.new_socket(name="Flip Y",description="",in_out="INPUT",socket_type="NodeSocketBool")
+    tree.interface.new_socket(name="Flip Z",description="",in_out="INPUT",socket_type="NodeSocketBool")
+    Group_Input = tree.nodes.new("NodeGroupInput")
+    Group_Output = tree.nodes.new("NodeGroupOutput")
+    Set_Position = tree.nodes.new("GeometryNodeSetPosition")
+    Position = tree.nodes.new("GeometryNodeInputPosition")
+    Combine_XYZ = tree.nodes.new("ShaderNodeCombineXYZ")
+    Map_Range = tree.nodes.new("ShaderNodeMapRange")
+    Map_Range_001 = tree.nodes.new("ShaderNodeMapRange")
+    Map_Range_002 = tree.nodes.new("ShaderNodeMapRange")
+    Map_Range.inputs[3].default_value     =  1.0
+    Map_Range_001.inputs[3].default_value =  1.0
+    Map_Range_002.inputs[3].default_value =  1.0
+    Map_Range.inputs[4].default_value     = -1.0
+    Map_Range_001.inputs[4].default_value = -1.0
+    Map_Range_002.inputs[4].default_value = -1.0
+    Vector_Math = tree.nodes.new("ShaderNodeVectorMath")
+    Vector_Math.operation = "MULTIPLY"
+    tree.links.new(Set_Position.outputs[0],Group_Output.inputs[0])
+    tree.links.new(Group_Input.outputs[0],Set_Position.inputs[0])
+    tree.links.new(Group_Input.outputs[1],Map_Range.inputs[0])
+    tree.links.new(Map_Range.outputs[0],Combine_XYZ.inputs[0])
+    tree.links.new(Map_Range_001.outputs[0],Combine_XYZ.inputs[1])
+    tree.links.new(Map_Range_002.outputs[0],Combine_XYZ.inputs[2])
+    tree.links.new(Group_Input.outputs[2],Map_Range_001.inputs[0])
+    tree.links.new(Group_Input.outputs[3],Map_Range_002.inputs[0])
+    tree.links.new(Combine_XYZ.outputs[0],Vector_Math.inputs[1])
+    tree.links.new(Position.outputs[0],Vector_Math.inputs[0])
+    tree.links.new(Vector_Math.outputs[0],Set_Position.inputs[2])
+    try:
+        from .utilities import SugiyamaGraph
+        SugiyamaGraph(tree, 4)
+    except: # there should not ever be a user error if this fails
+        pass
+    return tree

+ 63 - 0
misc_nodes.py

@@ -16,6 +16,7 @@ def TellClasses():
              InputTransformSpace,
              InputString,
              InputMatrix,
+             InputWidget,
              InputExistingGeometryObject,
              InputExistingGeometryData,
              InputThemeBoneColorSets,
@@ -1256,6 +1257,68 @@ class UtilityCatStrings(MantisNode):
         self.parameters["OutputString"] = self.evaluate_input("String_1")+self.evaluate_input("String_2")
         self.prepared, self.executed = True, True
 
+# TODO move this to the Xform file
+class InputWidget(MantisNode):
+    '''A node representing an existing object'''
+    def __init__(self, signature, base_tree):
+        super().__init__(signature, base_tree, InputWidgetSockets)
+        self.init_parameters()
+        self.node_type = "XFORM"
+    
+    def reset_execution(self):
+        super().reset_execution()
+        self.prepared=False
+
+    def bPrepare(self, bContext=None):
+        print(wrapGreen("Executing ")+wrapOrange("InputWidget Node ")+wrapWhite(f"{self}"))
+        path = self.evaluate_input('Name')
+        axes_flipped = self.evaluate_input('Flip Axes')
+        do_mirror = True
+        from os import path as os_path
+        file_name = os_path.split(path)[-1]
+        obj_name = os_path.splitext(file_name)[0]
+        obj_name_full = obj_name
+        if any(axes_flipped):
+            obj_name_full+="_flipped_"
+        for i, axis in enumerate("XYZ"):
+            if axes_flipped[i]: obj_name_full+=axis
+        from bpy import data
+        if  obj_name in data.objects.keys() and not \
+            obj_name_full in data.objects.keys():
+                self.bObject = data.objects.get(obj_name).copy()
+                self.bObject.name = obj_name_full
+                if bContext: bContext.collection.objects.link(self.bObject)
+        # now check to see if it exists
+        elif obj_name_full in data.objects.keys():
+            prWhite(f"INFO: {obj_name_full} is already in this .blend file; skipping import.")
+            self.bObject = data.objects.get(obj_name_full)
+            if any(axes_flipped): # check if we need to add a Flip modifier
+                if len(self.bObject.modifiers) > 1 and self.bObject.modifiers[-1].name == "Simple Flip":
+                    do_mirror=False
+        else:
+            from .utilities import import_object_from_file
+            self.bObject = import_object_from_file(path)
+            if any(axes_flipped):
+                self.bObject = self.bObject.copy()
+                self.bObject.name = obj_name_full
+                if bContext: bContext.collection.objects.link(self.bObject)
+        # now we'll check for the mirrors.
+        axes_flipped = self.evaluate_input('Flip Axes')
+        if any(axes_flipped) and do_mirror:
+            import_modifier = self.bObject.modifiers.new("Simple Flip", type="NODES")
+            ng = data.node_groups.get("Simple Flip")
+            if ng is None:
+                from .geometry_node_graphgen import gen_simple_flip_modifier
+                ng = gen_simple_flip_modifier()
+            import_modifier.node_group = ng
+            import_modifier["Socket_2"]=axes_flipped[0]
+            import_modifier["Socket_3"]=axes_flipped[1]
+            import_modifier["Socket_4"]=axes_flipped[2]
+        self.prepared, self.executed = True, True
+    
+    def bGetObject(self, mode=''):
+        return self.bObject
+    
 # TODO move this to the Xform file
 class InputExistingGeometryObject(MantisNode):
     '''A node representing an existing object'''

+ 11 - 1
misc_nodes_socket_templates.py

@@ -4,6 +4,15 @@ from dataclasses import replace
 SplineIndexTemplate = SockTemplate(name="Spline Index",
         bl_idname='UnsignedIntSocket', is_input=True, default_value=0,)
 
+InputWidgetSockets = [
+    WidgetName := SockTemplate(name='Name', is_input=True,
+        bl_idname="EnumWidgetLibrarySocket",),
+    FlipAxes := SockTemplate(name='Flip Axes', is_input=True,
+        bl_idname="BooleanThreeTupleSocket",),
+    xFormOutput := SockTemplate(name='Widget',
+        bl_idname='xFormSocket',is_input=False),
+]
+
 MatrixFromCurveSockets=[
     CurveTemplate := SockTemplate(name="Curve", bl_idname='EnumCurveSocket', 
         is_input=True,),
@@ -116,4 +125,5 @@ CollectionHierarchySockets = [
     ChildCollection := SockTemplate(name='Child Collection', is_input=True,
         bl_idname="BoneCollectionSocket",),
     CollectionDeclarationOutput,
-]
+]
+

+ 13 - 0
misc_nodes_ui.py

@@ -19,6 +19,7 @@ def TellClasses():
              InputStringNode,
              InputMatrixNode,
              # InputGeometryNode,
+             InputWidget,
              InputExistingGeometryObjectNode,
              InputExistingGeometryDataNode,
              InputThemeBoneColorSets,
@@ -716,6 +717,18 @@ class InputLayerMaskNode(Node, MantisUINode):
         self.outputs.new("LayerMaskInputSocket", "Layer Mask")
         self.initialized = True
 
+class InputWidget(Node, MantisUINode):
+    """Fetches a Widget from the Widget Library."""
+    bl_idname = "InputWidget"
+    bl_label = "Widget"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    mantis_node_class_name=bl_idname
+    
+    def init(self, context):
+        self.init_sockets(InputWidgetSockets)
+        self.initialized = True
+    
 class InputExistingGeometryObjectNode(Node, MantisUINode):
     """Represents an existing geometry object from within the scene."""
     bl_idname = "InputExistingGeometryObject"

+ 30 - 0
preferences.py

@@ -0,0 +1,30 @@
+import bpy
+import os
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+#
+
+class MantisPreferences(bpy.types.AddonPreferences):
+    bl_idname = __package__
+
+    # JSONprefix: bpy.props.StringProperty(
+    #     name = "Prefix code file",
+    #     subtype = 'FILE_PATH',
+    #     default = dir_path + '/preferences/prefix.json',)
+    # JSONchiral: bpy.props.StringProperty(
+    #     name = "Chiral Identifier file",
+    #     subtype = 'FILE_PATH',
+    #     default = dir_path + '/preferences/chiral_identifier.json',)
+    # JSONseperator:bpy.props.StringProperty(
+    #     name = "Seperator file",
+    #     subtype = 'FILE_PATH',
+    #     default = dir_path + '/preferences/seperator.json',)
+    WidgetsLibraryFolder:bpy.props.StringProperty(
+        name = "Widget Library Folder",
+        subtype = 'FILE_PATH',
+        default = os.path.join(dir_path, 'widgets'),)
+    
+    def draw(self, context):
+        layout = self.layout
+        layout.label(text="Mantis Preferences")
+        layout.prop(self, "WidgetsLibraryFolder", icon='FILE_FOLDER')

+ 51 - 5
socket_definitions.py

@@ -153,6 +153,7 @@ def TellClasses() -> List[MantisSocket]:
              EnumMetaRigSocket,
              EnumMetaBoneSocket,
              EnumCurveSocket,
+             EnumWidgetLibrarySocket,
              BoolUpdateParentNode,
             #  LabelSocket,
              IKChainLengthSocket,
@@ -1350,13 +1351,58 @@ class EnumMetaBoneSocket(MantisSocket):
     @classmethod
     def draw_color_simple(self):
         return self.color_simple
-    
-    
-    
-    
-    
 
 
+def get_widget_library_items(self, context):
+    from bpy import context
+    try_these_first = ['bl_ext.repos.mantis', 'bl_ext.blender_modules_enabled.mantis']
+    for mantis_key in try_these_first:
+        bl_mantis_addon = context.preferences.addons.get(mantis_key)
+        if bl_mantis_addon: break
+    return_value = [('NONE', 'None', 'None', 'ERROR', 0)]
+    widget_names={}
+    if bl_mantis_addon:
+        widgets_path = bl_mantis_addon.preferences.WidgetsLibraryFolder
+        import os
+        for path_root, dirs, files, in os.walk(widgets_path):
+            # TODO handle .blend files
+            # for file in files: # check .blend files first, objs should take precedence
+            #     if file.endswith('.blend'):
+            #         widget_names[file[:-6]] = os.path.join(path_root, file)
+            for file in files:
+                if file.endswith('.obj'):
+                    widget_names[file[:-4]] = os.path.join(path_root, file)
+    else:
+        prRed("Mantis Preferences not found. This is a bug. Please report it on gitlab.")
+        prRed("I will need to know your OS and info about how you installed Mantis.")
+    if widget_names.keys():
+        return_value=[]
+        for i, (name, path) in enumerate(widget_names.items()):
+            return_value.append( (path, name, path, 'GIZMO', i) )
+    return return_value
+
+# THIS is a special socket type that finds the widgets in your widgets library (set in preferences)
+class EnumWidgetLibrarySocket(MantisSocket):
+    '''Choose a Wdiget'''
+    bl_idname = 'EnumWidgetLibrarySocket'
+    bl_label = "Widget"
+    is_valid_interface_type=False
+    default_value  : bpy.props.EnumProperty(
+        items=get_widget_library_items,
+        name="Widget",
+        description="Which widget to use",
+        default = 0,
+        update = update_socket,)
+    color_simple = cString
+    color : bpy.props.FloatVectorProperty(default=cString, size=4)
+    def draw(self, context, layout, node, text):
+        ChooseDraw(self, context, layout, node, text, use_enum=False)
+    def draw_color(self, context, node):
+        return self.color
+    @classmethod
+    def draw_color_simple(self):
+        return self.color_simple 
+    
 class BoolUpdateParentNode(MantisSocket):
     '''Custom node socket type'''
     bl_idname = 'BoolUpdateParentNode'

+ 57 - 0
utilities.py

@@ -310,6 +310,63 @@ def bind_modifier_operator(modifier, operator):
             prWhite(f"Binding Deformer {modifier.name} to target {target.name}")
             operator(modifier=modifier.name)
 
+def import_widget_obj(path,):
+    from bpy.app import version as bpy_version
+    from bpy import context, data
+    from os import path as os_path
+    file_name = os_path.split(path)[-1]
+    obj_name = os_path.splitext(file_name)[0]
+    if bpy_version < (4,5,0):
+        original_active = context.active_object
+        # for blender versions prior to 4.5.0, we have to import with an operator
+        from bpy.ops import wm as wm_ops
+        ob_names_before = data.objects.keys()
+        wm_ops.obj_import(
+            filepath=path,
+            check_existing=False,
+            forward_axis='NEGATIVE_Z',
+            up_axis='Y',
+            validate_meshes=True,)
+        # just make sure the active object doesn't change
+        context.view_layer.objects.active = original_active
+        # the below is a HACK... I can find the objects in the .obj file
+        # by scanning the file for the "o" prefix and checking the name.
+        # but that may be slow if the obj is big. which would make a bad widget!
+        ob = None
+        for ob in data.objects:
+            if ob.name in ob_names_before: continue
+            return ob # return the first one, that should be the one
+        else: # no new object was found - fail.
+            # I don't expect this to happen unless there is an error in the operator.
+            raise RuntimeError(f"Failed to import {file_name}. This is probably"
+                                "a bug or a corrupted file.")
+    else:
+        prWhite(f"INFO: using Geometry Nodes to import {file_name}")
+        mesh = data.meshes.new(obj_name)
+        ob = data.objects.new(name=obj_name, object_data=mesh)
+        # we'll do a geometry nodes import
+        context.collection.objects.link(ob)
+        import_modifier = ob.modifiers.new("Import OBJ", type="NODES")
+        ng = data.node_groups.get("Import OBJ")
+        if ng is None:
+            from .geometry_node_graphgen import gen_import_obj_node_group
+            ng = gen_import_obj_node_group()
+        import_modifier.node_group = ng
+        import_modifier["Socket_0"]=path
+        return ob
+
+def import_object_from_file(path):
+    # first let's check to see if we need it.
+    from os import path as os_path
+    file_name = os_path.split(path)[-1]
+    obj_name = os_path.splitext(file_name)[0]
+    extension = os_path.splitext(file_name)[1]
+    if extension == '.obj':
+        return import_widget_obj(path,)
+    else:
+        raise RuntimeError(f"Failed to parse filename {path}")
+        
+
 ##############################
 #  READ TREE and also Schema Solve!
 ##############################