【VC++＆STEP CADデータ】STEPファイル(AP203)をyamlファイルに変換する


# 過去記事

https://qiita.com/belre/items/d5d8168f68f8cb062c06

https://qiita.com/belre/items/aeab3ba4b0a579927029

今までの記事は紹介みたいなのばかりで、
あまり内容としては重要なものがなかったので、
今回はもう少しSTEPデータの中身に近づけたものを試してみたいと思う。

# 概要

以前やったSTEPファイルをもう少し使える形に変換してみたいということで、
最終的に3Dレンダリングに持っていける形式にする。

STEPファイルはすごくわかりづらい形をしている。
以前まで使っていたスプロケットの例で言うと、

```STEPファイル:KANA_NK35B10D9_K.stp
ISO-10303-21;
HEADER;
FILE_DESCRIPTION( ( 'STEP AP203' ), '1' );
FILE_NAME( 'p:/temp/worker_7/temp/exporttempdir_4092_1410_eV1Z3/format_0/KANA_NK35B10D9_K-_0-N_A_(H 720 N_A  0 0 N_A N_A).stp', '2020-10-28T10:44:47', ( 'License CC BY-ND 4.0' ), ( 'CADENAS' ), ' ', 'PARTsolutions', ' ' );
ENDSEC;
DATA;
#1 = DESIGN_CONTEXT( '', #27, 'design' );
#2 = APPLICATION_PROTOCOL_DEFINITION( 'international standard', 'config_control_design', 1994, #27 );
#3 = PRODUCT_CATEGORY_RELATIONSHIP( 'NONE', 'NONE', #28, #29 );
#4 = DATE_AND_TIME( #30, #31 );
...
```

要は、DATAって書いてあるところに対して、
所定のスキーマAPIに従った関数があてはめられていて、
それを１行ずつパースするようだ。

APIは次のサイトにある：

https://www.steptools.com/stds/stp_aim/html/

だが、CADデータというのは
本来ジオメトリや軸、マテリアルの情報などが構造化されて出ているはずだ。

例えば、

**点→線→面→要素**

と言った具合に定義が大きくなっていく。
当然関数のレベルは、点と要素ではその大きさが全く違っているであろう。

構造化されたデータを記述するのに便利な記法は？
と考えたときに、パッと思いつくのは**XMLファイル**なのだろうが、それは冗長すぎる。
そこで、**Yamlファイル**にすれば多少は構造化されたファイルにしやすいのではないかと考え、サンプルとして作ってみた。

# プロジェクト

https://github.com/belre/STEP_AP203_Preview/tree/main/src/AP203toYaml

## 動作環境

|項目|値|
|---|---|
|IDE(開発環境)|Visual Studio 2019|
|言語|C++14|
|cmake|3.19.0 rc1|
|yaml-cpp|v0.6.0|
|STEPCode|v0.8.1|

submoduleを使っているので注意

## ライブラリ

https://github.com/jbeder/yaml-cpp/tree/1b50109f7bea60bd382d8ea7befce3d2bd67da5f

https://github.com/stepcode/stepcode

## 使用するSTEPデータ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/c67fdf1a-dcda-086b-5a88-9cea92f2cb61.png)

以前ダウンロードしたものを利用。
規格はAP203固定


## 実行結果

result.yamlでこんな感じで出力されるようになった

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/fc3c8011-b1ed-6d0d-d948-c80bd93eac79.png)

だいぶ見やすい。

今までのSTEPファイルは次のように、
上の３行のデータを次の形式でまとめていたが、
#56, #57, #58のデータ内移動が発生しており、
それが何なのかが不明であるため、
一目見て何をしようとしているかだいぶ分かりづらい感じであった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/54a82a00-b08b-404e-7c7f-9e5526f230f3.png)

## 注意点

### 再帰的なノードの実装処理

今回は、とりあえず簡略的に再帰を実装することで、上記のデータを取り出した。

