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
This commit is contained in:
tariqsulley
2025-09-19 11:26:14 +10:00
committed by Campbell Barton
parent c2766d2058
commit c2319f8293
4 changed files with 262 additions and 1 deletions

View File

@@ -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,

View File

@@ -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<Bounds<float3>> lattice_add_to_selected_collect_targets_and_calc_bounds(
bContext *C, const float orientation_matrix[3][3], Vector<Object *> &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<float3> 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<Bounds<float3>> 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<float3, 8> 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<Object *> targets;
std::optional<Bounds<float3>> 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);
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@@ -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);

View File

@@ -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);