原文:https://wiki.blender.org/index.php/Dev:2.5/Source/Architecture/Operators/Tutorial
逐行解释操作如何工作的。首先解释网格细分(mesh subdivide),一个相对简单的算子。接下来,我们将解释一个更复杂的模态操作,3D视图缩放。
网络细分(Mesh Subdivide)
注册
我们必须做的第一件事是向窗口管理器注册操作符类型。为此,我们定义了一个函数,在启动时由窗口管理器调用。
1 void MESH_OT_subdivide(wmOperatorType *ot) 2 { 3 PropertyRNA *prop; 4 5 /* identifiers */ 6 ot->name = "Subdivide"; 7 ot->description = "Subdivide selected edges"; 8 ot->idname = "MESH_OT_subdivide"; 9 10 /* api callbacks */ 11 ot->exec = edbm_subdivide_exec; 12 ot->poll = ED_operator_editmesh; 13 14 /* flags */ 15 ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; 16 17 /* properties */ 18 prop = RNA_def_int(ot->srna, "number_cuts", 1, 1, 100, "Number of Cuts", "", 1, 10); 19 /* avoid re-using last var because it can cause _very_ high poly meshes and annoy users (or worse crash) */ 20 RNA_def_property_flag(prop, PROP_SKIP_SAVE); 21 }
让我们从第一行开始:
void MESH_OT_subdivide(wmOperatorType *ot)
MESH定义了操作类别,_OT_(操作类型)是操作ID名称的标准部分。函数的目的是填充wmOperatorType。
/* identifiers */ ot->name = "Subdivide"; ot->description = "Subdivide selected edges"; ot->idname = "MESH_OT_subdivide";
ot->name值表示将在用户界面中使用的字符串,它是操作的可读名称。该描述用于工具提示。idname应与函数的名称相同,它是该操作的唯一标识符。
/* api callbacks */ ot->exec = edbm_subdivide_exec; ot->poll = ED_operator_editmesh;
API回调函数定义操作实际运行的方式。将运行poll回调来测试操作符是否可以执行,而exec回调将实际执行操作。我们稍后会详细讨论这些问题。
/* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
操作标志向窗口管理器提供如何使用操作的信息。在这里,OPTYPE_REGISTER意味着操作应在历史堆栈注册。OPTYPE_UNDO表明操作完成后应(译者:push 到undo??原文:OPTYPE_UNDO indicates that an undo push should be done after the operator has finished.)。
/* properties */ prop = RNA_def_int(ot->srna, "number_cuts", 1, 1, 100, "Number of Cuts", "", 1, 10); /* avoid re-using last var because it can cause _very_ high poly meshes and annoy users (or worse crash) */ RNA_def_property_flag(prop, PROP_SKIP_SAVE);
操作可以定义多个属性。这些属性然后可以由用户设置,并且由操作用来修改其行为。这些是RNA属性,因此有关如何定义它们的更多信息,请参阅RNA文档。在这种情况下,我们将简单地定义一个整数,指示切口的数量。
WM
void ED_operatortypes_mesh(void) { ... WM_operatortype_append(MESH_OT_subdivide); ... }
我们需要确保WindowManager将调用此注册函数。为此,每个操作类别都有一个函数将注册函数放入其中。
Poll
poll回调需要验证要运行操作的正确上下文是否有效。通常,许多操作将使用相同的poll回调。本例中,我们使用由大多数网格编辑操作使用的ED_operator_editmesh函数。
int ED_operator_editmesh(bContext *C) { Object *obedit = CTX_data_edit_object(C); if(obedit && obedit->type == OB_MESH) return NULL != ((Mesh *)obedit->data)->edit_mesh; return 0; }
此函数从上下文中获取编辑对象,并验证它是否是网格,且edit_mesh指针是否已设置。
如果轮询函数失败,就可以给用户一个简单的警告,解释原因。
可以更改前面的示例来完成:
int ED_operator_editmesh(bContext *C) { ... CTX_wm_operator_poll_msg_set(C, "selected object isn't a mesh or not in editmode"); return 0; }
Exec
exec回调用于在没有用户交互的情况下执行操作(与典型的变换操作相反)。该函数如下所示:
static int edbm_subdivide_exec(bContext *C, wmOperator *op) { Object *obedit = CTX_data_edit_object(C); BMEditMesh *em = BKE_editmesh_from_object(obedit); const int cuts = RNA_int_get(op->ptr, "number_cuts"); float smooth = RNA_float_get(op->ptr, "smoothness"); const float fractal = RNA_float_get(op->ptr, "fractal") / 2.5f; const float along_normal = RNA_float_get(op->ptr, "fractal_along_normal"); if (RNA_boolean_get(op->ptr, "quadtri") && RNA_enum_get(op->ptr, "quadcorner") == SUBD_CORNER_STRAIGHT_CUT) { RNA_enum_set(op->ptr, "quadcorner", SUBD_CORNER_INNERVERT); } BM_mesh_esubdivide(em->bm, BM_ELEM_SELECT, smooth, SUBD_FALLOFF_LIN, false, fractal, along_normal, cuts, SUBDIV_SELECT_ORIG, RNA_enum_get(op->ptr, "quadcorner"), RNA_boolean_get(op->ptr, "quadtri"), true, false, RNA_int_get(op->ptr, "seed")); EDBM_update_generic(em, true, true); return OPERATOR_FINISHED; }
让我们从函数声明开始。
static int edbm_subdivide_exec(bContext *C, wmOperator *op)
此函数获取两个参数、从中获取数据的上下文和操作的实例。wmOperator
是当前运行的操作,并存储其状态和属性(不要与用于创建wmOperator的wmOoperatorType相混淆)。
函数返回值用于指示运算符是否成功完成或取消。
Object *obedit = CTX_data_edit_object(C);
BMEditMesh *em = BKE_editmesh_from_object(obedit);
通常,在执行操作符时,首先要做的就是从上下文中获取相关数据。在这里,我们获得了场景,编辑对象和编辑网格。
const int cuts = RNA_int_get(op->ptr, "number_cuts"); float smooth = RNA_float_get(op->ptr, "smoothness"); const float fractal = RNA_float_get(op->ptr, "fractal") / 2.5f; const float along_normal = RNA_float_get(op->ptr, "fractal_along_normal");
接下来,我们使用RNA访问器函数获得操作属性。
BM_mesh_esubdivide(...);
此函数实际上将更改编辑并执行细分。如何工作的细节与当前不相关。
EDBM_update_generic(em, true, true);
请参阅此函数的源代码。
void EDBM_update_generic(BMEditMesh *em, const bool do_tessface, const bool is_destructive) { Object *ob = em->ob; /* order of calling isn't important */ DAG_id_tag_update(ob->data, OB_RECALC_DATA); WM_main_add_notifier(NC_GEOM | ND_DATA, ob->data); if (do_tessface) { BKE_editmesh_tessface_calc(em); } if (is_destructive) { /* TODO. we may be able to remove this now! - Campbell */ // BM_mesh_elem_table_free(em->bm, BM_ALL_NOLOOP); } else { /* in debug mode double check we didn't need to recalculate */ BLI_assert(BM_mesh_elem_table_check(em->bm) == true); } /* don't keep stale derivedMesh data around, see: [#38872] */ BKE_editmesh_free_derivedmesh(em); #ifdef DEBUG { BMEditSelection *ese; for (ese = em->bm->selected.first; ese; ese = ese->next) { BLI_assert(BM_elem_flag_test(ese->ele, BM_ELEM_SELECT)); } } #endif }
执行操作后,我们需要更新依赖的图并发送通知。我们将呼叫依赖图并告诉它数据已改变,这将导致任何依赖于该网格几何体内容的,例如修饰器重新执行。
notifier调用用于更新用户界面的其他部分。在这里,我们表明我们已经改变了一个物体的几何数据。例如,3D视图将接收此notifier并请求重绘。
return OPERATOR_FINISHED;
最后,我们返回操作符已经成功完成。在其他情况下,我们可能希望返回OPERATOR_CANCELLED,以指示什么都没有做。因为我们返回OPERATOR_FINISHED,这将导致撤销推送,并意味着将注册该操作。
重新执行
这个操作可以从最后一个操作面板重新执行。这是自动实现的,因为操作有一个exec回调。对于交互式操作来说,还需要更多的服务,我们将在下面看到这一点。
3D View Zoom(3D视图绽放)
注册
void VIEW3D_OT_zoom(wmOperatorType *ot) { /* identifiers */ ot->name = "Zoom view"; ot->description = "Zoom in/out in the view."; ot->idname = "VIEW3D_OT_zoom"; /* api callbacks */ ot->invoke = viewzoom_invoke; ot->exec = viewzoom_exec; ot->modal = viewzoom_modal; ot->poll = ED_operator_view3d_active; /* flags */ ot->flag = OPTYPE_BLOCKING; /* properties */ RNA_def_int(ot->srna, "delta", 0, INT_MIN, INT_MAX, "Delta", "", INT_MIN, INT_MAX); }
这与网格细分操作非常相似,但我们将讨论两个不同之处。
/* api callbacks */ ot->invoke = viewzoom_invoke; ot->exec = viewzoom_exec; ot->modal = viewzoom_modal; ot->poll = ED_operator_view3d_active;
除了exec和poll回调之外,这个操作符还具有invoke和modal回调。这些是用来使操作符交互,对像鼠标移动这样的事件作出反应。我们稍后再讨论这些问题。
/* flags */ ot->flag = OPTYPE_BLOCKING;
flag是不同的。我们不希望在历史堆栈中注册这个操作,也不希望它导致撤销推送。OPTYPE_BLOCKING标志指示这个操作应该捕获所有鼠标移动,即使它超出了窗口。
Poll
int ED_operator_view3d_active(bContext *C) { if(ED_operator_areaactive(C)) { SpaceLink *sl = (SpaceLink *)CTX_wm_space_data(C); return sl && (sl->spacetype == SPACE_VIEW3D); } return 0; }
这里的轮询回调不测试数据,但确保我们处于正确的空间类型,因为这是我们将要编辑的内容。
Invoke
static int viewzoom_invoke(bContext *C, wmOperator *op, wmEvent *event) { if(RNA_property_is_set(op->ptr, "delta")) { return viewzoom_exec(C, op); } else { /* makes op->customdata */ viewops_data(C, op, event); /* add temp handler */ WM_event_add_modal_handler(C, &CTX_wm_window(C)->handlers, op); return OPERATOR_RUNNING_MODAL; } }
invoke函数在运行时由用户调用,如果它不存在则使用exec。
static int viewzoom_invoke(bContext *C, wmOperator *op, wmEvent *event)
与exec回调相较不同之处在于事件。例如,这是导致调用操作的事件,它可以用来获取鼠标坐标。
if(RNA_property_is_set(op->ptr, "delta")) { return viewzoom_exec(C, op); }
首先,如果已经设置了所有属性,则操作员尝试执行exec。这不是必需的行为,但在某些情况下可能很方便。
else { /* makes op->customdata */ viewops_data(C, op, event);
否则,我们将开始一个modal操作。使用事件当前鼠标的位置,初始状态将被保存在OP -> customdata。这是一个可以用来存储任何数据的void *属性,用来存储操作时间。存放具体数据的细节在这里并不重要。
/* add temp handler */ WM_event_add_modal_handler(C, &CTX_wm_window(C)->handlers, op);
接下来,我们将自身注册为窗口级别的modal处理器。这意味着此窗口中的所有事件都将首先通过该操作,从而阻止所有其他事件处理器。
return OPERATOR_RUNNING_MODAL; }
最后,我们标示操作现在正在运行modal,因此尚未完成。
Modal
static int viewzoom_modal(bContext *C, wmOperator *op, wmEvent *event) { ViewOpsData *vod = op->customdata; /* execute the events */ switch(event->type) { case MOUSEMOVE: viewzoom_apply(vod, event->x, event->y); break; default: /* origkey may be zero when invoked from a button */ if(ELEM3(event->type, ESCKEY, LEFTMOUSE, RIGHTMOUSE) || (event->type==vod->origkey && event->val==0)) { request_depth_update(CTX_wm_region_view3d(C)); MEM_freeN(vod); op->customdata = NULL; return OPERATOR_FINISHED; } } return OPERATOR_RUNNING_MODAL; }
modal回调可在任何事件上调用,然后我们可以决定是否处理。
ViewOpsData *vod = op->customdata;
首先,我们获取invoke中创建customdata。在其他方面,这是用来获取原始的鼠标位置,以便我们知道鼠标如何移动的。
/* execute the events */ switch(event->type) { case MOUSEMOVE: viewzoom_apply(vod, event->x, event->y); break;
接下来,我们将寻找感兴趣的事件。如果鼠标移动,我们将传递鼠标坐标并应用缩放。函数的内部运作在这里也与我们无关。
default: /* origkey may be zero when invoked from a button */ if(ELEM3(event->type, ESCKEY, LEFTMOUSE, RIGHTMOUSE) || (event->type==vod->origkey && event->val==0)) {
这一行检查事件以停止操作。退出时,鼠标左键和右键都会取消。另外,释放我们最初按下的键(如果操作被绑在键盘上而不是鼠标上),将停止操作。
request_depth_update(CTX_wm_region_view3d(C));
MEM_freeN(vod);
op->customdata = NULL;
我们请求3D视图更新,因为我们改变了它。我们也需要释放我们临时储存的customdata。
return OPERATOR_FINISHED; }
标示此修饰器已完成操作,其处理器现在可移除。
return OPERATOR_RUNNING_MODAL;
如果操作尚未完成,则执行此行,标示我们要继续接收事件。
Exec
static int viewzoom_exec(bContext *C, wmOperator *op) { View3D *v3d = CTX_wm_view3d(C); RegionView3D *rv3d = CTX_wm_region_view3d(C); int delta = RNA_int_get(op->ptr, "delta"); ... request_depth_update(CTX_wm_region_view3d(C)); ED_region_tag_redraw(CTX_wm_region(C)); return OPERATOR_FINISHED; }
这很类似网格细分exec。我们从上下文中获取一些数据,获得操作属性。接着我们执行操作,然后发出一些信号来更新和重绘。
如果我们希望操作是可重复的,我们需要在invokel回调实现后,接着实现exec回调回,如果不能,我们可以把它放到一边。注意,modal回调应该在完成操作时设置delta(在我们的例子中,它在每次鼠标移动中设置它),这样重复执行可以使用它来缩放相同的数量。