Apps WebApp

004-ComputeStudySite|RhinoComputeとReactを用いてプログラミング言語の学習メモサイトを構築する

要件定義

RhinoComputeを用いたPython、JavaScript、React、Three.jsの学習環境の開発に向けて、具体的な要件を整理します。

これにより、開発の全体像を把握し、必要な作業を明確にします。

プロジェクトの目的

RhinoComputeとReactを用いてPython、JavaScript、React、Three.jsの学習環境を構築する

システム概要

Python

RhinoCommonを使用して3Dジオメトリを生成。

Flaskを用いて3DジオメトリデータをJSON形式で提供するAPIを構築。

RhinoCompute

ローカル環境でRhinoのコア機能を外部プログラムから呼び出すAPIサーバー。

PythonスクリプトからRhinoComputeを利用して3Dジオメトリを作成。

React

Three.jsを活用し、Webアプリケーション上で3Dジオメトリを表示。

Flask APIから取得した3Dデータを動的にレンダリング。

機能要件

技術要件

(1) 使用ツール・フレームワーク

  • RhinoCompute(ローカルでのAPIサーバー)
  • Python 3.11.9
  • Flask(軽量Webフレームワーク)
  • React(フロントエンドフレームワーク)
  • Three.js(3Dレンダリングライブラリ)

(2) 環境要件

  • OS: Windows 11 以上(Rhino8 対応)
  • ブラウザ: 最新のGoogle Chrome
  • Node.js: v18.20.5
  • Rhino 8: インストール済み

(3) 必要なライブラリ

  • Python:
    • compute-rhino3d
    • rhino3dm
    • flask-cors
  • React:
    • react 18.3.1
    • react-dom 18.3.1
    • web-vitals 4.2.4
    • three
    • @react-three/fiber
    • @react-three/drei
    • three-mesh-bvh@0.8.0

開発工程

環境構築(1日)

  • RhinoComputeのセットアップ
  • 必要なライブラリのインストール

Pythonスクリプトの作成(2日)

  • RhinoCommonでボックス生成
  • Flask APIの作成とテスト

Reactフロントエンドの構築(3日)

  • Three.jsを用いた3Dモデル描画
  • Flask APIとの通信機能実装

成果物の仕様

Step1:3Dボックス生成・表示

機能

  • 3Dモデル表示機能:Pythonスクリプトで生成されたボックスをReactアプリ上に描画。

基本操作

  • ユーザーがマウスでモデルを回転、拡大縮小できる。

拡張性

  • ボックス以外の形状にも簡単に対応可能。

Step2:3Dボール生成・表示・画面遷移

機能

  • 3Dモデル表示機能:Pythonスクリプトで生成されたボックスをReactアプリ上に描画。
  • 画面遷移機能:リンクごとに画面遷移し閲覧可能

基本操作

  • ユーザーがマウスでモデルを回転、拡大縮小できる。

拡張性

  • ボックス以外の形状にも簡単に対応可能。

環境構築

ルートディレクトリをプロジェクト名として作成します。

mkdir 004-ComputeStudySite
cd 004-ComputeStudySite

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 コンソール アプリケーションを実行している状態で、これらのエンドポイントのいずれかを参照して、すべてが動作していることを確認します。

Pythonライブラリのインストール

必要なPythonライブラリをインストールします。以下をbackendディレクトリで実行します。

pip install compute-rhino3d
pip install rhino3dm
pip install flask-cors

Pythonスクリプトの作成

RhinoComputeを利用して3Dボックスを生成するPythonスクリプトを作成します。

type nul > rhino_box.py

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")

Flask APIの作成とテスト

Flaskを使ってデータを提供するAPIを作成します。

type null > server.py

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 ディレクトリ内に作成されます。

必要なモジュールを手動でインストールします。

npm install web-vitals

この設定により、ファイル変更のポーリングが有効になり、Windows 環境でのホットリロードが機能します。

reactとreact-domのダウングレード

reactとreact-domをバージョン18にダウングレードしてライブラリをインストールできるようにします。

npm install react@18 react-dom@18

再度Reactを起動します。

npm start

Flask APIとの通信機能実装

  • CORS(app)を追加することで、Flaskアプリケーションに対して、外部(異なるポートやドメイン)からのリクエストを許可しています。
  • CORS(app)のデフォルト設定では、すべてのオリジン(ドメイン)からのリクエストを許可します。
  1. バックエンド(Flask)からデータを取得する処理
    • fetchBoxData.js では、fetch を使って http://localhost:5000/box からデータを取得しています。これにより、Flask APIのエンドポイントと通信し、3Dボックスのデータ(JSON)を取得しています。
  2. データの表示
    • App.js で取得したデータ(boxData)を画面に表示するロジックが含まれています。データが正常に取得できれば、ユーザーにその内容が視覚的に提示されます。

この部分が フロントエンドとバックエンドの通信 を行う部分で、まさに Flask APIとの通信機能実装 となります。

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)

src/utils/ フォルダに fetchBoxData.js を作成します。

cd src
mkdir utils
cd utils
type null > fetchBoxData.js

src/utils/fetchBoxData.js

