Revit APIでの幾何計算(2)（Revit API アドベントカレンダー2024 9日目）

Qiita Revit API アドベントカレンダー2024の9日目の記事です。

（前回の記事はこちら）

https://qiita.com/thivanao/items/866bdcaf092ee315e39b

## 今回の内容

今回は、前回に引き続き「幾何計算」の実装方法についてご紹介します。

用語等は前回の記事を参照してください。

## 交差判定・交点算出・交線算出

線同士・平面同士・線と平面で、それらが交わっているかどうかを判定したり、（交わっている場合に）その交点や交線を求める場合、Lineクラスの基本クラスであるCurveクラスやPlanerFaceクラスの基本クラスであるFaceクラスにより提供されている交点計算メソッド（Intersect）を利用することができます。

### Curve.Intersectメソッド

https://www.revitapidocs.com/2024/9a487e3d-bbb4-34b9-307d-2e4f63fddab6.htm

### Face.Intersectメソッド

https://www.revitapidocs.com/2024/91f650a2-bb95-650b-7c00-d431fa613753.htm

但し、これらは線同士（Curve.Intersect）及びフェース（有限の面）同士＆フェースと線（Face.Intersect）で利用できるメソッドであり、無限面（Plane）の交点計算は用意されていないため自分で用意する必要があります。

:::note info
幸い（？）無限面の交点計算（無限面同士・無限面と線）はベクトルの内積（ドット積：DotProduct）及びベクトルの外積（クロス積：CrossProduct）を駆使すれば算出可能です。
:::

交点計算の処理バリエーションを以下にまとめています。

|要素1|要素2|処理方法
|:-|:-|:-|
|線分[^K1]|線分     |Curve.Intersect|
|線分     |直線[^K2]|Curve.Intersect|
|線分     |平面[^K3]|平面と始点・終点の位置関係をベクトル内積により求める|
|線分     |Face[^K4]|Face.Intersect|
|直線     |直線     |Curve.Intersect|
|直線     |平面     |直線と平面の連立方程式を解く|
|直線     |Face     |Face.Intersect|
|平面     |平面     |法線ベクトルの外積から交線の方向ベクトルを求め、2平面の連立方程式による交線上の点と組み合わせて交線を作成|
|平面     |Face     |（割愛）|
|Face     |Face     |Face.Intersect|

[^K1]: 始点・終点を持つ有限線のこと
[^K2]: 始点・終点を持たない（原点とベクトルで表現される）無限線のこと
[^K3]: 無限面のこと
[^K4]: ここでは領域で区切られた平面のこと（当記事内の便宜上の表現のため数学的な用語とは異なります）

### 線同士の交点算出

```csharp
// 線同士の交点算出の実装例
public static XYZ GetCrossPoint( this Line o1, Line o2, double tolerance = RevitTolerance )
{
	SetComparisonResult ret = o1.Intersect( o2, out IntersectionResultArray resultArray );
    if( SetComparisonResult.Overlap != ret &&
		SetComparisonResult.Subset  != ret )
		return null; // オーバーラップでもサブセットでもない＝交差なし
	if( SetComparisonResult.Subset == ret )
	{
		if( !o1.IsBound || !o2.IsBound )
			return null; // サブセットだが片方が直線ならば点にならない
	}
	if( resultArray.Size != 1 )
		return null; // 交点が0個または2個以上の場合
	var result = resultArray.get_Item(0);
	return result.XYZPoint;
}
```

### 線とフェースとの交点算出

```csharp
// 線とフェースとの交点算出の実装例
public static XYZ GetCrossPoint( this Line o1, Face o2, double tolerance = RevitTolerance )
{
	SetComparisonResult ret = o2.Intersect( o1, out IntersectionResultArray resultArray );
	if( ret != SetComparisonResult.Overlap )
		return null;
	if( resultArray.Size != 1 )
		return null;
	var result = resultArray.get_Item(0);
	return result.XYZPoint;
}
```

以下の処理はリンク先による参考情報を元にRevit APIの型に当てはめて実装したものです。
詳しい説明等はリンク先をご覧ください。（参考にさせていただいた皆様ありがとうございます）

### 直線と平面との交点算出

https://qiita.com/edo_m18/items/c8808f318f5abfa8af1e

