Skip to content

Commit

Permalink
feat(merge groups): Update merge group handling
Browse files Browse the repository at this point in the history
This update overhauls the merge group system to have a more intuitive userinterface similar
to how Blender handles textures, materials etc. with a dropdown menu to select any existing
merge group as well as buttons for creating new merge groups, deleting merge groups, selecting
the root merge group root as well as a helper button for selecting all merge group objects for
a given merge group. **This new way of handling merge groups is entirely backwards compatible
and any existing merge groups using the old string format will be converted to the new format
when the file is loaded!**

Close #79 resolve #172

Co-authored-by: Kristian <[email protected]>
  • Loading branch information
StjerneIdioten and NMC-TBone committed Oct 13, 2023
1 parent 6c9f393 commit f48c9cb
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 56 deletions.
3 changes: 0 additions & 3 deletions addon/i3dio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@


def register():

try:
import lxml
except ImportError as e:
Expand Down Expand Up @@ -77,10 +76,8 @@ def register():
ui.user_attributes.register()
ui.mesh.register()
ui.light.register()

bpy.types.TOPBAR_MT_file_export.append(ui.exporter.menu_func_export)


def unregister():
bpy.types.TOPBAR_MT_file_export.remove(ui.exporter.menu_func_export)
ui.exporter.unregister()
Expand Down
9 changes: 6 additions & 3 deletions addon/i3dio/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .i3d import I3D
from .node_classes.node import SceneGraphNode
from .node_classes.skinned_mesh import SkinnedMeshRootNode
from .node_classes.merge_group import MergeGroup

