satoh

知っているようで知らないデジタル図形処理 ~任意形状図形の選択から交点・接点・接線計算まで、方程式では解けないノウハウを紹介

   

画面に描いた円弧をマウスで選択してみよう

円を選択する

円弧の選択を考える前に、円をマウスで選択することを考えます。
ここでいう選択は、円内部点ではなく、円周上をクリックしたとき「選択された」と考えます。
半径 R、中心点(Cx, Cy)の円を考えます。
円ヒットテスト
これは簡単ですね。中心からクリックされた点の距離がRであればよいわけです。
しかしながら、デジタル処理での誤差および、人間がクリックするのである程度の範囲を考えます。
上図のように、R±δを範囲とします。

int calcHitCircle(int x, int y, int cx, int cy, int r)
{
	double    det, rr1, rr2 ;

	det = (double)(x - cx) * (x - cx) + (double)(y - cy) * (y - cy) ;
	rr1 = (double)(r - HitDist) * (double)(r - HitDist) ;
	rr2 = (double)(r + HitDist) * (double)(r + HitDist) ;

	if (rr1 > det || det > rr2) return 0 ;

	return 1 ;
}

こんな感じになります。ここでHitDistはヒット範囲の距離の二乗です。

円弧を選択する

円弧の場合、
・円の選択ができてかつ開始点と終了点の間であること
を判断できればいいわけです。
クリック点の円弧中心からの角度が、開始角、終了角の範囲にあればよいのですが、ここでは三角関数を使わずにやってみます。
まず最初に、前章で示した「円の選択」を判断します、そもそも円の選択ができなければ処理は終わりです。
なので、クリック点は少なくとも円周上にあることが前提となります。
また、正規直交座標系であることと、円弧はCCW(開始点から終了点へ向かって反時計回り)であることとします。
円弧ヒットテスト
中心点 (Cx,Cy)
開始点(Sx,Sy)
終了点(Ex,Ey)
クリック点(X,Y)
とします。

円弧中心点を原点に移動

まず処理を簡単にするために、円弧中心点が原点(0,0)になるように、クリック点、開始点。終了点を平行移動します。
これで中心座標のことを考える必要がなくなります。

各点の存在する象限

クリック点、開始点、終了点の存在する 第1象限~第4象限を探します。
こうしたとき、
・開始点、終了点が別象限にある
・開始点、終了点が同一象限にある
条件で処理を変えます。
このあと、注目する点の判断を簡単にするために、
・現在の象限から第1象限に回転する
処理を考えます。

int _calcZone0(int org, double *x, double *y)
{
	double    rx, ry ;

	switch (org) {
		case    0:
			return 0 ;
		case    1:      //  -90 度
			rx = *y ;
			ry = - *x ;
			break ;
		case    2:      // -180 度
			rx = - *x ;
			ry = - *y ;
			break ;
		case    3:      // -270 度
			rx = - *y ;
			ry = *x ;
			break ;
	}

	*x = rx ;
	*y = ry ;

	return 0 ;
}

これらは最後に示すプログラムで利用します。

開始点終了点が同一象限内に存在する場合

まず、開始終了点とも第1象限に回転します。
これによって、
開始点X座標 < 終了点X座標 …. 180°以上の大円
開始点X座標 > 終了点X座標 …. 180未満の小円
の判断ができます。以下の図を参照してください。
円弧ヒットテスト(大円・同一象限)上図は、 開始点X座標 < 終了点X座標 …. 180°以上の大円
このとき、クリック点の存在する象限が本来の開始点存在象限と違えば必ず選択されたことになります。すでに円の選択で「選択状態」を判断しているからです。クリック点も同一象限であれば、クリック点も第1象限に回転し、
X <= Ex または X >= Sx
であれば「選択」です。

円弧ヒットテスト(小円・同一象限)上図は、 開始点X座標 > 終了点X座標 …. 180未満の小円
このとき、 クリック点の存在する象限が本来の開始点存在象限と違えば必ず「選択されてない」ことになります。つまり必ず同一象限であることがまず最初の条件です。ではここでもクリック点を第1象限に回転します。あとは、
X >= Sx かつ X <= Ex
であれば「選択」です

開始点終了点が別象限内に存在する場合

