前回の記事ではパス描画ツールの基本的な処理を実装した。今回はさらに機能を拡張し、パスの交差制御と頂点マーカーのドラッグ移動を実装していく。
完成イメージ
- パスが交差していたら警告メッセージを表示して操作を取り消す
- 頂点をドラッグ移動して位置を更新する
- 頂点を移動したら座標リストや面積計算の結果を更新する
実装
パスの交差を制限する
多角形の面積を計算する時、パス同士が交差してしまうことは望ましくない(交差点を基準に複数の多角形に分けてそれぞれの面積を計算した後に合算する方法もあるが面倒くさい)。なので、パスが交差する位置に点を打てないようにしてしまえばよい。 具体的には、既にキャンバスをクリックして位置が確定されているパス(以下、確定パス)と、確定パスの最先端のセグメントとマウスポインタの位置を結んだパス(以下、未確定パス)の2つのパスの交差判定を行い、両者が交差している状態では点を打てないようにする。確定パスの描画は前の記事で述べたので、ここでは未確定パスの描画について述べる。
まず、未確定パスのオブジェクトとしてunsettledPath
を定義し、initialItemSetting()
の中でPath
オブジェクトを生成してunsettledPath
に格納する。
export class PlotAreaComponent implements OnInit { @ViewChild('canvas', { static: true }) canvas: ElementRef<HTMLCanvasElement>; // パスオブジェクト関係 path: any; pathGroup: any; unsettledPath: any; // 追加 // 中略 // // 各種フラグ isCross = false; // 中略 // private initialItemSetting(): void { this.path = new Path(); this.unsettledPath = new Path(); // 追加 this.pathGroup = new Group(); this.pathGroup.addChild(this.path); } }
未確定パスunsettledPath
を描画するためのメソッド。未確定パスは、確定パスの最先端のセグメントを始点、マウスポインタの位置を終点としたパスであり、マウスポインタがキャンバス上を移動すれば未確定パスもそれに伴って移動する。確定パスの最先端のセグメントはthis.path.lastSegment.point
で取得し、マウスカーソルの位置はいつも通りthis.currentX
とthis.currentY
を使う。this.unsettledPath.add()
で始点と終点のPoint
ブジェクトを追加することで未確定パスを描画している。なお、マウスポインタが移動する時に移動前の未確定パスが残らないように予めthis.unsettledPath.removeSegments()
で未確定パスの始点と終点のセグメントを取り除いて描画を破棄している。
private drawUnsettledLine(): void { if (!this.path || !this.unsettledPath || this.polygonArea) { return; } this.unsettledPath.removeSegments(); // 未確定パスの設定 this.unsettledPath.strokeColor = 'rgb(0, 0, 0, 0.1)'; this.unsettledPath.strokeWidth = 1; // 確定パスの最先端にある頂点座標を取得する const lastSegment = this.path.lastSegment.point; // 未確定パスの始点 this.unsettledPath.add(new Point(lastSegment.x, lastSegment.y)); // 未確定パスの終点 this.unsettledPath.add(new Point(this.currentX, this.currentY)); // 確定パスと未確定パスの交差を判定する this.checkCrossing(); }
パスの各セグメントの座標から数学的に線分同士の交差判定を行う方法もあるが、今回はPaper.jsで用意されているgetIntersections()
を用いた判定を行う。パスA.getIntersections(パスB)
のようにすると、2つのパスA, Bの全ての交差をCurveLocation
オブジェクトの配列として取得することができる。今回は確定パスと未確定パスの交差点の配列を取得することになる。ただし、1つ注意すべきは、確定パスと未確定パスの接続点(確定パスの最先端のセグメント)も交差点の一種と見なされるため、既に交差点の配列にはその接続点が1つ含まれている。したがって、確定パスと未確定パスが本当に交差しているかどうかは、交差点の配列の要素数が1より大きいかどうかを確認すればよい。その場合は、交差判定用フラグisCross
をtrueにする。
private checkCrossing(): void { const interSection = this.path.getIntersections(this.unsettledPath); this.isCross = interSection.length > 1; }
drawUnsettledLine()
を呼び出す処理を追加する。
getCurrentPosision(event): void { const rect = event.target.getBoundingClientRect(); this.currentX = event.clientX - rect.left; this.currentY = event.clientY - rect.top; this.drawUnsettledLine(); // 追加 }
isCross
がtrueの時はキャンバスをクリックしても描画できないようにしたいので、メソッドの先頭にisCross
による条件分岐を追加する。また、交差している場合にその旨が分かるようにポップアップでメッセージを出すようにしている。
onClickCanvas(): void { if (this.isCross) { alert('パスが交差する位置に点を打つことは出来ません。'); // 交差フラグをクリアする this.isCross = false; return; } // 中略 // }
クリックして確定パスを描画したら元の未確定パスは消す必要があるので、this.unsettledPath.removeSegments()
で未確定パスのセグメントを削除し描画をクリアする。
private drawLine(): void { this.path.strokeColor = 'rgb(255, 0, 0)'; this.path.strokeWidth = 2; this.path.add(new Point(this.currentX, this.currentY)); this.unsettledPath.removeSegments(); // 追加 }
頂点のドラッグ移動を可能にする
パスを閉じて多角形の描画が完了した後、多角形の頂点マーカーの位置をドラッグ移動によって修正できるようにする。頂点のマーカーを移動させた後は多角形の面積を再計算する。 描画された各オブジェクトそれぞれに対してマウスイベントを設定していく必要がある。描画オブジェクトに対してマウスイベントを設定する処理はコンポーネントのクラスファイルに記述していくことになる。
まずは必要な変数を定義しておく。
export class PlotAreaComponent implements OnInit { @ViewChild('canvas', { static: true }) canvas: ElementRef<HTMLCanvasElement>; // 省略 // // 各種フラグ isCross = false; isMouseOnSegment = false; isMouseDragging = false; // オンマウス状態のパスの子オブジェクト activeSegment: any; // 省略 // }
次に、多角形のパスに対して各種のマウスイベントを設定していく。Path
オブジェクトのpath
を生成した後、setMouseEventToPath()
メソッド(後述)を呼び出し、path
に対してマウスイベントを設定する。
private initialItemSetting(): void { this.path = new Path(); this.setMouseEventToPath(); // 追加 this.unsettledPath = new Path(); this.pathGroup = new Group(); this.pathGroup.addChild(this.path); } private setMouseEventToPath(): void { // 内容は後述 // }
Path
オブジェクトには各種のイベントハンドラが用意されており、イベントと処理を紐付けたい時は、
// Pathオブジェクト上をマウスポインタが移動した時に何らかの処理を実行させる例 Path.onMouseMove = () => { 処理 }
のように記述する。
今回はonMouseMove
、onMouseDrag
、onMouseUp
、onMouseLeave
の4イベントを利用する。
onMouseMove
path
上でマウスポインタが移動した時に実行する処理を定義する。
まず、マウスポインタとpath
の当たり判定を行う。
paper.project
のhitTest()
メソッドは、第1引数の座標と描画オブジェクトとの当たり判定を行い結果(paper.HitResult
型のオブジェクト)を返す。当たりがなければnull
が返る。当たり判定のオプションはhitOptions
で指定し、当たり判定を有効にする部分や判定精度を設定できる。描画オブジェクトの塗り潰し部分、線分、セグメントに対して当たり判定を行いたい場合は、それぞれfill
、stroke
、segment
をtrueにすればよい。tolerance
は当たり判定の精度でデフォルトは0である。tolerance
が小さすぎるとマウス操作がシビアになるし、tolerance
が大きすぎると複数の要素(線分とセグメントなど)が隣接している場合にうまく当たり判定を取得できないので、状況に応じて調整するのがよい。
今回はセグメントに対する当たり判定だけあればいいので、hitOptions
のsegment
のみtrueにしている。マウスポインタとセグメントが当たった時はhitResult.segment
で対象のSegment
オブジェクトを取得することができるので、そのSegment
オブジェクトをactiveSegment
に格納する。また、マウスポインタがセグメント上か否かを判定するフラグisMouseOnSegment
をtrueにする。
this.path.onMouseMove = (event) => { 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; } };
onMouseDrag
path
上の要素がドラッグされている時に実行する処理を定義する。
activeSegment
が存在(マウスポインタがセグメント上にある)する場合のみ実行する。
まず、セグメントをドラッグしている最中か否かを判定するフラグisMouseDragging
をtrueにする。
event.point.x
とevent.point.y
でドラッグ中のマウスポインタの座標を参照できるので、activeSegment
の座標をマウスポインタの座標で更新する。同時に、頂点マーカーもドラッグに追随させたいので、ドラッグ中のactiveSegment
に対応するマーカーの座標をマウスポインタの座標で更新する。activeSegment
のindex
がN番目である時、それに対応するマーカーのShape
オブジェクトはpathGroup
のN+1番目の子要素になる。これは、pathGroup
の先頭(index = 0
)にはpath
が格納されていて、2番目(index = 1
)以降からマーカーのShape
オブジェクトが格納されているためである。
さらに、頂点のドラッグ移動によってパスが交差してしまうことを防ぐため、this.path.getIntersections(this.path)
でパスの交点の配列を取得して交差判定を行う。前項では確定パスと未確定パスという異なる2つのパスの交差を判定していたが、このように自分自身との交差判定を行うこともできる。ただし、確定パスと未確定パスの場合は接続点が1つあるので交差点の配列の長さ > 1
を交差ありの条件としていたが、今回は確定パス1本だけの交差判定なので交差点の配列の長さ > 0
が交差ありの条件となるので注意。
this.path.onMouseDrag = (event) => { if (this.activeSegment) { const index = this.activeSegment.index; this.isMouseDragging = true; // パスのセグメントの座標を更新する this.activeSegment.point.x = event.point.x; this.activeSegment.point.y = event.point.y; // パス頂点のマーカーの座標を更新する this.pathGroup.children[index + 1].position.x = event.point.x; this.pathGroup.children[index + 1].position.y = event.point.y; // パス同士の交差を判定する const interSection = this.path.getIntersections(this.path); this.isCross = interSection.length > 0; } };
onMouseUp
path
上の要素のドラッグが終了した時に実行する処理を定義する。
activeSegment
が存在する場合のみ実行する。
頂点をドラッグ移動してパスが交差してしまう場合は、マウスボタンを離した時にセグメントとマーカーを移動前の位置に強制的に戻す。移動前の位置の座標はvertexList
に格納されているので、activeSegment
に対応するvertexList
の要素を参照して座標を更新する。パスが交差していなければ問題ないので、activeSegment
の座標でvertexList
の要素の座標を更新する。
ドラッグが終了したらisMouseDragging
をfalseに戻し、多角形の面積を再計算する。
this.path.onMouseUp = () => { if (this.activeSegment) { const index = this.activeSegment.index; if (this.isCross) { // パスのセグメントの座標をドラッグ移動前に戻す this.activeSegment.point.x = this.vertexList[index].x; this.activeSegment.point.y = this.vertexList[index].y; // パス頂点のマーカーの座標をドラッグ移動前に戻す this.pathGroup.children[index + 1].position.x = this.vertexList[index].x; this.pathGroup.children[index + 1].position.y = this.vertexList[index].y; // セグメントからカーソルが離れるのでオンマウスのフラグをクリアする this.isMouseOnSegment = false; return; } this.vertexList[index].x = this.activeSegment.point.x; this.vertexList[index].y = this.activeSegment.point.y; this.isMouseDragging = false; // 面積を再計算する this.calculatePolygonArea(); } };
onMouseLeave
path
オブジェクトからマウスポインタが離れた時に実行する処理を定義する。
activeSegment
が存在する場合のみ実行する。
path
オブジェクトからマウスポインタが離れたらactiveSegment
をクリアしてisMouseOnSegment
をfalseに戻す。しかし、これだけだとセグメントをドラッグ移動している場合も(元々のセグメントの位置からずれるので)path
オブジェクトからマウスポインタが離れたと判定されてしまうため、セグメントのドラッグ中(isMouseDragging
がtrueの時)は処理を行わないようにする必要がある。
this.path.onMouseLeave = () => { if (this.activeSegment) { // セグメントをドラッグしている途中の場合は処理を行わない if (this.isMouseDragging) { return; } // セグメントからマウスが離れた場合はactiveItemとオンマウスのフラグをクリアする this.activeSegment = null; this.isMouseOnSegment = false; } };
以上4つの処理をPath
オブジェクトのイベントハンドラにバインドしてやることで、頂点を掴んで多角形の形を自由に変える機能を実現できる。
セグメント上でマウスカーソルの表示を切り替える
頂点をドラッグ移動させる機能は一応完成したが、最後にちょっとしたマウスカーソルの切替え処理を加えておく。マウスポインタが頂点とヒットした際にマウスカーソルをpointer
に切り替えてドラッグ可能な場所であることを分かりやすくする。isMouseOnSegment
フラグを利用し、[class.onSegment]="isMouseOnSegment"
でマウスポインタがセグメント上にある時はcanvs
要素にonSegment
クラスが付加されるようにし、onSegment
の要素に対してcssでデザイン調整を行う。
<canvas #canvas id="canvas" #dot width="600px" height="500px" (mousemove)="getCurrentPosision($event)" (click)="onClickCanvas()" [class.onSegment]="isMouseOnSegment"> </canvas>
canvas.onSegment { &:hover { cursor: move; } }
おわりに
以上のイベントを設定することで、頂点マーカーをドラッグ移動して面積を再計算する動作が実現される。 ここまでで、多角形の描画と面積計算、頂点の移動といった一通りの機能を実装できた。 次の記事ではさらに発展させて、多角形の辺上の任意の位置に頂点を追加する機能と、頂点を削除する機能を追加してみる。
参考