KiCADでGUIからバスエントリサイズを変更できるようにした

# はじめに
この記事は「大規模ソフトウェアを手探る」という実験のレポートの子記事です。親記事はこちら

https://qiita.com/ogirin/private/38afdb348fe4acad56a9

KiCADのIssueに挑戦したことを記事にしました。

# 取り組んだIssue 
取り組んだIssueはこちら

https://gitlab.com/kicad/code/kicad/-/issues/13661

バスエントリのサイズをGUIで変更可能にするというIssueです。

KiCADの回路図エディタにはバスという複数の配線をまとめる機能があり、そのバスと配線を繋ぐ部品がバスエントリです。

下の回路図の青い縦線がバス、緑の横線が配線、そしてそれらをつなぐ斜めの線がバスエントリです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/9e46ebe7-99f3-2357-35a0-3814c7644c53.jpeg" width="50%">

KiCAD8.0では一度設置したバスエントリのサイズは変更できません。最初から小さいサイズのバスエントリを設置することはできるのですが、(bus_entry (size 1.27 1.27))という文字列をコピー&ペーストしなければなりません。

バスエントリは斜めの線であること、サイズの変更が簡単にできないことの2点が原因でバスを使った回路図は汚くなりがちです。

この問題をどうするかについては[ここ](https://forum.kicad.info/t/smaller-bus-entry/27976)で議論されているのですが、上記のコピペが紹介されてる以外は根本的な解決には至っておりません。

そこでGUIでサイズを変更になるような機能改善に挑戦しました。

# コードの解析
Issueに対してMaintainerの方が "dialog_wire_bus_properties" を探ると良いとのコメントがされていました。それに則り、KiCADのソースコードの中でdialog_wire_bus_propertiesファイルを探してみたところ以下のような5つのファイルが見つかりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/f3528326-1d50-8223-062a-647a30c9ef5a.png)

コードと実際のKiCADの機能や表示を見比べて解析したところ、これらのファイルは以下のような配線やバス、バスエントリをダブルクリックした時に出てくるダイアログボックスの実装を担っているようでした。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/9412b9f3-b881-7d2f-51fe-72189cb474be.png" width="70%">

[](
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/655932f7-86af-f678-ab49-79cc9e602584.png)
)
具体的には`dialog_wire_bus_properties_base.h`と`dialog_wire_bus_properties_base.cpp`がダイアログボックスの構造や表示部分の実装、そして`dialog_wire_bus_properties.h`と`dialog_wire_bus_properties.cpp`がダイアログボックスの入出力の処理を行っているようでした。

このダイアログボックスを修正してバスエントリサイズを指定できるようにすることを目標としました。

# コードの修正1
すでに実装されているJunction sizeの変更に関する実装を参考にして、バスエントリのサイズを指定できる入力ボックスを以下のように実装しました。

まず、`dialog_wire_bus_properties_base.cpp`を修正してバスエントリサイズの入力欄を作成しました。引数の数字の意味はよくわからなかったので何度もビルドして調整しました。

```c++:dialog_wire_bus_properties_base.cpp
	m_busEntrySizeLabel = new wxStaticText( this, wxID_ANY, _("Bus entry size:"), wxDefaultPosition, wxDefaultSize, 0 );
	m_busEntrySizeLabel->Wrap( -1 );
	gbSizer1->Add( m_busEntrySizeLabel, wxGBPosition( 4, 0 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 );

	m_busEntrySizeCtrl = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 );
	m_busEntrySizeCtrl->SetMinSize( wxSize( 146,-1 ) );

	gbSizer1->Add( m_busEntrySizeCtrl, wxGBPosition( 4, 1 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 );
	
	m_busEntrySizeUnits = new wxStaticText( this, wxID_ANY, _("unit"), wxDefaultPosition, wxDefaultSize, 0 );
	m_busEntrySizeUnits->Wrap( -1 );
	gbSizer1->Add( m_busEntrySizeUnits, wxGBPosition( 4, 2 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxLEFT, 3 );
```

