
要件定義
RhinoComputeを用いた3Dボックス生成およびWeb上での表示アプリケーションの開発に向けて、具体的な要件を整理します。
これにより、開発の全体像を把握し、必要な作業を明確にします。
1.プロジェクトの目的
目標
- RhinoComputeを活用し、Pythonスクリプトで生成した3DボックスデータをReactアプリケーション上に表示するシステムを構築する。
- PythonとReact間の通信を通じて、動的に3DデータをWeb上に描画できるアプリケーションを開発する。
2.システムの概要
本プロジェクトでは、以下のシステム構成を採用します:
Python
RhinoCommonを使用して3Dジオメトリ(ボックス)を生成。
Flaskを用いてボックスデータをJSON形式で提供するAPIを構築。
RhinoCompute
ローカル環境でRhinoのコア機能を外部プログラムから呼び出すAPIサーバー。
PythonスクリプトからRhinoComputeを利用して3Dモデルを作成。
React
Three.jsを活用し、Webアプリケーション上で3Dボックスを表示。
Flask APIから取得した3Dデータを動的にレンダリング。
機能要件
(1) Pythonスクリプト
- RhinoCommonを使用して3Dボックスを作成する機能。
- RhinoCompute APIとの連携。
- JSON形式で3Dデータを出力する仕組み。
(2) Flask API
- Pythonスクリプトで生成した3Dボックスデータを提供するAPIを構築。
- APIエンドポイントを作成(例:
/box)。
(3) Reactフロントエンド
- Flask APIから取得したJSONデータをThree.jsで描画。
- 3Dモデルの基本操作(回転、拡大縮小)が可能なUIの提供。
- ユーザーフレンドリーなデザイン。
非機能要件
- パフォーマンス
3Dモデルの描画がスムーズに行われること(60fpsを目標)。 - セキュリティ
Flask APIはローカルで利用する前提とし、外部アクセスを制限。 - スケーラビリティ
他の3Dジオメトリ(例: 球体やカスタムオブジェクト)への拡張が容易であること。
技術要件
(1) 使用ツール・フレームワーク
- RhinoCompute(ローカルでのAPIサーバー)
- Python 3.9以上
- Flask(軽量Webフレームワーク)
- React(フロントエンドフレームワーク)
- Three.js(3Dレンダリングライブラリ)
(2) 環境要件
- OS: Windows 11 以上(Rhino8 対応)
- ブラウザ: 最新のGoogle Chrome
- Node.js: バージョン18以上
- Rhino 8: インストール済み
(3) 必要なライブラリ
- Python:
compute-rhino3dFlask
- React:
three@react-three/fiber@react-three/drei
開発のスケジュール
環境構築(1日)
- RhinoComputeのセットアップ
- 必要なライブラリのインストール
Pythonスクリプトの作成(2日)
- RhinoCommonでボックス生成
- Flask APIの作成とテスト
Reactフロントエンドの構築(3日)
- Three.jsを用いた3Dモデル描画
- Flask APIとの通信機能実装
統合テストとデバッグ(1日)
- PythonとReact間のデータ連携確認
- 描画やインタラクションの調整
成果物の仕様
3Dモデルの表示機能
- Pythonスクリプトで生成されたボックスをReactアプリ上に描画。
基本操作
- ユーザーがマウスでモデルを回転、拡大縮小できる。
拡張性
- ボックス以外の形状にも簡単に対応可能。
ディレクトリ構成
プロジェクトルート/
├── backend/ # Flask バックエンド
│ ├── compute.rhino3d
│ ├── server.py # Flask サーバー
│ ├── requirements.txt # Python 依存ライブラリ
│ ├── rhino_compute_utils.py # RhinoCompute 用のユーティリティ関数
│ └── Dockerfile
│
├── frontend/ # React フロントエンド
│ ├── public/ # 静的ファイル
│ │ ├── index.html # React エントリーポイント
│ │ └── favicon.ico # アイコン
│ │
│ ├── src/ # ソースコード
│ │ ├── App.js # メインコンポーネント
│ │ ├── index.js # React エントリーポイント
│ │ └── components/ # コンポーネントフォルダ
│ │ └── BoxCanvas.js # 3D ボックス描画用コンポーネント
│ │
│ ├── package.json # React 依存ライブラリ
│ ├── package-lock.json # ライブラリバージョンロックファイル
│ └── Dockerfile
│
├── docker-compose.yml # Docker 設定
└── README.md # プロジェクト概要
環境構築
RhinoComputeセットアップ
backendディレクトリを作成します。
mkdir backend
compute.rhino3dをbackendディレクトリにコピーして、rhino.computeに移動します。
cd backend/compute.rhino3d/src/rhino.compute/
環境変数を設定して、.NET8.0を使用させます。
set DOTNET_ROLL_FORWARD=Major
RhinoComputeを起動します。
dotnet run
エンドポイントをテスト
Rhino.Compute コンソール アプリケーションを実行している状態で、これらのエンドポイントのいずれかを参照して、すべてが動作していることを確認します。
- http://localhost:6500/version
- http://localhost:6500/healthcheck
- http://localhost:6500/activechildren
- http://localhost:6500/sdk
Pythonライブラリのインストール
backendディレクトリで以下を実行します。
pip install compute-rhino3d
pip install rhino3dm
pip install flask-cors
Pythonスクリプトの作成
RhinoCommonでボックス生成
RhinoComputeを利用して3Dボックスを生成するPythonスクリプトを作成します。
rhino-box.py
import compute_rhino3d.Util
import compute_rhino3d.Brep
import json
# RhinoComputeサーバーの設定
compute_rhino3d.Util.url = "http://localhost:6500/" # RhinoComputeのURL
compute_rhino3d.Util.apiKey = "" # 必要であればAPIキーを設定
def create_box():
# コーナー座標を定義 (リスト形式)
corner1 = [0, 0, 0] # Point3dの代わりにリスト形式
corner2 = [10, 10, 10]
# RhinoCompute APIを使ってボックスを作成
box = {
"corner1": corner1,
"corner2": corner2
}
return box
def serialize_brep(box):
# BrepをJSON形式に変換(手動でデータをフォーマット)
brep_data = {
"type": "box",
"data": box
}
return brep_data
if __name__ == "__main__":
# ボックスを作成
box = create_box()
# JSON形式に変換
json_data = serialize_brep(box)
# 結果をファイルに保存
with open("box_data.json", "w") as f:
json.dump(json_data, f)
print("ボックスデータをJSON形式で出力しました: box_data.json")
スクリプトの実行
Pythonスクリプトを実行して、box_data.jsonを生成します。
python rhino-box.py