STEPのデータ構造はいわゆるノードリンクモデルであり、階層化されている。
例えば、次のような感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281173/3a9bdb58-263b-9613-7427-fde54664d73b.png)

ただ、一般的なツリー構造とは違い、親と子に対しての数の制約がない。
現に今操作しているファイルも、親が２つの子が１つ、みたいなパターンがあり得るので、
一般的なデータ構造を使用することを考慮できない部分がある。

yamlで表現するには割と悪くないモデルなのではあるが、
親が多重化していると、微妙に扱いづらいので、
この辺りは要改良であろう。

#### 補足

**このデータ構造は、アプリケーション毎で別の制約を持たせることで、変更することは可能。** 
しかし、そんな規格が標準規格であると呼んでいいかは疑問の余地が残る。
例えばFusion 360で作成したSTEPデータは、他のアプリケーションで読めないみたいなことも起きており、ソフトによって生成されるノードの数も全然違う。
（Fusion 360で同データをエクスポートすると60000ノードくらいになって大変だなあ、と思ったり）

### 型表現

STEPデータには、主要な型表現がいくつか存在し、
その中でいくつかオブジェクトが他のノードの参照を行なう。

|型|値|定義|
|---|---|---|
|Entity型|(参照)またはDerive|スキーマのオブジェクト|
|Select型|型|型のクラスを選択し、それが同等の意味を持つように変換する。OOPでいうスーパークラスに近い|
|Aggregate型|配列|値またはInstance型の集合|
|Complex型|([スキーマ1][スキーマ2]...[スキーマN])|N個のスキーマの複合体|

ノードには**attribute**が存在し、
これは、他のノードを参照することを表す。

attributeを以下の形式で変換する。

```C++
/* operator []が定義されているのでこの形式で参照する */
auto attribute = &instance->attributes[j];

auto attr_select = attribute->Select();  // Select型
auto attr_instance = attribute->Entity();  // Entity型
auto attr_aggr = attribute->Aggregate();  // Aggregate型
```

これ以外にも値型(数値や文字列など)も存在する。

### Entity型

１個のオブジェクトを表す。
例として、

```
#1 = DESIGN_CONTEXT( '', #56, 'design' );
```

とあるが、#56を見ると、

```
#56 = APPLICATION_CONTEXT( 'configuration controlled 3D designs of mechanical parts and assemblies' );
```

と書いてあり、これはAPPLICATION_CONTEXTというスキーマ関数を#1から単に実行しているのと同じ。

### Aggregate型

複数のオブジェクトまたは値型を表す。

例として、複数の値型になっている例では、
３次元座標系CARTESIAN_POINT

```
#4946 = CARTESIAN_POINT( '', ( 32.9810691886818, -1.11762031629104, 3.02385093961795 ) );
```

があり、これは分かりやすい。

オブジェクトになっている例として、
スプライン補間をしている線B_SPLINE_CURVE_WITH_KNOTSなどがある。

```
#2734 = B_SPLINE_CURVE_WITH_KNOTS( '', 3, ( #3447, #3448, #3449, #3450, #3451, #3452, #3453, #3454 ), .UNSPECIFIED., .F., .F., ( 4, 2, 2, 4 ), ( 0.00401969515132148, 0.00439619365685814, 0.00477269216239481, 0.00552568917346814 ), .UNSPECIFIED. );
```

と長い表現があるが、#3447～#3454は、それぞれCARTESIAN_POINTが振られており、その各点においてスプライン補間を行なっている線であることを表す。

ちなみに、以上のSPLINEのデータの一部をyamlで表した結果は次の通り。

