Inkscape にナイフ機能を付けてみた 2/4 - 簡易ナイフ実装編

前回で変更を加えるべき箇所に検討がついたので、さくさく行きましょう。

新規ツールを作成する

新たなボタンを UI 上に付けることができたので、今度はそれに対応する新規ツールを作成します。
先述の通りナイフツールは鉛筆ツールと分割処理の組み合わせで実現できるため、鉛筆ツールをベースにして分割処理を付加することで実装したいと思います。

ということで、まずはベースとなる鉛筆ツールを丸々コピーした新規ツールを作成します。

pencil-tool.h / pencil-tool.cpp / pencil-toolbar.h / pencil-toolbar.cpp

コピペして knife-tool.h などにする。この際中身の pencil/Pencil/PENCIL という文字列も knife/Knife/KNIFE にしておく。
作成したファイルは makefile に忘れずに追記。
ツールクラスは文字列キーと自身を結びつけるために main 関数が呼ばれる前に自分のファクトリ関数を勝手に ToolFactory というシングルトンに登録してくれるのでその辺の処理は書く必要がありません。

tools-switch.h

ツール ID にナイフを追加する。

enum {
  TOOLS_INVALID,
  TOOLS_SELECT,
...
  TOOLS_FREEHAND_KNIFE,
...
};
tools-switch.cpp

ツールのキー文字列を定義。設定の読み書きやツールの切り替えに使用。

static char const *const tool_names[] = {
    NULL,
    "/tools/select",
    "/tools/nodes",
...
    "/tools/freehand/knife",
...
};

ツールの説明文を定義。

static char const *const tool_msg[] = {
    NULL,
    N_("<b>Click</b> to Select and Transform objects, <b>Drag</b> to select many objects."),
    N_("Modify selected path points (nodes) directly."),
...
    N_("Cut shapes"),
...
};
verbs.cpp

動作 ID に対し作成したツール ID を割り当てておく。

    switch (verb) {
        case SP_VERB_CONTEXT_SELECT:
            tools_switch(dt, TOOLS_SELECT);
            break;
        case SP_VERB_CONTEXT_NODE:
            tools_switch(dt, TOOLS_NODES);
            break;
    ...
        case SP_VERB_CONTEXT_KNIFE:
            tools_switch(dt, TOOLS_FREEHAND_KNIFE);
            break;
    ...
    }
toolbox.cpp

ナイフボタンのコールバック関数を knife-toolbar.h の関数に差し替える。設定用のキー文字列も。設定画面は作成していないので PREFS はそのまま。

static struct {
    gchar const *type_name;
    gchar const *data_name;
    GtkWidget *(*create_func)(SPDesktop *desktop);
    void (*prep_func)(SPDesktop *desktop, GtkActionGroup* mainActions, GObject* holder);
    gchar const *ui_name;
    gint swatch_verb_id;
    gchar const *swatch_tool;
    gchar const *swatch_tip;
} const aux_toolboxes[] = {
...
    { "/tools/freehand/knife", "knife_toolbox", 0, sp_knife_toolbox_prep,            "KnifeToolbar",
      SP_VERB_CONTEXT_PENCIL_PREFS, "/tools/freehand/knife", N_("Style of new paths created by Pencil")},
...
};
src/ui/tools/freehand-base.cpp

注意しなければならないのが、Pencil と Pen ツールが freehand-base を継承したクラスであること。
ダウンキャストが成功するかどうかで子クラスを識別するタイプの命令があるので、これをうまく書き換える必要がある。

static Glib::ustring const tool_name(FreehandBase *dc)
{
    return ( SP_IS_PEN_CONTEXT(dc)
             ? "/tools/freehand/pen"
             : (SP_IS_KNIFE_CONTEXT(dc)? "/tools/freehand/knife" : "/tools/freehand/pencil") );
}

