Gear = {
function _Gear(settings) {
const {tooth_shape = "involute",
hole_shape = "",
tooth_count, pressure_angle = 20} = settings;
const tw = .5;
const tooth_angle = 2 * Math.PI / tooth_count;
const radius = tooth_count * 0.5;
Object.assign(this,
settings,
{pitch: tooth_angle, tooth_count:tooth_count, tooth_width: tw});
if (tooth_shape == "lantern") {
if (tooth_count < 4) throw RangeError("must have at least 4 teeth");
const l_radius = 0.25 * Math.PI;
const l_c = r_th_to_p(radius, -tooth_angle / 4);
const dd = 2.2 * l_radius;
const N = 50;
this.tooth_profile = [];
{
const N2 = 5;
const r = radius - dd;
const gap = Math.PI / N * l_radius / r;
const th_max = -tooth_angle / 4 + gap;
for (var i = -N2; i <= N2; ++i) {
this.tooth_profile.push(r_th_to_p(r, i / N2 * th_max));
}
}
{
for (var i = 0.5; i < N/2; ++i) {
var p1 = r_th_to_p(l_radius, i / N * 2*Math.PI + tooth_angle / 4);
this.tooth_profile.push({x: l_c.x - p1.x, y: l_c.y + p1.y});
}
}
Object.assign(this, {ad:l_radius, dd:dd,
undercut_radius: radius - l_radius});
}
else if (tooth_shape == "lantern-cycloid") {
if (tooth_count < 4) throw RangeError("must have at least 4 teeth");
const l_radius = 0.25 * Math.PI;
const dd = (settings.cycloid_round_bottom ? 1.05 : 1.2) * l_radius;
this.tooth_profile = [];
{
const N = 30;
const r1 = radius - dd + l_radius;
const l_c = r_th_to_p(r1, tooth_angle / 4);
const th_max = Math.acos(0.5 * l_radius / r1);
if (settings.cycloid_round_bottom) {
for (var i = .5; i < N; ++i) {
var p1 = r_th_to_p(l_radius, -i/N * th_max - tooth_angle / 4);
this.tooth_profile.push({x: l_c.x - p1.x, y: l_c.y + p1.y});
}
}
else {
var p1 = r_th_to_p(l_radius / Math.cos(0.5 * th_max), -0.5 * th_max - tooth_angle / 4);
this.tooth_profile.push({x: l_c.x - p1.x, y: l_c.y + p1.y});
}
}
const r_other = settings.opposite_tooth_count / tooth_count * radius;
const lc = make_lantern_cycloid(radius, r_other, l_radius, tooth_angle, -Math.tan(tooth_angle / 4))
lc.points.forEach(p => this.tooth_profile.push(p));
Object.assign(this, {ad:2 * l_radius, dd:dd,
undercut_radius: radius, loa:lc.loa});
}
else if (tooth_shape == "cycloid") {
if (tooth_count < 2) throw RangeError("must have at least 4 teeth");
const dd = Math.min(1.40, radius * .85);
const ad = 1.33;
const param1 = 2 - (pressure_angle - 5) / 15;
var cycloid = make_cycloid_tooth(radius, param1, dd, ad, tooth_count,
settings.opposite_tooth_count, settings.cycloid_round_bottom);
Object.assign(this, {ad:ad, dd:dd,
r_hc: cycloid.r_hc, r_ec: cycloid.r_ec, loa_end_th:cycloid.loa_end_th,
tooth_no_uc:cycloid.points_no_uc, tooth_profile: cycloid.points});
this.loa = [{x:radius, y:0}];
const r_hc = cycloid.r_hc;
const th_max = cycloid.loa_end_th;
for (var i = 0; i <= 20; ++i) {
var th = th_max * (i + 0) / 20;
var p1 = r_th_to_p(r_hc, th);
this.loa.push({x:radius - r_hc + p1.x, y:-p1.y});
}
var p1 = this.loa[this.loa.length - 1];
this.undercut_radius = Math.hypot(p1.x, p1.y);
}
else if (tooth_shape =="involute") {
if (tooth_count < 3) throw RangeError("must have at least 4 teeth");
const modify_involute = settings.modify_involute || 0;
const ad = 1.00 + modify_involute;
const dd_unm = 1.16;
const dd = dd_unm - modify_involute;
const pressure_angle_rad = Math.max(5, pressure_angle) * Math.PI/180;
const involute = make_involute(radius, dd, ad, modify_involute, tooth_angle, pressure_angle_rad);
const undercut = make_undercut_involute(
radius, pressure_angle_rad, tooth_angle, dd, ad, dd_unm, modify_involute);
const combined = min_curve(undercut, involute.points, tooth_angle * 1e-5);
this.undercut_radius = combined.first_2 && Math.hypot(combined.first_2.x, combined.first_2.y);
Object.assign(this, {ad:ad, dd:dd, base: involute.base, involute_root_angle: involute.root_angle,
tooth_profile: combined.points, tooth_no_uc: involute.points,
pressure_angle_rad: pressure_angle_rad})
this.loa = [
{x:radius, y:0},
r_th_to_p(this.base, -this.pressure_angle_rad)]
}
else {
throw new RangeError("Not a known tooth shape: ", tooth_shape);
}
}
_Gear.prototype.iterate_points = function(f, i_start = 0, i_end) {
const {tooth_count} = this;
i_end ??= tooth_count;
const tooth_angle = 2 * Math.PI / tooth_count;
const profile = this.tooth_profile;
for (var i = i_start; i < i_end; ++i) {
var m = make_rot(tooth_angle * (i - this.tooth_width / 2));
profile.forEach(p => {
var p1 = tr(m, {x: p.x, y: -p.y});
f(p1);
});
var m = make_rot(tooth_angle * (i + this.tooth_width / 2));
profile.slice().reverse().forEach(p => {
var p1 = tr(m, p);
f(p1);
});
}
}
_Gear.prototype.draw_teeth = function(ctx, x, y, zoom, range, rot_rad) {
const {tooth_count} = this;
const [i_start, i_end] = range;
const radius = tooth_count * 0.5;
ctx.save();
ctx.translate(x, y);
ctx.scale(zoom, zoom);
ctx.rotate(rot_rad);
const angle_start = (i_start - .75) * Math.PI * 2 / tooth_count;
const angle_end = (i_end - .75) * Math.PI * 2 / tooth_count;
ctx.beginPath();
var p = r_th_to_p(radius - this.dd - 2, angle_start);
ctx.moveTo(p.x, p.y);
p = r_th_to_p(radius - this.dd, angle_start);
ctx.lineTo(p.x, p.y);
this.iterate_points(p => ctx.lineTo(p.x, p.y), i_start, i_end);
p = r_th_to_p(radius - this.dd, angle_end);
ctx.lineTo(p.x, p.y);
p = r_th_to_p(radius - this.dd - 2, angle_end);
ctx.lineTo(p.x, p.y);
ctx.closePath();
const line_width = 2 / ((1 + pixel_ratio) * zoom);
ctx.lineWidth = line_width;
ctx.fillStyle = this.color;
ctx.strokeStyle = this.color;
ctx.globalAlpha = 0.2;
ctx.fill();
ctx.globalAlpha = 1;
ctx.stroke();
ctx.restore();
}
_Gear.prototype.draw = function(ctx, x, y, zoom, show, rot_rad, rot_rad_guides) {
const {tooth_count} = this;
const radius = tooth_count * 0.5;
const line_width = 2 / ((1 + pixel_ratio) * zoom);
ctx.save();
ctx.translate(x, y);
ctx.scale(zoom, zoom);
ctx.save();
ctx.rotate(rot_rad);
ctx.beginPath();
this.iterate_points(p => ctx.lineTo(p.x, p.y));
ctx.closePath();
const max_hole_r = (radius - this.dd) * .85;
switch (this.hole_shape)
{
case "": break;
case "round": draw_round_hole(ctx, max_hole_r); break;
case "square": draw_square_hole(ctx, max_hole_r); break;
case "keyed": draw_keyed_hole(ctx, max_hole_r); break;
case "cross": draw_axle_hole(ctx, max_hole_r); break;
}
ctx.lineWidth = line_width;
ctx.fillStyle = this.color;
ctx.strokeStyle = this.color;
ctx.globalAlpha = 0.2;
ctx.fill();
ctx.globalAlpha = 1;
ctx.stroke();
if (show.includes("pitch"))
{
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI, false);
ctx.lineWidth = line_width;
ctx.strokeStyle = "#000";
ctx.setLineDash([2 * line_width, 6 * line_width]);
ctx.stroke();
}
if (show.includes("axes"))
{
const x1 = radius + this.ad;
ctx.beginPath();
ctx.moveTo(0, -x1);
ctx.lineTo(0, x1);
ctx.moveTo(-x1, 0);
ctx.lineTo(x1, 0);
ctx.strokeStyle = this.color;
ctx.lineWidth = line_width * .4;
ctx.setLineDash([10 * line_width, 2 * line_width]);
ctx.stroke();
}
ctx.restore();
ctx.save();
ctx.rotate(rot_rad_guides);
if (this.base && show.includes("base"))
{
ctx.beginPath();
ctx.arc(0, 0, this.base, 0, 2 * Math.PI, false);
ctx.strokeStyle = "#095";
ctx.lineWidth = line_width * .7;
ctx.setLineDash([6 * line_width, 2 * line_width]);
ctx.stroke();
}
if (this.undercut_radius && show.includes("uc"))
{
ctx.beginPath();
ctx.arc(0, 0, this.undercut_radius, 0, 2 * Math.PI, false);
ctx.strokeStyle = "#f0f";
ctx.lineWidth = line_width * .7;
ctx.setLineDash([6 * line_width, 2 * line_width]);
ctx.stroke();
}
if (show.includes("ad_dd"))
{
ctx.beginPath();
ctx.arc(0, 0, radius - this.dd, 0, 2 * Math.PI, false);
ctx.strokeStyle = "#f80";
ctx.lineWidth = line_width * .5;
ctx.setLineDash([16 * line_width, 4 * line_width]);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, radius + this.ad, 0, 2 * Math.PI, false);
ctx.stroke();
}
if (this.loa && show.includes("loa")) {
const loa = this.loa;
ctx.beginPath();
ctx.moveTo(loa[0].x, loa[0].y);
for (var i = 1; i < loa.length; ++i) {
ctx.lineTo(loa[i].x, loa[i].y);
}
ctx.strokeStyle = "#095";
ctx.lineWidth = line_width;
ctx.setLineDash([]);
ctx.stroke();
}
ctx.restore();
ctx.restore();
}
return _Gear;
}