歯車について勉強する7 の変更点

更新


#author("2025-07-03T09:17:49+00:00","default:administrator","administrator")
#author("2025-07-03T09:24:21+00:00","default:administrator","administrator")
[[工作/歯車について勉強する]]

* クラウンギアの歯形について考える [#b86716b8]

フリーソフトとして公開している自作の [[工作/Fusion360歯車スクリプト]] 内でクラウンギアの形状を求めるために考察した内容です。

クラウンギアはフェースギアとも呼ばれ、2つの軸が直角に交わる配置で噛み合うギアです。 相手のピニオン歯車には通常のインボリュート歯車が使えます。

&ref(工作/Fusion360歯車スクリプト/crown.gif,,50%);

目次:

#contents

* クラウンギアの円筒面上で歯形を計算すると良い [#v83ae40c]
&katex();
クラウンギアの基準円半径を $r_p$ とする。

中心軸から $r_p+\Delta r$ の距離にある円筒面によるピニオンギア形状の断面を考える。

円筒面上の座標は回転角 $\theta$ と高さ方向の変位 $h$ で指定可能である。

高さ $h$ をクラウンギアの基準面から測ることにすると、
ピニオンギアの中心軸は $h=mz_p/2+mx$ の高さに来る。

ここでモジュール $m$ とピニオンギア歯数 $z_p$ の積である $mz_p$ はピニオンギアの基準円直径、
その半分 $mz_p/2$ はピニオンギアの基準円半径である。

$x$ はモジュール単位で測った転位量であり、$mx$ はそれを距離に直した値である。

つまり、転位ゼロの時ピニオンギアの基準円とクラウンギアの基準面とが接する。
転位がゼロでなければその分だけピニオンギアの高さが変化する。

円筒面を切り開いて平面状にしたと考えると、横軸に $(r_p+\Delta r)\theta$ を縦軸に $h$ を取ればよいのだが、実際のスクリプト内では横軸は係数 $(r_p+\Delta r)$ を除いて $\theta$ のみで表すという変則的な取り方をしている。

&ref(bevel24.jpg);

&ref(bevel25.jpg);

ピニオンの軸に垂直な面上で計算されたピニオン歯車形状をこの円筒面上に持って来るには、
高さ方向はそのままで良いとして、横方向は 
$$
x=(r_p+\Delta r)\sin\theta
$$
の関係があるから、これを逆に解けば
$$
\theta=\sin^{-1}\frac{x}{r_p+\Delta r}
$$
を満たす点へ移されることになる。

* 切削用に歯先を延長したピニオンの形状 [#p9f364a5]

クラウンギアと組み合わされるピニオン歯車の形状は通常のインボリュート歯車で良いのであるけれど、
これをそのまま切削具として使ってしまうとピニオン歯車の歯先と切削後のクラウンギアの歯底とが接してしまう。

ピニオン歯車の歯先とクラウンギア歯底との間に適切な頂隙を確保するためには、
切削用のピニオン形状はピニオンギアの歯先を頂隙分だけ延長したものでなければならない。

また、この延長部分はインボリュート歯形である必要がないため、本来の歯先の角からインボリュート面に滑らかに接続するようフィレットを付けた形状とするのが良い。

&ref(bevel26.jpg,,50%);
&ref(bevel27.jpg,,50%);

頂隙は通常モジュールの 0.25 倍(歯元の丈から歯末の丈を引いた値)であるから、
延長された歯先は $h_a$ を歯末の丈として半径 $mz_p+mx+h_a+0.25m$ の円と接することになる。

歯の中心線上に中心を持つ円がこの延長歯先円と歯形の両方に接するなら、
この円弧がフィレット付き歯先延長形状の候補となる。

なぜ候補なのかというと、この円と歯形とが接する点が延長前の本来の歯先円よりも
内側に入ってしまう場合には、この円よりも小さな円を描く必要があるためだ。

この場合には、本来の歯先の角から歯形に垂線を立て、
この線上に中心を持つ円が延長歯先円と接する点を見つけてフィレットの中心とする。

計算でフィレットの中心と半径を求めるには、

+ まず延長前の本来の歯先の角から歯形に対する垂線方向を求める
+ この垂線と歯の中心線とが交わる点 $P_1$ を求める
+ この点を中心に歯先の角で歯形に接する円を描いた際に延長歯先円と交わらなければ前者のケースが有効~
つまり歯の中心線上の $P_1$ より外側で延長歯先円と歯形との両方に接する円を描いて延長歯形が求まる
++ $P_1$ と延長歯先半径との間で半径 $r$ を変化させ、
++ 半径 $r$ の円と中心線が交わる点から歯先円に触れる円を描く
++ 歯形までの距離が $r$ と等しくなる点が求めたい点
++ 「歯形までの距離」は本来の歯先から延長歯先円まで延長したインボリュート曲線上で最も近い点を探せば求まる
+ この点を中心に歯先の角で歯形に接する円を描いた際に延長歯先円と交わるなら後者のケースが有効~
つまり本来の歯先の角から引いた推薦上で歯形と延長歯先円に接する円弧を描いて延長歯形が求まる
++ 歯先の角と中心線との間の垂線上で歯先の角までの距離と延長歯先円までの距離とが等しくなる点を求めればよい

数値計算による関数の最小化や求根を含む複雑な計算を行うことになるのだけれど、
何とか正しい形状を求められるようになった。

* クラウンギアとピニオンギアの回転に伴うピニオン形状の移動 [#v9d598c2]

ピニオンギアの回転は単純に中心点に対する回転であるけれど、
回転後の座標値に対して
$$
\theta=\sin^{-1}\frac{x}{r_p+\Delta r}
$$
の計算により $\theta$ を求めることになる。

ピニオンの回転に伴い、クラウンギアの方も回転する。
こちらは単に $\theta$ に回転量を加えればよい。

この計算で円筒面上でのピニオンギアの軌跡を求め、その包絡線を歯溝形状とすることになる。

* はすばピニオンへの対応 [#pce02314]

ピニオン形状を円筒面へ移す際に、ピニオンの軸方向の位置により回転角が変化することになる。

「ピニオンの軸方向の位置」はピニオン軸上では $r_p+\Delta r$ に等しいが、$x\ne 0$ では
$$
(r_p+\Delta r)\cos\theta=(r_p+\Delta r)\sqrt{1-\Big(\frac{x}{r_p+\Delta r}\Big)^2}
$$
である。

はすば角度が $\beta$ であれば高さ変位 $\Delta z$ に対して回転角 $\Delta\phi$ は
$$
(mz_p/2)\Delta\phi=\Delta z\tan\beta
$$
を満たすはず。

つまり、
$$
\Delta\phi=\frac{\tan\beta}{mz_p/2}(r_p+\Delta r)\sqrt{1-\Big(\frac{x}{r_p+\Delta r}\Big)^2}
$$

・・・ん?

そもそも $\phi$ が求まらないと $x$ が求まらないにも関わらず、ここで得られたのは $x$ が求まらないと $\phi$ が求まらないという式になっている。

この式を使う限り、卵と鶏とを同時に手に入れる必要が出てくるわけか。

うーん、こういう単純な式では求めることはできなさそう。

たぶん、歯形断面上のすべての点について、元の点から延びるはすばピニオンの歯筋に沿って探索し、対応する円筒面との交点を求めてやる必要がある。

結構な計算量になるけれど、まあできなくはないか。

そして、この場合には歯溝形状は中心線に対して対称にはならないので、
まじめに両側を計算しなければならない。

* ピニオンの歯元のフィレット部分との接触 [#h412a776]

クラウンギアの基準線より内側ではクラウンギアの歯先がピニオンの歯元のフィレット部分と触れてしまう。

これを避けるには、切削用クラウンギアをフィレット値最大で作ってクラウンギアを製作し、
実際にはフィレット値を小さくしたクラウンギアと組み合わせて使うことになる。

インボリュート領域以外で歯車が接触するのは気持ちが悪いので避けた方が良い気がするのだけれど・・・
どうしても避ける必要があるのか、気にしなくていいのか、よく分かっていない。

* インボリュート領域と切り下げ領域との境目 [#s49a7625]

特にはすばのピニオンを使う場合、インボリュート領域と切り下げ領域とが比較的大きな角度で交わり、
明確なエッジが形成されるみたいだ。

&ref(工作/Fusion360歯車スクリプト/クラウンギア/crown13.jpg);

こういうエッジを含む形状をロフトで生成しようとすると断面の構成点数を非常に大きくしなければならなくて
計算量が増えるし、いくら増やしたところであまりきれいな結果が得られない。

そこで、インボリュート領域と切り下げ領域とをそれぞれ別々に計算して、
2回に分けて切り取りを行うことにした。

具体的には、
- 歯が最も深く入る角度から歯車を右に回転させて得られる切り取り領域と
- 歯が最も深く入る角度から歯車を左に回転させて得られる切り取り領域と

を別々に計算して、それぞれに対して切り取りを行う。

この方法により切り取り領域の生成は比較的滑らかな曲線のみで行いつつ、
切り取り後の歯面には急峻なエッジが正確に形成されるようになる。

* 歯形を生成できるようになりました [#a7f8130d]

ここでは特定の半径を持つ円筒面上におけるクラウンギア歯形の断面を、ピニオンギアの歯形を掃引することで求めています。

 LANG: p5js_live
  // src/css-text.ts
  function generateRandomId(length) {
    let result = "";
    const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    const charactersLength = characters.length;
    let counter = 0;
    while (counter < length) {
      result += characters.charAt(Math.floor(Math.random() * charactersLength));
      counter += 1;
    }
    return result;
  }
  var cssText = (id) => `
  @media screen and (min-width: 640px) {
    #${id} {
      position: absolute;
      left: 0;
      top: 0;
      opacity: 0.2;
    }
    #${id}:hover {
      opacity: 0.7;
    }
    #${id} .control-slider {
      padding-left: 10px;
    }
  }
  @media screen and (max-width: 639px) {
    #${id} .check-hide-tools {
      display: none !important;
    }
    #${id} {
      height: 30vh;
      overflow-y: scroll;
      line-height: 2;
    }
    #${id} > :last-child {
      padding-bottom: 10vh;
    }
    #${id} .control-checkbox {
      width:85px !important;
    }
    #${id} .control-slider input {
      width: 200px !important;
    }
  }
  #${id} {
    display: block;
    max-width: 500px
  }
  #${id} .control-slider {
    display: block
  }
  #${id} .control-slider :first-child {
    display: inline-block;
    width: 80px
  }
  #${id} .control-slider :nth-child(2) {
    display: inline-block;
    width: 50px
  }
  #${id} .control-slider input {
    display: inline-block;
    width: 350px;
  }
  #${id} .control-checkbox {
    display:inline-block;
    width:110px;
    overflow:hidden
  }
 `;
 
  // src/initializeControls.ts
  function initializeControls(parent, inputChanged = () => {
  }) {
    const id = generateRandomId(8);
    const wrapper = elem("div", "", [elem("style", cssText(id))], { id });
    parent.appendChild(wrapper);
    const wrapControls = elem("div", "", [], {});
    const checkHide = createCheckbox(wrapper, "ツールを非表示", () => {
      wrapControls.style.display = checkHide.checked ? "none" : "block";
    });
    checkHide.parentElement.classList.add("check-hide-tools");
    wrapper.appendChild(wrapControls);
    const controls = {
      m: createSlider(
        wrapControls,
        "モジュール",
        { min: 10, max: 400, value: 200, step: 1 },
        inputChanged
      ),
      alpha: createSlider(
        wrapControls,
        "圧力角",
        { min: 10, max: 32, value: 20, step: 1 },
        inputChanged
      ),
      z: createSlider(wrapControls, "ピニオン歯数", { min: 4, max: 60, value: 12, step: 1 }, inputChanged),
      shift: createSlider(
        wrapControls,
        "ピニオン転位",
        { min: -0.5, max: 2, value: 0, step: 0.05 },
        inputChanged
      ),
      fillet: createSlider(
        wrapControls,
        "フィレット",
        { min: 0, max: 0.4, value: 0.4, step: 0.01 },
        inputChanged
      ),
      backlash: createSlider(
        wrapControls,
        "バックラッシュ",
        { min: -5, max: 5, value: 0, step: 0.02 },
        inputChanged
      ),
      z2: createSlider(wrapControls, "クラウン歯数", { min: 20, max: 100, value: 60, step: 1 }, inputChanged),
      t: createSlider(
        wrapControls,
        "半径変位",
        { min: -5, max: 5, value: 0, step: 0.1 },
        inputChanged
      ),
      beta: createSlider(
        wrapControls,
        "はすば角",
        { min: -60, max: 60, value: 30, step: 1 },
        inputChanged
      )
    };
    return {
      get m() {
        return Number(controls.m.value);
      },
      get alpha() {
        return Number(controls.alpha.value) / 180 * Math.PI;
      },
      get z() {
        return Number(controls.z.value);
      },
      get shift() {
        return Number(controls.shift.value) * this.m;
      },
      get z2() {
        return Number(controls.z2.value);
      },
      get beta() {
        return Number(controls.beta.value) / 180 * Math.PI;
      },
      get fillet() {
        return Number(controls.fillet.value) * this.m;
      },
      get mk() {
        return 1 * this.m;
      },
      get mf() {
        return 1.25 * this.m;
      },
      get backlash() {
        return Number(controls.backlash.value) * Math.PI * this.m / 100;
      },
      get t() {
        return Number(controls.t.value) * this.m;
      }
    };
  }
  function elem(tag, html = "", children = [], props = {}, events = {}) {
    const result = document.createElement(tag);
    if (html != "") result.innerHTML = html;
    children.forEach((c) => result.appendChild(c));
    Object.entries(props).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).forEach(([k, v]) => {
      if (k == "class") {
        result.className = String(v);
      } else if (typeof v == "object") {
        Object.entries(v).forEach(([k2, v2]) => result[k][k2] = v2);
      } else {
        result[k] = v;
      }
    });
    const notIsArray = (maybeArray) => {
      return !Array.isArray(maybeArray);
    };
    Object.entries(events).forEach(([k, v]) => {
      if (notIsArray(v)) v = [v];
      v.forEach((f) => result.addEventListener(k, f));
    });
    return result;
  }
  function createSlider(parent, label, option, oninput) {
    const value = elem("span", String(option.value));
    const slider = elem(
      "input",
      "",
      [],
      { type: "range", ...option },
      { input: () => slider.doInput() }
    );
    slider.doInput = () => {
      value.innerHTML = slider.value;
      oninput();
    };
    const wrapper = elem("div", "", [elem("span", label), value, slider], {
      class: "control-slider"
    });
    parent.appendChild(wrapper);
    return slider;
  }
  function createCheckbox(parent, label, oninput, checked = false) {
    const check = elem(
      "input",
      "",
      [],
      { type: "checkbox", checked },
      { input: (e) => check.doInput(e) }
    );
    check.doInput = oninput;
    const wrapper = elem("label", "", [check, elem("span", label)], {
      class: "control-checkbox"
    });
    parent.appendChild(wrapper);
    return check;
  }
 
  // src/vector.ts
  var Vector = class _Vector {
    x;
    y;
    constructor(x, y) {
      this.x = x;
      this.y = y;
    }
    static polar(r, theta) {
      return new _Vector(r * Math.cos(theta), r * Math.sin(theta));
    }
    angle() {
      return Math.atan2(this.y, this.x);
    }
    equal(v) {
      const epsilon = 1e-6;
      return Math.abs(this.x - v.x) < epsilon && Math.abs(this.y - v.y) < epsilon;
    }
    add(v) {
      return new _Vector(this.x + v.x, this.y + v.y);
    }
    sub(v) {
      return new _Vector(this.x - v.x, this.y - v.y);
    }
    mul(scalar) {
      return new _Vector(this.x * scalar, this.y * scalar);
    }
    rotate(angle) {
      const cos3 = Math.cos(angle);
      const sin3 = Math.sin(angle);
      return new _Vector(this.x * cos3 - this.y * sin3, this.x * sin3 + this.y * cos3);
    }
    addRotated(angle, v) {
      return this.add(v.rotate(angle));
    }
    flipY() {
      return new _Vector(this.x, -this.y);
    }
    flipX() {
      return new _Vector(-this.x, this.y);
    }
    norm() {
      return Math.sqrt(this.x * this.x + this.y * this.y);
    }
    normalize(length = 1) {
      const len = this.norm() / length;
      return new _Vector(this.x / len, this.y / len);
    }
    polar() {
      return [this.norm(), this.angle()];
    }
    inner(v) {
      return this.x * v.x + this.y * v.y;
    }
    outer(v) {
      return this.x * v.y - this.y * v.x;
    }
  };
  function vec(x, y) {
    return new Vector(x, y);
  }
 
  // src/rack.ts
  function rackCurves(params) {
    const { m, z, alpha, shift, fillet, mf, mk, backlash } = params;
    const rp = z * m / 2;
    const { sin: sin3, tan: tan2, min: min2, PI: PI3 } = Math;
    const rc = 0.25 * m;
    const r1 = vec(rp + shift + mk, mk * tan2(alpha) + backlash / 2);
    const r2 = vec(rp + shift - (mf - rc), -(mf - rc) * tan2(alpha) + backlash / 2);
    const r3 = vec(rp + shift - mf, -mf * tan2(alpha) + backlash / 2);
    const fillet1 = rc / (1 - sin3(alpha));
    let fr = min2(fillet, fillet1);
    let c = r3.add(vec(fr, -fr / tan2((PI3 / 2 + alpha) / 2)));
    if (c.y < -PI3 * m / 4) {
      c = c.add(r3.sub(c).mul((-PI3 * m / 4 - c.y) / (r3.y - c.y)));
      fr = c.x - r3.x;
    }
    return {
      r1,
      r2,
      filletCenter: c,
      filletRadius: fr
    };
  }
 
  // src/curve.ts
  var Curve = class _Curve extends Array {
    constructor(point_or_option, ...points) {
      if (point_or_option instanceof Vector) {
        super(point_or_option, ...points);
        this.option = {};
      } else {
        super(...points);
        this.option = point_or_option ?? {};
      }
    }
    // 均等に nPoints 個の点を取り出して新しいカーブとして返す
    static extractPoints(c, nPoints) {
      if (c.length <= nPoints) return c;
      const total = c.totalLength();
      const c2 = new _Curve(c.option);
      let next = 0, len = 0;
      for (let i = 0; i < c.length - 1; i++) {
        if (len >= next) {
          c2.push(c[i]);
          next = total * c2.length / nPoints;
        }
        len += c[i].sub(c[i + 1]).norm();
      }
      c2.push(c[c.length - 1]);
      return c2;
    }
    static circle(r, origin = vec(0, 0), option = {}, n = 100) {
      const c = new _Curve(option);
      for (let i = 0; i <= n; i++) {
        const angle = 2 * Math.PI * i / n;
        c.push(vec(r * Math.cos(angle), r * Math.sin(angle)).add(origin));
      }
      return c;
    }
    option;
    totalLength() {
      let sum = 0;
      for (let i = 0; i < this.length - 1; i++) {
        sum += this[i].sub(this[i + 1]).norm();
      }
      return sum;
    }
    draw(p2, mark = false) {
      if (typeof this.option?.stroke == "undefined") {
        p2.stroke("black");
      } else {
        p2.stroke(this.option.stroke);
      }
      if (typeof this.option?.weight == "undefined") {
        p2.strokeWeight(1);
      } else {
        p2.strokeWeight(this.option.weight);
      }
      if (typeof this.option?.dash == "undefined") {
        p2.drawingContext.setLineDash([]);
      } else {
        p2.drawingContext.setLineDash(this.option.dash);
      }
      if (typeof this.option?.fill == "undefined") {
        p2.noFill();
        p2.beginShape();
        for (let i = 0; i < this.length; i++) {
          p2.vertex(this[i].x, this[i].y);
        }
        p2.endShape();
      } else {
        p2.fill(this.option.fill);
        p2.beginShape();
        for (let i = 0; i < this.length; i++) {
          p2.vertex(this[i].x, this[i].y);
        }
        p2.endShape(p2.CLOSE);
      }
      if (mark) {
        this.forEach((v) => p2.circle(v.x, v.y, 1.5));
      }
    }
    apply(f, option = this.option) {
      return new _Curve(option, ...this.map(f));
    }
    // c1, c2 は2点のみを含む線分
    crossPoint(c2) {
      return crossPoint(this[0], this[1], c2[0], c2[1]);
    }
    direction(i = 0) {
      return this[i].sub(this[i + 1]).normalize();
    }
    createReversed() {
      return new _Curve(this.option, ...this.slice().reverse());
    }
  };
  function crossPoint(p1, p2, q1, q2) {
    const a = p2.sub(p1);
    const b = q2.sub(q1);
    const c = q1.sub(p1);
    const d = a.outer(b);
    if (d == 0) return;
    const s = c.outer(a) / d;
    const t = c.outer(b) / d;
    if (s < 0 || s > 1 || t < 0 || t > 1) return;
    return p1.add(a.mul(t));
  }
  function crossPointXDesc(c1, c2) {
    let i = 0, j = 0;
    while (i < c1.length - 1 && j < c2.length - 1) {
      if (c1[i + 1].x > c2[j].x) {
        i++;
        continue;
      }
      if (c1[i].x < c2[j + 1].x) {
        j++;
        continue;
      }
      const p2 = crossPoint(c1[i], c1[i + 1], c2[j], c2[j + 1]);
      if (p2) return [p2, i, j];
      if (c1[i + 1].x > c2[j + 1].x) {
        i++;
      } else {
        j++;
      }
    }
    return [];
  }
  function distanceFromLine(p2, q1, q2) {
    const a = q2.sub(q1);
    const b = p2.sub(q1);
    return Math.abs(a.outer(b) / a.norm());
  }
  function closestPointXDesc(c1, c2) {
    let i = 0, j = 0, minimum = [Infinity];
    while (i < c1.length - 1 && j < c2.length - 1) {
      if (c1[i + 1].x > c2[j].x) {
        i++;
        continue;
      }
      if (c1[i].x < c2[j + 1].x) {
        j++;
        continue;
      }
      if (c1[i].x < c2[j].x) {
        const d = distanceFromLine(c1[i], c2[j], c2[j + 1]);
        if (d < minimum[0]) minimum = [d, i, j, 0];
      } else {
        const d = distanceFromLine(c2[j], c1[i], c1[i + 1]);
        if (d < minimum[0]) minimum = [d, i, j, 1];
      }
      if (c1[i + 1].x > c2[j + 1].x) {
        i++;
      } else {
        j++;
      }
    }
    if (minimum.length == 1) {
      if (j < c2.length - 1) {
        const d = distanceFromLine(c1[i], c2[j], c2[j + 1]);
        return [d, i, j, 0];
      } else {
        const d = distanceFromLine(c2[j], c1[i], c1[i + 1]);
        return [d, i, j, 1];
      }
    }
    return minimum;
  }
 
  // src/function.ts
  function minimize(l, r, f, epsilon = 1e-6) {
    const phi = (1 + Math.sqrt(5)) / 2;
    let m1 = r - (r - l) / phi;
    let m2 = l + (r - l) / phi;
    let f1 = f(m1);
    let f2 = f(m2);
    while (Math.abs(l - r) > epsilon) {
      if (f1 < f2) {
        r = m2;
        m2 = m1;
        f2 = f1;
        m1 = r - (r - l) / phi;
        f1 = f(m1);
      } else {
        l = m1;
        m1 = m2;
        f1 = f2;
        m2 = l + (r - l) / phi;
        f2 = f(m2);
      }
    }
    return (l + r) / 2;
  }
  function root(l, r, f, epsilon = 1e-6) {
    const fl = f(l);
    const fr = f(r);
    if (fl * fr > 0) {
      return Math.abs(fl) < Math.abs(fr) ? l : r;
    }
    if (fl > fr) [l, r] = [r, l];
    while (Math.abs(l - r) > epsilon) {
      const m = (l + r) / 2;
      if (f(m) < 0) {
        l = m;
      } else {
        r = m;
      }
    }
    return (l + r) / 2;
  }
 
  // src/gear.ts
  var { ceil, max, abs, sin, cos, PI } = Math;
  function gearCurve(params) {
    params = { ...params };
    const { m, z } = params;
    const rp = m * z / 2;
    const { r1, r2, filletCenter, filletRadius } = rackCurves(params);
    const shiftRotate = (t, p2) => p2.add(vec(0, -rp * t)).rotate(t);
    const dShiftRotate = (t, p2) => {
      return p2.rotate(t + PI / 2).add(
        vec(
          rp * (sin(t) + t * cos(t)),
          //
          rp * (-cos(t) + t * sin(t))
        )
      );
    };
    const rackTrace = (t) => {
      const p1 = shiftRotate(t, r1);
      const Dp = shiftRotate(t, r2).sub(p1);
      const dp1 = dShiftRotate(t, r1);
      const dDp = dShiftRotate(t, r2).sub(dp1);
      const s = dp1.outer(Dp) / dDp.outer(Dp);
      return [p1.sub(Dp.mul(s)), s];
    };
    const filletTrace = (t) => {
      const p0 = shiftRotate(t, filletCenter);
      const v1 = dShiftRotate(t, filletCenter).normalize(filletRadius);
      const p2 = p0.addRotated(-PI / 2, v1);
      const p3 = p0.addRotated(PI / 2, v1);
      return p2.x - p2.y < p3.x - p3.y ? p2 : p3;
    };
    let { cInvolute, involuteT } = calculateInvoluteCurve({ ...params, mk: params.mf }, rackTrace);
    let cFillet = new Curve();
    let filletT = [];
    [cFillet, filletT] = calculateFilletCurve(
      params,
      shiftRotate,
      filletTrace,
      filletCenter,
      filletRadius,
      r2
    );
    ({ cInvolute, involuteT, cFillet, filletT } = combineCurvesAtIntersection(
      cInvolute,
      involuteT,
      rackTrace,
      cFillet,
      filletT,
      filletTrace
    ));
    const rot = -PI / z / 2;
    cInvolute = cInvolute.apply((v) => v.rotate(rot));
    cFillet = cFillet.apply((v) => v.rotate(rot));
    const dt = 1 / 1e3;
    const result = [cFillet.createReversed(), cInvolute.createReversed()];
    if (cInvolute[0].y < -dt) {
      let distanceFromCircleToCurve = function(r) {
        const c2 = new Vector(rp + params.mf + params.shift - r, 0);
        const t = minimize(
          involuteT[0],
          involuteT.at(-1),
          (t2) => c2.sub(rackTrace(t2)[0].rotate(rot)).norm()
        );
        const r22 = c2.sub(rackTrace(t)[0].rotate(rot)).norm();
        return r22 - r;
      };
      const tk = root(
        involuteT[0],
        involuteT.at(-1),
        (t) => rackTrace(t)[0].norm() - (rp + params.mk + params.shift)
      );
      const pk = rackTrace(tk)[0].rotate(rot);
      let n = rackTrace(tk + dt)[0].rotate(rot).sub(pk).rotate(PI / 2);
      n = n.mul(pk.y / n.y);
      const c = pk.sub(n);
      const rMax = rp + params.mf + params.shift - c.norm();
      let t0 = tk;
      if (distanceFromCircleToCurve(rMax) > 0) {
        const r = root(0, n.norm(), (r3) => {
          const c3 = pk.sub(n.normalize(r3));
          return rp + params.mf + params.shift - c3.norm() - r3;
        });
        const c2 = pk.sub(n.normalize(r));
        result.push(arcCurve(r, pk.sub(c2).angle(), c2.angle(), c2));
        result.push(arcCurve(rp + params.mf + params.shift, c2.angle(), 0));
      } else {
        const r = root(0, rMax, distanceFromCircleToCurve);
        const c2 = new Vector(rp + params.mf + params.shift - r, 0);
        t0 = minimize(
          involuteT[0],
          involuteT.at(-1),
          (t) => c2.sub(rackTrace(t)[0].rotate(rot)).norm()
        );
        result.push(arcCurve(r, rackTrace(t0)[0].rotate(rot).sub(c2).angle(), 0, c2));
      }
      involuteT = Array.from({ length: 11 }, (_, i) => t0 + (involuteT.at(-1) - t0) * i / 10);
      cInvolute = new Curve(cInvolute.option, ...involuteT.map((t) => rackTrace(t)[0].rotate(rot)));
      result[1] = cInvolute.createReversed();
    }
    return result;
  }
  function calculateInvoluteCurve(params, rackTrace) {
    const { alpha, mk, mf, shift, z, m } = params;
    const rp = m * z / 2;
    let involuteS, involuteE;
    const involuteT = [];
    const involuteN = 300;
    involuteS = minimize(0, 90 - alpha, (t) => abs(rackTrace(t)[0].norm() - rp - mk - shift));
    if (rackTrace(involuteS)[0].angle() > PI / z / 2) {
      involuteS = minimize(0, involuteS, (t) => abs(rackTrace(t)[0].angle() - PI / z / 2));
    }
    involuteE = minimize(0, -2 * alpha, (t) => rackTrace(t)[0].norm());
    if (rackTrace(involuteE)[0].norm()) {
      involuteE = root(0, involuteE, (t) => rackTrace(t)[0].norm() - (rp - mf + shift));
    }
    const cInvolute = new Curve();
    for (let i = 0; i < involuteN; i++) {
      const t = involuteS + (involuteE - involuteS) * i / involuteN;
      cInvolute.push(rackTrace(t)[0]);
      involuteT.push(t);
    }
    return { cInvolute, involuteT };
  }
  function calculateFilletCurve(params, shiftRotate, filletTrace, center, radius, r2) {
    const { shift, alpha, m, z, mk } = params;
    const rp = m * z / 2;
    const rk = rp + mk;
    let filletS, filletE;
    const filletD = 0.05;
    for (filletS = 0; ; filletS -= filletD) {
      if (shiftRotate(filletS, center).norm() > rk + shift) break;
    }
    for (filletE = filletD; ; filletE += filletD) {
      if (shiftRotate(filletE, center).norm() > rk + shift) break;
    }
    const filletM = minimize(filletS, filletE, (t) => filletTrace(t).norm());
    if (shiftRotate(filletM, center).norm() > rp) {
      [filletS, filletE] = [filletE, filletS];
    }
    filletE = filletM;
    let filletF = false;
    let last = null;
    let di = 1;
    let filletS2 = filletS;
    const filletN = 60, filletT = [];
    const cFillet = new Curve();
    for (let i = 0; i <= filletN; i += di) {
      const t = filletS + (filletE - filletS) * i / filletN;
      const p2 = filletTrace(t);
      if (!filletF && !(p2.norm() > r2.norm() && p2.sub(r2).angle() < alpha)) {
        filletF = true;
        filletS2 = t;
        if (last) {
          cFillet.push(last);
          filletT.push(t - (filletE - filletS) * di / filletN);
        }
      }
      if (filletF) {
        cFillet.push(p2);
        filletT.push(t);
      }
      const c = shiftRotate(t, center);
      if (i > 0 && c.sub(shiftRotate(filletM, center)).norm() < m / 1e3) {
        const ts = abs(p2.sub(shiftRotate(filletM, center)).angle());
        const te = PI - PI / z / 2;
        const tn = ceil(abs(te - ts) / PI * 180);
        for (let j = 1; j <= tn; j++) {
          const tt = ts + (te - ts) * j / tn;
          cFillet.push(shiftRotate(filletM, center).add(Vector.polar(radius, tt)));
        }
        break;
      }
      if (i > 0 && p2.sub(last).norm() > m / 20) di /= 2;
      last = p2;
    }
    filletS = filletS2;
    return [cFillet, filletT];
  }
  function combineCurvesAtIntersection(cInvolute, involuteT, rackTrace, cFillet, filletT, filletTrace) {
    const [p2, i, j] = crossPointXDesc(cInvolute, cFillet);
    if (p2) {
      cInvolute.splice(i + 1, Infinity, p2);
      involuteT.splice(
        i + 1,
        Infinity,
        minimize(involuteT[i], involuteT[i + 1], (t) => rackTrace(t)[0].sub(p2).norm())
      );
      cFillet.splice(0, j + 1, p2);
      filletT.splice(
        0,
        j + 1,
        minimize(filletT[j], filletT[j + 1], (t) => filletTrace(t).sub(p2).norm())
      );
    } else {
      const [, i2, j2, f] = closestPointXDesc(cInvolute, cFillet);
      if (f) {
        cInvolute.splice(i2 + 1, Infinity, cFillet[j2]);
        cFillet.splice(0, j2);
        involuteT.splice(
          i2 + 1,
          Infinity,
          minimize(involuteT[i2], involuteT[i2 + 1], (t) => rackTrace(t)[0].sub(cFillet[j2]).norm())
        );
        filletT.splice(0, j2);
      } else {
        cInvolute.splice(i2 + 1, Infinity);
        cFillet.splice(0, j2 + 1, cInvolute[i2]);
        involuteT.splice(i2 + 1, Infinity);
        filletT.splice(
          0,
          j2 + 1,
          minimize(filletT[j2], filletT[j2 + 1], (t) => filletTrace(t).sub(cInvolute[i2]).norm())
        );
      }
    }
    return { cInvolute, involuteT, cFillet, filletT };
  }
  function arcCurve(r, st, et, c = vec(0, 0), dt = 0.02, epsilon = 1e-4) {
    const result = new Curve();
    if (r * abs(et - st) > epsilon) {
      const nt = max(2, ceil(abs(et - st) / dt));
      for (let i = 0; i <= nt; i++) {
        const t = st + (et - st) * i / nt;
        result.push(c.add(Vector.polar(r, t)));
      }
    }
    return result;
  }
 
  // src/gears_cache.ts
  var prevParams = null;
  var gear1 = null;
  function cachedGears(params) {
    const changedKeys = prevParams !== null ? getChangedKeys(params, prevParams) : [];
    if (!gear1 || changedKeys.filter((k) => !["m2", "shift2"].includes(k)).length > 0) {
      gear1 = gearCurve({ ...params, backlash: -params.backlash });
    }
    prevParams = pickParams(params);
    return { gear1 };
  }
  function objectPick(obj, keys) {
    return keys.reduce(
      (acc, key) => {
        acc[key] = obj[key];
        return acc;
      },
      {}
    );
  }
  function objectKeys(obj) {
    return Object.keys(obj);
  }
  function getChangedKeys(current, prev) {
    return objectKeys(prev).filter((key) => current[key] !== prev[key]);
  }
  function pickParams(params) {
    return objectPick(params, [
      "m",
      "z",
      "alpha",
      "shift",
      "z2",
      "shift2",
      "fillet",
      "backlash",
      "inner"
    ]);
  }
 
  // src/spline.ts
  var LengthMismatchException = class extends Error {
  };
  function interpolate(xs, ys) {
    if (xs.length != ys.length) throw new LengthMismatchException();
    if (xs.length < 5) {
      return (x) => {
        if (x <= xs[0]) return ys[0];
        for (let i = 1; i < xs.length; i++) {
          if (x <= xs[i]) {
            return ys[i - 1] + (ys[i] - ys[i - 1]) * (x - xs[i - 1]) / (xs[i] - xs[i - 1]);
          }
        }
        return ys[xs.length - 1];
      };
    }
    xs = xs.slice(0);
    const slopes = calcSlopes(xs, ys);
    const coefs = calcCoefs(xs, ys, slopes);
    return (x) => {
      let i = findSegment(x, xs);
      i = Math.max(0, Math.min(i, coefs.length - 1));
      return polynomial(x - xs[i], coefs[i]);
    };
  }
  function findSegment(x, xs) {
    let l = 0;
    let r = xs.length - 1;
    while (l <= r) {
      const m = l + r >>> 1;
      const mx = xs[m];
      if (mx < x) {
        l = m + 1;
      } else if (mx > x) {
        r = m - 1;
      } else if (mx == x) {
        return m;
      } else {
        throw new Error("NaN found in xs.");
      }
    }
    return l - 1;
  }
  function polynomial(x, coefs) {
    return coefs.reduce((acc, coef) => acc * x + coef, 0);
  }
  function calcSlopes(xs, ys) {
    const n = xs.length;
    const dydx = new Array(n - 1);
    for (let i = 0; i < dydx.length; i++) {
      dydx[i] = (ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]);
    }
    const weights = new Array(n - 1);
    for (let i = 1; i < weights.length; i++) {
      weights[i] = Math.abs(dydx[i] - dydx[i - 1]);
    }
    const result = new Array(n);
    result[0] = slopeFrom3Points(xs, ys, 0, 0, 1, 2);
    result[1] = slopeFrom3Points(xs, ys, 1, 0, 1, 2);
    for (let i = 2; i < n - 2; i++) {
      const wp1 = weights[i + 1];
      const wm1 = weights[i - 1];
      if (Math.abs(wp1) < Number.EPSILON && Math.abs(wm1) < Number.EPSILON) {
        const dx = xs[i + 1] - xs[i];
        const dxm1 = xs[i] - xs[i - 1];
        result[i] = (dx * dydx[i - 1] + dxm1 * dydx[i]) / (dx + dxm1);
      } else {
        result[i] = (wp1 * dydx[i - 1] + wm1 * dydx[i]) / (wp1 + wm1);
      }
    }
    result[n - 2] = slopeFrom3Points(xs, ys, n - 2, n - 3, n - 2, n - 1);
    result[n - 1] = slopeFrom3Points(xs, ys, n - 1, n - 3, n - 2, n - 1);
    return result;
  }
  function slopeFrom3Points(xs, ys, i, i1, i2, i3) {
    const dx = xs[i] - xs[i1];
    const dx2 = xs[i2] - xs[i1];
    const dx3 = xs[i3] - xs[i1];
    const dydx2 = (ys[i2] - ys[i1]) / dx2;
    const dydx3 = (ys[i3] - ys[i1]) / dx3;
    return (dydx3 - dydx2) * (2 * dx - dx2) / (dx3 - dx2) + dydx2;
  }
  function calcCoefs(xs, ys, slopes) {
    const n = xs.length;
    const coefs = [];
    for (let i = 0; i < n - 1; i++) {
      const dx = xs[i + 1] - xs[i];
      const dydx = (ys[i + 1] - ys[i]) / dx;
      const dfi = slopes[i];
      const dfi1 = slopes[i + 1];
      coefs.push([
        (-2 * dydx + dfi + dfi1) / (dx * dx),
        // 3rd
        (3 * dydx - 2 * dfi - dfi1) / dx,
        // 2nd
        dfi,
        // 1st
        ys[i]
        // 0
      ]);
    }
    return coefs;
  }
 
  // src/main.ts
  var { PI: PI2, asin, sqrt, cos: cos2, tan, sin: sin2, min, floor, max: max2, ceil: ceil2 } = Math;
  var sketch = (p2) => {
    let params;
    const canvas = {
      width: 1e3,
      height: 1e3,
      ox: 0,
      oy: 0
    };
    p2.setup = () => {
      const { width, height } = canvas;
      canvas.ox = width / 2;
      canvas.oy = height * (2 / 3);
      const c = p2.createCanvas(width, height);
      c.style("max-width", "100%");
      c.style("height", "auto");
      const wrapper = c.elt.parentElement;
      params = initializeControls(wrapper, inputChanged);
      inputChanged();
    };
    function inputChanged() {
      const { ox, oy, width, height } = canvas;
      p2.fill(220);
      p2.rect(0, 0, width, height);
      const { gear1: gear12 } = cachedGears(params);
      const { m, z, z2, shift, t, mk, mf, beta } = params;
      const rp = m * z / 2;
      const rp2 = m * z2 / 2;
      const factor = tan(beta) / rp;
      const rp2t = rp2 + t;
      const rp2t2 = rp2t ** 2;
      function trans(v, theta2) {
        v = v.rotate(-theta2 + t * factor);
        const phi = theta2 * z / z2;
        if (factor == 0) return new Vector(v.x, phi + asin(v.y / rp2t));
        function dt_error(dt3) {
          const a = -dt3 * factor;
          const y = v.x * sin2(a) + v.y * cos2(a);
          return rp2t - dt3 - sqrt(rp2t2 - y * y);
        }
        const ddt = m / 10;
        let dt2 = 0;
        while (dt_error(dt2) > 0) dt2 += ddt;
        dt2 = root(dt2 - ddt, dt2, dt_error);
        v = v.rotate(-dt2 * factor);
        return new Vector(v.x, phi + asin(v.y / rp2t));
      }
      let shape = [];
      gear12.forEach(
        (c) => c.forEach((v) => {
          if (shape.length > 0 && v.sub(shape.at(-1)).norm() < 1e-5) return;
          shape.unshift(v);
        })
      );
      shape = shape.toReversed().map((v) => v.flipY()).concat(shape);
      const nx = 200;
      let minY = Array(nx).fill(PI2);
      let maxY = Array(nx).fill(-PI2);
      const dx = (mk + mf) / (minY.length - 1);
      const rt = rp + mf + shift;
      function one_step(theta2, draw) {
        const translated = shape.map((v) => trans(v, theta2));
        if (draw) {
          drawGear(
            p2,
            translated.map((v) => new Vector(v.x, v.y * rp2t)),
            ox - rp - shift,
            oy
          );
        }
        function interpolate3(j, k) {
          const xj = translated[j].x;
          const yj = translated[j].y;
          const xj1 = translated[j - 1].x;
          const yj1 = translated[j - 1].y;
          const y = yj1 + (rt - k * dx - xj1) * (yj - yj1) / (xj - xj1);
          if (y < minY[k]) minY[k] = y;
          if (y > maxY[k]) maxY[k] = y;
        }
        let maxX = translated[0].x;
        for (let j = 1; j < translated.length; j++) {
          const k1 = (rt - translated[j - 1].x) / dx;
          const k2 = (rt - translated[j].x) / dx;
          for (let k = ceil2(min(k1, k2)); k <= floor(max2(k1, k2)); k++) {
            interpolate3(j, k);
          }
          maxX = max2(maxX, translated[j].x);
        }
        return maxX;
      }
      p2.strokeWeight(1);
      p2.stroke(160);
      const dt = PI2 * 2 / z ** 0.5 / 600;
      let theta = t * factor;
      const dir = t > 0 ? 1 : -1;
      let i = 0;
      while (one_step(theta, i % 20 == 0) > rt - mf - mk) {
        theta -= dir * dt;
        i++;
      }
      let profile1 = create_profile();
      theta = t * factor;
      minY = Array(nx).fill(PI2);
      maxY = Array(nx).fill(-PI2);
      i = 0;
      while (one_step(theta, i % 20 == 0) > rt - mf - mk) {
        theta += dir * dt;
        i++;
      }
      let profile2 = create_profile();
      if (dir < 0) {
        [profile1, profile2] = [profile2, profile1];
      }
      function create_profile() {
        const result = minY.map((y, i2) => new Vector(rt - dx * i2, y)).toReversed().concat(maxY.map((y, i2) => new Vector(rt - dx * i2, y))).filter((v) => -PI2 < v.y && v.y < PI2);
        for (let i2 = result.length - 1; i2 > 0; i2--) {
          if (distance(result[i2], result[i2 - 1]) < params.m / 1e3) {
            result.splice(i2, 1);
          }
        }
        return result;
      }
      function draw_profile(tooth) {
        p2.beginShape();
        tooth.forEach((v) => {
          p2.vertex(ox + v.x - (rp + shift), oy + v.y * rp2t);
        });
        p2.endShape();
      }
      function distance(v1, v2) {
        return sqrt((v1.x - v2.x) ** 2 + rp2t2 * (v1.y - v2.y) ** 2);
      }
      function interpolate2(profile) {
        const reduced = [[0, profile[0]]];
        const n = 20;
        const m2 = 30;
        for (let j = 1; j < n; j++) {
          const i2 = floor(profile.length * j / n);
          reduced.push([i2, profile[i2]]);
        }
        reduced.push([profile.length - 1, profile[profile.length - 1]]);
        function addPoint() {
          let errM = 0;
          let errI = 0;
          const ts = [0];
          for (let i2 = 1; i2 < reduced.length; i2++) {
            ts.push(ts.at(-1) + distance(reduced[i2][1], reduced[i2 - 1][1]));
          }
          const cx = interpolate(
            ts,
            reduced.map((r) => r[1].x)
          );
          const cy = interpolate(
            ts,
            reduced.map((r) => r[1].y)
          );
          for (let j = 1; j < reduced.length; j++) {
            const i1 = reduced[j - 1][0];
            const i2 = reduced[j][0];
            for (let i3 = i1 + 1; i3 < i2; i3++) {
              const { x, y } = profile[i3];
              const t2 = minimize(ts[j - 1], ts[j], (t3) => (x - cx(t3)) ** 2 + (y - cy(t3)) ** 2);
              const err = sqrt((x - cx(t2)) ** 2 + (y - cy(t2)) ** 2);
              if (err > errM) {
                errM = err;
                errI = i3;
              }
            }
          }
          reduced.push([errI, profile[errI]]);
          reduced.sort((a, b) => a[0] - b[0]);
          return errM;
        }
        while (reduced.length < m2) {
          addPoint();
        }
        return reduced.map((r) => r[1]);
      }
      function evenlySpaced(points, n) {
        let ts = [0];
        for (let i2 = 1; i2 < points.length; i2++) {
          ts.push(ts.at(-1) + distance(points[i2], points[i2 - 1]));
        }
        ts = ts.map((t2) => t2 / ts.at(-1));
        const cx = interpolate(
          ts,
          points.map((r) => r.x)
        );
        const cy = interpolate(
          ts,
          points.map((r) => r.y)
        );
        const m2 = n * 10;
        let len = [0];
        let last = points[0];
        for (let i2 = 1; i2 < m2; i2++) {
          const t2 = i2 / (m2 - 1);
          const curr = new Vector(cx(t2), cy(t2));
          len.push(len.at(-1) + distance(curr, last));
          last = curr;
        }
        len = len.map((l) => l / len.at(-1));
        const result = [points[0]];
        let j = 1;
        for (let i2 = 1; i2 < n - 1; i2++) {
          const t2 = i2 / (n - 1);
          while (len[j] < t2) j++;
          const t1 = (t2 - len[j - 1]) / (len[j] - len[j - 1]);
          result.push(new Vector(cx((j + t1) / m2), cy((j + t1) / m2)));
        }
        result.push(points.at(-1));
        return result;
      }
      function drawPoints(curve) {
        curve.forEach((v) => {
          p2.circle(ox + v.x - (rp + shift), oy + v.y * rp2t, 5);
        });
      }
      p2.strokeWeight(2);
      p2.stroke("red");
      draw_profile(profile1);
      let curve1 = interpolate2(profile1);
      curve1 = evenlySpaced(curve1, 30);
      drawPoints(curve1);
      p2.stroke("blue");
      draw_profile(profile2);
      let curve2 = interpolate2(profile2);
      curve2 = evenlySpaced(curve2, 30);
      drawPoints(curve2);
      p2.strokeWeight(1);
      p2.stroke("green");
      p2.line(ox, 0, ox, height);
      p2.line(0, oy, width, oy);
      const pitch = (z2 * m / 2 + t) / (z2 * m / 2) * (PI2 * m);
      p2.line(0, oy + pitch / 2, width, oy + pitch / 2);
      p2.line(0, oy - pitch / 2, width, oy - pitch / 2);
      p2.line(ox - mk, 0, ox - mk, height);
      p2.line(ox + mk, 0, ox + mk, height);
      p2.line(ox + mf, 0, ox + mf, height);
    }
    const drawGear = (p3, gear, ox, oy) => {
      p3.noFill();
      p3.beginShape();
      gear.forEach((v) => {
        p3.vertex(ox + v.x, oy + v.y);
      });
      p3.endShape();
    };
  };
  if (true) {
    sketch(p);
  } else {
    new p5(sketch, window["p5wrapper"]);
  }

* python が遅すぎる [#s83697ff]

上記の方法で正しい歯形が出力できるようになったのだけれど、
正確な歯形を得るには膨大な計算量を要求されるみたいだ。

とはいえ javascript だとさほどの待ち時間なく終わる程度のものなのだけれど。

その同じ処理を python で実行すると数十倍(?)待たされる。

どうするべきなのか???

ネイティブコードを呼び出すような書き換えは実行環境依存になっちゃうし、できればしたくないんだけどなあ・・・

Counter: 602 (from 2010/06/03), today: 1, yesterday: 7