```csharp
// 直線と平面との交点算出の実装例
public static XYZ GetUnboundCrossPoint( this Line o1, Plane o2 )
{
	// 当関数は線分非対応です
	Debug.Assert( !o1.IsBound );

	if( o1.IsParallel( o2 ) )
		return null;
	var N = o2.Normal;
	var X = o2.Origin;
	var X0 = o1.Origin;
	var M = o1.Direction;
	var H = N.DotProduct( X );
	var intersectPoint = X0 + ( ( H - N.DotProduct( X0 ) ) / N.DotProduct( M ) ) * M;
	return intersectPoint;
}
```

### 線分と平面との交点算出

http://www.sousakuba.com/Programming/gs_plane_line_intersect.html

```csharp
// 線分と平面との交点算出の実装例
public static XYZ GetUnboundCrossPoint( this Line o1, Plane o2 )
{
	// 当関数は無限線非対応です
	Debug.Assert( o1.IsBound );

	var P = o2.Origin;
	var N = o2.Normal;

	// 線分の端点
	var A = o1.GetEndPoint(0);
	var B = o1.GetEndPoint(1);

	// PA PBベクトル
	var PA = P - A;
	var PB = P - B;

	// PA PBそれぞれを平面法線と内積
	var dot_PA = N.DotProduct( PA );
	var dot_PB = N.DotProduct( PB );

	// 誤差調整
	if( dot_PA.IsAlmostZero() ) { dot_PA = 0.0; }
	if( dot_PB.IsAlmostZero() ) { dot_PB = 0.0; }

	// 交差判定
	if( dot_PA == 0.0 && dot_PB == 0.0 )
	{
		// 両端が平面上にあり、交点を計算できない
		crossPoint = null;
		return true;
	}
	else
	if( ( dot_PA >= 0.0 && dot_PB <= 0.0 ) ||
		( dot_PA <= 0.0 && dot_PB >= 0.0 ) )
	{
		// 内積の片方がプラスで片方がマイナスなので、交差している
		if( crossPoint == null )
			return true; // 交点計算不要
	}
	else
	{
		// 交差していない
		return false;
	}

	// 以下、交点を求める 
	var AB = B - A;

	//交点とAの距離 : 交点とBの距離 = dot_PA : dot_PB
	double rate = Math.Abs(dot_PA) / ( Math.Abs(dot_PA) + Math.Abs(dot_PB) );

	crossPoint = new XYZ( A.X + ( AB.X * rate ),
						  A.Y + ( AB.Y * rate ),
						  A.Z + ( AB.Z * rate ) );

	return true;
}
```

### 平面同士との交線算出

https://www.hiramine.com/programming/graphics/3d_planeintersection.html

```csharp
// 平面同士の交線算出の実装例
public static Line GetCrossLine( this Plane o1, Plane o2 )
{
	double d1 = o1.Normal.DotProduct( o1.Origin );
	double d2 = o2.Normal.DotProduct( o2.Origin );

	// 交線のベクトル及び交線上の点
	XYZ xVector = o1.Normal.CrossProduct( o2.Normal );
	if( !IsEqual( xVector.Z, 0.0, tolerance ) )
	{
		double x = ( d1 * o2.Normal.Y - d2 * o1.Normal.Y ) / ( xVector.Z);
		double y = ( d1 * o2.Normal.X - d2 * o1.Normal.X ) / (-xVector.Z);
		double z = 0.0;
		return Line.CreateUnbound( new XYZ( x, y, z ), xVector );
	}
	if( !IsEqual( xVector.Y, 0.0, tolerance ) )
	{
		double x = ( d1 * o2.Normal.Z - d2 * o1.Normal.Z ) / (-xVector.Y);
		double y = 0.0;
		double z = ( d1 * o2.Normal.X - d2 * o1.Normal.X ) / ( xVector.Y);
		return Line.CreateUnbound( new XYZ( x, y, z ), xVector );
	}
	if( !IsEqual( xVector.X, 0.0, tolerance ) )
	{
		double x = 0.0;
		double y = ( d1 * o2.Normal.Z - d2 * o1.Normal.Z ) / ( xVector.X);
		double z = ( d1 * o2.Normal.Y - d2 * o1.Normal.Y ) / (-xVector.X);
		return Line.CreateUnbound( new XYZ( x, y, z ), xVector );
	}

	// xVector のどの成分もゼロのときは２つの平面は平行で交線なし
	return null;
}
```