```yaml:result.yaml
edge_geometry:
  sc_fileid: 2734
  sc_function: B_Spline_Curve_With_Knots
  name: "''"
  degree: 3
  control_points_list:
    - sc_fileid: 3447
      sc_function: Cartesian_Point
      name: "''"
      coordinates:
        - -27.6749055384899
        - -5.60781048700473
        - 4.3
    - sc_fileid: 3448
      sc_function: Cartesian_Point
      name: "''"
      coordinates:
        - -27.7168136134358
        - -5.72658161523084
        - 4.3
    - sc_fileid: 3449
      sc_function: Cartesian_Point
      name: "''"
      coordinates:
        - -27.768318640701
        - -5.84384281529098
        - 4.2993313269661
    - sc_fileid: 3450
      sc_function: Cartesian_Point
      name: "''"
      coordinates:
        - -27.8867645478438
        - -6.06658150505576
        - 4.29557390864968
    - sc_fileid: 3451
      sc_function: Cartesian_Point
      name: "''"
      coordinates:
        - -27.9537675118713
        - -6.17267764724977
        - 4.29249895314713
    - sc_fileid: 3452
      sc_function: Cartesian_Point
      name: "''"
      coordinates:
        - -28.1777375748232
        - -6.47533524682626
        - 4.27799771103716
    - sc_fileid: 3453
      sc_function: Cartesian_Point
      name: "''"
      coordinates:
        - -28.3592036816
        - -6.65692035269854
        - 4.26099657575213
    - sc_fileid: 3454
      sc_function: Cartesian_Point
      name: "''"
      coordinates:
        - -28.5629624822334
        - -6.80713277063426
        - 4.23326633448113
  curve_form: UNSPECIFIED
  closed_curve: F
  self_intersect: F
  knot_multiplicities:
    - 4
    - 2
    - 2
    - 4
  knots:
    - 0.00401969515132148
    - 0.00439619365685814
    - 0.00477269216239481
    - 0.00552568917346814
  knot_spec: UNSPECIFIED
same_sense: T
orientation: F
```

yamlで表すだけで、どこにSPLINE補間があるのか、
B-SPLINE補間の特性がどのような描画をすべきかなどを
明確にできる。

### Select型

複数の型を同一視したいときに使う。

例として、axis2_placementと呼ばれるスキーマがあり、

https://www.steptools.com/stds/stp_aim/html/t_axis2_placement.html

これは原点と１方向の単位ベクトルでなるAXIS2_PLACEMENT_2Dと

https://www.steptools.com/stds/stp_aim/html/t_axis2_placement_2d.html

原点と２つの方向の単位ベクトルでなるAXIS2_PLACEMENT_3D

https://www.steptools.com/stds/stp_aim/html/t_axis2_placement_3d.html

２つが存在している。これらが何を利用されているというと、３次元空間の下で、決められた単位ベクトルの空間で図形を描画するのに使用されている。
（APIの詳細については分析が必要なので、ここでは触れない)

例：

```CIRCLEの場合
#2744 = CIRCLE( '', #3477, 2.54000000000000 );
#3477 = AXIS2_PLACEMENT_3D( '', #5346, #5347, #5348 );
#5346 = CARTESIAN_POINT( '', ( -30.0701715294190, -4.76264728814982, 0.000000000000000 ) );
#5347 = DIRECTION( '', ( 0.000000000000000, 0.000000000000000, 1.00000000000000 ) );
#5348 = DIRECTION( '', ( 1.00000000000000, 0.000000000000000, 0.000000000000000 ) );
```

このSelect型をAXIS2_PLACEMENT_2Dにしても、データ構造としては問題ありませんよ、という意味である。
よって、このAXIS2_PLACEMENTは２つのクラスのスーパークラスの意味合いに近いとされる。

しかし、STEPCodeでは現状このSelectorがしっかり機能していない（変換した型をポインタで変換する方法がない）ようなので、ちょっと工夫が必要で、
STEPのデータはここで#～と書かれているようなので、「～」の部分の値を数値化して読むことで工夫した

