class Gear {
constructor({parent, radius, nteeth, scale=1, pressureAngleDeg=20, clearanceFraction=0.25,
xc=0, yc=0, spinDeg=0, npath=16, drawCircles=false}={}) {
const {PI, sin, cos, sqrt, atan2, min, max} = Math;
Object.assign(this, {
parent, radius, nteeth, scale, xc, yc, spinDeg, pressureAngleDeg, clearanceFraction,
npath, drawCircles
});
this.module = 2 * radius / this.nteeth;
this.pitchRadius = this.module * this.nteeth / 2;
this.baseRadius = this.pitchRadius * cos(PI / 180 * pressureAngleDeg);
this.addendumRadius = this.pitchRadius + this.module;
this.dedendumRadius = this.pitchRadius - (1 + clearanceFraction) * this.module;
this.meshList = [ ];
const r2t = (r) => sqrt((r / this.baseRadius) ** 2 - 1);
const tmin = r2t(max(this.baseRadius, this.dedendumRadius));
const tmax = r2t(this.addendumRadius);
const t2xy = (t) => {
const C = cos(t), S = sin(t);
return [ this.baseRadius * (C + t * S), this.baseRadius * (S - t * C) ];
};
const xinv = [ ], yinv = [ ];
for(let i = 0; i < npath; ++i) {
const t = tmin + i / (npath - 1) * (tmax - tmin);
const [x,y] = t2xy(t);
xinv.push(scale * x);
yinv.push(scale * y);
}
if(this.baseRadius > this.dedendumRadius) {
const ratio = this.dedendumRadius / this.baseRadius;
xinv.unshift(ratio * xinv[0]);
yinv.unshift(ratio * yinv[0]);
npath++;
}
// Locate the other side of the tooth.
const tpitch = r2t(this.pitchRadius);
const [xpitch, ypitch] = t2xy(tpitch);
this.phiPitch = atan2(ypitch, xpitch);
const phiOffset = PI / nteeth + 2 * this.phiPitch;
// Define a function to draw the gear tooth profile.
function draw(c) {
let X0, Y0;
for(let tooth = 0; tooth < nteeth; ++tooth) {
let phi = 2 * PI * tooth / nteeth;
let C = cos(phi), S = sin(phi);
for(let i = 0; i < npath; ++i) {
const X = C * xinv[i] - S * yinv[i];
const Y = S * xinv[i] + C * yinv[i];
if(i == 0 && tooth == 0) {
X0 = X;
Y0 = Y;
c.moveTo(X, Y);
}
else c.lineTo(X, Y);
}
phi += phiOffset;
C = cos(phi), S = sin(phi);
for(let i = npath - 1; i >= 0; --i) {
const X = C * xinv[i] + S * yinv[i];
const Y = S * xinv[i] - C * yinv[i];
c.lineTo(X, Y);
}
}
c.lineTo(X0, Y0);
return c;
}
// Create an offset coordinate system centered at (xc,yc).
this.offset = this.parent.append("g")
.attr("transform", `translate(${this.scale * this.xc} ${this.scale * this.yc}) rotate(${spinDeg})`);
// Create a node for gear rotation and where styles can be applied.
this.node = this.offset.append("g")
.attr("transform", "rotate(0)");
// Draw the gear profile.
const teeth = this.node.append("g");
teeth.append("path")
.attr("d", draw(d3.path()));
if(this.drawCircles) {
// Draw the pitch circle
this.node.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", scale * this.pitchRadius)
.attr("fill", "none")
.attr("stroke-dasharray", "3 3");
// Draw the base circle
this.node.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", scale * this.baseRadius)
.attr("fill", "none")
.attr("stroke-dasharray", "2 4");
}
// Draw the axle shaft
const shaftSize = 0.5 * this.module * PI;
this.node.append("rect")
.attr("x", -0.5 * scale * shaftSize)
.attr("y", -0.5 * scale * shaftSize)
.attr("width", scale * shaftSize)
.attr("height", scale * shaftSize)
.attr("fill", "none");
}
// Set this gear's angle in degrees counter-clockwise relative to the initial spinDeg.
setAngle(angleDeg) {
this.node.attr("transform", `rotate(${angleDeg})`);
for(const {gear, factor} of this.meshList) {
gear.setAngle(angleDeg * factor);
}
}
// Set this gear's roll angle relative to another gear.
setRoll(gear, rollDeg) {
const R = this.module * (gear.nteeth + this.nteeth) / 2;
const phi = Math.PI / 180 * rollDeg;
this.xc = gear.xc + R * Math.cos(phi);
this.yc = gear.yc + R * Math.sin(phi);
const ratio = gear.nteeth / this.nteeth;
this.spinDeg = rollDeg * (1 + ratio) - (gear.spinDeg + 180 / Math.PI * gear.phiPitch) * ratio;
this.spinDeg += 180 - 180 / Math.PI * this.phiPitch;
this.offset.attr(
"transform",
`translate(${this.scale * this.xc} ${this.scale * this.yc}) rotate(${this.spinDeg})`);
}
// Create and return a new Gear that meshes with this gear.
mesh({nteeth, rollDeg=0}={}) {
const gear = new Gear({
parent:this.parent, scale:this.scale, nteeth, radius: this.module * nteeth / 2,
pressureAngleDeg: this.pressureAngleDeg, clearanceFraction: this.clearanceFraction,
npath: this.npath, drawCircles: this.drawCircles
});
gear.setRoll(this, rollDeg);
this.meshList.push({ gear, factor: -this.nteeth / gear.nteeth });
return gear;
}
// Create and return a new Gear that shares an axle with this gear.
coaxial({nteeth, radius, spinDeg=0}={}) {
const gear = new Gear({
parent:this.parent, scale:this.scale, nteeth, radius: radius || this.module * nteeth / 2,
pressureAngleDeg: this.pressureAngleDeg, clearanceFraction: this.clearanceFraction,
meshed: false, xc: this.xc, yc: this.yc, spinDeg: this.spinDeg + spinDeg,
npath: this.npath, drawCircles: this.drawCircles
});
this.meshList.push({ gear, factor:1 });
return gear;
}
}