## 距離算出

点同士及び線と点の距離算出には、XYZクラスのDistanceToメソッド及びLineクラスの基本クラスであるCurveクラスのDistanceメソッドを利用できます。

### XYZ.DistanceToメソッド（点同士の距離）

https://www.revitapidocs.com/2024/ecbbee02-8f32-d5e9-a565-9c072543ea4f.htm

### Curve.Distanceメソッド（線と点の距離）

https://www.revitapidocs.com/2024/95efa592-d444-acb8-6460-e2757b96e053.htm

平面と点の距離は、平面の法線と点に向かうベクトルとの内積の絶対値で計算可能です。

```csharp
// 点と平面との距離算出の実装例
public static double GetDistance( this XYZ o1, Plane o2 )
{
	XYZ v = o2.Origin - o1;
	return Math.Abs( o2.Normal.DotProduct( v ) );
}
```

線同士（片側もしくは両方が直線の場合を含む）の距離算出では、2つの曲線間におけるもっとも近い点を求めるRevit APIのメソッド（Curve.ComputeClosestPoints）を利用して距離を求めることができます。
（交点があれば距離は0で自明であるため、事前に交差チェックを行う）

### Curve.ComputeClosestPointsメソッド（線同士の最も近い点とその距離）

https://www.revitapidocs.com/2024/04ab73d1-bc85-9b87-aace-4272a0c7c3e4.htm

```csharp
// 線同士の距離算出の実装例
public static double GetDistance( this Line o1, Line o2 )
{
	SetComparisonResult ret = o1.Intersect( o2 );
	switch( ret )
	{
		case SetComparisonResult.Overlap:
		case SetComparisonResult.Equal:
		case SetComparisonResult.Subset:
		case SetComparisonResult.Superset:
			return 0.0; // 交点あり
		case SetComparisonResult.Disjoint:
			break;
	}
	o1.ComputeClosestPoints( o2, o1.IsBound, o2.IsBound, false, out var resultList );
	if( resultList.Count == 0 )
	{
		Debug.Assert( false );
		return 0.0;
	}
	var result = resultList[0];
	return result.Distance;
}
```

線（線分・直線）と平面の場合は、以下のように考えることで距離を算出できます。

* 線分の場合
  * 平面と交点がある：距離0
  * 平面と交点がない：始点と終点の距離の内小さい方
* 直線の場合
  * 平面と平行である：直線の任意の点と平面との距離
  * 平面と平行でない：距離0（共に無限であるから必ずどこかで交わるはず）

```csharp
// 線と平面との距離算出の実装例
public static double GetDistance( this Line o1, Plane o2 )
{
	if( o1.IsBound )
	{
		// 交点ありなら0.0
		if( o1.IsIntersect( o2 ) )
			return 0.0;
        else
        {
    		// 交差していない場合は始点または終点のどちらか近い方
    		var distance0 = o1.GetEndPoint(0).GetDistance( o2 );
    		var distance1 = o1.GetEndPoint(1).GetDistance( o2 );
    		return Math.Min( distance0, distance1 );
        }
	}
	else
	{
		// 平行でないなら必ず交点があるはず
		if( o1.IsParallel( o2 ) )
			return o1.Origin.GetDistance( o2 );
		else
			return 0.0;
	}
}
```

平面（無限面）同士は、平行なら一方の任意点と平面との距離となり、平面ではない場合は必ずどこかで接することになるので0になります。

```csharp
// 平面同士の距離算出の実装例
public static double GetDistance( this Plane o1, Plane o2 )
{
    return o1.IsParallel( o2 ) ? GetDistance( o1.Origin, o2 ) : 0.0;
}
```

## 平行線算出

ある線に対して指定点を通る平行線を作成する場合は、線を直線とみなして指定点からの近点(p)を算出し、pから指定点までの移動量分平行移動することで算出できます。

移動量（ベクトル）から移動行列（Transoform）の作成は Transform.CreateTranslation メソッドで、曲線（Curve）の行列変換は Curve.CreateTransformed メソッドで行うことができます。

### Transform.CreateTranslationメソッド（ベクトルから移動行列の作成）

https://www.revitapidocs.com/2024/b1a26f8c-1593-5b74-d78e-d4261ec5ebe5.htm

