Sculpt: Add primitive tool in Sculpt Mode.

This commit exposes the Add Mesh tools of Object and Edit Mode to
Sculpt Mode.

This functionality only works when Multires or Dyntopo are disabled.

At a high level, the mesh drawn by the gizmo in the 3D viewport is
created as a `BMesh`, converted to a `Mesh`, and then joined to the
existing geometry.

No new functionality or defaults are changed compared to the existing
tools. Further changes are planned for future PRs.

Addresses the first point of #149216

Pull Request: https://projects.blender.org/blender/blender/pulls/152856
This commit is contained in:
kunal 2026-02-26 01:06:33 +01:00 • committed by Sean Kim
parent 865d6fd489
commit 9c185de6bd
7 changed files with 432 additions and 130 deletions

View file

@ -3904,6 +3904,8 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel):
else ()
),
None,
_tools_view3d_add,
None,
(
_defs_sculpt.mask_border,
_defs_sculpt.mask_lasso,

View file

@ -3720,6 +3720,10 @@ class VIEW3D_MT_sculpt(Menu):
layout.separator()
layout.menu("VIEW3D_MT_add_object", text="Add Primitive")
layout.separator()
sculpt_filters_types = [
('SMOOTH', iface_("Smooth", i18n_contexts.operator_default)),
('SURFACE_SMOOTH', iface_("Surface Smooth")),
@ -3837,6 +3841,33 @@ class VIEW3D_MT_sculpt_trim(Menu):
props.trim_mode = 'JOIN'
class VIEW3D_MT_add_object(Menu):
bl_label = "Add Primitive"
def draw(self, _context):
layout = self.layout
props = layout.operator("wm.tool_set_by_id", text="Add Cube")
props.name = "builtin.primitive_cube_add"
props.space_type = 'VIEW_3D'
props = layout.operator("wm.tool_set_by_id", text="Add Cylinder")
props.name = "builtin.primitive_cylinder_add"
props.space_type = 'VIEW_3D'
props = layout.operator("wm.tool_set_by_id", text="Add Cone")
props.name = "builtin.primitive_cone_add"
props.space_type = 'VIEW_3D'
props = layout.operator("wm.tool_set_by_id", text="Add UV Sphere")
props.name = "builtin.primitive_uv_sphere_add"
props.space_type = 'VIEW_3D'
props = layout.operator("wm.tool_set_by_id", text="Add Ico Cube")
props.name = "builtin.primitive_ico_sphere_add"
props.space_type = 'VIEW_3D'
class VIEW3D_MT_sculpt_curves(Menu):
bl_label = "Curves"
@ -9289,6 +9320,7 @@ classes = (
VIEW3D_MT_sculpt_transform,
VIEW3D_MT_sculpt_showhide,
VIEW3D_MT_sculpt_trim,
VIEW3D_MT_add_object,
VIEW3D_MT_mask,
VIEW3D_MT_face_sets,
VIEW3D_MT_face_sets_init,

View file

@ -9,14 +9,25 @@
#include "BLI_math_matrix.h"
#include "BLI_sys_types.h"
#include "DNA_mesh_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "BLT_translation.hh"
#include "BKE_context.hh"
#include "BKE_editmesh.hh"
#include "BKE_lib_id.hh"
#include "BKE_mesh.h"
#include "DNA_mesh_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DEG_depsgraph.hh"
#include "ED_mesh.hh"
#include "ED_object.hh"
#include "ED_screen.hh"
#include "ED_sculpt.hh"
#include "GEO_join_geometries.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
@ -24,10 +35,6 @@
#include "WM_api.hh"
#include "WM_types.hh"
#include "ED_mesh.hh"
#include "ED_object.hh"
#include "ED_screen.hh"
#include "mesh_intern.hh" /* own include */
namespace blender {
@ -38,10 +45,12 @@ namespace blender {
struct MakePrimitiveData {
float mat[4][4];
bool was_editmode;
eContextObjectMode original_mode;
};
static Object *make_prim_init(bContext *C,
wmOperator *op,
const char *idname,
const float loc[3],
const float rot[3],
@ -51,14 +60,31 @@ static Object *make_prim_init(bContext *C,
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
Object *obedit = CTX_data_edit_object(C);
Object *obedit;
r_creation_data->was_editmode = false;
if (obedit == nullptr || obedit->type != OB_MESH) {
obedit = ed::object::add_type(C, OB_MESH, idname, loc, rot, false, local_view_bits);
ed::object::editmode_enter_ex(bmain, scene, obedit, 0);
const enum eContextObjectMode original_mode = CTX_data_mode_enum(C);
r_creation_data->original_mode = original_mode;
r_creation_data->was_editmode = true;
switch (original_mode) {
case CTX_MODE_OBJECT:
obedit = ed::object::add_type(C, OB_MESH, idname, loc, rot, false, local_view_bits);
ed::object::editmode_enter_ex(bmain, scene, obedit, 0);
break;
case CTX_MODE_SCULPT:
obedit = CTX_data_active_object(C);
ed::sculpt_paint::undo::geometry_begin(*scene, *obedit, op);
break;
case CTX_MODE_EDIT_MESH:
obedit = CTX_data_edit_object(C);
if (obedit->type != OB_MESH) {
obedit = ed::object::add_type(C, OB_MESH, idname, loc, rot, false, local_view_bits);
ed::object::editmode_enter_ex(bmain, scene, obedit, 0);
}
break;
default:
obedit = CTX_data_active_object(C);
BLI_assert_unreachable();
break;
}
ed::object::new_primitive_matrix(C, obedit, loc, rot, scale, r_creation_data->mat);
@ -66,32 +92,95 @@ static Object *make_prim_init(bContext *C,
return obedit;
}
static BMesh *make_prim_init_sculpt()
{
const BMAllocTemplate allocsize{.totvert = 0, .totedge = 0, .totloop = 0, .totface = 0};
BMeshCreateParams bm_create_params{};
bm_create_params.use_toolflags = true;
BMesh *bm = BM_mesh_create(&allocsize, &bm_create_params);
return bm;
}
static void make_prim_finish_sculpt_cancelled(BMesh *bm)
{
BM_mesh_free(bm);
}
static void init_facesets(Mesh *object_mesh, Mesh *primitive_mesh)
{
bke::AttributeAccessor object_attributes = object_mesh->attributes();
bke::AttributeReader<int> object_face_sets = object_attributes.lookup<int>(".sculpt_face_set");
if (!object_face_sets) {
return;
}
bke::MutableAttributeAccessor primitive_attributes = primitive_mesh->attributes_for_write();
bke::SpanAttributeWriter<int> primitive_face_sets =
primitive_attributes.lookup_or_add_for_write_span<int>(".sculpt_face_set",
bke::AttrDomain::Face);
primitive_face_sets.span.fill(object_mesh->face_sets_color_default);
primitive_face_sets.finish();
}
static void make_prim_finish_sculpt(bContext *C, Object *ob, BMesh *bm)
{
Mesh *object_mesh = id_cast<Mesh *>(ob->data);
BMeshToMeshParams bm_to_mesh_params{};
bm_to_mesh_params.calc_object_remap = false;
Mesh *primitive_mesh = BKE_mesh_from_bmesh_nomain(bm, &bm_to_mesh_params, object_mesh);
BM_mesh_free(bm);
init_facesets(object_mesh, primitive_mesh);
bke::GeometrySet joined = geometry::join_geometries(
{bke::GeometrySet::from_mesh(object_mesh, bke::GeometryOwnershipType::ReadOnly),
bke::GeometrySet::from_mesh(primitive_mesh, bke::GeometryOwnershipType::ReadOnly)},
{});
Mesh *result = joined.get_component_for_write<bke::MeshComponent>().release();
BKE_id_free(CTX_data_main(C), primitive_mesh);
BKE_mesh_nomain_to_mesh(result, object_mesh, ob);
DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, object_mesh);
}
static void make_prim_finish(bContext *C,
Object *obedit,
const MakePrimitiveData *creation_data,
int enter_editmode)
{
BMEditMesh *em = BKE_editmesh_from_object(obedit);
const bool exit_editmode = ((creation_data->was_editmode == true) && (enter_editmode == false));
/* Primitive has all verts selected, use vert select flush
* to push this up to edges & faces. */
EDBM_selectmode_flush_ex(em, SCE_SELECT_VERTEX);
/* TODO(@ideasman42): maintain UV sync for newly created data. */
EDBM_uvselect_clear(em);
BLI_assert(
ELEM(creation_data->original_mode, CTX_MODE_OBJECT, CTX_MODE_SCULPT, CTX_MODE_EDIT_MESH));
/* Only recalculate edit-mode tessellation if we are staying in edit-mode. */
EDBMUpdate_Params params{};
params.calc_looptris = !exit_editmode;
params.calc_normals = false;
params.is_destructive = true;
EDBM_update(id_cast<Mesh *>(obedit->data), &params);
/* userdef */
if (exit_editmode) {
ed::object::editmode_exit_ex(
CTX_data_main(C), CTX_data_scene(C), obedit, ed::object::EM_FREEDATA);
if (creation_data->original_mode == CTX_MODE_SCULPT) {
ed::sculpt_paint::undo::geometry_end(*obedit);
}
else {
EDBM_selectmode_flush_ex(em, SCE_SELECT_VERTEX);
/* TODO(@ideasman42): maintain UV sync for newly created data. */
EDBM_uvselect_clear(em);
/* Only recalculate edit-mode tessellation if we are staying in edit-mode. */
EDBMUpdate_Params params{};
params.calc_looptris = creation_data->original_mode == CTX_MODE_EDIT_MESH || enter_editmode;
params.calc_normals = false;
params.is_destructive = true;
EDBM_update(id_cast<Mesh *>(obedit->data), &params);
if (creation_data->original_mode == CTX_MODE_OBJECT && !enter_editmode) {
ed::object::editmode_exit_ex(
CTX_data_main(C), CTX_data_scene(C), obedit, ed::object::EM_FREEDATA);
}
}
WM_event_add_notifier(C, NC_OBJECT | ND_DRAW, obedit);
}
@ -109,6 +198,7 @@ static wmOperatorStatus add_primitive_plane_exec(bContext *C, wmOperator *op)
ed::object::add_generic_get_opts(
C, op, 'Z', loc, rot, nullptr, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Plane"),
loc,
rot,
@ -165,7 +255,6 @@ static wmOperatorStatus add_primitive_cube_exec(bContext *C, wmOperator *op)
{
MakePrimitiveData creation_data;
Object *obedit;
BMEditMesh *em;
float loc[3], rot[3], scale[3];
bool enter_editmode;
ushort local_view_bits;
@ -175,6 +264,7 @@ static wmOperatorStatus add_primitive_cube_exec(bContext *C, wmOperator *op)
ed::object::add_generic_get_opts(
C, op, 'Z', loc, rot, scale, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Cube"),
loc,
rot,
@ -182,22 +272,40 @@ static wmOperatorStatus add_primitive_cube_exec(bContext *C, wmOperator *op)
local_view_bits,
&creation_data);
em = BKE_editmesh_from_object(obedit);
if (creation_data.original_mode == CTX_MODE_SCULPT) {
BMesh *bm = make_prim_init_sculpt();
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
if (!BMO_op_callf(bm,
BMO_FLAG_DEFAULTS,
"create_cube matrix=%m4 size=%f calc_uvs=%b",
creation_data.mat,
RNA_float_get(op->ptr, "size"),
calc_uvs))
{
make_prim_finish_sculpt_cancelled(bm);
return OPERATOR_CANCELLED;
}
make_prim_finish_sculpt(C, obedit, bm);
}
else {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
if (!EDBM_op_call_and_selectf(em,
op,
"verts.out",
false,
"create_cube matrix=%m4 size=%f calc_uvs=%b",
creation_data.mat,
RNA_float_get(op->ptr, "size"),
calc_uvs))
{
return OPERATOR_CANCELLED;
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
}
if (!EDBM_op_call_and_selectf(em,
op,
"verts.out",
false,
"create_cube matrix=%m4 size=%f calc_uvs=%b",
creation_data.mat,
RNA_float_get(op->ptr, "size"),
calc_uvs))
{
return OPERATOR_CANCELLED;
}
}
/* BMESH_TODO make plane side this: M_SQRT2 - plane (diameter of 1.41 makes it unit size) */
@ -250,6 +358,7 @@ static wmOperatorStatus add_primitive_circle_exec(bContext *C, wmOperator *op)
ed::object::add_generic_get_opts(
C, op, 'Z', loc, rot, nullptr, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Circle"),
loc,
rot,
@ -311,7 +420,6 @@ static wmOperatorStatus add_primitive_cylinder_exec(bContext *C, wmOperator *op)
{
MakePrimitiveData creation_data;
Object *obedit;
BMEditMesh *em;
float loc[3], rot[3], scale[3];
bool enter_editmode;
ushort local_view_bits;
@ -324,34 +432,60 @@ static wmOperatorStatus add_primitive_cylinder_exec(bContext *C, wmOperator *op)
ed::object::add_generic_get_opts(
C, op, 'Z', loc, rot, scale, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Cylinder"),
loc,
rot,
scale,
local_view_bits,
&creation_data);
em = BKE_editmesh_from_object(obedit);
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
if (creation_data.original_mode == CTX_MODE_SCULPT) {
BMesh *bm = make_prim_init_sculpt();
if (!BMO_op_callf(bm,
BMO_FLAG_DEFAULTS,
"create_cone segments=%i radius1=%f radius2=%f cap_ends=%b "
"cap_tris=%b depth=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "vertices"),
RNA_float_get(op->ptr, "radius"),
RNA_float_get(op->ptr, "radius"),
cap_end,
cap_tri,
RNA_float_get(op->ptr, "depth"),
creation_data.mat,
calc_uvs))
{
make_prim_finish_sculpt_cancelled(bm);
return OPERATOR_CANCELLED;
}
make_prim_finish_sculpt(C, obedit, bm);
}
else {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
if (!EDBM_op_call_and_selectf(em,
op,
"verts.out",
false,
"create_cone segments=%i radius1=%f radius2=%f cap_ends=%b "
"cap_tris=%b depth=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "vertices"),
RNA_float_get(op->ptr, "radius"),
RNA_float_get(op->ptr, "radius"),
cap_end,
cap_tri,
RNA_float_get(op->ptr, "depth"),
creation_data.mat,
calc_uvs))
{
return OPERATOR_CANCELLED;
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
}
if (!EDBM_op_call_and_selectf(em,
op,
"verts.out",
false,
"create_cone segments=%i radius1=%f radius2=%f cap_ends=%b "
"cap_tris=%b depth=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "vertices"),
RNA_float_get(op->ptr, "radius"),
RNA_float_get(op->ptr, "radius"),
cap_end,
cap_tri,
RNA_float_get(op->ptr, "depth"),
creation_data.mat,
calc_uvs))
{
return OPERATOR_CANCELLED;
}
}
make_prim_finish(C, obedit, &creation_data, enter_editmode);
@ -388,7 +522,6 @@ static wmOperatorStatus add_primitive_cone_exec(bContext *C, wmOperator *op)
{
MakePrimitiveData creation_data;
Object *obedit;
BMEditMesh *em;
float loc[3], rot[3], scale[3];
bool enter_editmode;
ushort local_view_bits;
@ -401,34 +534,60 @@ static wmOperatorStatus add_primitive_cone_exec(bContext *C, wmOperator *op)
ed::object::add_generic_get_opts(
C, op, 'Z', loc, rot, scale, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Cone"),
loc,
rot,
scale,
local_view_bits,
&creation_data);
em = BKE_editmesh_from_object(obedit);
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
if (creation_data.original_mode == CTX_MODE_SCULPT) {
BMesh *bm = make_prim_init_sculpt();
if (!BMO_op_callf(bm,
BMO_FLAG_DEFAULTS,
"create_cone segments=%i radius1=%f radius2=%f cap_ends=%b "
"cap_tris=%b depth=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "vertices"),
RNA_float_get(op->ptr, "radius1"),
RNA_float_get(op->ptr, "radius2"),
cap_end,
cap_tri,
RNA_float_get(op->ptr, "depth"),
creation_data.mat,
calc_uvs))
{
make_prim_finish_sculpt_cancelled(bm);
return OPERATOR_CANCELLED;
}
make_prim_finish_sculpt(C, obedit, bm);
}
else {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
if (!EDBM_op_call_and_selectf(em,
op,
"verts.out",
false,
"create_cone segments=%i radius1=%f radius2=%f cap_ends=%b "
"cap_tris=%b depth=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "vertices"),
RNA_float_get(op->ptr, "radius1"),
RNA_float_get(op->ptr, "radius2"),
cap_end,
cap_tri,
RNA_float_get(op->ptr, "depth"),
creation_data.mat,
calc_uvs))
{
return OPERATOR_CANCELLED;
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
}
if (!EDBM_op_call_and_selectf(em,
op,
"verts.out",
false,
"create_cone segments=%i radius1=%f radius2=%f cap_ends=%b "
"cap_tris=%b depth=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "vertices"),
RNA_float_get(op->ptr, "radius1"),
RNA_float_get(op->ptr, "radius2"),
cap_end,
cap_tri,
RNA_float_get(op->ptr, "depth"),
creation_data.mat,
calc_uvs))
{
return OPERATOR_CANCELLED;
}
}
make_prim_finish(C, obedit, &creation_data, enter_editmode);
@ -478,6 +637,7 @@ static wmOperatorStatus add_primitive_grid_exec(bContext *C, wmOperator *op)
ed::object::add_generic_get_opts(
C, op, 'Z', loc, rot, nullptr, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Grid"),
loc,
rot,
@ -553,6 +713,7 @@ static wmOperatorStatus add_primitive_monkey_exec(bContext *C, wmOperator *op)
C, op, 'Y', loc, rot, nullptr, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Suzanne"),
loc,
rot,
@ -608,7 +769,6 @@ static wmOperatorStatus add_primitive_uvsphere_exec(bContext *C, wmOperator *op)
{
MakePrimitiveData creation_data;
Object *obedit;
BMEditMesh *em;
float loc[3], rot[3], scale[3];
bool enter_editmode;
ushort local_view_bits;
@ -618,31 +778,54 @@ static wmOperatorStatus add_primitive_uvsphere_exec(bContext *C, wmOperator *op)
ed::object::add_generic_get_opts(
C, op, 'Z', loc, rot, scale, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Sphere"),
loc,
rot,
scale,
local_view_bits,
&creation_data);
em = BKE_editmesh_from_object(obedit);
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
if (creation_data.original_mode == CTX_MODE_SCULPT) {
BMesh *bm = make_prim_init_sculpt();
if (!BMO_op_callf(
bm,
BMO_FLAG_DEFAULTS,
"create_uvsphere u_segments=%i v_segments=%i radius=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "segments"),
RNA_int_get(op->ptr, "ring_count"),
RNA_float_get(op->ptr, "radius"),
creation_data.mat,
calc_uvs))
{
make_prim_finish_sculpt_cancelled(bm);
return OPERATOR_CANCELLED;
}
make_prim_finish_sculpt(C, obedit, bm);
}
else {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
if (!EDBM_op_call_and_selectf(
em,
op,
"verts.out",
false,
"create_uvsphere u_segments=%i v_segments=%i radius=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "segments"),
RNA_int_get(op->ptr, "ring_count"),
RNA_float_get(op->ptr, "radius"),
creation_data.mat,
calc_uvs))
{
return OPERATOR_CANCELLED;
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
}
if (!EDBM_op_call_and_selectf(
em,
op,
"verts.out",
false,
"create_uvsphere u_segments=%i v_segments=%i radius=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "segments"),
RNA_int_get(op->ptr, "ring_count"),
RNA_float_get(op->ptr, "radius"),
creation_data.mat,
calc_uvs))
{
return OPERATOR_CANCELLED;
}
}
make_prim_finish(C, obedit, &creation_data, enter_editmode);
@ -679,7 +862,6 @@ static wmOperatorStatus add_primitive_icosphere_exec(bContext *C, wmOperator *op
{
MakePrimitiveData creation_data;
Object *obedit;
BMEditMesh *em;
float loc[3], rot[3], scale[3];
bool enter_editmode;
ushort local_view_bits;
@ -689,30 +871,51 @@ static wmOperatorStatus add_primitive_icosphere_exec(bContext *C, wmOperator *op
ed::object::add_generic_get_opts(
C, op, 'Z', loc, rot, scale, &enter_editmode, &local_view_bits, nullptr);
obedit = make_prim_init(C,
op,
CTX_DATA_(BLT_I18NCONTEXT_ID_MESH, "Icosphere"),
loc,
rot,
scale,
local_view_bits,
&creation_data);
em = BKE_editmesh_from_object(obedit);
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
if (creation_data.original_mode == CTX_MODE_SCULPT) {
BMesh *bm = make_prim_init_sculpt();
if (!BMO_op_callf(bm,
BMO_FLAG_DEFAULTS,
"create_icosphere subdivisions=%i radius=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "subdivisions"),
RNA_float_get(op->ptr, "radius"),
creation_data.mat,
calc_uvs))
{
make_prim_finish_sculpt_cancelled(bm);
return OPERATOR_CANCELLED;
}
make_prim_finish_sculpt(C, obedit, bm);
}
else {
BMEditMesh *em = BKE_editmesh_from_object(obedit);
if (!EDBM_op_call_and_selectf(
em,
op,
"verts.out",
false,
"create_icosphere subdivisions=%i radius=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "subdivisions"),
RNA_float_get(op->ptr, "radius"),
creation_data.mat,
calc_uvs))
{
return OPERATOR_CANCELLED;
if (calc_uvs) {
ED_mesh_uv_ensure(id_cast<Mesh *>(obedit->data), nullptr);
}
if (!EDBM_op_call_and_selectf(
em,
op,
"verts.out",
false,
"create_icosphere subdivisions=%i radius=%f matrix=%m4 calc_uvs=%b",
RNA_int_get(op->ptr, "subdivisions"),
RNA_float_get(op->ptr, "radius"),
creation_data.mat,
calc_uvs))
{
return OPERATOR_CANCELLED;
}
}
make_prim_finish(C, obedit, &creation_data, enter_editmode);

View file

@ -19,6 +19,8 @@
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_modifier.hh"
#include "BKE_paint.hh"
#include "BKE_screen.hh"
#include "BLT_translation.hh"
@ -1304,6 +1306,13 @@ static wmOperatorStatus view3d_interactive_add_modal(bContext *C,
static bool view3d_interactive_add_poll(bContext *C)
{
const enum eContextObjectMode mode = CTX_data_mode_enum(C);
if (mode == CTX_MODE_SCULPT) {
Object *ob = CTX_data_active_object(C);
return !BKE_sculpt_multires_active(CTX_data_scene(C), ob) &&
!BKE_object_sculpt_use_dyntopo(ob);
}
return ELEM(mode, CTX_MODE_OBJECT, CTX_MODE_EDIT_MESH);
}

View file

@ -1499,6 +1499,7 @@ if(WITH_UI_TESTS)
test_sculpt.mask_expand_and_invert
test_sculpt.mask_gestures
test_sculpt.trim_gestures
test_sculpt.primitive_tool_add
test_tools.sculpt_mode_toolbar
test_undo.compositor_make_group
test_undo.text_editor_edit_mode_mix

View file

@ -312,3 +312,38 @@ def trim_gestures():
yield from ui.call_operator(e, "Line Trim")
yield from e.leftmouse.cursor_motion(ui.cursor_motion_data_y(window))
t.assertEqual(mesh.attributes.domain_size('POINT'), 10)
def primitive_tool_add():
import bpy
e, t, window = ui.test_window()
yield e.ctrl.tab().s() # Sculpt via pie menu.
yield from ui.call_menu(e, "Sculpt -> Add Primitive -> Add Cube") # Select add cube tool
area = ui.get_window_area_by_type(window, 'VIEW_3D')
position = (area.x + area.width // 2, area.y + area.height // 2)
yield e.cursor_position_set(*position, move=True) # Move mouse to center
e.leftmouse.press()
yield
pixels = 10
for delta in range(pixels):
position = (position[0] + delta, position[1] + delta)
yield e.cursor_position_set(*position, move=True)
e.leftmouse.release()
yield
for delta in range(pixels):
position = (position[0] - delta, position[1] - delta)
yield e.cursor_position_set(*position, move=True)
e.leftmouse.tap()
yield
mesh = bpy.context.object.data
num_faces = mesh.attributes.domain_size('FACE')
t.assertEqual(num_faces, 12) # There should be 6 + 6 faces

View file

@ -38,36 +38,56 @@ def sculpt_mode_toolbar():
yield e.four()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin_brush.draw_face_sets")
yield e.shift.space()
yield e.five()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.primitive_cube_add")
yield e.shift.space()
yield e.six()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.primitive_cone_add")
yield e.shift.space()
yield e.seven()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.primitive_cylinder_add")
yield e.shift.space()
yield e.eight()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.primitive_uv_sphere_add")
yield e.shift.space()
yield e.nine()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.primitive_ico_sphere_add")
yield e.shift.space()
yield e.b()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.box_mask")
yield e.shift.space()
yield e.eight()
yield e.shift.three()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.box_hide")
yield e.shift.space()
yield e.shift.two()
yield e.shift.seven()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.box_face_set")
yield e.shift.space()
yield e.shift.six()
yield e.ctrl.one()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.box_trim")
yield e.shift.space()
yield e.shift.zero()
yield e.ctrl.five()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.line_project")
yield e.shift.space()
yield e.ctrl.one()
yield e.ctrl.six()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.mesh_filter")
yield e.shift.space()
yield e.ctrl.two()
yield e.ctrl.seven()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.cloth_filter")
yield e.shift.space()
yield e.ctrl.three()
yield e.ctrl.eight()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.color_filter")
yield e.shift.space()
@ -75,19 +95,19 @@ def sculpt_mode_toolbar():
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.face_set_edit")
yield e.shift.space()
yield e.ctrl.four()
yield e.ctrl.nine()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.mask_by_color")
yield e.shift.space()
yield e.ctrl.five()
yield e.ctrl.zero()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.move")
yield e.shift.space()
yield e.ctrl.six()
yield e.alt.one()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.rotate")
yield e.shift.space()
yield e.ctrl.seven()
yield e.alt.two()
t.assertEqual(window.workspace.tools.from_space_view3d_mode('SCULPT').idname, "builtin.scale")
yield e.shift.space()