Dev:2.5/Source/Architecture/Operators/Tutorial

提供: wiki
移動先: 案内検索

Line-by-line explanations for how operators work. First mesh subdivide is explained, a relatively simple operator. Next we'll explain a more complicated modal operator, 3d view zoom.

Mesh Subdivide

Registration

The first thing we have to do is register the operator type with the window manager. For this we define a function to be called by the window manager on startup.

void MESH_OT_subdivide(wmOperatorType *ot)
{
    PropertyRNA *prop;

    /* identifiers */
    ot->name = "Subdivide";
    ot->description = "Subdivide selected edges";
    ot->idname = "MESH_OT_subdivide";

    /* api callbacks */
    ot->exec = edbm_subdivide_exec;
    ot->poll = ED_operator_editmesh;

    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

    /* 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);
}

Let's start with the first line:

void MESH_OT_subdivide(wmOperatorType *ot)

MESH defines the operator category, and _OT_ (Operator Type) is a standard part of the operator ID name. The purpose of the function is to fill in the wmOperatorType.

    /* identifiers */
    ot->name = "Subdivide";
    ot->description = "Subdivide selected edges";
    ot->idname = "MESH_OT_subdivide";

The ot->name value indicates the string that will be used in the user interface, it is the human readable name of the operator. The description is used for tooltips. The idname should be the same name as the function, it's the unique identifier for this operator.

    /* api callbacks */
    ot->exec = edbm_subdivide_exec;
    ot->poll = ED_operator_editmesh;

API callback functions define how the operator actually runs. The poll callback will be run to test if the operator can be executed, while the exec callback will actually execute the operator. We'll go into more detail on these later.

    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

Operator flags gives the window manager information on how to use the operator. Here, OPTYPE_REGISTER means that the operator should be registered in the history stack. 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);

Operators can define a number of properties. These can then be set by the user, and used by the operator to modify its behavior. These are RNA properties, so for more information on how to define them, see the RNA documentation. In this case we'll simple define an integer indicating the number of cuts.

WM

void ED_operatortypes_mesh(void)
{
    ...
    WM_operatortype_append(MESH_OT_subdivide);
    ...
}

We need to ensure the windowmanager will call this registration function. For this, each operator category has a function to put the registration functions in.

Poll

The poll callback needs to verify the right context is available for the operator to run. Usually many operators will use the same poll callback. In this case we use the ED_operator_editmesh function which is used by most mesh editing operators.

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;
}

This functions get the edit object from the context, and verifies that it is a mesh, and that the edit_mesh pointer is set.

If the poll function fails, it's possible to give the user a simple warning that explains why.

This can be done changing the previous example:

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

The exec callback is used to execute operators without user interaction (as opposed to e.g. a typical transform operator). The function looks like this:

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;
}

Let's start with the function declaration.

static int edbm_subdivide_exec(bContext *C, wmOperator *op)

This functions gets two arguments, the context to get data from, and an instance of the operator. wmOperator is the operator that is currently running and stores its state and properties (not to be confused with the wmOperatorType which is used to create the wmOperator).

The function return value is used to indicate if the operator finished successfully or canceled.

    Object *obedit = CTX_data_edit_object(C);
    BMEditMesh *em = BKE_editmesh_from_object(obedit);

Typically the first thing to do on operator execution is get the relevant data from the context. Here we obtain the scene, edit object and edit mesh.

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

Next we get the operator properties using the RNA accessor functions.

    BM_mesh_esubdivide(...);

This function will actually change the editmesh and perform the subdivision. The specifics of how this works are not relevant here.

    EDBM_update_generic(em, true, true);

See the source for this function.

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
}

After performing the operation, we need to update the dependency graph and send a notifier. We call the dependency graph saying the object's data has changed, which will cause for example modifiers to be re-executed, or anything else that depends on the mesh geometry.

The notifier call is used to update other parts of the user interface. Here we indicate that we have changed an object's geometry data. For example 3D views will receive this notifier and request a redraw.

    return OPERATOR_FINISHED;

