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

更新


#author("2025-03-20T00:53:40+00:00","default:administrator","administrator")
#author("2025-03-20T00:55:18+00:00","default:administrator","administrator")
[[公開メモ]]

* 歯車について勉強するシリーズ [#dd0be2de]

- [[工作/歯車について勉強する]] 歯車の形状についての基礎
- [[工作/歯車について勉強する2]] 仮想的なラックを使って実用的な歯車形状を切り出す話
- [[工作/歯車について勉強する3]] 歯車に関するいろいろな計算機
- [[工作/歯車について勉強する4]] 転位歯車の中心間距離と逆インボリュート関数
- [[工作/歯車について勉強する5]] ウォームホイールの歯形計算について
- [[工作/歯車について勉強する/ゼネバ歯車]] 間欠歯車の一種です
- [[工作/Fusion360歯車スクリプト]] 勉強した結果を使って作ってみました
- [[工作/Fusion360歯車切削スクリプト]] Fusion 360 内で歯車の歯切りを行うスクリプトです
- [[工作/Fusion360曲面生成スクリプト]] 歯切り結果の表面をきれいにするスクリプトです
- [[工作/Fusion360ジョイント駆動スクリプト]] ジョイントを動かしてアニメーションさせるスクリプトです

* ちょっと思い立って歯車について勉強してみました [#dc67e7ef]

#contents

** 謝辞 [#o936d9ce]

以下のサイトを参考にさせていただきました。大変感謝しています。

- 歯車好きの元クルマエンジニア さんの はてなブログ 「歯車のハナシ」~
https://involutegearsoft.hatenablog.com/ ~
~
- KHK 小原歯車工業株式会社 さんのホームページ~
https://www.khkgears.co.jp/gear_technology/guide_info.html ~
~
- chromia さんの Qiita ページ 「歯車を描く」~
https://qiita.com/chromia/items/629311346c80dfd0eac7 ~

** 成果物 [#y7ffbd77]

いろんな歯車の歯形を計算して描画できるようになりました。

こちらは [[Fusion360 用の自作スクリプト>工作/Fusion360歯車スクリプト]] で生成したもの:~
&ref(工作/Fusion360歯車スクリプト/平歯車/spur-gears2.gif,,33%);
&ref(工作/Fusion360歯車スクリプト/はすば歯車/double-helical.gif,,25%);
&ref(工作/Fusion360歯車スクリプト/内歯車/internal3.gif,,33%);
&ref(工作/Fusion360歯車スクリプト/ラック/rack8.gif,,25%);
&ref(工作/Fusion360歯車スクリプト/ウォーム&ウォームホイール/worm_wheel5.gif,,25%);
&ref(工作/Fusion360歯車スクリプト/かさ歯車/bevel2.gif,,25%);

下図ではスライダーを動かすことでいろいろな歯車を描くことができます。

歯車を設置する際の軸間距離などを求めるための計算機はこちら~
→ [[工作/歯車について勉強する3#ud5c339c]]

 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) {
      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 inputChanged = () => {
      };
      const controls = {
        m: createSlider(
          wrapControls,
          "モジュール",
          { min: 10, max: 200, value: 80, step: 1 },
          inputChanged
        ),
        alpha: createSlider(
          wrapControls,
          "圧力角",
          { min: 10, max: 32, value: 20, step: 1 },
          inputChanged
        ),
        z: createSlider(wrapControls, "歯数A", { min: 4, max: 60, value: 19, step: 1 }, inputChanged),
        shift: createSlider(
          wrapControls,
          "転位A",
          { min: -0.5, max: 2, value: 0, step: 0.05 },
          inputChanged
        ),
        z2: createSlider(wrapControls, "歯数B", { min: 4, max: 60, value: 6, step: 1 }, inputChanged),
        shift2: createSlider(
          wrapControls,
          "転位B",
          { 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
        ),
        velocity: createSlider(
          wrapControls,
          "速度",
          { min: -1, max: 10, value: 1, step: 0.1 },
          inputChanged
        ),
        theta: createSlider(
          wrapControls,
          "回転角",
          { min: -360, max: 360, value: 0, step: 0.1 },
          inputChanged
        ),
        df: createCheckbox(wrapControls, "歯底円", inputChanged, false),
        db: createCheckbox(wrapControls, "基礎円", inputChanged, false),
        dp: createCheckbox(wrapControls, "基準円", inputChanged, true),
        dk: createCheckbox(wrapControls, "歯先円", inputChanged, false),
        play: createCheckbox(wrapControls, "自動更新", inputChanged, true),
        connection: createCheckbox(wrapControls, "接続点", inputChanged, false),
        inner: createCheckbox(wrapControls, "内歯車", inputChanged, false),
        stop: false
      };
      controls.theta.addEventListener("mousedown", () => {
        controls.stop = true;
      });
      controls.theta.addEventListener("mouseup", () => {
        controls.stop = false;
      });
      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 shift2() {
          return Number(controls.shift2.value) * this.m;
        },
        get theta() {
          return -(Number(controls.theta.value) / 180) * Math.PI;
        },
        set theta(value) {
          controls.theta.value = String(-value * 180 / Math.PI);
          controls.theta.doInput();
        },
        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 play() {
          return controls.play.checked && !controls.stop;
        },
        get velocity() {
          return Number(controls.velocity.value) / 180 * Math.PI;
        },
        get df() {
          return controls.df.checked;
        },
        get db() {
          return controls.db.checked;
        },
        get dp() {
          return controls.dp.checked;
        },
        get dk() {
          return controls.dk.checked;
        },
        get connection() {
          return controls.connection.checked;
        },
        get inner() {
          return controls.inner.checked;
        },
        incrementTheta() {
          if (this.theta - this.velocity < -2 * Math.PI) {
            this.theta += -this.velocity + 4 * Math.PI;
          } else {
            this.theta += -this.velocity;
          }
        }
      };
    }
    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 cos = Math.cos(angle);
        const sin = Math.sin(angle);
        return new _Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
      }
      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/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();
      }
    };
    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/rack.ts
    function rackCurves(params) {
      const { m, z, alpha, shift, fillet, mf, mk, backlash } = params;
      const rp = z * m / 2;
      const { sin, tan, min, PI } = Math;
      const rc = 0.25 * m;
      const r1 = vec(rp + shift + mk, mk * tan(alpha) + backlash / 2);
      const r2 = vec(rp + shift - (mf - rc), -(mf - rc) * tan(alpha) + backlash / 2);
      const r3 = vec(rp + shift - mf, -mf * tan(alpha) + backlash / 2);
      const fillet1 = rc / (1 - sin(alpha));
      let fr = min(fillet, fillet1);
      let c = r3.add(vec(fr, -fr / tan((PI / 2 + alpha) / 2)));
      if (c.y < -PI * m / 4) {
        c = c.add(r3.sub(c).mul((-PI * m / 4 - c.y) / (r3.y - c.y)));
        fr = c.x - r3.x;
      }
      return {
        r1,
        r2,
        filletCenter: c,
        filletRadius: fr
      };
    }
  
    // src/function.ts
    function minimize(l, r, f, epsilon = 1e-6) {
      while (l + epsilon < r) {
        const m1 = l + (r - l) / 3;
        const m2 = r - (r - l) / 3;
        const f1 = f(m1);
        const f2 = f(m2);
        if (f1 < f2) {
          r = m2;
        } else {
          l = m1;
        }
      }
      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
    function gearCurve(params) {
      params = { ...params };
      const { m, z } = params;
      if (params.inner) {
        [params.mk, params.mf] = [params.mf, params.mk];
        params.fillet = 0;
      }
      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) => {
        const { sin, cos, PI } = Math;
        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(-Math.PI / 2, v1);
        const p3 = p0.addRotated(Math.PI / 2, v1);
        return p2.x - p2.y < p3.x - p3.y ? p2 : p3;
      };
      let { cInvolute, involuteT } = calculateInvoluteCurve(params, rackTrace);
      let cFillet = new Curve();
      let filletT = [];
      if (!params.inner) {
        [cFillet, filletT] = calculateFilletCurve(
          params,
          shiftRotate,
          filletTrace,
          filletCenter,
          filletRadius,
          r2
        );
        ({ cInvolute, involuteT, cFillet, filletT } = combineCurvesAtIntersection(
          cInvolute,
          involuteT,
          rackTrace,
          cFillet,
          filletT,
          filletTrace
        ));
      }
      return assembleGearCurves(params, cInvolute, cFillet);
    }
    function calculateInvoluteCurve(params, rackTrace) {
      const { alpha, mk, mf, shift, z, m } = params;
      const rp = m * z / 2;
      const { abs, PI } = Math;
      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 { abs, PI, ceil } = Math;
      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 assembleGearCurves(params, cInvolute, cFillet) {
      const { m, shift, z, mf } = params;
      const rf = m * z / 2 - mf;
      const { PI } = Math;
      let gear = [];
      const cTop = arcCurve(cInvolute[0].norm(), PI / z / 2, cInvolute[0].angle());
      gear.push(cTop);
      const cInvolute2 = Curve.extractPoints(cInvolute, 30).filter(
        (_, i, array) => i < 2 || i > array.length - 3 || i % 2 == 1
      );
      gear.push(new Curve(cInvolute.option, ...cInvolute2));
      const cFillet2 = Curve.extractPoints(cFillet, 30).filter(
        (_, i, array) => i < 2 || i > array.length - 3 || i % 2 == 1
      );
      gear.push(new Curve(cFillet.option, ...cFillet2));
      if (params.inner) {
        const cBottom = arcCurve(cInvolute.at(-1).norm(), cInvolute.at(-1).angle(), -PI / z / 2);
        gear.push(cBottom);
      } else if (cFillet.length > 0) {
        const cBottom = arcCurve(rf + shift, cFillet[cFillet.length - 1].angle(), -PI / z / 2);
        gear.push(cBottom);
      }
      gear = gear.map((c) => c.apply((v) => v.rotate(-PI / z / 2)));
      let connections = [];
      connections.push(cInvolute[0].rotate(-PI / z / 2).polar());
      connections.push(
        cInvolute.at(-1).rotate(-PI / z / 2).polar()
      );
      if (cFillet.length > 0) {
        connections.push(
          cFillet.at(-1).rotate(-PI / z / 2).polar()
        );
      }
      let tooth = gear.reduce(
        (a, c) => a.slice(0, -1).concat(c.map((v) => v.polar())),
        []
      );
      tooth = tooth.map((v) => [v[0], -v[1]]).reverse().concat(tooth);
      connections = connections.map((v) => [v[0], -v[1]]).reverse().concat(connections);
      const result = { curve: [...tooth], connections: [...connections] };
      for (let i = 1; i < z; i++) {
        result.curve = result.curve.concat(
          tooth.map((v) => [v[0], v[1] - i * 2 * PI / z])
        );
        result.connections = result.connections.concat(
          connections.map((v) => [v[0], v[1] - i * 2 * PI / z])
        );
      }
      return result;
    }
    function arcCurve(r, st, et, c = vec(0, 0), dt = 0.02, epsilon = 1e-4) {
      const { abs, ceil, max } = Math;
      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;
    var gear2 = 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.inner ? -params.backlash : params.backlash });
      }
      if (!gear2 || changedKeys.filter((k) => !["m", "shift", "inner"].includes(k)).length > 0) {
        gear2 = gearCurve({ ...params, z: params.z2, shift: params.shift2, inner: false });
      }
      prevParams = pickParams(params);
      return { gear1, gear2 };
    }
    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/main.ts
    var sketch = (p2) => {
      let params;
      const canvas = {
        width: 1200,
        height: 1200,
        ox: 0,
        oy: 0
      };
      p2.setup = () => {
        const { width, height } = canvas;
        canvas.ox = width / 2;
        canvas.oy = height / 2;
        const c = p2.createCanvas(width, height);
        c.style("max-width", "100%");
        c.style("height", "auto");
        const wrapper = c.elt.parentElement;
        params = initializeControls(wrapper);
      };
      p2.draw = () => {
        const { ox, oy, width, height } = canvas;
        p2.fill(220);
        p2.rect(0, 0, width, height);
        const { gear1: gear12, gear2: gear22 } = cachedGears(params);
        if (params.play) params.incrementTheta();
        const { m, z, z2, shift, shift2, inner, theta, connection, alpha } = params;
        const [rp, rp2] = [m * z / 2, m * z2 / 2];
        const { PI, cos, tan, abs } = Math;
        const inv = (a) => tan(a) - a;
        const inverse_inv = (inva2) => {
          if (inva2 == 0) return 0;
          if (inva2 < 0) return -inverse_inv(-inva2);
          let a = inva2 > 2.4 ? Math.atan(inva2) : 1.441 * inva2 ** (1 / 3) - 0.374 * inva2;
          let a_prev = 2;
          for (let i = 0; i < 20; i++) {
            const tana = tan(a);
            a += (inva2 - tana + a) / tana ** 2;
            if (abs(a_prev - a) < 1e-15) return a;
            a_prev = a;
          }
          return a;
        };
        const s = inner ? -1 : 1;
        const a0 = m * (z + s * z2) / 2;
        const inva = tan(alpha) * (shift + s * shift2) / a0 + inv(alpha);
        const aw = inverse_inv(inva);
        const da = a0 * (cos(alpha) / cos(aw) - 1);
        if (!inner) {
          drawGear(p2, gear12, ox - rp - da / 2, oy, -theta / z, connection, 0);
          drawCircles(p2, ox - rp - da / 2, oy, params);
        } else {
          const innerR = rp + m * 5 / 2;
          const thetaI = (theta + PI) / z + PI;
          drawGear(p2, gear12, ox + rp - s * da / 2, oy, thetaI, connection, innerR);
          drawCircles(p2, ox + rp - s * da / 2, oy, params);
        }
        const theta2 = (theta + PI) / z2 + PI;
        const params2 = { ...params, z: z2, shift: shift2 };
        drawGear(p2, gear22, ox + rp2 + s * da / 2, oy, theta2, connection);
        drawCircles(p2, ox + rp2 + s * da / 2, oy, params2);
      };
      const drawGear = (p3, gear, ox, oy, dt = 0, connection = false, innerR = 0) => {
        p3.strokeWeight(1);
        p3.stroke("black");
        p3.fill("rgba(255, 255, 255, 0.5)");
        if (innerR) {
          p3.circle(ox, oy, innerR * 2);
          p3.fill(220);
        }
        p3.beginShape();
        gear.curve.forEach((v) => {
          p3.vertex(ox + v[0] * Math.cos(v[1] + dt), oy + v[0] * Math.sin(v[1] + dt));
        });
        p3.endShape("close");
        if (connection) {
          gear.connections.forEach((v) => {
            p3.circle(ox + v[0] * Math.cos(v[1] + dt), oy + v[0] * Math.sin(v[1] + dt), 2);
          });
        }
      };
      const drawCircles = (p3, ox, oy, params2) => {
        const { df, db, dp, dk, m, z, mk, mf, alpha, shift } = params2;
        p3.strokeWeight(1);
        p3.stroke("Blue");
        p3.noFill();
        p3.drawingContext.setLineDash([2, 6]);
        if (df) p3.circle(ox, oy, (m * z / 2 - mf + shift) * 2);
        if (db) p3.circle(ox, oy, m * z * Math.cos(alpha));
        if (dp) p3.circle(ox, oy, m * z);
        if (dp && shift) p3.circle(ox, oy, m * z + shift * 2);
        if (dk) p3.circle(ox, oy, (m * z / 2 + mk + shift) * 2);
        p3.drawingContext.setLineDash([]);
      };
    };
    sketch(p);

