@dromney/gear-gen
Version:
A set of types and pattern generators for working with front-end gears
449 lines (448 loc) • 17.3 kB
JavaScript
import { createSVGCircle, getPrecisionPolarTextDXFLines, getPrecisionPolarTextSVG, makeText } from "./svg.js";
import { deg2rad, downloadContent, fix2, fix6, linearToPolar, polarToLinear, rad2deg } from "./utils.js";
import DXFDrawing, { dxfp1, dxfp2 } from "./dxf.js";
const gearDefaults = {
D: 2,
N: 12,
PADeg: 27,
scale: 100,
jointAngleDeg: -60,
axleJoint: false,
internal: false,
layer: 1,
holeSize: 0.25,
crossSize: 8,
internalThicknessRatio: 0.5
};
const fiveCharID = () => (Math.random() + 1).toString(36).toUpperCase().substring(7);
export class Gear {
constructor(props) {
var _a;
this.holeSize = gearDefaults.holeSize;
this.crossSize = gearDefaults.crossSize;
this._N = gearDefaults.N;
this._D = gearDefaults.D;
this._PA = deg2rad(gearDefaults.PADeg);
this._jointAngle = deg2rad(gearDefaults.jointAngleDeg);
this._axleJoint = gearDefaults.axleJoint;
this._internal = gearDefaults.internal;
this._internalThickness = 0;
this._scale = 100;
this.toothModule = 2;
this.toothGap = 0.3;
this.baseAngle = 0;
this.svg = "";
this.dxf = "";
this.rot = 0;
this.x = 0;
this.y = 0;
this.svgOffsetX = 0;
this.svgOffsetY = 0;
const { id, parent, N, D, P, PADeg, jointAngleDeg = gearDefaults.jointAngleDeg, internal, axleJoint = gearDefaults.axleJoint, layer = gearDefaults.layer, internalThickness, scale } = props;
this.id = id || fiveCharID();
this._parent = parent;
this.layer = layer;
this._jointAngle = deg2rad(jointAngleDeg);
if (((_a = this.parent) === null || _a === void 0 ? void 0 : _a.internal) && internal === true)
throw new Error("Two internal gears cannot mesh");
this._internal = !!internal;
if (parent) {
if (scale)
throw new Error("Cannot set scale on child gear");
this._scale = parent._scale;
}
else {
this._scale = scale || gearDefaults.scale;
}
if (PADeg && (PADeg < 15 || PADeg > 35))
throw new Error("Pressure angle must be between 15 and 35 degrees");
if (N && D && P)
throw new Error("Too many params provided - N, D, P");
this._axleJoint = axleJoint;
if (axleJoint) {
if (!parent)
throw new Error("Cannot set axleJoint on root gear");
if (!PADeg)
this._PA = parent._PA;
else
this._PA = deg2rad(PADeg);
if (N && P) {
this._N = N;
this._D = N / P;
}
else if (D && P) {
this._D = D;
this._N = D * P;
}
else if (N && D) {
this._N = N;
this._D = D;
}
else if (N) {
this._N = N;
this._D = N / parent.P;
}
else if (D) {
this._D = D;
this._N = D * parent.P;
}
else {
throw new Error("Must provide a parent + N/D or N/P or D/P");
}
}
else {
if (parent) {
if (PADeg)
throw new Error("Cannot set PA on non-axle-joint child gear");
this._PA = parent._PA;
if (P)
throw new Error("Cannot set diametrical pitch P on non-axle-joint child gear");
if (N && D)
throw new Error("Too many params provided - can only provide number of teeth or diameter, since P is inherited");
if (N) {
this._N = N;
this._D = N / parent.P;
}
else if (D) {
this._D = D;
this._N = D * parent.P;
}
}
else {
if (!PADeg)
this._PA = deg2rad(gearDefaults.PADeg);
else
this._PA = deg2rad(PADeg);
if (N && P) {
this._N = N;
this._D = N / P;
}
else if (D && P) {
this._D = D;
this._N = D * P;
}
else if (N && D) {
this._N = N;
this._D = D;
}
else {
throw new Error("If no parent is provided, must provide N+D, or N+P or D+P");
}
}
}
this._internalThickness = internalThickness || this.D * gearDefaults.internalThicknessRatio;
this.updateStatic();
}
get N() { return this._N; }
set N(N) {
this._N = N;
this.updateStatic();
}
get D() { return this._D; }
set D(D) {
this._D = D;
this.updateStatic();
}
get PA() { return this._PA; }
set PA(PA) {
this._PA = PA;
this.updateStatic();
}
get jointAngle() { return this._jointAngle; }
get jointAngleDeg() { return rad2deg(this._jointAngle); }
set jointAngle(jointAngle) {
this._jointAngle = jointAngle;
this.updateStatic();
}
get axleJoint() { return this._axleJoint; }
set axleJoint(axleJoint) {
this._axleJoint = axleJoint;
this.updateStatic();
}
get internal() { return this._internal; }
set internal(internal) {
this._internal = internal;
this.updateStatic();
}
get internalThickness() {
if (!this.internal)
return 0;
return this._internalThickness;
}
set internalThickness(thickness) {
this._internalThickness = thickness;
this.updateStatic();
}
get parent() { return this._parent; }
set parent(parent) {
this._parent = parent;
this.updateStatic();
}
get scale() { return this._scale; }
set scale(scale) {
this._scale = scale;
this.updateStatic();
}
setND(N, D) {
this._N = N;
this._D = D;
this.updateStatic();
}
setNP(N, P) {
this._N = N;
this._D = N / P;
this.updateStatic();
}
setDP(D, P) {
this._N = D / P;
this._D = D;
this.updateStatic();
}
get P() { return this.N / this.D; }
get gearAngleDeg() { return 360 / this.N; }
get dOuter() { return (this.N + (this.internal ? this.toothModule + this.toothGap : this.toothModule)) / this.P; }
get dInner() { return (this.N - (this.internal ? this.toothModule : this.toothModule + this.toothGap)) / this.P; }
get rOuter() { return this.dOuter / 2; }
get rInner() { return this.dInner / 2; }
get dBase() { return this.D * Math.cos(this.PA); }
get rBase() { return this.dBase / 2; }
get r() { return this.D / 2; }
get rScaled() { return this.r * this.scale; }
get size() {
return Math.ceil(this.scale * (this.dOuter + this.internalThickness));
}
updateBaseAngle() {
let ac = 0;
const pt = (r, a) => ({ r, a });
for (var i = 1, step = .1, first = true, fix = 0; i < 100; i += step) {
var bpl = polarToLinear(pt(this.rBase, -i));
let len = deg2rad(i * this.rBase);
let opl = polarToLinear(pt(len, -i + 90));
let np = linearToPolar({ x: bpl.x + opl.x, y: bpl.y + opl.y });
if (np.r >= this.rInner) {
if (first) {
first = false;
step = (2 / this.N) * 10;
}
if (np.r < this.r)
ac = np.a;
if (np.r > this.rOuter) {
if (++fix < 10) {
i -= step;
step /= 2;
continue;
}
np.r = this.rOuter;
break;
}
}
}
this.baseAngle = ac;
}
get pointsPolar() {
let ac = 0;
const pt = (r, a) => ({ r, a });
const pts = [];
const firstPoint = pt(this.rInner, 0);
pts.push(firstPoint);
for (var i = 1, step = .1, first = true, fix = 0; i < 100; i += step) {
var bpl = polarToLinear(pt(this.rBase, -i));
let len = deg2rad(i * this.rBase);
let opl = polarToLinear(pt(len, -i + 90));
let np = linearToPolar({ x: bpl.x + opl.x, y: bpl.y + opl.y });
if (np.r >= this.rInner) {
if (first) {
first = false;
step = (2 / this.N) * 10;
}
if (np.r < this.r)
ac = np.a;
if (np.r > this.rOuter) {
if (++fix < 10) {
i -= step;
step /= 2;
continue;
}
np.r = this.rOuter;
pts.push(np);
break;
}
pts.push(np);
}
}
let mirrorAngle = this.gearAngleDeg / 2 + 2 * ac;
let firstPointAngle = (this.gearAngleDeg - mirrorAngle) > 0 ? 0 : -(this.gearAngleDeg - mirrorAngle) / 2;
pts[0] = pt(this.rInner, firstPointAngle);
while (pts[pts.length - 1].a > mirrorAngle / 2) {
pts.pop();
}
const firstHalf = [...pts];
const firstTooth = [...firstHalf, ...firstHalf.map(p => (pt(p.r, mirrorAngle - p.a))).reverse()];
const allTeethSections = Array.from(Array(this.N).keys()).map(toothN => firstTooth.map(p => (pt(p.r, p.a + this.gearAngleDeg * toothN))));
const allTeeth = Array.prototype.concat.apply([], allTeethSections);
allTeeth.push(firstPoint);
return allTeeth;
}
get pointsLinear() {
return this.pointsPolar.map(polarToLinear);
}
get firstToothMarker() {
const size = Math.PI / this.P / 8;
const position = polarToLinear({
r: this.D / 2,
a: this.baseAngle + (this.internal ? -1 : 1) * this.gearAngleDeg / 4
});
return Object.assign({ size }, position);
}
get description() {
return "*" + this.id + " N=" + this.N + " Pitch D=" + fix2(this.D) + " P=" + fix2(this.P) + " PA=" + fix2(this.PA);
}
get descriptionTextData() {
const maxHeight = 0.03;
const data = makeText(this.description, 2);
let height = this.internal ? 1 : this.rInner / 42;
height = height > maxHeight ? maxHeight : height;
return {
data,
height,
B: this.internal ? (this.dOuter / 2) + height * 9 : this.rInner - height * 10
};
}
updateSVG() {
const s = this.size;
let svg = `<svg version="1.2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="${s}px" height="${s}px" viewBox="${-s / 2} ${-s / 2} ${s} ${s}" overflow="scroll">`;
const pts = this.pointsLinear;
svg += `<path class="gear-profile" id="gear-${this.id}" fill="#ddd" stroke="#444" stroke-width="1" stroke-miterlimit="10"
d="M${pts.map((pt) => fix6(pt.x * this.scale) + ',' + fix6(pt.y * this.scale)).join(' ')}${this.internal ? createSVGCircle(this.size / 2) : ''}z"
/>`;
const defaultStyle = 'stroke="#444" stroke-width="0.5" stroke-miterlimit="10"';
if (!this.axleJoint)
svg += `<g>
<polyline class="gear-cross" ${defaultStyle} points="${this.crossSize},0 -${this.crossSize},0"/>
<polyline class="gear-cross" ${defaultStyle} points="0,${this.crossSize} 0,-${this.crossSize}"/>
</g>`;
const ft = this.firstToothMarker;
svg += `<circle class="gear-first-tooth-marker" fill="#c00" stroke="none" stroke-miterlimit="10" cx="${fix6(ft.x * this.scale)}" cy="${fix6(ft.y * this.scale)}" r="${fix6(ft.size * this.scale)}"/>`;
const td = this.descriptionTextData;
const textSVG = getPrecisionPolarTextSVG(td.data, td.B, td.height, this.scale);
svg += textSVG;
if (this.holeSize) {
svg += `<circle class="gear-hole" fill="none" stroke="#000" r="${fix2((this.holeSize / 2) * this.scale)}"/>`;
}
svg += `<g class="gear-guides" opacity="0.3">
<circle class="gear-guide-pitch" fill="none" stroke="#f00" stroke-miterlimit="10" r="${fix2((this.D / 2) * this.scale)}"/>
<circle class="gear-guide-outer" fill="none" stroke="#aaa" stroke-miterlimit="10" stroke-dasharray="2,2" r="${fix2((this.dOuter / 2) * this.scale)}"/>
<circle class="gear-guide-base" fill="none" stroke="#00f" stroke-miterlimit="10" stroke-dasharray="2,2" r="${fix2((this.dBase / 2) * this.scale)}"/>
</g>`;
svg += '</svg>';
this.svg = svg;
}
updateDXF() {
const DXF = new DXFDrawing();
DXF.addLayer("gear", DXF.ACI.WHITE, "CONTINUOUS");
DXF.addLayer("gear_texts", DXF.ACI.WHITE, "CONTINUOUS");
DXF.addLayer("gear_guides", DXF.ACI.BLUE, "CONTINUOUS");
DXF.setActiveLayer("gear");
const dsc = 1;
const pts = this.pointsLinear.map((pt) => [fix6(pt.x * dsc), -fix6(pt.y * dsc)]);
DXF.drawPolyLine(pts);
DXF.setActiveLayer("gear_guides");
DXF.drawLine(.1 * dsc, 0, -.1 * dsc, 0).drawLine(0, .1 * dsc, 0, -.1 * dsc);
const ft = this.firstToothMarker;
DXF.drawCircle(fix6(ft.x * dsc), -fix6(ft.y * dsc), fix6(ft.size * dsc));
DXF.setActiveLayer("gear_texts");
const td = this.descriptionTextData;
const textLines = getPrecisionPolarTextDXFLines(td.data, td.B, td.height, dsc);
textLines.forEach(line => DXF.drawPolyLine(line, false));
DXF.setActiveLayer("gear_guides");
DXF.drawCircle(0, 0, fix6((this.D / 2) * dsc));
DXF.drawCircle(0, 0, fix6((this.dOuter / 2) * dsc));
DXF.drawCircle(0, 0, fix6((this.dBase / 2) * dsc));
this.dxf = DXF.toDxfString();
}
get isInternalLink() {
return this.parent ? (this.internal && !this.parent.internal || !this.internal && this.parent.internal) : false;
}
get ratio() {
if (!this.parent)
return 1;
return this.parent.N / this.N;
}
getRot(globalRot) {
const update = (rot) => {
this.rot = rot;
return rot;
};
const jA = this.jointAngleDeg;
if (!this.parent)
return update(globalRot - jA);
if (this.axleJoint)
return update(this.parent.rot - jA);
const ratioAdjustment = this.ratio * (this.parent.rot + jA);
if (this.isInternalLink)
return update(((this.N + this.parent.N) % 2 ? (180 / this.N) : 0) + ratioAdjustment - jA);
return update(180 - ratioAdjustment - jA);
}
getRotSafe(globalRot) {
const jA = this.jointAngleDeg;
if (!this.parent)
return globalRot - jA;
if (this.axleJoint)
return this.parent.getRot(globalRot) - jA;
const ratioAdjustment = this.ratio * (this.parent.getRot(globalRot) + jA);
if (this.isInternalLink)
return ((this.N + this.parent.N) % 2 ? (180 / this.N) : 0) + ratioAdjustment - jA;
return 180 - ratioAdjustment - jA;
}
get defaultPos() {
return this.size * 0.5;
}
get offsetX() {
if (!this.parent || this.axleJoint)
return 0;
if (this.isInternalLink)
return Math.cos(this.jointAngle) * (this.rScaled - this.parent.rScaled);
return Math.cos(this.jointAngle) * (this.parent.rScaled + this.rScaled);
}
get offsetY() {
if (!this.parent || this.axleJoint)
return 0;
if (this.isInternalLink)
return -Math.sin(this.jointAngle) * (this.rScaled - this.parent.rScaled);
return -Math.sin(this.jointAngle) * (this.parent.rScaled + this.rScaled);
}
updatePositions() {
if (!this.parent) {
this.x = this.defaultPos;
this.y = this.defaultPos;
}
else {
this.x = this.parent.x + this.offsetX;
this.y = this.parent.y + this.offsetY;
}
this.svgOffsetX = fix2(this.x - this.size / 2);
this.svgOffsetY = fix2(this.y - this.size / 2);
}
updateStatic() {
this.updateBaseAngle();
this.updatePositions();
this.updateSVG();
this.updateDXF();
}
get totalRatio() {
if (!this.parent)
return this.ratio;
if (this.axleJoint)
return this.parent.totalRatio;
return this.parent.totalRatio * this.ratio;
}
get fileName() {
return `gear N${this.N} D${fix2(this.D)} P${fix2(this.P)} PA${fix2(this.PA)} @${fix2(this.scale)}`;
}
downloadSVG() {
downloadContent(this.svg, this.fileName + ".svg");
}
downloadDXF() {
downloadContent(dxfp1 + this.dxf + dxfp2, this.fileName + ".dxf");
}
}