Lastly, we return that the operator has finished successfully. In other cases we may want to return OPERATOR_CANCELLED, to indicate that nothing was done. Since we return OPERATOR_FINISHED, this will cause an undo push, and means the operator will be registered.

Re-Execution

This operator can be re-executed from the last operator panel. This is automatically possible because the operator has an exec callback. For interactive operators some more is needed, as we will see next.

3D View Zoom

Registration

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

This is quite similar to the mesh subdivide operator, but we will discuss two differences.

    /* api callbacks */
    ot->invoke = viewzoom_invoke;
    ot->exec = viewzoom_exec;
    ot->modal = viewzoom_modal;
    ot->poll = ED_operator_view3d_active;

Next to the exec and poll callbacks, this operator also has invoke and modal callbacks. These are used to make the operator interactive, reacting to events like mouse move. We'll discuss these further later.

    /* flags */
    ot->flag = OPTYPE_BLOCKING;

The flags are also different. We do not want to register this operator in the history stack, nor do we want it to cause an undo push. The OPTYPE_BLOCKING flag indicates that this operator should capture all mouse moves, even if it goes outside the window.

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;
}

The poll callback here does not test for data, but ensure we are in the right space type, since that is what we will be editing.

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;
    }
}

The invoke function is called when the operator is run by the user, if it does not exist exec will be used.

static int viewzoom_invoke(bContext *C, wmOperator *op, wmEvent *event)

What's different here compared to an exec callback is the event. This is the event that caused the operator to be invoked, which can be used to get the mouse coordinates for example.

    if(RNA_property_is_set(op->ptr, "delta")) {
        return viewzoom_exec(C, op);
    }

First the operator tries to exec if all properties are already set. This is not required behavior, but may be convenient in some cases.

    else {
        /* makes op->customdata */
        viewops_data(C, op, event);

Otherwise, we will start this as a modal operator. Using the current mouse location from event, the initial state will be saved in op->customdata. This is a void* property that can be used to store any data for the duration of the operator. The specifics of what is stored here are not important.

        /* add temp handler */
        WM_event_add_modal_handler(C, &CTX_wm_window(C)->handlers, op);

Next we register ourselfs as a modal handler at the window level. This means that all events in this window will first go through this operator, blocking all other event handlers.

        return OPERATOR_RUNNING_MODAL;
    }

Lastly, we indicate that the operator is now running modal, and hence not finished yet.

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;
}

The modal callback will be called on any event, which we can then decide to handle or not.

    ViewOpsData *vod = op->customdata;

First we retrieve the customdata that we created in invoke. Among other things, this is used to get the original mouse position so that we know how the mouse has moved.

    /* execute the events */
    switch(event->type) {
        case MOUSEMOVE:
            viewzoom_apply(vod, event->x, event->y);
            break;

Next we look for interesting events. In case of mouse move, we will pass along the mouse coordinates and apply the zoom. The internal working of this function again is not relevant here.

        default:
            /* origkey may be zero when invoked from a button */
            if(ELEM3(event->type, ESCKEY, LEFTMOUSE, RIGHTMOUSE) || (event->type==vod->origkey && event->val==0)) {

This line checks for events to stop the operator. Escape, left mouse and right mouse will always cancel. Additionally releasing the key we originally pressed (if the operator was tied to the keyboard instead of the mouse) , will stop the operator.

                request_depth_update(CTX_wm_region_view3d(C));

                MEM_freeN(vod);
                op->customdata = NULL;

We request some updates in the 3d view since we changed it. We also need to free the customdata that we stored temporarily.

                return OPERATOR_FINISHED;
            }

Indicate that this modifier has finished operation, and it's handlers can now be removed.

    return OPERATOR_RUNNING_MODAL;

This line gets executed if the operator is not finished yet, indicating we want to continue receiving events.

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;
}

This works quite similar to mesh subdivide exec. We get some data from the context, get the operator properties. Then we do the operator, and afterwards sends some signals to update and redraw things.

If we want the operator to be repeatable, we need to implement this exec callback next to the invoke callback, if not we can leave it out. Note that the modal callback should set delta by the time it has finished operation (in our case it sets it on each mouse move), so that a repeated execution can use it to zoom by the same amount.