Changeset View
Standalone View
source/blender/editors/armature/armature_add.c
| Show All 16 Lines | |||||
| * All rights reserved. | * All rights reserved. | ||||
| * Operators and API's for creating bones | * Operators and API's for creating bones | ||||
| */ | */ | ||||
| /** \file | /** \file | ||||
| * \ingroup edarmature | * \ingroup edarmature | ||||
| */ | */ | ||||
| #include "DNA_anim_types.h" | |||||
| #include "DNA_armature_types.h" | #include "DNA_armature_types.h" | ||||
| #include "DNA_constraint_types.h" | #include "DNA_constraint_types.h" | ||||
| #include "DNA_object_types.h" | #include "DNA_object_types.h" | ||||
| #include "DNA_scene_types.h" | #include "DNA_scene_types.h" | ||||
| #include "MEM_guardedalloc.h" | #include "MEM_guardedalloc.h" | ||||
| #include "BLI_blenlib.h" | #include "BLI_blenlib.h" | ||||
| #include "BLI_math.h" | #include "BLI_math.h" | ||||
| #include "BLI_ghash.h" | #include "BLI_ghash.h" | ||||
| #include "BLI_string_utils.h" | #include "BLI_string_utils.h" | ||||
| #include "BKE_action.h" | #include "BKE_action.h" | ||||
| #include "BKE_constraint.h" | #include "BKE_constraint.h" | ||||
| #include "BKE_context.h" | #include "BKE_context.h" | ||||
| #include "BKE_fcurve.h" | |||||
| #include "BKE_idprop.h" | #include "BKE_idprop.h" | ||||
| #include "BKE_deform.h" | #include "BKE_deform.h" | ||||
| #include "BKE_layer.h" | #include "BKE_layer.h" | ||||
| #include "BKE_library.h" | |||||
| #include "BKE_lib_id.h" | |||||
| #include "BKE_main.h" | |||||
| #include "DEG_depsgraph.h" | |||||
| #include "RNA_access.h" | #include "RNA_access.h" | ||||
| #include "RNA_define.h" | #include "RNA_define.h" | ||||
| #include "WM_api.h" | #include "WM_api.h" | ||||
| #include "WM_types.h" | #include "WM_types.h" | ||||
| #include "ED_armature.h" | #include "ED_armature.h" | ||||
| ▲ Show 20 Lines • Show All 314 Lines • ▼ Show 20 Lines | if (ebone_dst) { | ||||
| pchan_dst->bbone_prev = pchan_duplicate_map(ob->pose, name_map, pchan_src->bbone_prev); | pchan_dst->bbone_prev = pchan_duplicate_map(ob->pose, name_map, pchan_src->bbone_prev); | ||||
| } | } | ||||
| if (pchan_src->bbone_next) { | if (pchan_src->bbone_next) { | ||||
| pchan_dst->bbone_next = pchan_duplicate_map(ob->pose, name_map, pchan_src->bbone_next); | pchan_dst->bbone_next = pchan_duplicate_map(ob->pose, name_map, pchan_src->bbone_next); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
sybren: There used to be a comment here with a bit of explanation of what parameter is what. I think… | |||||
Done Inline ActionsThe previous comment here was clarify what src_ob and dst_ob was used for. Now this function simply does what the name suggests, it updates the bone duplicate constraint sub-target. /* add 'a' and 'b' together and return the result */ float add(float a, float b) zeddb: The previous comment here was clarify what `src_ob` and `dst_ob` was used for.
Now this… | |||||
| BLI_ghash_free(name_map, NULL, NULL); | BLI_ghash_free(name_map, NULL, NULL); | ||||
| } | } | ||||
| /* | static void updateDuplicateSubtarget(EditBone *dup_bone, | ||||
| * Note: When duplicating cross objects, editbones here is the list of bones | |||||
| * from the SOURCE object but ob is the DESTINATION object | |||||
| * */ | |||||
| void updateDuplicateSubtargetObjects(EditBone *dupBone, | |||||
| ListBase *editbones, | ListBase *editbones, | ||||
| Object *src_ob, | Object *ob, | ||||
| Object *dst_ob) | bool lookup_mirror_subtarget) | ||||
| { | { | ||||
| /* If an edit bone has been duplicated, lets | /* If an edit bone has been duplicated, lets update it's constraints if the | ||||
| * update it's constraints if the subtarget | * subtarget they point to has also been duplicated. | ||||
| * they point to has also been duplicated | |||||
| */ | */ | ||||
| EditBone *oldtarget, *newtarget; | EditBone *oldtarget, *newtarget; | ||||
| bPoseChannel *pchan; | bPoseChannel *pchan; | ||||
| bConstraint *curcon; | bConstraint *curcon; | ||||
| ListBase *conlist; | ListBase *conlist; | ||||
| if ((pchan = BKE_pose_channel_verify(dst_ob->pose, dupBone->name))) { | if ((pchan = BKE_pose_channel_verify(ob->pose, dup_bone->name))) { | ||||
| if ((conlist = &pchan->constraints)) { | if ((conlist = &pchan->constraints)) { | ||||
| for (curcon = conlist->first; curcon; curcon = curcon->next) { | for (curcon = conlist->first; curcon; curcon = curcon->next) { | ||||
| /* does this constraint have a subtarget in | /* does this constraint have a subtarget in | ||||
| * this armature? | * this armature? | ||||
| */ | */ | ||||
| const bConstraintTypeInfo *cti = BKE_constraint_typeinfo_get(curcon); | const bConstraintTypeInfo *cti = BKE_constraint_typeinfo_get(curcon); | ||||
| ListBase targets = {NULL, NULL}; | ListBase targets = {NULL, NULL}; | ||||
| bConstraintTarget *ct; | bConstraintTarget *ct; | ||||
| if (cti && cti->get_constraint_targets) { | if (cti && cti->get_constraint_targets) { | ||||
| cti->get_constraint_targets(curcon, &targets); | cti->get_constraint_targets(curcon, &targets); | ||||
| for (ct = targets.first; ct; ct = ct->next) { | for (ct = targets.first; ct; ct = ct->next) { | ||||
| if ((ct->tar == src_ob) && (ct->subtarget[0])) { | if ((ct->tar == ob) && (ct->subtarget[0])) { | ||||
| ct->tar = dst_ob; /* update target */ | |||||
| oldtarget = get_named_editbone(editbones, ct->subtarget); | oldtarget = get_named_editbone(editbones, ct->subtarget); | ||||
| if (oldtarget) { | if (oldtarget) { | ||||
| /* was the subtarget bone duplicated too? If | /* was the subtarget bone duplicated too? If | ||||
| * so, update the constraint to point at the | * so, update the constraint to point at the | ||||
| * duplicate of the old subtarget. | * duplicate of the old subtarget. | ||||
| */ | */ | ||||
| if (oldtarget->temp.ebone) { | if (oldtarget->temp.ebone) { | ||||
| newtarget = oldtarget->temp.ebone; | newtarget = oldtarget->temp.ebone; | ||||
| BLI_strncpy(ct->subtarget, newtarget->name, sizeof(ct->subtarget)); | BLI_strncpy(ct->subtarget, newtarget->name, sizeof(ct->subtarget)); | ||||
| } | } | ||||
| else if (lookup_mirror_subtarget) { | |||||
| /* The subtarget was not selected for duplication, try to see if a mirror bone of | |||||
| * the current target exists */ | |||||
| char name_flip[MAXBONENAME]; | |||||
| BLI_string_flip_side_name(name_flip, oldtarget->name, false, sizeof(name_flip)); | |||||
| newtarget = get_named_editbone(editbones, name_flip); | |||||
| if (newtarget) { | |||||
| BLI_strncpy(ct->subtarget, newtarget->name, sizeof(ct->subtarget)); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| if (cti->flush_constraint_targets) { | if (cti->flush_constraint_targets) { | ||||
| cti->flush_constraint_targets(curcon, &targets, 0); | cti->flush_constraint_targets(curcon, &targets, 0); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| void updateDuplicateSubtarget(EditBone *dupBone, ListBase *editbones, Object *ob) | static void updateDuplicateActionConstraintSettings(EditBone *dup_bone, | ||||
Done Inline ActionsEnd sentences with a period. The comment could also be put on two lines, I'm guessing. sybren: End sentences with a period. The comment could also be put on two lines, I'm guessing. | |||||
| EditBone *orig_bone, | |||||
| Object *ob, | |||||
| bConstraint *curcon) | |||||
| { | |||||
| bActionConstraint *act_con = (bActionConstraint *)curcon->data; | |||||
| bAction *act = (bAction *)act_con->act; | |||||
Done Inline ActionsThis block is indented too much. If pchan == NULL or conlist == NULL you can just return. This saves two indent levels. sybren: This block is indented too much. If `pchan == NULL` or `conlist == NULL` you can just return. | |||||
| float mat[4][4]; | |||||
Done Inline ActionsThis should be a switch statement, and not a sequence of if/elseif. sybren: This should be a `switch` statement, and not a sequence of if/elseif.
Every case should be its… | |||||
| unit_m4(mat); | |||||
| bPoseChannel *target_pchan = BKE_pose_channel_find_name(ob->pose, act_con->subtarget); | |||||
| BKE_constraint_mat_convertspace(ob, target_pchan, mat, curcon->tarspace, CONSTRAINT_SPACE_LOCAL); | |||||
| float max_axis_val = 0; | |||||
Done Inline ActionsThe next code block (i.e. empty-line-delimited bit of code) doesn't mirror anything, so the comment is a bit weird here. sybren: The next code block (i.e. empty-line-delimited bit of code) doesn't mirror anything, so the… | |||||
| int max_axis = 0; | |||||
| /* Which axis represents X now. IE, which axis defines the mirror plane. */ | |||||
| for (int i = 0; i < 3; i++) { | |||||
| float cur_val = fabsf(mat[0][i]); | |||||
| if (cur_val > max_axis_val) { | |||||
| max_axis = i; | |||||
| max_axis_val = cur_val; | |||||
| } | |||||
| } | |||||
Done Inline ActionsWhat does it mean to "represent X"? This code seems to find the matrix row with the largest first value, but it's not clear why this is even relevant. sybren: What does it mean to "represent X"? This code seems to find the matrix row with the largest… | |||||
| /* data->type is mapped as follows for backwards compatibility: | |||||
| * 00,01,02 - rotation (it used to be like this) | |||||
| * 10,11,12 - scaling | |||||
| * 20,21,22 - location | |||||
| */ | |||||
| /* Mirror the target range */ | |||||
| if (act_con->type < 10 && act_con->type != max_axis) { | |||||
| /* Y or Z rotation */ | |||||
| act_con->min = -act_con->min; | |||||
Done Inline ActionsWhere is this mapped? What defines those values? Where do they come from? So many magic numbers. sybren: Where is this mapped? What defines those values? Where do they come from? So many magic numbers. | |||||
Done Inline ActionsThis is copy pasted from other parts of the animation system in blender. If we wish to update this, it should be done in a separate commit. zeddb: This is copy pasted from other parts of the animation system in blender.
If we wish to update… | |||||
| act_con->max = -act_con->max; | |||||
| } | |||||
| else if (act_con->type == max_axis + 10) { | |||||
| /* X scaling */ | |||||
| } | |||||
| else if (act_con->type == max_axis + 20) { | |||||
| /* X location */ | |||||
| float imat[4][4]; | |||||
| invert_m4_m4(imat, mat); | |||||
| float min_vec[3], max_vec[3]; | |||||
| zero_v3(min_vec); | |||||
| zero_v3(max_vec); | |||||
| min_vec[0] = act_con->min; | |||||
| max_vec[0] = act_con->max; | |||||
| /* convert values into local object space */ | |||||
| mul_m4_v3(mat, min_vec); | |||||
| mul_m4_v3(mat, max_vec); | |||||
| min_vec[0] *= -1; | |||||
| max_vec[0] *= -1; | |||||
| /* convert back to the settings space */ | |||||
| mul_m4_v3(imat, min_vec); | |||||
| mul_m4_v3(imat, max_vec); | |||||
| act_con->min = min_vec[0]; | |||||
| act_con->max = max_vec[0]; | |||||
Done Inline ActionsWhy not write min_vec[0] = -min_vec[0]? Or write min_vec[0] *= -1? Personally I prefer the latter, as it's clear at first glance that it's only taking the negative (so no swapping min/max or changing the index). sybren: Why not write `min_vec[0] = -min_vec[0]`? Or write `min_vec[0] *= -1`? Personally I prefer the… | |||||
| } | |||||
| /* See if there is any channels that uses this bone */ | |||||
| ListBase ani_curves; | |||||
| BLI_listbase_clear(&ani_curves); | |||||
| if (list_find_data_fcurves(&ani_curves, &act->curves, "pose.bones[", orig_bone->name)) { | |||||
| /* Create a copy and mirror the animation */ | |||||
| for (LinkData *ld = ani_curves.first; ld; ld = ld->next) { | |||||
| FCurve *old_curve = ld->data; | |||||
| FCurve *new_curve = copy_fcurve(old_curve); | |||||
| bActionGroup *agrp; | |||||
| char *old_path = new_curve->rna_path; | |||||
| new_curve->rna_path = BLI_str_replaceN(old_path, orig_bone->name, dup_bone->name); | |||||
| MEM_freeN(old_path); | |||||
| /* Flip the animation */ | |||||
| int i; | |||||
Done Inline ActionsJust declare int i as part of the for-loop (so for (int i=0; ...). Outside of a for-loop it's a rather horribly-named variable. sybren: Just declare `int i` as part of the for-loop (so `for (int i=0; ...`). Outside of a for-loop… | |||||
Done Inline ActionsThis is because I can't initialize both i and bezt in the for loop then. I have moved down the declaration to just above the loop instead to make it better. zeddb: This is because I can't initialize both `i` and `bezt` in the for loop then.
I have moved down… | |||||
| BezTriple *bezt; | |||||
| for (i = 0, bezt = new_curve->bezt; i < new_curve->totvert; i++, bezt++) { | |||||
| const size_t slength = strlen(new_curve->rna_path); | |||||
| bool flip = false; | |||||
| if (BLI_strn_endswith(new_curve->rna_path, "location", slength) && | |||||
| new_curve->array_index == 0) { | |||||
| flip = true; | |||||
| } | |||||
| else if (BLI_strn_endswith(new_curve->rna_path, "rotation_quaternion", slength) && | |||||
| ELEM(new_curve->array_index, 2, 3)) { | |||||
| flip = true; | |||||
| } | |||||
| else if (BLI_strn_endswith(new_curve->rna_path, "rotation_euler", slength) && | |||||
| ELEM(new_curve->array_index, 1, 2)) { | |||||
| flip = true; | |||||
| } | |||||
| else if (BLI_strn_endswith(new_curve->rna_path, "rotation_axis_angle", slength) && | |||||
| ELEM(new_curve->array_index, 2, 3)) { | |||||
| flip = true; | |||||
| } | |||||
| if (flip) { | |||||
| bezt->vec[0][1] *= -1; | |||||
| bezt->vec[1][1] *= -1; | |||||
| bezt->vec[2][1] *= -1; | |||||
| } | |||||
| } | |||||
| /* Make sure that a action group name for the new bone exists */ | |||||
| agrp = BKE_action_group_find_name(act, dup_bone->name); | |||||
| if (agrp == NULL) { | |||||
| agrp = action_groups_add_new(act, dup_bone->name); | |||||
Done Inline ActionsJust *= -1 those sybren: Just `*= -1` those | |||||
| } | |||||
| BLI_assert(agrp != NULL); | |||||
| // TODO might need to remove the existing channel if it already exists | |||||
| // from previous mirror syncing. Need to double check this with Demeter. | |||||
| action_groups_add_channel(act, agrp, new_curve); | |||||
| } | |||||
| } | |||||
| BLI_freelistN(&ani_curves); | |||||
| /* Make deps graph aware of our changes */ | |||||
| DEG_id_tag_update(&act->id, ID_RECALC_ANIMATION_NO_FLUSH); | |||||
| } | |||||
| static void updateDuplicateKinematicConstraintSettings(bConstraint *curcon) | |||||
| { | { | ||||
| updateDuplicateSubtargetObjects(dupBone, editbones, ob, ob); | /* IK constraint */ | ||||
Done Inline Actionsact doesn't change within the for-loop, so you can do the DEG update outside the loop. sybren: `act` doesn't change within the `for`-loop, so you can do the DEG update outside the loop. | |||||
| bKinematicConstraint *ik = (bKinematicConstraint *)curcon->data; | |||||
| // TODO Perhaps clamp this to 180 degrees (soft limits of the constraint) | |||||
| ik->poleangle = -M_PI - ik->poleangle; | |||||
| } | } | ||||
| EditBone *duplicateEditBoneObjects( | static void updateDuplicateLocRotConstraintSettings(Object *ob, | ||||
| EditBone *curBone, const char *name, ListBase *editbones, Object *src_ob, Object *dst_ob) | bPoseChannel *pchan, | ||||
| bConstraint *curcon) | |||||
Done Inline ActionsIt's not clamping, it's modulo'ing. And yes, that's a good thing to do. In a separate function, in BLI_whatevermath. Once working on it anyway, you may want to add two normalisation functions, one producing a result on the interval [-180°, 180°), and one on [0°, 360°). sybren: It's not clamping, it's modulo'ing. And yes, that's a good thing to do. In a separate function… | |||||
| { | { | ||||
| EditBone *eBone = MEM_mallocN(sizeof(EditBone), "addup_editbone"); | /* This code assumes that bRotLimitConstraint and bLocLimitConstraint have the same fields in | ||||
| * the same memory locations. */ | |||||
| BLI_assert(sizeof(bLocLimitConstraint) == sizeof(bRotLimitConstraint)); | |||||
| /* Copy data from old bone to new bone */ | bRotLimitConstraint *limit = (bRotLimitConstraint *)curcon->data; | ||||
Done Inline ActionsAdd something like "This code assumes that bRotLimitConstraint and bLocLimitConstraint have the same fields in the same memory locations." Possibly it would be prudent to add a similar warning to the types themselves, pointing back at this code. sybren: Add something like "This code assumes that bRotLimitConstraint and bLocLimitConstraint have the… | |||||
| memcpy(eBone, curBone, sizeof(EditBone)); | float mat[4][4], imat[4][4]; | ||||
Done Inline ActionsDon't use mat or imat, but give them a descriptive name, something that is a bit closer to "constraint space conversion matrix". sybren: Don't use `mat` or `imat`, but give them a descriptive name, something that is a bit closer to… | |||||
| curBone->temp.ebone = eBone; | float min_vec[3], max_vec[3]; | ||||
| eBone->temp.ebone = curBone; | |||||
Done Inline ActionsUnclear why the 'min vector' and 'max vector' are relevant concepts at all. sybren: Unclear why the 'min vector' and 'max vector' are relevant concepts at all. | |||||
Done Inline ActionsThis is so we can easily transform the min/max values of the constraint. zeddb: This is so we can easily transform the min/max values of the constraint. | |||||
| if (name != NULL) { | min_vec[0] = limit->xmin; | ||||
| BLI_strncpy(eBone->name, name, sizeof(eBone->name)); | min_vec[1] = limit->ymin; | ||||
Done Inline ActionsBit of a weird choice in empty lines here, possibly better grouping possible? sybren: Bit of a weird choice in empty lines here, possibly better grouping possible? | |||||
Done Inline ActionsGrouped by min/max values. I don't see any better grouping here. zeddb: Grouped by min/max values. I don't see any better grouping here. | |||||
| min_vec[2] = limit->zmin; | |||||
| max_vec[0] = limit->xmax; | |||||
| max_vec[1] = limit->ymax; | |||||
| max_vec[2] = limit->zmax; | |||||
| unit_m4(mat); | |||||
| BKE_constraint_mat_convertspace(ob, pchan, mat, curcon->ownspace, CONSTRAINT_SPACE_LOCAL); | |||||
| if (curcon->type == CONSTRAINT_TYPE_ROTLIMIT) { | |||||
| /* Zero out any location translation */ | |||||
| mat[3][0] = mat[3][1] = mat[3][2] = 0; | |||||
| } | |||||
| invert_m4_m4(imat, mat); | |||||
| /* convert values into local object space */ | |||||
| mul_m4_v3(mat, min_vec); | |||||
| mul_m4_v3(mat, max_vec); | |||||
| if (curcon->type == CONSTRAINT_TYPE_ROTLIMIT) { | |||||
| float min_copy[3]; | |||||
| copy_v3_v3(min_copy, min_vec); | |||||
| min_vec[1] = max_vec[1] * -1; | |||||
| min_vec[2] = max_vec[2] * -1; | |||||
| max_vec[1] = min_copy[1] * -1; | |||||
| max_vec[2] = min_copy[2] * -1; | |||||
| } | |||||
| else { | |||||
| float min_x_copy = min_vec[0]; | |||||
| min_vec[0] = max_vec[0] * -1; | |||||
| max_vec[0] = min_x_copy * -1; | |||||
| } | |||||
| /* convert back to the settings space */ | |||||
| mul_m4_v3(imat, min_vec); | |||||
| mul_m4_v3(imat, max_vec); | |||||
| limit->xmin = min_vec[0]; | |||||
| limit->ymin = min_vec[1]; | |||||
| limit->zmin = min_vec[2]; | |||||
| limit->xmax = max_vec[0]; | |||||
| limit->ymax = max_vec[1]; | |||||
| limit->zmax = max_vec[2]; | |||||
| } | } | ||||
| ED_armature_ebone_unique_name(editbones, eBone->name, NULL); | static void updateDuplicateTransformConstraintSettings(Object *ob, | ||||
| BLI_addtail(editbones, eBone); | bPoseChannel *pchan, | ||||
| bConstraint *curcon) | |||||
| { | |||||
| bTransformConstraint *trans = (bTransformConstraint *)curcon->data; | |||||
Done Inline Actionstar → target own, imat → something more descriptive sybren: tar → target
own, imat → something more descriptive
| |||||
Done Inline ActionsThis is what the spaces are called in the constraints themselves. zeddb: This is what the spaces are called in the constraints themselves.
So I think that these are… | |||||
| float tar_mat[4][4], own_mat[4][4], imat[4][4]; | |||||
| unit_m4(own_mat); | |||||
| BKE_constraint_mat_convertspace(ob, pchan, own_mat, curcon->ownspace, CONSTRAINT_SPACE_LOCAL); | |||||
| /* ###Source map mirroring### */ | |||||
| float old_min, old_max; | |||||
| /* Source location */ | |||||
| invert_m4_m4(imat, own_mat); | |||||
| /* convert values into local object space */ | |||||
| mul_m4_v3(own_mat, trans->from_min); | |||||
| mul_m4_v3(own_mat, trans->from_max); | |||||
| old_min = trans->from_min[0]; | |||||
| old_max = trans->from_max[0]; | |||||
Done Inline ActionsWhy not just = -old_max? sybren: Why not just ` = -old_max`? | |||||
| trans->from_min[0] = -old_max; | |||||
| trans->from_max[0] = -old_min; | |||||
| /* convert back to the settings space */ | |||||
| mul_m4_v3(imat, trans->from_min); | |||||
| mul_m4_v3(imat, trans->from_max); | |||||
| /* Source rotation */ | |||||
| /* Zero out any location translation */ | |||||
| own_mat[3][0] = own_mat[3][1] = own_mat[3][2] = 0; | |||||
| invert_m4_m4(imat, own_mat); | |||||
| /* convert values into local object space */ | |||||
| mul_m4_v3(own_mat, trans->from_min_rot); | |||||
| mul_m4_v3(own_mat, trans->from_max_rot); | |||||
| old_min = trans->from_min_rot[1]; | |||||
| old_max = trans->from_max_rot[1]; | |||||
| trans->from_min_rot[1] = old_max * -1; | |||||
| trans->from_max_rot[1] = old_min * -1; | |||||
| old_min = trans->from_min_rot[2]; | |||||
| old_max = trans->from_max_rot[2]; | |||||
| trans->from_min_rot[2] = old_max * -1; | |||||
| trans->from_max_rot[2] = old_min * -1; | |||||
| /* convert back to the settings space */ | |||||
| mul_m4_v3(imat, trans->from_min_rot); | |||||
| mul_m4_v3(imat, trans->from_max_rot); | |||||
| /* Source scale does not require any mirroring */ | |||||
| /* ###Destination map mirroring### */ | |||||
Done Inline ActionsDon't name things temp, but describe what it is. The fact that it's here in an if in a loop in an if already shows that's temporary. Declaring where it's needed and then naming descriptively ('copy of scale' is likely better than 'temp vector') is better. sybren: Don't name things `temp`, but describe what it is. The fact that it's here in an if in a loop… | |||||
Done Inline ActionsIn this case, the variable is used to store values temporary. It stores loc/rot/scale data depending on how the constraint is mapped. zeddb: In this case, the variable is used to store values temporary.
It stores loc/rot/scale data… | |||||
| float temp_vec[3]; | |||||
| float imat_rot[4][4]; | |||||
| bPoseChannel *target_pchan = BKE_pose_channel_find_name(ob->pose, trans->subtarget); | |||||
| unit_m4(tar_mat); | |||||
| BKE_constraint_mat_convertspace( | |||||
| ob, target_pchan, tar_mat, curcon->tarspace, CONSTRAINT_SPACE_LOCAL); | |||||
| invert_m4_m4(imat, tar_mat); | |||||
| /* convert values into local object space */ | |||||
| mul_m4_v3(tar_mat, trans->to_min); | |||||
| mul_m4_v3(tar_mat, trans->to_max); | |||||
| mul_m4_v3(tar_mat, trans->to_min_scale); | |||||
| mul_m4_v3(tar_mat, trans->to_max_scale); | |||||
| /* Zero out any location translation */ | |||||
| tar_mat[3][0] = tar_mat[3][1] = tar_mat[3][2] = 0; | |||||
| invert_m4_m4(imat_rot, tar_mat); | |||||
| mul_m4_v3(tar_mat, trans->to_min_rot); | |||||
| mul_m4_v3(tar_mat, trans->to_max_rot); | |||||
| /* TODO This does not support euler order, but doing so will make this way more complex. | |||||
| * For now we have decided to not support all cornercases and advanced setups. (sebpa) | |||||
| */ | |||||
Done Inline Actionsswitch! sybren: switch! | |||||
| /* Helper variables to denote the axis in trans->map */ | |||||
| const char X = 0; | |||||
| const char Y = 1; | |||||
| const char Z = 2; | |||||
Done Inline ActionsYou could even go further and de-uglify it locally a bit by #define AXIS_X 0 and the like (or use a const char for that). That way at least we have some understandable word there, instead of just a 0. sybren: You could even go further and de-uglify it locally a bit by `#define AXIS_X 0` and the like (or… | |||||
| switch (trans->to) { | |||||
| case TRANS_SCALE: | |||||
| copy_v3_v3(temp_vec, trans->to_max_scale); | |||||
| for (int i = 0; i < 3; i++) { | |||||
| if ((trans->from == TRANS_LOCATION && trans->map[i] == X) || | |||||
| (trans->from == TRANS_ROTATION && trans->map[i] != X)) { | |||||
| /* X Loc to X/Y/Z Scale: Min/Max Flipped */ | |||||
| /* Y Rot to X/Y/Z Scale: Min/Max Flipped */ | |||||
| /* Z Rot to X/Y/Z Scale: Min/Max Flipped */ | |||||
| trans->to_max_scale[i] = trans->to_min_scale[i]; | |||||
| trans->to_min_scale[i] = temp_vec[i]; | |||||
| } | |||||
| } | |||||
| break; | |||||
| case TRANS_LOCATION: | |||||
| /* Invert the X location */ | |||||
| trans->to_min[0] *= -1; | |||||
| trans->to_max[0] *= -1; | |||||
| copy_v3_v3(temp_vec, trans->to_max); | |||||
| for (int i = 0; i < 3; i++) { | |||||
| if ((trans->from == TRANS_LOCATION && trans->map[i] == X) || | |||||
| (trans->from == TRANS_ROTATION && trans->map[i] != X)) { | |||||
| /* X Loc to X/Y/Z Loc: Min/Max Flipped (and Inverted) | |||||
| * Y Rot to X/Y/Z Loc: Min/Max Flipped | |||||
| * Z Rot to X/Y/Z Loc: Min/Max Flipped */ | |||||
| trans->to_max[i] = trans->to_min[i]; | |||||
| trans->to_min[i] = temp_vec[i]; | |||||
| } | |||||
| } | |||||
| break; | |||||
| case TRANS_ROTATION: | |||||
| /* Invert the Z rotation */ | |||||
| trans->to_min_rot[2] *= -1; | |||||
| trans->to_max_rot[2] *= -1; | |||||
| if ((trans->from == TRANS_LOCATION && trans->map[1] != X) || | |||||
| (trans->from == TRANS_ROTATION && trans->map[1] != Y) || trans->from == TRANS_SCALE) { | |||||
| /* Invert the Y rotation */ | |||||
| trans->to_min_rot[1] *= -1; | |||||
| trans->to_max_rot[1] *= -1; | |||||
| } | |||||
| copy_v3_v3(temp_vec, trans->to_max_rot); | |||||
| for (int i = 0; i < 3; i++) { | |||||
| if ((trans->from == TRANS_LOCATION && trans->map[i] == X && i != 1) || | |||||
| (trans->from == TRANS_ROTATION && trans->map[i] == Y && i != 1) || | |||||
| (trans->from == TRANS_ROTATION && trans->map[i] == Z)) { | |||||
| /* X Loc to X/Z Rot: Flipped | |||||
| * Y Rot to X/Z Rot: Flipped | |||||
| * Z Rot to X/Y/Z rot: Flipped */ | |||||
| trans->to_max_rot[i] = trans->to_min_rot[i]; | |||||
| trans->to_min_rot[i] = temp_vec[i]; | |||||
| } | |||||
| } | |||||
| break; | |||||
| } | |||||
| /* convert back to the settings space */ | |||||
| mul_m4_v3(imat, trans->to_min); | |||||
| mul_m4_v3(imat, trans->to_max); | |||||
| mul_m4_v3(imat_rot, trans->to_min_rot); | |||||
| mul_m4_v3(imat_rot, trans->to_max_rot); | |||||
| mul_m4_v3(imat, trans->to_min_scale); | |||||
| mul_m4_v3(imat, trans->to_max_scale); | |||||
| } | |||||
Done Inline ActionsSame as above, just do if (ob->pose == NULL) { return; } sybren: Same as above, just do `if (ob->pose == NULL) { return; }` | |||||
| static void updateDuplicateConstraintSettings(EditBone *dup_bone, EditBone *orig_bone, Object *ob) | |||||
| { | |||||
| /* If an edit bone has been duplicated, lets update it's constraints if the | |||||
| * subtarget they point to has also been duplicated. | |||||
| */ | |||||
| bPoseChannel *pchan; | |||||
| bConstraint *curcon; | |||||
| ListBase *conlist; | |||||
| if ((pchan = BKE_pose_channel_verify(ob->pose, dup_bone->name)) == NULL || | |||||
| (conlist = &pchan->constraints) == NULL) { | |||||
| return; | |||||
| } | |||||
| for (curcon = conlist->first; curcon; curcon = curcon->next) { | |||||
| switch (curcon->type) { | |||||
| case CONSTRAINT_TYPE_ACTION: | |||||
| updateDuplicateActionConstraintSettings(dup_bone, orig_bone, ob, curcon); | |||||
| break; | |||||
| case CONSTRAINT_TYPE_KINEMATIC: | |||||
| updateDuplicateKinematicConstraintSettings(curcon); | |||||
| break; | |||||
| case CONSTRAINT_TYPE_LOCLIMIT: | |||||
| case CONSTRAINT_TYPE_ROTLIMIT: | |||||
| updateDuplicateLocRotConstraintSettings(ob, pchan, curcon); | |||||
| break; | |||||
| case CONSTRAINT_TYPE_TRANSFORM: | |||||
| updateDuplicateTransformConstraintSettings(ob, pchan, curcon); | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| static void updateDuplicateCustomBoneShapes(bContext *C, EditBone *dup_bone, Object *ob) | |||||
| { | |||||
| if (ob->pose == NULL) { | |||||
| return; | |||||
| } | |||||
| bPoseChannel *pchan; | |||||
| pchan = BKE_pose_channel_verify(ob->pose, dup_bone->name); | |||||
| if (pchan->custom != NULL) { | |||||
| Main *bmain = CTX_data_main(C); | |||||
| char name_flip[MAX_ID_NAME - 2]; | |||||
| /* Skip the first two chars in the object name as those are used to store object type */ | |||||
| BLI_string_flip_side_name(name_flip, pchan->custom->id.name + 2, false, sizeof(name_flip)); | |||||
| Object *shape_ob = (Object *)BKE_libblock_find_name(bmain, ID_OB, name_flip); | |||||
| if (shape_ob != NULL) { | |||||
Done Inline ActionsCode style: use underscore instead of camel case sybren: Code style: use underscore instead of camel case | |||||
| /* A flipped shape object exists, use it! */ | |||||
| pchan->custom = shape_ob; | |||||
| } | |||||
| } | |||||
| } | |||||
| static void copy_pchan(EditBone *src_bone, EditBone *dst_bone, Object *src_ob, Object *dst_ob) | |||||
| { | |||||
| /* copy the ID property */ | /* copy the ID property */ | ||||
| if (curBone->prop) { | if (src_bone->prop) { | ||||
| eBone->prop = IDP_CopyProperty(curBone->prop); | dst_bone->prop = IDP_CopyProperty(src_bone->prop); | ||||
| } | } | ||||
| /* Lets duplicate the list of constraints that the | /* Lets duplicate the list of constraints that the | ||||
| * current bone has. | * current bone has. | ||||
| */ | */ | ||||
| if (src_ob->pose) { | if (src_ob->pose) { | ||||
| bPoseChannel *chanold, *channew; | bPoseChannel *chanold, *channew; | ||||
| chanold = BKE_pose_channel_verify(src_ob->pose, curBone->name); | chanold = BKE_pose_channel_verify(src_ob->pose, src_bone->name); | ||||
| if (chanold) { | if (chanold) { | ||||
| /* WARNING: this creates a new posechannel, but there will not be an attached bone | /* WARNING: this creates a new posechannel, but there will not be an attached bone | ||||
| * yet as the new bones created here are still 'EditBones' not 'Bones'. | * yet as the new bones created here are still 'EditBones' not 'Bones'. | ||||
| */ | */ | ||||
| channew = BKE_pose_channel_verify(dst_ob->pose, eBone->name); | channew = BKE_pose_channel_verify(dst_ob->pose, dst_bone->name); | ||||
| if (channew) { | if (channew) { | ||||
| BKE_pose_channel_copy_data(channew, chanold); | BKE_pose_channel_copy_data(channew, chanold); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| return eBone; | EditBone *duplicateEditBoneObjects( | ||||
| EditBone *cur_bone, const char *name, ListBase *editbones, Object *src_ob, Object *dst_ob) | |||||
| { | |||||
| EditBone *e_bone = MEM_mallocN(sizeof(EditBone), "addup_editbone"); | |||||
| /* Copy data from old bone to new bone */ | |||||
| memcpy(e_bone, cur_bone, sizeof(EditBone)); | |||||
| cur_bone->temp.ebone = e_bone; | |||||
| e_bone->temp.ebone = cur_bone; | |||||
| if (name != NULL) { | |||||
| BLI_strncpy(e_bone->name, name, sizeof(e_bone->name)); | |||||
| } | |||||
| ED_armature_ebone_unique_name(editbones, e_bone->name, NULL); | |||||
| BLI_addtail(editbones, e_bone); | |||||
| copy_pchan(cur_bone, e_bone, src_ob, dst_ob); | |||||
| return e_bone; | |||||
| } | } | ||||
| EditBone *duplicateEditBone(EditBone *curBone, const char *name, ListBase *editbones, Object *ob) | EditBone *duplicateEditBone(EditBone *cur_bone, const char *name, ListBase *editbones, Object *ob) | ||||
| { | { | ||||
| return duplicateEditBoneObjects(curBone, name, editbones, ob, ob); | return duplicateEditBoneObjects(cur_bone, name, editbones, ob, ob); | ||||
| } | } | ||||
| static int armature_duplicate_selected_exec(bContext *C, wmOperator *op) | static int armature_duplicate_selected_exec(bContext *C, wmOperator *op) | ||||
| { | { | ||||
| ViewLayer *view_layer = CTX_data_view_layer(C); | ViewLayer *view_layer = CTX_data_view_layer(C); | ||||
| const bool do_flip_names = RNA_boolean_get(op->ptr, "do_flip_names"); | const bool do_flip_names = RNA_boolean_get(op->ptr, "do_flip_names"); | ||||
| /* cancel if nothing selected */ | /* cancel if nothing selected */ | ||||
| Show All 37 Lines | for (ebone_iter = arm->edbo->first; ebone_iter && ebone_iter != ebone_first_dupe; | ||||
| EditBone *ebone; | EditBone *ebone; | ||||
| char new_bone_name_buff[MAXBONENAME]; | char new_bone_name_buff[MAXBONENAME]; | ||||
| char *new_bone_name = ebone_iter->name; | char *new_bone_name = ebone_iter->name; | ||||
| if (do_flip_names) { | if (do_flip_names) { | ||||
| BLI_string_flip_side_name( | BLI_string_flip_side_name( | ||||
| new_bone_name_buff, ebone_iter->name, false, sizeof(new_bone_name_buff)); | new_bone_name_buff, ebone_iter->name, false, sizeof(new_bone_name_buff)); | ||||
| /* Only use flipped name if not yet in use. Otherwise we'd get again inconsistent namings | /* Only use flipped name if not yet in use. Otherwise we'd get again inconsistent | ||||
| * (different numbers), better keep default behavior in this case. */ | * namings (different numbers), better keep default behavior in this case. */ | ||||
| if (ED_armature_ebone_find_name(arm->edbo, new_bone_name_buff) == NULL) { | if (ED_armature_ebone_find_name(arm->edbo, new_bone_name_buff) == NULL) { | ||||
| new_bone_name = new_bone_name_buff; | new_bone_name = new_bone_name_buff; | ||||
| } | } | ||||
| } | } | ||||
| ebone = duplicateEditBone(ebone_iter, new_bone_name, arm->edbo, ob); | ebone = duplicateEditBone(ebone_iter, new_bone_name, arm->edbo, ob); | ||||
| if (!ebone_first_dupe) { | if (!ebone_first_dupe) { | ||||
| Show All 11 Lines | for (ebone_iter = arm->edbo->first; ebone_iter && ebone_iter != ebone_first_dupe; | ||||
| if (!ebone_iter->parent) { | if (!ebone_iter->parent) { | ||||
| /* If this bone has no parent, | /* If this bone has no parent, | ||||
| * Set the duplicate->parent to NULL | * Set the duplicate->parent to NULL | ||||
| */ | */ | ||||
| ebone->parent = NULL; | ebone->parent = NULL; | ||||
| } | } | ||||
| else if (ebone_iter->parent->temp.ebone) { | else if (ebone_iter->parent->temp.ebone) { | ||||
| /* If this bone has a parent that was duplicated, | /* If this bone has a parent that was duplicated, | ||||
| * Set the duplicate->parent to the curBone->parent->temp | * Set the duplicate->parent to the cur_bone->parent->temp | ||||
| */ | */ | ||||
| ebone->parent = ebone_iter->parent->temp.ebone; | ebone->parent = ebone_iter->parent->temp.ebone; | ||||
| } | } | ||||
| else { | else { | ||||
| /* If this bone has a parent that IS not selected, | /* If this bone has a parent that IS not selected, | ||||
| * Set the duplicate->parent to the curBone->parent | * Set the duplicate->parent to the cur_bone->parent | ||||
| */ | */ | ||||
| ebone->parent = (EditBone *)ebone_iter->parent; | ebone->parent = (EditBone *)ebone_iter->parent; | ||||
| ebone->flag &= ~BONE_CONNECTED; | ebone->flag &= ~BONE_CONNECTED; | ||||
| } | } | ||||
| /* Update custom handle links. */ | /* Update custom handle links. */ | ||||
| if (ebone_iter->bbone_prev && ebone_iter->bbone_prev->temp.ebone) { | if (ebone_iter->bbone_prev && ebone_iter->bbone_prev->temp.ebone) { | ||||
| ebone->bbone_prev = ebone_iter->bbone_prev->temp.ebone; | ebone->bbone_prev = ebone_iter->bbone_prev->temp.ebone; | ||||
| } | } | ||||
| if (ebone_iter->bbone_next && ebone_iter->bbone_next->temp.ebone) { | if (ebone_iter->bbone_next && ebone_iter->bbone_next->temp.ebone) { | ||||
| ebone->bbone_next = ebone_iter->bbone_next->temp.ebone; | ebone->bbone_next = ebone_iter->bbone_next->temp.ebone; | ||||
| } | } | ||||
| /* Lets try to fix any constraint subtargets that might | /* Lets try to fix any constraint subtargets that might | ||||
| * have been duplicated | * have been duplicated | ||||
| */ | */ | ||||
| updateDuplicateSubtarget(ebone, arm->edbo, ob); | updateDuplicateSubtarget(ebone, arm->edbo, ob, false); | ||||
| } | } | ||||
| } | } | ||||
| /* correct the active bone */ | /* correct the active bone */ | ||||
| if (arm->act_edbone && arm->act_edbone->temp.ebone) { | if (arm->act_edbone && arm->act_edbone->temp.ebone) { | ||||
| arm->act_edbone = arm->act_edbone->temp.ebone; | arm->act_edbone = arm->act_edbone->temp.ebone; | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 100 Lines • ▼ Show 20 Lines | for (ebone_iter = arm->edbo->first; ebone_iter; ebone_iter = ebone_iter->next) { | ||||
| if (ebone) { | if (ebone) { | ||||
| if ((ebone->flag & BONE_SELECTED) == 0) { | if ((ebone->flag & BONE_SELECTED) == 0) { | ||||
| /* simple case, we're selected, the other bone isn't! */ | /* simple case, we're selected, the other bone isn't! */ | ||||
| ebone_iter->temp.ebone = ebone; | ebone_iter->temp.ebone = ebone; | ||||
| } | } | ||||
| else { | else { | ||||
| /* complicated - choose which direction to copy */ | /* complicated - choose which direction to copy */ | ||||
| float axis_delta; | float axis_delta; | ||||
Done Inline ActionsSame as before, probably can use continue here. Patch wasn't submitted with Arcanist, so I can't see outside it (no context available), so maybe this is not the case. sybren: Same as before, probably can use `continue` here. Patch wasn't submitted with Arcanist, so I… | |||||
Done Inline ActionsCan't use continue here. I have resubmitted the new revision of the patch with context. zeddb: Can't use `continue` here. I have resubmitted the new revision of the patch with context. | |||||
| axis_delta = ebone->head[axis] - ebone_iter->head[axis]; | axis_delta = ebone->head[axis] - ebone_iter->head[axis]; | ||||
| if (axis_delta == 0.0f) { | if (axis_delta == 0.0f) { | ||||
| axis_delta = ebone->tail[axis] - ebone_iter->tail[axis]; | axis_delta = ebone->tail[axis] - ebone_iter->tail[axis]; | ||||
| } | } | ||||
| if (axis_delta == 0.0f) { | if (axis_delta == 0.0f) { | ||||
| /* both mirrored bones exist and point to eachother and overlap exactly. | /* both mirrored bones exist and point to eachother and overlap exactly. | ||||
| Show All 21 Lines | for (ebone_iter = arm->edbo->first; ebone_iter; ebone_iter = ebone_iter->next) { | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| /* Find the selected bones and duplicate them as needed, with mirrored name */ | /* Find the selected bones and duplicate them as needed, with mirrored name */ | ||||
| for (ebone_iter = arm->edbo->first; ebone_iter && ebone_iter != ebone_first_dupe; | for (ebone_iter = arm->edbo->first; ebone_iter && ebone_iter != ebone_first_dupe; | ||||
| ebone_iter = ebone_iter->next) { | ebone_iter = ebone_iter->next) { | ||||
| if (EBONE_VISIBLE(arm, ebone_iter) && (ebone_iter->flag & BONE_SELECTED) && | if (EBONE_VISIBLE(arm, ebone_iter) && (ebone_iter->flag & BONE_SELECTED)) { | ||||
| /* will be set if the mirror bone already exists (no need to make a new one) */ | if (ebone_iter->temp.ebone != NULL) { | ||||
| (ebone_iter->temp.ebone == NULL)) { | /* will be set if the mirror bone already exists (no need to make a new one) | ||||
| * but we do need to make sure that the pchan settings (constraints etc) is syncronized | |||||
| */ | |||||
| bPoseChannel *pchan; | |||||
| /* Make sure we clean up the old data before overwriting it */ | |||||
| pchan = BKE_pose_channel_verify(obedit->pose, ebone_iter->temp.ebone->name); | |||||
| BKE_pose_channel_free(pchan); | |||||
| /* Sync pchan data */ | |||||
| copy_pchan(ebone_iter, ebone_iter->temp.ebone, obedit, obedit); | |||||
| /* Sync scale mode */ | |||||
| ebone_iter->temp.ebone->inherit_scale_mode = ebone_iter->inherit_scale_mode; | |||||
| continue; | |||||
| } | |||||
| char name_flip[MAXBONENAME]; | char name_flip[MAXBONENAME]; | ||||
| BLI_string_flip_side_name(name_flip, ebone_iter->name, false, sizeof(name_flip)); | BLI_string_flip_side_name(name_flip, ebone_iter->name, false, sizeof(name_flip)); | ||||
| /* bones must have a side-suffix */ | /* bones must have a side-suffix */ | ||||
| if (!STREQ(name_flip, ebone_iter->name)) { | if (!STREQ(name_flip, ebone_iter->name)) { | ||||
| EditBone *ebone; | EditBone *ebone; | ||||
| Show All 30 Lines | for (ebone_iter = arm->edbo->first; ebone_iter && ebone_iter != ebone_first_dupe; | ||||
| EditBone *ebone_parent = get_symmetrized_bone(arm, ebone_iter->parent); | EditBone *ebone_parent = get_symmetrized_bone(arm, ebone_iter->parent); | ||||
| if (ebone_parent == ebone_iter->parent) { | if (ebone_parent == ebone_iter->parent) { | ||||
| /* If the mirror lookup failed, (but the current bone has a parent) | /* If the mirror lookup failed, (but the current bone has a parent) | ||||
| * then we can assume the parent has no L/R but is a center bone. | * then we can assume the parent has no L/R but is a center bone. | ||||
| * So just use the same parent for both. | * So just use the same parent for both. | ||||
| */ | */ | ||||
| ebone->flag &= ~BONE_CONNECTED; | ebone->flag &= ~BONE_CONNECTED; | ||||
| } | } | ||||
Done Inline ActionsShouldn't this use head[axis] instead? sybren: Shouldn't this use `head[axis]` instead? | |||||
Done Inline ActionsRight you are! zeddb: Right you are! | |||||
| ebone->parent = ebone_parent; | ebone->parent = ebone_parent; | ||||
| } | } | ||||
| /* Update custom handle links. */ | /* Update custom handle links. */ | ||||
| ebone->bbone_prev = get_symmetrized_bone(arm, ebone_iter->bbone_prev); | ebone->bbone_prev = get_symmetrized_bone(arm, ebone_iter->bbone_prev); | ||||
| ebone->bbone_next = get_symmetrized_bone(arm, ebone_iter->bbone_next); | ebone->bbone_next = get_symmetrized_bone(arm, ebone_iter->bbone_next); | ||||
| /* Sync bbone handle types */ | |||||
| ebone->bbone_prev_type = ebone_iter->bbone_prev_type; | |||||
| ebone->bbone_next_type = ebone_iter->bbone_next_type; | |||||
| /* Lets try to fix any constraint subtargets that might | /* Lets try to fix any constraint subtargets that might | ||||
| * have been duplicated | * have been duplicated | ||||
| */ | */ | ||||
| updateDuplicateSubtarget(ebone, arm->edbo, obedit); | updateDuplicateSubtarget(ebone, arm->edbo, obedit, true); | ||||
| /* Try to update constraint options so that they are mirrored as well | |||||
| * (need to supply bone_iter as well in case we are working with existing bones) */ | |||||
| updateDuplicateConstraintSettings(ebone, ebone_iter, obedit); | |||||
| /* Mirror bone shapes if possible */ | |||||
| updateDuplicateCustomBoneShapes(C, ebone, obedit); | |||||
| } | } | ||||
| } | } | ||||
| ED_armature_edit_transform_mirror_update(obedit); | ED_armature_edit_transform_mirror_update(obedit); | ||||
| /* Selected bones now have their 'temp' pointer set, | /* Selected bones now have their 'temp' pointer set, | ||||
| * so we don't need this anymore */ | * so we don't need this anymore */ | ||||
| ▲ Show 20 Lines • Show All 434 Lines • Show Last 20 Lines | |||||
There used to be a comment here with a bit of explanation of what parameter is what. I think that's still a good idea here (but possibly a completely different comment because behaviour changed).