1255 lines
49 KiB
Python
1255 lines
49 KiB
Python
from . import base_package
|
|
|
|
import bpy, os
|
|
|
|
from bpy.types import (UIList, Panel, Operator, Menu)
|
|
from bpy.props import *
|
|
from collections import defaultdict
|
|
|
|
from pathlib import Path
|
|
|
|
folder_path = os.path.dirname(__file__)
|
|
|
|
options = [
|
|
'localize_collections',
|
|
'localize_objects',
|
|
'localize_meshes',
|
|
'localize_materials',
|
|
'localize_node_groups',
|
|
'localize_images',
|
|
'localize_armatures',
|
|
'localize_actions',
|
|
]
|
|
|
|
extra_types = [
|
|
'localize_lights',
|
|
'localize_cameras',
|
|
'localize_curves',
|
|
'localize_text_curves',
|
|
'localize_metaballs',
|
|
'localize_surface_curves',
|
|
'localize_volumes',
|
|
'localize_grease_pencil',
|
|
]
|
|
|
|
op_props = [
|
|
'activate',
|
|
'blend',
|
|
'collection',
|
|
'folder',
|
|
'index',
|
|
'object'
|
|
]
|
|
|
|
def only(item, *argv):
|
|
for arg in argv:
|
|
if arg != item:
|
|
return False
|
|
return True
|
|
|
|
def load_data(op: bpy.types.Operator, context: bpy.types.Context, scene_viewlayer, *, post_process=False, ind_prefs=None, obj:bpy.types.Object=None, col:bpy.types.Collection=None, ):
|
|
from typing import Dict, Set
|
|
from bpy.types import ID
|
|
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
props = context.scene.optiploy_props
|
|
scene, view_layer = scene_viewlayer
|
|
scene: bpy.types.Scene
|
|
view_layer: bpy.types.ViewLayer
|
|
activeCol = view_layer.active_layer_collection.collection
|
|
|
|
bone_shapes = set()
|
|
arms = set()
|
|
map_to_do = {}
|
|
gatherings = {
|
|
'override': list(),
|
|
'linked': list()
|
|
}
|
|
rev_leveled_map = dict()
|
|
refd_by = defaultdict(set)
|
|
|
|
def remap():
|
|
for linked, local in list(map_to_do.items()):
|
|
linked.user_remap(local)
|
|
map_to_do.clear()
|
|
|
|
def clean_remap(TYPE):
|
|
for ID in filter(lambda a: isinstance(a, TYPE), gatherings['override']):
|
|
map_to_do[ID] = ID.make_local()
|
|
remap()
|
|
for ID in filter(lambda a: isinstance(a, TYPE), gatherings['linked']):
|
|
map_to_do[ID] = ID.make_local()
|
|
remap()
|
|
|
|
def get_id_reference_map() -> Dict[ID, Set[ID]]:
|
|
"""Return a dictionary of direct datablock references for every datablock in the blend file."""
|
|
inv_map = {}
|
|
for key, values in bpy.data.user_map().items():
|
|
for value in values:
|
|
if value == key:
|
|
# So an object is not considered to be referencing itself.
|
|
continue
|
|
inv_map.setdefault(value, set()).add(key)
|
|
return inv_map
|
|
|
|
|
|
def recursive_get_referenced_ids(
|
|
ref_map: Dict[ID, Set[ID]], id: ID, referenced_ids: Set, visited: Set, level
|
|
):
|
|
"""Recursively populate referenced_ids with IDs referenced by id."""
|
|
if id in visited:
|
|
# Avoid infinite recursion from circular references.
|
|
return
|
|
visited.add(id)
|
|
|
|
if isinstance(id, bpy.types.Object) and isinstance(getattr(id, 'data', None), bpy.types.Armature):
|
|
arms.add(id)
|
|
bone_shapes.update(set(bone.custom_shape for bone in id.pose.bones))
|
|
OP_keep = list()
|
|
for ref in ref_map.get(id, []):
|
|
skip = False
|
|
if (ref in bone_shapes) and (id in arms):
|
|
continue
|
|
if id in refd_by[ref]:
|
|
# if the current ID was already referenced by its reference, then don't process it.
|
|
continue
|
|
|
|
refd_by[id].add(ref)
|
|
rev_leveled_map[ref] = max(rev_leveled_map.get(ref, -1), level)
|
|
|
|
if isinstance(ref, bpy.types.Collection) and not (ref in tuple(scene.collection.children_recursive)) and not (getattr(id.override_library, 'reference', None) == ref):
|
|
#ref.use_fake_user = True
|
|
OP_keep.append(ref)
|
|
continue
|
|
|
|
if isinstance(ref, bpy.types.Object) and not (ref in tuple(view_layer.objects)) and not (getattr(id.override_library, 'reference', None) == ref):
|
|
#ref.use_fake_user = True
|
|
OP_keep.append(ref)
|
|
continue
|
|
|
|
referenced_ids.add(ref)
|
|
recursive_get_referenced_ids(
|
|
ref_map=ref_map, id=ref, referenced_ids=referenced_ids, visited=visited, level=level+1
|
|
)
|
|
if OP_keep: id['OP_keep'] = OP_keep
|
|
|
|
def get_all_referenced_ids(id: ID, ref_map: Dict[ID, Set[ID]]) -> Set[ID]:
|
|
"""Return a set of IDs directly or indirectly referenced by id."""
|
|
referenced_ids = set()
|
|
rev_leveled_map[id] = 0
|
|
recursive_get_referenced_ids(
|
|
ref_map=ref_map, id=id, referenced_ids=referenced_ids, visited=set(), level=0
|
|
)
|
|
return referenced_ids
|
|
|
|
# Need local versions of bpy_extras.id_map_utils to modify how I see fit.
|
|
# Changes include:
|
|
|
|
# Finding at what level IDs are referenced
|
|
|
|
# Preventing IDs from being processed if they reference an ID who has referenced the current ID
|
|
|
|
# Collections and objects are overridden by default through override_hierarchy_create
|
|
override_support = (
|
|
bpy.types.Mesh,
|
|
bpy.types.Material,
|
|
bpy.types.SurfaceCurve,
|
|
bpy.types.Light,
|
|
bpy.types.Curve,
|
|
bpy.types.GreasePencil,
|
|
getattr(bpy.types, 'GreasePencilv3', bpy.types.GreasePencil),
|
|
bpy.types.MetaBall,
|
|
bpy.types.TextCurve,
|
|
bpy.types.Volume,
|
|
bpy.types.Armature,
|
|
bpy.types.Camera
|
|
#bpy.types.ShaderNodeTree,
|
|
#bpy.types.GeometryNodeTree,
|
|
#bpy.types.Image,
|
|
|
|
# do NOT add images to this lol
|
|
# i think it actually just makes a copy of the image. bad for optimization
|
|
|
|
)
|
|
|
|
additional = list()
|
|
prime_override = dict()
|
|
|
|
def override_order(reference):
|
|
rev_l = list(reversed(sorted(list(
|
|
rev_leveled_map.items()
|
|
) + additional, key=lambda a: a[1])))
|
|
|
|
for ID, _ in rev_l:
|
|
ID: bpy.types.ID
|
|
scene['test_prop'] = ID
|
|
possible_override = ID.override_create(remap_local_usages=True)
|
|
del scene['test_prop']
|
|
if possible_override != None:
|
|
drivers = getattr(getattr(getattr(possible_override, 'shape_keys', None),'animation_data', None),'drivers', None)
|
|
if drivers:
|
|
[setattr(target, 'id', possible_override) if target.id == ID else None for driver in drivers for variable in driver.driver.variables for target in variable.targets]
|
|
if (prime := prime_override.get(ID)) != None:
|
|
possible_override.user_remap(prime)
|
|
if isinstance(possible_override, bpy.types.Mesh) and (getattr(possible_override, 'shape_keys', None) != None):
|
|
bpy.data.batch_remove({possible_override.shape_keys})
|
|
bpy.data.batch_remove({possible_override})
|
|
else:
|
|
prime_override[ID] = possible_override
|
|
possible_override.use_fake_user = True
|
|
|
|
if ID == reference:
|
|
old, spawned = ID, possible_override
|
|
return spawned
|
|
|
|
def recurse2(ID, level=0, line:list=[]):
|
|
'''
|
|
|
|
This function was the missing piece of a puzzle. OptiPloy is complete now. No more errors when spawning.
|
|
I'd been searching for this functionality for EVER. Finally found it, without AI and without searching.
|
|
I feel like I have to flaunt it, idk
|
|
I know how simple it is, but what it does is so crucial
|
|
|
|
UPDATE:
|
|
I was so wrong when I wrote that and I shattered into pieces when I realized it didn't work.
|
|
Now it does!
|
|
The old version that message is referring to did not account for loops in the user hierarchy. This one does.
|
|
If ID2 is referencing/using ID1 but has already been in the "line", we need to stop here.
|
|
So instead of infinitely looping, lets mention ID1 again in the overriding process specifically for ID2.
|
|
So after the duplicate ID1 has been overridden, we can replace it with the original ID1 that was overridden.
|
|
|
|
We can't replace the linked IDs with the overridden IDs or else the overridden IDs will reference themselves.
|
|
THAT causes a data corruption error. But that's not happening in this case.
|
|
|
|
Now there are zero errors :)
|
|
|
|
UPDATE 2 may 24 2025:
|
|
sisyphean struggle
|
|
|
|
UPDATE 3 may 25 2025:
|
|
i talked with zayjax today about their rigs, and how one of their very complicated rigs broke the importer.
|
|
i also told them how i worked around it and fixed the importer. i had him try the importer on his many rigs,
|
|
and it worked. EVERY. SINGLE. TIME. could this be it??
|
|
|
|
UPDATE 4 may 27 2025:
|
|
i talked with dotflare this time about one of their problems. there was an issue in the way objects and collections
|
|
are handled if they are indirectly referenced by the import. if they are used, they are prone to getting deleted
|
|
because they have no users. somehow. so i attach them to the ID that uses those objects to keep them from getting
|
|
deleted.
|
|
|
|
'''
|
|
|
|
if rev_leveled_map.get(ID, -1) >= level: return
|
|
if type(ID) != bpy.types.Key:
|
|
rev_leveled_map[ID] = level
|
|
line = list(line)
|
|
line.append(ID)
|
|
|
|
refs = bpy.data.user_map().get(ID, [])
|
|
# refs is the list of IDs that are using the given ID
|
|
for ref in refs:
|
|
if ref == ID: continue
|
|
if type(ref) == bpy.types.Key:
|
|
if getattr(ID, 'shape_keys', None) == ID: continue
|
|
if getattr(ref, 'library', None) == None: continue
|
|
if ID in refd_by[ref]:
|
|
continue
|
|
refd_by[ID].add(ref)
|
|
if ref in line:
|
|
additional.append((ID, line.index(ref)-1))
|
|
continue
|
|
|
|
recurse2(ref, level + 1, line)
|
|
|
|
if obj:
|
|
if not obj in list(view_layer.objects):
|
|
activeCol.objects.link(obj)
|
|
if ind_prefs.importer == 'STABLE':
|
|
new_obj = obj.override_hierarchy_create(scene, view_layer, reference=None, do_fully_editable=True)
|
|
for user_col in obj.users_collection:
|
|
if user_col.library: continue
|
|
user_col.objects.unlink(obj)
|
|
obj = new_obj
|
|
if obj == None: return {'CANCELLED'}
|
|
spawned = obj
|
|
else:
|
|
parent = obj
|
|
while parent.parent:
|
|
parent = parent.parent
|
|
if parent in list(view_layer.objects): continue
|
|
activeCol.objects.link(parent)
|
|
rev_leveled_map[obj] = 0
|
|
recurse2(obj, 1)
|
|
spawned = override_order(obj)
|
|
override_order(obj)
|
|
rev_leveled_map.clear()
|
|
refd_by.clear()
|
|
|
|
if col:
|
|
if not col in scene.collection.children_recursive:
|
|
scene.collection.children.link(col)
|
|
if ind_prefs.importer == 'STABLE':
|
|
col_users = bpy.data.user_map(subset=[col])[col]
|
|
new_col = col.override_hierarchy_create(scene, view_layer, reference=None, do_fully_editable=True)
|
|
if new_col == None: return {'CANCELLED'}
|
|
for user in col_users:
|
|
if isinstance(user, bpy.types.Scene):
|
|
if col in list(user.collection.children):
|
|
user.collection.children.unlink(col)
|
|
if isinstance(user, bpy.types.Collection):
|
|
if user.library: continue
|
|
if col in list(user.children):
|
|
user.children.unlink(col)
|
|
col = new_col
|
|
spawned = col
|
|
else:
|
|
recurse2(col, 0, [])
|
|
for object in list(col.all_objects):
|
|
if object.parent: continue
|
|
recurse2(object, 0, [])
|
|
spawned = override_order(col)
|
|
override_order(col)
|
|
rev_leveled_map.clear()
|
|
refd_by.clear()
|
|
|
|
#return {'FINISHED'}
|
|
|
|
id_ref = get_id_reference_map()
|
|
id_ref = get_all_referenced_ids(spawned, id_ref)
|
|
|
|
sorted_refs = list(map(lambda a: a[0],
|
|
sorted(list(
|
|
rev_leveled_map.items()
|
|
), key=lambda a: a[1])
|
|
))
|
|
|
|
rev_leveled_map.clear()
|
|
refd_by.clear()
|
|
|
|
for ID in filter(lambda a: getattr(a, 'library', None) != None, sorted_refs):
|
|
if isinstance(ID, override_support):
|
|
possible_override = ID.override_create(remap_local_usages=True)
|
|
if possible_override != None:
|
|
drivers = getattr(getattr(getattr(possible_override, 'shape_keys', None),'animation_data', None),'drivers', None)
|
|
if drivers:
|
|
[setattr(target, 'id', possible_override) if target.id == ID else None for driver in drivers for variable in driver.driver.variables for target in variable.targets]
|
|
# This is really specific, but with good cause.
|
|
# Say you have a mesh ID with a shape key ID, and the shape key has values that are being driven by the mesh ID.
|
|
# On some occasions, when creating an overridden mesh with a shape key ID (which will therefore creating an overridden copy of the shape key), the values on the shape key ID will continue to be driven by the linked mesh. This "function comprehension" corrects that.
|
|
|
|
# Don't know how many other situations where something like this can happen, but I don't imagine it being difficult to fix.
|
|
|
|
# This has led me to come across a serious design flaw in Blender, but one I'm not sure can be fixed. You can replace IDs with other IDs, even if users are using those IDs on a read-only attribute. It will lead to data corruption.
|
|
|
|
ID = possible_override
|
|
|
|
gatherings['linked'].append(ID)
|
|
|
|
for ID in filter(lambda a: getattr(a, 'override_library', None) != None, sorted_refs):
|
|
if ID.override_library.reference in gatherings['linked']:
|
|
gatherings['linked'].remove(ID.override_library.reference)
|
|
gatherings['override'].append(ID)
|
|
|
|
'''
|
|
|
|
What's the reason for all this weird code?
|
|
|
|
This is the result of my desire for "data isolation." When OptiPloy used my first method of linking, spawning previously spawned collections with different settings would localize
|
|
the data in the previously spawned collections. That really annoyed me. Using bpy.data.temp_data() didn't help, because linking within it would break Blender, and my only real
|
|
solution was to have a second instance of Blender running to prepare the data for the main instance to use, which actually worked. But I didn't like it, because despite using
|
|
factory settings, its RAM usage would increase with every spawned item. Too bad considering how well it worked. But I *really* wanted it to all be local.
|
|
|
|
My solution? Library overrides!
|
|
|
|
Library overridden IDs float between a state of localized and linked. Technically with every "LO" ID, you *are* increasing storage usage, but not as much as you would be since it
|
|
still very much relies on the linked stuff. It *is* a good solution for this "data isolation" concept, because it prevents the localization of pre-existing data, but its far from
|
|
the best one. I have to implement checks for specific things. I don't doubt that makes people unhappy, but its not like this code is being ran 24/7. Right now, the only checks
|
|
being performed are for drivers between meshes and their shape keys (literally) and preventing bone shapes from being processed if they are only used by any armature. It's
|
|
possible I'll run into more situations that I need to counter, but it cannot be that hard to fix.
|
|
|
|
Famous last words?
|
|
|
|
'''
|
|
|
|
if ind_prefs.localize_collections:
|
|
clean_remap(bpy.types.Collection)
|
|
if ind_prefs.localize_objects:
|
|
clean_remap(bpy.types.Object)
|
|
if ind_prefs.localize_meshes:
|
|
clean_remap(bpy.types.Mesh)
|
|
if ind_prefs.localize_armatures:
|
|
clean_remap(bpy.types.Armature)
|
|
if ind_prefs.localize_materials:
|
|
clean_remap(bpy.types.Material)
|
|
if ind_prefs.localize_node_groups:
|
|
clean_remap(bpy.types.NodeGroup)
|
|
clean_remap(bpy.types.GeometryNodeTree)
|
|
clean_remap(bpy.types.ShaderNodeTree)
|
|
if ind_prefs.localize_images:
|
|
clean_remap(bpy.types.Image)
|
|
if ind_prefs.localize_actions:
|
|
clean_remap(bpy.types.Action)
|
|
|
|
if ind_prefs.localize_surface_curves:
|
|
clean_remap(bpy.types.SurfaceCurve)
|
|
if ind_prefs.localize_lights:
|
|
clean_remap(bpy.types.Light)
|
|
if ind_prefs.localize_cameras:
|
|
clean_remap(bpy.types.Camera)
|
|
if ind_prefs.localize_curves:
|
|
clean_remap(bpy.types.Curve)
|
|
if ind_prefs.localize_text_curves:
|
|
clean_remap(bpy.types.TextCurve)
|
|
if ind_prefs.localize_metaballs:
|
|
clean_remap(bpy.types.MetaBall)
|
|
if ind_prefs.localize_volumes:
|
|
clean_remap(bpy.types.Volume)
|
|
if ind_prefs.localize_grease_pencil:
|
|
clean_remap(bpy.types.GreasePencil)
|
|
clean_remap(bpy.types.GreasePencilv3)
|
|
|
|
if op.do_storage_benchmark:
|
|
return spawned
|
|
|
|
if col and prefs.to_cursor:
|
|
for object in spawned.all_objects:
|
|
if object.parent: continue
|
|
object.location = scene.cursor.location
|
|
|
|
if obj and prefs.to_cursor:
|
|
top = spawned
|
|
while top.parent != None:
|
|
top = top.parent
|
|
top.location = scene.cursor.location
|
|
|
|
context.scene['new_spawn'] = spawned # assign the newly spawned item to a globally accessible variable, giving developers the opportunity to further modify data in the scripts execution stage
|
|
scene['optiploy_last_spawned'] = spawned
|
|
context.scene['optiploy_last_spawned'] = spawned
|
|
if prefs.execute_scripts:
|
|
for text in filter(lambda a: isinstance(a, bpy.types.Text), gatherings['linked']):
|
|
text.as_module()
|
|
|
|
scn = context.scene
|
|
|
|
# init rigid body physics
|
|
for id in filter(lambda a: isinstance(a, bpy.types.Object), gatherings['override']):
|
|
if getattr(id, 'rigid_body', None):
|
|
if scn.rigidbody_world == None:
|
|
bpy.ops.rigidbody.world_add()
|
|
if (rbw := getattr(scn.rigidbody_world, 'collection', None)) == None:
|
|
rbw = bpy.data.collections.new('RigidBodyWorld')
|
|
scn.rigidbody_world.collection = rbw
|
|
if not id in list(rbw.objects): rbw.objects.link(id)
|
|
if getattr(id, 'rigid_body_constraint', None):
|
|
if scn.rigidbody_world == None:
|
|
bpy.ops.rigidbody.world_add()
|
|
if (rbc := getattr(scn.rigidbody_world, 'constraints', None)) == None:
|
|
rbc = bpy.data.collections.new('RigidBodyConstraints')
|
|
scn.rigidbody_world.constraints = rbc
|
|
if not id in list(rbc.objects): rbc.objects.link(id)
|
|
|
|
del context.scene['new_spawn']
|
|
|
|
map_to_do.clear()
|
|
gatherings['linked'].clear()
|
|
gatherings['override'].clear()
|
|
arms.clear()
|
|
additional.clear()
|
|
bone_shapes.clear()
|
|
del sorted_refs, map_to_do, gatherings
|
|
|
|
bpy.data.orphans_purge(True, False, True)
|
|
return {'FINISHED'}
|
|
|
|
class SPAWNER_GENERIC_SPAWN_UL_List(UIList):
|
|
def draw_item(self, context,
|
|
layout: bpy.types.UILayout, data,
|
|
item, icon,
|
|
active_data, active_propname,
|
|
index):
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
props = context.scene.optiploy_props
|
|
itemType = item.bl_rna.identifier
|
|
Type = 'OBJECT' if itemType == 'objects' else 'COLLECTION'
|
|
Icon = 'OBJECT_DATA' if itemType == 'objects' else 'OUTLINER_COLLECTION'
|
|
row = layout.row()
|
|
row.label(text=item.name, icon=Icon)
|
|
row = row.row()
|
|
row.alignment='RIGHT'
|
|
if Type == 'OBJECT':
|
|
if index != prefs.obj_index:
|
|
row.label(text='Spawn')
|
|
row.separator()
|
|
row.enabled = False
|
|
return
|
|
|
|
if Type == 'COLLECTION':
|
|
if index != prefs.col_index:
|
|
row.label(text='Spawn')
|
|
row.separator()
|
|
row.enabled = False
|
|
return
|
|
row = row.row(align=True)
|
|
row.alignment = 'RIGHT'
|
|
benchmark_op = row.operator('spawner.spawner', icon='DISK_DRIVE', text='')
|
|
op = row.operator('spawner.spawner')
|
|
op.activate=True
|
|
if props.view == 'BLENDS':
|
|
op.blend = int(props.selected_blend)
|
|
op.folder = -1
|
|
if props.view == 'FOLDERS':
|
|
folder = prefs.folders[int(props.selected_folder)]
|
|
op.folder = int(props.selected_folder)
|
|
op.blend = int(folder.selected_blend)
|
|
if Type == 'OBJECT':
|
|
op.object = item.name
|
|
op.collection = ''
|
|
if Type == 'COLLECTION':
|
|
op.collection = item.name
|
|
op.object = ''
|
|
|
|
for attr in op_props:
|
|
setattr(benchmark_op, attr, getattr(op, attr))
|
|
benchmark_op.do_storage_benchmark = True
|
|
op.do_storage_benchmark = False
|
|
|
|
class SPAWNER_PT_folder_settings(Panel):
|
|
bl_label = 'Settings'
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'WINDOW'
|
|
|
|
bl_options = {'INSTANCED'}
|
|
|
|
def draw(self, context):
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
props = context.scene.optiploy_props
|
|
layout = self.layout
|
|
layout.label(text='Folder Settings')
|
|
|
|
folder = prefs.folders[int(props.selected_folder)]
|
|
layout.prop(folder, 'override_behavior')
|
|
box = layout.box()
|
|
r = box.row()
|
|
r.label(text = 'Importer')
|
|
r.operator('wm.url_open', text='', icon='QUESTION').url = os.path.join(os.path.dirname(__file__), 'htmls', 'importers.html')
|
|
box.row().prop(folder, 'importer', expand=True)
|
|
box.enabled = folder.override_behavior
|
|
box = layout.box()
|
|
box.enabled = folder.override_behavior
|
|
for i in options:
|
|
box.prop(folder, i)
|
|
box.label(text='Extra Types')
|
|
for i in extra_types:
|
|
box.prop(folder, i)
|
|
|
|
class SPAWNER_PT_blend_settings(Panel):
|
|
bl_label = 'Settings'
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'WINDOW'
|
|
|
|
bl_options = {'INSTANCED'}
|
|
|
|
def draw(self, context):
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
props = context.scene.optiploy_props
|
|
layout = self.layout
|
|
folder = None
|
|
if props.view == 'BLENDS':
|
|
blend = prefs.blends[int(props.selected_blend)]
|
|
else:
|
|
folder = prefs.folders[int(props.selected_folder)]
|
|
blend = folder.blends[int(folder.selected_blend)]
|
|
layout.label(text='Blend Settings')
|
|
row = layout.row()
|
|
row.prop(blend, 'override_behavior')
|
|
#row.enabled = getattr(folder, 'override_behavior', True)
|
|
box = layout.box()
|
|
box.enabled = blend.override_behavior
|
|
r = box.row()
|
|
r.label(text = 'Importer')
|
|
r.operator('wm.url_open', text='', icon='QUESTION').url = os.path.join(os.path.dirname(__file__), 'htmls', 'importers.html')
|
|
box.row().prop(blend, 'importer', expand=True)
|
|
box = layout.box()
|
|
box.enabled = blend.override_behavior
|
|
for i in options:
|
|
box.prop(blend, i)
|
|
box.label(text='Extra Types')
|
|
for i in extra_types:
|
|
box.prop(blend, i)
|
|
|
|
class SPAWNER_PT_extra_settings(Panel):
|
|
bl_label = 'Extra Localization Options'
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'WINDOW'
|
|
|
|
bl_options = {'INSTANCED'}
|
|
|
|
def draw(self, context):
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
props = context.scene.optiploy_props
|
|
layout = self.layout
|
|
layout.label(text=self.bl_label)
|
|
box = layout.box()
|
|
for i in extra_types:
|
|
box.prop(prefs, i)
|
|
|
|
class SPAWNER_PT_panel(Panel):
|
|
bl_label = 'OptiPloy'
|
|
bl_space_type='VIEW_3D'
|
|
bl_region_type='UI'
|
|
bl_category = 'OptiPloy'
|
|
|
|
def draw(self, context):
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
data = prefs
|
|
props = context.scene.optiploy_props
|
|
layout = self.layout
|
|
box = layout.box()
|
|
row = box.row()
|
|
row.label(text='View Mode')
|
|
row.alignment = 'RIGHT'
|
|
row.scale_x = 2
|
|
row.scale_y = 1.6
|
|
row.prop(props, 'view', expand=True, icon_only=True, text='View')
|
|
|
|
if props.view == 'BLENDS':
|
|
if not len(prefs.blends):
|
|
layout.row().label(text='Add a .blend file in the preferences to get started!')
|
|
return
|
|
blend_ind = int(props.selected_blend or '0')
|
|
blend = prefs.blends[blend_ind]
|
|
|
|
row = layout.row()
|
|
row.alert = True
|
|
op = row.operator('spawner.open_blend', text='', icon='BLENDER')
|
|
op.text = '''Hold CTRL to reload the .blend file as a library.
|
|
Hold SHIFT to open the .blend file in a new instance of Blender.
|
|
Hold ALT to re-scan the .blend file in OptiPloy.
|
|
Don't worry about the button being red. It's only meant to call your attention.'''
|
|
op.icons='EVENT_CTRL,EVENT_SHIFT,EVENT_ALT,QUESTION'
|
|
op.size='56,56,56,56'
|
|
op.width=350
|
|
op.path = blend.filepath
|
|
op.folder = -1
|
|
op.blend = int(props.selected_blend or '0')
|
|
|
|
row = row.row()
|
|
row.alert = False
|
|
row.prop(props, 'selected_blend', text='')
|
|
row.popover('SPAWNER_PT_blend_settings', text='', icon='SETTINGS')
|
|
|
|
if props.view == 'FOLDERS':
|
|
if not len(prefs.folders):
|
|
layout.row().label(text='Add a folder of .blend files in the preferences to get started!')
|
|
return
|
|
folder_ind = int(props.selected_folder or '0')
|
|
folder = prefs.folders[folder_ind]
|
|
|
|
row = layout.row()
|
|
row.alert=True
|
|
op = row.operator('spawner.open_folder', text='', icon='FILE_FOLDER')
|
|
op.text = '''Hold ALT to re-scan the .blend file in OptiPloy.
|
|
Don't worry about the button being red. It's only meant to call your attention.'''
|
|
op.icons='EVENT_ALT,QUESTION'
|
|
op.size='56,56'
|
|
op.width=350
|
|
op.folder = folder_ind
|
|
row = row.row()
|
|
row.alert=False
|
|
#row.label(text='', icon='FILE_FOLDER')
|
|
row.prop(props, 'selected_folder', text='')
|
|
row.popover('SPAWNER_PT_folder_settings', text='', icon='SETTINGS')
|
|
if not len(folder.blends):
|
|
layout.row().label(text='This folder has no .blend files marked! Has it been scanned?')
|
|
return None
|
|
blend_ind = int(folder.selected_blend or '0')
|
|
blend = folder.blends[blend_ind]
|
|
row = layout.row()
|
|
row.alert = True
|
|
#row.label(text='', icon='BLENDER')
|
|
op = row.operator('spawner.open_blend', text='', icon='BLENDER')
|
|
op.text = '''Hold CTRL to reload the .blend file as a library.
|
|
Hold SHIFT to open the .blend file in a new instance of Blender.
|
|
Hold ALT to re-scan the .blend file in OptiPloy.
|
|
Don't worry about the button being red. It's only meant to call your attention.'''
|
|
op.icons='EVENT_CTRL,EVENT_SHIFT,EVENT_ALT,QUESTION'
|
|
op.size='56,56,56,56'
|
|
op.width=350
|
|
op.path = blend.filepath
|
|
op.folder = int(int(props.selected_folder))
|
|
op.blend = int(folder.selected_blend)
|
|
|
|
row = row.row()
|
|
row.alert = False
|
|
row.prop(folder, 'selected_blend', text='')
|
|
row.popover('SPAWNER_PT_blend_settings', text='', icon='SETTINGS')
|
|
|
|
if props.view != 'TOOLS':
|
|
box = layout.box()
|
|
if not (len(blend.objects) + len(blend.collections)):
|
|
box.row().label(text="Nothing in this file detected! Mark items as assets!")
|
|
return
|
|
objBox = box.box()
|
|
row = objBox.row()
|
|
row.label(text='Objects', icon='OBJECT_DATA')
|
|
op = row.operator('spawner.textbox', icon='QUESTION', text='')
|
|
if len(blend.objects):
|
|
objBox.row().template_list('SPAWNER_GENERIC_SPAWN_UL_List', 'Items',
|
|
blend, 'objects', prefs, 'obj_index')
|
|
op.text = '''Here are the objects you can spawn!
|
|
To spawn an item, it has to be the active item. This serves as a way of confirming.'''
|
|
op.icons = 'OBJECT_DATA,CHECKMARK'
|
|
op.size='56,56'
|
|
op.width=350
|
|
else:
|
|
objBox.row().label(text='No objects added!')
|
|
op.text = '''The selected .blend file has no objects marked as assets!'''
|
|
op.icons = 'ERROR'
|
|
op.size='56'
|
|
op.width=350
|
|
|
|
colBox = box.box()
|
|
row = colBox.row()
|
|
row.label(text='Collections', icon='OUTLINER_COLLECTION')
|
|
op = row.operator('spawner.textbox', icon='QUESTION', text='')
|
|
if len(blend.collections):
|
|
colBox.row().template_list('SPAWNER_GENERIC_SPAWN_UL_List', 'Collections',
|
|
blend, 'collections', prefs, 'col_index')
|
|
op.text = '''Here are the collections you can spawn!
|
|
To spawn an item, it has to be the active item. This serves as a way of confirming.'''
|
|
op.icons = 'OUTLINER_COLLECTION,CHECKMARK'
|
|
op.size='56,56'
|
|
op.width=350
|
|
else:
|
|
colBox.row().label(text='No collections added!')
|
|
op.text = '''The selected .blend file has no collections marked as assets!'''
|
|
op.icons = 'ERROR'
|
|
op.size='56'
|
|
op.width=350
|
|
|
|
if props.view == 'TOOLS':
|
|
col = layout.column()
|
|
r = col.row()
|
|
r.label(text='Importer')
|
|
r.operator('wm.url_open', text='', icon='QUESTION').url = os.path.join(os.path.dirname(__file__), 'htmls', 'importers.html')
|
|
col.box().row().prop(prefs, 'importer', expand=True)
|
|
layout.label(text='Post-Processing')
|
|
box = layout.box()
|
|
box.prop(prefs, 'to_cursor')
|
|
box.prop(prefs, 'execute_scripts')
|
|
layout.label(text='Behavior')
|
|
box = layout.box()
|
|
box.prop(prefs, 'localize_collections')
|
|
box.prop(prefs, 'localize_objects')
|
|
box.prop(prefs, 'localize_meshes')
|
|
box.prop(prefs, 'localize_materials')
|
|
box.prop(prefs, 'localize_node_groups')
|
|
box.prop(prefs, 'localize_images')
|
|
box.prop(prefs, 'localize_armatures')
|
|
box.prop(prefs, 'localize_actions')
|
|
box.popover('SPAWNER_PT_extra_settings')
|
|
layout.operator('preferences.addon_show', text='Open Preferences').module = base_package
|
|
if not context.preferences.use_preferences_save:
|
|
layout.operator('wm.save_userpref')
|
|
layout.separator()
|
|
op = layout.operator('spawner.textbox', text='Donate')
|
|
op.text = '''Like the add-on? Consider supporting my work:
|
|
LINK:https://ko-fi.com/hisanimations|NAME:Ko-Fi
|
|
LINK:https://superhivemarket.com/products/optiploy-pro|NAME:Buy OptiPloy Pro on Superhive'''
|
|
op.size = '56,56,56'
|
|
op.icons = 'BLANK1,NONE,NONE'
|
|
op.width = 350
|
|
|
|
class mod_saver(Operator):
|
|
def invoke(self, context, event):
|
|
ctrl, shift, alt = event.ctrl, event.shift, event.alt
|
|
scn = context.scene
|
|
scn['key_ctrl'] = ctrl
|
|
scn['key_shift'] = shift
|
|
scn['key_alt'] = alt
|
|
|
|
return_val = self.execute(context)
|
|
|
|
del scn['key_ctrl'], scn['key_shift'], scn['key_alt']
|
|
return return_val
|
|
|
|
class SPAWNER_OT_SPAWNER(mod_saver):
|
|
bl_idname = 'spawner.spawner'
|
|
bl_label = 'Spawn'
|
|
bl_description = 'Spawn it!'
|
|
bl_options = {'UNDO'}
|
|
|
|
type: EnumProperty(
|
|
items=(
|
|
('COLLECTION', 'Collection', 'This spawnable is a collection', 'COLLECTION', 0),
|
|
('OBJECT', 'Object', 'This spawnable is an object', 'OBJECT', 1)
|
|
),
|
|
name='Type',
|
|
options=set()
|
|
)
|
|
|
|
blend: IntProperty(default=-1)
|
|
folder: IntProperty(default=-1)
|
|
object: StringProperty(default='')
|
|
collection: StringProperty(default='')
|
|
activate: BoolProperty()
|
|
scene: StringProperty(default='')
|
|
|
|
index: IntProperty()
|
|
|
|
do_storage_benchmark: BoolProperty(default=False)
|
|
compress_append: BoolProperty(default=False, name='Compress Append Result', description='Choose to compress the result after loading the traditional way via appending')
|
|
compress_optiploy: BoolProperty(default=True, name='Compress OptiPloy Result', description='Choose to compress the result after loading via OptiPloy')
|
|
|
|
_time = None
|
|
|
|
def invoke(self, context, event):
|
|
if not self.do_storage_benchmark:
|
|
return super().invoke(context, event)
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def get_prefs(self, context):
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
blend = self.blend
|
|
folder = self.folder
|
|
|
|
if (folder != -1) and (blend != -1):
|
|
folder = prefs.folders[folder]
|
|
if folder.override_behavior:
|
|
prefs = folder
|
|
entry = folder.blends[blend]
|
|
if entry.override_behavior:
|
|
prefs = entry
|
|
|
|
if (blend != -1) and (folder == -1):
|
|
entry = prefs.blends[blend]
|
|
if entry.override_behavior:
|
|
prefs = entry
|
|
return prefs, entry
|
|
|
|
def load_test(self, context):
|
|
obj = self.object
|
|
col = self.collection
|
|
prefs, entry = self.get_prefs(context)
|
|
test_path = os.path.join(os.path.dirname(__file__), 'test.blend')
|
|
temp = bpy.data
|
|
|
|
if (was_saved := temp.is_saved):
|
|
re_open = temp.filepath
|
|
bpy.ops.wm.save_mainfile()
|
|
|
|
bpy.ops.wm.read_homefile(app_template="")
|
|
|
|
with temp.libraries.load(entry.filepath) as (f, t):
|
|
if obj:
|
|
t.objects = [obj]
|
|
if col:
|
|
t.collections = [col]
|
|
item = (t.objects or t.collections)[0]
|
|
temp.libraries.write(test_path, {item}, compress=self.compress_append)
|
|
old_size = os.path.getsize(test_path)
|
|
|
|
bpy.ops.wm.read_homefile(app_template="")
|
|
|
|
with temp.libraries.load(entry.filepath, link=True) as (f, t):
|
|
if obj:
|
|
t.objects = [obj]
|
|
if col:
|
|
t.collections = [col]
|
|
scn = temp.scenes.new('scn')
|
|
item = (t.objects or t.collections)[0]
|
|
|
|
if obj:
|
|
spawned = load_data(self, context, [scn, scn.view_layers[0]], ind_prefs=prefs, obj=item)
|
|
if col:
|
|
spawned = load_data(self, context, [scn, scn.view_layers[0]], ind_prefs=prefs, col=item)
|
|
|
|
temp.libraries.write(test_path, {spawned}, compress=self.compress_optiploy)
|
|
new_size = os.path.getsize(test_path)
|
|
os.remove(test_path)
|
|
if was_saved:
|
|
bpy.ops.wm.open_mainfile(filepath=re_open)
|
|
else:
|
|
bpy.ops.wm.read_homefile(app_template="")
|
|
|
|
def format_size(size_in_bytes):
|
|
"""
|
|
Convert size in bytes to a human-readable format.
|
|
"""
|
|
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
if size_in_bytes < 1024.0:
|
|
return f"{size_in_bytes:.2f} {unit}"
|
|
size_in_bytes /= 1024.0
|
|
|
|
self.report({'INFO'}, f'OptiPloy saves {format_size(old_size-new_size)}')
|
|
|
|
return {'FINISHED'}
|
|
|
|
def execute(self, context):
|
|
if not self.activate: return {'CANCELLED'}
|
|
if self.do_storage_benchmark:
|
|
return self.load_test(context)
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
obj = self.object
|
|
col = self.collection
|
|
|
|
|
|
prefs, entry = self.get_prefs(context)
|
|
|
|
if not os.path.exists(entry.filepath):
|
|
self.report({'ERROR'}, f"{entry.filepath} doesn't exist!")
|
|
#self.report({'ERROR'}, "The .blend file no longer exists!")
|
|
return {'CANCELLED'}
|
|
|
|
try:
|
|
with bpy.data.libraries.load(entry.filepath, link=True, relative=True) as (From, To):
|
|
if obj:
|
|
To.objects = [obj]
|
|
if col:
|
|
To.collections = [col]
|
|
except:
|
|
self.report({'ERROR'}, f'The .blend you are trying to open is corrupt!')
|
|
return {'CANCELLED'}
|
|
|
|
import_scene = bpy.data.scenes.get(self.scene, None) or context.scene
|
|
view_layer = getattr(import_scene, 'view_layers', [context.view_layer])[0] if self.scene else context.view_layer
|
|
|
|
scene_viewlayer = [import_scene, view_layer]
|
|
|
|
if obj:
|
|
if To.objects[0] == None:
|
|
self.report({'ERROR'}, f'Object "{obj}" could not be found in {os.path.basename(entry.filepath)}')
|
|
return {'CANCELLED'}
|
|
return load_data(self, context, scene_viewlayer, ind_prefs=prefs, obj=To.objects[0])
|
|
|
|
if col:
|
|
if To.collections[0] == None:
|
|
self.report({'ERROR'}, f'Collection "{col}" could not be found in {os.path.basename(entry.filepath)}')
|
|
return {'CANCELLED'}
|
|
return load_data(self, context, scene_viewlayer, ind_prefs=prefs, col=To.collections[0])
|
|
|
|
self.report({'WARNING'}, 'What?')
|
|
return {'CANCELLED'}
|
|
|
|
def draw(self, context):
|
|
sentences = f'''This is for checking how much storage you save with OptiPloy.
|
|
This process requires a clean slate.
|
|
{"This file has NOT been saved, and you will lose progress if you continue." if not bpy.data.is_saved else "This file will be saved, and then re-opened."}
|
|
Continue? '''.split('\n')
|
|
icons = f'DISK_DRIVE,ERROR,{"ERROR" if not bpy.data.is_saved else "CHECKMARK"},QUESTION'.split(',')
|
|
sizes = '56,56,56,56'.split(',')
|
|
for sentence, icon, size in zip(sentences, icons, sizes):
|
|
textBox(self.layout, sentence, icon, int(size))
|
|
self.layout.prop(self, 'compress_append')
|
|
self.layout.prop(self, 'compress_optiploy')
|
|
|
|
class SPAWNER_OT_POST_OPTIMIZE(mod_saver):
|
|
bl_idname = 'spawner.post_optimize'
|
|
bl_label = 'Optimize with OptiPloy'
|
|
bl_description = 'Optimize the selected linked objects with OptiPloy'
|
|
|
|
bl_options = {'UNDO'}
|
|
|
|
def execute(self, context):
|
|
#print(context.space_data, context.area.type, context.window, context.screen)
|
|
#return {'CANCELLED'}
|
|
|
|
#import_scene = bpy.data.scenes.get(self.scene, None) or context.scene
|
|
#view_layer = getattr(import_scene, 'view_layers', [context.view_layer])[0] if self.scene else context.view_layer
|
|
|
|
scene_viewlayer = [context.scene, context.view_layer]
|
|
|
|
if context.area.type == 'VIEW_3D':
|
|
ids = context.selected_objects
|
|
if context.area.type == 'OUTLINER':
|
|
ids = context.selected_ids
|
|
cols = set(filter(lambda a: isinstance(a, bpy.types.Collection) and getattr(a, 'library', None) != None, ids)) # get selected collections
|
|
objs = filter(lambda a: isinstance(a, bpy.types.Object), ids) # get selected objects
|
|
objs = set(filter(lambda a: not (True in [col in cols for col in a.users_collection]), objs)) # remove objects if their collection is selected
|
|
objs = set(filter(lambda a: not (getattr(a, 'parent', False) in objs), objs)) # only get the top most selected objects
|
|
true_ids = set()
|
|
for id in objs:
|
|
for col in id.users_collection:
|
|
if col.library:
|
|
cols.add(col)
|
|
break
|
|
else:
|
|
true_ids.add(id)
|
|
ids = true_ids.union(cols)
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
for id in ids:
|
|
conditions_for_instance = [
|
|
getattr(id, 'type', None) == 'EMPTY', # if object is an empty
|
|
getattr(id, 'instance_type', None) == 'COLLECTION', # if the empty has its instance type set to collection
|
|
getattr(getattr(id, 'instance_collection', None), 'library', None) != None, # and the instanced collection has linked library data
|
|
]
|
|
conditions_for_object = [
|
|
isinstance(id, bpy.types.Object),
|
|
id.library != None,
|
|
]
|
|
conditions_for_col = [
|
|
isinstance(id, bpy.types.Collection),
|
|
id.library != None,
|
|
]
|
|
|
|
if not False in conditions_for_instance:
|
|
col = id.instance_collection
|
|
return_val = load_data(self, context, scene_viewlayer, ind_prefs=prefs, col=col)
|
|
if return_val == {'FINISHED'}:
|
|
bpy.data.objects.remove(id)
|
|
elif not False in conditions_for_col:
|
|
load_data(self, context, scene_viewlayer, ind_prefs=prefs, col=id)
|
|
elif not False in conditions_for_object:
|
|
load_data(self, context, scene_viewlayer, ind_prefs=prefs, obj=id)
|
|
return {'FINISHED'}
|
|
|
|
class SPAWNER_OBJECT_UL_List(UIList):
|
|
def draw_item(self, context,
|
|
layout, data,
|
|
item, icon,
|
|
active_data, active_propname,
|
|
index):
|
|
layout.label(text=item.name)
|
|
|
|
def textBox(self, sentence, icon='NONE', line=56):
|
|
layout = self.box().column()
|
|
if sentence.startswith('LINK:'):
|
|
url, name = sentence.split('|')
|
|
url = url.split('LINK:', maxsplit=1)[1]
|
|
name = name.split('NAME:', maxsplit=1)[1]
|
|
#print(url, name)
|
|
layout.row().operator('wm.url_open', text=name, icon='URL').url = url
|
|
return None
|
|
sentence = sentence.split(' ')
|
|
mix = sentence[0]
|
|
sentence.pop(0)
|
|
broken = False
|
|
while True:
|
|
add = ' ' + sentence[0]
|
|
if len(mix + add) < line:
|
|
mix += add
|
|
sentence.pop(0)
|
|
if sentence == []:
|
|
layout.row().label(text=mix, icon='NONE' if broken else icon)
|
|
return None
|
|
|
|
else:
|
|
layout.row().label(text=mix, icon='NONE' if broken else icon)
|
|
broken = True
|
|
mix = sentence[0]
|
|
sentence.pop(0)
|
|
if sentence == []:
|
|
layout.row().label(text=mix)
|
|
return None
|
|
|
|
class generictext(bpy.types.Operator):
|
|
text: StringProperty(default='')
|
|
icons: StringProperty()
|
|
size: StringProperty()
|
|
width: IntProperty(default=400)
|
|
url: StringProperty(default='')
|
|
|
|
def invoke(self, context, event):
|
|
if not getattr(self, 'prompt', True):
|
|
return self.execute(context)
|
|
if event.shift and self.url != '':
|
|
bpy.ops.wm.url_open(url=self.url)
|
|
return self.execute(context)
|
|
self.invoke_extra(context, event)
|
|
return context.window_manager.invoke_props_dialog(self, width=self.width)
|
|
|
|
def invoke_extra(self, context, event):
|
|
pass
|
|
|
|
def draw(self, context):
|
|
sentences = self.text.split('\n')
|
|
icons = self.icons.split(',')
|
|
sizes = self.size.split(',')
|
|
for sentence, icon, size in zip(sentences, icons, sizes):
|
|
textBox(self.layout, sentence, icon, int(size))
|
|
self.draw_extra(context)
|
|
|
|
def draw_extra(self, context):
|
|
pass
|
|
|
|
def execute(self, context):
|
|
return {'FINISHED'}
|
|
|
|
class SPAWNER_OT_genericText(generictext):
|
|
bl_idname = 'spawner.textbox'
|
|
bl_label = 'Hints'
|
|
bl_description = 'A window will display any possible questions you have'
|
|
|
|
class SPAWNER_OT_open_blend(generictext):
|
|
|
|
bl_idname = 'spawner.open_blend'
|
|
bl_label = 'Blend Multi-Tool'
|
|
bl_description = 'Hold Shift to open the selected .blend file, hold Ctrl to reload, hold Alt to re-scan'
|
|
|
|
blend: IntProperty()
|
|
folder: IntProperty()
|
|
path: StringProperty(name='Path')
|
|
|
|
blend_path_add: StringProperty(default='', subtype='FILE_PATH')
|
|
use_current_blend:BoolProperty(default=False)
|
|
|
|
def invoke(self, context, event):
|
|
blendPath = Path(str(self.path))
|
|
|
|
if (event.ctrl + event.shift + event.alt) > 1:
|
|
return {'CANCELLED'}
|
|
self.text = 'tee hee no'
|
|
self.icons = 'NONE'
|
|
self.size='56'
|
|
self.width = 310
|
|
return context.window_manager.invoke_props_dialog(self, width=self.width)
|
|
|
|
if event.ctrl:
|
|
for lib in bpy.data.libraries:
|
|
blendPathLib = Path(bpy.path.abspath(lib.filepath))
|
|
if blendPathLib == blendPath: lib.reload(); return {'FINISHED'}
|
|
return {'FINISHED'}
|
|
|
|
if event.shift:
|
|
import subprocess
|
|
subprocess.Popen([bpy.app.binary_path, blendPath])
|
|
return {'FINISHED'}
|
|
|
|
if event.alt:
|
|
return bpy.ops.spawner.scan('INVOKE_DEFAULT', blend=self.blend, folder=self.folder)
|
|
|
|
return context.window_manager.invoke_props_dialog(self, width=self.width)
|
|
|
|
|
|
def draw_extra(self, context):
|
|
layout = self.layout
|
|
layout.separator()
|
|
col = layout.column()
|
|
main_row = col.row()
|
|
r = main_row.row()
|
|
r.alignment = 'LEFT'
|
|
r.label(text='Add .blend')
|
|
r = main_row.row()
|
|
r.alignment = 'RIGHT'
|
|
r.enabled = bpy.data.is_saved
|
|
r.prop(self, 'use_current_blend', text='Add Active .blend')
|
|
box = col.box()
|
|
col_box = box.column()
|
|
row = col_box.row()
|
|
row.prop(self, 'blend_path_add', text='Filepath')
|
|
row.enabled = 1-(self.use_current_blend and bpy.data.is_saved)
|
|
op = col_box.operator('spawner.add_entry')
|
|
op.filepath = bpy.data.filepath if (self.use_current_blend and bpy.data.is_saved) else bpy.path.abspath(self.blend_path_add)
|
|
op.execute_only = True
|
|
op.blend = True
|
|
op.folder = bool(self.folder+1)
|
|
op.folder_select = self.folder
|
|
|
|
class SPAWNER_OT_open_folder(generictext):
|
|
|
|
bl_idname = 'spawner.open_folder'
|
|
bl_label = 'Folder Multi-Tool'
|
|
bl_description = 'Hold Shift to open the selected folder, hold Ctrl to reload, hold Alt to re-scan'
|
|
|
|
folder: IntProperty()
|
|
path: StringProperty(name='Path')
|
|
|
|
folder_path_add: StringProperty(default='', subtype='DIR_PATH')
|
|
add_category: BoolProperty(default=False)
|
|
category_name: StringProperty(default='New Category')
|
|
|
|
def invoke(self, context, event):
|
|
blendPath = Path(str(self.path))
|
|
self.category_name = 'New Category'
|
|
|
|
if (event.ctrl + event.shift + event.alt) > 1:
|
|
return {'CANCELLED'}
|
|
self.text = 'tee hee no'
|
|
self.icons = 'NONE'
|
|
self.size='56'
|
|
self.width = 310
|
|
return context.window_manager.invoke_props_dialog(self, width=self.width)
|
|
|
|
if event.ctrl:
|
|
#for lib in bpy.data.libraries:
|
|
# blendPathLib = Path(bpy.path.abspath(lib.filepath))
|
|
# if blendPathLib == blendPath: lib.reload(); return {'FINISHED'}
|
|
return {'FINISHED'}
|
|
|
|
if event.shift:
|
|
#import subprocess
|
|
#subprocess.Popen([bpy.app.binary_path, blendPath])
|
|
return {'FINISHED'}
|
|
|
|
if event.alt:
|
|
return bpy.ops.spawner.scan('INVOKE_DEFAULT', blend=-1, folder=self.folder)
|
|
|
|
return context.window_manager.invoke_props_dialog(self, width=self.width)
|
|
|
|
|
|
def draw_extra(self, context):
|
|
layout = self.layout
|
|
layout.separator()
|
|
col = layout.column()
|
|
main_row = col.row()
|
|
r = main_row.row()
|
|
r.alignment = 'LEFT'
|
|
r.label(text='Add Folder')
|
|
r = main_row.row()
|
|
r.alignment = 'RIGHT'
|
|
r.prop(self, 'add_category', text='Add Category')
|
|
box = col.box()
|
|
col_box = box.column()
|
|
row = col_box.row()
|
|
if self.add_category:
|
|
r = row.row()
|
|
r.alignment = 'LEFT'
|
|
r.label(text='Category Name:')
|
|
r = row.row()
|
|
r.alignment = 'RIGHT'
|
|
row.prop(self, 'category_name', text='')
|
|
else:
|
|
row.prop(self, 'folder_path_add', text='Filepath')
|
|
#row.enabled = 1-self.add_category
|
|
op = col_box.operator('spawner.add_entry', text='Add Folder Entry')
|
|
op.directory = self.folder_path_add
|
|
op.execute_only = True
|
|
op.blend = False
|
|
op.folder = True
|
|
op.category = self.add_category
|
|
op.category_name = self.category_name
|
|
|
|
def draw_item(self:bpy.types.Menu, context):
|
|
layout = self.layout
|
|
layout.separator()
|
|
layout.operator('spawner.post_optimize')
|
|
|
|
classes = [
|
|
SPAWNER_PT_panel,
|
|
SPAWNER_GENERIC_SPAWN_UL_List,
|
|
SPAWNER_OT_SPAWNER,
|
|
SPAWNER_OT_POST_OPTIMIZE,
|
|
SPAWNER_OT_genericText,
|
|
SPAWNER_PT_extra_settings,
|
|
SPAWNER_PT_folder_settings,
|
|
SPAWNER_PT_blend_settings,
|
|
SPAWNER_OT_open_blend,
|
|
SPAWNER_OT_open_folder
|
|
]
|
|
|
|
def register():
|
|
for i in classes:
|
|
bpy.utils.register_class(i)
|
|
bpy.types.VIEW3D_MT_object.append(draw_item)
|
|
bpy.types.OUTLINER_MT_context_menu.append(draw_item)
|
|
bpy.types.OUTLINER_MT_object.append(draw_item)
|
|
bpy.types.OUTLINER_MT_collection.append(draw_item)
|
|
|
|
def unregister():
|
|
for i in reversed(classes):
|
|
bpy.utils.unregister_class(i)
|
|
bpy.types.VIEW3D_MT_object.remove(draw_item)
|
|
bpy.types.OUTLINER_MT_context_menu.remove(draw_item)
|
|
bpy.types.OUTLINER_MT_object.remove(draw_item)
|
|
bpy.types.OUTLINER_MT_collection.remove(draw_item)
|