本記事はアジアクエスト Advent Calendar 2024の記事です。
本記事では、Rhinoceros(以下、Rhino)のプラグイン開発において、UIをReactで作成し、選択されたオブジェクトの情報(名前、レイヤ名、表示色)をReact側に表示する方法と、スライダーを使用してオブジェクトのサイズを変更する方法を紹介します。
通常、RhinoプラグインのUIはWPFやWinForms、Eto.Formsなどのフレームワークを用いて作成されますが、WebViewを活用することで、Reactを使ったモダンなUIを実現できます。
今回の目的は、以下の2点です。
C#とJavaScript間のデータのやり取りを行い、RhinoCommon(RhinoのAPI)を用いてオブジェクトの情報取得とサイズ調整を行います。
C#からJavaScriptの関数を実行する方法、JavaScriptからC#の関数を実行する方法の工夫がポイントです。 なお、基本的にデータの受け渡しは文字列で行います。
※ 本記事では、ReactやRhinoプラグインの環境構築手順は割愛します。必要な環境が既に整っていることを前提としています。
Eto.Forms.WebView
の仕様でイベントハンドラーが限られているため、document.title
の変更を利用してJavaScript側の値をC#側に伝えます。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;
window.updateSelectedObject
関数を呼び出してJavaScript側にデータを送信します。EscapeForJavaScript()
のようにエスケープしてあげないと上手く受け渡しできません。デバッグも面倒なので個人的ハマりポイントでした。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;
}
}
}
Reactアプリの起動
コマンドラインでReactプロジェクトのディレクトリに移動し、以下のコマンドを実行してローカルサーバーを起動します。
npm start
Rhinoプラグインの設定
MyPanel.cs
のGuidAttribute
にユニークなGUIDを設定します。Rhinoプラグインのビルドと実行
ShowReactPanel
コマンドを実行して、パネルを表示します。動作確認
本記事では、Reactで作成したUIを使用して、Rhino上のオブジェクトの情報を表示し、スライダーでサイズを調整する方法を紹介しました。C#とJavaScript間でデータのやり取りを行い、ユーザーインタラクションに応じてRhinoのオブジェクトを操作する実装を解説しました。
UIを作る際はEtoやWPF、WinFormsなどよりもReactなどモダンなフレームワークを使うほうが人的リソース確保の面でも有効かと思います。
RhinocerosではEto UIを使うことが多いので今回はEto.Forms.WebView
を使いましたが、MSのWebView2を使っても実現できると思います。
またお察しの通り、ReactでなくてもJavaScript/TypeScriptに対応した任意のフレームワークで開発が可能ですが、弊社ではReactを使用することが多いので今回はReactで書いてみました。