ここまでやると新規ツールが作成、登録できたことになります。
実際には設定ダイアログもこの後作成していますが、設定ダイアログを開く動作 ID を追加、inkscape-preferences.h で鉛筆ツールの画面をコピペしてナイフの画面を作成した後、動作 ID を上記と同様の設定に適用していくのみなので割愛します。

ここまでの所感

とまあ一気にさくさく進んで新規ツールの作成ができたように見せましたが、実際はそれなりの苦労を要しました。
動作 ID、ツール ID、ツールの実体となるクラスの定義とその結びつけ方の定義の場所が散乱していることや、ID である enum を追加したらそれと同じ長さを期待されている設定配列も追加しなければいけないなどの暗黙かつ致命的なルールの存在が主な要因です。
これは単純に言うとモノとデータとメソッドが分離していて訳がわからないということなのですが、オブジェクト指向に頼らず enum と switch で全体的に頑張っているせいだと思われます。
InkscapeGUI 周りに C++ が使われていますが、元は Sodipodi という C 言語のプロジェクトから派生したソフトなので、コア部分に未だに C っぽさが残っています。

分割機能を付ける

さて次に、新しく作ったツールに対し図形の分割機能を付加します。

まずは分割機能の実装箇所を捜索します。
verbs.h で動作 ID を眺めてみると SP_VERB_SELECTION_CUT なるものが存在し、これが分割機能であると検討が付き、
更にその ID で grep をかけると、src/splivarot.h の sp_selected_path_cut 関数がその実体であることがわかります。

これをナイフが線を引き終わった際に呼び出すようにしていきます。
knife-tools.h には _handleButtonRelease という関数があり、一見直接これに命令を書き足せば済みそうですが、線を引くにはドラッグしたあと離す他に、2点をクリックして直線を引く方法もあるのでボタンを離した瞬間に線を引くのだけでは不十分で、もう少し深い位置の命令を探す必要があります。
_handleButtonRelease を眺めると、親クラスである freehand-base の spdc_concat_colors_and_flush を呼んでいるのが目に付きます。
flush という語感を頼りにその中身を見ると、更にその関数は最後に必ず同クラス内の spdc_flush_white 関数を呼んでいるのがわかります。
この中では、

SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr));

といった記述があり、実際に現在のレイヤに対して線と思われるオブジェクトを追加していることが読み取れ、線を引き終わった時の処理はここに書けば良さそうだという検討が付きます。
そこで、以下のような記述を追加します。

        if(SP_IS_KNIFE_CONTEXT(dc)){
        	SPObject * line = item;
        	GSList* objects = g_slist_alloc();
        	for(SPObject* obj = desktop->layers->currentLayer()->children; obj; obj = obj->next){
        		if(obj == line){ continue; }
        		objects = g_slist_append(objects, obj);
        	}
        	Inkscape::Selection *s = dc->selection;
        	for(GSList* obj=objects; obj; obj=obj->next){
        		s->clear();
        		s->add(line);
        		s->add((SPObject*)obj->data);
        		sp_selected_path_cut(s, s->desktop());
        	}
        	s->clear();
        	g_slist_free(objects);
        }

分割処理は GUI 上から行う際 2 個のオブジェクトを選択してから行いますが、関数として呼び出す際も同様に Selection クラスに 2 個のオブジェクトを登録したものを渡して行います。
ドキュメントに存在するオブジェクトは、destop の下の layer というものが持っています。
ここでは、引いた線 以外の layer の全オブジェクトに対し一対一の Selection を作成した後に分割処理である sp_selected_path_cut を呼び出しています。

これでひとまずは分割処理が可能になりましたが、このままでは実は複数図形に対してナイフカットを行うと落ちます。
これは一度分割処理を行うと割る側の線が消滅してしまうのに、上記の繰り返し処理ではそれを考慮せずに同じ線を複数回使おうとしているためです。

以降ではそのような問題へと対処していきます。

f:id:esudo:20151101012836p:plain