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

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

（前回の記事はこちら）

https://qiita.com/thivanao/items/cbee5072aa582672888c

## 今回の内容

今回は、CADアプリケーションなどで欠かせない各種「幾何計算」をRevit APIでどのように行うかを2回に分けてご紹介します。

:::note info
「CADの幾何計算」という用語が一般的な表現かどうかは微妙ですが（汗）、前職（CADベンダー）では「平行判定」「線上・面上判定」「近点・交点・投影点の算出」などの幾何学的計算処理（ライブラリ）を「幾何計算（ライブラリ）」と称しておりましたので、当記事ではそれらをまとめて「幾何計算」と呼ばせていただきます。
:::

最初に、本記事での用語を整理しておきます。（数学的な呼称とは一部異なる場合がありますが、便宜上以下の表現で統一させていただきます）

|用語|解説|
|:-|:-|
|点|単一の座標値で表現される、空間上の位置表現（三次元または二次元）|
|直線|始終点を持たない線（無限線）|
|線分|始点と終点を持つ線|
|線|直線と線分をまとめた表現|
|無限平面|区切られた領域を持たない平面|
|フェース|同一平面上の領域で区切られた平面（不正確な表現ですが本稿では便宜上こう呼称します）|
|平面|無限平面とフェースをまとめた表現|

Revit APIにて幾何計算を行うための道具には以下のようなものが用意されています。

|クラス名   |解説|
|:-        |:-|
|XYZ       |(X,Y,Z)で構成される三次元座標表現。ベクトルとして使用される場合もある。|
|UV        |(U,V)で構成される二次元座標表現。ベクトルとして使用される場合もある。|
|Line      |始点(endpoint1)・終点(endpoint2)で構成される線分(BoundLine)、または原点(Origin)と方向ベクトル(Direction)で構成される（無限）直線(UnBoundLine)を表現する。|
|Plane     |原点(Origin)と法線ベクトル(Normal)で構成される（無限）平面を表現する。|
|PlanarFace|同一平面上にある複数の点により構成される面を表現する。|

## 幾何計算の種類

幾何計算を利用したくなるシチュエーションはいろいろですが、既に配置されているオブジェクトの座標値やユーザが入力した座標値を元にして、それらの位置関係や距離・交差の有無やその座標値を知りたくなる機会はアドイン開発においては少なくないはずです。

先程紹介した各種クラスを用いて、以下のような幾何計算を定義することを検討していきます。

|幾何計算の種類|戻り値|解説|
|:-       |:-|:-|
|等値判定  |bool|同じ種類同士（点と点、線分と線分など）で、浮動小数点表現により発生する誤差含みで同値とみなされるもの同士かを判定|
|平行判定  |bool|ベクトル同士・線同士・平面同士で、平行の位置関係にあるかを判定|
|直行判定  |bool|ベクトル同士・線同士・平面同士で、向きが垂直方向にあるかを判定（位置関係のみの判定で交差しているかどうかは不問）|
|線上判定  |bool|点と線で、線上に点が存在するかを判定|
|面上判定  |bool|平面と点・平面と線で、平面上に点や線が存在するかを判定|
|交差判定  |bool|線同士・線と平面・平面同士とで、それらが交差するかどうかを判定|
|距離算出  |double|点・線・無限平面の任意の２つの組み合わせで、最も近い部分の距離を返す（線上・平面上・交差ありの場合などは0を返す）|
|近点算出  |XYZ|点と線・点と平面とで、点の垂線の足（最近点）の点を返す|
|垂線算出  |Line|点と線・点と平面とで、点と近点との区間の線を返す|
|交点算出  |XYZ|線同士・線と平面・平面同士で、それらが交差している点を返す|
|交線算出  |Line|平面同士で、それらが交差している線を返す|
|平行線算出|Line|線と点・線と平面とで、点を通るor平面上の平行な直線を返す|
|平行面算出|Plane|平面と点・平面と線とで、点または線を通る平行な平面を返す|
|投影点算出|XYZ|点と平面とで、平面上に投影された点を返す（近点と同じ）|
|投影線算出|Line|線と平面とで、平面上に投影された線を返す|

:::note info
交差判定と交点・交線算出、近点算出と垂線算出など、一方の処理の過程で別の判定・算出も同時に行われるものも存在するので、効率化の観点から呼び出し方法などを工夫する必要があるケースがあります。
:::

## 等値判定

浮動小数点数を扱ったことがある方には釈迦に説法かとは思いますが、doubleやfloatなどの浮動小数点数においては「同一の値」であることの判定に単純な==（同値判定）を行うことは基本的に禁忌です。

https://jpdscore.github.io/blog/vs/what-developers-should-know-about-floating-point/

基本的な考え方としては上記に示す通り、二つの浮動小数点の値の差の絶対値が、許容する誤差の範囲内であるかどうかで判定することとなるため、あらかじめ以下のような「ほぼ0か？」を判定する処理を用意しておきます。

```csharp
// ２つのdouble値の比較の実装例
public static int Compare( double o1, double o2, double tolerance = RevitTolerance )
{
	double result = o1 - o2;
	if( Math.Abs( result ) < tolerance )
		return 0;
	return result < 0.0 ? -1 : 1;
}
```
```csharp
// double値が誤差含みで同じか？の実装例
public static bool IsAlmostEqualTo( this double o1, double o2, double tolerance = RevitTolerance )
{
	return Compare( o1, o2, tolerance ) == 0;
}
```

