FBX IO: Add support for armature data custom properties #104888
@ -5,7 +5,7 @@
|
||||
bl_info = {
|
||||
"name": "FBX format",
|
||||
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
|
||||
"version": (5, 7, 4),
|
||||
"version": (5, 7, 6),
|
||||
"blender": (3, 6, 0),
|
||||
"location": "File > Import-Export",
|
||||
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",
|
||||
|
@ -560,8 +560,12 @@ def fbx_data_empty_elements(root, empty, scene_data):
|
||||
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"))
|
||||
bdata = empty.bdata
|
||||
val = bdata.get('fbx_type', None)
|
||||
null.add_string(val.encode() if val and isinstance(val, str) else b"Null")
|
||||
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")
|
||||
|
||||
@ -1986,12 +1990,6 @@ def fbx_data_animation_elements(root, scene_data):
|
||||
animations = scene_data.animations
|
||||
if not animations:
|
||||
return
|
||||
scene = scene_data.scene
|
||||
|
||||
fps = scene.render.fps / scene.render.fps_base
|
||||
|
||||
def keys_to_ktimes(keys):
|
||||
return (int(v) for v in convert_sec_to_ktime_iter((f / fps for f, _v in keys)))
|
||||
|
||||
# Animation stacks.
|
||||
for astack_key, alayers, alayer_key, name, f_start, f_end in animations:
|
||||
@ -2031,18 +2029,18 @@ def fbx_data_animation_elements(root, scene_data):
|
||||
acn_tmpl = elem_props_template_init(scene_data.templates, b"AnimationCurveNode")
|
||||
acn_props = elem_properties(acurvenode)
|
||||
|
||||
for fbx_item, (acurve_key, def_value, keys, _acurve_valid) in acurves.items():
|
||||
for fbx_item, (acurve_key, def_value, (keys, values), _acurve_valid) in acurves.items():
|
||||
elem_props_template_set(acn_tmpl, acn_props, "p_number", fbx_item.encode(),
|
||||
def_value, animatable=True)
|
||||
|
||||
# Only create Animation curve if needed!
|
||||
if keys:
|
||||
nbr_keys = len(keys)
|
||||
if nbr_keys:
|
||||
acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbx_uuid_from_key(acurve_key))
|
||||
acurve.add_string(fbx_name_class(b"", b"AnimCurve"))
|
||||
acurve.add_string(b"")
|
||||
|
||||
# key attributes...
|
||||
nbr_keys = len(keys)
|
||||
# flags...
|
||||
keyattr_flags = (
|
||||
1 << 2 | # interpolation mode, 1 = constant, 2 = linear, 3 = cubic.
|
||||
@ -2057,8 +2055,8 @@ def fbx_data_animation_elements(root, scene_data):
|
||||
# And now, the *real* data!
|
||||
elem_data_single_float64(acurve, b"Default", def_value)
|
||||
elem_data_single_int32(acurve, b"KeyVer", FBX_ANIM_KEY_VERSION)
|
||||
elem_data_single_int64_array(acurve, b"KeyTime", keys_to_ktimes(keys))
|
||||
elem_data_single_float32_array(acurve, b"KeyValueFloat", (v for _f, v in keys))
|
||||
elem_data_single_int64_array(acurve, b"KeyTime", astype_view_signedness(keys, np.int64))
|
||||
elem_data_single_float32_array(acurve, b"KeyValueFloat", values.astype(np.float32, copy=False))
|
||||
elem_data_single_int32_array(acurve, b"KeyAttrFlags", keyattr_flags)
|
||||
elem_data_single_float32_array(acurve, b"KeyAttrDataFloat", keyattr_datafloat)
|
||||
elem_data_single_int32_array(acurve, b"KeyAttrRefCount", (nbr_keys,))
|
||||
@ -2259,75 +2257,128 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No
|
||||
dupli_parent_bdata = {dup.get_parent().bdata for dup in animdata_ob if dup.is_dupli}
|
||||
has_animated_duplis = bool(dupli_parent_bdata)
|
||||
|
||||
currframe = f_start
|
||||
while currframe <= f_end:
|
||||
real_currframe = currframe - f_start if start_zero else currframe
|
||||
scene.frame_set(int(currframe), subframe=currframe - int(currframe))
|
||||
# Initialize keyframe times array. Each AnimationCurveNodeWrapper will share the same instance.
|
||||
# `np.arange` excludes the `stop` argument like when using `range`, so we use np.nextafter to get the next
|
||||
# representable value after f_end and use that as the `stop` argument instead.
|
||||
currframes = np.arange(f_start, np.nextafter(f_end, np.inf), step=bake_step)
|
||||
|
||||
if has_animated_duplis:
|
||||
# Changing the scene's frame invalidates existing dupli instances. To get the updated matrices of duplis for
|
||||
# this frame, we must get the duplis from the depsgraph again.
|
||||
for dup in depsgraph.object_instances:
|
||||
if (parent := dup.parent) and parent.original in dupli_parent_bdata:
|
||||
# ObjectWrapper caches its instances. Attempting to create a new instance updates the existing
|
||||
# ObjectWrapper instance with the current frame's matrix and then returns the existing instance.
|
||||
ObjectWrapper(dup)
|
||||
for ob_obj, (anim_loc, anim_rot, anim_scale) in animdata_ob.items():
|
||||
# We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
|
||||
p_rot = p_rots.get(ob_obj, None)
|
||||
loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
|
||||
p_rots[ob_obj] = rot
|
||||
anim_loc.add_keyframe(real_currframe, loc)
|
||||
anim_rot.add_keyframe(real_currframe, tuple(convert_rad_to_deg_iter(rot)))
|
||||
anim_scale.add_keyframe(real_currframe, scale)
|
||||
for anim_shape, me, shape in animdata_shapes.values():
|
||||
anim_shape.add_keyframe(real_currframe, (shape.value * 100.0,))
|
||||
for anim_camera_lens, anim_camera_focus_distance, camera in animdata_cameras.values():
|
||||
anim_camera_lens.add_keyframe(real_currframe, (camera.lens,))
|
||||
anim_camera_focus_distance.add_keyframe(real_currframe, (camera.dof.focus_distance * 1000 * gscale,))
|
||||
currframe += bake_step
|
||||
# Convert from Blender time to FBX time.
|
||||
fps = scene.render.fps / scene.render.fps_base
|
||||
real_currframes = currframes - f_start if start_zero else currframes
|
||||
real_currframes = (real_currframes / fps * FBX_KTIME).astype(np.int64)
|
||||
|
||||
# Generator that yields the animated values of each frame in order.
|
||||
def frame_values_gen():
|
||||
# Precalculate integer frames and subframes.
|
||||
int_currframes = currframes.astype(int)
|
||||
subframes = currframes - int_currframes
|
||||
|
||||
# Create simpler iterables that return only the values we care about.
|
||||
animdata_shapes_only = [shape for _anim_shape, _me, shape in animdata_shapes.values()]
|
||||
animdata_cameras_only = [camera for _anim_camera_lens, _anim_camera_focus_distance, camera
|
||||
in animdata_cameras.values()]
|
||||
# Previous frame's rotation for each object in animdata_ob, this will be updated each frame.
|
||||
animdata_ob_p_rots = p_rots.values()
|
||||
|
||||
# Iterate through each frame and yield the values for that frame.
|
||||
# Iterating .data, the memoryview of an array, is faster than iterating the array directly.
|
||||
for int_currframe, subframe in zip(int_currframes.data, subframes.data):
|
||||
scene.frame_set(int_currframe, subframe=subframe)
|
||||
|
||||
if has_animated_duplis:
|
||||
# Changing the scene's frame invalidates existing dupli instances. To get the updated matrices of duplis
|
||||
# for this frame, we must get the duplis from the depsgraph again.
|
||||
for dup in depsgraph.object_instances:
|
||||
if (parent := dup.parent) and parent.original in dupli_parent_bdata:
|
||||
# ObjectWrapper caches its instances. Attempting to create a new instance updates the existing
|
||||
# ObjectWrapper instance with the current frame's matrix and then returns the existing instance.
|
||||
ObjectWrapper(dup)
|
||||
next_p_rots = []
|
||||
for ob_obj, p_rot in zip(animdata_ob, animdata_ob_p_rots):
|
||||
# We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
|
||||
loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
|
||||
next_p_rots.append(rot)
|
||||
yield from loc
|
||||
yield from rot
|
||||
yield from scale
|
||||
animdata_ob_p_rots = next_p_rots
|
||||
for shape in animdata_shapes_only:
|
||||
yield shape.value
|
||||
for camera in animdata_cameras_only:
|
||||
yield camera.lens
|
||||
yield camera.dof.focus_distance
|
||||
|
||||
# Providing `count` to np.fromiter pre-allocates the array, avoiding extra memory allocations while iterating.
|
||||
num_ob_values = len(animdata_ob) * 9 # Location, rotation and scale, each of which have x, y, and z components
|
||||
num_shape_values = len(animdata_shapes) # Only 1 value per shape key
|
||||
num_camera_values = len(animdata_cameras) * 2 # Focal length (`.lens`) and focus distance
|
||||
num_values_per_frame = num_ob_values + num_shape_values + num_camera_values
|
||||
num_frames = len(real_currframes)
|
||||
all_values_flat = np.fromiter(frame_values_gen(), dtype=float, count=num_frames * num_values_per_frame)
|
||||
|
||||
# Restore the scene's current frame.
|
||||
scene.frame_set(back_currframe, subframe=0.0)
|
||||
|
||||
# View such that each column is all values for a single frame and each row is all values for a single curve.
|
||||
all_values = all_values_flat.reshape(num_frames, num_values_per_frame).T
|
||||
# Split into views of the arrays for each curve type.
|
||||
split_at = [num_ob_values, num_shape_values, num_camera_values]
|
||||
# For unequal sized splits, np.split takes indices to split at, which can be acquired through a cumulative sum
|
||||
# across the list.
|
||||
# The last value isn't needed, because the last split is assumed to go to the end of the array.
|
||||
split_at = np.cumsum(split_at[:-1])
|
||||
all_ob_values, all_shape_key_values, all_camera_values = np.split(all_values, split_at)
|
||||
|
||||
all_anims = []
|
||||
|
||||
# Set location/rotation/scale curves.
|
||||
# Split into equal sized views of the arrays for each object.
|
||||
split_into = len(animdata_ob)
|
||||
per_ob_values = np.split(all_ob_values, split_into) if split_into > 0 else ()
|
||||
for anims, ob_values in zip(animdata_ob.values(), per_ob_values):
|
||||
# Split again into equal sized views of the location, rotation and scaling arrays.
|
||||
loc_xyz, rot_xyz, sca_xyz = np.split(ob_values, 3)
|
||||
# In-place convert from Blender rotation to FBX rotation.
|
||||
np.rad2deg(rot_xyz, out=rot_xyz)
|
||||
|
||||
anim_loc, anim_rot, anim_scale = anims
|
||||
anim_loc.set_keyframes(real_currframes, loc_xyz)
|
||||
anim_rot.set_keyframes(real_currframes, rot_xyz)
|
||||
anim_scale.set_keyframes(real_currframes, sca_xyz)
|
||||
all_anims.extend(anims)
|
||||
|
||||
# Set shape key curves.
|
||||
# There's only one array per shape key, so there's no need to split `all_shape_key_values`.
|
||||
for (anim_shape, _me, _shape), shape_key_values in zip(animdata_shapes.values(), all_shape_key_values):
|
||||
# In-place convert from Blender Shape Key Value to FBX Deform Percent.
|
||||
shape_key_values *= 100.0
|
||||
anim_shape.set_keyframes(real_currframes, shape_key_values)
|
||||
all_anims.append(anim_shape)
|
||||
|
||||
# Set camera curves.
|
||||
# Split into equal sized views of the arrays for each camera.
|
||||
split_into = len(animdata_cameras)
|
||||
per_camera_values = np.split(all_camera_values, split_into) if split_into > 0 else ()
|
||||
zipped = zip(animdata_cameras.values(), per_camera_values)
|
||||
for (anim_camera_lens, anim_camera_focus_distance, _camera), (lens_values, focus_distance_values) in zipped:
|
||||
# In-place convert from Blender focus distance to FBX.
|
||||
focus_distance_values *= (1000 * gscale)
|
||||
anim_camera_lens.set_keyframes(real_currframes, lens_values)
|
||||
anim_camera_focus_distance.set_keyframes(real_currframes, focus_distance_values)
|
||||
all_anims.append(anim_camera_lens)
|
||||
all_anims.append(anim_camera_focus_distance)
|
||||
|
||||
animations = {}
|
||||
|
||||
# And now, produce final data (usable by FBX export code)
|
||||
# Objects-like loc/rot/scale...
|
||||
for ob_obj, anims in animdata_ob.items():
|
||||
for anim in anims:
|
||||
anim.simplify(simplify_fac, bake_step, force_keep)
|
||||
if not anim:
|
||||
continue
|
||||
for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
|
||||
anim_data = animations.setdefault(obj_key, ("dummy_unused_key", {}))
|
||||
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
|
||||
|
||||
# And meshes' shape keys.
|
||||
for channel_key, (anim_shape, me, shape) in animdata_shapes.items():
|
||||
final_keys = {}
|
||||
anim_shape.simplify(simplify_fac, bake_step, force_keep)
|
||||
if not anim_shape:
|
||||
for anim in all_anims:
|
||||
anim.simplify(simplify_fac, bake_step, force_keep)
|
||||
if not anim:
|
||||
continue
|
||||
for elem_key, group_key, group, fbx_group, fbx_gname in anim_shape.get_final_data(scene, ref_id, force_keep):
|
||||
anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
|
||||
for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
|
||||
anim_data = animations.setdefault(obj_key, ("dummy_unused_key", {}))
|
||||
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
|
||||
|
||||
# And cameras' lens and focus distance keys.
|
||||
for cam_key, (anim_camera_lens, anim_camera_focus_distance, camera) in animdata_cameras.items():
|
||||
final_keys = {}
|
||||
anim_camera_lens.simplify(simplify_fac, bake_step, force_keep)
|
||||
anim_camera_focus_distance.simplify(simplify_fac, bake_step, force_keep)
|
||||
if anim_camera_lens:
|
||||
for elem_key, group_key, group, fbx_group, fbx_gname in \
|
||||
anim_camera_lens.get_final_data(scene, ref_id, force_keep):
|
||||
anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
|
||||
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
|
||||
if anim_camera_focus_distance:
|
||||
for elem_key, group_key, group, fbx_group, fbx_gname in \
|
||||
anim_camera_focus_distance.get_final_data(scene, ref_id, force_keep):
|
||||
anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
|
||||
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
|
||||
|
||||
astack_key = get_blender_anim_stack_key(scene, ref_id)
|
||||
alayer_key = get_blender_anim_layer_key(scene, ref_id)
|
||||
name = (get_blenderID_name(ref_id) if ref_id else scene.name).encode()
|
||||
@ -2853,8 +2904,8 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
||||
for _alayer_key, alayer in astack.values():
|
||||
for _acnode_key, acnode, _acnode_name in alayer.values():
|
||||
nbr_acnodes += 1
|
||||
for _acurve_key, _dval, acurve, acurve_valid in acnode.values():
|
||||
if acurve:
|
||||
for _acurve_key, _dval, (keys, _values), acurve_valid in acnode.values():
|
||||
if len(keys):
|
||||
nbr_acurves += 1
|
||||
|
||||
templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks)
|
||||
@ -2988,8 +3039,8 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
||||
connections.append((b"OO", acurvenode_id, alayer_id, None))
|
||||
# Animcurvenode -> object property.
|
||||
connections.append((b"OP", acurvenode_id, elem_id, fbx_prop.encode()))
|
||||
for fbx_item, (acurve_key, default_value, acurve, acurve_valid) in acurves.items():
|
||||
if acurve:
|
||||
for fbx_item, (acurve_key, default_value, (keys, values), acurve_valid) in acurves.items():
|
||||
if len(keys):
|
||||
# Animcurve -> Animcurvenode.
|
||||
connections.append((b"OP", get_fbx_uuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))
|
||||
|
||||
|
@ -1234,8 +1234,10 @@ class AnimationCurveNodeWrapper:
|
||||
and easy API to handle those.
|
||||
"""
|
||||
__slots__ = (
|
||||
'elem_keys', '_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
|
||||
'force_keying', 'force_startend_keying')
|
||||
'elem_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
|
||||
'force_keying', 'force_startend_keying',
|
||||
'_frame_times_array', '_frame_values_array', '_frame_write_mask_array',
|
||||
)
|
||||
|
||||
kinds = {
|
||||
'LCL_TRANSLATION': ("Lcl Translation", "T", ("X", "Y", "Z")),
|
||||
@ -1254,7 +1256,9 @@ class AnimationCurveNodeWrapper:
|
||||
self.fbx_props = [self.kinds[kind][2]]
|
||||
self.force_keying = force_keying
|
||||
self.force_startend_keying = force_startend_keying
|
||||
self._keys = [] # (frame, values, write_flags)
|
||||
self._frame_times_array = None
|
||||
self._frame_values_array = None
|
||||
self._frame_write_mask_array = None
|
||||
if default_values is not ...:
|
||||
assert(len(default_values) == len(self.fbx_props[0]))
|
||||
self.default_values = default_values
|
||||
@ -1263,7 +1267,7 @@ class AnimationCurveNodeWrapper:
|
||||
|
||||
def __bool__(self):
|
||||
# We are 'True' if we do have some validated keyframes...
|
||||
return bool(self._keys) and (True in ((True in k[2]) for k in self._keys))
|
||||
return self._frame_write_mask_array is not None and bool(np.any(self._frame_write_mask_array))
|
||||
|
||||
def add_group(self, elem_key, fbx_group, fbx_gname, fbx_props):
|
||||
"""
|
||||
@ -1276,19 +1280,31 @@ class AnimationCurveNodeWrapper:
|
||||
self.fbx_gname.append(fbx_gname)
|
||||
self.fbx_props.append(fbx_props)
|
||||
|
||||
def add_keyframe(self, frame, values):
|
||||
def set_keyframes(self, keyframe_times, keyframe_values):
|
||||
"""
|
||||
Add a new keyframe to all curves of the group.
|
||||
Set all keyframe times and values of the group.
|
||||
Values can be a 2D array where each row is the values for a separate curve.
|
||||
"""
|
||||
assert(len(values) == len(self.fbx_props[0]))
|
||||
self._keys.append((frame, values, [True] * len(values))) # write everything by default.
|
||||
# View 1D keyframe_values as 2D with a single row, so that the same code can be used for both 1D and
|
||||
# 2D inputs.
|
||||
if len(keyframe_values.shape) == 1:
|
||||
keyframe_values = keyframe_values[np.newaxis]
|
||||
# There must be a time for each column of values.
|
||||
assert(len(keyframe_times) == keyframe_values.shape[1])
|
||||
# There must be as many rows of values as there are properties.
|
||||
assert(len(self.fbx_props[0]) == len(keyframe_values))
|
||||
write_mask = np.full_like(keyframe_values, True, dtype=bool) # write everything by default
|
||||
self._frame_times_array = keyframe_times
|
||||
self._frame_values_array = keyframe_values
|
||||
self._frame_write_mask_array = write_mask
|
||||
|
||||
def simplify(self, fac, step, force_keep=False):
|
||||
"""
|
||||
Simplifies sampled curves by only enabling samples when:
|
||||
* their values relatively differ from the previous sample ones.
|
||||
"""
|
||||
if not self._keys:
|
||||
if self._frame_times_array is None:
|
||||
# Keyframes have not been added yet.
|
||||
return
|
||||
|
||||
if fac == 0.0:
|
||||
@ -1297,15 +1313,22 @@ class AnimationCurveNodeWrapper:
|
||||
# So that, with default factor and step values (1), we get:
|
||||
min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
|
||||
min_absdiff_fac = 0.1 # A tenth of reldiff...
|
||||
keys = self._keys
|
||||
|
||||
p_currframe, p_key, p_key_write = keys[0]
|
||||
p_keyed = list(p_key)
|
||||
are_keyed = [False] * len(p_key)
|
||||
for currframe, key, key_write in keys:
|
||||
are_keyed = []
|
||||
for values, frame_write_mask in zip(self._frame_values_array, self._frame_write_mask_array):
|
||||
# Initialise to no frames written.
|
||||
frame_write_mask[:] = False
|
||||
|
||||
# Create views of the 'previous' and 'current' mask and values. The memoryview, .data, of each array is used
|
||||
# for its iteration and indexing performance compared to the array.
|
||||
key = values[1:].data
|
||||
p_key = values[:-1].data
|
||||
key_write = frame_write_mask[1:].data
|
||||
p_key_write = frame_write_mask[:-1].data
|
||||
|
||||
p_keyedval = values[0]
|
||||
is_keyed = False
|
||||
for idx, (val, p_val) in enumerate(zip(key, p_key)):
|
||||
key_write[idx] = False
|
||||
p_keyedval = p_keyed[idx]
|
||||
if val == p_val:
|
||||
# Never write keyframe when value is exactly the same as prev one!
|
||||
continue
|
||||
@ -1319,14 +1342,14 @@ class AnimationCurveNodeWrapper:
|
||||
# If enough difference from previous sampled value, key this value *and* the previous one!
|
||||
key_write[idx] = True
|
||||
p_key_write[idx] = True
|
||||
p_keyed[idx] = val
|
||||
are_keyed[idx] = True
|
||||
p_keyedval = val
|
||||
is_keyed = True
|
||||
elif abs(val - p_keyedval) > (min_reldiff_fac * max((abs(val) + abs(p_keyedval)), min_absdiff_fac)):
|
||||
# Else, if enough difference from previous keyed value, key this value only!
|
||||
key_write[idx] = True
|
||||
p_keyed[idx] = val
|
||||
are_keyed[idx] = True
|
||||
p_currframe, p_key, p_key_write = currframe, key, key_write
|
||||
p_keyedval = val
|
||||
is_keyed = True
|
||||
are_keyed.append(is_keyed)
|
||||
|
||||
# If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
|
||||
# See T41766.
|
||||
@ -1339,20 +1362,20 @@ class AnimationCurveNodeWrapper:
|
||||
|
||||
# If we did key something, ensure first and last sampled values are keyed as well.
|
||||
if self.force_startend_keying:
|
||||
for idx, is_keyed in enumerate(are_keyed):
|
||||
for is_keyed, frame_write_mask in zip(are_keyed, self._frame_write_mask_array):
|
||||
if is_keyed:
|
||||
keys[0][2][idx] = keys[-1][2][idx] = True
|
||||
frame_write_mask[:1] = True
|
||||
frame_write_mask[-1:] = True
|
||||
|
||||
def get_final_data(self, scene, ref_id, force_keep=False):
|
||||
"""
|
||||
Yield final anim data for this 'curvenode' (for all curvenodes defined).
|
||||
force_keep is to force to keep a curve even if it only has one valid keyframe.
|
||||
"""
|
||||
curves = [[] for k in self._keys[0][1]]
|
||||
for currframe, key, key_write in self._keys:
|
||||
for curve, val, wrt in zip(curves, key, key_write):
|
||||
if wrt:
|
||||
curve.append((currframe, val))
|
||||
curves = [
|
||||
(self._frame_times_array[write_mask], values[write_mask])
|
||||
for values, write_mask in zip(self._frame_values_array, self._frame_write_mask_array)
|
||||
]
|
||||
|
||||
force_keep = force_keep or self.force_keying
|
||||
for elem_key, fbx_group, fbx_gname, fbx_props in \
|
||||
@ -1363,8 +1386,9 @@ class AnimationCurveNodeWrapper:
|
||||
fbx_item = FBX_ANIM_PROPSGROUP_NAME + "|" + fbx_item
|
||||
curve_key = get_blender_anim_curve_key(scene, ref_id, elem_key, fbx_group, fbx_item)
|
||||
# (curve key, default value, keyframes, write flag).
|
||||
group[fbx_item] = (curve_key, def_val, c,
|
||||
True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
|
||||
times = c[0]
|
||||
write_flag = len(times) > (0 if force_keep else 1)
|
||||
group[fbx_item] = (curve_key, def_val, c, write_flag)
|
||||
yield elem_key, group_key, group, fbx_group, fbx_gname
|
||||
|
||||
|
||||
|
@ -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, 17),
|
||||
'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(
|
||||
|
@ -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,15 +742,12 @@ 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
|
||||
break
|
||||
for out in active.outputs:
|
||||
if is_visible_socket(out):
|
||||
valid = True
|
||||
break
|
||||
if valid:
|
||||
# get material_output node
|
||||
materialout = None # placeholder node
|
||||
@ -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
|
||||
|
@ -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,7 +320,18 @@ 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)
|
||||
|
||||
return [rock2, rock1, roll2, roll1, result]
|
||||
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
|
||||
def parent_roll_mch_chain(self):
|
||||
@ -295,7 +344,36 @@ 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):
|
||||
rock2, rock1, roll2, roll1, result = chain
|
||||
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
|
||||
for bone in chain:
|
||||
@ -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