次に`dialog_wire_bus_properties.cpp`を修正して入力されたサイズを反映させる処理を実装しました。
(bus_entry (size 1.27 1.27))の文字列をコピペすると小さいサイズのバスエントリを追加できるという情報に基づいて`bus_entry`の文字列を検索してKiCADの実装を探ったところ、バスエントリのタイプは`SCH_BUS_WIRE_ENTRY_T`で、サイズの情報は`VECTOR2I`として持っていました。 

Junction sizeの実装を真似ながら、`TransferDataToWindow`関数で現在のバスエントリサイズを取得してダイアログに表示する処理、`TransferDataFromWindow`関数で入力された数字を`busEntry->SetSize()`に渡してサイズの変更の処理を行うようにしました。

今回編集しているダイアログは、ワイヤやバスと共通のものなので条件分岐が非常に多いです。選択したアイテムがバスエントリではない場合、`m_busEntrySize.SetValue( INDETERMINATE_ACTION )`として薄く表示して入力できないようにしています。
<details open><summary> dialog_wire_bus_properties.cppの変更箇所</summary>

```c++:dialog_wire_bus_properties.cpp
DIALOG_WIRE_BUS_PROPERTIES::DIALOG_WIRE_BUS_PROPERTIES( SCH_EDIT_FRAME* aParent,
                                                        std::deque<SCH_ITEM*>& aItems ) :
        DIALOG_WIRE_BUS_PROPERTIES_BASE( aParent ),
        m_frame( aParent ),
        m_items( aItems ),
        m_wireWidth( aParent, m_staticTextWidth, m_lineWidth, m_staticWidthUnits ),
        m_junctionSize( aParent, m_dotSizeLabel, m_dotSizeCtrl, m_dotSizeUnits ),
        m_busEntrySize( aParent, m_busEntrySizeLabel, m_busEntrySizeCtrl, m_busEntrySizeUnits )// ここを追加
{
    ... // 省略
}

bool DIALOG_WIRE_BUS_PROPERTIES::TransferDataToWindow()
{
    VECTOR2I      busEntrySize( -1, -1 );

    for( SCH_ITEM* item : m_items )
    {
        if( item->HasLineStroke() )
        {
            stroke = item->GetStroke();
            color = stroke.GetColor();
            if( item->Type() == SCH_BUS_WIRE_ENTRY_T )
            {
                SCH_BUS_ENTRY_BASE* busEntry = static_cast<SCH_BUS_ENTRY_BASE*>( item );
                busEntrySize = busEntry->GetSize();
            }
        }
        ... //省略
    }
    
    if( std::all_of( m_items.begin(), m_items.end(),
            [&]( const SCH_ITEM* item )
            {
                return item->Type() != SCH_BUS_WIRE_ENTRY_T
                        || static_cast<const SCH_BUS_ENTRY_BASE*>( item )->GetSize() == busEntrySize;
            } ) )
    {
        if( busEntrySize.x >=0 && busEntrySize.y >=0 )
            m_busEntrySize.SetValue( busEntrySize.x );
        else
        {
            // No bus entry found in selected items: disable m_busEntrySize
            m_busEntrySize.Enable( false );
            m_busEntrySize.SetValue( INDETERMINATE_ACTION );
        }
    }
    else
    {
        m_busEntrySize.SetValue( INDETERMINATE_ACTION );
    }

}

bool DIALOG_WIRE_BUS_PROPERTIES::TransferDataFromWindow()
{
    for( SCH_ITEM* item : m_items )
    {
        if( item->HasLineStroke() )
        {
            if (item->Type() == SCH_BUS_WIRE_ENTRY_T )
            {
                SCH_BUS_ENTRY_BASE* busEntry = static_cast<SCH_BUS_ENTRY_BASE*>( item );
                if( !m_busEntrySize.IsIndeterminate() )
                {
                    busEntry->SetSize(VECTOR2I(m_busEntrySize.GetValue(), m_busEntrySize.GetValue()));
                }
            }

        }
    }
}
```

</details>



これらの変更によりダイアログボックスにBus entry sizeの入力欄が追加されて大きさを変えられるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/2d3a1493-d8e9-50aa-8e76-e19df7577f7c.png)