このファイルにはバックエンドの API (http://localhost:5000/box) からデータを取得する関数を記述します。

// 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;

動作確認

  1. frontend フォルダで以下のコマンドを実行して、Reactアプリを再起動します。
npm start
  1. ブラウザで http://localhost:3000/ にアクセスして、バックエンドから取得したデータが表示されるか確認します。

しかしこの段階では、http://localhost:3000/をリロードしてもRhinoCompute側では処理が発生していません。また、http://localhost:5000/boxをリロードしたときもRhinoCompute側では処理が発生していません。

現状は以下のようになっています。

http://localhost:3000/をリロードしたとき、http://localhost:5000/box側は処理が発生している

http://localhost:6500/boxをリロードしたとき、RhinoComputeは処理が発生している。

つまり、ReactとFlask間は連携できているが、FlaskとRhinoCompute間は連携できていないということです。

データの流れを整理

データの概念図

以下でデータの連携の大まかな流れを確認します。

各種リンクをリロードしたときに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とserver.pyの修正

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)

以下のように、http://localhost:3000/をリロードすると、RhinoComputeとFlaskサーバーがGETしていることがわかります。

これで、FlaskとRhinoComputeを連携でき、それにより、React、Flask、RhinoComputeが連携できるようになりました。

3Dボックスを表示

RhinoComputeで生成した3Dジオメトリデータを元にThree.jsでボックスを作成しWeb上に表示します。

ライブラリをインストール

frontendディレクトリに移動して、Three.jsをインストールします。以下のコマンドでインストールできます。

npm install three
npm install @react-three/fiber
npm install @react-three/drei
npm install three-mesh-bvh@0.8.0
npm install @mediapipe/tasks-vision@latest

まとめてインストールしたい場合は以下のコマンドでインストール

npm install three @react-three/fiber @react-three/drei three-mesh-bvh@0.8.0 @mediapipe/tasks-vision@latest

App.js

// 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;

components/Box.js

cd frontend/src
mkdir components
type null > 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;

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)}

動作確認

サイドバーとページ遷移の実装

サイドバーを追加して、ページ遷移の機能を実装するには以下のステップを行います。

React Router のインストール

ページ遷移には react-router-dom を利用します。以下のコマンドでインストールします:

npm install react-router-dom

サイドバーコンポーネントの作成

以下のようなサイドバーを作成します。サイドバーにリンクを配置して、クリックした際に異なるページに遷移できるようにします。

cd src/components
type null > Sidebar.js
// src/components/Sidebar.js
import React from "react";
import { Link } from "react-router-dom";

const Sidebar = () => {
  return (
    <div className="sidebar" style={{ width: "200px", background: "#f0f0f0", padding: "10px" }}>
      <h2>Menu</h2>
      <ul style={{ listStyleType: "none", padding: 0 }}>
        <li>
          <Link to="/">main</Link>
        </li>
        <li>
          <Link to="/python">Python</Link>
        </li>
        <li>
            <Link to="/python/box">Python Box</Link>
        </li>
        <li>
          <Link to="/javascript">JavaScript</Link>
        </li>
        <li>
          <Link to="/react">React</Link>
        </li>
        <li>
          <Link to="/threejs">Three.js</Link>
        </li>
      </ul>
    </div>
  );
};

export default Sidebar;

ページ構成とルート設定

App.js でルーティングを設定します。

// src/App.js
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Sidebar from "./components/Sidebar";
import MainPage from "./pages/MainPage";
import PythonPage from "./pages/PythonPage";
import PythonBoxPage from './pages/PythonBoxPage';
import JavaScriptPage from "./pages/JavaScriptPage";
import ReactPage from "./pages/ReactPage";
import ThreeJsPage from "./pages/ThreeJsPage";

const App = () => {
  return (
    <Router>
      <div style={{ display: "flex" }}>
        <Sidebar />
        <div style={{ marginLeft: "200px", padding: "20px", width: "100%" }}>
          <Routes>
            <Route path="/" element={<MainPage />} />
            <Route path="/python" element={<PythonPage />} />
            <Route path="/python/box" element={<PythonBoxPage />} />
            <Route path="/javascript" element={<JavaScriptPage />} />
            <Route path="/react" element={<ReactPage />} />
            <Route path="/threejs" element={<ThreeJsPage />} />
          </Routes>
        </div>
      </div>
    </Router>
  );
};

export default App;

各ページコンポーネントの作成

例えば、BoxPage を以下のように作成します:

cd src
mkdir pages
type null > Main.js
type null > PythonPage.js
type null > PythonBoxPage.js
type null > JavaScriptPage.js
type null > ReactPage.js
type null > ThreeJsPage.js

PythonBoxPage.js

先ほどまでApp.jsでボックスを表示させていましたが、PythonBoxPage.jsにその役割を移行するイメージです。

代わりにApp.jsでは各ページへの遷移をする役割を持ちます。

import React, { useEffect, useState } from 'react';
import Box from '../components/Box';

const PythonBoxPage = () => {
  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>Python Box</h1>
      <Box data={boxData} />
    </div>
  );
};

export default PythonBoxPage;

他ページ

他のページ (PythonPage.js, JavaScriptPage.js など) も同様に作成しますが、シンプルなテキストだけを表示する形で十分です。

// src/pages/PythonPage.js
import React from "react";

const PythonPage = () => {
  return <h1>Python Page</h1>;
};

export default PythonPage;

スタイル調整 (オプション)

サイドバーやページ全体のデザインをより良くするために CSS を適用します。

cd src
mkdir styles
type null > App.css

App.css

/* src/styles/App.css */
.sidebar {
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  background-color: #282c34;
  color: white;
  padding: 1rem;
}

.sidebar a {
  color: white;
  text-decoration: none;
}

.sidebar a:hover {
  text-decoration: underline;
}

これで、サイドバーからリンクをクリックして各ページに遷移し、ボックスを表示するページや他のページが閲覧できるようになります!

動作確認

-Apps, WebApp
-