### Curve.CreateTransformedメソッド（曲線から行列変換後の形状を作成）

https://www.revitapidocs.com/2024/4f014114-64f7-fff1-f24f-bfc6e0cad82a.htm

```csharp
// 指定点を通る平行線算出の実装例
public static Line GetParallelLine( this Line o1, XYZ o2 )
{
	// o1上のo2の近点を取得
	XYZ p = o2.GetNearPoint(o1);
	// pからo2への移動量分平行移動する
	XYZ v = o2 - p;
    Transform trans = Transform.CreateTranslation(v);
    // 元形状が線の平行移動なので返還後も線のはず
	Line line = o1.CreateTransformed( trans ) as Line;
	return line;
}
```

## 平行面算出

平面に対して指定した点を通る平行面の算出は、単純に法線が同一で指定点を原点とする平面を作成することで実現可能です。

平面の生成は Plane.CreateByNormalAndOrigin メソッドで行います。

### Plane.CreateByNormalAndOriginメソッド（法線と原点を指定して平面を作成）

https://www.revitapidocs.com/2024/ab735815-f6bc-aae1-8afc-8505bba18cb5.htm

```csharp
// 指定点を通る平行面算出の実装例
public static Plane GetParallelPlane( this Plane o1, XYZ o2 )
{
	return Plane.CreateByNormalAndOrigin( o1.Normal, o2 );
}
```

## 投影点算出

平面に指定した点を投影した点（投影点）の算出は近点の算出と同義ですので前回の記事をご覧ください。

https://qiita.com/thivanao/items/866bdcaf092ee315e39b#%E7%B7%9A%E4%B8%8A%E5%88%A4%E5%AE%9A%E9%9D%A2%E4%B8%8A%E5%88%A4%E5%AE%9A%E8%BF%91%E7%82%B9%E7%AE%97%E5%87%BA%E5%9E%82%E7%B7%9A%E7%AE%97%E5%87%BA

## 投影線算出

平面に指定した線（線分または直線）を投影した線（投影線）の算出は、始点・終点（直線の場合は原点と任意の面上点）の投影点をそれぞれ求めて、そこから線分または直線を作成することで可能です。

指定した線が面と垂直の位置関係にある場合（もしくはほぼ垂直で、投影点同士の距離がRevitで作成可能な線分の最小長さに満たない場合）の扱いについては注意が必要です。

```csharp
// 平面に対する指定線の投影線算出の実装例
public static Line GetProjectLine( this Plane o1, Line o2 )
{
	if( o2.IsBound )
	{
		var pt0 = o2.GetEndPoint(0).GetNearPoint( o1 );
		var pt1 = o2.GetEndPoint(1).GetNearPoint( o1 );
		return Geom.CreateLine( pt0, pt1 );
	}
	else
	{
		Curve curve = o2.Clone();
		curve.MakeBound( 0.0, 100.0 );
		var pt0 = curve.GetEndPoint(0).GetNearPoint( o1 );
		var pt1 = curve.GetEndPoint(1).GetNearPoint( o1 );
		if( pt0.IsAlmostEqualTo( pt1 ) )
			return null; // 線にならないのでnullを返すこととする
		return Line.CreateUnbound( pt0, pt1 - pt0 );
	}
}
```

以上、2回にわたりRevit APIにおける幾何計算の紹介を行いました。
実装例として挙げさせていただいたコードは精度的な問題や、極限的なケースで不都合がある場合もありますので、厳密な要件が求められる場合ではその点ご注意ください。

次回は、Revitのモデル上の要素の抽出方法（フィルタによる抽出・ユーザ入力による抽出・IDによる抽出）などを整理してお話ししたいと思います。

## このシリーズの予定
(1) [RevitAPIについてのリンク集](https://qiita.com/thivanao/items/cbee5072aa582672888c)
(2) [Revitでの幾何計算(1)](https://qiita.com/thivanao/items/866bdcaf092ee315e39b)
(3) Revitでの幾何計算(2) ★今回★
(4) Revit APIでの要素抽出方法あれこれ
(5) Revitアドインのエントリーポイント
(6) RevitでCADっぽい挙動（ラバー・一時図形・ラージカーソル）を実現してみる
(7) Revitアドイン開発 Debug Tips