実行後、box_data.jsonに3DボックスデータがJSON形式で保存されます。
Flask APIの作成とテスト
Flaskを使ってデータを提供するAPIを作成します。
server.py
from flask import Flask, jsonify
import json
app = Flask(__name__)
@app.route('/box', methods=['GET'])
def get_box():
# JSONファイルからデータを読み込み
with open("box_data.json", "r") as f:
data = json.load(f)
return jsonify(data)
if __name__ == "__main__":
app.run(debug=True)
実行
python server.py
これで、http://localhost:5000/boxにアクセスすることで、JSON形式のボックスデータが取得可能になります。

このデータはボックスのコーナー座標情報が含まれており、Reactフロントエンドでこの情報を元に3Dボックスを表示することができます。
次に、Reactでこのデータを利用してボックスを表示するために、Three.jsとReactの連携を行うコードを作成します。
Reactフロントエンドの構築
Reactアプリケーションの作成
mkdir frontend
furontendディレクトリで React アプリケーションを作成します。
cd frontend
npx create-react-app .
これにより、React アプリケーションが frontend ディレクトリ内に作成されます。次に、package.json に以下の scripts を追加します。
"scripts": {
"winstart": "WATCHPACK_POLLING=true react-scripts start"
}
必要なモジュールを手動でインストールします。
npm install web-vitals
この設定により、ファイル変更のポーリングが有効になり、Windows 環境でのホットリロードが機能します。
以下のコマンドでReactを起動させ動作確認します。
npm start
Reactライブラリのインストール
reactとreact-domのダウングレード
reactとreact-domをバージョン18にダウングレードしてライブラリをインストールできるようにします。
npm install react@18 react-dom@18
依存関係の再インストール
既存の依存関係を削除します。(node_modulesとpackage-lock.jsonを削除します)
Remove-Item -Recurse -Force .\node_modules, .\package-lock.json
必要なパッケージをインストールします(--legacy-peer-depsを使用して依存関係の衝突を無視)。
npm install --legacy-peer-deps
再度Reactを起動します。
npm start

