先日も書きましたが最近はEmacsの中で動く作図ツールを作っています。
ソース: misohena/el-easydraw: Embedded drawing tool for Emacs (github.com)
以前囲碁の棋譜編集ツールを作ってその時にも書きましたが、Emacsの中でこのくらいのことは出来ても罰は当たらないと思うんですよね(このくらい出来て当然だろ!の意)。
org-modeは素晴らしいツールでいろんな事が出来ますが、文書の中に別の要素を埋め込んで統一的に編集する機能はまだまだ改善の余地が沢山あると思います。(ソースコードブロックのようなテキストベースでプログラマーが誰でも喜ぶような物は充実していますけど) 特にGUI要素が全然足りません。例えば図を描くならditaaやPlantUMLなんかもありますが、やっぱりGUIで描きたくないですか? 20年以上前のWordに出来たようなことが現代の編集環境で出来ないというのはとても残念な事だと思います。(Xwidgetsが使えれば色々出来るのかもしれませんがWindowsなので未だに使ったことがありません。Cygwinで環境を整えれば使えるのかもしれませんが……)
ということでEmacsの中でシームレスに作図が出来るようにと作りました。まだまだ改善するところが沢山あって思っていた以上に難航していますが、日々テストと称していろんな図を作成して遊んでいます。
実装
画像表示(ビュー)とマウス操作(コントロール)はこれまで培ってきたEmacsでのSVGやオーバーレイ、マウスイベント処理の延長線上にあります。
- Emacs Lispで画像を表示する方法(Emacs 27.1で確認)
- Emacs Lispでポップアップメニューを出す方法(x-popup-menuの使用例)
- Emacs Lisp でマウスの動きを追跡する
- org-mode文書の中に囲碁の碁盤を埋め込む
その上で一番最初に手を付けたのは当たり判定でした。SVGで図形を表示するのは簡単ですがマウスでクリックした点と図形との当たり判定は自分で行わなければなりません。残念ながらEmacsはそこまで面倒を見てくれません。ベジェ曲線を含んだパスの判定をするにはそれなりに手間がかかりますが、これが出来なければ話になりません。幸いこの手の当たり判定やベジェ曲線については多少扱ったことがあったのですぐに実装出来ました(完全かはともかく)。それでもこういう当たり判定処理はちゃんと動くと嬉しいものですね。
編集対象となる図形データ(モデル)は、基本的にはSVGのDOMツリーです。edraw-bodyというidを持つg要素の下が編集エリアで、それ以外の所にUIに必要なもの(グリッドやアンカー点等)を配置します(もちろん出力時にはUI要素は削除します)。できるだけDOMツリーを尊重して編集対象となるデータは常にDOMツリーに持たせてそれを書き替える形にしようと思いました。しかし今となってはちょっと怪しくなっています。shapeクラスを作ってそれ経由でDOM要素を操作する形になっていますが、shapeオブジェクトが編集中のデータを一部持ってしまっています。毎回属性をparseして編集して文字列に戻すのも大変ですし(特にpathデータ)、ドラッグ移動中や選択中のポイントが必ずしも属性と一対一で対応するわけではないので困るということもありました。とはいえそういったものは一部の例外で、基本的にはshapeオブジェクトはDOMノードをラップする存在です。あ、ちなみに今回初めてeieioを使っています(これもまた色々暗中模索でした)。
そういった所を実装して割とすぐに簡単な図が描けるようになりましたが、その後の細々とした改良に沢山の時間を費やしています。
例えば矢印は手間がかかりました。SVGにはマーカーという機能があってあらかじめ定義しておいた図形(マーカー)をパスの頂点にくっつけることが出来るのですが、あらかじめ定義しておく必要がある時点で少し面倒ですし、色も含めて定義しておく必要があるので線の色が変わると定義も更新しなければなりません。重複する定義をまとめる仕組みや変更を検知して更新する仕組みが必要でした。なので中身は見た目よりもずっと面倒くさいことになっています。でもこの手のソフトを作るなら矢印は絶対に欲しいと思っていたので頑張りました。是非矢印を有効(パスを右クリックしてSet→End Marker→Arrow)にした上でstroke色を変えてみてください。矢印の色も一緒に変わるのは決して当たり前なことでは無いんです。それが証拠に線を半透明にするとボロが出ます(笑)。(SVG2ではfill="context-stroke"という指定が出来るようになって多少やりやすくなりますがlibrsvgでは最近対応したばかりでまだ手元のEmacsでは使えません。librsvgでの対応が待たれる事項は他にも沢山あります)
パスの操作はいちいち場合分けが大変で苦労しました。SVGのパスデータ(path要素のd=属性)にはやっかいなところがいくつかあって(本文末尾参照)、アンカーポイントの削除、追加、パスの分割、連結、ハンドルのあれやこれやを実装する際に悩みの種となりました。SVG仕様の細かいところまで対応する必要は無かったのかもしれませんが、将来的にどんなデータを扱うのか分かりませんので。例えば今のところ複数のサブパス(一つのd=の中に複数のパスが存在するケース)は扱えないのですが、将来的には対応したいところです。でないとドーナツ型が作れませんし()。いや、まぁ、やってやれないことも無いんですけどね……(←U字になってる)。
その後も先日紹介したカラーピッカー、プロパティエディタ、コピー&ペースト、複数選択、UNDO/REDO等々少しずつ実装していきました。前回の囲碁エディタのようにもう少し短期間で切りの良いところまでいけると思ったのですが、一つ一つの改良とテストに思っていたよりも時間がかかってしまいました。
org-modeとの連携とリンク形式、インライン画像表示、編集、エクスポート
一通り作図が出来るエディタが出来たらorg-modeとの連携部分も作らねばなりません。前回の囲碁エディタは #+begin_igo
~ #+end_igo
というスペシャルブロックを使いましたが、今回は [[edraw:]]
という独自のリンクタイプを追加することにしました。ブロックだと行の中に図を挿入できないからです。もちろんorg-modeで画像を挿入する通常の方法がリンクなのでそれにならったというのもあります。
現在サポートしているリンクの形式は次の通りです。
[[edraw:file=./example.edraw.svg]] [[edraw:data=<base64data>] [[*Example][edraw:file=./example.edraw.svg]] [[*Example][edraw:data=<base64data>]]
ファイルへ(.edraw.svg)へのリンクの他、base64によるデータ埋め込みにも対応しています。外部ファイルが必須となると途端にお手軽さが減ってしまいます。一つの文書ファイルで完結していた方が取り扱いが楽なのは間違いありません。幸いSVGはXMLなのでラスター画像よりは大きくありませんし、それをさらにgzip圧縮してからbase64エンコードしています。
外部ファイルの場合、拡張子は.edraw.svgを推奨しています。Emacs Easy Drawが扱えるのは独自のルールに従ったSVGのサブセットのみです。他のソフトが出力したSVGを編集できるわけではありませんが、Emacs Easy Drawが出力したSVGをブラウザなど他のソフトで表示することは可能です。
通常のリンク形式(file, http, https)を拡張することも考えたのですがData URI対応の経験から既存の処理と混ざると非常に面倒だと思ったのでひとまず完全に独自のリンクタイプとしました。gzipで圧縮したSVGはブラウザで直接表示できないので、どのみちエクスポート時には独自の変換処理が必要になります。将来的には.edarw.svgファイルへの通常リンク(例: [[file:./example.edraw.svg]]
)を直接編集できるようにすることも考えています。ただ、データをorgファイル内に埋め込みたいと思うならやはり独自形式の方が都合が良いと思います。
これらのリンクは edraw-org-link-image-mode
というマイナーモードによってバッファ内にインライン表示できます。org-modeの org-toggle-inline-images
に相当しますが、こちらはマッチする形式は即画像で表示します(私は通常の画像リンクも即更新するように修正して使っているのでそのやり方を踏襲しました)。
リンクまたはインライン表示された画像上で C-c C-o を押すとエディタが開きます。編集の後 C-c C-c (またはメニューからFinish Edit)で編集したSVGデータをバッファ内(ファイルリンクの場合は指しているファイル)に書き戻します。
もちろんEmacsの中で表示・編集できるだけでなく、HTMLエクスポートの際にはimgまたはsvgタグとして出力できます。以下は実際にEmacs Easy Drawで描いた図です。このブログはOrg2blogで書かれているので、手元のorg文書に埋め込まれている図がそのまま皆様の目の前に現れています。
Pathツールの使い方
PhotoshopやIllustrator等でおなじみのPathツール(ペンツール)ですが、知らない人は最初戸惑うかもしれません。なので使い方を示すアニメーションを作ってみました。
単にクリックするとその場所に点(アンカーポイント)を追加します。次々にクリックするとアンカーポイント間が直線で結ばれます。
マウスボタンを押し下げてからそのままドラッグすると曲線の制御点(ハンドル)を動かすモードになります。押した点から伸ばした方向に向かう滑らかな曲線が出来ます。
一つのアンカーポイントからは二つのハンドルが伸びています。それぞれ前の区間と後ろの区間に対する制御点なのですが、この二つがアンカーポイントを挟んで互いに180度反対側にあると(要するに三つの点が一つの直線上にあると)、そのアンカーポイントを通る線は尖った部分が無く完全に滑らかになります(その直線を接線とした曲線になります)。
もしアンカーポイントを選択してもハンドルが二つ現れない場合は、アンカーポイントを右クリックして「Make Smooth」を選んでください。ちょうど良さそうな点を計算してそこに二つのハンドルを置きます。
逆にハンドルを消したい場合は「Make Corner」を選んでください。滑らかさが消えて完全に尖った形(折れ線)になります。
片側のハンドルだけを単独で動かしたい場合は、一つのハンドルをクリックして選択状態にしてください。そのハンドルだけ単独で動くようになります。(PhotoshopやIllustratorの「切り替えツール」は今のところありません)
ちなみに選択中のハンドルやアンカー(もちろんシェイプも)は矢印キーで移動できます。S-矢印キーで10ピクセル単位、M-矢印キーで数値入力で移動します。細かい調整の際には重宝します。
現在のパスを終了して新しいパスを開始したいときは再度Pathツールを選択してください。ツールバーのボタンを押すかaキーを押すとPathツールが初期状態から始まり、次のクリックでは新しいパスシェイプと最初のアンカーポイントが作成されます。
このとき(Pathツールを選択した直後)、現在選択中のパスの端点(一番端っこのアンカーポイント)をクリックすると、そのアンカーポイントからパスを伸ばす(再開する)ことが出来ます。
パスを伸ばしているときに既存のパスの端点をクリックすると、現在のパスをその端点を持つパスと連結します。
クリックで既存のアンカーと繋げたくない場合は、 C-u + クリック
で確実に新しいアンカーポイントを追加できます。
操作方法の問題点について
他にも操作には色々注意点がありますが、とりあえず運用でカバーしつつ実用的な図がかけるところまでは出来たのではないかと思います。
完全に私の好みに合わせて作っているので他の方には使いづらい所もあるかとは思いますが、その辺りはご了承ください。
うまく自分の好みに合わせられず使いづらいところもありますが、その辺りは自分の実力が足りないのが悪いのだと諦められるので納得しやすいところではあります(笑)。
細かいところの改善は限りが無く時間は有限なので手を付けていないところも沢山あります。
無限の改善点の狭間で
まだまだ改善点は尽きません。思いついた物はtodo.orgに書き留めています。
一時期は一つ直していくそばから沢山の修正点が見つかる状況でしたが、それも少し落ち着いてきました。
この手のツールは作っていけば行くほど次第に労多くして功少なしな事項ばかりが残っていくのが常です。
本当に切りがないので、このあたりでひとまず開発のペースを緩めようと思います。元々UNDOを実装するところまでは一気に作ろうと思っていてそれが出来たので。それに秋山の紅葉が私を呼んでいますので。
SVGは本当に表現力があって色々出来るので皆さんもEmacsに足りない要素をどんどん追加していきましょう。
おまけ:SVGパスデータの構造についての図
以下はSVG path要素のd属性について説明するためにEmacs Easy Drawで描いた図です。
pathは <path d="M10,10L30,10C40,20 40,80 30,100" stroke="red" />
のような記述で自由に線を引くための要素ですが、そのd属性(パスデータ)の編集にはいくつか注意点があるのでそれについて描いたものです。
基本的な構造(M, L, Cコマンド):
Zコマンドでパスを閉じると始点と終点が切れ目のない繋がった形状になる(単に始点へ線を引くのとは幅の広い線において色々違いが出てくる)。最後の点と始点の間は直線で結ばれる。
曲線で閉じるにはMと同一点までCで閉じ線を引かないといけない(その上でZが必要)。その場合MとCの点は一つのアンカーポイントとして同一視して処理しなければならない(Mを動かしたらCも一緒に動かしたり、Mの前の点を求めるときはCをスキップする等)。
一つのパスデータには複数(0以上)のサブパスを含めることができる。
直後にMコマンドが無いZコマンドはMの位置を開始点にした新しいサブパスを作る。
従って、一つのMコマンドが表す点は複数のサブパスで共有される場合がある。このMの点を削除したり分割したりする場合は注意が必要になる。
最後のアンカーポイントの前方ハンドルを記録するため、 -forward-handle-point という独自の拡張コマンドを追加している。当然これはSVG出力時には削除される。
(あー、矢印の色が……。複数の図を一つのHTMLに出力した(埋め込んだ)ときにマーカーIDが重複する問題に気がついてしまった……。修正するならエクスポート時にマーカーIDに図のIDを付け加えるとかかなぁ)