RhinocerosプラグインのUIをReactで作る

RhinocerosプラグインのUIをReactで作る

本記事はアジアクエスト Advent Calendar 2024の記事です。

目次

    0. 概要

    本記事では、Rhinoceros(以下、Rhino)のプラグイン開発において、UIをReactで作成し、選択されたオブジェクトの情報(名前、レイヤ名、表示色)をReact側に表示する方法と、スライダーを使用してオブジェクトのサイズを変更する方法を紹介します。

    通常、RhinoプラグインのUIはWPFWinFormsEto.Formsなどのフレームワークを用いて作成されますが、WebViewを活用することで、Reactを使ったモダンなUIを実現できます。

    今回の目的は、以下の2点です。

    1. 選択されたオブジェクトの情報をReact側に表示する。
    2. Reactで作成したスライダーを操作して、オブジェクトのサイズを倍率で変更する。

    完成イメージ
    202412_rhinoceros_01

    C#とJavaScript間のデータのやり取りを行い、RhinoCommon(RhinoのAPI)を用いてオブジェクトの情報取得とサイズ調整を行います。

    C#からJavaScriptの関数を実行する方法、JavaScriptからC#の関数を実行する方法の工夫がポイントです。 なお、基本的にデータの受け渡しは文字列で行います。

    1. 環境

    • OS: Windows 11
    • Rhinoceros: バージョン8
    • .NET: バージョン7
    • React: 18.3.1

    ※ 本記事では、ReactやRhinoプラグインの環境構築手順は割愛します。必要な環境が既に整っていることを前提としています。

    2. React側の準備

    • JavaScriptからC#への通信:
      • Eto.Forms.WebViewの仕様でイベントハンドラーが限られているため、document.titleの変更を利用してJavaScript側の値をC#側に伝えます。
    • C#からJavaScriptへの通信:
      • window.updateSelectedObject関数を定義し、C#側から選択されたオブジェクトの情報を受け取ります。

    ディレクトリ構成

    project-root/
    ├── public/
    │ └── index.html
    ├── src/
    │ └── App.jsx
    ├── package.json
    └── その他の設定ファイル

    各ファイルのソースコード

    src/App.jsx

    import React, { useEffect, useState } from 'react';

    function App() {
    const [sliderValue, setSliderValue] = useState(1);
    const [objectName, setObjectName] = useState('');
    const [layerName, setLayerName] = useState('');
    const [displayColor, setDisplayColor] = useState('');

    useEffect(() => {
    // スライダーの値が変更されたときにC#に通知
    document.title = `size:${sliderValue}`;
    }, [sliderValue]);

    useEffect(() => {
    // グローバル関数を定義してC#からデータを受け取る
    window.updateSelectedObject = function(data) {
    setObjectName(data.name);
    setLayerName(data.layer);
    setDisplayColor(data.color);
    };

    () => {
    delete window.updateSelectedObject;
    };
    }, []);

    const handleSliderChange = (event) => {
    const newValue = event.target.value;
    setSliderValue(newValue);
    };

    return (
    <div>
    <h1>Rhino React UI</h1>
    <div>
    <p>オブジェクト名: {objectName}</p>
    <p>レイヤ名: {layerName}</p>
    <p>表示色: {displayColor}</p>
    <div
    style={{
    width: '50px',
    height: '50px',
    backgroundColor: displayColor,
    border: '1px solid #000',
    }}
    ></div>
    </div>
    <label>
    オブジェクトのサイズ倍率:
    <input
    type="range"
    min="0.1"
    max="5"
    step="0.1"
    value={sliderValue}
    onChange={handleSliderChange}
    />
    {sliderValue}
    </label>
    </div>
    );
    }

    export default App;

    3. Rhinoceros側の準備

    • C#からJavaScriptへの通信:
      • 選択されたオブジェクトの情報を取得し、window.updateSelectedObject関数を呼び出してJavaScript側にデータを送信します。
      • EscapeForJavaScript()のようにエスケープしてあげないと上手く受け渡しできません。デバッグも面倒なので個人的ハマりポイントでした。
    • JavaScriptからC#への通信:
      • document.titleを使用して値をC#側に伝えます。

    ディレクトリ構成

    RhinoPlugin/
    ├── RhinoPlugin.csproj
    ├── MyPlugin.cs
    ├── MyPanel.cs
    ├── MyCommand.cs
    └── その他のプラグインファイル

    各ファイルのソースコード

    MyPlugin.cs

    using System;
    using Rhino;
    using Rhino.PlugIns;
    using Rhino.UI;

    namespace MyRhinoPlugin
    {
    public class MyPlugin : PlugIn
    {
    public MyPlugin()
    {
    Instance = this;
    }

    public static MyPlugin Instance { get; private set; }
    }
    }

    MyPanel.cs

    using System;
    using Eto.Forms;
    using Rhino;
    using Rhino.UI;
    using System.Runtime.InteropServices;
    using Rhino.DocObjects;
    using Rhino.Geometry;

    namespace MyRhinoPlugin
    {
    [Guid("YOUR-PANEL-GUID-HERE")] // 例: [Guid("12345678-1234-1234-1234-1234567890ab")]
    public class MyPanel : Panel
    {
    private WebView _webView;
    private Guid _objectId; // 操作対象のオブジェクトID
    private BoundingBox _originalBoundingBox; // 元のオブジェクトのバウンディングボックス
    private double _originalSize; // 元のオブジェクトのサイズ(対角線の長さ)

    public MyPanel()
    {
    InitializeComponents();

    // Rhinoのイベントハンドラを設定
    RhinoDoc.SelectObjects += OnSelectObjects;
    }

    private void InitializeComponents()
    {
    var serverUrl = "http://localhost:3000";

    _webView = new WebView
    {
    Url = new Uri(serverUrl),
    };

    // DocumentTitleChangedイベントをハンドル
    _webView.DocumentTitleChanged += OnDocumentTitleChanged;

    Content = _webView;
    }

    // オブジェクト選択イベントハンドラ
    private void OnSelectObjects(object sender, RhinoObjectSelectionEventArgs e)
    {
    if (e.Selected)
    {
    // 最初に選択されたオブジェクトのIDとサイズを保存
    _objectId = e.RhinoObjects[0].Id;
    var obj = e.RhinoObjects[0];
    _originalBoundingBox = obj.Geometry.GetBoundingBox(true);
    _originalSize = _originalBoundingBox.Diagonal.Length;

    // オブジェクトの名前、レイヤ名、表示色を取得
    var objectName = obj.Name ?? "";
    var layerName = obj.Document.Layers[obj.Attributes.LayerIndex].Name;
    var color = obj.Attributes.DrawColor(obj.Document);

    var colorString = $"rgb({color.R}, {color.G}, {color.B})";

    // エスケープ処理
    var escapedObjectName = EscapeForJavaScript(objectName);
    var escapedLayerName = EscapeForJavaScript(layerName);
    var escapedColorString = EscapeForJavaScript(colorString);

    // JavaScriptの関数を呼び出してReact側にデータを送信
    var script = $"window.updateSelectedObject();";
    _webView.ExecuteScriptAsync(script);
    }
    }

    // DocumentTitleChangedイベントでサイズ変更を検知
    private void OnDocumentTitleChanged(object sender, WebViewTitleEventArgs e)
    {
    if (e.Title.StartsWith("size:"))
    {
    var sizeStr = e.Title.Substring(5);
    UpdateObjectSize(sizeStr);
    }
    }

    // オブジェクトのサイズを更新
    private void UpdateObjectSize(string sizeStr)
    {
    if (_objectId == Guid.Empty || _originalSize == 0) return;

    if (double.TryParse(sizeStr, out var scaleFactor))
    {
    var doc = RhinoDoc.ActiveDoc;
    var obj = doc.Objects.FindId(_objectId);
    if (obj == null) return;

    // 現在のバウンディングボックス
    var currentBoundingBox = obj.Geometry.GetBoundingBox(true);
    var currentSize = currentBoundingBox.Diagonal.Length;

    // 必要なスケール倍率を計算
    var requiredScale = (scaleFactor * _originalSize) / currentSize;

    // スケーリングの変換行列を作成
    var center = _originalBoundingBox.Center;
    var xform = Transform.Scale(center, requiredScale);

    // オブジェクトをスケーリング
    doc.Objects.Transform(_objectId, xform, true);
    doc.Views.Redraw();
    }
    }

    // JavaScript用に文字列をエスケープ
    private string EscapeForJavaScript(string s)
    {
    if (string.IsNullOrEmpty(s)) return "";

    return s.Replace("\\", "\\\\").Replace("'", "\\'");
    }
    }
    }

    MyCommand.cs

    using Rhino;
    using Rhino.Commands;
    using Rhino.UI;

    namespace MyRhinoPlugin
    {
    public class MyCommand : Command
    {
    public MyCommand()
    {
    Instance = this;
    }

    public static MyCommand Instance { get; private set; }

    public override string EnglishName => "ShowReactPanel";

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
    // コマンド実行時にパネルに登録
    var panelId = typeof(MyPanel).GUID;
    Panels.OpenPanel(panelId);

    return Result.Success;
    }
    }
    }

    4. 動作手順

    手順

    1. Reactアプリの起動

      コマンドラインでReactプロジェクトのディレクトリに移動し、以下のコマンドを実行してローカルサーバーを起動します。

      npm start
    2. Rhinoプラグインの設定

      • MyPanel.csGuidAttributeにユニークなGUIDを設定します。
    3. Rhinoプラグインのビルドと実行

      • Rhinoプラグインをビルドし、Rhinoにロードします。
      • RhinoのコマンドラインでShowReactPanelコマンドを実行して、パネルを表示します。
    4. 動作確認

      • Rhino上でサイズを変更したいオブジェクトを選択します。
      • パネル内に選択されたオブジェクトの名前、レイヤ名、表示色が表示されていることを確認します。
      • スライダーを操作し、オブジェクトのサイズが元のサイズに対する倍率で変更されることを確認します。

    5. まとめ

    本記事では、Reactで作成したUIを使用して、Rhino上のオブジェクトの情報を表示し、スライダーでサイズを調整する方法を紹介しました。C#とJavaScript間でデータのやり取りを行い、ユーザーインタラクションに応じてRhinoのオブジェクトを操作する実装を解説しました。

    UIを作る際はEtoやWPF、WinFormsなどよりもReactなどモダンなフレームワークを使うほうが人的リソース確保の面でも有効かと思います。

    RhinocerosではEto UIを使うことが多いので今回はEto.Forms.WebViewを使いましたが、MSのWebView2を使っても実現できると思います。
    またお察しの通り、ReactでなくてもJavaScript/TypeScriptに対応した任意のフレームワークで開発が可能ですが、弊社ではReactを使用することが多いので今回はReactで書いてみました。

    アジアクエスト株式会社では一緒に働いていただける方を募集しています。
    興味のある方は以下のURLを御覧ください。