diff --git a/MatterControl.MeshOperations/PlaneCutObject3D.cs b/MatterControl.MeshOperations/PlaneCutObject3D.cs index 48bd46e19..4c36784e0 100644 --- a/MatterControl.MeshOperations/PlaneCutObject3D.cs +++ b/MatterControl.MeshOperations/PlaneCutObject3D.cs @@ -28,13 +28,16 @@ either expressed or implied, of the FreeBSD Project. */ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using ClipperLib; using MatterHackers.DataConverters3D; using MatterHackers.Localizations; using MatterHackers.MatterControl.DesignTools.Operations; using MatterHackers.PolygonMesh; +using MatterHackers.PolygonMesh.Csg; using MatterHackers.VectorMath; +using MatterHackers.DataConverters2D; using Polygon = System.Collections.Generic.List; using Polygons = System.Collections.Generic.List>; @@ -49,14 +52,13 @@ namespace MatterHackers.MatterControl.DesignTools public double CutHeight { get; set; } = 10; - private double cutMargin = .1; + private double cutMargin = .01; public Mesh Cut(Mesh inMesh) { var mesh = new Mesh(inMesh.Vertices, inMesh.Faces); // copy every face that is on or below the cut plane - // cut the faces at the cut plane mesh.Split(new Plane(Vector3.UnitZ, CutHeight), cutMargin, cleanAndMerge: false); @@ -64,7 +66,13 @@ namespace MatterHackers.MatterControl.DesignTools RemoveFacesAboveCut(mesh); // calculate and add the PWN face from the loops - // vertexSourceBottom.TriangulateFaces(bottomTeselatedSource, mesh); + var cutPlane = new Plane(Vector3.UnitZ, new Vector3(0, 0, CutHeight)); + var slice = SliceLayer.CreateSlice(inMesh, cutPlane); + + var aPolys = slice.Vertices().CreatePolygons(); + aPolys = aPolys.GetCorrectedWinding(); + + aPolys.CreateVertexStorage().Vertices().TriangulateFaces(null, mesh, CutHeight); return mesh; } diff --git a/MatterControlLib/DesignTools/Primitives/Gear2D.cs b/MatterControlLib/DesignTools/Primitives/Gear2D.cs index 840775c65..c5b33afed 100644 --- a/MatterControlLib/DesignTools/Primitives/Gear2D.cs +++ b/MatterControlLib/DesignTools/Primitives/Gear2D.cs @@ -212,6 +212,8 @@ namespace MatterHackers.MatterControl.DesignTools private int _internalToothCount; + public double OuterEdgeWidth { get; set; } + public int InternalToothCount { get => _internalToothCount; @@ -225,8 +227,6 @@ namespace MatterHackers.MatterControl.DesignTools public bool Debug { get; set; } = false; - public double OuterEdgeWidth { get; set; } - private List debugData = new List(); public override IEnumerable Vertices() @@ -461,15 +461,18 @@ namespace MatterHackers.MatterControl.DesignTools { this.CenterHoleDiameter = 0; - //var externalGear = CreateExternalGearShape(); - var innerRadius = this.pitchRadius + (1 - this.profileShift) * this.addendum + this.Clearance; var outerRadius = innerRadius + OuterEdgeWidth; var outerCircle = Circle(this.center.X, center.Y, outerRadius, 1000); - //var internalGear = outerCircle.Subtract(externalGear); - //debugData.Add(internalGear); - // return internalGear; + var simpleInnerGear = false; + if (simpleInnerGear) + { + var externalGear = CreateExternalGearShape(); + var internalGear = outerCircle.Subtract(externalGear); + debugData.Add(internalGear); + return internalGear; + } var singleTooth = this.CreateInternalToothProfile(); debugData.Add(singleTooth); diff --git a/MatterControlLib/DesignTools/Primitives/Gear2D_2.cs b/MatterControlLib/DesignTools/Primitives/Gear2D_2.cs new file mode 100644 index 000000000..63df971ee --- /dev/null +++ b/MatterControlLib/DesignTools/Primitives/Gear2D_2.cs @@ -0,0 +1,1263 @@ +/* +Involute Spur Gear Builder (c) 2020 Dr. Rainer Hessmer +ported to C# 2021 by Lars Brubaker + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +using MatterHackers.VectorMath; +using System; + +#if false + +namespace Gear2D_2 +{ + public class GearBase + { + public Vector2 ORIGIN = default(Vector2); + + public double CLIPPER_SCALE = 100000; + public double CLIPPER_LIGHTEN_FACTOR = 0.0005; + + /* + function getParameterDefinitions() { + return [ + { name: 'circularPitch', caption: 'Circular pitch (the circumference of the pitch circle divided by the number of teeth):', type: 'float', initial: 8 }, + { name: 'pressureAngle', caption: 'Pressure Angle (common values are 14.5, 20 and 25 degrees):', type: 'float', initial: 20 }, + { name: 'clearance', caption: 'Clearance (minimal distance between the apex of a tooth and the trough of the other gear; in length units):', type: 'float', initial: 0.05 }, + { name: 'backlash', caption: 'Backlash (minimal distance between meshing gears; in length units):', type: 'float', initial: 0.05 }, + { name: 'profileShift', caption: 'Profile Shift (indicates what portion of gear one\'s addendum height should be shifted to gear two. E.g., a value of 0.1 means the adddendum of gear two is increased by a factor of 1.1 while the height of the addendum of gear one is reduced to 0.9 of its normal height):', type: 'float', initial: 0.0 }, + { name: 'gear1ToothCount', caption: 'Gear 1 Tooth Count (n1 > 0: external gear; n1 = 0: rack; n1 < 0: internal gear):', type: 'int', initial: 30 }, + { name: 'gear1PitchDiameter', caption: 'Gear 1 Pitch Circle Diameter', type: 'float', isCalculated: true }, + { name: 'gear1OuterDiameter', caption: 'Gear 1 Outer Circle Diameter', type: 'float', isCalculated: true }, + { name: 'gear1CenterHoleDiamater', caption: 'Gear 1 Center Hole Diameter (0 for no hole):', type: 'float', initial: 4 }, + { name: 'gear2ToothCount', caption: 'Gear 2 Tooth Count:', type: 'int', initial: 8 }, + { name: 'gear2PitchDiameter', caption: 'Gear 2 Pitch Circle Diameter', type: 'float', isCalculated: true }, + { name: 'gear2OuterDiameter', caption: 'Gear 2 Outer Circle Diameter', type: 'float', isCalculated: true }, + { name: 'gear2CenterHoleDiamater', caption: 'Gear 2 Center Hole Diameter (0 for no hole):', type: 'float', initial: 4 }, + { name: 'gearCentersDistance', caption: 'Gear Centers Distance', type: 'float', isCalculated: true }, + { name: 'showOption', caption: 'Show:', type: 'choice', values:[3, 1, 2], initial: 3, captions:["Gear 1 and 2", "Gear 1 Only", "Gear 2 Only"]}, + ]; + } + */ + + public void update() { + var gearSet = new GearSet(); + parameters.injectFromUI(gearSet); + var status = gearSet.update(); + if (!status.ok()) { + alert(status.message); + return; + } + parameters.writeToUI(gearSet); + parameters.setQueryParameters(); + + display(gearSet); + exportSvg(gearSet); + } + + public void display(GearSet gearSet) { + drawing.clear(); + var topGroup = drawing.group(); + topGroup.panZoom(); + + // Scale to fit with some border around + var borderRatio = 0.02; + var border = borderRatio * Math.max(gearSet.width, gearSet.height); + var totalWidth = gearSet.width + 2 * border; + var totalHeight = gearSet.height + 2 * border; + + var scalingFactor = Math.min(drawingWidth / totalWidth, drawingHeight / totalHeight); + mainGroup = topGroup.group().scale(scalingFactor, scalingFactor).x(-gearSet.center.X).y(-gearSet.center.Y); + mainGroup.dx(drawingWidth / scalingFactor / 2); + mainGroup.dy(drawingHeight / scalingFactor / 2); + + //console.log(drawingWidth / scalingFactor / 2); + //console.log(gearSet.width / 2); + + mainGroup.stroke(regularLinesStyle).fill('none'); + + gearSet.createGraphics(mainGroup); + } + + public void exportSvg(gearSet) { + // For export we use a separate svg instance that is properly sized in mm. + // The viewbox is always in px. 1 mm = 3.543307 px (see https://mpetroff.net/2013/08/analysis-of-svg-units/) + var pxPerMillimeter = 3.543307; + // We leave some space around the escapement drawing. + var borderRatio = 0.05; + var border = borderRatio * Math.max(gearSet.width, gearSet.height); + var totalWidth = gearSet.width + 2 * border; + var totalHeight = gearSet.height + 2 * border; + var drawingForExport = SVG('drawingForExport') + .size(totalWidth + 'mm', totalHeight + 'mm') + .viewbox(gearSet.left - border, -gearSet.top - border, totalWidth, totalHeight); + var topGroup = drawingForExport.group(); + + gearSet.createGraphics(topGroup); + var exportedSVG = drawingForExport.exportSvg({ whitespace: true }); + document.getElementById("exportedSVG").value = exportedSVG; + + // based on code from Andreas Köberle (http://stackoverflow.com/questions/10120975/how-to-save-an-svg-generated-by-raphael) + var anchor = document.getElementById('downloadSVG'); + anchor.innerHTML = 'Download SVG'; + anchor.download =`involute_gear_${ gearSet.gear1.toothCount? gearSet.gear1.toothCount : 0} _to_${ gearSet.gear2.toothCount}.svg`; + anchor.type = 'image/svg+xml'; + + // see Eric Bidelman: http://updates.html5rocks.com/2012/06/Don-t-Build-Blobs-Construct-Them + var blob = new Blob([exportedSVG], { type: 'image/svg+xml'}); + anchor.href = (window.URL || webkitURL).createObjectURL(blob); + } + } + + public class GearSet + { + public double diametralPitch; + public double circularPitch; + public int gear2ToothCount; + public double gear2CenterHoleDiamater; + public double gear2PitchDiameter; + public double gear2OuterDiameter; + public RegularGear gear2; + public double clearance; + public double backlash; + public double profileShift; + public double gear1PitchDiameter; + private double gear1OuterDiameter; + private double gearCentersDistance; + private int gear1ToothCount; + private RegularGear gear1; + private double gear1CenterHoleDiamater; + + public void update() { + // convert circular pitch to diametral pitch + this.diametralPitch = Math.PI / this.circularPitch; + + // Gear 2 must always be a regular gear. + if (this.gear2ToothCount < 3) { + return Status.createError('Gear 2 must have at least three teeth.'); + } + this.gear2 = new RegularGear( + this, + this.gear2ToothCount, + this.gear2CenterHoleDiamater, + this.clearance, + this.backlash, + this.profileShift + ); + this.gear2PitchDiameter = this.gear2.pitchDiameter; + this.gear2OuterDiameter = 2 * this.gear2.outerRadius; + + if (this.gear1ToothCount == 0) { + // Create rack + this.gear1 = new Rack( + this, + this.clearance, + this.backlash, + -this.profileShift + ); + this.gear1PitchDiameter = 0; + this.gear1OuterDiameter = 0; + this.gearCentersDistance = 0; + + this.gear1.isLeft = true; + this.gear2.isLeft = false; + } + + if (this.gear1ToothCount > 0) { + // Regular gear + if (this.gear1ToothCount < 3) { + return Status.createError('External gear 1 must have at least three teeth.'); + } + + this.gear1 = new RegularGear( + this, + this.gear1ToothCount, + this.gear1CenterHoleDiamater, + this.clearance, + this.backlash, + -this.profileShift + ); + this.gear1PitchDiameter = this.gear1.pitchDiameter; + this.gear1OuterDiameter = 2 * this.gear1.outerRadius; + this.gearCentersDistance = this.gear1.pitchRadius + this.gear2.pitchRadius; + + this.gear1.isLeft = true; + this.gear2.isLeft = false; + } + + if (this.gear1ToothCount < 0) { + // Internal gear + if (-this.gear1ToothCount - this.gear2ToothCount < 1) { + return Status.createError('Internal gear 1 must have at least one more tooth than gear 2.'); + } + + this.gear1 = new InternalGear( + this, + -this.gear1ToothCount, + this.clearance, + this.backlash, + -this.profileShift + ); + this.gear1PitchDiameter = this.gear1.pitchDiameter; + this.gear1OuterDiameter = 2 * this.gear1.outerRadius; + this.gearCentersDistance = this.gear1.pitchRadius - this.gear2.pitchRadius; + + this.gear1.isLeft = false; + this.gear2.isLeft = true; + } + + this.gear1.connectedGear = this.gear2; + this.gear2.connectedGear = this.gear1; + + this.gear1.center = ORIGIN; + this.gear2.center = this.calcGear2Center(); + + this.gear1.update(); + this.gear2.update(); + + this.topLeft = createPoint(Math.min(this.gear1.topLeft.X, this.gear2.topLeft.X), Math.max(this.gear1.topLeft.Y, this.gear2.topLeft.Y)); + this.lowerRight = createPoint(Math.max(this.gear1.lowerRight.X, this.gear2.lowerRight.X), Math.min(this.gear1.lowerRight.Y, this.gear2.lowerRight.Y)); + + this.width = this.lowerRight.X - this.topLeft.X; + this.height = this.topLeft.Y - this.lowerRight.Y; + this.center = multiplyVector(0.5, addVectors(this.topLeft, this.lowerRight)); + + this.left = this.topLeft.X; + this.top = this.topLeft.Y; + + return Status.OK; + } + + public void calcGear2Center() { + if (this.gear1ToothCount == 0) { + // Rack + return addVectors(this.gear1.center, createPoint(this.gear2.pitchRadius, 0)); + } + if (this.gear1ToothCount > 0) { + // Regular gear + return addVectors(this.gear1.center, createPoint(this.gear1.pitchRadius + this.gear2.pitchRadius, 0)); + } + if (this.gear1ToothCount < 0) { + // Inner gear + return addVectors(this.gear1.center, createPoint(this.gear1.pitchRadius - this.gear2.pitchRadius, 0)); + } + } + + public void createGraphics(parent) { + var crossMarkerLength = Math.min(this.circularPitch / 2, this.width / 50); + if ((this.showOption & 2) > 0) { + // show gear 2 + this.gear2.createGraphics(parent, crossMarkerLength); + } + if ((this.showOption & 1) > 0) { + // show gear 1 + this.gear1.createGraphics(parent, crossMarkerLength); + } + } + } + + public class RegularGear { + public GearSet gearSet; + public int toothCount; + public double centerHoleDiameter; + public double clearance; + public double backlash; + public double profileShift; + public double addendumExtension; + public double angle; + public double pitchDiameter; + public double pitchRadius; + public double addendum; + public double shiftedAddendum; + public double outerRadius; + public double angleToothToTooth; + + public RegularGear(GearSet gearSet, int toothCount, double centerHoleDiameter, double clearance, double backlash, double profileShift, double addendumExtension = 0) { + this.gearSet = gearSet; + this.toothCount = toothCount; + + this.centerHoleDiameter = centerHoleDiameter; + this.clearance = clearance; + this.backlash = backlash; + this.profileShift = profileShift; + + // addendumExtension is only set for a pinion that is used as a cutter of an inner gear. In this case the + // addendum extension creates the clearance between the inner gear and its pinion. + this.addendumExtension = addendumExtension; + + this.angle = 0; // angle in rad of the complete gear (changes during rotation animation) + + // Pitch diameter: Diameter of pitch circle. + this.pitchDiameter = this.toothCount / this.gearSet.diametralPitch; + this.pitchRadius = this.pitchDiameter / 2; + + // Addendum: Radial distance from pitch circle to outside circle. + this.addendum = 1 / this.gearSet.diametralPitch; + + // Typically no profile shift is used meaning that this.shiftedAddendum = this.addendum + this.shiftedAddendum = this.addendum * (1 + this.profileShift); + + //Outer Circle + this.outerRadius = this.pitchRadius + this.shiftedAddendum + this.addendumExtension; + this.angleToothToTooth = degreeToRad(360 / this.toothCount); + } + + double degreeToRad(double degrees) + { + return MathHelper.DegreesToRadians(degrees); + } + + public void update() { + this.topLeft = addVectors(createPoint(-this.outerRadius, this.outerRadius), this.center); + this.lowerRight = addVectors(createPoint(this.outerRadius, -this.outerRadius), this.center); + + this.toothPointsTemplate = this.createToothPath(); + } + + createGraphics(parent, crossMarkerLength) { + var gearGroup = parent.group(); + var helperGroup = gearGroup.group(); + helperGroup.stroke(helperLinesStyle).fill('none'); + // Pitch circle + drawCircle(helperGroup, ORIGIN, this.pitchRadius); + // Outer circle + drawCircle(helperGroup, ORIGIN, this.outerRadius); + + drawCross(helperGroup, ORIGIN, crossMarkerLength); + + var regularGroup = gearGroup.group(); + regularGroup.stroke(regularLinesStyle).fill('none'); + + if (this.centerHoleDiameter > 0) { + drawCircle(regularGroup, ORIGIN, this.centerHoleDiameter / 2); + } + + //var {cutterPath, lowerLeftCorner} = this.createToothCutter(); + //insertSvgPath(regularGroup, cutterPath, /* isClosed=*/true); + + //var toothSectorPath = this.createToothSectorPath(); + //insertSvgPath(regularGroup, toothSectorPath, /* isClosed=*/true); + + //var {cutterPaths, lowerLeftCornerIndex} = this.createToothCutterPaths(); + //cutterPaths.forEach(toothCutterPath => insertSvgPath(regularGroup, toothCutterPath, /* isClosed=*/true)); + + //var cornersPath = []; + //cutterPaths.forEach(toothCutterPath => cornersPath.push(clonePoint(toothCutterPath[lowerLeftCornerIndex]))); + + //var helperGroup2 = gearGroup.group(); + //helperGroup2.stroke(markerLinesStyle).fill('none'); + //drawCircles(helperGroup2, cornersPath, 0.0001); + //insertSvgPath(helperGroup2, cornersPath, /* isClosed=*/true) + + + //var halfToothPath = this.createHalfToothPath(); + //insertSvgPath(regularGroup, halfToothPath, /* isClosed=*/false); + //drawCircles(helperGroup, halfToothPath, 0.0002); + //drawCircle(helperGroup, toothCutoutPath[dedendumStartIndex], 0.1); + + //var toothPath = this.createToothPath(); + //insertSvgPath(regularGroup, toothPath, /* isClosed=*/false); + //drawCircles(helperGroup, toothPath, 0.01); + + //var nextIndex = (dedendumStartIndex - 1) % toothCutoutPath.length; + //drawCircle(helperGroup, toothCutoutPath[nextIndex], 0.1); + + //var corners = []; + //cutterPaths.forEach(toothCutterPath => corners.push(toothCutterPath[lowerLeftCornerIndex])); + //drawCircles(helperGroup, corners, 0.1); + + this.insertGearSvgPath(regularGroup); + + gearGroup.move(this.center.X, this.center.Y); + } + + public void createToothSectorPath() { + // create outer circle sector covering one tooth + return [ + ORIGIN, + createPoint(0, this.outerRadius), + rotatePointAroundCenter(createPoint(0, this.outerRadius), ORIGIN, this.angleToothToTooth) + ]; + } + + public void insertGearSvgPath(group) { + var svgPath = group.path(); + var firstSvgPoint; + // Next create N (tooth count) rotated tooth paths and connect them via arcs. + var angleOffset; + if (this.isLeft) { + // rotate counter clockwise so that the starter tooth points east. + angleOffset = -Math.PI / 2 - this.angleToothToTooth / 2; + } else { + // rotate clock wise so that the starter tooth meshes with the left gear. + angleOffset = Math.PI / 2 - this.angleToothToTooth; + } + + for (var i = 0; i < this.toothCount; i++) { + var angle = i * this.angleToothToTooth + angleOffset; + var rotatedToothPoints = rotatePointsAroundCenter(this.toothPointsTemplate, ORIGIN, angle); + + if (i == 0) { + // Start with the second point since the closing arc of the last tooth will add the first point. + addLineSegmentsToPath(svgPath, rotatedToothPoints.slice(1), /*moveToFirst=*/ true); + firstSvgPoint = createSvgPoint(rotatedToothPoints[0]); + } else { + // connect the previous last point with an arc to the new, rotated tooth points. + svgPath.A(this.outerRadius, this.outerRadius, 0, 0, 1, createSvgPoint(rotatedToothPoints[0])); + addLineSegmentsToPath(svgPath, rotatedToothPoints.slice(1)); + } + } + + // Close the path by connecting the final arc. + svgPath.A(this.outerRadius, this.outerRadius, 0, 0, 1, firstSvgPoint); + // Close the path + svgPath.Z(); + + return svgPath; + } + + public void createToothPath() { + var halfToothPath = this.createHalfToothPath(); + + var toothPath = []; + // Add mirrored half tooth + for (var i = halfToothPath.length - 1; i > 0; i--) { + var point = halfToothPath[i]; + toothPath.push(createPoint(-point.X, point.Y)); + } + // Add the unmirrored original half. + halfToothPath.forEach(point => toothPath.push(point)); + + return toothPath; + } + + public void createHalfToothPath() { + var toothCutoutPath = this.createToothCutoutPath(); + + // Intersect with a slice that is half the pitch angle. + var angle = this.angleToothToTooth / 2; + var cosAngle = Math.cos(angle); + var sinAngle = Math.sin(angle); + + var halfPointOnCircle = {X: -this.outerRadius * sinAngle, Y: this.outerRadius * cosAngle}; + var tangentIntercept = { + X: 0, + Y: this.outerRadius * (cosAngle + sinAngle * sinAngle / cosAngle) + }; + + var intersectPath = [ + ORIGIN, + halfPointOnCircle, + tangentIntercept + ]; + ClipperLib.JS.ScaleUpPath(intersectPath, CLIPPER_SCALE); + + var clipper = new ClipperLib.Clipper(); + clipper.AddPath(toothCutoutPath, ClipperLib.PolyType.ptSubject, true); // true means closed path; + clipper.AddPath(intersectPath, ClipperLib.PolyType.ptClip, true); // true means closed path; + + var solutionPaths = new ClipperLib.Paths(); + var succeeded = clipper.Execute(ClipperLib.ClipType.ctIntersection, solutionPaths, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); + + var lightenedPaths = ClipperLib.JS.Lighten(solutionPaths[0], this.gearSet.circularPitch * CLIPPER_LIGHTEN_FACTOR * CLIPPER_SCALE); + var clippedToothCutoutPath = lightenedPaths[0]; + + ClipperLib.JS.ScaleDownPath(clippedToothCutoutPath, CLIPPER_SCALE); + + // Find dedendum start point at x == 0; + var dedendumStartIndex = clippedToothCutoutPath.findIndex((point) => Math.abs(point.X) < 0.01 * this.addendum && point.Y < this.pitchRadius); + //console.log("dedendumStartIndex: ", dedendumStartIndex); + + // Start from the dedendumStartIndex and iterate over all points until the next point is outside of the outer radius. + var halfToothPath = [clippedToothCutoutPath[dedendumStartIndex]]; + var currentIndex = dedendumStartIndex; + var squaredOuterRadius = this.outerRadius * this.outerRadius; + var getNextIndex = (index) => (index - 1 + clippedToothCutoutPath.length) % clippedToothCutoutPath.length + while (true) { + var nextIndex = getNextIndex(currentIndex); + if (squaredLenth(clippedToothCutoutPath[nextIndex]) >= squaredOuterRadius) { + break; + } + currentIndex = nextIndex; + halfToothPath.push(clippedToothCutoutPath[currentIndex]); + } + +// Interpolate between the last point in the trimmed path and the next point +// to find the point that intersects with the outer radius. +var lastInsidePoint = clippedToothCutoutPath[currentIndex]; +var lastInsideLength = length(lastInsidePoint); + +var firstOnOrOutsidePoint = clippedToothCutoutPath[getNextIndex(currentIndex)]; +var firstOnOrOutsideLength = length(firstOnOrOutsidePoint); + +var ratio = (this.outerRadius - lastInsideLength) / (firstOnOrOutsideLength - lastInsideLength); + +var vectorBetweenPoints = subtractVectors(firstOnOrOutsidePoint, lastInsidePoint); +var pointOnOuterRadius = addVectors(lastInsidePoint, multiplyVector(ratio, vectorBetweenPoints)); + + halfToothPath.push(pointOnOuterRadius); + + return halfToothPath; + } + + createToothCutoutPath() { + var { cutterPaths, lowerLeftCornerIndex} = this.createToothCutterPaths(); + + // Also create a path from one of the addendum corners to get smooth undercut curves. + var cornersPath = []; + cutterPaths.forEach(toothCutterPath => cornersPath.push(clonePoint(toothCutterPath[lowerLeftCornerIndex]))); + cornersPath.reverse(); + + const combinedPaths = [...cutterPaths]; + combinedPaths.push(cornersPath); + + var clipper = new ClipperLib.Clipper(); + combinedPaths.forEach(path => { + ClipperLib.JS.ScaleUpPath(path, CLIPPER_SCALE); + clipper.AddPath(path, ClipperLib.PolyType.ptSubject, true); // true means closed path; + }); + + // Union the shapes of all the tooth cutter paths. + var solutionPaths = new ClipperLib.Paths(); + var succeeded = clipper.Execute(ClipperLib.ClipType.ctUnion, solutionPaths, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); + + return solutionPaths[0]; + } + + createToothCutterPaths() { + var angleStepSize = Math.PI / 600; + + var { cutterPath, lowerLeftCornerIndex} = this.createToothCutter(); + var cutterPaths = [cutterPath]; + + // To create the tooth profile we move the (virtual) infinite gear and then turn the resulting cutter position back. + // For illustration see http://lcamtuf.coredump.cx/gcnc/ch6/, section 'Putting it all together'. + // We continue until the moved tooth cutter's lower left corner is outside of the outer circle of the gear. + // Going any further will no longer influence the shape of the tooth. + var stepCounter = 0; + while (true) { + var angle = stepCounter * angleStepSize; + var xTranslation = angle * this.pitchRadius; + + // we move in both directions + var transformedCutterPath = createTranslatedPath(cutterPath, xTranslation, 0); + transformedCutterPath = rotatePointsAroundCenter(transformedCutterPath, ORIGIN, angle); + + cutterPaths.push(transformedCutterPath) + + // Rotate in opposite direction. This is required to get the undercuts. + //console.log("xTranslation: " + xTranslation); + transformedCutterPath = createTranslatedPath(cutterPath, -xTranslation, 0); + transformedCutterPath = rotatePointsAroundCenter(transformedCutterPath, ORIGIN, -angle); + + cutterPaths.unshift(transformedCutterPath); + + //var movedLowerLeftCorner = createTranslatedPoint(lowerLeftCorner, xTranslation, 0); + //movedLowerLeftCorner = rotatePointAroundCenter(movedLowerLeftCorner, ORIGIN, angle); + + if (length(transformedCutterPath[lowerLeftCornerIndex]) > this.outerRadius) { + // The cutter is now completely outside the gear and additional steps will no longer influences the shape of the gear tooth. + break; + } + + stepCounter++; + } + + return { + cutterPaths: cutterPaths, + lowerLeftCornerIndex: lowerLeftCornerIndex + } + } + +public void createToothCutter() { + // we create a trapezoidal cutter as described at http://lcamtuf.coredump.cx/gcnc/ch6/ under the section 'Putting it all together' + var toothWidth = this.gearSet.circularPitch / 2; + //console.log("toothWidth: " + toothWidth); + //console.log("addendum: " + this.addendum); + //console.log("shiftedAddendum: " + this.shiftedAddendum); + //console.log("clearance: " + this.clearance); + + var cutterDepth = this.addendum + this.clearance; + var cutterOutsideLength = 3 * this.addendum; + //console.log("cutterDepth: " + cutterDepth); + //console.log("cutterOutsideLength: " + cutterOutsideLength); + + var cosPressureAngle = Math.cos(this.gearSet.pressureAngle * Math.PI / 180); + var tanPressureAngle = Math.tan(this.gearSet.pressureAngle * Math.PI / 180); + + // If a positive backlash is defined then we widen the trapezoid accordingly. + // Each side of the tooth needs to widened by a fourth of the backlash (perpendiculr to cutter faces). + var dx = this.backlash / 4 / cosPressureAngle; + + // Create the cutout at 6 o'clock position pointing upwards + var yBottom = this.pitchRadius + this.profileShift * this.addendum - cutterDepth; + var yTop = this.pitchRadius + this.profileShift * this.addendum + cutterOutsideLength; + + var lowerRightCorner = createPoint(toothWidth / 2 + dx - tanPressureAngle * cutterDepth, yBottom); + var upperRightCorner = createPoint(toothWidth / 2 + dx + tanPressureAngle * cutterOutsideLength, yTop); + var upperLeftCorner = createPoint(-upperRightCorner.X, upperRightCorner.Y); + var lowerLeftCorner = createPoint(-lowerRightCorner.X, lowerRightCorner.Y); + + var cutterPath = [lowerLeftCorner, upperLeftCorner, upperRightCorner, lowerRightCorner]; + + return { + cutterPath: cutterPath, + lowerLeftCornerIndex: 0 + } + } + } + + public class InternalGear { + public InternalGear(GearSet gearSet, int toothCount, double clearance, double backlash, double profileShift) { + this.gearSet = gearSet; + this.toothCount = toothCount; + this.clearance = clearance; + this.backlash = backlash; + this.profileShift = profileShift; + + this.angle = 0; // angle in rad of the complete gear (changes during rotation animation) + + // Pitch diameter: Diameter of pitch circle. + this.pitchDiameter = this.toothCount / this.gearSet.diametralPitch; + this.pitchRadius = this.pitchDiameter / 2; + + // Addendum: Radial distance from pitch circle to inside circle. + this.addendum = 1 / this.gearSet.diametralPitch; + + // Typically no profile shift is used meaning that this.shiftedAddendum = this.addendum + this.shiftedAddendum = this.addendum * (1 + this.profileShift); + + // Inner Circle (addendum) + this.innerRadius = this.pitchRadius - this.shiftedAddendum; + + // Dedendum Circle + this.dedendumRadius = this.pitchRadius + this.shiftedAddendum; + + this.angleToothToTooth = degreeToRad(360 / this.toothCount); + + // Outer circle; just a circle that is greater than the dedendum circle. + this.outerRadius = this.pitchRadius + 2.5 * this.addendum; + } + + public void update() { + this.topLeft = addVectors(createPoint(-this.outerRadius, this.outerRadius), this.center); + this.lowerRight = addVectors(createPoint(this.outerRadius, -this.outerRadius), this.center); + + this.pinion = this.connectedGear; + this.toothPointsTemplate = this.createToothPath(); + } + + public void createGraphics(parent, crossMarkerLength) { + var gearGroup = parent.group(); + var helperGroup = gearGroup.group(); + helperGroup.stroke(helperLinesStyle).fill('none'); + // Inner circle (addendum) + drawCircle(helperGroup, ORIGIN, this.innerRadius); + // Pitch circle + drawCircle(helperGroup, ORIGIN, this.pitchRadius); + // Dedendum circle + drawCircle(helperGroup, ORIGIN, this.dedendumRadius); + + drawCross(helperGroup, ORIGIN, crossMarkerLength); + + //var zeroedHalfCutterPath = this.createZeroedHalfToothCutterPath(); + //insertSvgPath(helperGroup, zeroedHalfCutterPath, true); + + //var halfToothSectorPath = this.createHalfToothSectorPath(); + //insertSvgPath(helperGroup, halfToothSectorPath, /* isClosed=*/true); + + var regularGroup = gearGroup.group(); + regularGroup.stroke(regularLinesStyle).fill('none'); + + //var halfToothPath = this.createEnlargedPinionHalfToothPath(); + //insertSvgPath(regularGroup, halfToothPath, /* isClosed=*/false); + + //var zeroedCutterPath = this.createZeroedCutterPath(); + //insertSvgPath(regularGroup, zeroedCutterPath, false); + + var helperGroup2 = gearGroup.group(); + helperGroup2.stroke(helperLinesStyle2).fill('none'); + + //var zeroedHalfCutterPath = this.createZeroedHalfToothCutterPath(); + //insertSvgPath(helperGroup2, zeroedHalfCutterPath, true); + + //var halfToothSectorPath = this.createHalfToothSectorPath(); + //insertSvgPath(helperGroup2, halfToothSectorPath, true); + + //var cutterPaths = this.createHalfToothCutterPaths(zeroedHalfCutterPath); + //cutterPaths.forEach(path => insertSvgPath(helperGroup2, path, true)); + + //var cornersPaths = this.createCornersPaths(cutterPaths); + //cornersPaths.forEach(path => insertSvgPath(helperGroup, path, true)); + + var markerGroup = gearGroup.group(); + markerGroup.stroke(markerLinesStyle).fill('none'); + + /* + cornersPaths.forEach(path => { + drawCircle(markerGroup, path[0], 0.01); + drawCircle(markerGroup, path.slice(-1)[0], 0.01); + }); + cornersPaths[0].slice(0,5).forEach(point => { + drawCircle(markerGroup, point, 0.01); + }); + */ + + //var edgesPath = this.createEdgesPath(cutterPaths); + //insertSvgPath(markerGroup, edgesPath, true); + //drawCircles(markerGroup, edgesPath, 0.02); + + //var halfToothPath = this.createHalfToothPath(); + //insertSvgPath(markerGroup, halfToothPath, false); + //drawCircle(markerGroup, halfToothPath[0], 0.01); + //drawCircle(markerGroup, halfToothPath.slice(-1)[0], 0.01); + + //var toothPath = this.createToothPath(); + //insertSvgPath(markerGroup, this.toothPointsTemplate, false); + + this.insertGearSvgPath(regularGroup); + + // Outer circle + drawCircle(regularGroup, ORIGIN, this.outerRadius); + + gearGroup.move(this.center.X, this.center.Y); + } + + public void insertGearSvgPath(group) { + var svgPath = group.path(); + + var firstSvgPoint; + var angleOffset; + if (this.isLeft) { + // rotate counter clockwise so that the starter tooth points east. + angleOffset = Math.PI - this.angleToothToTooth / 2; + } else { + // rotate clock wise so that the starter tooth meshes with the left gear. + angleOffset = -this.angleToothToTooth / 2; + } + for (var i = 0; i < this.toothCount; i++) { + var angle = -i * this.angleToothToTooth + angleOffset; + var rotatedToothPoints = rotatePointsAroundCenter(this.toothPointsTemplate, ORIGIN, angle); + + if (i == 0) { + // Start with the second point since the closing arc of the last tooth will add the first point. + addLineSegmentsToPath(svgPath, rotatedToothPoints.slice(1), /*moveToFirst=*/ true); + firstSvgPoint = createSvgPoint(rotatedToothPoints[0]); + } else { + // connect the previous last point with an arc to the new, rotated tooth points. + svgPath.A(this.outerRadius, this.outerRadius, 0, 0, 1, createSvgPoint(rotatedToothPoints[0])); + addLineSegmentsToPath(svgPath, rotatedToothPoints.slice(1)); + } + } + + // Close the path by connecting the final arc. + svgPath.A(this.outerRadius, this.outerRadius, 0, 0, 1, firstSvgPoint); + // Close the path + svgPath.Z(); + + return svgPath; + } + + createHalfToothSectorPath() { + // create outer circle sector covering half a tooth + return [ + ORIGIN, + createPoint(this.outerRadius, 0), + rotatePointAroundCenter(createPoint(this.outerRadius, 0), ORIGIN, -this.angleToothToTooth / 2) + ]; + } + + createCornersPaths(cutterPaths) { + // Create a paths from each of the corners to avoid ragged edges. + var cornersPaths = []; + // Ignore the last point which is always the origin. + for (var i = 0; i < cutterPaths[0].length - 1; i++) { + var cornersPath = []; + cutterPaths.forEach(cutterPath => cornersPath.push(clonePoint(cutterPath[i]))); + cornersPaths.push(cornersPath); + } + + return cornersPaths; + } + + createEdgesPath(cutterPaths) { + // This step is described in the accompanying pdf doc. + // Conceptually, first we create shapes by connecting correponding points from the various cutter paths. + // Then we connect the top most endpoints of these shapes. + // Finally we add the top half of the rightmost shape. + + // Create a paths from each of the corners to avoid ragged edges. + var edgesPath = [ORIGIN]; + // Ignore the last point which is always the origin. + for (var i = cutterPaths[0].length - 1; i > 0; i--) { + var endPoint1 = cutterPaths[0][i]; + var endPoint2 = cutterPaths.slice(-1)[0][i]; + + if (endPoint1.Y < endPoint2.Y) { + edgesPath.push(clonePoint(endPoint1)); + } else { + edgesPath.push(clonePoint(endPoint2)); + } + } + + // Add the top half of the points of the rightmost shape. + for (var i = 0; i < (cutterPaths.length + 1) / 2; i++) { + edgesPath.push(clonePoint(cutterPaths[i][0])); + } + + return edgesPath; + } + + createToothPath() { + var halfToothPath = this.createHalfToothPath(); + var rotatedHalfToothPath = rotatePointsAroundCenter(halfToothPath, ORIGIN, this.angleToothToTooth / 2); + + var toothPath = [...rotatedHalfToothPath]; + // Add mirrored half tooth + for (var i = rotatedHalfToothPath.length - 1; i > 0; i--) { + var point = rotatedHalfToothPath[i]; + toothPath.push(createPoint(point.X, -point.Y)); + } + + return toothPath; } + + createHalfToothPath() { + var zeroedHalfCutterPath = this.createZeroedHalfToothCutterPath(); + var cutterPaths = this.createHalfToothCutterPaths(zeroedHalfCutterPath); + // Also create a paths from each of the corners to avoid ragged edges. + var cornersPaths = this.createCornersPaths(cutterPaths); + var edgesPath = this.createEdgesPath(cutterPaths); + var halfToothSectorPath = this.createHalfToothSectorPath(); + + // Scale them all up to prepare for union and interesction operations. + cutterPaths.forEach(path => { + ClipperLib.JS.ScaleUpPath(path, CLIPPER_SCALE); + }); + cornersPaths.forEach(path => { + ClipperLib.JS.ScaleUpPath(path, CLIPPER_SCALE); + }); + ClipperLib.JS.ScaleUpPath(edgesPath, CLIPPER_SCALE); + // The edges path often self intersects. If needed split into non-intersecting paths. + var nonIntersectingEdgesPaths = ClipperLib.Clipper.SimplifyPolygon(edgesPath, ClipperLib.PolyFillType.pftNonZero); + ClipperLib.JS.ScaleUpPath(halfToothSectorPath, CLIPPER_SCALE); + + // Union all these shapes then clip with the half tooth sector path. + var cutoutFromAllPaths = this.unionAndClipPaths([...cutterPaths, ...cornersPaths, ...nonIntersectingEdgesPaths], halfToothSectorPath); + + // Remove the origin and order the points so that the point intersecting with the addendum is the first point. + var indexOfOrigin; + for (var i = 0; i < cutoutFromAllPaths.length; i++) { + var point = cutoutFromAllPaths[i]; + if (point.X == 0 && point.Y == 0) { + indexOfOrigin = i; + break; + } + } + console.log(`Index of origin: ${indexOfOrigin}`); + var maxSquaredRadius = 0; + var indexMaxSquaredRadius = 0; + var halfToothPath = []; + for (var i = 1; i < cutoutFromAllPaths.length; i++) { + var point = cutoutFromAllPaths[(indexOfOrigin + i) % cutoutFromAllPaths.length]; + var squaredRadius = squaredLenth(point); + if (squaredRadius > maxSquaredRadius) { + maxSquaredRadius = squaredRadius; + indexMaxSquaredRadius = i - 1; + } + halfToothPath.push(point); + } + console.log(`Index of max radius point: ${indexMaxSquaredRadius}`); + + // Shave off the part beyond the max radius point. + halfToothPath = halfToothPath.slice(0, indexMaxSquaredRadius + 1); + + var lightenedPaths = ClipperLib.JS.Lighten(halfToothPath, this.gearSet.circularPitch * CLIPPER_LIGHTEN_FACTOR * CLIPPER_SCALE); + var lightenedHalfToothPath = lightenedPaths[0]; + console.log(`Length of lightened half tooth path: ${lightenedHalfToothPath.length}`); + + ClipperLib.JS.ScaleDownPath(lightenedHalfToothPath, CLIPPER_SCALE); + return lightenedHalfToothPath.reverse(); + } + + unionAndClipPaths(scaledUpPaths, scaledUpHalfToothSectorPath) { + var orientation = ClipperLib.Clipper.Orientation(scaledUpHalfToothSectorPath); + + var clipper = new ClipperLib.Clipper(); + scaledUpPaths.forEach(path => { + // Make sure the orientation matches. + if (ClipperLib.Clipper.Orientation(path) != orientation) { + path.reverse(); + } + clipper.AddPath(path, ClipperLib.PolyType.ptSubject, true); // true means closed path; + }); + + // Union the shapes of all paths. + var solutionPaths = new ClipperLib.Paths(); + var succeeded = clipper.Execute(ClipperLib.ClipType.ctUnion, solutionPaths, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); + + clipper = new ClipperLib.Clipper(); + clipper.AddPath(scaledUpHalfToothSectorPath, ClipperLib.PolyType.ptSubject, true); // true means closed path; + clipper.AddPath(solutionPaths[0], ClipperLib.PolyType.ptClip, true); // true means closed path; + + solutionPaths = new ClipperLib.Paths(); + succeeded = clipper.Execute(ClipperLib.ClipType.ctIntersection, solutionPaths, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); + return solutionPaths[0]; + } + + createHalfToothCutterPaths(zeroedHalfCutterPath) { + var gearRatio = this.toothCount / this.pinion.toothCount; + + // To create the tooth profile we move the (virtual) infinite gear and then turn the resulting cutter position back. + // For illustration see http://lcamtuf.coredump.cx/gcnc/ch6/, section 'Putting it all together'. + // We continue until the moved tooth cutter's lower left corner is outside of the outer circle of the gear. + // Going any further will no longer influence the shape of the tooth. + var angleStepSize = this.angleToothToTooth / 10; + //var angleStepSize = Math.PI / Math.sqrt(this.toothCount - this.pinion.toothCount) / halfCount; + var cutterPaths = []; + + var pinionRotationAngle = 0; + var rotatedCutter = this.createRotatedCutter(zeroedHalfCutterPath, pinionRotationAngle, gearRatio); + + cutterPaths.push(rotatedCutter); + + var desiredSquaredDistance = (this.gearSet.circularPitch / 25) ** 2; + + // Empirical value for when a half tooth is covered. + var maxPinionRotationAngle = Math.PI / Math.sqrt(this.toothCount - this.pinion.toothCount); + + var cutterEndpointIndex = zeroedHalfCutterPath.length - 1; + var outerCounter = 0; + while(outerCounter < 200 && pinionRotationAngle < maxPinionRotationAngle) { + var previousRotatedCutter = rotatedCutter; + var previousPinionRotationAngle = pinionRotationAngle; + //console.log(`Outer counter ${outerCounter}; pinionRotationAngle: ${pinionRotationAngle}; maxPinionRotationAngle: ${maxPinionRotationAngle}`); + outerCounter++; + + var searchingUp = true; + var innerCounter = 0; + while(innerCounter < 20) { + //console.log(` inner counter ${innerCounter}`); + innerCounter++; + pinionRotationAngle = previousPinionRotationAngle + angleStepSize; + rotatedCutter = this.createRotatedCutter(zeroedHalfCutterPath, pinionRotationAngle, gearRatio); + + // We look at the start and end point of each cutter path. We dtermine rotation angles so that the squared distance between + // consecutive start and end points is below an accuracy threshold. + var squaredDistanceStartPoint = squaredDistance(previousRotatedCutter[0], rotatedCutter[0]); + var squaredDistanceEndPoint = squaredDistance(previousRotatedCutter[cutterEndpointIndex], rotatedCutter[cutterEndpointIndex]); + var actualSquaredDistance = Math.max(squaredDistanceStartPoint, squaredDistanceEndPoint); + if (actualSquaredDistance < desiredSquaredDistance / 10) { + if (!searchingUp) { + // We crossed from too big an angle to one that is small enough. + break; + } else { + angleStepSize = 2 * angleStepSize; + } + } else if (squaredDistance < desiredSquaredDistance) { + // new angle is okay + break; + } else { + // squared distance is too large + searchingUp = false; + angleStepSize = 0.5 * angleStepSize; + } + } + cutterPaths.push(rotatedCutter); + cutterPaths.unshift(this.createRotatedCutter(zeroedHalfCutterPath, -pinionRotationAngle, gearRatio)); + } + + console.log(`Cutter paths count: ${cutterPaths.length}`); + return cutterPaths; + } + + createRotatedCutter(zeroedHalfCutterPath, pinionRotationAngle, gearRatio) { + // Rotating the pinion by an angle will also roll it along the inner side of the outer ring gear. + // First rotate the pinion. + // This results in the center of the pinion to be rotated around the center of the ring gear. + var pinionCenterRayAngle = -pinionRotationAngle / gearRatio; + + var transformedCutterPath = rotatePointsAroundCenter(zeroedHalfCutterPath, ORIGIN, pinionCenterRayAngle); + var rotatedPinionCenter = rotatePointAroundCenter(this.pinion.center, ORIGIN, pinionCenterRayAngle); + + // The pinion turns into the opposite direction. + var rotatedCutter = rotatePointsAroundCenter(transformedCutterPath, rotatedPinionCenter, pinionRotationAngle); + // Close the half tooth profile. + rotatedCutter.push(ORIGIN); + return rotatedCutter; + } + + createZeroedHalfToothCutterPath() { + var enlargedPinionHalfToothPath = this.createEnlargedPinionHalfToothPath(); + + // Rotate so that the half tooth points to 3 o'clock. Then shift so that it mashes with the internal gear. + var rotatedHalfToothPath = rotatePointsAroundCenter(enlargedPinionHalfToothPath, ORIGIN, -Math.PI / 2 - this.pinion.angleToothToTooth / 2); + return createTranslatedPath(rotatedHalfToothPath, this.pinion.center.X, 0).reverse(); + } + + createEnlargedPinionHalfToothPath() { + // To cut the internal gear teeth, the actual pinion comes close but we need to enlarge it to properly cater for clearance and backlash + var enlargedPinion = new RegularGear( + this.gearSet, + this.pinion.toothCount, + /* centerHoleDiameter = */ 0, + /* clearance = */ 0, + -this.pinion.backlash, + this.pinion.profileShift, + /* addendumExtension = */ this.pinion.clearance + ); + + return enlargedPinion.createHalfToothPath(); + //return enlargedPinion.createToothPath(); + } + } + + class Rack { + constructor(gearSet, clearance, backlash, profileShift) { + this.gearSet = gearSet; + this.clearance = clearance; + this.backlash = backlash; + this.profileShift = profileShift; + + this.addendum = 1 / this.gearSet.diametralPitch; + } + + update() { + this.rackShape = this.createRackShape(); + + var minX = this.rackShape[this.rackShape.length - 1].X; + var maxX = this.rackShape[1].X; + this.topLeft = addVectors(createPoint(minX, this.gearSet.diametralPitch / 2), this.center); + this.lowerRight = addVectors(createPoint(maxX, -this.gearSet.diametralPitch / 2), this.center); + } + + createRackShape() { + var rackToothTemplate = this.createRackTooth(); + + // The template is a tooth pointing right and centered on the x-axis. + // We build the rack by starting with the template and then create an equal number + // of teeth above and below. + + var rackShape = []; + var halfCount = Math.floor(this.connectedGear.outerRadius / this.gearSet.circularPitch); + for (var i = -halfCount; i < halfCount + 1; i++) { + rackToothTemplate.forEach(point => { + var deltaY = -i * this.gearSet.circularPitch; + rackShape.push(createTranslatedPoint(point, 0, deltaY)); + }); + } + + // create a bar backing the teeth. + var width = 0.5 * this.gearSet.circularPitch; + var lowerLeftBacking = createTranslatedPoint(rackShape[rackShape.length - 1], -width, 0); + var upperLeftBacking = createTranslatedPoint(rackShape[0], -width, 0); + rackShape.push(lowerLeftBacking); + rackShape.push(upperLeftBacking); + + return rackShape; + //return rackToothTemplate; + } + + createRackTooth() { + // we create a trapezoidal cutter as described at http://lcamtuf.coredump.cx/gcnc/ch6/ under the section 'Putting it all together' + var toothWidth = this.gearSet.circularPitch / 2; + + //console.log("toothWidth: " + toothWidth); + //console.log("addendum: " + this.addendum); + //console.log("shiftedAddendum: " + this.shiftedAddendum); + //console.log("clearance: " + this.clearance); + + var toothDepth = this.addendum + this.clearance; + //console.log("toothDepth: " + toothDepth); + + var cosPressureAngle = Math.cos(this.gearSet.pressureAngle * Math.PI / 180); + var tanPressureAngle = Math.tan(this.gearSet.pressureAngle * Math.PI / 180); + + // If a positive backlash is defined then we narrow the trapezoid accordingly. + // Each side of the tooth needs to narrowed by a fourth of the backlash (perpendiculr tooth faces). + var dx = this.backlash / 4 / cosPressureAngle; + console.log("backlash: " + this.backlash); + console.log("dx: " + dx); + console.log("profileShift: " + this.profileShift); + + // Create the tooth pointing right. + var profileShiftOffset = this.profileShift * this.addendum; + + var upperLeftCorner = createPoint(-toothDepth + profileShiftOffset, toothWidth / 2 - dx + tanPressureAngle * toothDepth); + var upperRightCorner = createPoint(this.addendum + profileShiftOffset, toothWidth / 2 - dx - tanPressureAngle * this.addendum); + var lowerRightCorner = createPoint(upperRightCorner.X, -upperRightCorner.Y); + var lowerLeftCorner = createPoint(upperLeftCorner.X, -upperLeftCorner.Y); + + return [upperLeftCorner, upperRightCorner, lowerRightCorner, lowerLeftCorner]; + } + + createGraphics(parent, crossMarkerLength) { + var gearGroup = parent.group(); + var helperGroup = gearGroup.group(); + helperGroup.stroke(helperLinesStyle).fill('none'); + + // Draw pitch 'line'. + helperGroup.line(this.center.X, 2 * this.gearSet.circularPitch, this.center.X, -2 * this.gearSet.circularPitch); + + var regularGroup = gearGroup.group(); + regularGroup.stroke(regularLinesStyle).fill('none'); + + insertSvgPath(regularGroup, this.rackShape, /*isClosed=*/ true); + + gearGroup.move(this.center.X, this.center.Y); + } + } + + function createPoint(x, y) { + return { X: x, Y: y}; + } + + function clonePoint(point) { + return { X: point.X, Y: point.Y}; + } + + function createTranslatedPoint(point, dx, dy) { + return { X: point.X + dx, Y: point.Y + dy}; + } + + function createTranslatedPath(path, dx, dy) { + return path.map(point => createTranslatedPoint(point, dx, dy)); + } + + // Translate point p by vector v + function translatePoint(p, v) { + return { X: p.X + v.X, Y: p.Y + v.Y}; + } + + function translatePoints(points, v) { + return points.map(point => translatePoint(point, v)); + } + + // Squared distance between two 2d points. + function squaredDistance(a, b) { + return square(a.X - b.X) + square(a.Y - b.Y); + } + + // Distance between two 2d points. + function distance(a, b) { + return Math.sqrt(squaredDistance(a, b)); + } + + function squaredLenth(vector) { + return square(vector.X) + square(vector.Y) + } + + function length(vector) { + return Math.sqrt(squaredLenth(vector)); + } + + function square(x) { return x * x } + + function addVectors(v1, v2) { return { X: v1.X + v2.X, Y: v1.Y + v2.Y}; } + function subtractVectors(v1, v2) { return { X: v1.X - v2.X, Y: v1.Y - v2.Y}; } + function multiplyVector(a, v) { return { X: a * v.X, Y: a * v.Y}; } + + function radToDegree(angle) { + return angle / Math.PI * 180; + } + + function degreeToRad(angle) { + return angle * Math.PI / 180; + } + + function rotatePointAroundCenter(point, center, angle) { + var cosAngle = Math.cos(angle); + var sinAngle = Math.sin(angle); + return _rotatePointAroundCenter(point, center, cosAngle, sinAngle); + } + + function _rotatePointAroundCenter(point, center, cosAngle, sinAngle) { + // Move so that center ends up at the origin + var movedPoint = {X: point.X - center.X, Y: point.Y - center.Y}; + var rotated = { + X: movedPoint.X * cosAngle - movedPoint.Y * sinAngle, + Y: movedPoint.X * sinAngle + movedPoint.Y * cosAngle, + }; + // Undo the move + return {X: rotated.X + center.X, Y: rotated.Y + center.Y}; + } + + function rotatePointsAroundCenter(points, center, angle) { + var cosAngle = Math.cos(angle); + var sinAngle = Math.sin(angle); + + return points.map(point => _rotatePointAroundCenter(point, center, cosAngle, sinAngle)); + } + + function drawCircles(parent, points, radius) { + points.forEach(point => drawCircle(parent, point, radius)); + } + + function drawCircle(parent, point, radius, strokeStyle) { + parent.circle(2 * radius).cx(point.X).cy(point.Y); + } + + function drawCrosses(parent, points, length) { + points.forEach(point => drawCross(parent, point, length)); + } + + function drawCross(parent, point, length) { + var halfLength = length / 2; + parent.line(point.X, point.Y - halfLength, point.X, point.Y + halfLength); //.stroke(markerLinesStyle); + parent.line(point.X - halfLength, point.Y, point.X + halfLength, point.Y); // .stroke(markerLinesStyle); + } + + function createSvgPoint(point) { + return {x: point.X, y: point.Y}; + } + + // Converts array of {X:..., Y:...} points into an SVG paths. + function insertSvgPath(group, points, isClosed = true) { + var svgPath = group.path(); + for (var i = 0; i < points.length; i++) { + var svgPoint = createSvgPoint(points[i]); + if (i == 0) { + svgPath.M(svgPoint); + } else { + svgPath.L(svgPoint); + } + } + if (isClosed) { + svgPath.Z(); + } + } + + function addLineSegmentsToPath(svgPath, points, moveToFirst = false) { + for (var i = 0; i < points.length; i++) { + var svgPoint = createSvgPoint(points[i]); + if (i == 0 && moveToFirst) { + svgPath.M(svgPoint); + } else { + svgPath.L(svgPoint); + } + } + } +} +#endif \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/View3D/Object3DControlsLayer.cs b/MatterControlLib/PartPreviewWindow/View3D/Object3DControlsLayer.cs index 29b670625..b28b10c4f 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/Object3DControlsLayer.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/Object3DControlsLayer.cs @@ -1090,7 +1090,9 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private void DrawObject3DControlVolumes(DrawEventArgs e) { - foreach (var item in this.Object3DControls) + var currentControls = this.Object3DControls.ToArray(); + + foreach (var item in currentControls) { item.Visible = !SuppressObject3DControls; } @@ -1103,7 +1105,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow // draw on top of anything that is already drawn GL.Disable(EnableCap.DepthTest); - foreach (var object3DControl in this.Object3DControls) + foreach (var object3DControl in currentControls) { if (object3DControl.DrawOnTop) { @@ -1115,7 +1117,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow GL.Enable(EnableCap.DepthTest); // Draw again setting the depth buffer and ensuring that all the interaction objects are sorted as well as we can - foreach (var object3DVolume in this.Object3DControls) + foreach (var object3DVolume in currentControls) { object3DVolume.Draw(new DrawGlContentEventArgs(true, e)); } diff --git a/MatterControlLib/SetupWizard/HelpTreePanel.cs b/MatterControlLib/SetupWizard/HelpTreePanel.cs index 617f07162..d3acaa884 100644 --- a/MatterControlLib/SetupWizard/HelpTreePanel.cs +++ b/MatterControlLib/SetupWizard/HelpTreePanel.cs @@ -46,11 +46,12 @@ namespace MatterHackers.MatterControl { public class HelpTreePanel : SearchableTreePanel { - private string guideKey = null; + private readonly string guideKey = null; public HelpTreePanel(ThemeConfig theme, string guideKey = null) : base(theme) { + this.guideKey = guideKey; horizontalSplitter.Panel1.BackgroundColor = Color.Black.WithAlpha(12); var toolbar = new Toolbar(theme.TabbarPadding) @@ -174,10 +175,10 @@ namespace MatterHackers.MatterControl AddContent(keys, "Keys".Localize(), true, true); AddContent(actions, "Action".Localize(), false, true); - foreach (var keyAction in keyActions) + foreach (var (key, action) in keyActions) { - AddContent(keys, keyAction.key, true, false); - AddContent(actions, keyAction.action, false, false); + AddContent(keys, key, true, false); + AddContent(actions, action, false, false); } // center the vertical bar in the view by adding margin to the small side @@ -233,10 +234,10 @@ namespace MatterHackers.MatterControl AddContent(mouseKeys, "Mouse".Localize(), true, true); AddContent(mouseActions, "Action".Localize(), false, true); - foreach (var keyAction in mouseKeyActions) + foreach (var (key, action) in mouseKeyActions) { - AddContent(mouseKeys, keyAction.key, true, false); - AddContent(mouseActions, keyAction.action, false, false); + AddContent(mouseKeys, key, true, false); + AddContent(mouseActions, action, false, false); } // center the vertical bar in the view by adding margin to the small side @@ -360,8 +361,7 @@ namespace MatterHackers.MatterControl private TreeNode initialSelection = null; private TreeNode rootNode; - private Dictionary nodesByPath = new Dictionary(); - private IEnumerable searchResults; + private readonly Dictionary nodesByPath = new Dictionary(); private HashSet searchHits; private TreeNode ProcessTree(HelpArticle container) diff --git a/Submodules/MatterSlice b/Submodules/MatterSlice index 46b47e5e2..524c83a15 160000 --- a/Submodules/MatterSlice +++ b/Submodules/MatterSlice @@ -1 +1 @@ -Subproject commit 46b47e5e26d806fc4a23d098fa7cab637bf40e47 +Subproject commit 524c83a15f0e0926aa1eed676eb1a97946c62231 diff --git a/Submodules/agg-sharp b/Submodules/agg-sharp index f4ce5c663..a7e68b55a 160000 --- a/Submodules/agg-sharp +++ b/Submodules/agg-sharp @@ -1 +1 @@ -Subproject commit f4ce5c663a639111503d89fa1e4812ee6e944a1d +Subproject commit a7e68b55a7d4aaabadf9a1b9e7abd06bade60ab0