2025-12-01
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Modifier, Context, Collection, NodeTree, Operator, Object
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from bpy.props import IntProperty, StringProperty, BoolProperty, FloatProperty
|
||||
from .prefs import get_addon_prefs
|
||||
|
||||
NODETREE_NAME = "GN-shape_key"
|
||||
COLLECTION_NAME = "GeoNode Shape Keys"
|
||||
|
||||
|
||||
def geomod_get_identifier(modifier: Modifier, param_name: str) -> str:
|
||||
if hasattr(modifier.node_group, 'interface'):
|
||||
# 4.0
|
||||
input = modifier.node_group.interface.items_tree.get(param_name)
|
||||
else:
|
||||
# 3.6
|
||||
input = modifier.node_group.inputs.get(param_name)
|
||||
|
||||
if input:
|
||||
return input.identifier
|
||||
|
||||
|
||||
def geomod_get_data_path(modifier: Modifier, param_name: str) -> str:
|
||||
return f'modifiers["{modifier.name}"]["{geomod_get_identifier(modifier, param_name)}"]'
|
||||
|
||||
|
||||
def geomod_set_param_value(modifier: Modifier, param_name: str, param_value: Any):
|
||||
input_id = geomod_get_identifier(modifier, param_name)
|
||||
# Note: Must use setattr, see T103865.
|
||||
setattr(modifier, f'["{input_id}"]', param_value)
|
||||
|
||||
|
||||
def geomod_get_param_value(modifier: Modifier, param_name: str):
|
||||
input_id = geomod_get_identifier(modifier, param_name)
|
||||
return modifier[input_id]
|
||||
|
||||
|
||||
def geomod_set_param_use_attribute(modifier: Modifier, param_name: str, use_attrib: bool):
|
||||
input_id = geomod_get_identifier(modifier, param_name)
|
||||
modifier[input_id + "_use_attribute"] = use_attrib
|
||||
|
||||
|
||||
def geomod_set_param_attribute(modifier: Modifier, param_name: str, attrib_name: str):
|
||||
input_id = geomod_get_identifier(modifier, param_name)
|
||||
modifier[input_id + "_use_attribute"] = True
|
||||
modifier[input_id + "_attribute_name"] = attrib_name
|
||||
|
||||
|
||||
def get_resource_blend_path(context) -> tuple[str, bool]:
|
||||
"""Return the desired filepath to the .blend file containing the node set-up.
|
||||
Also return a boolean which indicates whether it should be linked or appended.
|
||||
"""
|
||||
addon_prefs = get_addon_prefs(context)
|
||||
|
||||
filepath = Path(addon_prefs.blend_path)
|
||||
if not filepath.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Node tree file not found: '{filepath.as_posix()}'. Browse it in the add-on preferences."
|
||||
)
|
||||
|
||||
return filepath.as_posix(), addon_prefs.node_import_type == 'LINK'
|
||||
|
||||
|
||||
def link_shape_key_node_tree(context) -> NodeTree:
|
||||
# Load shape key node tree from a file.
|
||||
if NODETREE_NAME in bpy.data.node_groups:
|
||||
return bpy.data.node_groups[NODETREE_NAME]
|
||||
|
||||
blend_path, do_link = get_resource_blend_path(context)
|
||||
|
||||
with bpy.data.libraries.load(blend_path, link=do_link, relative=True) as (data_from, data_to):
|
||||
data_to.node_groups.append(NODETREE_NAME)
|
||||
|
||||
return bpy.data.node_groups[NODETREE_NAME]
|
||||
|
||||
|
||||
def ensure_shapekey_collection(context: Context) -> Collection:
|
||||
"""Ensure and return a collection used for the objects created by the add-on."""
|
||||
scene = context.scene
|
||||
coll = bpy.data.collections.get(COLLECTION_NAME)
|
||||
if not coll:
|
||||
coll = bpy.data.collections.new(COLLECTION_NAME)
|
||||
scene.collection.children.link(coll)
|
||||
coll.hide_render = True
|
||||
|
||||
coll.hide_viewport = False
|
||||
if coll not in list(scene.collection.children):
|
||||
scene.collection.children.link(coll)
|
||||
|
||||
context.view_layer.layer_collection.children[coll.name].exclude = False
|
||||
|
||||
return coll
|
||||
|
||||
|
||||
def get_gnsk_targets(gnsk):
|
||||
return gnsk.storage_object.geonode_shapekey_targets
|
||||
|
||||
|
||||
def get_active_gnsk_targets(obj):
|
||||
active_gnsk = obj.geonode_shapekeys[obj.geonode_shapekey_index]
|
||||
return get_gnsk_targets(active_gnsk)
|
||||
|
||||
|
||||
class OBJECT_OT_gnsk_add_shape(Operator):
|
||||
"""Create a GeoNode modifier set-up and a duplicate object"""
|
||||
|
||||
bl_idname = "object.add_geonode_shape_key"
|
||||
bl_label = "Add GeoNode Shape Key"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
# TODO: Maybe add an option to keyframe this to only be active on this frame.
|
||||
shape_name: StringProperty(
|
||||
name="Shape Name",
|
||||
description="Name to identify this shape (used in the shape key and modifier names)",
|
||||
)
|
||||
uv_name: StringProperty(
|
||||
name="UVMap",
|
||||
description="UV Map to use for the deform space magic. All selected objects must have a map with this name, or the default will be used",
|
||||
)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
for o in context.selected_objects:
|
||||
if o.type != 'MESH':
|
||||
continue
|
||||
uvs = o.data.uv_layers
|
||||
if len(uvs) == 0:
|
||||
self.report({'ERROR'}, 'All selected mesh objects must have a UV Map!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
uvs = context.object.data.uv_layers
|
||||
self.uv_name = "GNSK" if "GNSK" in uvs else uvs[0].name
|
||||
|
||||
self.shape_name = "ShapeKey: Frame " + str(context.scene.frame_current)
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop_search(self, 'uv_name', context.object.data, 'uv_layers', icon='GROUP_UVS')
|
||||
layout.prop(self, 'shape_name')
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
get_resource_blend_path(context)
|
||||
except FileNotFoundError:
|
||||
self.report({'ERROR'}, "Node tree file not found. Check your Add-On preferences.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != 'MESH':
|
||||
obj.select_set(False)
|
||||
|
||||
mesh_objs = context.selected_objects
|
||||
for obj in mesh_objs:
|
||||
if self.uv_name not in obj.data.uv_layers:
|
||||
self.report({'ERROR'}, f'Object "{obj.name}" has no UV Map named "{self.uv_name}"!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
sk_ob = self.make_combined_sculpt_mesh(context, mesh_objs)
|
||||
|
||||
for obj in mesh_objs:
|
||||
# Add GeoNode modifiers.
|
||||
gnsk = obj.geonode_shapekeys.add()
|
||||
gnsk.name = self.shape_name
|
||||
|
||||
mod = obj.modifiers.new(gnsk.name, type='NODES')
|
||||
mod.node_group = link_shape_key_node_tree(context)
|
||||
|
||||
# Find desired modifier index: After any other GNSK modifier, or if
|
||||
# none, before the SubSurf modifier.
|
||||
mod_index = self.get_desired_modifier_index(obj, mod)
|
||||
with context.temp_override(object=obj):
|
||||
bpy.ops.object.modifier_move_to_index(modifier=mod.name, index=mod_index)
|
||||
gnsk.name = mod.name # In case the modifier got a .001 suffix.
|
||||
|
||||
gnsk.storage_object = sk_ob
|
||||
|
||||
geomod_set_param_value(mod, 'Sculpt', sk_ob)
|
||||
uv_map = sk_ob.data.uv_layers.get(self.uv_name)
|
||||
if not uv_map:
|
||||
uv_map = sk_ob.data.uv_layers[0]
|
||||
geomod_set_param_attribute(mod, 'UVMap', uv_map.name)
|
||||
|
||||
# Add references from the shape key object to the deformed objects.
|
||||
# This is used for the visibility switching operator.
|
||||
tgt = sk_ob.geonode_shapekey_targets.add()
|
||||
tgt.name = obj.name
|
||||
tgt.obj = obj
|
||||
|
||||
# Change to Sculpt Mode.
|
||||
orig_ui = context.area.ui_type
|
||||
context.area.ui_type = 'VIEW_3D'
|
||||
bpy.ops.object.mode_set(mode='SCULPT')
|
||||
context.area.ui_type = orig_ui
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def make_combined_sculpt_mesh(
|
||||
self, context, mesh_objs: list[Object]
|
||||
) -> Object:
|
||||
# Save evaluated objects into a new, combined object.
|
||||
for obj in mesh_objs:
|
||||
self.make_evaluated_object(context, obj)
|
||||
|
||||
# Join all the shape key objects into one object...
|
||||
bpy.ops.object.join()
|
||||
sk_ob = context.active_object
|
||||
sk_ob.name = self.shape_name
|
||||
return sk_ob
|
||||
|
||||
def get_desired_modifier_index(self, obj: Object, mod: Modifier) -> int:
|
||||
"""Figure out the desired index to insert the next GeoNodes ShapeKey modifier at.
|
||||
If there are any other GNSK modifiers, we should insert after the last one.
|
||||
Otherwise, insert before any SubSurf modifiers, if any.
|
||||
Otherwise, insert at bottom of stack.
|
||||
"""
|
||||
|
||||
for i, m in reversed(list(enumerate(obj.modifiers))):
|
||||
if m == mod:
|
||||
continue
|
||||
if m.type == 'NODES' and m.node_group == mod.node_group:
|
||||
return i + 1
|
||||
|
||||
for i, m in enumerate(obj.modifiers):
|
||||
if m.type == 'SUBSURF':
|
||||
return i
|
||||
|
||||
return -1
|
||||
|
||||
@staticmethod
|
||||
def disable_modifiers_after_subsurf(obj: Object) -> dict[str, dict[str, Any]]:
|
||||
"""Disable modifiers that might cause the propagation of the sculpted shape to fail.
|
||||
This includes the Subsurf modifier and any subsequent modifiers.
|
||||
Possibly more in future.
|
||||
"""
|
||||
modifier_states = {}
|
||||
found_subsurf = False
|
||||
for m in obj.modifiers:
|
||||
if m.type == 'SUBSURF':
|
||||
found_subsurf = True
|
||||
|
||||
if found_subsurf:
|
||||
modifier_states[m.name] = {
|
||||
'show_viewport': m.show_viewport,
|
||||
}
|
||||
|
||||
# Mute driver, if any.
|
||||
if obj.animation_data:
|
||||
fc = obj.animation_data.drivers.find(f'modifiers["{m.name}"].show_viewport')
|
||||
if fc:
|
||||
fc.mute = True
|
||||
m.show_viewport = False
|
||||
|
||||
return modifier_states
|
||||
|
||||
@staticmethod
|
||||
def restore_modifiers(obj: Object, modifier_states: dict[str, dict[str, Any]]):
|
||||
"""Reset SubSurf and subsequent modifiers."""
|
||||
for mod_name, prop_dict in modifier_states.items():
|
||||
for key, value in prop_dict.items():
|
||||
setattr(obj.modifiers[mod_name], key, value)
|
||||
|
||||
# Unmute driver, if any.
|
||||
if obj.animation_data:
|
||||
fc = obj.animation_data.drivers.find(f'modifiers["{mod_name}"].{key}')
|
||||
if fc:
|
||||
fc.mute = False
|
||||
|
||||
def make_evaluated_object(
|
||||
self, context: Context, obj: Object
|
||||
) -> Object:
|
||||
|
||||
obj.override_library.is_system_override = False
|
||||
|
||||
# Disable the first SubSurf and all subsequent modifiers.
|
||||
# NOTE: Other generative modifiers beside SubSurf may have to trigger this too.
|
||||
modifier_states = self.disable_modifiers_after_subsurf(obj)
|
||||
eval_dg = context.evaluated_depsgraph_get()
|
||||
|
||||
sk_mesh = bpy.data.meshes.new_from_object(obj.evaluated_get(eval_dg))
|
||||
sk_ob = bpy.data.objects.new(obj.name + "." + self.shape_name, sk_mesh)
|
||||
sk_ob.data.name = sk_ob.name
|
||||
sk_coll = ensure_shapekey_collection(context)
|
||||
sk_coll.objects.link(sk_ob)
|
||||
|
||||
# Add shape keys
|
||||
sk_ob.use_shape_key_edit_mode = True
|
||||
sk_ob.shape_key_add(name="Basis")
|
||||
sk_ob.hide_render = True
|
||||
adjust = sk_ob.shape_key_add(name="New Shape", from_mix=True)
|
||||
adjust.value = 1
|
||||
sk_ob.active_shape_key_index = 1
|
||||
sk_ob.add_rest_position_attribute = True
|
||||
|
||||
sk_ob.matrix_world = obj.matrix_world
|
||||
|
||||
self.restore_modifiers(obj, modifier_states)
|
||||
|
||||
obj.hide_set(True)
|
||||
sk_ob.hide_set(False)
|
||||
sk_ob.select_set(True)
|
||||
context.view_layer.objects.active = sk_ob
|
||||
|
||||
return sk_ob
|
||||
|
||||
|
||||
class OBJECT_OT_gnsk_remove_shape(Operator):
|
||||
"""Create a GeoNode modifier set-up and a duplicate object"""
|
||||
|
||||
bl_idname = "object.remove_geonode_shape_key"
|
||||
bl_label = "Add GeoNode Shape Key"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
remove_from_all: BoolProperty(
|
||||
name="Remove From All",
|
||||
description="Remove this shape from all affected objects, and delete the local object",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
|
||||
if not obj:
|
||||
cls.poll_message_set("No active object.")
|
||||
return False
|
||||
|
||||
if len(obj.geonode_shapekeys) == 0:
|
||||
cls.poll_message_set("Nothing to remove.")
|
||||
return False
|
||||
|
||||
if 0 > obj.geonode_shapekey_index or obj.geonode_shapekey_index > len(obj.geonode_shapekeys):
|
||||
cls.poll_message_set("No active element.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def invoke(self, context, _event):
|
||||
if len(get_active_gnsk_targets(context.object)) > 1:
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, 'remove_from_all')
|
||||
|
||||
targets = get_active_gnsk_targets(context.object)
|
||||
if len(targets) > 1 and self.remove_from_all:
|
||||
layout.label(text="Shape will be removed from:")
|
||||
for target in targets:
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.prop(target, 'obj', text="")
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
active_gnsk = ob.geonode_shapekeys[ob.geonode_shapekey_index]
|
||||
|
||||
objs = [ob]
|
||||
storage_ob = active_gnsk.storage_object
|
||||
delete_storage = False
|
||||
if self.remove_from_all:
|
||||
delete_storage = True
|
||||
if not storage_ob:
|
||||
self.report({'WARNING'}, f'Storage object was not found.')
|
||||
else:
|
||||
objs = [target.obj for target in storage_ob.geonode_shapekey_targets]
|
||||
|
||||
for ob in objs:
|
||||
mod_removed = False
|
||||
|
||||
for gnsk_idx, gnsk in enumerate(ob.geonode_shapekeys):
|
||||
if gnsk.storage_object == storage_ob:
|
||||
break
|
||||
|
||||
mod = gnsk.modifier
|
||||
if mod:
|
||||
ob.modifiers.remove(mod)
|
||||
mod_removed = True
|
||||
|
||||
# Remove the GNSK slot
|
||||
ob.geonode_shapekeys.remove(gnsk_idx)
|
||||
|
||||
# Fix the active index
|
||||
ob.geonode_shapekey_index = min(gnsk_idx, len(ob.geonode_shapekeys) - 1)
|
||||
|
||||
# Remove the target reference from the storage object
|
||||
for i, target in enumerate(storage_ob.geonode_shapekey_targets):
|
||||
if target.obj == ob:
|
||||
break
|
||||
storage_ob.geonode_shapekey_targets.remove(i)
|
||||
if len(storage_ob.geonode_shapekey_targets) == 0:
|
||||
delete_storage = True
|
||||
|
||||
# Give feedback
|
||||
if mod_removed:
|
||||
self.report({'INFO'}, f'{ob.name}: Successfully deleted Object and Modifier.')
|
||||
else:
|
||||
self.report(
|
||||
{'WARNING'}, f'{ob.name}: Modifier for "{active_gnsk.name}" was not found.'
|
||||
)
|
||||
|
||||
if delete_storage:
|
||||
# Remove the storage object.
|
||||
bpy.data.objects.remove(storage_ob)
|
||||
# Remove collection if it's empty.
|
||||
coll = ensure_shapekey_collection(context)
|
||||
if len(coll.all_objects) == 0:
|
||||
bpy.data.collections.remove(coll)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_gnsk_toggle_object(Operator):
|
||||
"""Swap between the sculpt and overridden objects"""
|
||||
|
||||
bl_idname = "object.geonode_shapekey_switch_focus"
|
||||
bl_label = "Switch to Render Objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
gnsk_index: IntProperty(default=-1)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
obj = context.object
|
||||
if not obj:
|
||||
cls.poll_message_set("No active object.")
|
||||
return False
|
||||
|
||||
if obj.geonode_shapekey_targets:
|
||||
return True
|
||||
|
||||
if len(obj.geonode_shapekeys) == 0:
|
||||
cls.poll_message_set("Nothing to switch to.")
|
||||
return False
|
||||
|
||||
if 0 > obj.geonode_shapekey_index or obj.geonode_shapekey_index > len(obj.geonode_shapekeys):
|
||||
cls.poll_message_set("No active element.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
targets = ob.geonode_shapekey_targets
|
||||
|
||||
if self.gnsk_index > -1:
|
||||
ob.geonode_shapekey_index = self.gnsk_index
|
||||
|
||||
if targets:
|
||||
for target in targets:
|
||||
# Make sure to leave sculpt/edit mode, otherwise, sometimes
|
||||
# Blender can end up in a weird state, where the LINKED object
|
||||
# is in Sculpt mode (WTF?!) and you can't leave or do anything.
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
obs = [ob]
|
||||
collection = bpy.data.collections.get(COLLECTION_NAME)
|
||||
if collection:
|
||||
obs = collection.all_objects
|
||||
for ob in obs:
|
||||
ob.select_set(False)
|
||||
ob.hide_set(True)
|
||||
|
||||
target_ob = target.obj
|
||||
target_ob.hide_set(False)
|
||||
target_ob.select_set(True)
|
||||
context.view_layer.objects.active = target_ob
|
||||
|
||||
# Trigger an update... otherwise, since we insert the modifier
|
||||
# somewhere other than the bottom of the stack, it sometimes
|
||||
# doesn't update live.
|
||||
target_ob.name = target_ob.name
|
||||
|
||||
elif len(ob.geonode_shapekeys) > 0:
|
||||
storage = ob.geonode_shapekeys[ob.geonode_shapekey_index].storage_object
|
||||
if not storage:
|
||||
self.report({'ERROR'}, "No storage object to swap to.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
ob.select_set(False)
|
||||
ob.hide_set(True)
|
||||
|
||||
storage.hide_set(False)
|
||||
storage.select_set(True)
|
||||
context.view_layer.objects.active = storage
|
||||
|
||||
orig_ui = context.area.ui_type
|
||||
context.area.ui_type = 'VIEW_3D'
|
||||
bpy.ops.object.mode_set(mode='SCULPT')
|
||||
context.area.ui_type = orig_ui
|
||||
|
||||
else:
|
||||
self.report({'ERROR'}, "No storage or target to swap to.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_gnsk_influence_slider(Operator):
|
||||
"""Change the influence on all affected meshes"""
|
||||
|
||||
bl_idname = "object.geonode_shapekey_influence_slider"
|
||||
bl_label = "Change Influence of All Selected"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
gnsk_index: IntProperty(default=0)
|
||||
# TODO: If one day, Library Overrides support adding drivers (and saving and reloading the file),
|
||||
# this should be changed to instead allow assigning the active object's influence as the
|
||||
# driver for all others.
|
||||
insert_keyframe: BoolProperty(
|
||||
name="Insert Keyframe",
|
||||
description="Insert a keyframe on all affected values",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def update_slider(self, context):
|
||||
ob = context.object
|
||||
gnsk = ob.geonode_shapekeys[self.gnsk_index]
|
||||
for target in get_gnsk_targets(gnsk):
|
||||
obj = target.obj
|
||||
for obj_gnsk in obj.geonode_shapekeys:
|
||||
if obj_gnsk.storage_object == gnsk.storage_object:
|
||||
geomod_set_param_value(obj_gnsk.modifier, 'Factor', self.slider_value)
|
||||
if self.insert_keyframe:
|
||||
obj.keyframe_insert(geomod_get_data_path(obj_gnsk.modifier, 'Factor'))
|
||||
break
|
||||
|
||||
slider_value: FloatProperty(
|
||||
name="Influence",
|
||||
description="Influence to set on all affected objects",
|
||||
update=update_slider,
|
||||
min=0,
|
||||
max=1,
|
||||
)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
self.insert_keyframe = context.scene.tool_settings.use_keyframe_insert_auto
|
||||
wm = context.window_manager
|
||||
self.slider_value = geomod_get_param_value(
|
||||
context.object.geonode_shapekeys[self.gnsk_index].modifier, 'Factor'
|
||||
)
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
layout.prop(self, 'slider_value', slider=True)
|
||||
layout.prop(self, 'insert_keyframe')
|
||||
|
||||
ob = context.object
|
||||
gnsk = ob.geonode_shapekeys[self.gnsk_index]
|
||||
targets = get_gnsk_targets(gnsk)
|
||||
layout.label(text="Affected objects:")
|
||||
for target in targets:
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.prop(target, 'obj', text="")
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_gnsk_select_objects(Operator):
|
||||
"""Select objects that share a sculpt object with this GeoNode ShapeKey"""
|
||||
|
||||
bl_idname = "object.geonode_shapekey_select_objects"
|
||||
bl_label = "Select Objects"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
gnsk_index: IntProperty(default=0)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
gnsk = obj.geonode_shapekeys[self.gnsk_index]
|
||||
|
||||
count_hidden = 0
|
||||
count_selected = 0
|
||||
for target in gnsk.storage_object.geonode_shapekey_targets:
|
||||
target_ob = target.obj
|
||||
if target_ob.visible_get():
|
||||
target_ob.select_set(True)
|
||||
count_selected += 1
|
||||
else:
|
||||
count_hidden += 1
|
||||
|
||||
if count_hidden > 0:
|
||||
self.report({'WARNING'}, f"{count_hidden} hidden objects were not selected.")
|
||||
else:
|
||||
self.report({'INFO'}, f"All {count_selected} objects were selected.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_gnsk_setup_uvs(Operator):
|
||||
"""Ensure a set of non-overlapping UVs in a UVMap across all selected meshes"""
|
||||
|
||||
bl_idname = "object.geonode_shapekey_ensure_uvmap"
|
||||
bl_label = "Ensure GNSK UVMap"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
active_layers_bkp = {}
|
||||
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
active_layers_bkp[obj] = obj.data.uv_layers.active.name
|
||||
if "GNSK" not in obj.data.uv_layers:
|
||||
obj.data.uv_layers.new(name="GNSK")
|
||||
obj.data.uv_layers.active = obj.data.uv_layers['GNSK']
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.reveal()
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.uv.smart_project(island_margin=0.001)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Restore active UV Layer
|
||||
for obj, layer in active_layers_bkp.items():
|
||||
obj.data.uv_layers.active = obj.data.uv_layers.get(layer)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
registry = [
|
||||
OBJECT_OT_gnsk_add_shape,
|
||||
OBJECT_OT_gnsk_remove_shape,
|
||||
OBJECT_OT_gnsk_toggle_object,
|
||||
OBJECT_OT_gnsk_influence_slider,
|
||||
OBJECT_OT_gnsk_select_objects,
|
||||
OBJECT_OT_gnsk_setup_uvs,
|
||||
]
|
||||
Reference in New Issue
Block a user