logger = logging.getLogger(__name__)
logger.debug(f"Loading: {__name__}")
Expand Down Expand Up @@ -181,9 +182,11 @@ def _add_object_to_i3d(i3d: I3D, obj: BlenderObject, parent: SceneGraphNode = No
break

if node is None:
if 'MERGE_GROUPS' in i3d.settings['features_to_export'] and obj.i3d_merge_group.group_id != "":
# Currently the check for a mergegroup relies solely on whether or not a name is set for it
node = i3d.add_merge_group_node(obj, _parent)
if 'MERGE_GROUPS' in i3d.settings['features_to_export'] and obj.i3d_merge_group_index != -1:
blender_merge_group = bpy.context.scene.i3dio_merge_groups[obj.i3d_merge_group_index]
if obj.i3d_merge_group_index not in i3d.merge_groups:
i3d.merge_groups[obj.i3d_merge_group_index] = MergeGroup(xml_i3d.merge_group_prefix + blender_merge_group.name)
node = i3d.add_merge_group_node(obj, _parent, blender_merge_group.root is obj)
else:
# Default to a regular shape node
node = i3d.add_shape_node(obj, _parent)
Expand Down
38 changes: 14 additions & 24 deletions addon/i3dio/i3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, name: str, i3d_file_path: str, conversion_matrix: mathutils.M
self.shapes: Dict[Union[str, int], IndexedTriangleSet] = {}
self.materials: Dict[Union[str, int], Material] = {}
self.files: Dict[Union[str, int], File] = {}
self.merge_groups: Dict[str, MergeGroup] = {}
self.merge_groups: Dict[int, MergeGroup] = {}
self.skinned_meshes: Dict[str, SkinnedMeshRootNode] = {}

self.i3d_mapping: List[SceneGraphNode] = []
Expand Down Expand Up @@ -72,34 +72,24 @@ def add_shape_node(self, mesh_object: bpy.types.Object, parent: SceneGraphNode =
"""Add a blender object with a data type of MESH to the scenegraph as a Shape node"""
return self._add_node(ShapeNode, mesh_object, parent)

def add_merge_group_node(self, merge_group_object: bpy.types.Object, parent: SceneGraphNode = None) \
def add_merge_group_node(self, merge_group_object: bpy.types.Object, parent: SceneGraphNode = None, is_root: bool = False) \
-> [SceneGraphNode, None]:
self.logger.debug("Adding merge group node")
merge_group_id = merge_group_object.i3d_merge_group.group_id
merge_group_name = xml_i3d.merge_group_prefix + merge_group_id
node_to_return: [MergeGroupRoot or MergeGroupRoot] = None
if merge_group_name not in self.merge_groups:
self.logger.debug("New merge group")
merge_group = self.merge_groups[merge_group_name] = MergeGroup(merge_group_name)
if merge_group_object.i3d_merge_group.is_root:
merge_group = self.merge_groups[merge_group_object.i3d_merge_group_index]

node_to_return: [MergeGroupRoot or MergeGroupChild] = None

if is_root:
if merge_group.root_node is not None:
self.logger.warning(f"Merge group '{merge_group.name}' already has a root node! "
f"The object '{merge_group_object.name}' will be ignored for export")
else:
node_to_return = self._add_node(MergeGroupRoot, merge_group_object, parent)
merge_group.set_root(node_to_return)
else:
node_to_return = self._add_node(MergeGroupChild, merge_group_object, parent)
merge_group.add_child(node_to_return)
else:
self.logger.debug("Merge group already exists")
merge_group = self.merge_groups[merge_group_name]
if merge_group_object.i3d_merge_group.is_root:
if merge_group.root_node is not None:
self.logger.warning(f"Merge group '{merge_group_id}' already has a root node! "
f"The object '{merge_group_object.name}' will be ignored for export")
else:
node_to_return = self._add_node(MergeGroupRoot, merge_group_object, parent)
merge_group.set_root(node_to_return)
else:
node_to_return = self._add_node(MergeGroupChild, merge_group_object, parent)
merge_group.add_child(node_to_return)
node_to_return = self._add_node(MergeGroupChild, merge_group_object, parent)
merge_group.add_child(node_to_return)

return node_to_return

def add_bone(self, bone_object: bpy.types.Bone, parent: Union[SkinnedMeshBoneNode, SkinnedMeshRootNode]) \
Expand Down
2 changes: 1 addition & 1 deletion addon/i3dio/node_classes/merge_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class MergeGroupRoot(ShapeNode):

def __init__(self, id_: int, merge_group_object: [bpy.types.Object, None], i3d: I3D,
parent: [SceneGraphNode or None] = None):
self.merge_group_name = xml_i3d.merge_group_prefix + merge_group_object.i3d_merge_group.group_id
self.merge_group_name = i3d.merge_groups[merge_group_object.i3d_merge_group_index].name
self.skin_bind_ids = f"{id_:d} "
super().__init__(id_=id_, mesh_object=merge_group_object, i3d=i3d, parent=parent)

Expand Down
2 changes: 0 additions & 2 deletions addon/i3dio/ui/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def register(cls):
classes.append(cls)
return cls


@register
class I3DExportUIProperties(bpy.types.PropertyGroup):
selection: EnumProperty(
Expand Down Expand Up @@ -168,7 +167,6 @@ class I3DExportUIProperties(bpy.types.PropertyGroup):
default='CLEAN'
)


@register
@orientation_helper(axis_forward='-Z', axis_up='Y')
class I3D_IO_OT_export(Operator, ExportHelper):
Expand Down
218 changes: 195 additions & 23 deletions addon/i3dio/ui/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Panel
)

from bpy.app.handlers import (persistent, save_pre, load_post)

from bpy.props import (
StringProperty,
BoolProperty,
Expand Down Expand Up @@ -557,20 +559,40 @@ def draw(self, context):
obj.i3d_attributes.property_unset('weather_required_mask')
obj.i3d_attributes.property_unset('weather_prevent_mask')


@register
class I3DMergeGroupObjectData(bpy.types.PropertyGroup):
is_root: BoolProperty(
name="Root of merge group",
description="Check if this object is gonna be the root object holding the mesh",
default=False
)

group_id: StringProperty(name='Merge Group',
description='The merge group this object belongs to',
default=''
)
class I3DMergeGroupMemberObject(bpy.types.PropertyGroup):
object: PointerProperty(
name="Merge Group Member",
type=bpy.types.Object,
)

@register
class I3DMergeGroup(bpy.types.PropertyGroup):
def add_member(self, member_object):
self.members.add().object = member_object

def remove_member(self, member_object):
for idx,member in enumerate(self.members):
if member.object is member_object:
self.members.remove(idx)

name: StringProperty(
name='Merge Group Name',
description='The name of the merge group',
default='MergeGroup'
)

members: CollectionProperty(
name="Members",
description="Members of the merge group",
type=I3DMergeGroupMemberObject)

root: PointerProperty(
name="Merge Group Root Object",
description="The object acting as the root for the merge group",
type=bpy.types.Object,
)

@register
class I3D_IO_PT_merge_group_attributes(Panel):
bl_space_type = 'PROPERTIES'
Expand All @@ -587,16 +609,162 @@ def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
obj = bpy.context.active_object
obj = context.object

row = layout.row()
row.prop(obj.i3d_merge_group, 'is_root')
if obj.i3d_merge_group.group_id == '': # Defaults to a default initialized placeholder
row.enabled = False
row = layout.row(align=True)
row.use_property_decorate = False

row = layout.row()
row.prop(obj.i3d_merge_group, 'group_id')
row.operator('i3dio.choose_merge_group', text="", icon='DOWNARROW_HLT')

col = row.column(align=True)
merge_group_index = obj.i3d_merge_group_index
if merge_group_index == -1:
col.operator("i3dio.new_merge_group", text="New", icon="ADD")
else:
merge_group = context.scene.i3dio_merge_groups[merge_group_index]
col.prop(merge_group, "name", text="")
col = row.column(align=True)
col.operator('i3dio.select_merge_group_root', text="", icon="COLOR_RED")
col = row.column(align=True)
col.operator('i3dio.select_mg_objects', text="", icon='GROUP_VERTEX')
col = row.column(align=True)
col.operator('i3dio.new_merge_group', text="", icon='DUPLICATE')
col = row.column(align=True)
col.operator('i3dio.remove_merge_group', text="", icon='PANEL_CLOSE')


@register
class I3D_IO_OT_choose_merge_group(bpy.types.Operator):
bl_idname = "i3dio.choose_merge_group"
bl_label = "Choose Merge Group"
bl_description = "Choose a merge group to assign this object to"
bl_options = {'INTERNAL', 'UNDO'}
bl_property = "enum"

def get_enum_options(self, context):
merge_groups_item_list = sorted([(str(idx), mg.name, "") for idx,mg in enumerate(context.scene.i3dio_merge_groups)],key=lambda x: x[1])
return merge_groups_item_list

enum: EnumProperty(items=get_enum_options, name="Items")

def execute(self, context):
obj = context.object
selected_mg_index = int(self.enum)
if obj.i3d_merge_group_index != selected_mg_index:
context.scene.i3dio_merge_groups[obj.i3d_merge_group_index].remove_member(obj)
context.scene.i3dio_merge_groups[selected_mg_index].add_member(obj)
obj.i3d_merge_group_index = selected_mg_index
context.area.tag_redraw()
return {"FINISHED"}

def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"RUNNING_MODAL"}

@register
class I3D_IO_OT_new_merge_group(bpy.types.Operator):
bl_idname = "i3dio.new_merge_group"
bl_label = "New Merge Group"
bl_description = "Create a new merge group"
bl_options = {'INTERNAL', 'UNDO'}

def execute(self, context):
MERGE_GROUP_DEFAULT_NAME = "MergeGroup"

obj = context.object
name = MERGE_GROUP_DEFAULT_NAME
count = 1
while context.scene.i3dio_merge_groups.find(name) != -1:
name = f"{MERGE_GROUP_DEFAULT_NAME}.{count:03d}"
count += 1
mg = context.scene.i3dio_merge_groups.add()
if obj.i3d_merge_group_index != -1:
context.scene.i3dio_merge_groups[obj.i3d_merge_group_index].remove_member(obj)
mg.name = name
mg.root = obj
mg.add_member(obj)
obj.i3d_merge_group_index = len(context.scene.i3dio_merge_groups) - 1
return {'FINISHED'}


@register
class I3D_IO_OT_remove_merge_group(bpy.types.Operator):
bl_idname = "i3dio.remove_merge_group"
bl_label = "Remove From Merge Group"
bl_description = "Remove this object from it's current merge group"
bl_options = {'INTERNAL', 'UNDO'}

def execute(self, context):
mg_index = context.object.i3d_merge_group_index
for member in context.scene.i3dio_merge_groups[mg_index].members:
member.object.i3d_merge_group_index = -1
context.scene.i3dio_merge_groups.remove(mg_index)
for mg in context.scene.i3dio_merge_groups[mg_index::]:
for member in mg.members:
member.object.i3d_merge_group_index -= 1
return {'FINISHED'}

@register
class I3D_IO_OT_select_merge_group_root(bpy.types.Operator):
bl_idname = "i3dio.select_merge_group_root"
bl_label = "Select Merge Group Root"
bl_description = "When greyed out it means that the current object is the merge group root"
bl_options = {'INTERNAL'}

@classmethod
def poll(cls, context):
return context.scene.i3dio_merge_groups[context.object.i3d_merge_group_index].root is not context.object

def execute(self, context):
context.scene.i3dio_merge_groups[context.object.i3d_merge_group_index].root = context.object
return {'FINISHED'}

@register
class I3D_IO_OT_select_mg_objects(bpy.types.Operator):
bl_idname = "i3dio.select_mg_objects"
bl_label = "Select Objects in MG"
bl_description = "Select all objects in the same merge group"
bl_options = {'UNDO'}

@classmethod
def poll(cls, context):
return context.object.i3d_merge_group_index != -1

def execute(self, context):
for member in context.scene.i3dio_merge_groups[context.object.i3d_merge_group_index].members:
if member.object is not None:
member.object.select_set(True)
else:
print("Deleted Member Object")
return {'FINISHED'}

@persistent
def prune_merge_groups(dummy):
for scene in bpy.data.scenes:
for mg in scene.i3dio_merge_groups:
for idx, member in reversed(list(enumerate(mg.members))):
if member.object is None:
mg.members.remove(idx)

@persistent
def handle_old_merge_groups(dummy):
for scene in bpy.data.scenes:
for obj in scene.objects:
if (old_mg := obj.get('i3d_merge_group')) != None:
group_id = old_mg.get('group_id')
is_root = old_mg.get('is_root')
if group_id != None and group_id != "":
if (mg_idx := scene.i3dio_merge_groups.find(group_id)) != -1:
mg = scene.i3dio_merge_groups[mg_idx]
obj.i3d_merge_group_index = mg_idx
else:
mg = scene.i3dio_merge_groups.add()
mg.name = group_id
obj.i3d_merge_group_index = len(scene.i3dio_merge_groups) - 1
mg.add_member(obj)
if is_root != None and is_root == 1:
mg.root = obj
del obj['i3d_merge_group']

@register
class I3D_IO_PT_joint_attributes(Panel):
Expand Down Expand Up @@ -679,18 +847,22 @@ def draw(self, context):
row = layout.row()
row.prop(obj.i3d_mapping, 'mapping_name')


def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Object.i3d_attributes = PointerProperty(type=I3DNodeObjectAttributes)
bpy.types.Object.i3d_merge_group = PointerProperty(type=I3DMergeGroupObjectData)
bpy.types.Object.i3d_merge_group_index = IntProperty(default = -1)
bpy.types.Object.i3d_mapping = PointerProperty(type=I3DMappingData)

bpy.types.Scene.i3dio_merge_groups = CollectionProperty(type=I3DMergeGroup)
save_pre.append(prune_merge_groups)
load_post.append(handle_old_merge_groups)

def unregister():
load_post.remove(handle_old_merge_groups)
save_pre.remove(prune_merge_groups)
del bpy.types.Scene.i3dio_merge_groups
del bpy.types.Object.i3d_mapping
del bpy.types.Object.i3d_merge_group
del bpy.types.Object.i3d_merge_group_index
del bpy.types.Object.i3d_attributes

for cls in classes:
Expand Down

0 comments on commit f48c9cb

Please sign in to comment.