From c2319f8293d97775e4abccb64cf5701f02c0214b Mon Sep 17 00:00:00 2001 From: tariqsulley Date: Fri, 19 Sep 2025 11:26:14 +1000 Subject: [PATCH] Modeling: support adding lattices to selected objects Support adding lattices to selected objects, deforming them with with the lattice modifier. By default the lattice fits to the object bounds and is oriented to the active object (if it's set). Resolves #144076 Ref !144888 --- scripts/startup/bl_ui/space_view3d.py | 17 +- source/blender/editors/object/object_add.cc | 244 ++++++++++++++++++ .../blender/editors/object/object_intern.hh | 1 + source/blender/editors/object/object_ops.cc | 1 + 4 files changed, 262 insertions(+), 1 deletion(-) diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index 36124f1f356..0e4011edd02 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -2618,6 +2618,20 @@ class VIEW3D_MT_grease_pencil_add(Menu): layout.operator("object.grease_pencil_add", text="Object Line Art", icon='OBJECT_DATA').type = 'LINEART_OBJECT' +class VIEW3D_MT_lattice_add(Menu): + bl_idname = "VIEW3D_MT_lattice_add" + bl_label = "Lattice" + bl_translation_context = i18n_contexts.operator_default + bl_options = {'SEARCH_ON_KEY_PRESS'} + + def draw(self, _context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.operator("object.add", text="Lattice", icon='OUTLINER_OB_LATTICE').type = 'LATTICE' + layout.operator("object.lattice_add_to_selected", text="Lattice Deform Selected", icon='OUTLINER_OB_LATTICE') + + class VIEW3D_MT_empty_add(Menu): bl_idname = "VIEW3D_MT_empty_add" bl_label = "Empty" @@ -2676,7 +2690,7 @@ class VIEW3D_MT_add(Menu): else: layout.operator("object.armature_add", text="Armature", icon='OUTLINER_OB_ARMATURE') - layout.operator("object.add", text="Lattice", icon='OUTLINER_OB_LATTICE').type = 'LATTICE' + layout.menu("VIEW3D_MT_lattice_add", icon='OUTLINER_OB_LATTICE') layout.separator() @@ -9166,6 +9180,7 @@ classes = ( VIEW3D_MT_camera_add, VIEW3D_MT_volume_add, VIEW3D_MT_grease_pencil_add, + VIEW3D_MT_lattice_add, VIEW3D_MT_empty_add, VIEW3D_MT_add, VIEW3D_MT_image_add, diff --git a/source/blender/editors/object/object_add.cc b/source/blender/editors/object/object_add.cc index 9e11a919f6f..4827fb4ec49 100644 --- a/source/blender/editors/object/object_add.cc +++ b/source/blender/editors/object/object_add.cc @@ -30,6 +30,7 @@ #include "DNA_vfont_types.h" #include "BLI_array_utils.hh" +#include "BLI_bounds.hh" #include "BLI_ghash.h" #include "BLI_listbase.h" #include "BLI_math_color.h" @@ -691,6 +692,18 @@ Object *add_type(bContext *C, return add_type_with_obdata(C, type, name, loc, rot, enter_editmode, local_view_bits, nullptr); } +static bool object_can_have_lattice_modifier(const Object *ob) +{ + return ELEM(ob->type, + OB_MESH, + OB_CURVES_LEGACY, + OB_SURF, + OB_FONT, + OB_CURVES, + OB_GREASE_PENCIL, + OB_LATTICE); +} + /* for object add operator */ static wmOperatorStatus object_add_exec(bContext *C, wmOperator *op) { @@ -738,6 +751,237 @@ void OBJECT_OT_add(wmOperatorType *ot) add_generic_props(ot, true); } +/* -------------------------------------------------------------------- */ +/** \name Add Lattice Deformation to Selected Operator + * \{ */ + +static std::optional> lattice_add_to_selected_collect_targets_and_calc_bounds( + bContext *C, const float orientation_matrix[3][3], Vector &r_targets) +{ + ViewLayer *view_layer = CTX_data_view_layer(C); + View3D *v3d = CTX_wm_view3d(C); + Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); + + Bounds local_bounds; + local_bounds.min = float3(FLT_MAX); + local_bounds.max = float3(-FLT_MAX); + bool has_bounds = false; + + float inverse_orientation_matrix[3][3]; + invert_m3_m3_safe_ortho(inverse_orientation_matrix, orientation_matrix); + + LISTBASE_FOREACH (Base *, base, &view_layer->object_bases) { + if (!BASE_SELECTED_EDITABLE(v3d, base) || !object_can_have_lattice_modifier(base->object)) { + continue; + } + + r_targets.append(base->object); + const Object *object_eval = DEG_get_evaluated(depsgraph, base->object); + if (object_eval && DEG_object_transform_is_evaluated(*object_eval)) { + if (std::optional> object_bounds = BKE_object_boundbox_get(object_eval)) { + const float(*object_to_world_matrix)[4] = object_eval->object_to_world().ptr(); + /* Generate all 8 corners of the bounding box. */ + std::array corners = bounds::corners(*object_bounds); + for (float3 &corner : corners) { + mul_m4_v3(object_to_world_matrix, corner); + mul_m3_v3(inverse_orientation_matrix, corner); + local_bounds.min = math::min(local_bounds.min, corner); + local_bounds.max = math::max(local_bounds.max, corner); + } + has_bounds = true; + } + } + } + + if (has_bounds) { + return local_bounds; + } + return std::nullopt; +} + +static wmOperatorStatus lattice_add_to_selected_exec(bContext *C, wmOperator *op) +{ + Main *bmain = CTX_data_main(C); + Scene *scene = CTX_data_scene(C); + Object *ob_active = CTX_data_active_object(C); + ushort local_view_bits; + bool enter_editmode; + float location[3], rotation_euler[3]; + WM_operator_view3d_unit_defaults(C, op); + add_generic_get_opts( + C, op, 'Z', location, rotation_euler, nullptr, &enter_editmode, &local_view_bits, nullptr); + + const float margin = RNA_float_get(op->ptr, "margin"); + const bool add_modifiers = RNA_boolean_get(op->ptr, "add_modifiers"); + const int resolution_u = RNA_int_get(op->ptr, "resolution_u"); + const int resolution_v = RNA_int_get(op->ptr, "resolution_v"); + const int resolution_w = RNA_int_get(op->ptr, "resolution_w"); + CTX_data_ensure_evaluated_depsgraph(C); + float orientation_matrix[3][3]; + + if (ob_active) { + copy_m3_m4(orientation_matrix, ob_active->object_to_world().ptr()); + normalize_m3(orientation_matrix); + } + else { + unit_m3(orientation_matrix); + } + + Vector targets; + std::optional> bounds_opt = + lattice_add_to_selected_collect_targets_and_calc_bounds(C, orientation_matrix, targets); + + /* Disable fit to selected when there are no valid targets + * (either nothing is selected or meshes with no geometry). */ + if (targets.is_empty() || !bounds_opt.has_value()) { + RNA_boolean_set(op->ptr, "fit_to_selected", false); + } + const bool fit_to_selected = RNA_boolean_get(op->ptr, "fit_to_selected"); + + Object *ob_lattice = add_type( + C, OB_LATTICE, nullptr, location, rotation_euler, enter_editmode, local_view_bits); + Lattice *lt = (Lattice *)ob_lattice->data; + + if (fit_to_selected && bounds_opt.has_value()) { + /* Calculate the center and size of this combined bounding box. */ + const float3 center_local = bounds_opt->center(); + const float3 size_local = bounds_opt->size() + float3(margin * 2); + + /* Orient lattice center and apply rotation. */ + float3 center_world = center_local; + mul_m3_v3(orientation_matrix, center_world); + BKE_object_mat3_to_rot(ob_lattice, orientation_matrix, false); + + copy_v3_v3(ob_lattice->loc, center_world); + copy_v3_v3(ob_lattice->scale, size_local); + + /* Prevent invalid or zero lattice size, fallback to 1.0f. */ + for (int i = 0; i < 3; i++) { + if (!isfinite(ob_lattice->scale[i]) || ob_lattice->scale[i] <= FLT_EPSILON) { + ob_lattice->scale[i] = 1.0f; + } + } + } + else { + /* Fallback when fit to selected is off. */ + copy_v3_fl(ob_lattice->scale, RNA_float_get(op->ptr, "radius")); + + /* Apply user specified Euler rotation instead of cached quat. */ + ob_lattice->rotmode = ROT_MODE_EUL; + copy_v3_v3(ob_lattice->rot, rotation_euler); + } + + if (add_modifiers) { + for (Object *ob : targets) { + BLI_assert(ob != ob_lattice); + BLI_assert(object_can_have_lattice_modifier(ob)); + + LatticeModifierData *lmd = (LatticeModifierData *)modifier_add( + op->reports, bmain, scene, ob, nullptr, eModifierType_Lattice); + if (UNLIKELY(lmd == nullptr)) { + continue; + } + + lmd->object = ob_lattice; + DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY); + WM_main_add_notifier(NC_OBJECT | ND_MODIFIER, ob); + } + } + + BKE_lattice_resize( + lt, max_ii(1, resolution_u), max_ii(1, resolution_v), max_ii(1, resolution_w), ob_lattice); + + DEG_id_tag_update(&ob_lattice->id, ID_RECALC_GEOMETRY | ID_RECALC_TRANSFORM); + return OPERATOR_FINISHED; +} + +static bool object_add_to_selected_poll_property(const bContext * /*C*/, + wmOperator *op, + const PropertyRNA *prop) +{ + const char *prop_id = RNA_property_identifier(prop); + + /* Shows only relevant redo properties. + * If `fit_to_selected` is: + * - true: location & rotation are ignored. + * - false: margin is ignored since it only applies to the "fit". + */ + if (RNA_boolean_get(op->ptr, "fit_to_selected")) { + if (STR_ELEM(prop_id, "radius", "align", "location", "rotation")) { + return false; + } + } + else { + if (STREQ(prop_id, "margin")) { + return false; + } + } + return true; +} + +void OBJECT_OT_lattice_add_to_selected(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Add Lattice Deformer"; + ot->description = "Add a lattice and use it to deform selected objects"; + ot->idname = "OBJECT_OT_lattice_add_to_selected"; + + /* API callbacks. */ + ot->exec = lattice_add_to_selected_exec; + ot->poll = ED_operator_objectmode; + ot->poll_property = object_add_to_selected_poll_property; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + /* properties */ + PropertyRNA *prop; + + prop = RNA_def_boolean(ot->srna, + "fit_to_selected", + true, + "Fit to Selected", + "Resize lattice to fit selected deformable objects"); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); + + add_unit_props_radius(ot); + prop = RNA_def_float(ot->srna, + "margin", + 0.0f, + 0.0f, + FLT_MAX, + "Margin", + "Add margin to lattice dimensions", + 0.0f, + 10.0f); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); + + prop = RNA_def_boolean(ot->srna, + "add_modifiers", + true, + "Add Modifiers", + "Automatically add lattice modifiers to selected objects"); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); + + prop = RNA_def_int(ot->srna, + "resolution_u", + 2, + 1, + 64, + "Resolution U", + "Lattice resolution in U direction", + 1, + 64); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); + + prop = RNA_def_int( + ot->srna, "resolution_v", 2, 1, 64, "V", "Lattice resolution in V direction", 1, 64); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); + + prop = RNA_def_int( + ot->srna, "resolution_w", 2, 1, 64, "W", "Lattice resolution in W direction", 1, 64); + add_generic_props(ot, true); +} + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/editors/object/object_intern.hh b/source/blender/editors/object/object_intern.hh index 8c603fff88f..fc53918d2d1 100644 --- a/source/blender/editors/object/object_intern.hh +++ b/source/blender/editors/object/object_intern.hh @@ -115,6 +115,7 @@ void OBJECT_OT_select_same_collection(wmOperatorType *ot); /* object_add.cc */ void OBJECT_OT_add(wmOperatorType *ot); +void OBJECT_OT_lattice_add_to_selected(wmOperatorType *ot); void OBJECT_OT_add_named(wmOperatorType *ot); void OBJECT_OT_transform_to_mouse(wmOperatorType *ot); void OBJECT_OT_metaball_add(wmOperatorType *ot); diff --git a/source/blender/editors/object/object_ops.cc b/source/blender/editors/object/object_ops.cc index 9f33167f978..c51d3cae9ef 100644 --- a/source/blender/editors/object/object_ops.cc +++ b/source/blender/editors/object/object_ops.cc @@ -101,6 +101,7 @@ void operatortypes_object() WM_operatortype_append(OBJECT_OT_volume_add); WM_operatortype_append(OBJECT_OT_volume_import); WM_operatortype_append(OBJECT_OT_add); + WM_operatortype_append(OBJECT_OT_lattice_add_to_selected); WM_operatortype_append(OBJECT_OT_add_named); WM_operatortype_append(OBJECT_OT_transform_to_mouse); WM_operatortype_append(OBJECT_OT_effector_add);