[Astro] #148 インボリュート歯車ジェネレーター — 歯形生成・噛み合わせ・ヘリカル化・DXF出力の実装記録

[Astro] #148 インボリュート歯車ジェネレーター — 歯形生成・噛み合わせ・ヘリカル化・DXF出力の実装記録

はじめに

Vectorで見かけたスミスチャート解析ソフトの古い作りをきっかけに歯車ジェネレーターを作り始め、最終的に「インボリュート歯形生成 → 噛み合わせ → 歯元フィレット → DXFエクスポート → 歯車列 → はすば歯車」まで一通り実装しました。

スミスチャートのprototype

歯車は数式さえ合っていれば「噛み合った時に干渉せず綺麗に回るか」を目視で検証できるという、RFエンジニアリング寄りのツール(スミスチャート等)とは違った実装のしやすさがありました。

モジュール内容
PROFILE ENGINEインボリュート歯形生成。基礎円・歯先円・歯元円、圧力角対応
FILLET2次ベジェ曲線による歯元フィレットの近似丸め
GEAR TRAIN2〜5段の歯車列。軸間距離・角速度比・回転方向を自動計算
HELICALZ方向にひねりを加えたはすば歯車。自前ロフト生成
DXF EXPORTERLWPOLYLINE + CIRCLE、mm単位、レーザーカット向け平置きレイアウト
ASTRO UIvrm-to-mmd.astro の3パネルレイアウトを踏襲したUI移植

スクリーンショット

歯車列(3段)

インボリュート歯車ジェネレーター 歯車列

はすば歯車(ヘリカル化)

はすば歯車 ヘリカルギア

DXFエクスポート(DXF Viewerで確認)

歯車DXFエクスポート確認

動画(GIF)

インボリュート歯車ジェネレーター

1. 技術スタック

ライブラリバージョン役割
three0.160.0 (ESM)3Dレンダリング、ジオメトリ生成
three/addons/controls/OrbitControls-カメラ操作(ドラッグ回転・ホイールズーム)
Astro5ページフレームワーク

出力形式: .dxf(AC1015 / LWPOLYLINE + CIRCLE、mm単位)

プロトタイピング段階では単体HTML(three.min.js r128 UMD版をCDN読み込み)で進め、機能が固まってからAstro + ESモジュール構成に移植する流れを取りました。

2. インボリュート歯形生成

歯形は標準インボリュート曲線の式から生成します。モジュール m・歯数 N・圧力角 α から、ピッチ半径・基礎円半径・歯先円半径・歯元円半径を求めます。

const r = m * N / 2;               // ピッチ半径
const rb = r * Math.cos(alpha);    // 基礎円半径
const ra = r + m;                  // 歯先円半径(addendum = 1モジュール)
const rd = r - 1.25 * m;           // 歯元円半径(dedendum = 1.25モジュール)

歯厚の角度は、ピッチ円上のインボリュート関数 inv(α) = tan(α) - α を使い、任意半径での歯厚半角を次の式で求めます。

function halfTooth(radius) {
  const a = Math.acos(Math.min(1, rb / radius));
  const invA = Math.tan(a) - a;
  return Math.PI / (2 * N) + invAlphaPitch - invA;
}

この半角を使って歯の右フランク・歯先円弧・左フランクを極座標で生成し、歯数分だけ回転コピーして1枚の歯車輪郭にします。

3. 歯元フィレット(2次ベジェによる近似)

実際のホブ加工で生じるトロコイド曲線を厳密に再現するのは煩雑なため、フランクの立ち上がり点と歯底円上の点を、鋭角コーナーを制御点にした2次ベジェで繋ぐ近似フィレットを採用しました。

function bezier(A, C, B, steps) {
  const out = [];
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const x = (1 - t) ** 2 * A.x + 2 * (1 - t) * t * C.x + t ** 2 * B.x;
    const y = (1 - t) ** 2 * A.y + 2 * (1 - t) * t * C.y + t ** 2 * B.y;
    out.push({ x, y });
  }
  return out;
}

A は歯底円上の点、C は鋭角コーナー(制御点)、B はフランク側の点です。歯数が少ない場合、歯元同士が干渉して見えることがありますが、これは実装の不具合ではなく実際の低歯数インボリュートギアでも起こる「アンダーカット」現象と一致します。

4. 歯車列の噛み合わせ

2枚の歯車を噛み合わせる際、軸間距離はピッチ半径の和になります。

const centerDist = gearA.r + gearB.r;
pivotA.position.set(-gearA.r, 0, 0);
pivotB.position.set(gearB.r, 0, 0);

角速度は歯数比の逆数、回転方向は逆にします。初期位相は「片方は歯の中心を接触点側に、もう片方は谷の中心を接触点側に向ける」ことで噛み合わせます。

const theta2 = -theta1 * (N1 / N2) + (Math.PI + Math.PI / N2);

3段以上に拡張する場合、この位相合わせの式を隣接ペアに再帰的に適用しますが、3段目以降は解析的に厳密な位相を出すのが煩雑なため、各段に Phase Trim(±30°)を用意し、初期フレームでの見た目のズレを手動で微調整できるようにしました。角速度比自体は歯数比で厳密に計算しているため、噛み合わせの回転そのものは正しく、位相トリムはあくまで見た目の初期整合用です。

const thetas = [theta0];
for (let i = 1; i < gearCount; i++) {
  const offset = Math.PI + Math.PI / teeth[i] + trims[i];
  thetas.push(-thetas[i - 1] * (teeth[i - 1] / teeth[i]) + offset);
}