以下のような場合です。
円弧ヒットテスト(別一象限) ここでも簡単にするため、開始点が第1象限になるように、すべてを回転します。
象限番号として、第1象限を1、第2象限を2、….とします。
すると、
・クリック点の象限番号が終了点の象限番号より大きければ「選択されない」
・クリック点の象限番号が、開始点+1~終了点-1の象限番号なら「選択」
がわかります。
上記条件以外は、
・クリック点の象限番号が開始点と同じであれば、
X <= Sx であれば「選択」
・クリック点の象限番号が終了点と同じであれば、
クリック点と終了点を第1象限に回転すれば、
X >= Ex であれば「選択」
ということがわかります。

プログラム

では早速プログラムしてみます。

calcHitArc(int x, int y, int sx, int sy, int ex, int ey, int cx, int cy, int r)
{
	int i ;
	struct {
		double    x ;
		double    y ;
		int     zone ;
	} cd[3] ;

	cd[0].x = sx - cx ;         // 円弧中心を原点に平行移動する
	cd[0].y = sy - cy ;
	cd[1].x = ex - cx ;
	cd[1].y = ey - cy ;
	cd[2].x = x  - cx ;
	cd[2].y = y  - cy ;
								// まず円でチェック

	if (calcHitCircle(x, y, cx, cy, r) == 0) return 0 ;

								// 各位置の象限を探す
	for (i = 0 ; i < 3 ; i++) {
		if      (cd[i].x >= 0 && cd[i].y >= 0) cd[i].zone = 0 ;  // 第1象限
		else if (cd[i].x <  0 && cd[i].y >= 0) cd[i].zone = 1 ;  // 第2象限
		else if (cd[i].x <  0 && cd[i].y <  0) cd[i].zone = 2 ;  // 第3象限
		else                                   cd[i].zone = 3 ;  // 第4象限
	}

								// 始点終点が同じ象限にある
	if (cd[0].zone == cd[1].zone) {
		_calcZone0(cd[0].zone, &cd[0].x, &cd[0].y) ;
		_calcZone0(cd[0].zone, &cd[1].x, &cd[1].y) ;

		if (cd[0].x < cd[1].x) {        // 始点が左側ならば大円(180度以上)

			if (cd[2].zone != cd[0].zone) return 1 ;  // 象限が違えば必ずヒット
			_calcZone0(cd[0].zone, &cd[2].x, &cd[2].y) ;
			if (cd[0].x >= cd[2].x || cd[2].x >= cd[1].x) return 1 ;

		} else {                        // 小円(180度未満)

			if (cd[2].zone != cd[0].zone) return 0 ;
			_calcZone0(cd[0].zone, &cd[2].x, &cd[2].y) ;
			if (cd[1].x <= cd[2].x && cd[2].x <= cd[0].x) return 1 ;

		}
								// 始点終点が違う象限にある
	} else {

								// 始点を第1象限にする
		_calcZone0(cd[0].zone, &cd[0].x, &cd[0].y) ;
		_calcZone0(cd[0].zone, &cd[1].x, &cd[1].y) ;
		_calcZone0(cd[0].zone, &cd[2].x, &cd[2].y) ;

		cd[2].zone -= cd[0].zone ;
		if (cd[2].zone < 0) cd[2].zone += 4 ;
		cd[1].zone -= cd[0].zone ;
		if (cd[1].zone < 0) cd[1].zone += 4 ;
		cd[0].zone = 0 ;

												// 終点より以降の象限ならヒットしない
		if (cd[1].zone < cd[2].zone) return 0 ;
												// 始点終点の象限未満ならヒット
		if (cd[0].zone < cd[2].zone && cd[2].zone < cd[1].zone) return 1 ;

		if (cd[0].zone == cd[2].zone) {         // 始点と象限が同じなら

			if (cd[0].x >= cd[2].x) return 1 ;

		} else {                                // 必ず終点と象限が同じ

			_calcZone0(cd[1].zone, &cd[1].x, &cd[1].y) ;
			_calcZone0(cd[1].zone, &cd[2].x, &cd[2].y) ;

			if (cd[1].x <= cd[2].x) return 1 ;
		}
	}

	return 0 ;
}

 - C, ヒットテスト