Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Created February 18, 2024 20:54
Show Gist options
  • Save Sphinxxxx/a0718cfd9c0d182695c6870ed7b94116 to your computer and use it in GitHub Desktop.
Save Sphinxxxx/a0718cfd9c0d182695c6870ed7b94116 to your computer and use it in GitHub Desktop.
Involute gear meshing test
<script>
console.clear();
/*
(c) 2017, Vladimir Agafonkin
Simplify.js, a high-performance JS polyline simplification library
mourner.github.io/simplify-js
*/
window.simplify = (function(){"use strict";function n(n,r){var t=n[0]-r[0],u=n[1]-r[1];return t*t+u*u}function r(n,r,t){var u=r[0],i=r[1],f=t[0]-u,o=t[1]-i;if(0!==f||0!==o){var e=((n[0]-u)*f+(n[1]-i)*o)/(f*f+o*o);e>1?(u=t[0],i=t[1]):e>0&&(u+=f*e,i+=o*e)}return f=n[0]-u,o=n[1]-i,f*f+o*o}function t(r,t){for(var u,i=r[0],f=[i],o=1,e=r.length;e>o;o++)u=r[o],n(u,i)>t&&(f.push(u),i=u);return i!==u&&f.push(u),f}function u(n,t,i,f,o){for(var e,v=f,a=t+1;i>a;a++){var c=r(n[a],n[t],n[i]);c>v&&(e=a,v=c)}v>f&&(e-t>1&&u(n,t,e,f,o),o.push(n[e]),i-e>1&&u(n,e,i,f,o))}function i(n,r){var t=n.length-1,i=[n[0]];return u(n,0,t,r,i),i.push(n[t]),i}function f(n,r,u){if(n.length<=2)return n;var f=void 0!==r?r*r:1;return n=u?n:t(n,f),n=i(n,f)}return f})();
</script>
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<script src="https://unpkg.com/[email protected]/kdbush.min.js"></script>
<script src="https://unpkg.com/[email protected]/flatbush.js"></script>
<script src="https://sphinxxxx.github.io/vue-components/vue-inputs.js"></script>
<script src="https://sphinxxxx.github.io/zoomable-svg/dist/zoomable-svg.min.js"></script>
<!--
<script src="https://unpkg.com/[email protected]/dist/polygon-clipping.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/polyclip-ts/dist/polyclip-ts.umd.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/martinez.min.js"></script>
<script>
//martinez is ~10 x faster than the others ..but quite buggy
function unionAll(polys) {
//console.log('ua', polys);
const a = Date.now();
let unioned = [polys[0]];
for(let i = 1; i < polys.length; i++) {
//console.log('ua', i, polys.length);
unioned = martinez.union(unioned, [polys[i]]);
//console.log(unioned.length, unioned[0].length, unioned[0][0].length);
}
const b = Date.now();
console.log('tt-ua', b - a)
return unioned;
}
window.polyclip = {
union(...geom) {
return unionAll(geom);
}
}
//window.polyclip = window.polygonClipping;
//window.polyclip = window['polyclip-ts'];
</script>
-->
<script>
class CoordsAsValueTypes {
constructor() {
this._map = new Map();
}
get(coord) {
const [x, y] = coord,
xs = this._map;
if (!xs.has(x)) { xs.set(x, new Map()); }
const ys = xs.get(x);
if (!ys.has(y)) { ys.set(y, coord); }
return ys.get(y);
}
toArray() {
const coords = [];
for (const [x, ys] of this._map) {
for (const [y, coord] of ys) {
coords.push(coord);
}
}
return coords;
}
}
class RingToothSegMap {
constructor(polylines, innerRadius) {
this._coords = new CoordsAsValueTypes();
const rSq = innerRadius * innerRadius;
function partOfTooth(seg) {
const [[x1, y1], [x2, y2]] = seg,
hyp1 = (x1 * x1) + (y1 * y1),
hyp2 = (x2 * x2) + (y2 * y2);
return ((hyp1 >= rSq) || (hyp2 >= rSq));
}
const allSegs = polylines.flatMap(poly => this.createSegments(poly).filter(partOfTooth));
const coordsList = this._coords.toArray();
//https://github.com/mourner/kdbush
const index = new KDBush(coordsList.length);
let minY = 0;
for (const [x, y] of coordsList) {
index.add(x, y);
if (y < minY) { minY = y; }
}
index.finish();
this.segments = allSegs;
this.coordsList = coordsList;
this.coordsIndex = index;
this.minY = minY;
}
createSegments(polyline) {
if (polyline.length < 2) { return []; }
const coords = this._coords,
segs = [];
let from = coords.get(polyline[0]);
for (let i = 1; i < polyline.length; i++) {
const to = coords.get(polyline[i]);
//Flip the segment so it points in the negative X direction (towards the center of the ring):
const seg = (from[0] > to[0]) ? [from, to] : [to, from];
segs.push(seg);
from = to;
}
return segs;
}
deleteAbove(segs) {
const allSegs = this.segments,
coordsList = this.coordsList,
coordsIndex = this.coordsIndex;
//Go through each segment. Any segment above (negative Y direction) the polyline
//will not be part of the outline:
const markedForDeletion = new Set(),
minY = this.minY;
for (const [from, to] of segs/*this.createSegments(polyline)*/) {
const maxY = Math.min(from[1], to[1]);
const notOutlineCoords = coordsIndex.range(to[0], minY, from[0], maxY)
.map(i => coordsList[i]);
for (const coord of notOutlineCoords) {
if ((coord === from) || (coord === to)) { continue; }
markedForDeletion.add(coord);
}
}
this.segments = allSegs.filter(([from, to]) => !(markedForDeletion.has(from) && markedForDeletion.has(to)));
}
_createRect(seg) {
const [from, to] = seg;
const minX = to[0],
maxX = from[0];
let minY = from[1],
maxY = to[1];
if (minY > maxY) {
[minY, maxY] = [maxY, minY];
}
return [minX, minY, maxX, maxY];
}
prepareOverlaps() {
//https://github.com/mourner/flatbush
const segs = this.segments,
index = new Flatbush(segs.length);
for (const seg of segs) {
index.add(...this._createRect(seg));
}
index.finish();
this.segsIndex = index;
}
getOverlapping(seg) {
const allSegs = this.segments,
iSeg = allSegs.indexOf(seg);
//console.log('go', iSeg, allSegs.length);
function countsAsOverlap(i) {
if (i === iSeg) { return false; }
const candidate = allSegs[i];
//Connected segments don't count as overlaps:
if (candidate.some(coord => seg.includes(coord))) { return false; };
return true;
}
const overlaps = this.segsIndex
.search(...this._createRect(seg), countsAsOverlap)
.map(i => allSegs[i]);
//if (overlaps.length) { console.log('over', seg, overlaps); }
return overlaps;
}
startCoord() {
//The tooth is traced from the bottom right corner:
let maxX = 0, maxSeg;
for (const seg of this.segments) {
const [x, y] = seg[0];
if ((y > 0) && (x > maxX)) {
maxX = x;
maxSeg = seg;
//console.log('ss', maxX);
}
}
return maxSeg[0];
}
}
</script>
<link href="https://sphinxxxx.github.io/vue-components/vue-inputs.css" rel="stylesheet" />
<main id="app">
<h2>Involute gear meshing test</h2>
<div class="slider-grid">
<slider v-model="pinionDegs" caption="Rotation" :attr="{ min: -180, max: 180, step: 1 }"></slider>
<label>
<span>Fixed pinion</span>
<input type="checkbox" v-model="view.fixedPinion">
</label>
<slider v-model="pinionTeeth" caption="Pinion" :attr="{ min: 2 }"></slider>
<label>
<span>Undercut</span>
<input type="checkbox" v-model="tooth.undercut">
</label>
<slider v-model="gearTeeth" caption="Gear" :attr="{ min: 2 }"></slider>
<slider v-model="sampleDegs" caption="Sampling dist." :attr="{ min: 1, max: 20 }"></slider>
</div>
<svg fill="none" stroke="dodgerblue" :width="400" :height="400" viewBox="-100,-100 200,200" viewBox="30 -10 20 20" xmlns="http://www.w3.org/2000/svg"
_pointermove="onDrag" _click="onClick">
<g :transform="transContainer">
<g id="rack" :transform="transRack" stroke="lime">
<path :d="'M' + [0, -gearR, 'v' + 2 * gearR]" stroke-dasharray="20" />
<path :d="'M' + rackTooth(-2 * tooth.width) + rackTooth(0) + rackTooth(2 * tooth.width)" fill="lime" fill-opacity=".5" />
</g>
<g id="pinion" :transform="transPinion" stroke="tomato">
<g stroke-dasharray="20">
<circle :r="pinionR" />
<path :d="'M0,0 ' + [pinionR, 0]" />
<circle :r="pinionR + tooth.addendum" stroke-width=".3" />
<circle :r="pinionR - tooth.addendum" stroke-width=".3" />
</g>
<g stroke-width=".5">
<path v-for="poly in pinionCutsProxy.cuts" :d="'M' + poly" />
</g>
<g stroke="black">
<path v-for="part in pinionCutsProxy.parts" v-if="part.length"
:d="'M' + part /*+ ' l1,1'*/" stroke-width="4" stroke-opacity=".2" />
<path :d="'M' + pinionCutsProxy.outline" :transform="`rotate(${pinionToothRot/2})`" />
</g>
<path :d="'M' + ringCutter" _fill="tomato" fill-opacity=".5" />
<path :d="'M' + ringCutter" :transform="`rotate(${pinionToothRot})`"/>
<!--g v-for="(c, i) in pinionCoords">
<path :d="'M0,0 ' + c" />
<circle :r="4" :cx="c[0]" :cy="c[1]" :_data-dragger="i" fill="transparent" />
</g-->
<g stroke="hotpink">
<circle :r="pinionBaseCircleR.baseR" stroke-dasharray="4" />
<path v-for="line in pinionBaseCircleR.cuts" :d="'M' + line" stroke-width="4" stroke-opacity=".3" />
<circlemarker v-for="point in pinionBaseCircleR.cuts[2]" r=".1" :coord="point" />
</g>
</g>
<g id="gear" :transform="transGear">
<g stroke-dasharray="20">
<circle :r="gearR" />
<circle :r="gearR + tooth.addendum" stroke-width=".3" />
<circle :r="gearR - tooth.addendum" stroke-width=".3" />
</g>
<!--g>
<polyline v-for="c in ringCutter" :points="gearPoly(c)" />
</g-->
<g stroke="dodgerblue" _fill="violet" fill-opacity=".1" stroke-opacity="1.6">
<g v-for="poly in ringCuttersProxy.cutters">
<polyline :points="poly" />
<_circlemarker r="1" :coord="poly[0]" />
</g>
<g v-for="poly in ringCuttersProxy.pointPaths" stroke="orange" stroke-width="2" stroke-opacity=".5">
<polyline :points="poly" />
<_circlemarker r="1" :coord="poly[0]" />
</g>
<!--
-->
<g stroke="lime" stroke-width="2">
<polyline :points="ringCuttersProxy.tipPath" />
<circlemarker :r="2" :coord="ringCuttersProxy.tipPath[0]" />
<polyline :points="ringCuttersProxy.undercutPath" />
<circlemarker :r="1" :coord="ringCuttersProxy.undercutPath[0]" />
</g>
<!--g v-for="poly in ringCuttersProxy">
<circlemarker v-for="coord in poly" :r=".1" :coord="coord" />
</g-->
<g v-for="poly in DEBUG_segments" stroke="red" stroke-width="3" stroke-opacity=".4">
<polyline :points="poly" />
<circlemarker r=".1" :coord="poly[0]" />
</g>
</g>
<g id="ringTooth" stroke="black">
<_polyline :points="ringToothComp" _stroke-dasharray="4 8" />
<g v-for="poly in [ringToothComp]">
<circlemarker v-for="coord in poly" :r=".05" :coord="coord" />
</g>
<polyline :points="ringToothComp" :transform="`rotate(${gearToothRot})`" />
</g>
<g id="debug" stroke="red">
<_path :d="'M' + DEBUG_markers" />
<circlemarker v-for="coord in DEBUG_markers" r="2" :coord="coord" />
</g>
</g>
</g>
</svg>
<!--slider v-model="view.zoom" caption="Zoom" :attr="{ min: .1, max: 10, step: .1 }"></slider-->
</main>
(function() {
function deg2rad(degs) {
return Math.PI * degs / 180;
}
function createVector(degs, length) {
const rads = deg2rad(degs),
vector = [
[0, 0],
[Math.cos(rads) * length, Math.sin(rads) * length]
];
return vector;
}
//https://stackoverflow.com/a/17411276/1869660
function rotate(cx, cy, x, y, angle) {
var radians = deg2rad(angle),
cos = Math.cos(radians),
sin = Math.sin(radians),
nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
return [nx, ny];
}
// line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/
// Determine the intersection point of two line segments
// Return FALSE if the lines don't intersect
function intersect(segmentA, segmentB, epsilon = 1e-9) {
const [[x1, y1], [x2, y2]] = segmentA,
[[x3, y3], [x4, y4]] = segmentB;
const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
if (denominator === 0) {
// Lines are parallel
return false;
}
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator,
ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
// is the intersection along the segments
const outsideSegment = (u) => ((u < -epsilon) || (u > 1 + epsilon));
if (outsideSegment(ua) || outsideSegment(ub)) {
return false;
}
// Return a object with the x and y coordinates of the intersection
const x = x1 + ua * (x2 - x1),
y = y1 + ua * (y2 - y1);
return [x, y];
}
function intersectCircle(segment, circleR, epsilon = 1e-9) {
//*
const outsideSegment = (t) => ((t < -epsilon) || (t > 1 + epsilon));
//https://math.stackexchange.com/a/2862/148688
//
// tx1 + (1−t)x2 = x
// ty1 + (1−t)y2 = y
// x² + y² = r²
//
// (tx1 + (1−t)x2)² + (ty1 + (1−t)y2)² = r²
//
// Solve for t: At² + Bt + C = 0
// A = x1² − 2x1x2 + x2² + y1² − 2y1y2 + y2²
// B = 2x1x2 − 2x2² + 2y1y2 − 2y2²
// C = x2² + y2² − r²
//
const [[x1, y1], [x2, y2]] = segment, r = circleR,
A = x1**2 - 2*x1*x2 + x2**2 + y1**2 - 2*y1*y2 + y2**2,
B = 2*x1*x2 - 2*x2**2 + 2*y1*y2 - 2*y2**2,
C = x2**2 + y2**2 - r**2;
//Use the quadratic formula:
//
// −B ± √(B² − 4AC)
// t = −−−−−−−−−−−−−−−−
// 2A
//
const bac = B**2 - 4*A*C;
if (bac < 0) { return false; }
const bacRoot = Math.sqrt(bac);
let t = (-B - bacRoot) / (2*A);
if (outsideSegment(t)) {
t = (-B + bacRoot) / (2*A);
if (outsideSegment(t)) { return false; }
}
const x = t*x1 + (1-t)*x2,
y = t*y1 + (1-t)*y2;
return [x, y];
/*/
let r1 = Math.hypot(...segment[0]),
r2 = Math.hypot(...segment[1]),
inv = false;
if(r1 > r2) {
[r1, r2] = [r2, r1];
inv = true;
}
if ((r1 <= circleR) && (r2 >= circleR)) {
const lerpBy = (circleR - r1) / (r2 - r1),
point = lerp(...segment, inv ? (1 - lerpBy) : lerpBy);
return point;
}
return false;
//*/
}
//https://algorithmtutor.com/Computational-Geometry/Determining-if-two-consecutive-segments-turn-left-or-right/
function clockwise(a, b, c) {
function vector(from, to) {
return [to[0] - from[0], to[1] - from[1]];
}
// Calculates the cross product of vectors v1 and v2:
// - If v2 is clockwise from v1 wrt origin then it returns +ve value.
// - If v2 is anti-clockwise from v1 wrt origin then it returns -ve value.
// - If v2 and v1 are collinear then it returns 0.
function cross_product(v1, v2) {
return v1[0] * v2[1] - v2[0] * v1[1];
}
const v1 = vector(a, b),
v2 = vector(a, c),
cross = cross_product(v1, v2);
return (cross > 0);
}
//https://en.wikipedia.org/wiki/Linear_interpolation#Programming_language_support
function lerp(a, b, t) {
const [x1, y1] = a,
[x2, y2] = b;
//Precise method, which guarantees v = v1 when t = 1:
return [
((1 - t) * x1) + (t * x2),
((1 - t) * y1) + (t * y2),
];
}
function flipY(coord) {
return [coord[0], -coord[1]];
}
function coordEqual(a, b) {
return (a[0] === b[0] && a[1] === b[1]);
}
class Pairs {
constructor() {
this._map = new Map();
}
add(a, b) {
const map = this._map;
function doAdd(aa, bb) {
if(!map.has(aa)) {
map.set(aa, new Set());
}
map.get(aa).add(bb);
}
if (map.has(b)) {
doAdd(b, a)
}
else {
doAdd(a, b);
}
}
has(a, b) {
const map = this._map;
function checkHas(aa, bb) {
return (map.has(aa) && map.get(aa).has(bb));
}
return (checkHas(a, b) || checkHas(b, a));
}
}
Vue.component('circlemarker', {
template: `<circle v-if="coord" :r="r || 1" :fill="fill || 'transparent'" :cx="coord[0]" :cy="coord[1]">
<title>{{ coord }}</title>
</circle>`,
props: ['coord', 'r', 'fill'],
});
new Vue({
el: '#app',
data: {
tooth: {
width: 8,
addendum: 5, //tooth_width * 2 / π
dedendum: 5, //TODO: Calc with clearance..
pressureAngle: 20,
profileShift: .5,
clearance: 1,
backlash: 0,
undercut: true,
},
//pinionR: 40,
pinionTeeth: 16,
pinionDegs: 0,
//gearR: 85,
gearTeeth: 34,
sampleDegs: 9,
view: {
fixedPinion: true,
zoom: 1,
},
DEBUG_markers: [],
DEBUG_segments: [],
},
computed: {
circularPitch() {
//TODO: Is this still considered correct if we apply profile shift?
return this.tooth.width * 2;
},
pinionR() {
const circum = this.pinionTeeth * this.circularPitch,
r = circum / (2 * Math.PI);
return r;
},
gearR() {
const circum = this.gearTeeth * this.circularPitch,
r = circum / (2 * Math.PI);
return r;
},
gearX() { return (this.pinionR - this.gearR); },
gearDegs() { return this.pinionToGearDegs(this.pinionDegs); },
transContainer() {
return this.view.fixedPinion ? `rotate(${-this.pinionDegs})` : '';
},
transRack() {
const y = this.rackOffset(this.pinionDegs);
return `translate(${this.rackToWorld([0, y])})`;
},
transPinion() {
return `rotate(${this.pinionDegs})`;
},
transGear() {
return `translate(${this.gearX}) rotate(${this.gearDegs})`;
},
pinionToothRot() {
return this.toothRotation(this.pinionR);
},
gearToothRot() {
return this.toothRotation(this.gearR);
},
pinionCutsProxy() {
const start = performance.now();
const cuts = this.pinionCuts(this.tooth.undercut);
const end = performance.now();
//console.log('cut-c', end - start);
return cuts;
},
pinionBaseCircleR() {
//Theory: To avoid searching for "kick" and "loop" on the involute,
//maybe just start at the `base circle`?
const getRackFlank = (angle) => this.pinionCutter(angle).slice(2);
const rackFlank = getRackFlank(0);
const [[x2, y2], [x1, y1]] = rackFlank,
run = x2 - x1,
rise = y2 - y1;
const yAtX0 = y1 - (x1 * rise/run),
pinionAngle = (yAtX0 / this.pinionR) / Math.PI * 180;
const cut1 = getRackFlank(pinionAngle),
cut2 = getRackFlank(pinionAngle + .1),
p1 = intersect(cut1, cut2, 99),
r = Math.hypot(...p1);
//Faster way to get the radius:
//https://www.sdp-si.com/resources/elements-of-metric-gear-technology/page2.php#Section3
//TODO: Faster way to get `pinionAngle` or `yAtX0` above?
// console.log('bc-pdf', this.pinionR * Math.cos(Math.PI * this.tooth.pressureAngle / 180));
// console.log('bc-est', r, [yAtX0, pinionAngle]);
const involute = [p1];
let prevCut = cut1;
for (let i = 1; i < 100; i++) {
const cut = getRackFlank(pinionAngle + i*2),
point = intersect(prevCut, cut, 99);
//console.log('ii', point[0]);
involute.push(point);
if (Math.hypot(...point) > (this.pinionR + this.tooth.addendum)) {
break;
}
prevCut = cut;
}
return {
baseR: r,
cuts: [cut1, cut2, involute.map(flipY)],
}
},
ringCutter() {
const pinTooth = this.pinionCutsProxy.outline,
angle = this.pinionToothRot / 2;
return pinTooth.map(c => [-c[0], c[1]]).reverse();
},
ringCuttersProxy() {
return this.ringCutters(false, true);
},
ringToothComp() {
const a = Date.now();
const tooth = this.ringTooth2();
const b = Date.now();
console.log('rtc', b - a);
return tooth;
},
},
mounted() {
const svg = document.querySelector('svg'),
{ width: w, height: h } = svg.getBoundingClientRect();
const size = this.gearR * 2,
vb = (w > h) ? [-size/2, -size/2, size * w/h, size]
: [-size/2, -size/2, size, size * h/w];
svg.setAttribute('viewBox', vb);
zoomableSvg(svg);
},
methods: {
rackTooth(offsetY = 0, profileShift = 0) {
const t = this.tooth,
w = t.width,
rads = deg2rad(t.pressureAngle),
add = t.addendum;
const riseOverRun = Math.sin(rads) / Math.cos(rads),
addRise = add * riseOverRun,
run = 2 * add + t.clearance,
rise = run * riseOverRun;
const xBase = -add + profileShift,
yBase = (w / 2) + addRise;
let x = xBase,
y = -yBase + offsetY;
const base1 = [x, y];
const tip1 = [x + run, y + rise];
x = xBase;
y = yBase + offsetY;
const base2 = [x, y];
const tip2 = [x + run, y - rise];
return [base1, tip1, tip2, base2];
},
rackOffset(pinionDegs) {
const arcLength = deg2rad(pinionDegs) * this.pinionR;
return -arcLength;
},
rackToWorld(coord) {
const [x, y] = coord;
return [x - this.pinionR, y];
},
worldToPinion(coord, pinionDegs) {
const pinionCoord = rotate(0, 0, ...coord, pinionDegs);
return pinionCoord;
},
pinionToWorld(coord, pinionDegs) {
const worldCoord = rotate(0, 0, ...coord, -pinionDegs);
return worldCoord;
},
pinionCutter(angle) {
const cutter = this.rackTooth(this.rackOffset(angle)),
worldCoords = cutter.map(coord => this.rackToWorld(coord)),
pinionCoords = worldCoords.map(coord => this.worldToPinion(coord, angle));
return pinionCoords;
},
pinionCuts(undercuts = true) {
const detail = this.sampleDegs;
function incrAngle(a) {
return a + detail;
//return a + (a + detail) / detail;
}
//const createCut = (angle) => {
// const cutter = this.rackTooth(this.rackOffset(angle)),
// worldCoords = cutter.map(coord => this.rackToWorld(coord)),
// pinionCoords = worldCoords.map(coord => this.worldToPinion(coord, angle));
// return pinionCoords;
//};
const cuts = [],
traceUndercutSide = [],
traceUndercutBottom = [],
traceInvolute = [],
outerR = (this.pinionR + this.tooth.addendum);
const baseCut = this.pinionCutter(0);
cuts.push(baseCut);
let rackBase1 = baseCut[0],
rackTip1 = baseCut[1],
rackTip2 = baseCut[2],
lowestCut = rackTip2,
undercutBottomEnd;
const firstInvSeg = baseCut.slice(2),
pinionBottom = baseCut.slice(1, 3);
let prevKickSeg = baseCut.slice(0, 2),
prevInvSeg = firstInvSeg,
prevInvPoint = rackTip2,
invStartPoint = prevInvPoint;
let prevR = 0,
prevKickPoint,
involuteDone = false,
dealingWithLoop = false,
kickDone = !undercuts,
undercutDone = !undercuts;
//Sample cuts at different angles to get the outline of a tooth:
for (let a = incrAngle(0); a < 180; a = incrAngle(a)) {
const pinionCoords = this.pinionCutter(a);
cuts.push(pinionCoords);
//Trace undercuts:
if(!undercutDone) {
//Trace the side until the rack tooth moves outside the involute curve:
const undercutSide = pinionCoords[1];
if(clockwise(rackBase1, rackTip1, undercutSide)) {
undercutDone = true;
//We may need this last segment when we later look for the intersection between undercut and involute:
if(traceUndercutSide.length) {
traceUndercutSide.push(undercutSide);
}
}
else {
traceUndercutSide.push(undercutSide);
}
//Trace the bottom until it goes back into the base cut:
const undercutBottom = pinionCoords[2],
bottomStillArced = (undercutBottom[0] > rackTip1[0]) &&
(undercutBottomEnd ? clockwise(lowestCut, undercutBottomEnd, undercutBottom)
: undercutBottom[0] > lowestCut[0]);
if (bottomStillArced) {
traceUndercutBottom.push(undercutBottom);
const end = intersect(pinionBottom, [undercutBottom, undercutSide]);
if (!undercutBottomEnd || (end[1] < undercutBottomEnd[1])) {
undercutBottomEnd = end;
}
lowestCut = undercutBottom;
}
}
//Trace the main involute:
if(!involuteDone) {
const involuteSegment = pinionCoords.slice(2);
let involutePoint = intersect(prevInvSeg, involuteSegment) || prevInvSeg[1],
r = Math.hypot(...involutePoint);
//If we're not doing undercuts, and for very small gears (3-4 teeth),
//the involute makes a loop motion at the beginning (in the part that would normally be cut away).
//We can safely skip that part:
let discardInvPoint = false;
if (!undercuts && (r < prevR)) {
dealingWithLoop = true;
discardInvPoint = true;
//console.log('jam', a, JSON.stringify(traceInvolute.concat([involutePoint])));
}
if(!discardInvPoint) {
//If we have reached the end of a loop, replace the start point of the loop
//(which is too far ahead) with this more correct start of the involute curve:
if(dealingWithLoop) {
traceInvolute.pop();
involutePoint = intersect(firstInvSeg, involuteSegment, 99);
dealingWithLoop = false;
}
traceInvolute.push(involutePoint);
}
if (!kickDone) {
if (!prevKickPoint) {
prevKickPoint = involutePoint;
}
const kickSegment = pinionCoords.slice(0, 2),
kickPoint = intersect(prevKickSeg, kickSegment);
if (kickPoint && (kickPoint[0] > prevKickPoint[0])) {
//console.log('kick', kickPoint, kickSegment, prevKickSeg);
traceInvolute.unshift(flipY(kickPoint));
invStartPoint = flipY(kickSegment[1]);
}
else {
kickDone = true;
}
prevKickSeg = kickSegment;
prevKickPoint = kickPoint;
}
//When we reach the addendum circle, we are done tracing the involute:
if (r >= outerR) {
//involutePoint = intersectCircle([prevInvPoint, involutePoint], outerR);
involuteDone = true;
}
if(!discardInvPoint) {
prevInvSeg = involuteSegment;
prevInvPoint = involutePoint;
}
prevR = r;
}
if(involuteDone && undercutDone) {
break;
}
}
traceInvolute.unshift(invStartPoint);
//Trim the involute where it crosses the addendum circle.
//In case the involute is so curved that the tooth never reaches addendum,
//cut it just before it reaches the region of the next tooth,
//to avoid duplicate points when rotating the outline to draw the whole gear.
const maxAngle = 180 - .49 * this.pinionToothRot;
const involute = this.trimOutline(traceInvolute, outerR, maxAngle, false)
.map(flipY);
//To create a complete tooth outline, first arrange all traces so they follow
//the bottom side of a tooth, going from the pinion base to the pinion tip:
let outline;
//const pinionBase = [rackTip1[0], 0];
const hasUndercut = (traceUndercutBottom.length + traceUndercutSide.length) > 0;
if(hasUndercut) {
if (undercutBottomEnd) {
traceUndercutBottom.push(undercutBottomEnd);
}
const undercut = [
...traceUndercutBottom.map(flipY).reverse(),
rackTip1,
...traceUndercutSide,
];
//..and create the outline of a complete tooth:
outline = this.pinionOutline(involute, undercut);
}
else {
outline = this.pinionOutline(involute);
}
//console.log('cuts u-i', traceUndercutBottom, traceUndercutSide, traceInvolute);
return {
cuts,
parts: [
traceUndercutBottom,
traceUndercutSide,
involute,
],
outline,
};
},
pinionOutline(involute, undercut) {
//Find intersection of undercut and involute and merge outline:
let halfOutline, intersection;
if(undercut) {
//With the current direction of the traced arrays,
//the intersection will be between one of the very last undercut segments
//and one of the first involute segments:
for(let i = undercut.length - 1; i > 0; i--) {
const cutRight = undercut[i - 1],
cutLeft = undercut[i];
for(let j = 1; j < involute.length; j++) {
const invRight = involute[j - 1],
invLeft = involute[j];
//Skip while the involute segment is to the right of the undercut segment:
if(cutRight[0] < invLeft[0]) { continue; }
//Abort if we have moved to an involute segment which is to the left of the undercut segment:
if(invRight[0] < cutLeft[0] ) { break; }
intersection = intersect([cutRight, cutLeft], [invRight, invLeft]);
if(intersection) {
//Remove duplicate coords if the intersection is at the end or beginning of a segment
//(often happens with sparse cut sampling)
if (coordEqual(intersection, cutRight)) { i--; }
if (coordEqual(intersection, invLeft)) { j++; }
//Smooth the involute and undercut separately,
//so we're sure we don't lose the intersection point:
halfOutline = [
...this.smoothOutline([
...undercut.slice(0, i),
intersection,
]),
...this.smoothOutline([
intersection,
...involute.slice(j),
]).slice(1),
];
//console.log('out', intersection, halfOutline[i]/*, [cutRight, cutLeft], [invRight, invLeft]*/);
break;
}
}
if(intersection) { break; }
}
}
else {
halfOutline = this.smoothOutline(involute, 'pin');
}
this.DEBUG_undercut = intersection
? halfOutline.indexOf(intersection)
: -1;
//const angle = -this.pinionToothRot;
//const otherHalf = halfOutline.map(c => rotate(0, 0, ...flipY(c), angle)).reverse();
const horizontalHalf = halfOutline.map(c => rotate(0, 0, ...c, this.pinionToothRot/2)),
otherHalf = horizontalHalf.map(flipY).reverse();
const outline = horizontalHalf.concat(otherHalf);
//console.log('out', JSON.stringify(outline.flat()));
//console.log('out2', outline[this.DEBUG_undercut]);
return outline;
},
worldToGear(coord, gearDegs) {
const gearCoord = rotate(0, 0, coord[0] - this.gearX, coord[1], gearDegs);
return gearCoord;
},
pinionToGearDegs(pinionDegs) {
return (this.pinionR / this.gearR) * pinionDegs;
},
ringTooth2() {
const { cutters, pointPaths, tipPath, undercutPath } = this.ringCutters(false, true);
const innerR = (this.gearR - this.tooth.dedendum);
const sm1 = Date.now();
const segsMap = new RingToothSegMap([...cutters, ...pointPaths], innerR);
console.log('segs1', segsMap.segments.length);
//Start by removing segments above the tip and undercut paths.
//This is a good starting point, which we know will remove a lot of unneeded segments:
segsMap.deleteAbove(segsMap.createSegments(tipPath));
segsMap.deleteAbove(segsMap.createSegments(undercutPath));
console.log('segs2', segsMap.segments.length);
//Then brute-force remove the rest of unneeded segments:
segsMap.deleteAbove(segsMap.segments);
//DEBUG
const sm2 = Date.now();
console.log('segs3', segsMap.segments.length, sm2 - sm1);
this.DEBUG_segments = segsMap.segments;
segsMap.prepareOverlaps();
const traced = [];
let visitedX;
function traceIt(coord, tag) {
//if (traced.includes(coord)) { debugger; }
//if (traced.find(co => co[0] === coord[0])) { throw new Error('COORD', coord); }
traced.push(coord);
visitedX = coord[0];
//console.log('tr', traced.length, JSON.stringify(coord), tag);
}
function findSegs(startCoord) {
return segsMap.segments.filter(seg => seg[0] === startCoord);
}
function findOutermostSeg(fromCoord, segs) {
let outermostSeg = segs[0];
for (let i = 1; i < segs.length; i++) {
const testSeg = segs[i];
if (clockwise(fromCoord, testSeg[1], outermostSeg[1])) {
outermostSeg = testSeg;
}
}
return outermostSeg;
}
function findFirstCrosser(curr) {
let maxX = 0, crossPoint, crosserSeg;
for (const seg of segsMap.getOverlapping(curr)) {
//We will never need to move backwards in the X direction:
const to = seg[1];
if (to[0] >= visitedX) { continue; }
//Only follow crossroads that take us along the outline, not into the tooth interior:
if (clockwise(...curr, to)) { continue; }
const cross = intersect(curr, seg);
if (cross && (cross[0] > maxX)) {
maxX = cross[0];
crossPoint = cross;
crosserSeg = seg;
}
}
if (crosserSeg) {
traceIt(crossPoint/*, `Cross between ${curr} and ${crosserSeg}`*/);
//visitedX = maxX;
}
return crosserSeg;
}
let coord = segsMap.startCoord(),
currSeg = findOutermostSeg(coord, findSegs(coord));
traceIt(coord, 'Start');
while (currSeg) {
const crosser = findFirstCrosser(currSeg);
if (crosser) {
currSeg = crosser;
continue;
}
coord = currSeg[1];
traceIt(coord/*, `End of ${currSeg}`*/);
const segsForward = findSegs(coord);
if (segsForward.length === 0) { break; }
currSeg = findOutermostSeg(coord, segsForward);
}
//console.log('rr', traced);
let outline = this.smoothOutline(traced, 'ring');
//Trim the outline either at addendum or halfway to the next tooth:
//Cut the outline just before it reaches the region of the next tooth,
//to avoid duplicate points when rotating the outline to draw the whole gear.
const maxAngle = this.gearToothRot * .49;
outline = this.trimOutline(outline, innerR, maxAngle, true);
//Putting it all together..
const otherHalf = outline.map(flipY).reverse();
return otherHalf.concat(outline);
},
trimOutline(outline, circleR, vectorDegs, movesClockwise) {
//Trim the outline either at a circle (usually addendum/dedendum)
//or when it crosses a vector (usually halfway to the next tooth):
//Make a vector that will cut the outline:
const cutVector = createVector(vectorDegs, circleR * 99);
let prevCoord = outline[0];
for (let i = 1; i < outline.length; i++) {
let coord = outline[i];
const moreThanHalfway = (clockwise(...cutVector, coord) === movesClockwise);
if(moreThanHalfway) {
coord = outline[i] = intersect(cutVector, [prevCoord, coord]);
outline = outline.slice(0, i + 1);
}
const pastCircle = intersectCircle([prevCoord, coord], circleR);
if (pastCircle) {
outline[i] = pastCircle;
outline = outline.slice(0, i + 1);
}
prevCoord = coord;
}
return outline;
},
/*
adjustTrace(outline, tipPath) {
if (!tipPath?.length) { return outline; }
const dentIndexes = [];
let o = 0, t = 0;
let tip = tipPath[t], tx = tip[0];
const lastTip = tipPath[tipPath.length - 1];
while(outline[o][0] > tx) {
o++;
if(o >= outline.length) { return outline; }
}
while (t < tipPath.length - 1) {
const nextTip = tipPath[t + 1],
nextX = nextTip[0];
let traced;
while(o < outline.length) {
const traced = outline[o];
if(coordEqual(traced, tip)) { o++; continue; }
const endOfSegment = coordEqual(traced, nextTip) || (traced[0] < nextX);
if(endOfSegment && !(nextTip === lastTip)) { break; }
if(clockwise(tip, nextTip, traced)) {
dentIndexes.push(o);
}
o++;
}
tip = nextTip;
tx = nextX;
t++;
}
if(dentIndexes.length) {
const replaceLastCoord = dentIndexes.includes(outline.length - 1)
outline = outline.filter((_, i) => !dentIndexes.includes(i));
if(replaceLastCoord) {
outline.push(tipPath[tipPath.length - 1]);
}
}
return outline;
},
findDents(outline) {
const dentIndexes = [];
let [a, b, c, d] = outline,
e;
for (let i = 4; i < (outline.length - 1); i++) {
e = outline[i];
if(clockwise(a, b, c) && !clockwise(b, c, d) && clockwise(c, d, e)) {
dentIndexes.push(i - 2);
a = b;
b = d;
c = e;
d = outline[++i];
}
else {
a = b;
b = c;
c = d;
d = e;
}
}
return dentIndexes;
},
*/
ringCutters(selfClosing, bothHalves) {
const detail = this.sampleDegs,
//Extra check for 2-tooth pinions or other configs that never really clear the ring:
nextToothCutoff = createVector(-this.gearToothRot / 2, this.gearR * 2);
this.DEBUG_markers = nextToothCutoff;
//We'll make the ring tooth profile by sampling the pinion tooth at different angles,
//puttimg all those polygons on top of each other, and then tracing the outline around them.
const placeCut = (pinion, angle) => {
const worldShape = pinion.map(c => this.pinionToWorld(c, angle)),
gearCutter = worldShape.map(c => this.worldToGear(c, this.pinionToGearDegs(angle)));
return gearCutter;
};
const cutters = [],
tipPath = [],
undercutPath = [],
pinionTooth = this.ringCutter,
tipIndex = pinionTooth.findIndex(coord => (coord[0] > this.pinionR) && (coord[1] > 0));
//DEBUG
const undercutIndex = this.DEBUG_undercut;
//console.log('cut-uc', pinionTooth[undercutIndex-1], pinionTooth[undercutIndex], pinionTooth[undercutIndex+1]);
//
let maxA = 0,
prevUndercutX = 0;
for (let a = 0; a < 180; a += detail) {
const cutter = placeCut(pinionTooth, a),
gearImprint = this.trimRingCutter(cutter, nextToothCutoff);
//There are two sharp points that will be challenging to trace in ringTooth2():
//The pinion's tip and beginning of undercut.
//These will leave jagged edges in the outline if the sampling is too coarse.
//
// (The tip is a problem where the ring gear isn't much bigger than the pinion,
// because the tip will carve out a wide entry into the ring.
// The undercut is a problem with small pinions in larger rings,
// where the extra sharp undercut carves the entry into the ring.)
//
//We log those points' path to make sure the final outline is at least the same width:
tipPath.push(cutter[tipIndex]);
if(undercutIndex >= 0) {
const uc1 = flipY(cutter[undercutIndex]),
uc2 = cutter[cutter.length - 1 - undercutIndex];
undercutPath.unshift(uc1);
undercutPath.push(uc2);
prevUndercutX = uc1[0];
}
maxA = a;
if (!gearImprint.length) {
break;
}
if(selfClosing) { gearImprint.push(gearImprint[0]); }
cutters.push(gearImprint);
//For invalid gears, just return the base cut..
if(this.gearR <= this.pinionR) { break; }
}
//The above gives us a fair, albeit jagged outline. To smooth out most of the jaggedness,
//we add polygons for how each tooth *vertex* moves through the ring:
const pointPaths = [];
for (const vertex of pinionTooth) {
const poly = [];
for (let a = (bothHalves ? -maxA : 0); a <= maxA; a += detail) {
const worldCoord = this.pinionToWorld(vertex, a),
gearCoord = this.worldToGear(worldCoord, this.pinionToGearDegs(a));
poly.push(gearCoord);
}
const imprint = this.trimRingCutter(poly, nextToothCutoff);
if(imprint.length) {
pointPaths.push(imprint);
}
}
//For our ringTooth2 tracing algorithm, we need mirrored cutters for the whole profile,
//and they need to turn in the same direction as the original cutters:
if (bothHalves) {
const otherHalf = cutters.slice(1).map(poly => poly.map(flipY).reverse());
cutters.push(...otherHalf);
}
//console.log('gc', cuts.map(poly => poly.length));
return {
cutters,
tipPath,
undercutPath: this.trimRingCutter(undercutPath, nextToothCutoff),
pointPaths,
};
},
trimRingCutter(poly, nextToothCutoff) {
const innerR = (this.gearR - this.tooth.dedendum);
function isIn(coord) {
return (
clockwise(...nextToothCutoff, coord) &&
(Math.hypot(...coord) >= innerR)
);
}
let firstIn = -1, lastIn;
for(let i = 0; i < poly.length; i++) {
if(isIn(poly[i])) {
firstIn = i;
break;
}
}
if(firstIn < 0) { return []; }
for(let i = poly.length - 1; i >= 0; i--) {
if(isIn(poly[i])) {
lastIn = i;
break;
}
}
//console.log('tr', innerR, firstIn, lastIn);
const firstKeeper = (firstIn > 0) ? firstIn - 1 : 0;
return poly.slice(firstKeeper, lastIn + 2);
},
smoothOutline(outline, tag) {
const len = outline.length;
//Smooth just a little bit to remove redundant vertexes and
//small dents which can be a side effect of high detail levels:
outline = simplify(outline, this.tooth.width * (this.sampleDegs / 1000), true);
//console.log('smooth', tag, len, outline.length);
return outline;
},
toothRotation(radius) {
//TODO: Circular pitch or something would be better, in case of profile shift?
const rotationArc = 2 * this.tooth.width,
angle = 180 * rotationArc / (Math.PI * radius);
return angle;
},
/*
onDrag(e) {
//https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events#determining_button_states
if(!(e.isPrimary && e.buttons === 1)) {
this.__currDragger = null;
return;
}
const svg = e.currentTarget,
dragger = this.__currDragger || e.target.closest('[data-dragger]');
if(dragger) {
this.__currDragger = dragger;
e.preventDefault();
Vue.set(this.pinionCoords, dragger.dataset.dragger, this.mouseToPinion(e));
}
},
onClick(e) {
//The end of a drag event:
if(e.target.closest('[data-dragger]')) { return; }
this.pinionCoords.push(this.mouseToPinion(e));
},
mouseToPinion(e) {
//https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates
//https://stackoverflow.com/questions/69916593/what-is-the-replacement-for-the-deprecated-svgpoint-javascript-api
//https://stackoverflow.com/questions/6073505/what-is-the-difference-between-screenx-y-clientx-y-and-pagex-y
const p = new DOMPoint(e.clientX, e.clientY),
svg = e.currentTarget,
coord = p.matrixTransform(svg.getScreenCTM().inverse());
return this.worldToPinion([coord.x, coord.y], this.pinionDegs);
},
*/
}
});
})();
html, body {
height: 100%;
}
body {
display: flex;
flex-flow: column;
margin: 0;
font-family: Georgia, sans-serif;
button, input, select {
font: inherit;
box-sizing: border-box;
padding: .25em .5em;
outline-color: dodgerblue;
}
input[type=range] {
padding: 0;
}
input:not([type=button]), select {
border: 1px solid silver;
}
ul, ol {
margin: 0;
padding: 0;
list-style: none;
}
main {
flex: 1 1 auto;
display: flex;
flex-flow: column;
//justify-content: safe center;
//align-items: safe center;
}
}
#app {
background-image: radial-gradient(
circle closest-corner at 50% 50%,
silver 0%, white 100%
);
h2 {
margin: .3em 0;
text-align: center;
}
.slider-grid {
margin: 0 1ch;
}
svg {
flex: 1 1 auto;
width: 100%;
background: white;
//https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not
touch-action: none;
circle, path, polyline, polygon {
vector-effect: non-scaling-stroke;
}
[data-dragger] {
cursor: pointer;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment