ここまで実装してきたパス描画ツールに最後にもう少し機能を追加していく。 パスが既に閉じられた多角形に対して、任意の辺に頂点を追加したり任意の頂点を削除できるようにする。 開発環境やディレクトリ構成は前回、前々回と変わらないので省略する。これまで実装してきた機能も含めた全体のソースはこちら。
完成イメージ
多角形の1辺のパス上で右クリックして頂点編集メニューを開き、頂点の追加
を選んでセグメントの追加を行えるようにする。また、既に存在しているセグメント上で右クリックして頂点編集メニューを開き、頂点の削除
を選んでセグメントの削除を行えるようにする。
以下、右クリックで開くデフォルトのコンテキストメニューに対して、今回実装したいコンテキストメニューのことを「頂点編集メニュー」と呼ぶことにする。頂点編集メニューは、Angular MaterialのmatMenu
を利用して実装していく。
実装
頂点編集メニューを表示する
まず始めに、contextmenu
イベントと頂点編集メニュー表示用メソッドのopenMenu($event)
を紐付ける。
頂点編集メニューのテンプレートは、plot-area.component.html
の一番下に追加していく。メニューの実態は<mat-menu>
タグ内部に記述する。さらに、メニューのトリガー用の要素として<div>
を定義し、matMenuTriggerFor="contextMenu"
でmat-men
要素と紐付ける。[style.left]
と[style.top]
にメニュー表示の起点となる座標contextMenuPosition
を指定する。
<canvas #canvas width="600px" height="500px" (mousemove)="getCurrentPosision($event)" (click)="onClickCanvas()" [class.onSegment]="isMouseOnSegment" (contextmenu)="openMenu($event)"> </canvas> <!-- 中略 --> <!-- 頂点追加・削除実行用コンテキストメニュー --> <div style="visibility: hidden; position: fixed;" [matMenuTriggerFor]="contextMenu" [style.left]="contextMenuPosition.x"[style.top]="contextMenuPosition.y"> </div> <mat-menu #contextMenu="matMenu"> <ng-template matMenuContent class="context-menu"> <div mat-menu-item>頂点の追加</div> <div mat-menu-item>頂点の削除</div> </ng-template> </mat-menu>
続いてコンポーネントのクラスファイルに追加していく。
まず @ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger
でhtmlファイルに記述したメニュー要素を取得し、contextMenu
という変数でこれ以降利用していく。
始めに、頂点編集メニューが開かれているかどうかを判定するフラグisEditMenuOpened
をtrueにする。
次に、右クリックした時点のマウスポインタの座標値をこの後別の場所で使うので、editStartX
とeditStartY
にそれぞれ格納しておく。
頂点編集メニューを表示する起点はマウスポインタの位置にしたいので、contextMenuPosition.x
とcontextMenuPosition.y
にはevent
オブジェクトから取得できるマウスポインタの座標値を格納する。なお、matMenu
はクライアント領域(ウインドウ枠内の領域)が基準となるので、キャンバス領域を基準にしたcurrentX
とcurrentY
を起点に指定してしまうとメニューの表示位置がずれてしまうので注意。最後のthis.contextMenu.openMenu()
で頂点編集メニューを表示する。
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger; // マウスポインターの座標関係 currentX: number; currentY: number; editStartX: number; // 追加 editStartY: number; // 追加 // 各種フラグ isCross = false; isMouseOnSegment = false; isMouseOnStroke = false; // 追加 isMouseDragging = false; // オンマウス状態のパスの子オブジェクト activeSegment: any; activeLocation: any; // 追加 // コンテキストメニュー関係 contextMenuPosition = { x: '0px', y: '0px' }; // 追加 isEditMenuOpened = false; // 追加 // 中略 // openMenu(event: MouseEvent): boolean { this.isEditMenuOpened = true; // デフォルトのコンテキストメニューを開かないようにする event.preventDefault(); // 右クリックした時点のマウスポインターの座標を保持する this.editStartX = this.currentX; this.editStartY = this.currentY; this.contextMenuPosition.x = event.clientX + 'px'; this.contextMenuPosition.y = event.clientY + 'px'; this.contextMenu.openMenu(); }
これでとりあえずキャンバス上のどこでも右クリックすれば、「頂点の追加」「頂点の削除」の項目があるだけの頂点編集メニューが表示される。ついでに頂点編集メニューのデザインも適当に設定しておくと良い。
::ng-deep .mat-menu-panel { width: 100px; height: 60px; background-color: #ffffff; .mat-menu-item { font-size: 12px; text-align: center; line-height: 25px; height: 25px; &:hover { // .mat-menu-itemの要素はデフォルトでマウスカーソルがpointerになる color: #ffffff; background-color: #5e5e5e; } } }
頂点編集メニューを制御する
右クリックで任意の場所に頂点編集メニューを表示できるようになったので、次はその表示タイミングや選択項目の活性・非活性状態の制御を行う。
右クリックした時のマウスポインタの位置と、それに対応する頂点編集メニューの状態は下記の通りにする。
マウスポインタの位置 | 頂点編集メニューの状態 |
---|---|
多角形の辺(ストローク)上 | 頂点の追加 を活性、頂点の削除 を非活性にする |
多角形のセグメント上 | 頂点の追加 を非活性、頂点の削除 を活性にする |
多角形の辺・セグメント以外 | 表示しない (デフォルトのコンテキストメニューを表示) |
まずは頂点編集メニューを閉じた時に呼ぶafterMenuClosed()
を定義し、各種フラグや当たり判定で取得するオブジェクトをクリアする。activeLocation
とisMouseOnStroke
については後述する。
afterMenuClosed(): void { this.isEditMenuOpened = false; this.activeSegment = null; this.activeLocation = null; this.isMouseOnSegment = false; this.isMouseOnStroke = false; }
次に、前回の記事で登場したsetMouseEventToPath()
のonMouseMove
とonMouseLeave
イベントに色々追加していく。
onMouseUp
頂点編集メニューが開かれている時は多角形に対するイベントを一切無効にしたいので、isEditMenuOpened
フラグを使って制御する。これまでは、マウスポインタと多角形のセグメントとの当たり判定を考慮してactiveSegment
だけを取得していたが、今回はそれに加えてマウスポインタと重なる多角形の辺情報activeLocation
を取得する。hitOptions
のstroke
をtrueに変更すればマウスポインタと辺がヒットした時にその位置に関する情報をCurveLocation
オブジェクトとして取得できる。マウスポインタが辺上にあるかどうかを判定するフラグisMouseOnStroke
は、activeLocation
の有無を利用して更新しておく。
this.path.onMouseMove = (event) => { // 頂点編集メニューが表示されている場合はイベントを実行しない if (this.isEditMenuOpened) { return; } // 追加 if (this.polygonArea) { // セグメントとストロークの当たり判定のみを有効にする const hitOptions = { fill: false, stroke: true, // 変更 segments: true, tolerance: 1, }; const hitResult = paper.project.hitTest(event.point, hitOptions); this.activeSegment = hitResult && hitResult.segment; this.isMouseOnSegment = !!this.activeSegment; this.activeLocation = hitResult && hitResult.location; // 追加 this.isMouseOnStroke = !!this.activeLocation; // 追加 } };
onMouseLeave
頂点編集メニューを操作する時にマウスポインタが多角形から離れることもあるが、そこでactiveSegment
がnullになってしまうのは不都合である。頂点編集メニューが開かれている時はonMouseLeave
の処理を行わないようにしたいので、先頭にisEditMenuOpened
フラグによる制御を入れる。onMouseLeave
イベントの処理の修正はこれだけ。
this.path.onMouseLeave = () => { // 頂点編集メニューが表示されている場合はイベントを実行しない if (this.isEditMenuOpened) { return; } // 追加 if (this.activeSegment) { // セグメントをドラッグしている途中の場合は処理を行わない if (this.isMouseDragging) { return; } // セグメントからマウスが離れた場合はactiveItemとオンマウスのフラグをクリアする this.activeSegment = null; this.isMouseOnSegment = false; } this.isMouseOnStroke = false; };
これで、マウスポインタと多角形のセグメント、辺が重なっているかどうかを判定できるようになったので、openMenu()
内で頂点編集メニューを開くか否かの処理を記述することができる。
openMenu(event: MouseEvent): boolean { // カーソルが多角形のセグメント上にもストローク上にもない場合は頂点編集メニューを開かない if (!this.isMouseOnSegment && !this.isMouseOnStroke) { return true; } // 追加 this.isEditMenuOpened = true; // デフォルトのコンテキストメニューを開かないようにする event.preventDefault(); // 右クリックした時点のマウスポインターの座標を保持する this.editStartX = this.currentX; this.editStartY = this.currentY; this.contextMenuPosition.x = event.clientX + 'px'; this.contextMenuPosition.y = event.clientY + 'px'; this.contextMenu.openMenu(); }
後はhtmlファイルを編集すれば良い。メニューのトリガー用のdiv
要素には、MatMenuTrigger
に用意されているmenuClosed
イベントとafterMenuClosed()
を紐付ける。また、頂点編集メニューテンプレート内のmat-menu-item
要素に対して、非活性スタイルを適用するクラスdisabled
を脱着できるようにすればメニュー項目の活性・非活性の制御を実現できる。
<div style="visibility: hidden; position: fixed;" [matMenuTriggerFor]="contextMenu" (menuClosed)="afterMenuClosed()" [style.left]="contextMenuPosition.x"[style.top]="contextMenuPosition.y"> </div> <mat-menu #contextMenu="matMenu"> <ng-template matMenuContent class="context-menu"> <div mat-menu-item [class.disabled]="!isMouseOnStroke">頂点の追加</div> <div mat-menu-item [class.disabled]="!isMouseOnSegment">頂点の削除</div> </ng-template> </mat-menu>
.disabled { pointer-events: none; color: #dfdfdf; }
頂点の追加、削除処理を実装する
頂点編集メニューを制御できるようになったので、メニューの各項目に対応するメソッドを定義して中身を記述していく。
頂点(セグメント)を追加するためのメソッド。
既存のPath
オブジェクトのセグメントリストに新しいセグメントを挿入するPath.insert()
を利用する。引数にはセグメントを挿入するインデックスinsertIndex
とその座標オブジェクトを渡してやれば良い。
マウスポインタが、ある線分とヒットした時に取得できるCurveLocation
オブジェクトのindex
は、その線分の始点側のセグメントのindex
に一致する。これを利用すれば、例えばn番目とn+1番目のセグメントで結ばれる線分の中に新たにセグメントを挿入したい時は、insertIndex
としてn(= 線分の始点 = CurveLocation.index) + 1
を指定してやれば良い。挿入するセグメントの座標はeditStartX
とeditStartY
を指定する。
後は、頂点座標リストvertexList
にもsplice()
で新しいセグメントの座標を追加し、plotMarker()
を呼び出して新しいセグメントの位置にマーカー(Shape
オブジェクト)を描画すれば完了である。最後に頂点編集メニューを閉じておく。
addSegment(): void { const insertIndex = this.activeLocation.index + 1; this.path.insert(insertIndex, new Point(this.editStartX, this.editStartY)); this.vertexList.splice(insertIndex, 0, {x: this.editStartX, y: this.editStartY}); this.plotMarker(this.editStartX, this.editStartY, insertIndex); this.contextMenu.closeMenu(); }
頂点を削除するためのメソッド。
まず始めに、頂点が3個未満の多角形は存在し得ないので、現在の頂点数が3個の場合は削除できないようにする。
次は頂点を削除する処理を実装していく。既存のPath
オブジェクトのセグメントリストからセグメントを1つ削除する場合は、Path.removeSegment()
の引数に削除するセグメントのインデックスremoveIndex
を渡すことになる。removeIndex
は、マウスカーソルがヒットしているセグメントのインデックスをそのまま指定すれば良い。
頂点座標リストvertexList
からもremoveIndex
番目の要素を削除してやる。さらに、セグメントを削除した位置にあったマーカーを消すため、pathGroup
の子要素から当該のShape
オブジェクトを削除する。Group
オブジェクトの子要素を削除する時はremoveChildren()
を利用するが、引数には削除する要素の開始インデックスと終了インデックス(端点は含まない)を渡す。例えばn番目の要素を削除するのであればremoveChildren(n, n+1)
となる。ただし、1つ注意が必要なのは、pathGroup
の子要素の先頭(index = 0
)にはpath
オブジェクトを格納しているため、Shape
オブジェクトは2番目(index = 1
)以降から始まることになる。したがって、n番目の要素を削除するのであればremoveChildren(n+1, n+2)
とする必要がある。
最後に頂点編集メニューを閉じ、マウスポインタがセグメントから離れるのでisMouseOnSegment
をfalseに更新する。
removeSegment(): void { // 現在の頂点数が3個の場合は削除できないようにする if (this.path.segments.length === 3) { alert('多角形の描画には3個以上の頂点が必要です。'); this.contextMenu.closeMenu(); return; } const removeIndex = this.activeSegment.index; this.path.removeSegment(removeIndex); this.vertexList.splice(removeIndex, 1); this.pathGroup.removeChildren(removeIndex + 1, removeIndex + 2); this.contextMenu.closeMenu(); this.isMouseOnSegment = false; }
後は、実装したメソッドと頂点編集メニューの項目をクリックイベントで紐付けてやれば終わりとなる。
<mat-menu #contextMenu="matMenu"> <ng-template matMenuContent class="context-menu"> <div mat-menu-item (click)="addSegment()" [class.disabled]="!isMouseOnStroke">頂点の追加</div> <div mat-menu-item (click)="removeSegment()" [class.disabled]="!isMouseOnSegment">頂点の削除</div> </ng-template> </mat-menu>
おわりに
前々回、前回の記事から続いていたが、以上でパス描画ツールは完成となる。 Paper.js公式ページのサンプル集をざっと見ても分かる通り、さらに多くの機能が用意されている。 今回その中のほんの一部を利用しただけなので今後も色々試していきたい。
参考