Inkscape にナイフ機能を付けてみた 3/4 - 複数分割編

ひとまず複数の図形が一度に切れるようにしたいと思います。

複数図形の分割に対応する

図形を1つ分割をすると線が消えてしまい、それを使いまわしての他の図形の分割ができないので、線を削除している部分を探し、ナイフの場合には削除しないような例外的な処理を追加します。
実処理を行っている関数に例外フラグを追加し、それを立てた状態でラップしたナイフ専用の関数を作り、ナイフから呼び出している分割処理関数をそれに差し替えます。
引いた線はナイフツール側で最後に削除するようにします。

src/splivarot.cpp
...
void sp_selected_path_boolop(Inkscape::Selection *selection, SPDesktop *desktop, bool_op bop, const unsigned int verb=SP_VERB_NONE, const Glib::ustring description="", bool remain_cutter = false);
...
void
sp_selected_path_knife(Inkscape::Selection *selection, SPDesktop *desktop)
{
  sp_selected_path_boolop(selection, desktop, bool_op_cut,SP_VERB_SELECTION_CUT, _("Knife"),true);
}
...
void
sp_selected_path_boolop(Inkscape::Selection *selection, SPDesktop *desktop, bool_op bop, const unsigned int verb, const Glib::ustring description, bool remain_cutter)
{
...
  for (GSList *l = il; l != NULL; l = l->next) {
    // if this is the bottommost object,
    if (!strcmp(reinterpret_cast<SPObject *>(l->data)->getRepr()->attribute("id"), id)) {
      // delete it so that its clones don't get alerted; this object will be restored shortly, with the same id
      SP_OBJECT(l->data)->deleteObject(false);
    } else {
      // delete the object for real, so that its clones can take appropriate action
      if(!remain_cutter) { SP_OBJECT(l->data)->deleteObject(); }
    }
...
}

src/ui/tools/freehand-base.cpp

static void spdc_flush_white(FreehandBase *dc, SPCurve *gc)
{
...
  if ( c && !c->is_empty() ) {
...
    if(SP_IS_KNIFE_CONTEXT(dc)){
...
        for(GSList* obj=objects; obj; obj=obj->next){
          s->clear();
          s->add(line);
          s->add((SPObject*)obj->data);
          sp_selected_path_knife(s, s->desktop());
        }
        s->clear();
        line->deleteObject();
        g_slist_free(objects);
     }
  }
...
}

バグ改善

さて、これでひとまず目的通りの動きをするようになったのですが、図形を細かく切り刻んでいるとプログラムが落ちてしまうという問題が発生しました。
先程追加したプログラム中で、レイヤーから取得したオブジェクトリスト群の中に常にデータが空のオブジェクトが含まれるのが判明し、そのせいだと思い修正しましたがそれは関係ありませんでした。

src/ui/tools/freehand-base.cpp

NULLオブジェクトをカットしない

static void spdc_flush_white(FreehandBase *dc, SPCurve *gc)
{
...
  if ( c && !c->is_empty() ) {
...
    if(SP_IS_KNIFE_CONTEXT(dc)){
...
      for(GSList* obj=objects; obj; obj=obj->next){
        if(obj->data == NULL){ continue; }
...
    }
...
  }
...
}

コードを見ても原因が今ひとつ分からなかったので、どういう状況で落ちるのかを地道にソフトウェア上で試しながら探すことにしました。
すると、線で図形を切ると稀に線状の図形が新たに作られる時があり、それを切ると落ちるということが分かりました。

線状の図形が発生するのは、直接線がかかっていない図形に対して切断処理を行おうとすると、
図形と線の位置関係によって稀に元の図形にゴミが残るという Inkscape のバグでした。
以下は未改造の Inkscape で試したものです。
下図の図形と線での切断処理を行ったところ、
f:id:esudo:20151105021353p:plain
下図のように元の図形オブジェクト中に線状のゴミが発生しました。このゴミは正確には線ではなく、面積ゼロの閉じた図形になっています。
f:id:esudo:20151105021357p:plain

そこで面積がゼロの図形を作りナイフで切ってみると、プログラムが落ちました。どうやら直接の原因は面積がゼロの図形のようです。
今回は Inkscape に元から存在しているゴミが発生するバグの修正はひとまず見送り、これを切断すると落ちるのを修正します。

原因がわかったところでもう一度コードを見直してみると、パスの分割などを行っている sp_selected_path_boolop 関数内に面積ゼロの閉じた図形に対してのみ処理が分岐している事が判明しました。
そこで先程追加した線の保持フラグを無視して線の削除が行われていることで線の二重削除が起こり、プログラムが落ちていたのでした。
その処理内でも線を削除しないように書き換えると、ひとまず落ちずに動くようになりました。

src/splivarot.cpp

例外時も線を消さない

void
sp_selected_path_boolop(Inkscape::Selection *selection, SPDesktop *desktop, bool_op bop, const unsigned int verb, const Glib::ustring description, bool remain_cutter)
{
...
  if (res->descr_cmd.size() <= 1)
  {
    // only one command, presumably a moveto: it isn't a path
    if(!remain_cutter){
      for (GSList *l = il; l != NULL; l = l->next)
      {
        SP_OBJECT(l->data)->deleteObject();
      }
    }
    DocumentUndo::done(doc, SP_VERB_NONE, description);
    selection->clear();
    
    delete res;
    g_slist_free(il);
    return;
  }
...
}

更に、面積ゼロの図形を切ると、それに統合されていた図形も消えてしまっていたので、全ての図形で閉じた図形毎にパスを分解してから分割処理を行うように変更し、複数図形の分割は完成です。

src/ui/tools/freehand-base.cpp

パスを分割してからカット

...
#include "path-chemistry.h"
...
static void spdc_flush_white(FreehandBase *dc, SPCurve *gc)
{
...
  if ( c && !c->is_empty() ) {
...
    if(SP_IS_KNIFE_CONTEXT(dc)){
      SPObject * line = item;

      Inkscape::SelectionHelper::selectAll(dc->desktop);
      Inkscape::Selection *s = dc->selection;
      s->remove(line);
      sp_selected_path_break_apart(s->desktop(),true);

      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);
      }
      
      for(GSList* obj=objects; obj; obj=obj->next){
        if(obj->data == NULL){ continue; }
        s->clear();
        s->add(line);
        s->add((SPObject*)obj->data);
        sp_selected_path_knife(s, s->desktop(),false);
      }
      s->clear();
      line->deleteObject();
      g_slist_free(objects);
    }
  }
...
}

目標としていた機能は完成しましたが、単純に処理の組み合わせを行っているのでアンドゥ履歴がおかしくなる、設定でナイフの線に太さを持たせた場合に分割が綺麗に行われない、といった問題が残っています。
最後にこれらの問題を解決し、一つのツールとしての完成形を目指そうと思います。