Three.jsを用いた3Dモデル描画
React コンポーネントの作成: App.jsを編集して、box_data.jsonからボックスデータを取得し、Three.jsで描画します。
frontendディレクトリで以下を実行します。
npm install three @react-three/fiber @react-three/drei
npm install three-mesh-bvh@0.8.0
App.js
import React, { useEffect, useState } from 'react';
import { Canvas } from '@react-three/fiber';
import { Box } from '@react-three/drei'; // DreiのBoxコンポーネントを使用
const App = () => {
const [boxData, setBoxData] = useState(null);
// Flask APIからボックスデータを取得
useEffect(() => {
fetch('http://localhost:5000/box')
.then((response) => response.json())
.then((data) => setBoxData(data));
}, []);
// ボックスデータがまだ取得できていない場合、ローディング中の表示
if (!boxData) {
return <div>Loading...</div>;
}
const { corner1, corner2 } = boxData.data;
// Three.jsのBoxのサイズをボックスデータから計算
const width = corner2[0] - corner1[0];
const height = corner2[1] - corner1[1];
const depth = corner2[2] - corner1[2];
return (
<div>
<Canvas>
{/* Three.jsで3Dボックスを描画 */}
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} />
<Box args={[width, height, depth]} position={[0, 0, 0]}>
<meshStandardMaterial color="orange" />
</Box>
</Canvas>
</div>
);
};
export default App;
以下のようなエラーが発生します。
Uncaught runtime errors:
×
ERROR
Failed to fetch
TypeError: Failed to fetch
at http://localhost:3001/main.267a7dbb853600ba0f9c.hot-update.js:34:5
at commitHookEffectListMount (http://localhost:3001/static/js/bundle.js:25978:30)
at commitPassiveMountOnFiber (http://localhost:3001/static/js/bundle.js:27471:17)
at commitPassiveMountEffects_complete (http://localhost:3001/static/js/bundle.js:27443:13)
at commitPassiveMountEffects_begin (http://localhost:3001/static/js/bundle.js:27433:11)
at commitPassiveMountEffects (http://localhost:3001/static/js/bundle.js:27423:7)
at flushPassiveEffectsImpl (http://localhost:3001/static/js/bundle.js:29306:7)
at flushPassiveEffects (http://localhost:3001/static/js/bundle.js:29259:18)
at commitRootImpl (http://localhost:3001/static/js/bundle.js:29218:9)
at commitRoot (http://localhost:3001/static/js/bundle.js:29001:9)
ERROR
Failed to fetch
TypeError: Failed to fetch
at http://localhost:3001/main.267a7dbb853600ba0f9c.hot-update.js:34:5
at commitHookEffectListMount (http://localhost:3001/static/js/bundle.js:25978:30)
at invokePassiveEffectMountInDEV (http://localhost:3001/static/js/bundle.js:27667:17)
at invokeEffectsInDev (http://localhost:3001/static/js/bundle.js:29564:15)
at commitDoubleInvokeEffectsInDEV (http://localhost:3001/static/js/bundle.js:29547:11)
at flushPassiveEffectsImpl (http://localhost:3001/static/js/bundle.js:29320:9)
at flushPassiveEffects (http://localhost:3001/static/js/bundle.js:29259:18)
at commitRootImpl (http://localhost:3001/static/js/bundle.js:29218:9)
at commitRoot (http://localhost:3001/static/js/bundle.js:29001:9)
at performSyncWorkOnRoot (http://localhost:3001/static/js/bundle.js:28510:7)
Flask APIとの通信機能実装
CORS(app)を追加することで、Flaskアプリケーションに対して、外部(異なるポートやドメイン)からのリクエストを許可しています。CORS(app)のデフォルト設定では、すべてのオリジン(ドメイン)からのリクエストを許可します。
- バックエンド(Flask)からデータを取得する処理:
fetchBoxData.jsでは、fetchを使ってhttp://localhost:5000/boxからデータを取得しています。これにより、Flask APIのエンドポイントと通信し、3Dボックスのデータ(JSON)を取得しています。
- データの表示:
App.jsで取得したデータ(boxData)を画面に表示するロジックが含まれています。データが正常に取得できれば、ユーザーにその内容が視覚的に提示されます。
この部分が フロントエンドとバックエンドの通信 を行う部分で、まさに Flask APIとの通信機能実装 となります。
CORSポリシーの設定を確認
React(ポート3001)からFlask(ポート5000)にリクエストを送る際、CORS (Cross-Origin Resource Sharing) の制限が問題になることがあります。
FlaskアプリにCORSを設定しているか確認してください。server.pyに以下を追加します。
from flask_cors import CORSCORS(app) # この行を追加
server.py
from flask import Flask, jsonify
from flask_cors import CORS # 追加: flask_corsモジュールのインポート
app = Flask(__name__)
# CORSを有効にする
CORS(app) # この行を追加すると、すべてのオリジンからのリクエストを許可する
@app.route('/box', methods=['GET'])
def get_box():
# ここで、3Dボックスのデータを返す処理を実装します
return jsonify({
"type": "box",
"data": {
"corner1": [0, 0, 0],
"corner2": [10, 10, 10]
}
})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
fetchBoxData.js を作成
src/utils/ フォルダに fetchBoxData.js を作成します。このファイルにはバックエンドの API (http://localhost:5000/box) からデータを取得する関数を記述します。
src/utils/fetchBoxData.js
// src/utils/fetchBoxData.js
export const fetchBoxData = async () => {
try {
const response = await fetch('http://localhost:5000/box');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching box data:', error);
return null;
}
};
App.js でデータを取得
次に、App.js でこの fetchBoxData 関数を使ってデータを取得し、画面に表示します。
src/App.js
import React, { useEffect, useState } from 'react';
import { fetchBoxData } from './utils/fetchBoxData'; // 作成したAPI呼び出し関数
function App() {
const [boxData, setBoxData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const getBoxData = async () => {
const data = await fetchBoxData();
if (data) {
setBoxData(data);
}
setLoading(false);
};
getBoxData();
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Box Data</h1>
<pre>{JSON.stringify(boxData, null, 2)}</pre>
</div>
);
}
export default App;
動作確認
frontendフォルダで以下のコマンドを実行して、Reactアプリを再起動します。
npm start
- ブラウザで
http://localhost:3000/にアクセスして、バックエンドから取得したデータが表示されるか確認します。
もしデータが表示されない場合、ブラウザの 開発者ツール を使って コンソール や ネットワーク タブでエラーやリクエストの詳細を確認し、問題を特定してください。
これで、フロントエンドからバックエンドのデータを取得して表示する準備が整いました。データが正常に取得できれば、ボックス情報が画面に表示されるはずです。

上記のようにブラウザに表示できれば、フロントエンドとバックエンド間の通信が正しく行われていることを意味します。
ただし、現時点では3DボックスのデータとしてJSONが表示されているだけで、実際の3D描画は行われていません。
データの流れを整理
データの概念図
各種リンクをリロードしたときにRhinoComputeを起動しているコマンドプロンプトで以下のように挙動すればOKです。
http://localhost:3000/ をリロードした時、
[12:39:41 INF] HTTP GET /box responded 200 in 1.1171 ms
[12:39:41 INF] HTTP GET /box responded 200 in 0.7969 ms
http://localhost:5000/box をリロードした時、
[12:40:15 INF] HTTP GET /box responded 200 in 1.3534 ms
http://localhost:6500/ をリロードした時、
[12:40:27 INF] HTTP GET /box responded 200 in 0.7817 ms
http://localhost:3000/ 以下が表示されます。

各種コード

rhino_box.py
import rhino3dm
import requests
import json
# RhinoCompute の URL を設定
compute_url = "http://localhost:6500/box" # RhinoCompute サーバーが動作しているURL
def create_box():
"""
RhinoCompute を使用してボックスを生成し、JSON データとして返します。
"""
try:
# RhinoComputeへの接続確認
response = requests.get(compute_url)
if response.status_code != 200:
raise Exception("Failed to connect to RhinoCompute")
# ボックスの角 (corner1, corner2) と高さ
corner1 = rhino3dm.Point3d(0, 0, 0) # 原点
corner2 = rhino3dm.Point3d(100, 100, 100) # サイズを指定
# ボックスのBoundingBoxを生成
bbox = rhino3dm.BoundingBox(corner1, corner2)
# ボックスを生成
box = rhino3dm.Box(bbox)
# BoxからBrepを作成
brep = rhino3dm.Brep.CreateFromBox(box)
# Brepの頂点座標をJSON形式で整形して返す
vertices = []
for vertex in brep.Vertices:
# Point3d を使って座標を取得
point = vertex.Location # vertexのLocation属性がPoint3d型
vertices.append({"x": point.X, "y": point.Y, "z": point.Z})
return {"vertices": vertices}
except Exception as e:
# エラー処理
print(f"エラーが発生しました: {e}")
return {"error": str(e)}
server.py
from flask import Flask, jsonify
import rhino_box # rhino_box.py をインポート
from flask_cors import CORS # CORSのインポート
app = Flask(__name__)
CORS(app) # CORSを有効にする
@app.route('/box', methods=['GET'])
def get_box():
try:
# rhino_box.py で定義した create_box() を呼び出してボックスを生成
box_data = rhino_box.create_box() # create_box() は rhino_box.py に定義
if "error" in box_data:
return jsonify(box_data), 500
return jsonify(box_data), 200
except Exception as e:
return jsonify({"error": f"Server error: {e}"}), 500
if __name__ == '__main__':
app.run(debug=True)
utils/fetchBoxData.js
export async function fetchBoxData() {
try {
console.log("Fetching box data...");
const response = await fetch("http://localhost:5000/box"); // FlaskのAPIエンドポイント
if (!response.ok) {
throw new Error("Failed to fetch box data");
}
const data = await response.json();
console.log("Fetched Box Data:", data); // データを表示
return data;
} catch (error) {
console.error("Error fetching box data:", error);
return null; // データ取得失敗
}
}
App.js
import React, { useEffect, useState } from 'react';
import Box from './components/Box'; // Boxコンポーネント
function App() {
const [boxData, setBoxData] = useState(null);
useEffect(() => {
fetch('http://localhost:5000/box')
.then(response => response.json())
.then(data => {
console.log("Fetched Data:", data); // 追加: データが正しく取得されているか確認
setBoxData(data); // データを保存
})
.catch(error => {
console.error('Error fetching box data:', error);
});
}, []);
if (!boxData) {
return <div>Loading Box Data...</div>; // データがロードされるまで
}
return (
<div>
<h1>Box Data</h1>
<pre>{JSON.stringify(boxData, null, 2)}</pre> {/* データ表示 */}
<Box data={boxData} /> {/* Boxコンポーネントにデータを渡す */}
</div>
);
}
export default App;
components/Box.js
import React from 'react';
import { Canvas } from '@react-three/fiber';
import { Box as ThreeBox } from '@react-three/drei';
function Box({ data }) {
const vertices = data?.vertices || [];
console.log("Vertices Data:", vertices); // 追加: 受け取ったデータが正しいか確認
return (
<Canvas>
{vertices.length > 0 && (
<ThreeBox args={[100, 100, 100]} position={[0, 0, 0]}>
<meshStandardMaterial color="orange" />
</ThreeBox>
)}
</Canvas>
);
}
export default Box;
3Dボックスを表示
Three.jsをインストール
Three.jsをインストールします。以下のコマンドでインストールできます
npm install three
Box.js
Reactコンポーネントを作成して、RhinoComputeから取得した頂点データをThree.jsを使って描画します。
import React, { useEffect, useRef } from 'react';
import * as THREE from 'three';
function Box({ data }) {
const mountRef = useRef(null);
useEffect(() => {
// データがない場合は描画しない
if (!data || !data.vertices) return;
// シーン、カメラ、レンダラーをセットアップ
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const mountNode = mountRef.current;
mountNode.appendChild(renderer.domElement);
// 頂点データからボックスの寸法を計算
const vertices = data.vertices;
const width = Math.abs(vertices[0].x - vertices[1].x);
const height = Math.abs(vertices[0].y - vertices[3].y);
const depth = Math.abs(vertices[0].z - vertices[4].z);
// ボックスのジオメトリを作成
const geometry = new THREE.BoxGeometry(width, height, depth);
// ボックスのマテリアルを作成(色は赤)
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: false });
// メッシュとしてボックスをシーンに追加
const box = new THREE.Mesh(geometry, material);
scene.add(box);
// カメラの初期位置を設定
camera.position.z = 500;
// アニメーションループ
const animate = function () {
requestAnimationFrame(animate);
box.rotation.x += 0.01; // 回転
box.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
// ウィンドウリサイズ時にカメラアスペクト比を更新
const handleResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', handleResize);
// クリーンアップ
return () => {
window.removeEventListener('resize', handleResize);
mountNode.removeChild(renderer.domElement);
};
}, [data]);
return <div ref={mountRef} style={{ width: '100%', height: '100vh' }} />;
}
export default Box;
App.js
App.jsで、取得したボックスデータをBoxコンポーネントに渡して、3Dボックスを表示します。
// src/App.js
import React, { useEffect, useState } from 'react';
import Box from './components/Box'; // Boxコンポーネントをインポート
function App() {
const [boxData, setBoxData] = useState(null);
useEffect(() => {
// Flask APIからボックスデータを取得
fetch('http://localhost:5000/box')
.then(response => response.json())
.then(data => {
setBoxData(data); // ボックスデータを状態に保存
})
.catch(error => console.error('Error fetching box data:', error));
}, []);
if (!boxData) {
return <div>Loading Box Data...</div>; // データが読み込まれるまで表示
}
return (
<div>
<h1>3D Box</h1>
<Box data={boxData} /> {/* Boxコンポーネントにボックスデータを渡す */}
</div>
);
}
export default App;
rhino_box.py
import rhino3dm
import requests
import json
# RhinoCompute の URL を設定
compute_url = "http://localhost:6500/box" # RhinoCompute サーバーが動作しているURL
def create_box():
"""
RhinoCompute を使用してボックスを生成し、JSON データとして返します。
"""
try:
# RhinoComputeへの接続確認
response = requests.get(compute_url)
if response.status_code != 200:
raise Exception("Failed to connect to RhinoCompute")
# ボックスの角 (corner1, corner2) と高さ
corner1 = rhino3dm.Point3d(0, 0, 0) # 原点
corner2 = rhino3dm.Point3d(100, 100, 100) # サイズを指定
# ボックスのBoundingBoxを生成
bbox = rhino3dm.BoundingBox(corner1, corner2)
# ボックスを生成
box = rhino3dm.Box(bbox)
# BoxからBrepを作成
brep = rhino3dm.Brep.CreateFromBox(box)
# Brepの頂点座標をJSON形式で整形して返す
vertices = []
for vertex in brep.Vertices:
# Point3d を使って座標を取得
point = vertex.Location # vertexのLocation属性がPoint3d型
vertices.append({"x": point.X, "y": point.Y, "z": point.Z})
return {"vertices": vertices}
except Exception as e:
# エラー処理
print(f"エラーが発生しました: {e}")
return {"error": str(e)}