5. はすば歯車(ヘリカルギア)化

Three.jsの ExtrudeGeometry にはZ方向にひねりながら押し出す機能がないため、歯形を輪切りにして少しずつ回転させたスライスを積み、側面を三角形で繋ぐ自前のロフト生成を実装しました。

ひねり量は実際の設計式(ひねり角βとピッチ半径から求まる)をそのまま使います。

const twistTotal = twistSign * depth * Math.tan(helixRad) / pitchRadius;

function buildWallStrip(contourPts, depth, twistTotal, slices) {
  const positions = [];
  for (let s = 0; s <= slices; s++) {
    const t = s / slices;
    const ang = twistTotal * t;
    const cosA = Math.cos(ang), sinA = Math.sin(ang);
    for (const p of contourPts) {
      positions.push(p.x * cosA - p.y * sinA, p.x * sinA + p.y * cosA, depth * t);
    }
  }
  // ... スライス間を三角形2枚(クアッド)で接続
}

平行軸で噛み合うはすば歯車は、隣り合う2枚が互いに逆ねじれ(右ねじれ/左ねじれ)でないと噛み合わないため、歯車列内で i % 2 === 0 ? 1 : -1 によりねじれ方向を交互に切り替えています。上下のキャップ(断面)は同じ2Dシェイプを、片方だけひねり角分回転させて配置し、底面は法線を反転させるため三角形の頂点順序を入れ替えています。

ヘリカル角βを0°にすると twistTotal = 0 に退化し、これまでの平歯車と完全に一致する形状になることも確認済みです。

6. DXFエクスポート

歯形生成時の点群配列はすでにmm単位の実座標なので、DXFのLWPOLYLINEフォーマットにそのまま変換できます。

function pointsToLWPolylineEntity(pts, offsetX, offsetY, layer) {
  let s = `0\nLWPOLYLINE\n8\n${layer}\n90\n${pts.length}\n70\n1\n`;
  for (const p of pts) {
    s += `10\n${(p.x + offsetX).toFixed(4)}\n20\n${(p.y + offsetY).toFixed(4)}\n`;
  }
  return s;
}

レーザーカット用に、噛み合わせ位置ではなく各歯車を横並びに配置した平置きレイアウトで出力します。

let offsetX = 0;
gears.forEach((g, i) => {
  if (i > 0) offsetX += gears[i - 1].ra + margin + g.ra;
  entities += pointsToLWPolylineEntity(g.pts, offsetX, 0, `GEAR${i + 1}`);
});

ヘッダーで $INSUNITS にミリメートルを指定し、[[DXF Viewer]]で読み込むとLWPOLYLINE・CIRCLEがレイヤー分けされた状態で正しく表示されることを確認しました。DXFはあくまでZ=0の正面断面(ねじれ前の輪郭)であり、はすば歯車のねじれ角は製造時に別途指定するパラメータとして扱う設計です。

7. Astro移植とハマったポイント

UIはvrm-to-mmd.astroの「左パネル(操作)・中央ビューポート・右パネル(情報)」という3パネル構成と #00ff88 基調のダークテーマをそのまま踏襲しました。Three.jsも単体HTMLで使っていたr128 UMD版から、[email protected] のESモジュール + OrbitControls に置き換えています。

<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/[email protected]/build/three.module.js",
    "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
  }
}
</script>

移植時に一つハマったのが、Astroのスコープ付きCSSの挙動です。歯車列の段数を増減させると、各歯車の設定カードをJS側で innerHTML により動的生成しますが、そのカードのレイアウトだけ崩れる問題が発生しました。

原因は、.astro<style> ブロックはビルド時に「テンプレートに直接書かれた要素」にしかスコープ属性が付与されず、クライアント側JSが後から挿入したDOMはそのスコープ判定の対象外になるためでした。静的に書いたSHARED PARAMETERS側のスライダーは正常に見えるのに、動的生成のGEAR TRAINカードだけラベルと値がくっついて見える、という形で症状が出ました。

対処は該当セレクタを :global() で囲むだけです。

:global(.slider-row) { margin-bottom: 10px; }
:global(.slider-row label) { display: flex; justify-content: space-between; }
:global(.gear-card) { border: 1px solid #1a1a1a; border-radius: 4px; padding: 8px; }

JSから動的にDOMを挿入するAstroページでは、その対象クラスは最初から :global() を前提に設計しておいた方が安全そうです。

8. まとめ

インボリュート歯車ジェネレーターは、「生成 → 噛み合わせ → フィレット → DXF出力 → 歯車列 → はすば歯車」という順番で機能を積み上げ、最終的にAstro + Three.js ESモジュール構成へ移植しました。

  • インボリュート曲線の数式だけで歯形が完全に決まる — 検証は「干渉せず噛み合うか」を目視で確認できる
  • 歯車列は原理上何段でも連結可能 — 軸間距離・角速度比は隣接ペアの計算を再帰的に適用するだけ
  • ExtrudeGeometryにない「ひねり押し出し」は自前のロフト生成で対応 — スライスを積んで側面を三角形で繋ぐだけの比較的単純な処理
  • DXF出力は歯形の点群配列をそのままLWPOLYLINEに変換 — レーザーカット・3Dプリント向けの実用的な展開図になる
  • Astroの動的DOM生成には:global()が必須 — スコープ付きCSSの対象外になる罠に注意

今後の拡張として、内歯車・遊星歯車機構、正確なnormal/transverseモジュール変換によるはすば歯車の精度向上、Three.jsメッシュからのSTLエクスポート(3Dプリント向けソリッド出力)などを検討しています。