モーショングラフィックスソフト9VAeきゅうべえに点の結合機能を追加する

[9VAeきゅうべえ](https://qiita.com/danjiro/items/253e5a33a38599098274)は、[SVGイラストから動画素材を作成できる](https://dnjiro.hatenablog.com/entry/2020/02/03/144517)モーショングラフィックスエディタです。

9VAeの次のバージョン（0.7.2）では「点の結合」という機能が追加されます。ここでは、グラフィックスエディタにどのようにして新機能を追加していくかを紹介します。

グラフィックエディタの中身を解説した記事は少ないので、参考になるかも。

![0702b.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67734/6c14da52-2e84-dcaf-0ecd-7096d0ee30c0.gif)


#  [9VAeきゅうべえとは](https://qiita.com/danjiro/items/253e5a33a38599098274)

- SVGイラストを読み込んで修正し、絵を描かずにアニメが作れるベクトルグラフィックスエディタです。
- 動画のデータサイズが非常に小さく、ラズベリーパイのSDカードに収まります。
- いろんな環境で動作します。商業利用可能、広告なし、出力にロゴがつかないフリーソフトです。

# 「点の結合」機能とは

以下の問題を解決する機能です

- 髪の毛を変形するとき、髪の毛の太さを一定に保って変形するのがめんどうでした。髪の毛の点の太さを表現するために設定した２点の距離を一定に保つ機能がほしい。
- 立方体を描いたとき、各面の頂点の位置が同じなので、いろんな移動、変形を行ったとき、同じ頂点はつねに同じ位置に動いてほしい。
- 手足などのパーツをアニメキャストにして動かそうとした場合、アニメキャストが極座標でキーフレーム補間されるため、各ページでアニメキャストの端を重ねても、中間の状態で、アニメキャストがばらばらに動いてしまうことがあります。つながっている点を指定し、その点が離れないようにキーフレーム補間できるようにしたい。（上のアニメーションの腕が動く例）
- アニメキャストを極座標でキーフレーム補間しているのは正確に回転できるようにするためで、アニメキャストをバラバラに動かす場合はそれでよいのですが、複数のアニメキャストをつないで動かすには、結合情報をいれないとうまく動きません。


# 「点の結合」のユーザーインタフェース

## メニュー項目の追加

点選択モードで結合したい点を選ぶ方法と、重なった図形を選択し、同じ点を結合する方法の２つ、および、それらを解除する方法の合計４つの操作を追加します。


- 点選択モードで、結合していない点を選び、選択枠中心メニューから「点の結合」をクリックすれば、選んだ点が結合される。
- 点選択モードで、結合された点だけを選べば、選択枠中心メニューが「点の結合解除」になり、クリックすれば、選んだ点の結合が解除される。
- 通常選択モードで、重なった図形を選び、選択枠中心メニューから「同じ点の結合」をクリックすれば、座標が同じ点が結合される。
- 通常選択モードで、重なった図形を選び、座標が同じ点がすべて結合されていた場合、選択枠中心メニューが「同じ点の結合解除」になり、クリックすれば、座標が同じ点の結合が解除される。

メニューに１項目追加するだけなので簡単です。


## 結合された点の操作

結合された点がいっしょに動くのはよいとして、結合した点の位置関係を修正したいこともあります。これが簡単にできるようにしたいので、１点のドラッグ動作だけ、結合を無視してその点だけ移動できるようにしました。

- 通常選択モードで、結合された点を含む図形を移動、変形すれば、選択されていない図形に含まれる結合された点がすべて、選択された図形の結合された点と同じように移動、変形する。
- 点選択モードで、結合された点を選択すれば、結合された点がすべて選択される。選ばれた点を移動、変形すれば、選択されていない図形に含まれる結合された点がすべて、選択された図形の結合された点と同じように移動、変形する。
- 例外的に点選択モードで、結合された点１点をドラッグして移動すれば、その点だけ移動し、他の結合した点は移動しない。結合された点の位置関係を修正するための動作。
- 点選択モードで、１点だけ選択し、その後、その点をクリックして移動した場合は、結合した点も移動する。
- 点の連続修正の場合、結合された点を移動すれば、他の結合された点もすべて同じように移動する。



# 「点の結合」情報を記憶する方法

動作仕様は上でよいとして、点の結合情報をどう記憶するか考えました。条件として次のことを考えました。

- 旧バージョンの9VAeで読み込んでも矛盾がおこらないこと
- この機能を簡単に取り外しできること
- 結合できる点の数に制限がないこと
- 構造が簡単であること

以上を考えて、次のようにしました。

- 結合情報を記憶する新しいオブジェクト anObBnd を定義する
- anObBnd は各ページに０個、もしくは１個存在する
- anObBnd は結合した点の情報（anBond）を可変長配列に記憶する

```c
typedef struct {      // 結合点
    struct anObjct_ *bob; //図形オブジェクト（ポリゴンかアニメキャスト）
    short       bop;  //点の番号
    short       boWid; //0以上：距離　-1:続く　-2:終了
    short       flg; //編集用
} anBond;

typedef struct {	 // 点の結合データを記憶するオブジェクト
    short       bndNo;     // 結合点の数
    anBond      bnz[];     // 結合点の配列
} anObBnd;
```
結合情報は１次元配列に記憶されます。その中でどの点が結合されているかを区別するために、結合点（anBond） のメンバー（boWid）を利用します。

- 結合点のboWidは、結合点の広がりを記録するものです。結合点が全く同一点になければいけないのか、ある程度の範囲におさまっていればよいのか自由度がもたせられるように結合点の範囲の数値を記憶します。
- 次の結合点は、同じグループに属する場合、boWidを、-1 にします。
- 同じグループに属する最後の結合点は、boWidを、-2 にします。
- 結合点が１つしかない場合、そのデータは削除します。結合点は必ず２点以上の状態を保ちます。
- オブジェクトの中の点は、点の番号で指定します。

図形の中の点が結合されているかどうかは、そのページに存在する、anObBndの結合点情報を順番にみて、リストにはいっていた場合、その前後、boWidが０以上　から -2 までの範囲の点と結合していることになります。

旧バージョンの9VAeで読み込んだ場合、新しいオブジェクトanObBndが無視されます。その他の情報は旧バージョンと同じなので、アニメキャストの結合以外、ほぼ正しく再生されます。



# 点の結合ができるオブジェクト

9VAeで内部に点を持つオブジェクトには以下のものがあります。その中で、ポリゴンとアニメキャストの制御点、アニメキャストの動き矢印の点だけ結合できることにしました。

|種類|結合できる点|
|:--:|:--:|
|ポリゴン（塗りあり）|すべての制御点、座標点|
|ポリゴン（塗りなし）|すべての制御点、座標点|
|ポリゴン（穴）|すべての制御点、座標点|
|アニメキャスト（動き矢印なし）|すべての制御点、動き矢印の点|
|アニメキャスト（動き矢印つき）|すべての制御点、動き矢印の点|
|文字（グラデーション背景なし）|角、辺、文字中心の９点|
|文字（グラデーション背景あり）|角、辺、文字中心の９点|
|動きグラフ|なし|
|背景スクロール|なし|
|タイル|なし|

# アルゴリズム

## 図形選択

- 図形選択したあと、選択した図形に含まれる結合点を調べる（anExpandBondSel）
    - 結合した点すべてに結合点選択フラグ（AnSELBedt）をつける。再起的処理。
    - 選択されていない図形の内部点にも結合点選択フラグ（AnSELBedt）がつく

## 点選択

- 点選択したあと、選択した結合点を調べる（anExpandBondSel）
    - 結合した点すべてに結合点選択フラグ（AnSELBedt）をつける。再起的処理。
    - 選択された図形の結合点には選択フラグ（AnSELedt）もつける
    - 選択されていない図形の内部点にも結合点選択フラグ（AnSELBedt）がつく

## 図形移動

- 重なり順序の下から図形ごとに移動、描画処理を行う
- 選択されていない図形の点に結合点選択フラグ（AnSELBedt）がついていた場合、その図形は点選択移動モードで、結合点選択フラグがついた点だけ移動させる

## 点移動

- 重なり順序の下から図形ごとに移動、描画処理を行う
- 選択された図形の結合点はすべて選ばれるので、同時に移動する
- 選択されていない図形の点に結合点選択フラグ（AnSELBedt）がついていた場合、その図形は点選択移動モードで、結合点選択フラグがついた点だけ移動させる

## アニメキャストの再生（キーフレーム補間）

- キーフレーム補間する２つのオブジェクトと混ぜる割合が入力される
- アニメキャストの枠の形がきまったあと、結合点による補正処理（anMoveBondAnmCast）を行う
    - 結合した点を登録された順番に調べる（結合した時の重ね順序、点の番号の順番にならんでいる）
    - 先頭の結合点が自分ならアニメキャスト以外の結合点がないか探し、あればそれを先頭の点にする。見つからなければ何もしない。（アニメキャストしか結合していない場合、最初のアニメキャストは自由に動く。あとのアニメキャストは先頭のアニメキャストの動きに結合する。アニメキャスト以外の図形と結合していれば最初の図形に結合する）
    - 先頭の結合点から、キーフレーム１、キーフレーム２の位置を計算し、その移動距離を記憶する
    - 先頭以外のアニメキャストの点は、キーフレーム１の距離に記憶した移動距離を加えた位置に変更する
    - アニメキャストの結合点の補正が完了したら、アニメキャストの内部の極座標を再計算（anSetAnimRct）する

## 同じ点の結合

- 複数の図形を選択した状態で、同じ位置にある結合していない点があるか調べる（anXstSameBondPnt）
- 同じ位置にある結合していない点を数える。結合された点のペアは数えない（anCountSameBondPnt）
- 同じ位置にある結合していない点があるのに、結合可能な点が０の場合（相手が結合された点の場合）同じ位置の点の結合を全部解除し、もう一度、結合可能な点の数を数える（anCountSameBondPnt）
- 結合可能な点が２以上の場合、結合情報（anBond bnz[]）に加える点のリストを作成（anBondPoints）
- 結合情報の追加の実行（unAddBondPg）

## 同じ点の結合解除

- 複数の図形を選択した状態で、同じ位置にある点を調べる（anXstSameBondPnt）
- 同じ位置にある点が全て結合している場合、結合解除の処理になる
- 同じ位置にある結合している点に、フラグ（AnUSEedt）をつける（anCountSameBondPnt）
- ページの結合情報の中にフラグ（AnUSEedt）と同じ点があれば結合情報（anBond）に削除マーク（-1）をつける（anBondPoints）
- 結合情報削除の実行（unDelBondPoint）
    - 削除フラグのついた結合情報（anBond）を削除する。その結果、先頭、最後のマーク（boWid）を修正する
    - 点を削除した結果、１点しかのこらない場合、その結合情報も削除する


## 選んだ点の結合

- 

## 選んだ点の結合解除

- 

## 結合点の処理で注意すべき点

- 点を追加、削除すると、追加、削除した点の位置によって点の番号がずれる
- アニメキャストは続きのページ作成、前のページ作成、前後のページへのコピーでは動き矢印がなくなる。動き矢印の結合点を消去しなければならない。
- 結合点は最低２点の組み合わせ。１点しかない結合点は消す。
- 結合点を記憶するオブジェクトは各ページに１つである。図形をコピーしたとき、結合点の情報を１つにまとめる必要がある。


# 「点の結合」が関連する処理

これで、データ構造と動作仕様がきまりました。あとは、9VAeの既存の処理の中で、結合情報が正しく保たれるように処理を書いていきます。たとえば、結合する点は、点の番号で指定しているため、その図形の点を削除したり、追加したりすると、結合情報の点の番号を変更する必要があります。また、図形を複製、削除した場合、結合情報も複製、削除する必要があります。これらをすべて矛盾なく動作させるのは、かなり複雑な処理になります。

どのような処理が関連するか書き出しました。扱っているデータ、操作履歴にも依存するため関連する処理を全て書き出すのは難しく、たいてい抜けが出て、あとからバグとして発見されます。

まず、結合情報の表示処理を作成、次にデータを保存、読み出しする機能を作成し、テストデータ、バグデータが保存、再現できるようにします。そのあと、それぞれの機能を実装していきます。

|処理|複数ページ|動作|実装状況|
|:--:|:--|:--|:--:|
|点の結合表示||点選択表示で結合した点を○で囲む|○|
|点の結合表示（連続修正）||移動中の結合した点を●で囲む|○|
|点の結合（同じ図形内）|○|２点以上選ばれていたら結合する|○|
|点の結合（別の図形）|○|同上|○|
|点の結合（異なる種類の図形）|○|同上|○|
|点の結合（結合されていない点と結合された点が混在）|○|結合されていない点が１点含まれていれば結合する。ただし結合解除した直後に、結合した点がまだ選ばれている場合、すべての結合点が選ばれていれば、その結合解除が優先する|○|
|点の結合（異なる結合に含まれる結合点）|○|結合された点だけが選ばれていても、同じ結合でない結合点であれば結合する|○|
|点の結合解除（同じ図形内）|○|あるページで選んだ点が全部結合されていたら結合解除する。１点だけになった結合点は解除する。|○|
|点の結合解除（別の図形）|○|同上|○|
|点の結合解除（異なる種類の図形）|○|同上|○|
|点の結合解除（結合解除した直後）|○|結合解除した直後に、結合した点がまだ選ばれている場合、すべての結合点が選ばれていれば、その結合点を解除する。全部の結合点が選ばれていない、かつ結合されていない点が選ばれている場合は結合する。|○|
|同じ点の結合（同じ点がない）|○|同じ点が１つもなければ実行できない|○|
|同じ点の結合（同じ点は１つも結合していない、文字が下）|○|結合されていない同じ点が１つでもあれば結合する|○|
|同じ点の結合（同じ点は１つも結合していない、文字が上）|○|結合されていない同じ点が１つでもあれば結合する|○|
|同じ点の結合（結合している同じ点がある）|○|結合されていない同じ点が１つでもあれば結合する。すでに結合されている点は結合されない。|○|
|同じ点の結合解除（同じ点が全部結合されている）|○|同じ点の結合がすべて解除される|○|
|１点のドラッグ中||１点だけドラッグで移動する（文字の結合点は移動しない）|○|
|１点のドラッグ後||１点だけ移動する（文字の結合点は移動しない）|○|
|複数点のドラッグ移動中|○|選択された複数点が移動する（文字の結合点は移動しない）|○|
|複数点のドラッグ移動後|○|選択された複数点が移動する（文字の結合点は移動しない）|○|
|複数点のドラッグ変形中|○|選択された複数点が変形する（文字の結合点は移動しない）|○|
|複数点のドラッグ変形後|○|選択された複数点が変形する（文字の結合点は移動しない）|○|
|１点の選択（クリック）||クリックした点と結合された点が全部選ばれて移動（文字の結合点は移動しない）|○|
|１点の選択（四角形）||選択された点と結合された点が全部選ばれる|○|
|１点の選択（Ctrlで囲む）||選択された点と結合された点が全部選ばれる|○|
|複数点の選択（四角形）||選択された点と結合された点が全部選ばれる|○|
|複数点の選択（Ctrlで囲む）||選択された点と結合された点が全部選ばれる|○|
|図形のドラッグ移動中|○|選ばれていない図形（文字を除く）の結合された点もいっしょに移動|○|
|図形のドラッグ移動後|○|選ばれていない図形（文字を除く）の結合された点もいっしょに移動|○|
|図形のドラッグ変形中|○|選ばれていない図形（文字を除く）の結合された点もいっしょに変形|○|
|図形のドラッグ変形後|○|選ばれていない図形（文字を除く）の結合された点もいっしょに変形|○|
|図形のクリック移動中|○|選ばれていない図形（文字を除く）の結合された点もいっしょに移動|○|
|図形のクリック移動後|○|選ばれていない図形（文字を除く）の結合された点もいっしょに移動|○|
|図形のクリック変形中|○|選ばれていない図形（文字を除く）の結合された点もいっしょに変形|○|
|図形のクリック変形後|○|選ばれていない図形（文字を除く）の結合された点もいっしょに変形|○|
|点の連続修正||結合された点（文字を除く）もいっしょに移動|○|
|点の追加（ポリゴン）|○|結合点の場所がずれない|○|
|点の追加（ポリゴンの穴）|○|結合点の場所がずれない|○|
|点の追加（動き矢印）||結合点の場所がずれない|○|
|複数点の削除（ポリゴン）|○|結合点の場所がずれない、１点だけの結合は削除|○|
|複数点の削除（ポリゴンの穴）|○|結合点の場所がずれない、１点だけの結合は削除|○|
|複数点の削除（動き矢印）||結合点の場所がずれない、１点だけの結合は削除|○|
|点を削除して穴の結合|○|結合点は解除される|○|
|穴の反転|○|結合点の場所がずれない|○|
|図形削除（ポリゴン）|○|削除した図形の結合を削除、１点だけの結合は削除|○|
|図形削除（ポリゴンの穴）|○|削除した図形の結合を削除、１点だけの結合は削除|○|
|図形削除（アニメキャスト）|○|削除した図形の結合を削除、１点だけの結合は削除|○|
|図形削除（文字）|○|削除した図形の結合を削除、１点だけの結合は削除|○|
|文字の内容変更（１行）|○|文字枠との結合が保たれる|○|
|文字の内容変更（１行、現在ページのみ）|○|文字枠との結合が保たれる|○|
|文字の内容変更（複数行）|○|文字枠との結合が保たれる|○|
|文字の内容変更（複数行、現在ページのみ）||文字枠との結合が保たれる|○|
|文字の内容変更（空文字削除）|○|削除した文字の結合を削除、１点だけの結合は削除|○|
|文字の内容変更（空文字削除、現在ページのみ）||削除した文字の結合を削除、１点だけの結合は削除|○|
|文字枠のサイズ変更|○|文字枠との結合が保たれる|○|
|アニメキャスト別のアニメにして修正|○|結合点が保たれる|○|
|アニメキャスト別のアニメにして修正（現在ページのみ）|○|結合点が保たれる|○|
|アニメキャスト入力置換|○|結合点が保たれる|○|
|アニメキャスト入力置換（現在ページのみ）||この機能はドロップ|X|
|アニメキャストネットから入力置換|○|||
|アニメキャストネットから入力置換（現在ページのみ）||この機能はドロップ|X|
|アニメキャスト9VA入力置換|○|結合点が保たれる|○|
|アニメキャスト9VA入力置換（現在ページのみ）||結合点が保たれる|○|
|以下の処理を穴あきポリゴンP、動き矢印つきアニメキャストA、文字でチェックT|||PAT|
|続きのページを作る||結合点が保たれる、アニメキャストの矢印はコピーされない|○○○|
|前のページを作る||結合点が保たれる、アニメキャストの矢印はコピーされない|○○○|
|前のページにコピー（上書きあり）||結合点が保たれる、アニメキャストの矢印はコピーされない|○○○|
|前のページにコピー（上書きなし）||結合点が保たれる、アニメキャストの矢印はコピーされない|○○○|
|後ろのページにコピー（上書きあり）||結合点が保たれる、アニメキャストの矢印はコピーされない|○○○|
|後ろのページにコピー（上書きなし）||結合点が保たれる、アニメキャストの矢印はコピーされない|○○○|
|記憶ツールにコピー|○|結合点が保たれる|○○○|
|記憶ツールにカット|○|結合点が保たれる|○○○|
|記憶ツールから上書き|○|結合点が保たれる|○○○|
|記憶ツールから複製|○|結合点が保たれる|○○○|
|ページを記憶する|○|結合点が保たれる|○○○|
|ページを切り取る|○|結合点が保たれる|○○○|
|記憶したページを入れる|○|結合点が保たれる|○○○|
|記憶ツールからアニメキャスト作成|○|アニメキャストの中の結合点が保たれる|○○○|
|記憶ツールから点を置き換え（正順）|○|置き換え点の結合は解除される。置き換えてない点の結合が保たれる|○|
|記憶ツールから点を置き換え（逆順）|○|置き換え点の結合は解除される。置き換えてない点の結合が保たれる|○|
|中間ページの作成||アニメキャストのゴーストは結合を反映した形になる|○|
|あいだの形にする（後ろの図形をゴーストにする）|○|アニメキャストのゴーストは結合を反映した形になる|○|
|あいだの形にする（前の図形をゴーストにする）|○|アニメキャストのゴーストは結合を反映した形になる|○|
|あいだの形にする（後ろの選択した点）|○|結合点が保たれる|○○○|
|あいだの形にする（前の選択した点）|○|結合点が保たれる|○○○|
|ゴーストの実体化(図形内部の結合点)|○|前後のページの結合点情報が追加される|○○○|
|ゴーストの実体化(別の図形の結合点)|○|前後のページの結合点情報が追加される|○○○|
|ゴーストの実体化(結合点１つだけ)|○|１点の結合点は削除される|○○○|
|図形の複製(図形内部の結合点)|○|結合点が保たれる|○○○|
|図形の複製(別の図形の結合点)|○|結合点が保たれる|○○○|
|図形の複製(結合点１つだけ)|○|１点しかない結合点は削除される|○○○|
|上書き保存|○|結合点が保たれる|○○○|
|名前をつけて保存(SVG)||||
|名前をつけて保存(EVA)||||
|名前をつけて保存(EVD)||||
|開く(SVG)||||
|開く(EVA)||||
|開く(EVD)||||
|後ろに続けて開く||||
|旧バージョンで開く||||
|旧バージョンで9VAe読み込み||||
|旧バージョンで後ろに続けて開く||||
|再生|○|アニメキャストは結合を反映して再生|○|
|再生（ここから再生）|○|アニメキャストは結合を反映して再生|○|
|再生（大きく再生）|○|アニメキャストは結合を反映して再生|○|
|GIF出力|○|アニメキャストは結合を反映して出力|○|
|APNG出力|○|アニメキャストは結合を反映して出力|○|
|APNG連番出力|○|アニメキャストは結合を反映して出力|○|
|MP4出力|○|アニメキャストは結合を反映して出力|○|
|||||


# 文字枠との結合

ブロック図を書きたい場合、点の端を文字枠の角や辺の中点に固定したい要望があります。これを実現するため、文字に吸着する場所に結合用の点を追加します。

- Ctrlキーを押しながら点を移動させると、文字枠の角、辺の中点に吸着します。
- その状態で文字と線を選んで「同じ点の結合」を実行すると結合します。
- 文字を移動させれば線の端の位置もいっしょに移動します。
- 文字を変更すると、文字枠の大きさがかわり、グラデーション制御点も再配置されます。それにしたがって、接続した線の端の位置が変わります。