# マージリクエストの作成と返信

GUIでバスエントリを変更する機能を実装できたので、マージリクエストの作成をしました。

https://gitlab.com/kicad/code/kicad/-/merge_requests/2049

するとOwnerの方から以下のような返信が...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/f5e84270-6941-2eb5-99fe-89dcb9367c25.png)

大まかに約すと、
1. ダイアログの編集はwxFormbuilderを使って、".fpb"ファイルを作成してね
2. バスエントリサイズは50mil単位に制限されなけれえばならないよ。あらゆる値を受け付ける今の実装ではバグの温床になってしまうよ
3. マージリクエストをするときはrebaseしてね
4. clang-formatを使ってね

というものでした。3,4はKiCADの規約を読んでなかった筆者が悪いのですが、1,2は初耳です。

どうやらKiCADのGUIはwxFormbuilderというツールで作成されており、そこで`*_base.cpp`や`*_base.h`を自動生成しているようです。そのため`*.fpb`ファイルをwxFormbuilderで修正しない限り、次の自動生成の際に今回修正した`*_base.cpp`が上書きされてしまいます。

また、バスエントリサイズは50mil単位にしないといけないというのは、別の数字(例えば35mil)でも表示はできるものの、バスと配線を繋ぐバスエントリサイズとしての役割を果たすかどうかはわからないとのことでした。KiCADの回路図エディタの標準のグリッドサイズが50milのため、バスや配線が置けるのは50mil間隔であり、バスエントリのサイズが変な数字だと上手く接続できないという理由のようです。

# コードの修正2

ダイアログの入力欄を数字入力ではなく、選択肢とすることにしました。

## wxFormBuilder
まずwxFormBuilderで一回目の修正の時には使わなかった`dialog_wire_bus_properties.fpb`ファイルを開きます。(wxFormBuilderのインストールは[こちら](https://github.com/wxFormBuilder/wxFormBuilder))

画面の左にあるObject treeにテキストや入力欄を追加して、そして細かい位置を右側にあるpropertiesで修正します。

修正後の写真は以下の通り。このままfile > Generate Code とするだけで`_base.h`と`_base.cpp`が自動生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/edb1d76c-cfcf-6f4e-9671-28ed64df1fd7.png)

[](
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/60e86f6d-feee-e9cb-6251-8297170b1e08.png)
)


一回目の修正時に直接`_base.cpp`を触って、パラメータの意味がわからないまま、ビルドしては調整、ビルドしては調整と繰り返していたのがこんな簡単にできるなんて...

wxFormbulder、便利すぎます。

## 選択式に変更
バスエントリサイズについて`100mils`か`50mils`かを選ぶ形にしました。

入力式ではなく選択式にした結果、m_junctionSizeに使われている便利なオブジェクトが使えず、KiCADでの単位の実装を探る必要が生じました。

GUIでのデバッガの使い方がわからなかったため、unitという文字列を検索して出てきた関数の定義元を辿るなどして地道に探りました。

その結果、KiCADの内部では独自の単位が使われているということが分かりました。
milからKiCADの内部単位に直すには`EDA_UNIT_UTILS::UI::FromUserUnit`関数を使用する必要があります。`TransferDataToWindow`関数で現在のバスエントリのサイズを確認して`100mils`か`50mils`のどちらかを表示し、`TransferDataFromWindow`関数で入力に応じて`setSize`をしています。

<details open><summary> dialog_wire_bus_properties.cppの変更箇所</summary>

