FBX IO: Speed up animation simplification using NumPy #104904
@ -5,7 +5,7 @@
|
||||
bl_info = {
|
||||
"name": "FBX format",
|
||||
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
|
||||
"version": (5, 7, 4),
|
||||
"version": (5, 8, 1),
|
||||
"blender": (3, 6, 0),
|
||||
"location": "File > Import-Export",
|
||||
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",
|
||||
|
@ -552,14 +552,20 @@ def fbx_data_element_custom_properties(props, bid):
|
||||
|
||||
def fbx_data_empty_elements(root, empty, scene_data):
|
||||
"""
|
||||
Write the Empty data block (you can control its FBX datatype with the 'fbx_type' string custom property).
|
||||
Write the Empty data block (you can control its FBX datatype with the 'fbx_type' string custom property) or Armature
|
||||
NodeAttribute.
|
||||
"""
|
||||
empty_key = scene_data.data_empties[empty]
|
||||
|
||||
null = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(empty_key))
|
||||
null.add_string(fbx_name_class(empty.name.encode(), b"NodeAttribute"))
|
||||
val = empty.bdata.get('fbx_type', None)
|
||||
null.add_string(val.encode() if val and isinstance(val, str) else b"Null")
|
||||
bdata = empty.bdata
|
||||
if bdata.type == 'EMPTY':
|
||||
val = bdata.get('fbx_type', None)
|
||||
fbx_type = val.encode() if val and isinstance(val, str) else b"Null"
|
||||
else:
|
||||
fbx_type = b"Null"
|
||||
null.add_string(fbx_type)
|
||||
|
||||
elem_data_single_string(null, b"TypeFlags", b"Null")
|
||||
|
||||
@ -567,7 +573,10 @@ def fbx_data_empty_elements(root, empty, scene_data):
|
||||
props = elem_properties(null)
|
||||
elem_props_template_finalize(tmpl, props)
|
||||
|
||||
# No custom properties, already saved with object (Model).
|
||||
# Empty/Armature Object custom properties have already been saved with the Model.
|
||||
# Only Armature data custom properties need to be saved here with the NodeAttribute.
|
||||
if bdata.type == 'ARMATURE':
|
||||
fbx_data_element_custom_properties(props, bdata.data)
|
||||
|
||||
|
||||
def fbx_data_light_elements(root, lamp, scene_data):
|
||||
@ -2583,7 +2592,6 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
||||
if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
|
||||
continue
|
||||
ob = ob_obj.bdata
|
||||
use_org_data = True
|
||||
org_ob_obj = None
|
||||
|
||||
# Do not want to systematically recreate a new mesh for dupliobject instances, kind of break purpose of those.
|
||||
@ -2593,16 +2601,28 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
||||
data_meshes[ob_obj] = data_meshes[org_ob_obj]
|
||||
continue
|
||||
|
||||
is_ob_material = any(ms.link == 'OBJECT' for ms in ob.material_slots)
|
||||
# There are 4 different cases for what we need to do with the original data of each Object:
|
||||
# 1) The original data can be used without changes.
|
||||
# 2) A copy of the original data needs to be made.
|
||||
# - If an export option modifies the data, e.g. Triangulate Faces is enabled.
|
||||
# - If the Object has Object-linked materials. This is because our current mapping of materials to FBX requires
|
||||
# that multiple Objects sharing a single mesh must have the same materials.
|
||||
# 3) The Object needs to be converted to a mesh.
|
||||
# - All mesh-like Objects that are not meshes need to be converted to a mesh in order to be exported.
|
||||
# 4) The Object needs to be evaluated and then converted to a mesh.
|
||||
# - Whenever use_mesh_modifiers is enabled and either there are modifiers to apply or the Object needs to be
|
||||
# converted to a mesh.
|
||||
# If multiple cases apply to an Object, then only the last applicable case is relevant.
|
||||
do_copy = any(ms.link == 'OBJECT' for ms in ob.material_slots) or settings.use_triangles
|
||||
do_convert = ob.type in BLENDER_OTHER_OBJECT_TYPES
|
||||
do_evaluate = do_convert and settings.use_mesh_modifiers
|
||||
|
||||
if settings.use_mesh_modifiers or settings.use_triangles or ob.type in BLENDER_OTHER_OBJECT_TYPES or is_ob_material:
|
||||
# We cannot use default mesh in that case, or material would not be the right ones...
|
||||
use_org_data = not (is_ob_material or ob.type in BLENDER_OTHER_OBJECT_TYPES)
|
||||
# If the Object is a mesh, and we're applying modifiers, check if there are actually any modifiers to apply.
|
||||
# If there are then the mesh will need to be evaluated, and we may need to make some temporary changes to the
|
||||
# modifiers or scene before the mesh is evaluated.
|
||||
backup_pose_positions = []
|
||||
tmp_mods = []
|
||||
if use_org_data and ob.type == 'MESH':
|
||||
if settings.use_triangles:
|
||||
use_org_data = False
|
||||
if ob.type == 'MESH' and settings.use_mesh_modifiers:
|
||||
# No need to create a new mesh in this case, if no modifier is active!
|
||||
last_subsurf = None
|
||||
for mod in ob.modifiers:
|
||||
@ -2627,34 +2647,26 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
||||
# found applicable subsurf modifier.
|
||||
if settings.use_subsurf and mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK':
|
||||
if last_subsurf:
|
||||
use_org_data = False
|
||||
do_evaluate = True
|
||||
last_subsurf = mod
|
||||
else:
|
||||
use_org_data = False
|
||||
do_evaluate = True
|
||||
if settings.use_subsurf and last_subsurf:
|
||||
# XXX: When exporting with subsurf information temporarily disable
|
||||
# the last subsurf modifier.
|
||||
tmp_mods.append((last_subsurf, last_subsurf.show_render, last_subsurf.show_viewport))
|
||||
last_subsurf.show_render = False
|
||||
last_subsurf.show_viewport = False
|
||||
if not use_org_data:
|
||||
|
||||
if do_evaluate:
|
||||
# If modifiers has been altered need to update dependency graph.
|
||||
if backup_pose_positions or tmp_mods:
|
||||
depsgraph.update()
|
||||
ob_to_convert = ob.evaluated_get(depsgraph) if settings.use_mesh_modifiers else ob
|
||||
ob_to_convert = ob.evaluated_get(depsgraph)
|
||||
# NOTE: The dependency graph might be re-evaluating multiple times, which could
|
||||
# potentially free the mesh created early on. So we put those meshes to bmain and
|
||||
# free them afterwards. Not ideal but ensures correct ownerwhip.
|
||||
# free them afterwards. Not ideal but ensures correct ownership.
|
||||
tmp_me = bpy.data.meshes.new_from_object(
|
||||
ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
# Triangulate the mesh if requested
|
||||
if settings.use_triangles:
|
||||
import bmesh
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(tmp_me)
|
||||
bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||
bm.to_mesh(tmp_me)
|
||||
bm.free()
|
||||
|
||||
# Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry Nodes,
|
||||
# can change the materials.
|
||||
orig_mats = tuple(slot.material for slot in ob.material_slots)
|
||||
@ -2663,7 +2675,29 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
||||
if orig_mats != eval_mats:
|
||||
# Override the default behaviour of getting materials from ob_obj.bdata.material_slots.
|
||||
ob_obj.override_materials = eval_mats
|
||||
elif do_convert:
|
||||
tmp_me = bpy.data.meshes.new_from_object(ob, preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
elif do_copy:
|
||||
# bpy.data.meshes.new_from_object removes shape keys (see #104714), so create a copy of the mesh instead.
|
||||
tmp_me = ob.data.copy()
|
||||
else:
|
||||
tmp_me = None
|
||||
|
||||
if tmp_me is None:
|
||||
# Use the original data of this Object.
|
||||
data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False)
|
||||
else:
|
||||
# Triangulate the mesh if requested
|
||||
if settings.use_triangles:
|
||||
import bmesh
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(tmp_me)
|
||||
bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||
bm.to_mesh(tmp_me)
|
||||
bm.free()
|
||||
# A temporary mesh was created for this Object, which should be deleted once the export is complete.
|
||||
data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True)
|
||||
|
||||
# Change armatures back.
|
||||
for armature, pose_position in backup_pose_positions:
|
||||
print((armature, pose_position))
|
||||
@ -2675,8 +2709,6 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
||||
mod.show_viewport = show_viewport
|
||||
if backup_pose_positions or tmp_mods:
|
||||
depsgraph.update()
|
||||
if use_org_data:
|
||||
data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False)
|
||||
|
||||
# In case "real" source object of that dupli did not yet still existed in data_meshes, create it now!
|
||||
if org_ob_obj is not None:
|
||||
|
@ -2826,8 +2826,13 @@ class FbxImportHelperNode:
|
||||
elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
|
||||
|
||||
if settings.use_custom_props:
|
||||
# Read Armature Object custom props from the Node
|
||||
blen_read_custom_properties(self.fbx_elem, arm, settings)
|
||||
|
||||
if self.fbx_data_elem:
|
||||
# Read Armature Data custom props from the NodeAttribute
|
||||
blen_read_custom_properties(self.fbx_data_elem, arm_data, settings)
|
||||
|
||||
# instance in scene
|
||||
view_layer.active_layer_collection.collection.objects.link(arm)
|
||||
arm.select_set(True)
|
||||
|
@ -5,7 +5,7 @@
|
||||
bl_info = {
|
||||
'name': 'glTF 2.0 format',
|
||||
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
||||
"version": (4, 0, 15),
|
||||
"version": (4, 0, 19),
|
||||
'blender': (4, 0, 0),
|
||||
'location': 'File > Import-Export',
|
||||
'description': 'Import-Export as glTF 2.0',
|
||||
|
@ -15,9 +15,9 @@ def export_clearcoat(blender_material, export_settings):
|
||||
clearcoat_extension = {}
|
||||
clearcoat_roughness_slots = ()
|
||||
|
||||
clearcoat_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat')
|
||||
clearcoat_roughness_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Roughness')
|
||||
clearcoat_normal_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Normal')
|
||||
clearcoat_socket = gltf2_blender_get.get_socket(blender_material, 'Coat')
|
||||
clearcoat_roughness_socket = gltf2_blender_get.get_socket(blender_material, 'Coat Roughness')
|
||||
clearcoat_normal_socket = gltf2_blender_get.get_socket(blender_material, 'Coat Normal')
|
||||
|
||||
if isinstance(clearcoat_socket, bpy.types.NodeSocket) and not clearcoat_socket.is_linked:
|
||||
clearcoat_extension['clearcoatFactor'] = clearcoat_socket.default_value
|
||||
|
@ -228,15 +228,15 @@ def __get_image_data_mapping(sockets, default_sockets, results, export_settings)
|
||||
# some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes)
|
||||
if socket.name == 'Metallic':
|
||||
dst_chan = Channel.B
|
||||
elif socket.name == 'Roughness':
|
||||
elif socket.name == 'Roughness' and socket.node.type == "BSDF_PRINCIPLED":
|
||||
dst_chan = Channel.G
|
||||
elif socket.name == 'Occlusion':
|
||||
dst_chan = Channel.R
|
||||
elif socket.name == 'Alpha':
|
||||
dst_chan = Channel.A
|
||||
elif socket.name == 'Clearcoat':
|
||||
elif socket.name == 'Coat':
|
||||
dst_chan = Channel.R
|
||||
elif socket.name == 'Clearcoat Roughness':
|
||||
elif socket.name == 'Coat Roughness':
|
||||
dst_chan = Channel.G
|
||||
elif socket.name == 'Thickness': # For KHR_materials_volume
|
||||
dst_chan = Channel.G
|
||||
|
@ -130,19 +130,19 @@ def pbr_metallic_roughness(mh: MaterialHelper):
|
||||
clearcoat(
|
||||
mh,
|
||||
location=locs['clearcoat'],
|
||||
clearcoat_socket=pbr_node.inputs['Clearcoat'],
|
||||
clearcoat_socket=pbr_node.inputs['Coat'],
|
||||
)
|
||||
|
||||
clearcoat_roughness(
|
||||
mh,
|
||||
location=locs['clearcoat_roughness'],
|
||||
roughness_socket=pbr_node.inputs['Clearcoat Roughness'],
|
||||
roughness_socket=pbr_node.inputs['Coat Roughness'],
|
||||
)
|
||||
|
||||
clearcoat_normal(
|
||||
mh,
|
||||
location=locs['clearcoat_normal'],
|
||||
normal_socket=pbr_node.inputs['Clearcoat Normal'],
|
||||
normal_socket=pbr_node.inputs['Coat Normal'],
|
||||
)
|
||||
|
||||
transmission(
|
||||
@ -231,6 +231,12 @@ def calc_locations(mh):
|
||||
locs['metallic_roughness'] = (x, y)
|
||||
if mh.pymat.pbr_metallic_roughness.metallic_roughness_texture is not None:
|
||||
y -= height
|
||||
locs['transmission'] = (x, y)
|
||||
if 'transmissionTexture' in transmission_ext:
|
||||
y -= height
|
||||
locs['normal'] = (x, y)
|
||||
if mh.pymat.normal_texture is not None:
|
||||
y -= height
|
||||
locs['specularTexture'] = (x, y)
|
||||
if 'specularTexture' in specular_ext:
|
||||
y -= height
|
||||
@ -243,18 +249,12 @@ def calc_locations(mh):
|
||||
locs['clearcoat_roughness'] = (x, y)
|
||||
if 'clearcoatRoughnessTexture' in clearcoat_ext:
|
||||
y -= height
|
||||
locs['transmission'] = (x, y)
|
||||
if 'transmissionTexture' in transmission_ext:
|
||||
locs['clearcoat_normal'] = (x, y)
|
||||
if 'clearcoatNormalTexture' in clearcoat_ext:
|
||||
y -= height
|
||||
locs['emission'] = (x, y)
|
||||
if mh.pymat.emissive_texture is not None:
|
||||
y -= height
|
||||
locs['normal'] = (x, y)
|
||||
if mh.pymat.normal_texture is not None:
|
||||
y -= height
|
||||
locs['clearcoat_normal'] = (x, y)
|
||||
if 'clearcoatNormalTexture' in clearcoat_ext:
|
||||
y -= height
|
||||
locs['occlusion'] = (x, y)
|
||||
if mh.pymat.occlusion_texture is not None:
|
||||
y -= height
|
||||
|
@ -52,7 +52,8 @@ class glTFImporter():
|
||||
'KHR_materials_transmission',
|
||||
'KHR_materials_specular',
|
||||
'KHR_materials_sheen',
|
||||
'KHR_materials_ior'
|
||||
'KHR_materials_ior',
|
||||
'KHR_materials_volume'
|
||||
]
|
||||
|
||||
# Add extensions required supported by custom import extensions
|
||||
|
@ -24,7 +24,7 @@ from itertools import chain
|
||||
|
||||
from .interface import NWConnectionListInputs, NWConnectionListOutputs
|
||||
|
||||
from .utils.constants import blend_types, geo_combine_operations, operations, navs, get_nodes_from_category, rl_outputs
|
||||
from .utils.constants import blend_types, geo_combine_operations, operations, navs, get_texture_node_types, rl_outputs
|
||||
from .utils.draw import draw_callback_nodeoutline
|
||||
from .utils.paths import match_files_to_socket_names, split_into_components
|
||||
from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_active_tree, get_nodes_links, is_viewer_socket,
|
||||
@ -507,14 +507,19 @@ class NWPreviewNode(Operator, NWBase):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_output_sockets(cls, node_tree):
|
||||
return [item for item in node_tree.interface.items_tree if item.item_type == 'SOCKET' and item.in_out in {'OUTPUT', 'BOTH'}]
|
||||
|
||||
def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
|
||||
# check if a viewer output already exists in a node group otherwise create
|
||||
if hasattr(node, "node_tree"):
|
||||
index = None
|
||||
if len(node.node_tree.outputs):
|
||||
viewer_socket = None
|
||||
output_sockets = self.get_output_sockets(node.node_tree)
|
||||
if len(output_sockets):
|
||||
free_socket = None
|
||||
for i, socket in enumerate(node.node_tree.outputs):
|
||||
if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
|
||||
for socket in output_sockets:
|
||||
if is_viewer_socket(socket) and socket.socket_type == socket_type:
|
||||
# if viewer output is already used but leads to the same socket we can still use it
|
||||
is_used = self.is_socket_used_other_mats(socket)
|
||||
if is_used:
|
||||
@ -525,19 +530,18 @@ class NWPreviewNode(Operator, NWBase):
|
||||
links = groupout_input.links
|
||||
if connect_socket not in [link.from_socket for link in links]:
|
||||
continue
|
||||
index = i
|
||||
viewer_socket = socket
|
||||
break
|
||||
if not free_socket:
|
||||
free_socket = i
|
||||
if not index and free_socket:
|
||||
index = free_socket
|
||||
free_socket = socket
|
||||
if not viewer_socket and free_socket:
|
||||
viewer_socket = free_socket
|
||||
|
||||
if not index:
|
||||
if not viewer_socket:
|
||||
# create viewer socket
|
||||
node.node_tree.outputs.new(socket_type, viewer_socket_name)
|
||||
index = len(node.node_tree.outputs) - 1
|
||||
node.node_tree.outputs[index].NWViewerSocket = True
|
||||
return index
|
||||
viewer_socket = node.node_tree.interface.new_socket(viewer_socket_name, in_out={'OUTPUT'}, socket_type=socket_type)
|
||||
viewer_socket.NWViewerSocket = True
|
||||
return viewer_socket
|
||||
|
||||
def init_shader_variables(self, space, shader_type):
|
||||
if shader_type == 'OBJECT':
|
||||
@ -582,10 +586,9 @@ class NWPreviewNode(Operator, NWBase):
|
||||
next_node = link.from_node
|
||||
external_socket = link.from_socket
|
||||
if hasattr(next_node, "node_tree"):
|
||||
for socket_index, s in enumerate(next_node.outputs):
|
||||
if s == external_socket:
|
||||
for socket_index, socket in enumerate(next_node.node_tree.interface.items_tree):
|
||||
if socket.identifier == external_socket.identifier:
|
||||
break
|
||||
socket = next_node.node_tree.outputs[socket_index]
|
||||
if is_viewer_socket(socket) and socket not in sockets:
|
||||
sockets.append(socket)
|
||||
# continue search inside of node group but restrict socket to where we came from
|
||||
@ -599,11 +602,17 @@ class NWPreviewNode(Operator, NWBase):
|
||||
if hasattr(node, "node_tree"):
|
||||
if node.node_tree is None:
|
||||
continue
|
||||
for socket in node.node_tree.outputs:
|
||||
for socket in cls.get_output_sockets(node.node_tree):
|
||||
if is_viewer_socket(socket) and (socket not in sockets):
|
||||
sockets.append(socket)
|
||||
cls.scan_nodes(node.node_tree, sockets)
|
||||
|
||||
@classmethod
|
||||
def remove_socket(cls, tree, socket):
|
||||
interface = tree.interface
|
||||
interface.remove(socket)
|
||||
interface.active_index = min(interface.active_index, len(interface.items_tree) - 1)
|
||||
|
||||
def link_leads_to_used_socket(self, link):
|
||||
# return True if link leads to a socket that is already used in this material
|
||||
socket = get_internal_socket(link.to_socket)
|
||||
@ -710,22 +719,22 @@ class NWPreviewNode(Operator, NWBase):
|
||||
link_end = output_socket
|
||||
while tree.nodes.active != active:
|
||||
node = tree.nodes.active
|
||||
index = self.ensure_viewer_socket(
|
||||
viewer_socket = self.ensure_viewer_socket(
|
||||
node, 'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
|
||||
link_start = node.outputs[index]
|
||||
node_socket = node.node_tree.outputs[index]
|
||||
link_start = node.outputs[viewer_socket_name]
|
||||
node_socket = viewer_socket
|
||||
if node_socket in delete_sockets:
|
||||
delete_sockets.remove(node_socket)
|
||||
connect_sockets(link_start, link_end)
|
||||
# Iterate
|
||||
link_end = self.ensure_group_output(node.node_tree).inputs[index]
|
||||
link_end = self.ensure_group_output(node.node_tree).inputs[viewer_socket_name]
|
||||
tree = tree.nodes.active.node_tree
|
||||
connect_sockets(active.outputs[out_i], link_end)
|
||||
|
||||
# Delete sockets
|
||||
for socket in delete_sockets:
|
||||
tree = socket.id_data
|
||||
tree.outputs.remove(socket)
|
||||
self.remove_socket(tree, socket)
|
||||
|
||||
nodes.active = active
|
||||
active.select = True
|
||||
@ -733,11 +742,8 @@ class NWPreviewNode(Operator, NWBase):
|
||||
return {'FINISHED'}
|
||||
|
||||
# What follows is code for the shader editor
|
||||
output_types = [x.nodetype for x in
|
||||
get_nodes_from_category('Output', context)]
|
||||
valid = False
|
||||
if active:
|
||||
if active.rna_type.identifier not in output_types:
|
||||
for out in active.outputs:
|
||||
if is_visible_socket(out):
|
||||
valid = True
|
||||
@ -786,15 +792,15 @@ class NWPreviewNode(Operator, NWBase):
|
||||
link_end = output_socket
|
||||
while tree.nodes.active != active:
|
||||
node = tree.nodes.active
|
||||
index = self.ensure_viewer_socket(
|
||||
viewer_socket = self.ensure_viewer_socket(
|
||||
node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
|
||||
link_start = node.outputs[index]
|
||||
node_socket = node.node_tree.outputs[index]
|
||||
link_start = node.outputs[viewer_socket_name]
|
||||
node_socket = viewer_socket
|
||||
if node_socket in delete_sockets:
|
||||
delete_sockets.remove(node_socket)
|
||||
connect_sockets(link_start, link_end)
|
||||
# Iterate
|
||||
link_end = self.ensure_group_output(node.node_tree).inputs[index]
|
||||
link_end = self.ensure_group_output(node.node_tree).inputs[viewer_socket_name]
|
||||
tree = tree.nodes.active.node_tree
|
||||
connect_sockets(active.outputs[out_i], link_end)
|
||||
|
||||
@ -802,7 +808,7 @@ class NWPreviewNode(Operator, NWBase):
|
||||
for socket in delete_sockets:
|
||||
if not self.is_socket_used_other_mats(socket):
|
||||
tree = socket.id_data
|
||||
tree.outputs.remove(socket)
|
||||
self.remove_socket(tree, socket)
|
||||
|
||||
nodes.active = active
|
||||
active.select = True
|
||||
@ -1820,8 +1826,7 @@ class NWAddTextureSetup(Operator, NWBase):
|
||||
def execute(self, context):
|
||||
nodes, links = get_nodes_links(context)
|
||||
|
||||
texture_types = [x.nodetype for x in
|
||||
get_nodes_from_category('Texture', context)]
|
||||
texture_types = get_texture_node_types()
|
||||
selected_nodes = [n for n in nodes if n.select]
|
||||
|
||||
for node in selected_nodes:
|
||||
|
@ -3,7 +3,6 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from collections import namedtuple
|
||||
from nodeitems_utils import node_categories_iter
|
||||
|
||||
|
||||
#################
|
||||
@ -160,11 +159,23 @@ draw_color_sets = {
|
||||
}
|
||||
|
||||
|
||||
def get_nodes_from_category(category_name, context):
|
||||
for category in node_categories_iter(context):
|
||||
if category.name == category_name:
|
||||
return sorted(category.items(context), key=lambda node: node.label)
|
||||
|
||||
def get_texture_node_types():
|
||||
return [
|
||||
"ShaderNodeTexBrick",
|
||||
"ShaderNodeTexChecker",
|
||||
"ShaderNodeTexEnvironment",
|
||||
"ShaderNodeTexGradient",
|
||||
"ShaderNodeTexIES",
|
||||
"ShaderNodeTexImage",
|
||||
"ShaderNodeTexMagic",
|
||||
"ShaderNodeTexMusgrave",
|
||||
"ShaderNodeTexNoise",
|
||||
"ShaderNodeTexPointDensity",
|
||||
"ShaderNodeTexSky",
|
||||
"ShaderNodeTexVoronoi",
|
||||
"ShaderNodeTexWave",
|
||||
"ShaderNodeTexWhiteNoise"
|
||||
]
|
||||
|
||||
def nice_hotkey_name(punc):
|
||||
# convert the ugly string name into the actual character
|
||||
|
@ -170,25 +170,18 @@ def get_internal_socket(socket):
|
||||
# get the internal socket from a socket inside or outside the group
|
||||
node = socket.node
|
||||
if node.type == 'GROUP_OUTPUT':
|
||||
source_iterator = node.inputs
|
||||
iterator = node.id_data.outputs
|
||||
iterator = node.id_data.interface.items_tree
|
||||
elif node.type == 'GROUP_INPUT':
|
||||
source_iterator = node.outputs
|
||||
iterator = node.id_data.inputs
|
||||
iterator = node.id_data.interface.items_tree
|
||||
elif hasattr(node, "node_tree"):
|
||||
if socket.is_output:
|
||||
source_iterator = node.outputs
|
||||
iterator = node.node_tree.outputs
|
||||
else:
|
||||
source_iterator = node.inputs
|
||||
iterator = node.node_tree.inputs
|
||||
iterator = node.node_tree.interface.items_tree
|
||||
else:
|
||||
return None
|
||||
|
||||
for i, s in enumerate(source_iterator):
|
||||
if s == socket:
|
||||
break
|
||||
return iterator[i]
|
||||
for s in iterator:
|
||||
if s.identifier == socket.identifier:
|
||||
return s
|
||||
return iterator[0]
|
||||
|
||||
|
||||
def is_viewer_link(link, output_node):
|
||||
|
@ -9,7 +9,7 @@ Pose Library based on the Asset Browser.
|
||||
bl_info = {
|
||||
"name": "Pose Library",
|
||||
"description": "Pose Library based on the Asset Browser.",
|
||||
"author": "Sybren A. Stüvel",
|
||||
"author": "Sybren A. Stüvel, Julian Eisel",
|
||||
"version": (2, 0),
|
||||
"blender": (3, 0, 0),
|
||||
"location": "Asset Browser -> Animations, and 3D Viewport -> Animation panel",
|
||||
|
@ -9,6 +9,7 @@ Pose Library - GUI definition.
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
AssetHandle,
|
||||
AssetRepresentation,
|
||||
Context,
|
||||
Menu,
|
||||
Panel,
|
||||
@ -41,11 +42,11 @@ class VIEW3D_AST_pose_library(bpy.types.AssetShelf):
|
||||
return PoseLibraryPanel.poll(context)
|
||||
|
||||
@classmethod
|
||||
def asset_poll(cls, asset: AssetHandle) -> bool:
|
||||
return asset.file_data.id_type == 'ACTION'
|
||||
def asset_poll(cls, asset: AssetRepresentation) -> bool:
|
||||
return asset.id_type == 'ACTION'
|
||||
|
||||
@classmethod
|
||||
def draw_context_menu(cls, _context: Context, _asset: AssetHandle, layout: UILayout):
|
||||
def draw_context_menu(cls, _context: Context, _asset: AssetRepresentation, layout: UILayout):
|
||||
# Make sure these operator properties match those used in `VIEW3D_PT_pose_library_legacy`.
|
||||
layout.operator("poselib.apply_pose_asset", text="Apply Pose").flipped = False
|
||||
layout.operator("poselib.apply_pose_asset", text="Apply Pose Flipped").flipped = True
|
||||
@ -89,10 +90,10 @@ def pose_library_list_item_context_menu(self: UIList, context: Context) -> None:
|
||||
return True
|
||||
|
||||
def is_pose_library_asset_browser() -> bool:
|
||||
asset_library_ref = getattr(context, "asset_library_ref", None)
|
||||
asset_library_ref = getattr(context, "asset_library_reference", None)
|
||||
if not asset_library_ref:
|
||||
return False
|
||||
asset = getattr(context, "asset_file_handle", None)
|
||||
asset = getattr(context, "asset", None)
|
||||
if not asset:
|
||||
return False
|
||||
return bool(asset.id_type == 'ACTION')
|
||||
@ -185,7 +186,7 @@ def _on_asset_library_changed() -> None:
|
||||
|
||||
def register_message_bus() -> None:
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=(bpy.types.FileAssetSelectParams, "asset_library_ref"),
|
||||
key=(bpy.types.FileAssetSelectParams, "asset_library_reference"),
|
||||
owner=_msgbus_owner,
|
||||
args=(),
|
||||
notify=_on_asset_library_changed,
|
||||
|
@ -25,9 +25,9 @@ import bpy
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
from bpy.types import (
|
||||
Action,
|
||||
AssetRepresentation,
|
||||
Context,
|
||||
Event,
|
||||
FileSelectEntry,
|
||||
Object,
|
||||
Operator,
|
||||
)
|
||||
@ -75,7 +75,7 @@ class POSELIB_OT_create_pose_asset(PoseAssetCreator, Operator):
|
||||
return True
|
||||
|
||||
asset_space_params = asset_browser.params(asset_browse_area)
|
||||
if asset_space_params.asset_library_ref != 'LOCAL':
|
||||
if asset_space_params.asset_library_reference != 'LOCAL':
|
||||
cls.poll_message_set("Asset Browser must be set to the Current File library")
|
||||
return False
|
||||
|
||||
@ -262,7 +262,7 @@ class POSELIB_OT_paste_asset(Operator):
|
||||
cls.poll_message_set("Current editor is not an asset browser")
|
||||
return False
|
||||
|
||||
asset_lib_ref = context.space_data.params.asset_library_ref
|
||||
asset_lib_ref = context.space_data.params.asset_library_reference
|
||||
if asset_lib_ref != 'LOCAL':
|
||||
cls.poll_message_set("Asset Browser must be set to the Current File library")
|
||||
return False
|
||||
@ -313,13 +313,13 @@ class PoseAssetUser:
|
||||
if not (
|
||||
context.object
|
||||
and context.object.mode == "POSE" # This condition may not be desired.
|
||||
and context.asset_file_handle
|
||||
and context.asset
|
||||
):
|
||||
return False
|
||||
return context.asset_file_handle.id_type == 'ACTION'
|
||||
return context.asset.id_type == 'ACTION'
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
asset: FileSelectEntry = context.asset_file_handle
|
||||
asset: AssetRepresentation = context.asset
|
||||
if asset.local_id:
|
||||
return self.use_pose(context, asset.local_id)
|
||||
return self._load_and_use_pose(context)
|
||||
@ -329,13 +329,14 @@ class PoseAssetUser:
|
||||
pass
|
||||
|
||||
def _load_and_use_pose(self, context: Context) -> Set[str]:
|
||||
asset = context.asset_file_handle
|
||||
asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset)
|
||||
asset = context.asset
|
||||
asset_lib_path = asset.full_library_path
|
||||
|
||||
if not asset_lib_path:
|
||||
self.report( # type: ignore
|
||||
{"ERROR"},
|
||||
# TODO: Add some way to get the library name from the library reference (just asset_library_ref.name?).
|
||||
# TODO: Add some way to get the library name from the library reference
|
||||
# (just asset_library_reference.name?).
|
||||
tip_("Selected asset %s could not be located inside the asset library") % asset.name,
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
@ -40,6 +40,7 @@ initial_load_order = [
|
||||
'utils.mechanism',
|
||||
'utils.animation',
|
||||
'utils.metaclass',
|
||||
'utils.objects',
|
||||
'feature_sets',
|
||||
'rigs',
|
||||
'rigs.utils',
|
||||
@ -708,6 +709,14 @@ def register():
|
||||
get=color_set_get, set=color_set_set, search=color_set_search
|
||||
)
|
||||
|
||||
# Object properties
|
||||
obj_store = bpy.types.Object
|
||||
|
||||
obj_store.rigify_owner_rig = PointerProperty(
|
||||
type=bpy.types.Object,
|
||||
name="Rigify Owner Rig",
|
||||
description="Rig that owns this object and may delete or overwrite it upon re-generation")
|
||||
|
||||
prefs = RigifyPreferences.get_instance()
|
||||
prefs.register_feature_sets(True)
|
||||
prefs.update_external_rigs()
|
||||
@ -772,6 +781,10 @@ def unregister():
|
||||
del coll_store.rigify_color_set_id
|
||||
del coll_store.rigify_color_set_name
|
||||
|
||||
obj_store: typing.Any = bpy.types.Object
|
||||
|
||||
del obj_store.rigify_owner_rig
|
||||
|
||||
# Classes.
|
||||
for cls in classes:
|
||||
unregister_class(cls)
|
||||
|
@ -21,6 +21,7 @@ from . import base_rig
|
||||
from itertools import count
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .utils.objects import ArtifactManager
|
||||
from .rig_ui_template import ScriptGenerator
|
||||
|
||||
|
||||
@ -192,6 +193,7 @@ class BaseGenerator:
|
||||
obj: ArmatureObject
|
||||
|
||||
script: 'ScriptGenerator'
|
||||
artifacts: 'ArtifactManager'
|
||||
|
||||
rig_list: List[base_rig.BaseRig]
|
||||
root_rigs: List[base_rig.BaseRig]
|
||||
|
@ -24,6 +24,7 @@ from .utils.rig import get_rigify_type, get_rigify_target_rig,\
|
||||
get_rigify_rig_basename, get_rigify_force_widget_update, get_rigify_finalize_script,\
|
||||
get_rigify_mirror_widgets, get_rigify_colors
|
||||
from .utils.action_layers import ActionLayerBuilder
|
||||
from .utils.objects import ArtifactManager
|
||||
|
||||
from . import base_generate
|
||||
from . import rig_ui_template
|
||||
@ -135,6 +136,8 @@ class Generator(base_generate.BaseGenerator):
|
||||
if obj_found:
|
||||
self.saved_visible_layers = {coll.name: coll.is_visible for coll in obj.data.collections}
|
||||
|
||||
self.artifacts.generate_init_existing(obj)
|
||||
|
||||
def __find_legacy_collection(self) -> bpy.types.Collection:
|
||||
"""For backwards comp, matching by name to find a legacy collection.
|
||||
(For before there was a Widget Collection PointerProperty)
|
||||
@ -217,8 +220,12 @@ class Generator(base_generate.BaseGenerator):
|
||||
|
||||
validate_collection_references(self.metarig)
|
||||
|
||||
if ROOT_COLLECTION not in collections:
|
||||
coll = collections.get(ROOT_COLLECTION)
|
||||
|
||||
if not coll:
|
||||
coll = collections.new(ROOT_COLLECTION)
|
||||
|
||||
if coll.rigify_ui_row <= 0:
|
||||
coll.rigify_ui_row = 2 + choose_next_uid(collections, 'rigify_ui_row', min_value=1)
|
||||
|
||||
def __duplicate_rig(self):
|
||||
@ -452,6 +459,8 @@ class Generator(base_generate.BaseGenerator):
|
||||
self.__unhide_rig_object(obj)
|
||||
|
||||
# Collect data from the existing rig
|
||||
self.artifacts = ArtifactManager(self)
|
||||
|
||||
self.__save_rig_data(obj, obj_found)
|
||||
|
||||
# Select the chosen working collection in case it changed
|
||||
@ -633,6 +642,8 @@ class Generator(base_generate.BaseGenerator):
|
||||
|
||||
obj.data.collections.active_index = 0
|
||||
|
||||
self.artifacts.generate_cleanup()
|
||||
|
||||
###########################################
|
||||
# Restore active collection
|
||||
view_layer.active_layer_collection = self.layer_collection
|
||||
|
@ -8,6 +8,7 @@ from collections import OrderedDict
|
||||
from typing import Union, Optional, Any
|
||||
|
||||
from .utils.animation import SCRIPT_REGISTER_BAKE, SCRIPT_UTILITIES_BAKE
|
||||
from .utils.mechanism import quote_property
|
||||
|
||||
from . import base_generate
|
||||
|
||||
@ -918,6 +919,157 @@ class RigLayers(bpy.types.Panel):
|
||||
'''
|
||||
|
||||
|
||||
class PanelExpression(object):
|
||||
"""A runtime expression involving bone properties"""
|
||||
|
||||
_rigify_expr: str
|
||||
|
||||
def __init__(self, expr: str):
|
||||
self._rigify_expr = expr
|
||||
|
||||
def __repr__(self):
|
||||
return self._rigify_expr
|
||||
|
||||
def __add__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} + {repr(other)})")
|
||||
|
||||
def __sub__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} - {repr(other)})")
|
||||
|
||||
def __mul__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} * {repr(other)})")
|
||||
|
||||
def __matmul__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} @ {repr(other)})")
|
||||
|
||||
def __truediv__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} / {repr(other)})")
|
||||
|
||||
def __floordiv__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} // {repr(other)})")
|
||||
|
||||
def __mod__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} % {repr(other)})")
|
||||
|
||||
def __lshift__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} << {repr(other)})")
|
||||
|
||||
def __rshift__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} >> {repr(other)})")
|
||||
|
||||
def __and__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} & {repr(other)})")
|
||||
|
||||
def __xor__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} ^ {repr(other)})")
|
||||
|
||||
def __or__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} | {repr(other)})")
|
||||
|
||||
def __radd__(self, other):
|
||||
return PanelExpression(f"({repr(other)} + {self._rigify_expr})")
|
||||
|
||||
def __rsub__(self, other):
|
||||
return PanelExpression(f"({repr(other)} - {self._rigify_expr})")
|
||||
|
||||
def __rmul__(self, other):
|
||||
return PanelExpression(f"({repr(other)} * {self._rigify_expr})")
|
||||
|
||||
def __rmatmul__(self, other):
|
||||
return PanelExpression(f"({repr(other)} @ {self._rigify_expr})")
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return PanelExpression(f"({repr(other)} / {self._rigify_expr})")
|
||||
|
||||
def __rfloordiv__(self, other):
|
||||
return PanelExpression(f"({repr(other)} // {self._rigify_expr})")
|
||||
|
||||
def __rmod__(self, other):
|
||||
return PanelExpression(f"({repr(other)} % {self._rigify_expr})")
|
||||
|
||||
def __rlshift__(self, other):
|
||||
return PanelExpression(f"({repr(other)} << {self._rigify_expr})")
|
||||
|
||||
def __rrshift__(self, other):
|
||||
return PanelExpression(f"({repr(other)} >> {self._rigify_expr})")
|
||||
|
||||
def __rand__(self, other):
|
||||
return PanelExpression(f"({repr(other)} & {self._rigify_expr})")
|
||||
|
||||
def __rxor__(self, other):
|
||||
return PanelExpression(f"({repr(other)} ^ {self._rigify_expr})")
|
||||
|
||||
def __ror__(self, other):
|
||||
return PanelExpression(f"({repr(other)} | {self._rigify_expr})")
|
||||
|
||||
def __neg__(self):
|
||||
return PanelExpression(f"-{self._rigify_expr}")
|
||||
|
||||
def __pos__(self):
|
||||
return PanelExpression(f"+{self._rigify_expr}")
|
||||
|
||||
def __abs__(self):
|
||||
return PanelExpression(f"abs({self._rigify_expr})")
|
||||
|
||||
def __invert__(self):
|
||||
return PanelExpression(f"~{self._rigify_expr}")
|
||||
|
||||
def __int__(self):
|
||||
return PanelExpression(f"int({self._rigify_expr})")
|
||||
|
||||
def __float__(self):
|
||||
return PanelExpression(f"float({self._rigify_expr})")
|
||||
|
||||
def __round__(self, digits=None):
|
||||
return PanelExpression(f"round({self._rigify_expr}, {digits})")
|
||||
|
||||
def __trunc__(self):
|
||||
return PanelExpression(f"trunc({self._rigify_expr})")
|
||||
|
||||
def __floor__(self):
|
||||
return PanelExpression(f"floor({self._rigify_expr})")
|
||||
|
||||
def __ceil__(self):
|
||||
return PanelExpression(f"ceil({self._rigify_expr})")
|
||||
|
||||
def __lt__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} < {repr(other)})")
|
||||
|
||||
def __le__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} <= {repr(other)})")
|
||||
|
||||
def __eq__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} == {repr(other)})")
|
||||
|
||||
def __ne__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} != {repr(other)})")
|
||||
|
||||
def __gt__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} > {repr(other)})")
|
||||
|
||||
def __ge__(self, other):
|
||||
return PanelExpression(f"({self._rigify_expr} >= {repr(other)})")
|
||||
|
||||
def __bool__(self):
|
||||
raise NotImplementedError("This object wraps an expression, not a value; casting to boolean is meaningless")
|
||||
|
||||
|
||||
class PanelReferenceExpression(PanelExpression):
|
||||
"""
|
||||
A runtime expression referencing an object.
|
||||
@DynamicAttrs
|
||||
"""
|
||||
|
||||
def __getitem__(self, item):
|
||||
return PanelReferenceExpression(self._rigify_expr + quote_property(item))
|
||||
|
||||
def __getattr__(self, item):
|
||||
return PanelReferenceExpression(self._rigify_expr + '.' + quote_property(item))
|
||||
|
||||
def get(self, item, default=None):
|
||||
return PanelReferenceExpression(f"{self._rigify_expr}.get({repr(item)}, {repr(default)})")
|
||||
|
||||
|
||||
def quote_parameters(positional: list[Any], named: dict[str, Any]):
|
||||
"""Quote the given positional and named parameters as a code string."""
|
||||
positional_list = [repr(v) for v in positional]
|
||||
@ -1036,6 +1188,51 @@ class PanelLayout(object):
|
||||
"""Add a split layout to the panel."""
|
||||
return self.add_nested_layout('split', params)
|
||||
|
||||
@staticmethod
|
||||
def expr_bone(bone_name: str):
|
||||
"""Returns an expression referencing the specified pose bone."""
|
||||
return PanelReferenceExpression(f"pose_bones[%r]" % bone_name)
|
||||
|
||||
@staticmethod
|
||||
def expr_and(*expressions):
|
||||
"""Returns a boolean and expression of its parameters."""
|
||||
return PanelExpression("(" + " and ".join(repr(e) for e in expressions) + ")")
|
||||
|
||||
@staticmethod
|
||||
def expr_or(*expressions):
|
||||
"""Returns a boolean or expression of its parameters."""
|
||||
return PanelExpression("(" + " or ".join(repr(e) for e in expressions) + ")")
|
||||
|
||||
@staticmethod
|
||||
def expr_if_else(condition, true_expr, false_expr):
|
||||
"""Returns a conditional expression."""
|
||||
return PanelExpression(f"({repr(true_expr)} if {repr(condition)} else {repr(false_expr)})")
|
||||
|
||||
@staticmethod
|
||||
def expr_call(func: str, *expressions):
|
||||
"""Returns an expression calling the specified function with given parameters."""
|
||||
return PanelExpression(func + "(" + ", ".join(repr(e) for e in expressions) + ")")
|
||||
|
||||
def set_layout_property(self, prop_name: str, prop_value: Any):
|
||||
assert self.index > 0 # Don't change properties on the root layout
|
||||
self.add_line("%s.%s = %r" % (self.layout, prop_name, prop_value))
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
raise NotImplementedError("This is a write only property")
|
||||
|
||||
@active.setter
|
||||
def active(self, value):
|
||||
self.set_layout_property('active', value)
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
raise NotImplementedError("This is a write only property")
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value):
|
||||
self.set_layout_property('enabled', value)
|
||||
|
||||
|
||||
class BoneSetPanelLayout(PanelLayout):
|
||||
"""Panel restricted to a certain set of bones."""
|
||||
|
@ -4,7 +4,9 @@
|
||||
|
||||
import bpy
|
||||
import math
|
||||
import json
|
||||
|
||||
from typing import Optional
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from ...utils.rig import is_rig_base_bone
|
||||
@ -13,12 +15,15 @@ from ...utils.bones import put_bone, align_bone_orientation
|
||||
from ...utils.naming import make_derived_name
|
||||
from ...utils.misc import matrix_from_axis_roll, matrix_from_axis_pair
|
||||
from ...utils.widgets import adjust_widget_transform_mesh
|
||||
from ...utils.animation import add_fk_ik_snap_buttons
|
||||
from ...utils.mechanism import driver_var_transform
|
||||
|
||||
from ..widgets import create_foot_widget, create_ball_socket_widget
|
||||
|
||||
from ...base_rig import stage
|
||||
from ...rig_ui_template import PanelLayout
|
||||
|
||||
from .limb_rigs import BaseLimbRig
|
||||
from .limb_rigs import BaseLimbRig, SCRIPT_UTILITIES_OP_SNAP_IK_FK
|
||||
|
||||
|
||||
DEG_360 = math.pi * 2
|
||||
@ -33,6 +38,7 @@ class Rig(BaseLimbRig):
|
||||
pivot_type: str
|
||||
heel_euler_order: str
|
||||
use_ik_toe: bool
|
||||
use_toe_roll: bool
|
||||
|
||||
ik_matrix: Matrix
|
||||
roll_matrix: Matrix
|
||||
@ -55,6 +61,7 @@ class Rig(BaseLimbRig):
|
||||
self.pivot_type = self.params.foot_pivot_type
|
||||
self.heel_euler_order = 'ZXY' if self.main_axis == 'x' else 'XZY'
|
||||
self.use_ik_toe = self.params.extra_ik_toe
|
||||
self.use_toe_roll = self.params.extra_toe_roll
|
||||
|
||||
if self.use_ik_toe:
|
||||
self.fk_name_suffix_cutoff = 3
|
||||
@ -116,6 +123,37 @@ class Rig(BaseLimbRig):
|
||||
list[str]
|
||||
]
|
||||
|
||||
####################################################
|
||||
# UI
|
||||
|
||||
def add_global_buttons(self, panel, rig_name):
|
||||
super().add_global_buttons(panel, rig_name)
|
||||
|
||||
ik_chain, tail_chain, fk_chain = self.get_ik_fk_position_chains()
|
||||
|
||||
add_leg_snap_ik_to_fk(
|
||||
panel,
|
||||
master=self.bones.ctrl.master,
|
||||
fk_bones=fk_chain, ik_bones=ik_chain, tail_bones=tail_chain,
|
||||
ik_ctrl_bones=self.get_ik_control_chain(),
|
||||
ik_extra_ctrls=self.get_extra_ik_controls(),
|
||||
heel_control=self.bones.ctrl.heel,
|
||||
rig_name=rig_name
|
||||
)
|
||||
|
||||
def add_ik_only_buttons(self, panel, rig_name):
|
||||
super().add_ik_only_buttons(panel, rig_name)
|
||||
|
||||
if self.use_toe_roll:
|
||||
bone = self.bones.ctrl.heel
|
||||
|
||||
self.make_property(
|
||||
bone, 'Toe_Roll', default=0.0,
|
||||
description='Pivot on the tip of the toe when rolling forward with the heel control'
|
||||
)
|
||||
|
||||
panel.custom_prop(bone, 'Toe_Roll', text='Roll On Toe', slider=True)
|
||||
|
||||
####################################################
|
||||
# IK controls
|
||||
|
||||
@ -282,6 +320,17 @@ class Rig(BaseLimbRig):
|
||||
put_bone(self.obj, rock1, heel_bone.tail, matrix=self.roll_matrix, scale=0.5)
|
||||
put_bone(self.obj, rock2, heel_bone.head, matrix=self.roll_matrix, scale=0.5)
|
||||
|
||||
if self.use_toe_roll:
|
||||
roll3 = self.copy_bone(toe, make_derived_name(heel, 'mch', '_roll3'), scale=0.3)
|
||||
|
||||
toe_pos = Vector(self.get_bone(toe).tail)
|
||||
toe_pos.z = self.get_bone(roll2).head.z
|
||||
|
||||
put_bone(self.obj, roll3, toe_pos, matrix=self.roll_matrix)
|
||||
|
||||
return [rock2, rock1, roll2, roll3, roll1, result]
|
||||
|
||||
else:
|
||||
return [rock2, rock1, roll2, roll1, result]
|
||||
|
||||
@stage.parent_bones
|
||||
@ -295,6 +344,35 @@ class Rig(BaseLimbRig):
|
||||
self.rig_roll_mch_bones(self.bones.mch.heel, self.bones.ctrl.heel, self.bones.org.heel)
|
||||
|
||||
def rig_roll_mch_bones(self, chain: list[str], heel: str, org_heel: str):
|
||||
if self.use_toe_roll:
|
||||
rock2, rock1, roll2, roll3, roll1, result = chain
|
||||
|
||||
# Interpolate rotation in Euler space via drivers to simplify Snap With Roll
|
||||
self.make_driver(
|
||||
roll3, 'rotation_euler', index=0,
|
||||
expression='max(0,x*i)' if self.main_axis == 'x' else 'x*i',
|
||||
variables={
|
||||
'x': driver_var_transform(
|
||||
self.obj, heel, type='ROT_X', space='LOCAL',
|
||||
rotation_mode=self.heel_euler_order,
|
||||
),
|
||||
'i': (heel, 'Toe_Roll'),
|
||||
}
|
||||
)
|
||||
|
||||
self.make_driver(
|
||||
roll3, 'rotation_euler', index=2,
|
||||
expression='max(0,z*i)' if self.main_axis == 'z' else 'z*i',
|
||||
variables={
|
||||
'z': driver_var_transform(
|
||||
self.obj, heel, type='ROT_Z', space='LOCAL',
|
||||
rotation_mode=self.heel_euler_order,
|
||||
),
|
||||
'i': (heel, 'Toe_Roll'),
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
rock2, rock1, roll2, roll1, result = chain
|
||||
|
||||
# This order is required for correct working of the constraints
|
||||
@ -392,14 +470,170 @@ class Rig(BaseLimbRig):
|
||||
description="Generate a separate IK toe control for better IK/FK snapping"
|
||||
)
|
||||
|
||||
params.extra_toe_roll = bpy.props.BoolProperty(
|
||||
name='Toe Tip Roll',
|
||||
default=False,
|
||||
description="Generate a slider to pivot forward heel roll on the tip rather than the base of the toe"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(cls, layout, params, end='Foot'):
|
||||
layout.prop(params, 'foot_pivot_type')
|
||||
layout.prop(params, 'extra_ik_toe')
|
||||
layout.prop(params, 'extra_toe_roll')
|
||||
|
||||
super().parameters_ui(layout, params, end)
|
||||
|
||||
|
||||
##########################
|
||||
# Leg IK to FK operator ##
|
||||
##########################
|
||||
|
||||
SCRIPT_REGISTER_OP_LEG_SNAP_IK_FK = [
|
||||
'POSE_OT_rigify_leg_roll_ik2fk', 'POSE_OT_rigify_leg_roll_ik2fk_bake']
|
||||
|
||||
SCRIPT_UTILITIES_OP_LEG_SNAP_IK_FK = SCRIPT_UTILITIES_OP_SNAP_IK_FK + ['''
|
||||
#######################
|
||||
## Leg Snap IK to FK ##
|
||||
#######################
|
||||
|
||||
class RigifyLegRollIk2FkBase(RigifyLimbIk2FkBase):
|
||||
heel_control: StringProperty(name="Heel")
|
||||
use_roll: bpy.props.BoolVectorProperty(
|
||||
name="Use Roll", size=3, default=(True, True, False),
|
||||
description="Specifies which rotation axes of the heel roll control to use"
|
||||
)
|
||||
|
||||
MODES = {
|
||||
'ZXY': ((0, 2), (1, 0, 2)),
|
||||
'XZY': ((2, 0), (2, 0, 1)),
|
||||
}
|
||||
|
||||
def save_frame_state(self, context, obj):
|
||||
return get_chain_transform_matrices(obj, self.fk_bone_list + self.ctrl_bone_list[-1:])
|
||||
|
||||
def assign_extra_controls(self, context, obj, all_matrices, ik_bones, ctrl_bones):
|
||||
for extra in self.extra_ctrl_list:
|
||||
set_transform_from_matrix(
|
||||
obj, extra, Matrix.Identity(4), space='LOCAL', keyflags=self.keyflags
|
||||
)
|
||||
|
||||
if any(self.use_roll):
|
||||
foot_matrix = all_matrices[len(ik_bones) - 1]
|
||||
ctrl_matrix = all_matrices[len(self.fk_bone_list)]
|
||||
heel_bone = obj.pose.bones[self.heel_control]
|
||||
foot_bone = ctrl_bones[-1]
|
||||
|
||||
# Relative rotation of heel from orientation of master IK control
|
||||
# to actual foot orientation.
|
||||
heel_rest = convert_pose_matrix_via_rest_delta(ctrl_matrix, foot_bone, heel_bone)
|
||||
heel_rot = convert_pose_matrix_via_rest_delta(foot_matrix, ik_bones[-1], heel_bone)
|
||||
|
||||
# Decode the euler decomposition mode
|
||||
rot_mode = heel_bone.rotation_mode
|
||||
indices, use_map = self.MODES[rot_mode]
|
||||
use_roll = [self.use_roll[i] for i in use_map]
|
||||
roll, turn = indices
|
||||
|
||||
# If the last rotation (yaw) is unused, move it to be first for better result
|
||||
if not use_roll[turn]:
|
||||
rot_mode = rot_mode[1:] + rot_mode[0:1]
|
||||
|
||||
local_rot = (heel_rest.inverted() @ heel_rot).to_euler(rot_mode)
|
||||
|
||||
heel_bone.rotation_euler = [
|
||||
(val if use else 0) for val, use in zip(local_rot, use_roll)
|
||||
]
|
||||
|
||||
if self.keyflags is not None:
|
||||
keyframe_transform_properties(
|
||||
obj, bone_name, self.keyflags, no_loc=True, no_rot=no_rot, no_scale=True
|
||||
)
|
||||
|
||||
if 'Toe_Roll' in heel_bone and self.tail_bone_list:
|
||||
toe_matrix = all_matrices[len(ik_bones)]
|
||||
toe_bone = obj.pose.bones[self.tail_bone_list[0]]
|
||||
|
||||
# Compute relative rotation of heel determined by toe
|
||||
heel_rot_toe = convert_pose_matrix_via_rest_delta(toe_matrix, toe_bone, heel_bone)
|
||||
toe_rot = (heel_rest.inverted() @ heel_rot_toe).to_euler(rot_mode)
|
||||
|
||||
# Determine how much of the already computed heel rotation seems to be applied
|
||||
heel_rot = list(heel_bone.rotation_euler)
|
||||
heel_rot[roll] = max(0.0, heel_rot[roll])
|
||||
|
||||
# This relies on toe roll interpolation being done in Euler space
|
||||
ratios = [
|
||||
toe_rot[i] / heel_rot[i] for i in (roll, turn)
|
||||
if use_roll[i] and heel_rot[i] * toe_rot[i] > 0
|
||||
]
|
||||
|
||||
val = min(1.0, max(0.0, min(ratios) if ratios else 0.0))
|
||||
if val < 1e-5:
|
||||
val = 0.0
|
||||
|
||||
set_custom_property_value(
|
||||
obj, heel_bone.name, 'Toe_Roll', val, keyflags=self.keyflags)
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Use:")
|
||||
row.prop(self, 'use_roll', index=0, text="Rock", toggle=True)
|
||||
row.prop(self, 'use_roll', index=1, text="Roll", toggle=True)
|
||||
row.prop(self, 'use_roll', index=2, text="Yaw", toggle=True)
|
||||
|
||||
class POSE_OT_rigify_leg_roll_ik2fk(
|
||||
RigifyLegRollIk2FkBase, RigifySingleUpdateMixin, bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
bl_idname = "pose.rigify_leg_roll_ik2fk_" + rig_id
|
||||
bl_label = "Snap IK->FK With Roll"
|
||||
bl_description = "Snap the IK chain to FK result, using foot roll to preserve the current IK "\
|
||||
"control orientation as much as possible"
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.init_invoke(context)
|
||||
return self.execute(context)
|
||||
|
||||
class POSE_OT_rigify_leg_roll_ik2fk_bake(
|
||||
RigifyLegRollIk2FkBase, RigifyBakeKeyframesMixin, bpy.types.Operator):
|
||||
bl_idname = "pose.rigify_leg_roll_ik2fk_bake_" + rig_id
|
||||
bl_label = "Apply Snap IK->FK To Keyframes"
|
||||
bl_description = "Snap the IK chain keyframes to FK result, using foot roll to preserve the "\
|
||||
"current IK control orientation as much as possible"
|
||||
|
||||
def execute_scan_curves(self, context, obj):
|
||||
self.bake_add_bone_frames(self.fk_bone_list, TRANSFORM_PROPS_ALL)
|
||||
self.bake_add_bone_frames(self.ctrl_bone_list[-1:], TRANSFORM_PROPS_ROTATION)
|
||||
return self.bake_get_all_bone_curves(
|
||||
self.ctrl_bone_list + self.extra_ctrl_list, TRANSFORM_PROPS_ALL)
|
||||
''']
|
||||
|
||||
|
||||
def add_leg_snap_ik_to_fk(panel: PanelLayout, *, master: Optional[str] = None,
|
||||
fk_bones=(), ik_bones=(), tail_bones=(),
|
||||
ik_ctrl_bones=(), ik_extra_ctrls=(), heel_control, rig_name=''):
|
||||
panel.use_bake_settings()
|
||||
panel.script.add_utilities(SCRIPT_UTILITIES_OP_LEG_SNAP_IK_FK)
|
||||
panel.script.register_classes(SCRIPT_REGISTER_OP_LEG_SNAP_IK_FK)
|
||||
|
||||
assert len(fk_bones) == len(ik_bones) + len(tail_bones)
|
||||
|
||||
op_props = {
|
||||
'prop_bone': master,
|
||||
'fk_bones': json.dumps(fk_bones),
|
||||
'ik_bones': json.dumps(ik_bones),
|
||||
'ctrl_bones': json.dumps(ik_ctrl_bones),
|
||||
'tail_bones': json.dumps(tail_bones),
|
||||
'extra_ctrls': json.dumps(ik_extra_ctrls),
|
||||
'heel_control': heel_control,
|
||||
}
|
||||
|
||||
add_fk_ik_snap_buttons(
|
||||
panel, 'pose.rigify_leg_roll_ik2fk_{rig_id}', 'pose.rigify_leg_roll_ik2fk_bake_{rig_id}',
|
||||
label='IK->FK With Roll', rig_name=rig_name, properties=op_props,
|
||||
)
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
# generated by rigify.utils.write_metarig
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
@ -622,7 +622,8 @@ class BaseLimbRig(BaseRig):
|
||||
self.make_property(self.prop_bone, 'IK_Stretch', default=1.0, description='IK Stretch')
|
||||
panel.custom_prop(self.prop_bone, 'IK_Stretch', text='IK Stretch', slider=True)
|
||||
|
||||
self.make_property(self.prop_bone, 'pole_vector', default=False, description='Use a pole target control')
|
||||
self.make_property(self.prop_bone, 'pole_vector', default=0, min=0, max=1,
|
||||
description='Use a pole target control')
|
||||
|
||||
self.add_ik_only_buttons(panel, rig_name)
|
||||
|
||||
|
10
rigify/ui.py
10
rigify/ui.py
@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Callable, Any
|
||||
from mathutils import Color
|
||||
|
||||
from .utils.errors import MetarigError
|
||||
from .utils.layers import ROOT_COLLECTION, validate_collection_references
|
||||
from .utils.layers import ROOT_COLLECTION, SPECIAL_COLLECTIONS, validate_collection_references
|
||||
from .utils.rig import write_metarig, get_rigify_type, get_rigify_target_rig, \
|
||||
get_rigify_colors, get_rigify_params
|
||||
from .utils.widgets import write_widget
|
||||
@ -1017,6 +1017,14 @@ class Generate(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
metarig = verify_armature_obj(context.object)
|
||||
|
||||
for bcoll in metarig.data.collections:
|
||||
if bcoll.rigify_ui_row > 0 and bcoll.name not in SPECIAL_COLLECTIONS:
|
||||
break
|
||||
else:
|
||||
self.report({'ERROR'}, 'No bone collections have UI buttons assigned - all bones would be invisible.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
try:
|
||||
generate.generate_rig(context, metarig)
|
||||
except MetarigError as rig_exception:
|
||||
|
@ -29,6 +29,8 @@ DEF_COLLECTION = "DEF"
|
||||
ORG_COLLECTION = "ORG"
|
||||
MCH_COLLECTION = "MCH"
|
||||
|
||||
SPECIAL_COLLECTIONS = (ROOT_COLLECTION, DEF_COLLECTION, MCH_COLLECTION, ORG_COLLECTION)
|
||||
|
||||
REFS_TOGGLE_SUFFIX = '_layers_extra'
|
||||
REFS_LIST_SUFFIX = "_coll_refs"
|
||||
|
||||
|
206
rigify/utils/objects.py
Normal file
206
rigify/utils/objects.py
Normal file
@ -0,0 +1,206 @@
|
||||
# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from bpy.types import LayerCollection, Collection, Object
|
||||
|
||||
from .misc import ArmatureObject
|
||||
from .naming import strip_org
|
||||
|
||||
from mathutils import Matrix
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..generate import Generator
|
||||
from ..base_rig import BaseRig
|
||||
|
||||
|
||||
# noinspection SpellCheckingInspection
|
||||
def create_object_data(obj_type, name):
|
||||
if obj_type == 'EMPTY':
|
||||
return None
|
||||
if obj_type == 'MESH':
|
||||
return bpy.data.meshes.new(name)
|
||||
if obj_type in ('CURVE', 'SURFACE', 'FONT'):
|
||||
return bpy.data.curves.new(name, obj_type)
|
||||
if obj_type == 'META':
|
||||
return bpy.data.metaballs.new(name)
|
||||
if obj_type == 'CURVES':
|
||||
return bpy.data.hair_curves.new(name)
|
||||
if obj_type == 'POINTCLOUD':
|
||||
return bpy.data.pointclouds.new(name)
|
||||
if obj_type == 'VOLUME':
|
||||
return bpy.data.volumes.new(name)
|
||||
if obj_type == 'GREASEPENCIL':
|
||||
return bpy.data.grease_pencils.new(name)
|
||||
if obj_type == 'ARMATURE':
|
||||
return bpy.data.armatures.new(name)
|
||||
if obj_type == 'LATTICE':
|
||||
return bpy.data.lattices.new(name)
|
||||
raise ValueError(f"Invalid object type {obj_type}")
|
||||
|
||||
|
||||
class ArtifactManager:
|
||||
generator: 'Generator'
|
||||
|
||||
collection: Collection | None
|
||||
layer_collection: LayerCollection | None
|
||||
|
||||
used_artifacts: list[Object]
|
||||
temp_artifacts: list[Object]
|
||||
|
||||
artifact_reuse_table: dict[tuple[str, ...], Object]
|
||||
|
||||
def __init__(self, generator: 'Generator'):
|
||||
self.generator = generator
|
||||
self.collection = None
|
||||
self.layer_collection = None
|
||||
self.used_artifacts = []
|
||||
self.temp_artifacts = []
|
||||
self.artifact_reuse_table = {}
|
||||
|
||||
def _make_name(self, owner: 'BaseRig', name: str):
|
||||
return self.generator.obj.name + ":" + strip_org(owner.base_bone) + ":" + name
|
||||
|
||||
def create_new(self, owner: 'BaseRig', obj_type: str, name: str):
|
||||
"""
|
||||
Creates an artifact object of the specified type and name. If it already exists, all
|
||||
references are updated to point to the new instance, and the existing one is deleted.
|
||||
|
||||
Parameters:
|
||||
owner: rig component that requests the object.
|
||||
obj_type: type of the object to create.
|
||||
name: unique name of the object within the rig component.
|
||||
Returns:
|
||||
Object that was created.
|
||||
"""
|
||||
return self.find_or_create(owner, obj_type, name, recreate=True)[1]
|
||||
|
||||
def find_or_create(self, owner: 'BaseRig', obj_type: str, name: str, *, recreate=False):
|
||||
"""
|
||||
Creates or reuses an artifact object of the specified type.
|
||||
|
||||
Parameters:
|
||||
owner: rig component that requests the object.
|
||||
obj_type: type of the object to create.
|
||||
name: unique name of the object within the rig component.
|
||||
recreate: instructs that the object should be re-created from scratch even if it exists.
|
||||
Returns:
|
||||
(bool, Object) tuple, with the boolean specifying if the object already existed.
|
||||
"""
|
||||
|
||||
obj_name = self._make_name(owner, name)
|
||||
key = (owner.base_bone, name)
|
||||
|
||||
obj = self.artifact_reuse_table.get(key)
|
||||
|
||||
# If the existing object has incorrect type, delete it
|
||||
if obj and obj.type != obj_type:
|
||||
if obj in self.used_artifacts:
|
||||
owner.raise_error(f"duplicate reuse of artifact object {obj.name}")
|
||||
|
||||
print(f"RIGIFY: incompatible artifact object {obj.name} type: {obj.type} instead of {obj_type}")
|
||||
del self.artifact_reuse_table[key]
|
||||
bpy.data.objects.remove(obj)
|
||||
obj = None
|
||||
|
||||
# Reuse the existing object
|
||||
if obj:
|
||||
if obj in self.used_artifacts:
|
||||
owner.raise_error(f"duplicate reuse of artifact object {obj.name}")
|
||||
|
||||
if recreate:
|
||||
# Forcefully re-create and replace the existing object
|
||||
obj.name += '-OLD'
|
||||
if data := obj.data:
|
||||
data.name += '-OLD'
|
||||
|
||||
new_obj = bpy.data.objects.new(obj_name, create_object_data(obj_type, obj_name))
|
||||
|
||||
obj.user_remap(new_obj)
|
||||
self.artifact_reuse_table[key] = new_obj
|
||||
bpy.data.objects.remove(obj)
|
||||
obj = new_obj
|
||||
|
||||
# Ensure the existing object is visible
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False, view_layer=self.generator.view_layer)
|
||||
|
||||
if not obj.visible_get(view_layer=self.generator.view_layer):
|
||||
owner.raise_error(f"could not un-hide existing artifact object {obj.name}")
|
||||
|
||||
# Try renaming the existing object
|
||||
obj.name = obj_name
|
||||
if data := obj.data:
|
||||
data.name = obj_name
|
||||
|
||||
found = True
|
||||
|
||||
# Create an object from scratch
|
||||
else:
|
||||
obj = bpy.data.objects.new(obj_name, create_object_data(obj_type, obj_name))
|
||||
|
||||
self.generator.collection.objects.link(obj)
|
||||
self.artifact_reuse_table[key] = obj
|
||||
|
||||
found = False
|
||||
|
||||
self.used_artifacts.append(obj)
|
||||
|
||||
obj.rigify_owner_rig = self.generator.obj
|
||||
obj["rigify_artifact_id"] = key
|
||||
|
||||
obj.parent = self.generator.obj
|
||||
obj.parent_type = 'OBJECT'
|
||||
obj.matrix_parent_inverse = Matrix.Identity(4)
|
||||
obj.matrix_basis = Matrix.Identity(4)
|
||||
|
||||
return found, obj
|
||||
|
||||
def new_temporary(self, owner: 'BaseRig', obj_type: str, name="temp"):
|
||||
"""
|
||||
Creates a new temporary object of the specified type.
|
||||
The object will be removed after generation finishes.
|
||||
"""
|
||||
obj_name = "TEMP:" + self._make_name(owner, name)
|
||||
obj = bpy.data.objects.new(obj_name, create_object_data(obj_type, obj_name))
|
||||
obj.rigify_owner_rig = self.generator.obj
|
||||
obj["rigify_artifact_id"] = 'temporary'
|
||||
self.generator.collection.objects.link(obj)
|
||||
self.temp_artifacts.append(obj)
|
||||
return obj
|
||||
|
||||
def remove_temporary(self, obj):
|
||||
"""
|
||||
Immediately removes a temporary object previously created using new_temporary.
|
||||
"""
|
||||
self.temp_artifacts.remove(obj)
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
def generate_init_existing(self, armature: ArmatureObject):
|
||||
for obj in bpy.data.objects:
|
||||
if obj.rigify_owner_rig != armature:
|
||||
continue
|
||||
|
||||
aid = obj["rigify_artifact_id"]
|
||||
if isinstance(aid, list) and all(isinstance(x, str) for x in aid):
|
||||
self.artifact_reuse_table[tuple(aid)] = obj
|
||||
else:
|
||||
print(f"RIGIFY: removing orphan artifact {obj.name}")
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
def generate_cleanup(self):
|
||||
for obj in self.temp_artifacts:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
self.temp_artifacts = []
|
||||
|
||||
for key, obj in self.artifact_reuse_table.items():
|
||||
if obj in self.used_artifacts:
|
||||
obj.hide_viewport = True
|
||||
obj.hide_render = True
|
||||
else:
|
||||
del self.artifact_reuse_table[key]
|
||||
bpy.data.objects.remove(obj)
|
@ -267,8 +267,15 @@ def upgrade_metarig_layers(metarig: ArmatureObject):
|
||||
default_layers = [i == 1 for i in range(32)]
|
||||
default_map = {
|
||||
'faces.super_face': ['primary', 'secondary'],
|
||||
'limbs.arm': ['fk', 'tweak'],
|
||||
'limbs.front_paw': ['fk', 'tweak'],
|
||||
'limbs.leg': ['fk', 'tweak'],
|
||||
'limbs.paw': ['fk', 'tweak'],
|
||||
'limbs.rear_paw': ['fk', 'tweak'],
|
||||
'limbs.simple_tentacle': ['tweak'],
|
||||
'limbs.super_finger': ['tweak'],
|
||||
'limbs.super_limb': ['fk', 'tweak'],
|
||||
'spines.basic_spine': ['fk', 'tweak'],
|
||||
}
|
||||
|
||||
for pose_bone in metarig.pose.bones:
|
||||
|
@ -5,7 +5,7 @@
|
||||
bl_info = {
|
||||
"name": "Manage UI translations",
|
||||
"author": "Bastien Montagne",
|
||||
"version": (1, 3, 4),
|
||||
"version": (2, 0, 0),
|
||||
"blender": (4, 0, 0),
|
||||
"location": "Main \"File\" menu, text editor, any UI control",
|
||||
"description": "Allows managing UI translations directly from Blender "
|
||||
@ -17,32 +17,32 @@ bl_info = {
|
||||
}
|
||||
|
||||
|
||||
from . import (
|
||||
settings,
|
||||
edit_translation,
|
||||
update_repo,
|
||||
update_addon,
|
||||
update_ui,
|
||||
)
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
importlib.reload(settings)
|
||||
importlib.reload(edit_translation)
|
||||
importlib.reload(update_svn)
|
||||
importlib.reload(update_repo)
|
||||
importlib.reload(update_addon)
|
||||
importlib.reload(update_ui)
|
||||
else:
|
||||
import bpy
|
||||
from . import (
|
||||
settings,
|
||||
edit_translation,
|
||||
update_svn,
|
||||
update_addon,
|
||||
update_ui,
|
||||
)
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
classes = settings.classes + edit_translation.classes + update_svn.classes + update_addon.classes + update_ui.classes
|
||||
classes = settings.classes + edit_translation.classes + update_repo.classes + update_addon.classes + update_ui.classes
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.WindowManager.i18n_update_svn_settings = \
|
||||
bpy.types.WindowManager.i18n_update_settings = \
|
||||
bpy.props.PointerProperty(type=update_ui.I18nUpdateTranslationSettings)
|
||||
|
||||
# Init addon's preferences (unfortunately, as we are using an external storage for the properties,
|
||||
@ -58,4 +58,4 @@ def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
del bpy.types.WindowManager.i18n_update_svn_settings
|
||||
del bpy.types.WindowManager.i18n_update_settings
|
||||
|
@ -110,19 +110,10 @@ class UI_AP_i18n_settings(AddonPreferences):
|
||||
set=lambda self, val: _setattr(self._settings, "WARN_MSGID_NOT_CAPITALIZED", val),
|
||||
)
|
||||
|
||||
GETTEXT_MSGFMT_EXECUTABLE: StringProperty(
|
||||
name="Gettext 'msgfmt' executable",
|
||||
description="The gettext msgfmt 'compiler'. You’ll likely have to edit it if you’re under Windows",
|
||||
subtype='FILE_PATH',
|
||||
default="msgfmt",
|
||||
get=lambda self: self._settings.GETTEXT_MSGFMT_EXECUTABLE,
|
||||
set=lambda self, val: setattr(self._settings, "GETTEXT_MSGFMT_EXECUTABLE", val),
|
||||
)
|
||||
|
||||
FRIBIDI_LIB: StringProperty(
|
||||
name="Fribidi Library",
|
||||
description="The FriBidi C compiled library (.so under Linux, .dll under windows...), you’ll likely have "
|
||||
"to edit it if you’re under Windows, e.g. using the one included in svn's libraries repository",
|
||||
"to edit it if you’re under Windows, e.g. using the one included in Blender libraries repository",
|
||||
subtype='FILE_PATH',
|
||||
default="libfribidi.so.0",
|
||||
get=lambda self: self._settings.FRIBIDI_LIB,
|
||||
@ -178,7 +169,6 @@ class UI_AP_i18n_settings(AddonPreferences):
|
||||
layout.label(text="WARNING: preferences are lost when add-on is disabled, be sure to use \"Save Persistent\" "
|
||||
"if you want to keep your settings!")
|
||||
layout.prop(self, "WARN_MSGID_NOT_CAPITALIZED")
|
||||
layout.prop(self, "GETTEXT_MSGFMT_EXECUTABLE")
|
||||
layout.prop(self, "FRIBIDI_LIB")
|
||||
layout.prop(self, "SOURCE_DIR")
|
||||
layout.prop(self, "I18N_DIR")
|
||||
|
@ -132,7 +132,7 @@ class UI_OT_i18n_addon_translation_update(Operator):
|
||||
_cached_enum_addons[:] = []
|
||||
if not hasattr(self, "settings"):
|
||||
self.settings = settings.settings
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
|
||||
module_name, mod = validate_module(self, context)
|
||||
|
||||
@ -220,7 +220,7 @@ class UI_OT_i18n_addon_translation_import(Operator):
|
||||
_cached_enum_addons[:] = []
|
||||
if not hasattr(self, "settings"):
|
||||
self.settings = settings.settings
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
|
||||
module_name, mod = validate_module(self, context)
|
||||
if not (module_name and mod):
|
||||
@ -323,7 +323,7 @@ class UI_OT_i18n_addon_translation_export(Operator):
|
||||
_cached_enum_addons[:] = []
|
||||
if not hasattr(self, "settings"):
|
||||
self.settings = settings.settings
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
|
||||
module_name, mod = validate_module(self, context)
|
||||
if not (module_name and mod):
|
||||
|
@ -28,7 +28,7 @@ import tempfile
|
||||
|
||||
# Operators ###################################################################
|
||||
|
||||
def i18n_updatetranslation_svn_branches_callback(pot, lng, settings):
|
||||
def i18n_updatetranslation_work_repo_callback(pot, lng, settings):
|
||||
if not lng['use']:
|
||||
return
|
||||
if os.path.isfile(lng['po_path']):
|
||||
@ -40,10 +40,10 @@ def i18n_updatetranslation_svn_branches_callback(pot, lng, settings):
|
||||
print("{} PO written!".format(lng['uid']))
|
||||
|
||||
|
||||
class UI_OT_i18n_updatetranslation_svn_branches(Operator):
|
||||
"""Update i18n svn's branches (po files)"""
|
||||
bl_idname = "ui.i18n_updatetranslation_svn_branches"
|
||||
bl_label = "Update I18n Branches"
|
||||
class UI_OT_i18n_updatetranslation_work_repo(Operator):
|
||||
"""Update i18n working repository (po files)"""
|
||||
bl_idname = "ui.i18n_updatetranslation_work_repo"
|
||||
bl_label = "Update I18n Work Repo"
|
||||
|
||||
use_skip_pot_gen: BoolProperty(
|
||||
name="Skip POT",
|
||||
@ -54,7 +54,7 @@ class UI_OT_i18n_updatetranslation_svn_branches(Operator):
|
||||
def execute(self, context):
|
||||
if not hasattr(self, "settings"):
|
||||
self.settings = settings.settings
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
self.settings.FILE_NAME_POT = i18n_sett.pot_path
|
||||
|
||||
context.window_manager.progress_begin(0, len(i18n_sett.langs) + 1)
|
||||
@ -88,7 +88,7 @@ class UI_OT_i18n_updatetranslation_svn_branches(Operator):
|
||||
with concurrent.futures.ProcessPoolExecutor() as exctr:
|
||||
pot = utils_i18n.I18nMessages(kind='PO', src=self.settings.FILE_NAME_POT, settings=self.settings)
|
||||
num_langs = len(i18n_sett.langs)
|
||||
for progress, _ in enumerate(exctr.map(i18n_updatetranslation_svn_branches_callback,
|
||||
for progress, _ in enumerate(exctr.map(i18n_updatetranslation_work_repo_callback,
|
||||
(pot,) * num_langs,
|
||||
[dict(lng.items()) for lng in i18n_sett.langs],
|
||||
(self.settings,) * num_langs,
|
||||
@ -102,7 +102,7 @@ class UI_OT_i18n_updatetranslation_svn_branches(Operator):
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
def i18n_cleanuptranslation_svn_branches_callback(lng, settings):
|
||||
def i18n_cleanuptranslation_work_repo_callback(lng, settings):
|
||||
if not lng['use']:
|
||||
print("Skipping {} language ({}).".format(lng['name'], lng['uid']))
|
||||
return
|
||||
@ -115,15 +115,15 @@ def i18n_cleanuptranslation_svn_branches_callback(lng, settings):
|
||||
("Errors in this po, solved as best as possible!\n\t" + "\n\t".join(errs) if errs else "") + "\n")
|
||||
|
||||
|
||||
class UI_OT_i18n_cleanuptranslation_svn_branches(Operator):
|
||||
"""Clean up i18n svn's branches (po files)"""
|
||||
bl_idname = "ui.i18n_cleanuptranslation_svn_branches"
|
||||
bl_label = "Clean up I18n Branches"
|
||||
class UI_OT_i18n_cleanuptranslation_work_repo(Operator):
|
||||
"""Clean up i18n working repository (po files)"""
|
||||
bl_idname = "ui.i18n_cleanuptranslation_work_repo"
|
||||
bl_label = "Clean up I18n Work Repo"
|
||||
|
||||
def execute(self, context):
|
||||
if not hasattr(self, "settings"):
|
||||
self.settings = settings.settings
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
# 'DEFAULT' and en_US are always valid, fully-translated "languages"!
|
||||
stats = {"DEFAULT": 1.0, "en_US": 1.0}
|
||||
|
||||
@ -131,7 +131,7 @@ class UI_OT_i18n_cleanuptranslation_svn_branches(Operator):
|
||||
context.window_manager.progress_update(0)
|
||||
with concurrent.futures.ProcessPoolExecutor() as exctr:
|
||||
num_langs = len(i18n_sett.langs)
|
||||
for progress, _ in enumerate(exctr.map(i18n_cleanuptranslation_svn_branches_callback,
|
||||
for progress, _ in enumerate(exctr.map(i18n_cleanuptranslation_work_repo_callback,
|
||||
[dict(lng.items()) for lng in i18n_sett.langs],
|
||||
(self.settings,) * num_langs,
|
||||
chunksize=4)):
|
||||
@ -142,7 +142,7 @@ class UI_OT_i18n_cleanuptranslation_svn_branches(Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def i18n_updatetranslation_svn_trunk_callback(lng, settings):
|
||||
def i18n_updatetranslation_blender_repo_callback(lng, settings):
|
||||
reports = []
|
||||
if lng['uid'] in settings.IMPORT_LANGUAGES_SKIP:
|
||||
reports.append("Skipping {} language ({}), edit settings if you want to enable it.".format(lng['name'], lng['uid']))
|
||||
@ -156,32 +156,21 @@ def i18n_updatetranslation_svn_trunk_callback(lng, settings):
|
||||
"Cleaned up {} commented messages.\n".format(lng['name'], lng['uid'], po.clean_commented()) +
|
||||
("Errors in this po, solved as best as possible!\n\t" + "\n\t".join(errs) if errs else ""))
|
||||
if lng['uid'] in settings.IMPORT_LANGUAGES_RTL:
|
||||
po.write(kind="PO", dest=lng['po_path_trunk'][:-3] + "_raw.po")
|
||||
po.rtl_process()
|
||||
po.write(kind="PO", dest=lng['po_path_trunk'])
|
||||
po.write(kind="PO_COMPACT", dest=lng['po_path_git'])
|
||||
ret = po.write(kind="MO", dest=lng['mo_path_trunk'])
|
||||
if (ret.stdout):
|
||||
reports.append(ret.stdout.decode().rstrip("\n"))
|
||||
if (ret.stderr):
|
||||
stderr_str = ret.stderr.decode().rstrip("\n")
|
||||
if ret.returncode != 0:
|
||||
reports.append("ERROR: " + stderr_str)
|
||||
else:
|
||||
reports.append(stderr_str)
|
||||
po.write(kind="PO_COMPACT", dest=lng['po_path_blender'])
|
||||
po.update_info()
|
||||
return lng['uid'], po.nbr_trans_msgs / po.nbr_msgs, reports
|
||||
|
||||
|
||||
class UI_OT_i18n_updatetranslation_svn_trunk(Operator):
|
||||
"""Update i18n svn's branches (po files)"""
|
||||
bl_idname = "ui.i18n_updatetranslation_svn_trunk"
|
||||
bl_label = "Update I18n Trunk"
|
||||
class UI_OT_i18n_updatetranslation_blender_repo(Operator):
|
||||
"""Update i18n data (po files) in Blneder source code repository"""
|
||||
bl_idname = "ui.i18n_updatetranslation_blender_repo"
|
||||
bl_label = "Update I18n Blender Repo"
|
||||
|
||||
def execute(self, context):
|
||||
if not hasattr(self, "settings"):
|
||||
self.settings = settings.settings
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
# 'DEFAULT' and en_US are always valid, fully-translated "languages"!
|
||||
stats = {"DEFAULT": 1.0, "en_US": 1.0}
|
||||
|
||||
@ -189,7 +178,7 @@ class UI_OT_i18n_updatetranslation_svn_trunk(Operator):
|
||||
context.window_manager.progress_update(0)
|
||||
with concurrent.futures.ProcessPoolExecutor() as exctr:
|
||||
num_langs = len(i18n_sett.langs)
|
||||
for progress, (lng_uid, stats_val, reports) in enumerate(exctr.map(i18n_updatetranslation_svn_trunk_callback,
|
||||
for progress, (lng_uid, stats_val, reports) in enumerate(exctr.map(i18n_updatetranslation_blender_repo_callback,
|
||||
[dict(lng.items()) for lng in i18n_sett.langs],
|
||||
(self.settings,) * num_langs,
|
||||
chunksize=4)):
|
||||
@ -197,61 +186,31 @@ class UI_OT_i18n_updatetranslation_svn_trunk(Operator):
|
||||
stats[lng_uid] = stats_val
|
||||
print("".join(reports) + "\n")
|
||||
|
||||
# Copy pot file from branches to trunk.
|
||||
shutil.copy2(self.settings.FILE_NAME_POT, self.settings.TRUNK_PO_DIR)
|
||||
|
||||
print("Generating languages' menu...")
|
||||
context.window_manager.progress_update(progress + 2)
|
||||
# First complete our statistics by checking po files we did not touch this time!
|
||||
po_to_uid = {os.path.basename(lng.po_path): lng.uid for lng in i18n_sett.langs}
|
||||
for po_path in os.listdir(self.settings.TRUNK_PO_DIR):
|
||||
uid = po_to_uid.get(po_path, None)
|
||||
po_path = os.path.join(self.settings.TRUNK_PO_DIR, po_path)
|
||||
if uid and uid not in stats:
|
||||
po = utils_i18n.I18nMessages(uid=uid, kind='PO', src=po_path, settings=self.settings)
|
||||
stats[uid] = po.nbr_trans_msgs / po.nbr_msgs if po.nbr_msgs > 0 else 0
|
||||
languages_menu_lines = utils_languages_menu.gen_menu_file(stats, self.settings)
|
||||
with open(os.path.join(self.settings.TRUNK_MO_DIR, self.settings.LANGUAGES_FILE), 'w', encoding="utf8") as f:
|
||||
f.write("\n".join(languages_menu_lines))
|
||||
with open(os.path.join(self.settings.GIT_I18N_ROOT, self.settings.LANGUAGES_FILE), 'w', encoding="utf8") as f:
|
||||
with open(os.path.join(self.settings.BLENDER_I18N_ROOT, self.settings.LANGUAGES_FILE), 'w', encoding="utf8") as f:
|
||||
f.write("\n".join(languages_menu_lines))
|
||||
context.window_manager.progress_end()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class UI_OT_i18n_updatetranslation_svn_statistics(Operator):
|
||||
class UI_OT_i18n_updatetranslation_statistics(Operator):
|
||||
"""Create or extend a 'i18n_info.txt' Text datablock"""
|
||||
"""(it will contain statistics and checks about current branches and/or trunk)"""
|
||||
bl_idname = "ui.i18n_updatetranslation_svn_statistics"
|
||||
"""(it will contain statistics and checks about current working repository PO files)"""
|
||||
bl_idname = "ui.i18n_updatetranslation_statistics"
|
||||
bl_label = "Update I18n Statistics"
|
||||
|
||||
use_branches: BoolProperty(
|
||||
name="Check Branches",
|
||||
description="Check po files in branches",
|
||||
default=True,
|
||||
)
|
||||
|
||||
use_trunk: BoolProperty(
|
||||
name="Check Trunk",
|
||||
description="Check po files in trunk",
|
||||
default=False,
|
||||
)
|
||||
|
||||
report_name = "i18n_info.txt"
|
||||
|
||||
def execute(self, context):
|
||||
if not hasattr(self, "settings"):
|
||||
self.settings = settings.settings
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
|
||||
buff = io.StringIO()
|
||||
lst = []
|
||||
if self.use_branches:
|
||||
lst += [(lng, lng.po_path) for lng in i18n_sett.langs]
|
||||
if self.use_trunk:
|
||||
lst += [(lng, lng.po_path_trunk) for lng in i18n_sett.langs
|
||||
if lng.uid not in self.settings.IMPORT_LANGUAGES_SKIP]
|
||||
lst = [(lng, lng.po_path) for lng in i18n_sett.langs]
|
||||
|
||||
context.window_manager.progress_begin(0, len(lst))
|
||||
context.window_manager.progress_update(0)
|
||||
@ -278,7 +237,7 @@ class UI_OT_i18n_updatetranslation_svn_statistics(Operator):
|
||||
data = text.as_string()
|
||||
data = data + "\n" + buff.getvalue()
|
||||
text.from_string(data)
|
||||
self.report({'INFO'}, "Info written to {} text datablock!".format(self.report_name))
|
||||
self.report({'INFO'}, "Info written to %s text datablock!" % self.report_name)
|
||||
context.window_manager.progress_end()
|
||||
|
||||
return {'FINISHED'}
|
||||
@ -289,8 +248,8 @@ class UI_OT_i18n_updatetranslation_svn_statistics(Operator):
|
||||
|
||||
|
||||
classes = (
|
||||
UI_OT_i18n_updatetranslation_svn_branches,
|
||||
UI_OT_i18n_cleanuptranslation_svn_branches,
|
||||
UI_OT_i18n_updatetranslation_svn_trunk,
|
||||
UI_OT_i18n_updatetranslation_svn_statistics,
|
||||
UI_OT_i18n_updatetranslation_work_repo,
|
||||
UI_OT_i18n_cleanuptranslation_work_repo,
|
||||
UI_OT_i18n_updatetranslation_blender_repo,
|
||||
UI_OT_i18n_updatetranslation_statistics,
|
||||
)
|
@ -58,29 +58,15 @@ class I18nUpdateTranslationLanguage(PropertyGroup):
|
||||
)
|
||||
|
||||
po_path: StringProperty(
|
||||
name="PO File Path",
|
||||
description="Path to the relevant po file in branches",
|
||||
name="PO Work File Path",
|
||||
description="Path to the relevant po file in the work repository",
|
||||
subtype='FILE_PATH',
|
||||
default="",
|
||||
)
|
||||
|
||||
po_path_trunk: StringProperty(
|
||||
name="PO Trunk File Path",
|
||||
description="Path to the relevant po file in trunk",
|
||||
subtype='FILE_PATH',
|
||||
default="",
|
||||
)
|
||||
|
||||
mo_path_trunk: StringProperty(
|
||||
name="MO File Path",
|
||||
description="Path to the relevant mo file",
|
||||
subtype='FILE_PATH',
|
||||
default="",
|
||||
)
|
||||
|
||||
po_path_git: StringProperty(
|
||||
name="PO Git Master File Path",
|
||||
description="Path to the relevant po file in Blender's translations git repository",
|
||||
po_path_blender: StringProperty(
|
||||
name="PO Blender File Path",
|
||||
description="Path to the relevant po file in Blender's source repository",
|
||||
subtype='FILE_PATH',
|
||||
default="",
|
||||
)
|
||||
@ -92,7 +78,7 @@ class I18nUpdateTranslationSettings(PropertyGroup):
|
||||
langs: CollectionProperty(
|
||||
name="Languages",
|
||||
type=I18nUpdateTranslationLanguage,
|
||||
description="Languages to update in branches",
|
||||
description="Languages to update in work repository",
|
||||
)
|
||||
|
||||
active_lang: IntProperty(
|
||||
@ -140,34 +126,35 @@ class UI_PT_i18n_update_translations_settings(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
|
||||
if not i18n_sett.is_init and bpy.ops.ui.i18n_updatetranslation_svn_init_settings.poll():
|
||||
if not i18n_sett.is_init and bpy.ops.ui.i18n_updatetranslation_init_settings.poll():
|
||||
# Cannot call the operator from here, this code might run while `pyrna_write_check()` returns False
|
||||
# (which prevents any operator call from Python), during initialization of Blender.
|
||||
UI_OT_i18n_updatetranslation_svn_init_settings.execute_static(context, settings.settings)
|
||||
UI_OT_i18n_updatetranslation_init_settings.execute_static(context, settings.settings)
|
||||
|
||||
if not i18n_sett.is_init:
|
||||
layout.label(text="Could not init languages data!")
|
||||
layout.label(text="Please edit the preferences of the UI Translate add-on")
|
||||
layout.operator("ui.i18n_updatetranslation_svn_init_settings", text="Init Settings")
|
||||
layout.operator("ui.i18n_updatetranslation_init_settings", text="Init Settings")
|
||||
else:
|
||||
split = layout.split(factor=0.75)
|
||||
split.template_list("UI_UL_i18n_languages", "", i18n_sett, "langs", i18n_sett, "active_lang", rows=8)
|
||||
col = split.column()
|
||||
col.operator("ui.i18n_updatetranslation_svn_init_settings", text="Reset Settings")
|
||||
col.operator("ui.i18n_updatetranslation_init_settings", text="Reset Settings")
|
||||
deselect = any(l.use for l in i18n_sett.langs)
|
||||
op = col.operator("ui.i18n_updatetranslation_svn_settings_select",
|
||||
op = col.operator("ui.i18n_updatetranslation_settings_select",
|
||||
text="Deselect All" if deselect else "Select All")
|
||||
op.use_invert = False
|
||||
op.use_select = not deselect
|
||||
col.operator("ui.i18n_updatetranslation_svn_settings_select", text="Invert Selection").use_invert = True
|
||||
col.operator("ui.i18n_updatetranslation_settings_select", text="Invert Selection").use_invert = True
|
||||
col.separator()
|
||||
col.operator("ui.i18n_updatetranslation_svn_branches", text="Update Branches")
|
||||
col.operator("ui.i18n_updatetranslation_svn_trunk", text="Update Trunk")
|
||||
col.operator("ui.i18n_updatetranslation_work_repo", text="Update Work Repo")
|
||||
col.operator("ui.i18n_cleanuptranslation_work_repo", text="Clean up Work Repo")
|
||||
col.separator()
|
||||
col.operator("ui.i18n_cleanuptranslation_svn_branches", text="Clean up Branches")
|
||||
col.operator("ui.i18n_updatetranslation_svn_statistics", text="Statistics")
|
||||
col.operator("ui.i18n_updatetranslation_blender_repo", text="Update Blender Repo")
|
||||
col.separator()
|
||||
col.operator("ui.i18n_updatetranslation_statistics", text="Statistics")
|
||||
|
||||
if i18n_sett.active_lang >= 0 and i18n_sett.active_lang < len(i18n_sett.langs):
|
||||
lng = i18n_sett.langs[i18n_sett.active_lang]
|
||||
@ -177,9 +164,7 @@ class UI_PT_i18n_update_translations_settings(Panel):
|
||||
row.label(text="[{}]: \"{}\" ({})".format(lng.uid, iface_(lng.name), lng.num_id), translate=False)
|
||||
row.prop(lng, "use", text="")
|
||||
col.prop(lng, "po_path")
|
||||
col.prop(lng, "po_path_trunk")
|
||||
col.prop(lng, "mo_path_trunk")
|
||||
col.prop(lng, "po_path_git")
|
||||
col.prop(lng, "po_path_blender")
|
||||
layout.separator()
|
||||
layout.prop(i18n_sett, "pot_path")
|
||||
|
||||
@ -196,10 +181,10 @@ class UI_PT_i18n_update_translations_settings(Panel):
|
||||
|
||||
# Operators ###################################################################
|
||||
|
||||
class UI_OT_i18n_updatetranslation_svn_init_settings(Operator):
|
||||
"""Init settings for i18n svn's update operators"""
|
||||
class UI_OT_i18n_updatetranslation_init_settings(Operator):
|
||||
"""Init settings for i18n files update operators"""
|
||||
|
||||
bl_idname = "ui.i18n_updatetranslation_svn_init_settings"
|
||||
bl_idname = "ui.i18n_updatetranslation_init_settings"
|
||||
bl_label = "Init I18n Update Settings"
|
||||
bl_option = {'REGISTER'}
|
||||
|
||||
@ -209,28 +194,27 @@ class UI_OT_i18n_updatetranslation_svn_init_settings(Operator):
|
||||
|
||||
@staticmethod
|
||||
def execute_static(context, self_settings):
|
||||
i18n_sett = context.window_manager.i18n_update_svn_settings
|
||||
i18n_sett = context.window_manager.i18n_update_settings
|
||||
|
||||
# First, create the list of languages from settings.
|
||||
i18n_sett.langs.clear()
|
||||
root_br = self_settings.BRANCHES_DIR
|
||||
root_tr_po = self_settings.TRUNK_PO_DIR
|
||||
root_git_po = self_settings.GIT_I18N_PO_DIR
|
||||
root_tr_mo = os.path.join(self_settings.TRUNK_DIR, self_settings.MO_PATH_TEMPLATE, self_settings.MO_FILE_NAME)
|
||||
if not (os.path.isdir(root_br) and os.path.isdir(root_tr_po)):
|
||||
root_work = self_settings.WORK_DIR
|
||||
root_blender_po = self_settings.BLENDER_I18N_PO_DIR
|
||||
print(root_work)
|
||||
print(root_blender_po)
|
||||
print(self_settings.FILE_NAME_POT)
|
||||
if not (os.path.isdir(root_work) and os.path.isdir(root_blender_po)):
|
||||
i18n_sett.is_init = False
|
||||
return;
|
||||
for can_use, uid, num_id, name, isocode, po_path_branch in utils_i18n.list_po_dir(root_br, self_settings):
|
||||
for can_use, uid, num_id, name, isocode, po_path_work in utils_i18n.list_po_dir(root_work, self_settings):
|
||||
lng = i18n_sett.langs.add()
|
||||
lng.use = can_use
|
||||
lng.uid = uid
|
||||
lng.num_id = num_id
|
||||
lng.name = name
|
||||
if can_use:
|
||||
lng.po_path = po_path_branch
|
||||
lng.po_path_trunk = os.path.join(root_tr_po, isocode + ".po")
|
||||
lng.mo_path_trunk = root_tr_mo.format(isocode)
|
||||
lng.po_path_git = os.path.join(root_git_po, isocode + ".po")
|
||||
lng.po_path = po_path_work
|
||||
lng.po_path_blender = os.path.join(root_blender_po, isocode + ".po")
|
||||
|
||||
i18n_sett.pot_path = self_settings.FILE_NAME_POT
|
||||
i18n_sett.is_init = True
|
||||
@ -241,18 +225,17 @@ class UI_OT_i18n_updatetranslation_svn_init_settings(Operator):
|
||||
|
||||
self.execute_static(context, self.settings)
|
||||
|
||||
if context.window_manager.i18n_update_svn_settings.is_init is False:
|
||||
if context.window_manager.i18n_update_settings.is_init is False:
|
||||
return {'CANCELLED'}
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class UI_OT_i18n_updatetranslation_svn_settings_select(Operator):
|
||||
"""(De)select (or invert selection of) all languages for i18n svn's update operators"""
|
||||
class UI_OT_i18n_updatetranslation_settings_select(Operator):
|
||||
"""(De)select (or invert selection of) all languages for i18n files update operators"""
|
||||
|
||||
bl_idname = "ui.i18n_updatetranslation_svn_settings_select"
|
||||
bl_idname = "ui.i18n_updatetranslation_settings_select"
|
||||
bl_label = "Init I18n Update Select Languages"
|
||||
|
||||
# Operator Arguments
|
||||
use_select: BoolProperty(
|
||||
name="Select All",
|
||||
description="Select all if True, else deselect all",
|
||||
@ -264,7 +247,6 @@ class UI_OT_i18n_updatetranslation_svn_settings_select(Operator):
|
||||
description="Inverse selection (overrides 'Select All' when True)",
|
||||
default=False,
|
||||
)
|
||||
# /End Operator Arguments
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@ -272,10 +254,10 @@ class UI_OT_i18n_updatetranslation_svn_settings_select(Operator):
|
||||
|
||||
def execute(self, context):
|
||||
if self.use_invert:
|
||||
for lng in context.window_manager.i18n_update_svn_settings.langs:
|
||||
for lng in context.window_manager.i18n_update_settings.langs:
|
||||
lng.use = not lng.use
|
||||
else:
|
||||
for lng in context.window_manager.i18n_update_svn_settings.langs:
|
||||
for lng in context.window_manager.i18n_update_settings.langs:
|
||||
lng.use = self.use_select
|
||||
return {'FINISHED'}
|
||||
|
||||
@ -285,6 +267,6 @@ classes = (
|
||||
I18nUpdateTranslationSettings,
|
||||
UI_UL_i18n_languages,
|
||||
UI_PT_i18n_update_translations_settings,
|
||||
UI_OT_i18n_updatetranslation_svn_init_settings,
|
||||
UI_OT_i18n_updatetranslation_svn_settings_select,
|
||||
UI_OT_i18n_updatetranslation_init_settings,
|
||||
UI_OT_i18n_updatetranslation_settings_select,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user