内歯車について:
- インボリュート曲線部分のみで歯形としているため歯数の少ない場合に歯先円まで届かない(実質的な歯先円が大きくなる)
- フィレットは付けていない
- 転位の値によって表示されない

* 歯車の形状 [#b993ee5e]

参考: https://qiita.com/chromia/items/629311346c80dfd0eac7

&katex();
#ref(gear.png,right,around);

** 形状を指定するパラメータ [#b8a12317]

歯車は歯のピッチと歯の枚数、そして、歯の肉厚によって形が決まる。

- モジュール (歯のピッチを $\pi$ で割った値) $m$
- 歯の数 $z$
- 圧力角(歯の肉厚を決める) $\alpha$

歯のピッチは円周の $1/z$ だから歯車の直径 $d$ として $\pi d/z$

ここから $mz=d$ の関係が出る

** 圧力角 [#v226ff77]

- 圧力角が小さいと歯と歯の滑りが小さくなり摩擦が小さくなるが、~
歯車が小さいときに接触時間が短くなる。
- 圧力角が大きいと歯と歯の滑りが大きくなり摩擦が大きくなるが、~
歯車が小さいときも接触時間は長く取れる。

圧力角は 20°とするのが一般的だそう。

** 基準円とその他の円 [#u9222a3a]

上の $d$ は「基準円」の直径であり、他の直径と区別するため以下では $d_p$ と書く。

- かみ合う2つの歯車は基準円同士がちょうど接する(転位なしの場合)
- 基準円の上で歯の厚さはピッチの半分になり、残りの半分が隙間になる
- 基準円の上で歯の外形線と半径とがなす角が圧力角 $\alpha$ である

#ref(gear2.png,right,around,50%);

その他に重要な直径として以下がある。

|歯先円(歯の先端)                |$d_k=d_p+2m$        |
|基礎円(歯の外形が半径と平行になる)|$d_b=d_p\cos \alpha$|
|歯底円(歯間の底)                |$d_f=d_p-2.5m$      |

~

- 基礎円より上がインボリュート曲線になり相手の歯車と接する
- 基礎円より下は相手の歯車と接することはない
- 歯先円は、基準円から外側の歯先の長さ(歯末のたけ) $(d_k-d_p)/2=m$ を決める
- 歯底円は、相手の歯先から歯底までの間の間隙の深さ(頂げき) $(d_p-d_k)/2=0.25m$ を決める

** 基礎円の外側(歯末)の歯形 [#r36a2295]

基礎円より外側の歯の形状はインボリュート曲線とすることが多いらしい。

この曲線は外形線が基礎円と交わる点を基準とした極座標 $r,\theta$ を用いて、
$$
\theta=\tan a-a\ \ \text{ただし}\ \ a(r)=\cos^{-1}\frac{r_b}r
$$
と表される。ここに現れる
$$
\mathrm{inv}\,a=\tan a-a
$$
はインボリュート関数と呼ばれる。

$\theta$ の基準を歯先の中心に取るなら、

$$
\begin{aligned}
\theta(r)=\mathrm{inv}\, a(r) - \mathrm{inv}\, a(r_p) - \frac14\frac{2\pi} z
\end{aligned}
$$

とする。

例えば以下のようにして描ける。

 LANG: js
   const involute_theta0 =
     Math.tan(Math.acos(rb / rp)) - Math.acos(rb / rp) + Math.PI / z / 2;
   const involute_r2theta = (r) => {
     return -(Math.tan(Math.acos(rb / r)) - Math.acos(rb / r) - involute_theta0);
   };
 
   // インボルート曲線
   const c_involute = [];
   const involute_n = 30; // 何分割するか
   let involute_rs = rk;               // 書き始めの r
   let involute_re = Math.max(rb, rf); // 書き終わりの r
   // 圧力角が大きいときは rk まで書くと書きすぎになる
   if (involute_r2theta(involute_rs) < 0) {
     for (
       ;
       involute_r2theta(involute_rs) < 0;
       involute_rs -= (involute_rs - involute_re) / 100
     ) {
       // do nothing
     }
     // involute_r2theta(r) = 0 となる点を求める
     involute_rs = bisect_root(
       involute_rs,
       involute_rs + (involute_rs - involute_re) / 100,
       involute_r2theta
     );
     c_involute.push([involute_rs, 0]); // 最初の点
   }
   // 残りの点を求める
   for (let i = c_involute.length; i <= involute_n; i++) {
     let r = involute_rs + ((involute_re - involute_rs) / involute_n) * i;
     c_involute.push([r, involute_r2theta(r)]);
   }

 LANG: js
 // 二分法を使い a と b の間で f(x) = 0 の点を探す
 // epsilon は x 方向の精度
 function bisect_root(a, b, f, epsilon = 1e-6) {
   const fa = f(a),
   fb = f(b);
   if (fa * fb > 0) return Math.NaN;
 
   const flag = fa < 0;
   for (; Math.abs(a - b) > epsilon; ) {
     let m = (a + b) / 2;
     if (flag ? f(m) < 0 : f(m) > 0) {
       a = m;
     } else {
       b = m;
     }
   }
   return a;
 }

** 基準円の内側(歯元)の歯形 [#qb4d3450]

#ref(gear3.png,right,around);

基準円より下は相手の歯車と接する部分ではないため、特にこだわらないのであればまっすぐ中心へ下ろしてしまったり、強度を増すために適当な半径のフィレットを付けたりするだけでも構わない。

現実の歯車はラックと同じ形状のホブと呼ばれる切削具で切り出されるのだけれど、その際の歯先移動の包絡線により自動的にフィレット付きの歯元歯形が得られる。この波形は次項で説明するトロコイド曲線になる。

このページの挿入図は初期に適当なフィレットを付けて描いたものであるのに対して、冒頭のインタラクティブに動くものでは正しいトロコイド曲線が使われている。

** 歯数の少ない歯車では [#r3c3922b]

歯数が少ない歯車、圧力角が20度の場合には歯数が 18 未満になると、相手の歯数が大きいときに基準円から下を中心に向かって真っすぐ下ろしたのでは相手の歯先と干渉してしまう。

相手の歯数が大きいほど干渉は大きくなるため、相手の歯数が無限大でも大丈夫な形で「逃げ」を作るのが普通。というか、歯数無限大の歯車はラックであるから、ラック形状と同等の切削具で切られる歯車には自動的にこの逃げが作成される。ラック&ピニオンの状況を考えると、ラックの歯の角度は圧力角 $\alpha$ そのもので、$r_p$ に相当する高さではやはり歯の幅はピッチの半分になる。高さは基準円から上が $m$、下が $1.25m$ である。
#ref(gear4.png,right,around);
ラックの歯先が通る直線はピニオンの中心から $r_p-m$ の距離にあり、ラックの移動速度はピニオンの半径 $r_p$ の点の円周の速さと同じである。

下図の時点で頂点の $y$ 座標は $\pi m/4-m \tan \alpha$ である。このあとピニオンが $\omega t$ 回転したとき $y$ 座標は $\pi m/4-m \tan \alpha-r_p\omega t$ で $x$ 座標は $x=r_p-m$ で変わらないから、この点をピニオンの中心から見た極座標で表した曲座標 $\theta$ と動径 $r$ は

$$
r=\sqrt{(r_p-m)^2+(\pi m/4-m \tan \alpha-r_p\omega t)^2}
$$
$$
\theta=\text{atan}\,\frac{\pi m/4-m \tan \alpha-r_p\omega t}{r_p-m}
$$
このときピニオン自体 $\omega t$ 回転しているのでピニオンと共に回転する座標では
$$
\theta'=\theta-\omega t
$$
となる。この曲線はトロコイド曲線と呼ばれるそうだ。

歯先の中心を基準とした極座標に直すには、
$$
\theta''=-(\theta'-2\pi/z/4)+2\pi/z/4
$$
とする。

これがインボルート曲線より内側に入る部分を取り除いて、適当なフィレットを付けて求めたのが挿入図の歯車の形状である。

実際の歯車を切削する際には、相手方のラック形状のホブの先端を頂隙分だけフィレット付きで延長しておくことにより、歯元に自然な形でなめらかなフィレットが付く。冒頭のインタラクティブな図形描画で指定する「フィレット」はこのホブ先端に付けるフィレットの径のことで、描かれる歯車の形状に必ずしもこの系の円弧が現れるわけではない。

 LANG: js
   // トロコイド曲線
   // t = 0 から増やすと歯元から歯先へ描けるが
   // t が負の領域が必要になることもある
   const trochoid = (t) => {
     let y = (Math.PI * m) / 4 - m * Math.tan(a) + rp * t;
     let x = rp - m;
     let r = Math.sqrt(x * x + y * y);
     let theta = Math.atan2(y, x) - t;
     return [r, -(theta - Math.PI / z / 2) + Math.PI / z / 2];
   };

** バックラッシュ [#yc6379a2]
#ref(backlash.jpg,around,right,33%);
歯を円周方向に薄くすることで無理なくバックラッシュを実装できる。

そのためには歯を切り出すためのホブの歯の厚みを変えて、それを使って切削すればいい。

上では円周方向のバックラッシュをピッチに対するパーセンテージで指定できるようにした。

1% とすると歯面を円周方向にピッチの 0.5% だけ下げることで、両サイド合わせて 1% だけ歯の厚みが減少する。1% のバックラッシュを持つ歯車を2つ組み合わせると合計で 2% のバックラッシュが生まれることになる。

** 転位 [#yce3846a]

通常より切削具を離した状態で切削を行うと、歯数は同じで一回り大きな歯車ができあがる。
このような歯車は転位歯車(shifted gear)と呼ばれる。

転位を使うことで歯車間の距離を微調節したり、歯先の干渉を減らしたりできる。

はすば歯車では標準的な歯垂直モジュールを指定すると基準円直径が無理数になってしまうため、軸間距離も無理数になってしまう。転位を行うことで軸間距離を切りの良い数字にすると設計が楽になる。

遊星歯車で噛み合い距離をぴったり合わせるのにも転位が使われる。

転位した歯車の軸間距離を求めるには難しい計算が必要になる。
単に基準円が増減するというだけでなく、軸間距離の補正量は相手の歯数にも依存して変化する。
[[工作/歯車について勉強する3#m951ac17]] に計算機を置いた。

また転位した歯車は、組み合わせによっては干渉して回らないことがあるのでそこにも注意しないとダメみたいだ。

* 内歯車 [#k7a9b79f]
#ref(internal.jpg,around,right,33%);

基本的には通常の歯車と同じ形状になるけれど、歯元と歯先が入れ替わるので長さを調整する。

内歯車は歯先に直線領域が残ると確実に相手の歯形と干渉するので、「接続点」を表示して直線領域が生成されないよう調整する。

また相手歯車から見ると、内歯車の歯先はラックの場合に比べても内側に入ってくるため、ラック形状でアンダーカットされた小歯数の外歯車では内歯車が相手の場合に歯先と歯先がぶつかるインボルート干渉が生じてしまう。

小さな形の歯車を使いたいときには圧力角を大きくするか、転位を行うと良いとのこと。

内歯車の歯先にフィレットを付ければ歯車が回らないという問題は避けられそうに思えるけれど、かみ合い率が低下してしまうので良くないのだとか。こちらの説明が参考になった。

https://sites.google.com/view/involutegearsoft/%E3%81%8B%E3%81%BF%E5%90%88%E3%81%84%E7%8E%87%E8%A8%88%E7%AE%97?authuser=0

はすばの場合も含めた計算機を [[工作/歯車について勉強する3#a6919964]] に置いた。

* はすば歯車 [#ef891f7d]

#ref(hasuba.png,right,around,33%);

はすば歯車は歯が傾いた歯車。

歯と歯が斜めに当たるため、軸方向に力を生じてしまうけれど、強度が高くなったり静かになったりと、いろいろ性能がいいそうだ。

はすばの傾き角 $\beta$ は基準半径の仮想的な円筒を切り開いた展開図上で歯と軸方向とがなす角のこと。

軸に垂直な断面上で見える歯の厚さに比べて、歯に垂直な方向に計った歯の厚さは傾いた分だけ薄くなり、$\cos \beta$ 倍になるから、歯に直角にモジュール $m_n$ を持つはすば歯車の、軸に垂直な断面には現れ正面モジュール $m_t$ は
$$m_n=m_t\cos\beta$$
の関係にある。

ということで、歯に直角にモジュール $m_n$ を持つはすば歯車を作るには軸に直角な断面上にモジュール $m_t=m_n/\cos\beta$ の歯車形状を作り、それを中心軸に対してねじれ角 $\beta$ の分だけひねりながら押し出せばよい。

https://www.khkgears.co.jp/gear_technology/basic_guide/KHK365.html によると、このとき圧力角も
$$
\tan^{-1}\frac{\tan \alpha}{\cos\beta}
$$
の形で調整が必要らしい。

ただし、本当にこれをそのままやってしまうと歯の高さも $1/\cos\beta$ 倍になってしまうため、歯の高さを $m_n$ に合わせるため、歯末や歯元の高さを $\cos\beta$ 倍してモジュール $m_n$ の高さに戻してやる必要がある。

これは、歯の形状を生成してからつぶす、という意味ではなく、$d_k$ や $d_f$ の値を変えるということなので間違ってはいけない(当初勘違いしていた)。

* ウォーム [#zd6a89e5]

ウォームはねじはぐるまのこと。~
ウォームホイールはウォームとかみ合う歯車のこと。~
ウォームとウォームホイールの中央を通る断面はラック&ピニオンとほぼ同じ形になる。

円筒を切り開いて考えれば、基準円直径 $Dp$、歯直角モジュール $m$、条数 $n$ のウォームのねじれ角 $\beta$ は、

$$
\beta = \sin^{-1}\frac{n m}{D_p}
$$

で求められ、正面モジュールは、

$$
m_n=\frac{m}{\cos\beta}
$$

となる。

このねじれ角を持つはすばラックの歯形を軸を中心に巻き付けたものがウォームになる。
* ウォームホイール [#i73fa961]

ウォームホイールはウォームとかみ合う、一見すると普通のはすば歯車に見えるやつのこと。

実際、中央を通る断面だけ見れば普通のはすば歯車と同じ形をしている。

2点異なるのは、
+ 中央以外の部分ではウォームが回転しながら進むのを反映して、同様に移動する切削具に切り取られた形になる
+ 内歯車のトロコイド干渉と同様にウォームホイールの歯先がウォームの歯溝から抜ける際に歯先と歯先がぶつかってしまうため、ウォームホイールの歯先をトリミングする必要がある

2つ目は計算で求めるのは難しそうなので、 [[工作/Fusion360歯車スクリプト/ウォーム&ウォームホイール]] で紹介したように、

- CAD ソフトの中で歯切りと同等の演算処理を行うことで再現する
- 本来より大きなウォームを仮定した形状を少し変形させて近似する

というのが現実的っぽいです。


* かさ歯車 [#n81025a3]

歯形を円筒に張り付けておいて、原点を中心にスケールしながら複製すれば作れるみたい?

ねじるときは基準面上での半径と回転軸からの半径との変換がややこしい。。。


* コメント・質問 [#p50ba207]

#article_kcaptcha

Counter: 406 (from 2010/06/03), today: 5, yesterday: 5