座標値に関しては、Revit APIのXYZクラスに等値判定を行うメソッド（IsAlmostEqualTo）が用意されているので利用可能です。

### XYZ.IsAlmostEqualToメソッド

https://www.revitapidocs.com/2024/dcad6f16-9898-2f6f-1325-5bae45de241a.htm

これらを駆使し、点・線・平面を構成する情報が「誤差含み」で同じであるかを判定する処理を実装できます。

## 平行判定・直行判定

Revit APIのXYZクラスには、XYZクラスをベクトルとして扱った場合に利用できるベクトルの内積（ドット積：DotProduct）及びベクトルの外積（クロス積：CrossProduct）のメソッドが提供されています。

### XYZ.DotProductメソッド

https://www.revitapidocs.com/2024/63e0ee6c-b612-7140-7805-d32c10f7a8bc.htm

https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%83%E3%83%88%E7%A9%8D

### XYZ.CrossProductメソッド

https://www.revitapidocs.com/2024/c5c099ad-e9f5-976b-94ee-d96af1c677f3.htm

https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E7%A9%8D

これらのベクトル積の計算と等値判定等を組み合わせることで、平行判定・直行判定を実装可能です。

```csharp
// 平行判定の実装例
public static bool IsParallel( this XYZ v1, XYZ v2, double tolerance = RevitTolerance )
{
	return IsAlmostEqualTo( v1.CrossProduct( v2 ).GetLength(), 0.0, tolerance );
}
```
```csharp
// 直行判定の実装例
public static bool IsOrthogonal( this XYZ v1, XYZ v2, double tolerance = RevitTolerance )
{
	return IsAlmostEqualTo( v1.DotProduct( v2 ), 0.0, tolerance );
}
```

## 線上判定・面上判定・近点算出・垂線算出

Revit APIのLineクラスとPlaneクラスには、それぞれパラメタとして点（XYZ）を指定して「線上」「面上」の投影点を返すメソッド（Project）が提供されています。

:::note info
厳密にはこれらのProjectメソッドは、Lineクラスの基本クラスであるCurveやPlaneクラスの基本クラスであるSurfaceクラスによって提供されています。
:::

### Curve.Projectメソッド

https://www.revitapidocs.com/2024/b87fc3e4-ea25-2a75-5b5a-53065b099d2a.htm

### Surface.Projectメソッド

https://www.revitapidocs.com/2024/802cc09b-d0a4-dfc5-8ca1-e8c5e8cd4ced.htm

これらのメソッドからの戻り値及び出力パラメタにより、近点と距離が取得できるのでそれらの情報を利用します。

【線上・面上判定】

* 距離が誤差含みで0であれば線上・面上である
* 線分やフェースなど、範囲や領域が限定されているものに対しての判定を行う場合は、投影点が範囲・領域上にあるかどうかの判定も必要

```csharp
// 線上判定の実装例
public static bool IsOnLine( this XYZ o1, Line o2, double tolerance = RevitTolerance )
{
	IntersectionResult result = o2.Project( o1 );
	if( result == null )
		return false;
	double distance = result.Distance;
	return IsAlmostEqualTo( 0.0, distance, tolerance );
}
```
```csharp
// 面上判定の実装例
public static bool IsOnPlane( this XYZ o1, Plane o2, double tolerance = RevitTolerance )
{
	o2.Project( o1, out UV uv, out double distance );
	return IsAlmostEqualTo( 0.0, distance, tolerance );
}
```

【近点算出】

* 取得した近点を返す
* 線分やフェースなど、範囲や領域が限定されているものに対して算出を行う場合は、投影点が範囲・領域上になかった場合にどう扱うかの考慮も必要（nullを返す、最も近い範囲・領域の点との距離を計算して返す、等）

```csharp
// 近点算出（線）の実装例
public static XYZ GetNearPoint( this XYZ o1, Line o2, double tolerance = RevitTolerance )
{
	var result = o2.Project(o1);
	return result.XYZPoint;
}
```

【垂線算出】

* 近点を求め、指定点との間の線分を作成して返す
* 線分やフェースなど、範囲や領域が限定されているものに対して算出を行う場合は、投影点が範囲・領域上になかった場合にどう扱うかの考慮も必要（nullを返す、等）
* 線上点・面上点を指定した場合のように、垂線の長さが0になる場合の考慮も必要（nullを返す、等）

```csharp
// 垂線算出の実装例
public static Line GetPerpendicularLine( this XYZ o1, Line o2, double tolerance = RevitTolerance )
{
	Line line = o2;
	if( o2.IsBound )
		line = o2.ToUnboundLine();
	XYZ onLine = GetNearPoint( o1, line, tolerance );
	return Geom.CreateLine( o1, onLine );
}
```

:::note warn
幾何計算に限らずRevitにおけるジオメトリ作成時の注意点として、線分などの曲線を作成する際は隣接する２点間の距離がRevit APIの Application.ShortCurveTolerance で取得できる最小値より大きい状態である必要があります。（最小値より小さいと実行時に例外が発生する）
上記 Geom.CreateLine は 指定する２点間の距離と Application.ShortCurveTolerance とを比較し、最小値より小さい場合はnullを返すよう実装しているものとします。
:::

続きは次回ご紹介いたします。お楽しみに！

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