```C++:main.cpp
std::string out_text;
attr_select->STEPwrite(out_text);

// ap203 only?
std::regex id_detect_regex("\\#(\\d+)");
std::smatch match;
if(!std::regex_search(out_text, match, id_detect_regex) || 
 					match.length() < 2)
{
	std::cout << "Regex Pattern Match failed" << std::endl;
	continue;
}

int id = 0;
try 
{
	id = std::stol(match[1].str());
}
catch(exception) 
{
	std::cout << "Regex ID error" << std::endl;
	continue;
}

auto select_entity = inst_mgr->FindFileId(id);
if(select_entity == nullptr) 
{
	std::cout << "Regex Conversion error" << std::endl;
	continue;
}

auto select_instance = select_entity->GetApplication_instance();
```

ここで、inst_mgrというのはInstanceの管理をしているクラスであり、
そこに定義されているFileIdを直接読み取ることでSelectorを取り出している。

この部分はIssueでも議論されていたようなので、
上手い方法があれば解決に向かうかもしれないが、現状は難しい。
（C++では動的なクラス型があるわけではないので・・・）

### Complex型

STEPでは、次のような表記がある

```
#99 =  ( NAMED_UNIT( * )PLANE_ANGLE_UNIT(  )SI_UNIT( $, .RADIAN. ) );
```

これはスキーマを複数個連ねていることから、**Complex型**として定義され、別の表現法を用いなくてはいけない。

次のようにすると、Complex型を取り出せる。

```C++
for (auto iter = instance_complex->head; iter != nullptr; iter = iter->sc)
{
  auto inst = dynamic_cast<SDAI_Application_instance*>(iter);
  // instをcomplex型のSDAI_Application_instance*型として
  // 取得しなおすことが出来る。
}
```

# 効果

このようにすると、規定したyamlファイルにおいて、どこを変えたらそのパラメータを反映されるかを可視化出来るようになる。
例えば、今回のSTEPデータの変換結果で、

```yaml
- sc_fileid: 49
  sc_function: Shape_Definition_Representation
  definition:
    sc_fileid: 89
    sc_function: Product_Definition_Shape
    name: "'NONE'"
    description: "'NONE'"
    definition:
      sc_fileid: 85
      sc_function: Product_Definition
      id: "'_SET-SCREW5-5'"
      description: "'_SET-SCREW5-5'"
      formation:
        sc_fileid: 84
        sc_function: Product_Definition_Formation_With_Specified_Source
        id: "' '"
        description: "'NONE'"
        of_product:
          sc_fileid: 86
          sc_function: Product
          id: "'_SET-SCREW5-5'"
          name: "'_SET-SCREW5-5'"
          description: "'PART-_SET-SCREW5-5-DESC'"
          frame_of_reference:
            - sc_fileid: 107
              sc_function: Mechanical_Context
              name: "''"
              frame_of_reference:
                sc_fileid: 56
                sc_function: Application_Context
                application: "'configuration controlled 3D designs of mechanical parts and assemblies'"
              discipline_type: "'mechanical'"
        make_or_buy: NOT_KNOWN
      frame_of_reference:
        sc_fileid: 1
        sc_function: Design_Context
        name: "''"
        frame_of_reference:
          sc_fileid: 56
          sc_function: Application_Context
          application: "'configuration controlled 3D designs of mechanical parts and assemblies'"
        life_cycle_stage: "'design'"
...(省略)...

```

という感じだったが、実は「_SET-SCREW5-5」は随所にあり、ここをFreeCADで変えたりすると、反映されている部分が見づらかったのだ。
それを、86:nameの値を変えるだけで分かるというのが一目で分かったというだけでも結構大きな効果である。

ただし、この変更がどのように名前として割り当てられるのかについては、CADソフトによって挙動が違うはずなので、完全にワンルールではなかった（Free CADでは、_SET-SCREW5-5が２つ読み込まれていたので、もう１つの方のモデルの名前が_SET-SCREW5-006に変換されてしまっていた）
このあたり、フリーで環境を作るときにいろいろ吟味できると面白いのかなと思う。
CADソフトウエアの問題点などを評価するのにも使えるはずなので、いろいろ考えていきたい。