```c++:dialog_wire_bus_properties.cpp
DIALOG_WIRE_BUS_PROPERTIES::DIALOG_WIRE_BUS_PROPERTIES( SCH_EDIT_FRAME* aParent,
                                                        std::deque<SCH_ITEM*>& aItems ) :
        DIALOG_WIRE_BUS_PROPERTIES_BASE( aParent ),
        m_frame( aParent ),
        m_items( aItems ),
        m_wireWidth( aParent, m_staticTextWidth, m_lineWidth, m_staticWidthUnits ),
        m_junctionSize( aParent, m_dotSizeLabel, m_dotSizeCtrl, m_dotSizeUnits )
{

    m_busEntrySizeCombo->Append( "100mils" );
    m_busEntrySizeCombo->Append( "50mils" );
    ... // 省略
}

bool DIALOG_WIRE_BUS_PROPERTIES::TransferDataToWindow()
{
    VECTOR2I      busEntrySize( -1, -1 );

    for( SCH_ITEM* item : m_items )
    {
        if( item->HasLineStroke() )
        {
            stroke = item->GetStroke();
            color = stroke.GetColor();
            if( item->Type() == SCH_BUS_WIRE_ENTRY_T )
            {
                SCH_BUS_ENTRY_BASE* busEntry = static_cast<SCH_BUS_ENTRY_BASE*>( item );
                busEntrySize = busEntry->GetSize();
            }
        }
        ... //省略
    }
    
    if( std::all_of( m_items.begin(), m_items.end(),
                     [&]( const SCH_ITEM* item )
                     {
                         return item->Type() != SCH_BUS_WIRE_ENTRY_T
                                || static_cast<const SCH_BUS_ENTRY_BASE*>( item )->GetSize()
                                           == busEntrySize;
                     } ) )
    {
        if( busEntrySize.x >= 0 && busEntrySize.y >= 0 )
        {
            if( busEntrySize.x
                == EDA_UNIT_UTILS::UI::FromUserUnit( schIUScale, EDA_UNITS::MILS, 50 ) )
                m_busEntrySizeCombo->SetStringSelection( "50mils" );
            else if( busEntrySize.x
                     == EDA_UNIT_UTILS::UI::FromUserUnit( schIUScale, EDA_UNITS::MILS, 100 ) )
                m_busEntrySizeCombo->SetStringSelection( "100mils" );
            else
            {
                m_busEntrySizeCombo->SetStringSelection( INDETERMINATE_STYLE );
            }
        }
        else
        {
            m_busEntrySizeLabel->Enable( false );
            m_busEntrySizeCombo->Enable( false );
            m_busEntrySizeCombo->SetStringSelection( INDETERMINATE_STYLE );
        }
    }
    else
    {
        m_busEntrySizeCombo->SetStringSelection( INDETERMINATE_STYLE );
    }

}

bool DIALOG_WIRE_BUS_PROPERTIES::TransferDataFromWindow()
{
    for( SCH_ITEM* item : m_items )
    {
        if( item->HasLineStroke() )
        {
            // 省略
            if( item->Type() == SCH_BUS_WIRE_ENTRY_T
                && m_busEntrySizeCombo->GetStringSelection() != INDETERMINATE_STYLE )
            {
                if( m_busEntrySizeCombo->GetStringSelection() == "100mils" )

                    static_cast<SCH_BUS_ENTRY_BASE*>( item )->SetSize( VECTOR2I(
                            EDA_UNIT_UTILS::UI::FromUserUnit( schIUScale, EDA_UNITS::MILS, 100 ),
                            EDA_UNIT_UTILS::UI::FromUserUnit( schIUScale, EDA_UNITS::MILS,
                                                              100 ) ) );
                else if( m_busEntrySizeCombo->GetStringSelection() == "50mils" )
                    static_cast<SCH_BUS_ENTRY_BASE*>( item )->SetSize( VECTOR2I(
                            EDA_UNIT_UTILS::UI::FromUserUnit( schIUScale, EDA_UNITS::MILS, 50 ),
                            EDA_UNIT_UTILS::UI::FromUserUnit( schIUScale, EDA_UNITS::MILS, 50 ) ) );
            }
            }

        }
    }
}
```

</details>

修正後のダイアログは以下の通りです。Bus entry sizeの入力欄を押すと50milsか100milsかの選択肢が表示され、どちらかを選ぶことができます。これで大きさを50mil単位に制限できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/0afa620d-2dab-e821-43be-685eb99941f8.png)

# マージリクエスト ~2度目の挑戦~
masterからrebaseもして、clang-formatも使い、万全の状態でpushしました。
するとこんな返信が...

