|
(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); |
|
}, |
|
/* |
|
DEBUG_segments() { |
|
const polys = this.ringCuttersProxy.pointPaths, |
|
segs = []; |
|
for(const poly of polys) { |
|
|
|
let from = poly[0]; |
|
for(let i = 1; i < poly.length; i++) { |
|
const to = poly[i]; |
|
let seg = [from, to]; |
|
//console.log('ds', seg); |
|
|
|
if(to[0] > from[0]) { seg.reverse(); } |
|
if(seg[1][1] < 0) { |
|
//TODO: `flippedCoords` |
|
seg = seg.map(flipY); |
|
} |
|
|
|
segs.push(seg); |
|
from = to; |
|
} |
|
} |
|
return segs; |
|
}, |
|
//*/ |
|
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; |
|
}, |
|
/* |
|
gearPoly(pinionCoord) { |
|
const poly = []; |
|
for (let a = -180; a <= 180; a += this.sampleDegs) { |
|
const worldCoord = this.pinionToWorld(pinionCoord, a), |
|
gearCoord = this.worldToGear(worldCoord, this.pinionToGearDegs(a)); |
|
poly.push(gearCoord); |
|
} |
|
const trimmed = this.trimRingCutter(poly); |
|
//console.log('gp', trimmed, poly); |
|
return trimmed; |
|
}, |
|
*/ |
|
ringTooth() { |
|
const a = Date.now(); |
|
//Note: martinez-polygon-clipping requires self-closing polygons: |
|
const cutters = this.ringCutters(true); |
|
const b = Date.now(); |
|
//console.log(JSON.stringify(cutters)); |
|
|
|
const aa = Date.now(); |
|
const halfResult = polyclip.union(...cutters.map(poly => [poly])), |
|
halfOutline = halfResult[0][0]; |
|
const bb = Date.now(); |
|
|
|
const aaa = Date.now(); |
|
const otherHalf = halfOutline.map(flipY), |
|
result = polyclip.union([halfOutline], [otherHalf]); |
|
const bbb = Date.now(); |
|
|
|
console.log('rtt', b - a, bb - aa, bbb - aaa); |
|
//console.log('rt', halfResult, result); |
|
return result[0][0]; |
|
}, |
|
_ringTooth2() { |
|
const { cutters, tipPath, undercutPath1, undercutPath2 } = this.ringCutters(false, true); |
|
|
|
//We only draw the bottom half of the tooth, and mirror it later: |
|
function isBottomHalf(coord) { |
|
return (coord[1] > 0); |
|
} |
|
//We will trace the outline starting at the bottom of the ring tooth (tip of pinion), |
|
//moving towards the center of the ring gear. We will never have to go backwards in the x direction: |
|
let visitedX = Number.POSITIVE_INFINITY; |
|
|
|
//Divide all the cutter polylines into their individual line segments |
|
//which we'll use to trace our tooth outline: |
|
const segs = []; |
|
let startSeg, minY = 0, maxX = 0; |
|
for(const poly of cutters) { |
|
|
|
let from = poly[0]; |
|
for (let i = 1; i < poly.length; i++) { |
|
const to = poly[i]; |
|
if(isBottomHalf(to)) { |
|
const seg = [from, to]; |
|
segs.push(seg); |
|
|
|
//We'll start tracing from the tooth's bottom right corner: |
|
if((from[0] > maxX) && isBottomHalf(from)) { |
|
startSeg = seg; |
|
maxX = from[0]; |
|
} |
|
} |
|
from = to; |
|
} |
|
} |
|
|
|
const that = this; |
|
this.DEBUG_segments = segs; |
|
that.DEBUG_markers = []; |
|
|
|
function findNextSeg(curr) { |
|
if (!curr) { return null; } |
|
return segs.find(seg => seg[0] === curr[1]); |
|
} |
|
function findPrevSeg(curr) { |
|
if(!curr) { return null; } |
|
return segs.find(seg => seg[1] === curr[0]); |
|
} |
|
const innerR = (this.gearR - this.tooth.dedendum); |
|
function pastEndOfOutline(coord) { |
|
const r = Math.hypot(...coord); |
|
return (r < innerR); |
|
} |
|
|
|
const visitedCrosses = new Pairs(); |
|
function findFirstCrosser(curr, prev, next) { |
|
//console.log('c1', curr[0], visitedX); |
|
|
|
let crossingSeg, crossCoord, minDist = Number.POSITIVE_INFINITY; |
|
for (const seg of segs) { |
|
if (seg === prev || seg === curr || seg === next) { continue; } |
|
if (visitedCrosses.has(curr, seg)) { continue; } |
|
|
|
//We will never have to move backwards in the x direction, |
|
//and we only draw the bottom half: |
|
const to = seg[1]; |
|
if ( !(to[0] < visitedX && isBottomHalf(to)) ) { continue; } |
|
//console.log('c2', seg[0][0]); |
|
|
|
const cross = intersect(curr, seg); |
|
if(!cross) { continue; } |
|
|
|
//Normally, we should only look at crossing that lie in the correct x direction, |
|
//but for some sharp undercuts, `curr` may be slanted backwards, and we need to skip the "x test": |
|
const maxX = (curr[0][0] < curr[1][0]) ? curr[1][0] : visitedX; |
|
if(cross[0] < maxX) { |
|
//Only follow crossings which go around the outline, |
|
//and don't take us inwards into the tooth: |
|
if (clockwise(curr[0], cross, seg[1])) { continue; } |
|
|
|
//Because of our `maxX` hack above, we now need to take the absolute value: |
|
const distX = Math.abs(curr[0][0] - cross[0]); |
|
if (distX < minDist) { |
|
crossingSeg = seg; |
|
crossCoord = cross; |
|
minDist = distX; |
|
//console.log('ccc', minDist); |
|
} |
|
} |
|
} |
|
/*DEBUG |
|
const marked = (crossCoord || curr[1]).slice(); |
|
marked[0] += that.pinionR - that.gearR; |
|
//console.log('mk', marked); |
|
that.DEBUG_markers.push([marked[0], marked[1]]); |
|
//if (marked[0] === 2.016727183917901) { debugger } |
|
//*/ |
|
|
|
if(crossingSeg) { |
|
visitedCrosses.add(curr, crossingSeg); |
|
visitedX = crossCoord[0]; |
|
//console.log('cross', curr[0], crossingSeg); |
|
} |
|
return { |
|
crosser: crossingSeg, |
|
point: crossCoord, |
|
}; |
|
} |
|
|
|
let outline = [startSeg[0]]; |
|
|
|
let prevSeg = findPrevSeg(startSeg), |
|
currSeg = startSeg, |
|
nextSeg, |
|
skips = []; |
|
while (currSeg && isBottomHalf(currSeg[1])) { |
|
nextSeg = findNextSeg(currSeg); |
|
|
|
const { crosser, point } = findFirstCrosser(currSeg, prevSeg, nextSeg); |
|
if(crosser) { |
|
skips.push(point); |
|
//Normally, we skip from crossing to crossing until we find a corner worthy of tracing, |
|
//but in some cases there are long concave segments that are actually part of the outline: |
|
if ((skips.length > 3) || pastEndOfOutline(point)) { |
|
outline.push(...skips); |
|
skips = []; |
|
} |
|
prevSeg = findPrevSeg(crosser);; |
|
currSeg = crosser; |
|
continue; |
|
} |
|
|
|
outline.push(currSeg[1]); |
|
visitedX = currSeg[1][0]; |
|
prevSeg = currSeg; |
|
currSeg = nextSeg; |
|
|
|
if(skips.length) { |
|
//console.log('skipped', skips.length, visitedX.toFixed(1)); |
|
skips = []; |
|
} |
|
} |
|
//* |
|
//For small ring gears, the pinion's tip will carve most of the outline, |
|
//and our sampling may be too coarse to get an even result: |
|
outline = this.adjustTrace(outline, tipPath); |
|
//Similarly for small pinions and their deep undercuts: |
|
outline = this.adjustTrace(outline, undercutPath1); |
|
outline = this.adjustTrace(outline, undercutPath2); |
|
|
|
//Other random dents along the line: |
|
// |
|
// const dentIndexes = this.findDents(outline, tipPath); |
|
// //console.log('dent', dentIndexes); |
|
// if(dentIndexes.length) { |
|
// outline = outline.filter((_, i) => !dentIndexes.includes(i)); |
|
// } |
|
// |
|
outline = this.smoothOutline(outline, '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); |
|
/* |
|
const halfToothRads = .99 * this.tooth.width / this.gearR, |
|
cutLength = this.gearR * 2, |
|
halfToothVector = [ |
|
[0, 0], |
|
[Math.cos(halfToothRads) * cutLength, Math.sin(halfToothRads) * cutLength] |
|
]; |
|
|
|
let prevCoord = outline[0]; |
|
for (let i = 1; i < outline.length; i++) { |
|
let coord = outline[i]; |
|
|
|
const moreThanHalfway = clockwise(...halfToothVector, coord); |
|
if(moreThanHalfway) { |
|
coord = outline[i] = intersect(halfToothVector, [prevCoord, coord]); |
|
outline = outline.slice(0, i + 1); |
|
} |
|
|
|
if (pastEndOfOutline(coord)) { |
|
outline[i] = intersectCircle([prevCoord, coord], innerR); |
|
outline = outline.slice(0, i + 1); |
|
} |
|
|
|
prevCoord = coord; |
|
} |
|
*/ |
|
|
|
//Putting it all together.. |
|
const otherHalf = outline.map(flipY).reverse(); |
|
return otherHalf.concat(outline); |
|
}, |
|
ringTooth2() { |
|
const { cutters, pointPaths, tipPath, undercutPath1, undercutPath2, undercutWhole } = 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); |
|
|
|
segsMap.filterBy(segsMap.createSegments(tipPath)); |
|
segsMap.filterBy(segsMap.createSegments(undercutWhole)); |
|
console.log('segs2', segsMap.segments.length); |
|
|
|
//DEBUG: Check if this speeds anything up.. |
|
//for (const poly of cutters) { segsMap.filterBy(poly); } |
|
//for (const poly of pointPaths) { segsMap.filterBy(poly); } |
|
segsMap.filterBy(segsMap.segments); |
|
|
|
const sm2 = Date.now(); |
|
console.log('segs3', segsMap.segments.length, sm2 - sm1); |
|
//DEBUG |
|
|
|
this.DEBUG_segments = segsMap.segments; |
|
|
|
/**/ |
|
//We only draw the bottom half of the tooth, and mirror it later: |
|
function isBottomHalf(coord) { |
|
return (coord[1] > 0); |
|
} |
|
|
|
//We will trace the outline starting at the bottom of the ring tooth (tip of pinion), |
|
//moving towards the center of the ring gear. We will never have to go backwards in the x direction: |
|
let visitedX = Number.POSITIVE_INFINITY; |
|
|
|
//Divide all the cutter polylines into their individual line segments |
|
//which we'll use to trace our tooth outline: |
|
const segs = []; |
|
let startSeg, minY = 0, maxX = 0; |
|
for(const poly of cutters) { |
|
|
|
let from = poly[0]; |
|
for (let i = 1; i < poly.length; i++) { |
|
const to = poly[i]; |
|
if(isBottomHalf(to)) { |
|
const seg = [from, to]; |
|
segs.push(seg); |
|
|
|
//We'll start tracing from the tooth's bottom right corner: |
|
if((from[0] > maxX) && isBottomHalf(from)) { |
|
startSeg = seg; |
|
maxX = from[0]; |
|
} |
|
} |
|
from = to; |
|
} |
|
} |
|
|
|
const that = this; |
|
//that.DEBUG_markers = []; |
|
|
|
function findNextSeg(curr) { |
|
if (!curr) { return null; } |
|
return segs.find(seg => seg[0] === curr[1]); |
|
} |
|
function findPrevSeg(curr) { |
|
if(!curr) { return null; } |
|
return segs.find(seg => seg[1] === curr[0]); |
|
} |
|
function pastEndOfOutline(coord) { |
|
const r = Math.hypot(...coord); |
|
return (r < innerR); |
|
} |
|
|
|
const visitedCrosses = new Pairs(); |
|
function findFirstCrosser(curr, prev, next) { |
|
//console.log('c1', curr[0], visitedX); |
|
|
|
let crossingSeg, crossCoord, minDist = Number.POSITIVE_INFINITY; |
|
for (const seg of segs) { |
|
if (seg === prev || seg === curr || seg === next) { continue; } |
|
if (visitedCrosses.has(curr, seg)) { continue; } |
|
|
|
//We will never have to move backwards in the x direction, |
|
//and we only draw the bottom half: |
|
const to = seg[1]; |
|
if ( !(to[0] < visitedX && isBottomHalf(to)) ) { continue; } |
|
//console.log('c2', seg[0][0]); |
|
|
|
const cross = intersect(curr, seg); |
|
if(!cross) { continue; } |
|
|
|
//Normally, we should only look at crossing that lie in the correct x direction, |
|
//but for some sharp undercuts, `curr` may be slanted backwards, and we need to skip the "x test": |
|
const maxX = (curr[0][0] < curr[1][0]) ? curr[1][0] : visitedX; |
|
if(cross[0] < maxX) { |
|
//Only follow crossings which go around the outline, |
|
//and don't take us inwards into the tooth: |
|
if (clockwise(curr[0], cross, seg[1])) { continue; } |
|
|
|
//Because of our `maxX` hack above, we now need to take the absolute value: |
|
const distX = Math.abs(curr[0][0] - cross[0]); |
|
if (distX < minDist) { |
|
crossingSeg = seg; |
|
crossCoord = cross; |
|
minDist = distX; |
|
//console.log('ccc', minDist); |
|
} |
|
} |
|
} |
|
|
|
if(crossingSeg) { |
|
visitedCrosses.add(curr, crossingSeg); |
|
visitedX = crossCoord[0]; |
|
//console.log('cross', curr[0], crossingSeg); |
|
} |
|
return { |
|
crosser: crossingSeg, |
|
point: crossCoord, |
|
}; |
|
} |
|
|
|
let outline = [startSeg[0]]; |
|
|
|
let prevSeg = findPrevSeg(startSeg), |
|
currSeg = startSeg, |
|
nextSeg, |
|
skips = []; |
|
while (currSeg && isBottomHalf(currSeg[1])) { |
|
nextSeg = findNextSeg(currSeg); |
|
|
|
const { crosser, point } = findFirstCrosser(currSeg, prevSeg, nextSeg); |
|
if(crosser) { |
|
skips.push(point); |
|
//Normally, we skip from crossing to crossing until we find a corner worthy of tracing, |
|
//but in some cases there are long concave segments that are actually part of the outline: |
|
if ((skips.length > 3) || pastEndOfOutline(point)) { |
|
outline.push(...skips); |
|
skips = []; |
|
} |
|
prevSeg = findPrevSeg(crosser);; |
|
currSeg = crosser; |
|
continue; |
|
} |
|
|
|
outline.push(currSeg[1]); |
|
visitedX = currSeg[1][0]; |
|
prevSeg = currSeg; |
|
currSeg = nextSeg; |
|
|
|
if(skips.length) { |
|
//console.log('skipped', skips.length, visitedX.toFixed(1)); |
|
skips = []; |
|
} |
|
} |
|
//* |
|
//For small ring gears, the pinion's tip will carve most of the outline, |
|
//and our sampling may be too coarse to get an even result: |
|
outline = this.adjustTrace(outline, tipPath); |
|
//Similarly for small pinions and their deep undercuts: |
|
outline = this.adjustTrace(outline, undercutPath1); |
|
outline = this.adjustTrace(outline, undercutPath2); |
|
|
|
//Other random dents along the line: |
|
// |
|
// const dentIndexes = this.findDents(outline, tipPath); |
|
// //console.log('dent', dentIndexes); |
|
// if(dentIndexes.length) { |
|
// outline = outline.filter((_, i) => !dentIndexes.includes(i)); |
|
// } |
|
// |
|
outline = this.smoothOutline(outline, '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 = [], |
|
undercutPath1 = [], |
|
undercutPath2 = [], |
|
undercutWhole = [], |
|
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]; |
|
undercutWhole.unshift(uc1); |
|
undercutWhole.push(uc2); |
|
|
|
if(uc1[0] < prevUndercutX) { |
|
undercutPath1.push(uc1); |
|
} |
|
else { |
|
undercutPath2.unshift(uc1); |
|
} |
|
undercutPath2.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) { |
|
/* |
|
// (Not needed for ringTooth2()..) |
|
// |
|
//We must add an extra point near the center of the tooth |
|
//to avoid concave curves closing in on themselves outside the tooth profile |
|
//(important when the pinion is almost as large as the ring). |
|
const definitelyInside = [this.gearR - 2 * this.tooth.addendum, 0] |
|
imprint.push(definitelyInside); |
|
if(selfClosing) { imprint.unshift(definitelyInside); } |
|
*/ |
|
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)); |
|
//console.log('gc', undercutPath1, undercutPath2); |
|
return { |
|
cutters, |
|
tipPath, |
|
undercutPath1, |
|
undercutPath2, |
|
undercutWhole: this.trimRingCutter(undercutWhole, 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); |
|
}, |
|
*/ |
|
} |
|
}); |
|
|
|
})(); |