歯車について勉強する

更新


公開メモ

歯車について勉強するシリーズ

ちょっと思い立って歯車について勉強してみました

謝辞

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

成果物

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

こちらは Fusion360 用の自作スクリプト で生成したもの:
spur-gears2.gif double-helical.gif internal3.gif rack8.gif worm_wheel5.gif bevel2.gif

下図ではスライダーを動かすことでいろいろな歯車を描くことができます。
マウスホイールで拡大・縮小、画面のドラッグでスクロールが可能です。

歯車を設置する際の軸間距離などを求めるための計算機はこちら
工作/歯車について勉強する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: 400, 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);
      },
      set m(v) {
        controls.m.value = String(v);
        controls.m.doInput();
      },
      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/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/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/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: 800,
      height: 800,
      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);
    };
    let dragging = false;
    p2.mousePressed = (e) => {
      if (e.target.tagName !== "CANVAS") return;
      dragging = true;
      return true;
    };
    p2.mouseDragged = (e) => {
      if (!dragging && e.target.tagName !== "CANVAS") return;
      canvas.ox += p2.mouseX - p2.pmouseX;
      canvas.oy += p2.mouseY - p2.pmouseY;
      return true;
    };
    p2.mouseReleased = () => {
      dragging = false;
      return true;
    };
    p2.mouseWheel = (event) => {
      if (event.target.tagName !== "CANVAS") return;
      const delta = -event.deltaY / 200;
      params.m *= 1 + delta;
      event.preventDefault();
      return true;
    };
    let pinchS = 0;
    p2.touchStarted = (e) => {
      if (e.target.tagName !== "CANVAS") return;
      dragging = true;
      if (e.touches.length == 2) {
        pinchS = params.m / Math.sqrt(
          (e.touches[1].clientX - e.touches[0].clientX) ** 2 + (e.touches[1].clientY - e.touches[0].clientY) ** 2
        );
      }
      e.preventDefault();
      return true;
    };
    p2.touchMoved = (e) => {
      if (!dragging) return;
      if (e.touches.length == 1) {
        canvas.ox += p2.mouseX - p2.pmouseX;
        canvas.oy += p2.mouseY - p2.pmouseY;
      } else if (e.touches.length == 2) {
        params.m = pinchS * Math.sqrt(
          (e.touches[1].clientX - e.touches[0].clientX) ** 2 + (e.touches[1].clientY - e.touches[0].clientY) ** 2
        );
        canvas.ox += (p2.mouseX - p2.pmouseX) / 2;
        canvas.oy += (p2.mouseY - p2.pmouseY) / 2;
      }
      e.preventDefault();
      return true;
    };
    p2.touchEnded = (e) => {
      dragging = false;
      e.preventDefault();
      return true;
    };
    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), 3);
        });
      }
    };
    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);

内歯車について:

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

歯車の形状

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

gear.png

形状を指定するパラメータ

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

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

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

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

圧力角

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

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

基準円とその他の円

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

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

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

歯先円(歯の先端)$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$ を決める

基礎円の外側(歯末)の歯形

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

この曲線は外形線が基礎円と交わる点を基準とした極座標 $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;
}

基準円の内側(歯元)の歯形

gear3.png

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

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

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

歯数の少ない歯車では

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

相手の歯数が大きいほど干渉は大きくなるため、相手の歯数が無限大でも大丈夫な形で「逃げ」を作るのが普通。というか、歯数無限大の歯車はラックであるから、ラック形状と同等の切削具で切られる歯車には自動的にこの逃げが作成される。ラック&ピニオンの状況を考えると、ラックの歯の角度は圧力角 $\alpha$ そのもので、$r_p$ に相当する高さではやはり歯の幅はピッチの半分になる。高さは基準円から上が $m$、下が $1.25m$ である。

gear4.png

ラックの歯先が通る直線はピニオンの中心から $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];
  };

バックラッシュ

backlash.jpg

歯を円周方向に薄くすることで無理なくバックラッシュを実装できる。

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

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

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

転位

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

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

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

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

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

内歯車

internal.jpg

基本的には通常の歯車と同じ形状になる。大きな歯車の歯形をさらに大きな円盤から打ち抜いて作った「反転した歯車形状」が内歯車になるのである。反転時に歯元と歯先が入れ替わるので長さを調整するがある。

内歯車の歯形はインボリュート曲線のみで構成され、インボリュート曲線の開始点である基礎円よりも内側には歯形が定義されない。歯数の少ない内歯車では標準的な歯先円を取ると歯先円が基礎円よりも内側に入ってしまうが、そのような場合には歯先円を基礎円と同じ半径になるまで拡大して、すなわち歯先の丈をその分だけ短くしてやらないと歯形を生成できないことになる。

基礎円まで歯先の切り取られた内歯車は標準的な歯末の丈を持つものに比べるとかみ合いの得られる範囲が減っており、歯の強度や噛み合いの安定度が低下するが、それでも使用用途に合うなら使えばいいのだと考える。

相手歯車から見ると、内歯車の歯先はラックの場合に比べても内側に入ってくるため、ラック形状でアンダーカットされた小歯数の外歯車でも歯数の大きな内歯車が相手だと内歯車の歯先が相手歯車の歯元に食い込むインボリュート干渉を生じてしまう。この干渉は小歯車のフィレットを薄くすることで避けられる場合もあるようだ。

あるいは干渉を防ぐために圧力角を大きくしたり、転位を行うのも有効なのだそうだ。

これとは別に、小歯車の歯数と内歯車の歯数とが近い場合には内歯車の歯先と小歯車の歯先とがぶつかるトロコイド干渉が生じる場合がある。

はすばの場合も含めた計算機を 工作/歯車について勉強する3#a6919964 に置いた。 インボリュート干渉やトロコイド干渉の有無を計算することができる。

はすば歯車

hasuba.png

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

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

はすばの傾き角 $\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$ の値を変えるということなので間違ってはいけない(当初勘違いしていた)。

ウォーム

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

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

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

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

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

となる。

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

ウォームホイール

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

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

ただしその正確な歯形は通常のインボリュート歯車とは全く異なるものとなるため、 正しい歯形を得たければウォーム状の歯切り工具による切削と同等の演算処理を行う必要があります。

工作/歯車について勉強する5 にて詳しく考察しました。

かさ歯車

球面インボリュート関数により表される歯形を球面上に張り付けておいて、原点を中心にスケールしながら必要に応じて回転することで成形できる。

通常の平歯車の歯形が(平面)インボリュート関数で表されるのに対して、 かさ歯車の歯形は球面インボリュート関数などの(平面)インボリュート関数では表せない形になるため、 かさ歯車専用の計算が必要になる。

工作/歯車について勉強する6 にて詳しく考察した。

クラウンギア(フェースギア)

ウォームホイールと同様に、相手ピニオンギアと干渉する領域を取り除く切削加工の工程を 計算内でシミュレーションすることにより歯形を求めることになる。

複雑な歯形を正確に再現するためには数多くの計算点を取らねばならず、 計算時間が長くかかってしまう。

特にはすばの場合にはクラウンギアとピニオンギアとの交点を直接的な演算では求められないため、 各点ごとに数値的に方程式を解く必要があり、さらに計算が重くなる。

こういう複雑な計算をする用途では python は javascript に比べても数十倍遅いようで、 現状では1つの歯車の歯形を求めるのに数分を要してしまっている。

工作/歯車について勉強する7 にて詳しく考察した。

コメント・質問





Counter: 555 (from 2010/06/03), today: 6, yesterday: 13