![660BB351-03B6-41AD-A956-242226F84B7F_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/655958cf-dbe9-0e41-6a9a-474a20ced3bb.jpeg)

mm単位を使う人にとっては不便では?とのことでした。

多くの回路素子のピンの間隔が100milであるように、回路の世界では未だヤードポンド法のmil「ミル」やin「インチ」が使われています。そのため、KiCADには以下のように単位をすぐに切り替えられる機能があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/d15cdfde-fb2e-fc5f-4a3e-eed5f7842669.png)

mmを愛用する人にとってはmilで書かれると戸惑うかもしれません。~~とは言っても回路図なんて物理的な長さと全く関係ないし、そもそもニ択なんだからmilでもいいだろと個人的には思うのですが、~~ ありがたいことにメンションまでされたのでユーザーが使っている単位を参照して選択肢を変えることにしました。

# コードの修正3
wxFormBuilderでbus entry sizeの選択窓の右にテキストを追加し、そこに単位を表示することにしました。

色々探った結果、現在ユーザーが使っている単位系は`GetUserUnits`関数で取得できるとわかったので、単位系に応じて数字を単位のmsgと100mils,50milsにそれぞれ対応する`busEntryLargeSize`、`busEntrySmallSize`を更新して表示するようにしました。

また、KiCADの文字列の機能として`_( "a string to translate" )`とすると翻訳する文字列として扱われると教わったので使用しています。

```c++:dialog_wire_bus_properties.cpp
G_WIRE_BUS_PROPERTIES::TransferDataToWindow()
{
    ... //省略

    int largeSizeIndex = m_busEntrySizeCombo->FindString( busEntryLargeSize );
    int smallSizeIndex = m_busEntrySizeCombo->FindString( busEntrySmallSize );
    if( largeSizeIndex != wxNOT_FOUND )
        m_busEntrySizeCombo->Delete( largeSizeIndex );
    if( smallSizeIndex != wxNOT_FOUND )
        m_busEntrySizeCombo->Delete( smallSizeIndex );

    wxString msg;
    switch( GetUserUnits() )
    {
    case EDA_UNITS::INCHES:
        msg = _( "inches" );
        busEntryLargeSize = "0.1";
        busEntrySmallSize = "0.05";
        break;
    case EDA_UNITS::MILS:
        msg = _( "mils" );
        busEntryLargeSize = "100";
        busEntrySmallSize = "50";
        break;
    case EDA_UNITS::MILLIMETRES:
        msg = _( "mm" );
        busEntryLargeSize = "2.54";
        busEntrySmallSize = "1.27";
        break;
    default:
        msg = _( "Units" );
        busEntryLargeSize = _( "Large" );
        busEntrySmallSize = _( "Small" );
        break;
    }
    m_busEntrySizeUnits->SetLabel( msg );
    m_busEntrySizeCombo->Append( busEntryLargeSize );
    m_busEntrySizeCombo->Append( busEntrySmallSize );

}
```

デモ動画がこちらです。GUIでバスエントリのサイズを変更でき、表示が現在の単位系に合わせて変更されています。

![bus_entry_size_final2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/79eaaa42-4cf9-4f22-a13b-7cd70146d9f5.gif)



[](
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/12558703-6a7a-9f3b-8272-9d80bd65d8a4.png)
)

[](
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/f977ab5e-1404-be18-ceb9-7b4534498f5d.png)
)

[](
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927182/c58ffde3-6e87-f027-ecd4-c3d835a08f46.gif" width="150%">
)

# マージリクエスト ~3度目の挑戦~
pullしてマージされることを期待していたのですが3週間経っても音沙汰がありません。一度催促のコメントをしたのですが無視されています。もしかしたら偉い方々の方針と齟齬があってマージされていないのかもしれませんが真相はわかりません。

# 終わりに
締まらない終わり方となってしまいましたが、KiCADを手探って機能追加できたことは事実です。貴重な経験となりました。KiCADの次のバージョンのリリース時にマージされていたりしないかなと淡い期待をしています。

他に取り組んだIssueについても親記事に載っているのでそちらもぜひご覧ください。