Autodesk Inventor API Hacking (SketchEntityがActivatedObject内かどうか)

# 0. はじめに
[Freeradical](https://www.freeradical.jp)の中の人、[yamarah](https://qiita.com/yamarah)です。
`InteractionEvents`などを使ってSketch要素を操作する場合は、対象の`SketchEntity`が編集中の(Activeな)Sketchに含まれるのか、はたまた他のSketchの要素なのかを判断したくなるでしょう。
「`SketchEntity.Parent == ActiveDocument.ActivatedObject`で良いんじゃない?」と思うかもしれませんが、そう簡単にはいかないのです。

# 1. 考慮すべき条件分け
Sketch編集中と言っても、実は色々な状況があります。すぐに思いつくのは、アセンブリ環境でパーツをInPlace編集している場合です。
同じパーツのオカレンスが他にもある可能性があるので、アセンブリ環境ではProxyに対して比較/操作する必要があります。
次に、忘れがちなのが、Sketchブロックです。Sketchブロック編集には2種類あって、通常のSketch編集中にInPlaceでブロック編集する方法と、モデルツリー内のブロックフォルダーから編集する場合です。

ですので、

* 2パターン (アセンブリでのInPlace編集かどうか)
* 3パターン (通常のSketch編集 / 通常のSketch中のInPlaceブロック編集 / ブロックのみを編集)

の掛け合わせで、計6パターンの状況があります。

# 2. ブロックについて
`SketchBlock`は、Sketchにおけるブロックの実体です。
`SketchBlockDefinition`は、`SketchBlock`の雛形です。
Assembly/Partの関係に例えると、次のようになります。

|AssemblyとPartの関係|SketchとSketchBlockの関係|
|:--|:--|
|AssemblyDocument|PlanarSketch (*1)|
|ComponentOccurrence|SketchBlock|
|PartDocument|SketchBlockDefinition|
(*1) SketchBlockを内包する親SketchBlockDefinitionの可能性もある

なお、`SketchBlockDefinition`は`PlanarSketch`を継承していますので、状況によっては`SketchBlockDefinition`を`PlanarSketch`と扱って処理できます。

# 3. ActivatedObjectに入っているもの
次に、`ActiveEditDocument.ActivatedObject`に入っているものを確認しておきましょう。

|状況|ActivatedObject|
|:--|:--|
|通常のSketch編集|PlanarSketch|
|Sketch編集中にBlockをInPlace編集|SketchBlock|
|Blockのみを編集|SketchBlockDefinition|
なんとなく、2番目の処理がやっかいそうなのが分かります。

# 4. 実際のコード
まずは、ActivatedObjectを可能性のある各形式に変換します。

```c#
// PartDocument編集中を確認。
if (!(InventorApplication.ActiveEditDocument is PartDocument partDoc)) return;

// ActivatedObjectを、PlanarSketch, SketchBlockDefinition, SketchBlockに変換してみる。
var activeSketch = partDoc.ActivatedObject as PlanarSketch; // 普通のSketch編集中と、InPlaceではないBlock編集中時に非null
var activeBlockDef = partDoc.ActivatedObject as SketchBlockDefinition;    // InPlaceではないBlock編集中時に非null
var activeBlock = partDoc.ActivatedObject as SketchBlock;   // InPlaceでBlock編集中に非null
```

先に述べた通り、`SketchBlockDefinition`は`PlanarSketch`に変換可能なので注意してください。
次に、`SketchBlock`が取得できた場合は、そこから`activeSketch`と`activeBlockDef`を取得します。

```c#
// InPlaceでBlock編集中時は、activeBlockからactiveSketchとblockDefを取得する。
if (activeBlock != null)
{
    activeBlockDef = activeBlock.Definition;
    activeSketch = activeBlockDef as PlanarSketch;
}
```

この時点では`activeSketch == null`の可能性があるので、それをはじきます。

```c#
// activeSketchがnullの場合は中止。例えば、ActivatedObjectがSketch3Dだった場合とか。
if (activeSketch == null) return;
```

次に、アセンブリ環境中にパーツをInPlace編集している場合を考慮します。その場合は、各ObjectをActiveOccurrence経由のproxyに差し替えます。

```c#
// Assembly環境で、PartDocumentをInPlace編集している可能性を考慮する。
ComponentOccurrence? activeOccurrence = null;
if (partDoc != InventorApplication.ActiveDocument)
{
    // InPlace編集中だった場合
    var topAssyDoc = InventorApplication.ActiveDocument as AssemblyDocument;
    if (topAssyDoc == null) return; // あり得ないはずだが、念のため

    // activeSketchをSketchProxyに変換する。
    object proxy;
    activeOccurrence = topAssyDoc.ComponentDefinition.ActiveOccurrence;
    activeOccurrence.CreateGeometryProxy(activeSketch, out proxy);
    activeSketch = (PlanarSketch)proxy;

    // blockDefが非nullの場合は、activeBlockDef == activeSketchなので、同じproxy値を設定する。
    if (activeBlockDef != null)
    {
        activeBlockDef = (SketchBlockDefinition)proxy;
    }

    // activeBlockが設定されている場合は、同様にSketchBlockProxyに変換する。
    if (activeBlock != null)
    {
        activeOccurrence.CreateGeometryProxy(activeBlock, out proxy);
        activeBlock = (SketchBlock)proxy;
    }
}
```

Proxyに差し替えたので、以後はAssembly中かどうか気にしなくて良いです。
残りの条件分け、Blockがらみの条件を抽出します。

```c#
// SketchBlockを編集中かどうか
bool inSketchBlockEdit = activeBlockDef != null;
// Sketch編集中に、SketchBlockをInPlace編集中かどうか
bool inInplaceSketchBlockEdit = activeBlock != null;

// Debug
System.Diagnostics.Debug.WriteLine($"{nameof(inSketchBlockEdit)} : {inSketchBlockEdit}");
System.Diagnostics.Debug.WriteLine($"{nameof(inInplaceSketchBlockEdit)} : {inInplaceSketchBlockEdit}");
```

ここまでが下準備です。毎回評価する必要はないので、`OnExecute`なり`OnNewEditObject`で更新すると良いでしょう。

では、実際に`SketchEntity`が`ActivatedObject`に含まれるか検証しましょう。
実験用の`SketchEntity`の供給元として、毎度おなじみの`SelectSet`を使います。

```c#
// SelectSetから要素を1つ取る。
var selectSet = InventorApplication.ActiveDocument.SelectSet;   // ActiveEditDocumentではないことに注意
if (selectSet.Count == 0) return;
if (!(selectSet[1] is SketchEntity entity)) return; // Indexは1から始まる
```

やっと実際の確認部分です。

```c#
// entityが、ActivatedObjectの直接の要素かどうか。
bool isDirectChildOfActivatedObject = false;

if (inInplaceSketchBlockEdit)
{
    // Sketch環境でBlockをInPlace編集中は、entityの親が編集中のBlockかどうかで確認する。
    if (activeBlock == entity.ContainingSketchBlock)
    {
        isDirectChildOfActivatedObject = true;
    }
}
else
{
    // それ以外(通常のSketch編集、Blockのみを編集)の場合は、entityの親がactiveSketchかどうかで判断する。
    if (entity.Parent == activeSketch)
    {
        isDirectChildOfActivatedObject = true;
    }
}

// Debug
System.Diagnostics.Debug.WriteLine($"{nameof(isDirectChildOfActivatedObject)} : {isDirectChildOfActivatedObject}");
```

ここで、`isDirectChildOfActivatedObject`の意味ですが、名前の通り、ActivatedObjectに直接所有されている(ユーザーに操作可能に見える)かどうかです。
例えば、通常のSketch編集中に、そのSketchに含まれているBlock内の各要素は、Sketch(== `ActivatedObject`)に内包されていると言えます。しかし、ここで判断したいのは、その要素がActivatedObjectの直接の子要素かどうかなので、Block経由の場合はfalseになって欲しいわけです。

最後に、おまけです。アセンブリ環境でパーツをInPlace編集中に、ある`SketchEntity`がInPlace編集中の`Occurrence`に含まれるかどうかを確認します。
このコードは動作確認していないのですが、問題ないと思います。

```c#
// ActiveOccurrenceに含まれるかどうか
bool inActiveOccurrence = false;
if (activeOccurrence == null)
{
    // アセンブリ中の編集でなければ、常にtrue
    inActiveOccurrence = true;
}
else
{
    var containingSketchProxy = entity.Parent as PlanarSketchProxy;
    if (containingSketchProxy?.ContainingOccurrence == activeOccurrence)
    {
        inActiveOccurrence = true;
    }
}

// Debug
System.Diagnostics.Debug.WriteLine($"{nameof(inActiveOccurrence)} : {inActiveOccurrence}");
```

以上です。

# 5. まとめ
繰り返しになりますが、上記コードを1つにまとめたものを貼っておきます。

```c#
// PartDocument編集中を確認。
if (!(InventorApplication.ActiveEditDocument is PartDocument partDoc)) return;

// ActivatedObjectを、PlanarSketch, SketchBlockDefinition, SketchBlockに変換してみる。
var activeSketch = partDoc.ActivatedObject as PlanarSketch; // 普通のSketch編集中と、InPlaceではないBlock編集中時に非null
var activeBlockDef = partDoc.ActivatedObject as SketchBlockDefinition;    // InPlaceではないBlock編集中時に非null
var activeBlock = partDoc.ActivatedObject as SketchBlock;   // InPlaceでBlock編集中に非null

// InPlaceでBlock編集中時は、activeBlockからactiveSketchとblockDefを取得する。
if (activeBlock != null)
{
    activeBlockDef = activeBlock.Definition;
    activeSketch = activeBlockDef as PlanarSketch;
}

// activeSketchがnullの場合は中止。例えば、ActivatedObjectがSketch3Dだった場合とか。
if (activeSketch == null) return;

// Assembly環境で、PartDocumentをInPlace編集している可能性を考慮する。
ComponentOccurrence? activeOccurrence = null;
if (partDoc != InventorApplication.ActiveDocument)
{
    // InPlace編集中だった場合
    var topAssyDoc = InventorApplication.ActiveDocument as AssemblyDocument;
    if (topAssyDoc == null) return; // あり得ないはずだが、念のため

    // activeSketchをSketchProxyに変換する。
    object proxy;
    activeOccurrence = topAssyDoc.ComponentDefinition.ActiveOccurrence;
    activeOccurrence.CreateGeometryProxy(activeSketch, out proxy);
    activeSketch = (PlanarSketch)proxy;

    // blockDefが非nullの場合は、activeBlockDef == activeSketchなので、同じproxy値を設定する。
    if (activeBlockDef != null)
    {
        activeBlockDef = (SketchBlockDefinition)proxy;
    }

    // activeBlockが設定されている場合は、同様にSketchBlockProxyに変換する。
    if (activeBlock != null)
    {
        activeOccurrence.CreateGeometryProxy(activeBlock, out proxy);
        activeBlock = (SketchBlock)proxy;
    }
}

// SketchBlockを編集中かどうか
bool inSketchBlockEdit = activeBlockDef != null;
// Sketch編集中に、SketchBlockをInPlace編集中かどうか
bool inInplaceSketchBlockEdit = activeBlock != null;

// Debug
System.Diagnostics.Debug.WriteLine($"{nameof(inSketchBlockEdit)} : {inSketchBlockEdit}");
System.Diagnostics.Debug.WriteLine($"{nameof(inInplaceSketchBlockEdit)} : {inInplaceSketchBlockEdit}");

// SelectSetから要素を1つ取る。
var selectSet = InventorApplication.ActiveDocument.SelectSet;   // ActiveEditDocumentではないことに注意
if (selectSet.Count == 0) return;
if (!(selectSet[1] is SketchEntity entity)) return; // Indexは1から始まる

// entityが、ActivatedObjectの直接の要素かどうか。
bool isDirectChildOfActivatedObject = false;

if (inInplaceSketchBlockEdit)
{
    // Sketch環境でBlockをInPlace編集中は、entityの親が編集中のBlockかどうかで確認する。
    if (activeBlock == entity.ContainingSketchBlock)
    {
        isDirectChildOfActivatedObject = true;
    }
}
else
{
    // それ以外(通常のSketch編集、Blockのみを編集)の場合は、entityの親がactiveSketchかどうかで判断する。
    if (entity.Parent == activeSketch)
    {
        isDirectChildOfActivatedObject = true;
    }
}

// Debug
System.Diagnostics.Debug.WriteLine($"{nameof(isDirectChildOfActivatedObject)} : {isDirectChildOfActivatedObject}");

// ActiveOccurrenceに含まれるかどうか
bool inActiveOccurrence = false;
if (activeOccurrence == null)
{
    // アセンブリ中の編集でなければ、常にtrue
    inActiveOccurrence = true;
}
else
{
    var containingSketchProxy = entity.Parent as PlanarSketchProxy;
    if (containingSketchProxy?.ContainingOccurrence == activeOccurrence)
    {
        inActiveOccurrence = true;
    }
}

// Debug
System.Diagnostics.Debug.WriteLine($"{nameof(inActiveOccurrence)} : {inActiveOccurrence}");
```

# 99. 親の記事に戻る
[Autodesk Inventor API Hacking (概略)](https://qiita.com/yamarah/items/cee08eb8f2b7aecbda83)