diff --git a/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs b/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs index 68ed398cb..e1d4230c0 100644 --- a/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs +++ b/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs @@ -132,28 +132,6 @@ namespace MatterHackers.MatterControl.DesignTools.Operations return objectToTranslate; } - private static VertexStorage CombinePaths(IVertexSource a, IVertexSource b, ClipType clipType) - { - List> aPolys = a.CreatePolygons(); - List> bPolys = b.CreatePolygons(); - - Clipper clipper = new Clipper(); - - clipper.AddPaths(aPolys, PolyType.ptSubject, true); - clipper.AddPaths(bPolys, PolyType.ptClip, true); - - List> intersectedPolys = new List>(); - clipper.Execute(clipType, intersectedPolys); - - Clipper.CleanPolygons(intersectedPolys); - - VertexStorage output = intersectedPolys.CreateVertexStorage(); - - output.Add(0, 0, ShapePath.FlagsAndCommand.Stop); - - return output; - } - public static Affine GetCenteringTransformExpandedToRadius(this IVertexSource vertexSource, double radius) { var circle = SmallestEnclosingCircle.MakeCircle(vertexSource.Vertices().Select((v) => new Vector2(v.position.X, v.position.Y))); @@ -281,16 +259,6 @@ namespace MatterHackers.MatterControl.DesignTools.Operations }; } - public static IVertexSource Minus(this IVertexSource a, IVertexSource b) - { - return CombinePaths(a, b, ClipType.ctDifference); - } - - public static IVertexSource Plus(this IVertexSource a, IVertexSource b) - { - return CombinePaths(a, b, ClipType.ctUnion); - } - public static double XSize(this IObject3D item) { return item.GetAxisAlignedBoundingBox().XSize; diff --git a/MatterControlLib/DesignTools/Operations/OperationSourceObject3D.cs b/MatterControlLib/DesignTools/Operations/OperationSourceObject3D.cs index c235cfc25..a2d20570c 100644 --- a/MatterControlLib/DesignTools/Operations/OperationSourceObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/OperationSourceObject3D.cs @@ -243,6 +243,12 @@ namespace MatterHackers.MatterControl.DesignTools.Operations } // and select this + var rootItem = this.Ancestors().Where(i => scene.Children.Contains(i)).FirstOrDefault(); + if (rootItem != null) + { + scene.SelectedItem = rootItem; + } + scene.SelectedItem = this; this.Invalidate(InvalidateType.Children); diff --git a/MatterControlLib/DesignTools/Operations/TransformWrapperObject3D.cs b/MatterControlLib/DesignTools/Operations/TransformWrapperObject3D.cs index 4c374293f..64738333e 100644 --- a/MatterControlLib/DesignTools/Operations/TransformWrapperObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/TransformWrapperObject3D.cs @@ -99,6 +99,7 @@ namespace MatterHackers.MatterControl.DesignTools.Operations list.AddRange(ItemWithTransform.Children); }); } + Invalidate(InvalidateType.Children); } diff --git a/MatterControlLib/DesignTools/Primitives/Gear2D.cs b/MatterControlLib/DesignTools/Primitives/Gear2D.cs new file mode 100644 index 000000000..c7bbe785d --- /dev/null +++ b/MatterControlLib/DesignTools/Primitives/Gear2D.cs @@ -0,0 +1,580 @@ +/* +Involute Spur Gear Builder (c) 2014 Dr. Rainer Hessmer +ported to C# 2019 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 System; +using System.Collections.Generic; +using ClipperLib; +using MatterHackers.Agg; +using MatterHackers.Agg.Transform; +using MatterHackers.Agg.VertexSource; +using MatterHackers.DataConverters2D; +using MatterHackers.VectorMath; + +namespace MatterHackers.MatterControl.DesignTools +{ + // Involute Spur Gear Builder + // For calculating and drawing involute spur gears. + // As an improvement over the majority of other freely available scripts and utilities it fully accounts for undercuts. + // For additional information please head over to: + // http://www.hessmer.org/blog/2014/01/01/online-involute-spur-gear-builder part 1 + // http://www.hessmer.org/blog/2015/07/13/online-involute-spur-gear-builder-part-2/ part 2 + // The implementation is inspired by the subtractive process that Michal Zalewski's describes in + // http://lcamtuf.coredump.cx/gcnc/ch6/#6.2 part six of his excellent + // http://lcamtuf.coredump.cx/gcnc/ Guerrilla guide to CNC machining, mold making, and resin casting + public class Gear2D : VertexSourceLegacySupport + { + private double _circularPitch = 8; + + /// + /// Gets or sets distance from one face of a tooth to the corresponding face of an adjacent tooth on the same gear, measured along the pitch circle. + /// + public double CircularPitch + { + get => _circularPitch; + set + { + _circularPitch = value; + CalculateDependants(); + } + } + + private double diametralPitch; + private double addendum; + private double clearance = .05; + + // Most common stock gears have a 20° pressure angle, with 14½° and 25° pressure angle gears being much less + // common. Increasing the pressure angle increases the width of the base of the gear tooth, leading to greater strength and load carrying capacity. Decreasing + // the pressure angle provides lower backlash, smoother operation and less sensitivity to manufacturing errors. (reference: http://en.wikipedia.org/wiki/Involute_gear) + private double pressureAngle = 20; + + private double backlash = .05; + private double profileShift = 0; + private double shiftedAddendum; + private double outerRadius; + private double angleToothToTooth; + + private int _toothCount = 30; + public int ToothCount + { + get => _toothCount; + + set + { + _toothCount = value; + CalculateDependants(); + } + } + + private GearType gearType = GearType.External; + private int stepsPerToothAngle = 10; + private double pitchDiameter; + private double pitchRadius; + private Vector2 center = Vector2.Zero; + private Gear2D connectedGear; + private double centerHoleDiameter = 4; + + public enum GearType + { + External, + Internal, + Rack + } + + public Gear2D() + { + CalculateDependants(); + } + + private void CalculateDependants() + { + // convert circular pitch to diametral pitch + this.diametralPitch = Math.PI / this.CircularPitch; // Ratio of the number of teeth to the pitch diameter + // this.circularPitch = Math.PI / this.diametralPitch; + + this.center = Vector2.Zero; // center of the gear + // this.angle = 0; // angle in degrees of the complete gear (changes during rotation animation) + + // Pitch diameter: Diameter of pitch circle. + this.pitchDiameter = this.ToothCount / this.diametralPitch; + this.pitchRadius = this.pitchDiameter / 2; + + // Addendum: Radial distance from pitch circle to outside circle. + this.addendum = 1 / this.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.angleToothToTooth = 360.0 / this.ToothCount; + } + + public override IEnumerable Vertices() + { + IVertexSource shape; + // shape = CreateRackShape(); + shape = CreateRegularGearShape(); + // shape = CreateInternalGearShape(); + // shape = CreateSingleTooth().tooth; + // shape = CreateInternalGearShape(); + + return shape.Vertices(); + } + + private IVertexSource CreateRackShape() + { + IVertexSource rack = new VertexStorage(); + + // we draw one tooth in the middle and then five on either side + for (var i = 0; i < ToothCount; i++) + { + var tooth = this.CreateRackTooth(); + tooth = tooth.Translate(0, (0.5 + -ToothCount / 2 + i) * this.CircularPitch); + rack = rack.Union(tooth); + } + + // creating the bar backing the teeth + var rightX = -(this.addendum + this.clearance); + var width = 4 * this.addendum; + var halfHeight = ToothCount * this.CircularPitch / 2; + var bar = new RoundedRect(rightX - width, -halfHeight, rightX, halfHeight, 0); + + var rackFinal = rack.Union(bar) as VertexStorage; + rackFinal.Translate(this.addendum * this.profileShift, 0); + return rackFinal; + } + + private IVertexSource CreateRegularGearShape() + { + var tooth = this.CreateSingleTooth(); + + // we could now take the tooth cutout, rotate it tooth count times and union the various slices together into a complete gear. + // However, the union operations become more and more complex as the complete gear is built up. + // So instead we capture the outer path of the tooth and concatenate rotated versions of this path into a complete outer gear path. + // Concatenating paths is inexpensive resulting in significantly faster execution. + var outlinePaths = tooth.tooth; + + // first we need to find the corner that sits at the center + for (var i = 1; i < this.ToothCount; i++) + { + var angle = i * this.angleToothToTooth; + var roatationMatrix = Affine.NewRotation(MathHelper.DegreesToRadians(angle)); + var rotatedCorner = new VertexSourceApplyTransform(tooth.tooth, roatationMatrix); + outlinePaths = new CombinePaths(outlinePaths, rotatedCorner); + } + + var gearShape = tooth.wheel.Subtract(outlinePaths); + + if (this.centerHoleDiameter > 0) + { + var radius = this.centerHoleDiameter / 2; + var centerhole = new Ellipse(0, 0, radius, radius) + { + ResolutionScale = 10 + }; + gearShape = gearShape.Subtract(centerhole) as VertexStorage; + } + + return gearShape;// .RotateZDegrees(-90); + } + + private IVertexSource CreateInternalGearShape() + { + var singleTooth = this._createInternalToothProfile(); + // return singleTooth; + + var outlinePaths = singleTooth; + var corners = outlinePaths as VertexStorage; + + // first we need to find the corner that sits at the center + var centerCornerIndex = 0; + var radius = this.pitchRadius + (1 + this.profileShift) * this.addendum + this.clearance; + + var delta = 0.0000001; + for (var i = 0; i < corners.Count; i++) + { + var corner = corners[i]; + if (corner.Y < delta && (corner.X + radius) < delta) + { + centerCornerIndex = i; + break; + } + } + + var outerCorners = new VertexStorage(); + for (var i = 2; i < corners.Count - 2; i++) + { + var corner = corners[(i + centerCornerIndex) % corners.Count]; + outerCorners.add(corner.position); + } + + var reversedOuterCorners = new VertexStorage(); + foreach (var vertex in new ReversePath(outerCorners).Vertices()) + { + reversedOuterCorners.add(vertex.position); + } + outerCorners = reversedOuterCorners; + + var cornersCount = outerCorners.Count; + + for (var i = 1; i < this.ToothCount; i++) + { + var angle = i * this.angleToothToTooth; + var roatationMatrix = Affine.NewRotation(MathHelper.DegreesToRadians(angle)); + for (var j = 0; j < cornersCount; j++) + { + var rotatedCorner = roatationMatrix.Transform(outerCorners[j].position); + outerCorners.add(rotatedCorner); + } + } + + outerCorners = this._smoothConcaveCorners(outerCorners) as VertexStorage; + + var innerRadius = this.pitchRadius + (1 - this.profileShift) * this.addendum + this.clearance; + var outerRadius = innerRadius + 4 * this.addendum; + var outerCircle = new Ellipse(this.center, outerRadius, outerRadius); + // return outerCircle; + + // return gearCutout; + return outerCircle.Subtract(outerCorners); + } + + private IVertexSource CreateRackTooth() + { + var toothWidth = this.CircularPitch / 2; + var toothDepth = this.addendum + this.clearance; + + var sinPressureAngle = Math.Sin(this.pressureAngle * Math.PI / 180); + var cosPressureAngle = Math.Cos(this.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 (vertical to cutter faces). + var dx = this.backlash / 4 / cosPressureAngle; + + var leftDepth = this.addendum + this.clearance; + + var upperLeftCorner = new Vector2(-leftDepth, toothWidth / 2 - dx + (this.addendum + this.clearance) * sinPressureAngle); + var upperRightCorner = new Vector2(this.addendum, toothWidth / 2 - dx - this.addendum * sinPressureAngle); + var lowerRightCorner = new Vector2(upperRightCorner[0], -upperRightCorner[1]); + var lowerLeftCorner = new Vector2(upperLeftCorner[0], -upperLeftCorner[1]); + + var tooth = new VertexStorage(); + tooth.MoveTo(upperLeftCorner); + tooth.LineTo(upperRightCorner); + tooth.LineTo(lowerRightCorner); + tooth.LineTo(lowerLeftCorner); + + return tooth; + } + + private (IVertexSource tooth, IVertexSource wheel) CreateSingleTooth() + { + // create outer circle sector covering one tooth + var toothSectorPath = new Arc(Vector2.Zero, new Vector2(this.outerRadius, this.outerRadius), MathHelper.DegreesToRadians(90), MathHelper.DegreesToRadians(90 - this.angleToothToTooth)) + { + ResolutionScale = 10 + }; + + var toothCutOut = CreateToothCutout(); + + return (toothCutOut, toothSectorPath); + } + + private IVertexSource CreateToothCutout() + { + var angleToothToTooth = 360.0 / this.ToothCount; + var angleStepSize = this.angleToothToTooth / this.stepsPerToothAngle; + + IVertexSource toothCutout = new VertexStorage(); + + var toothCutterShape = this.CreateToothCutter(); + var bounds = toothCutterShape.GetBounds(); + var lowerLeftCorner = new Vector2(bounds.Left, bounds.Bottom); + + // 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 = new Vector2(angle * Math.PI / 180 * this.pitchRadius, 0); + + var movedLowerLeftCorner = lowerLeftCorner + xTranslation; + movedLowerLeftCorner = Vector2.Rotate(movedLowerLeftCorner, MathHelper.DegreesToRadians(angle)); + + if (movedLowerLeftCorner.Length > this.outerRadius) + { + // the cutter is now completely outside the gear and no longer influences the shape of the gear tooth + break; + } + + // we move in both directions + var movedToothCutterShape = toothCutterShape.Translate(xTranslation); + movedToothCutterShape = movedToothCutterShape.RotateZDegrees(angle); + toothCutout = toothCutout.Union(movedToothCutterShape); + + if (xTranslation[0] > 0) + { + movedToothCutterShape = toothCutterShape.Translate(new Vector2(-xTranslation[0], xTranslation[1])); + movedToothCutterShape = movedToothCutterShape.RotateZDegrees(-angle); + toothCutout = toothCutout.Union(movedToothCutterShape); + } + + stepCounter++; + } + + toothCutout = this._smoothConcaveCorners(toothCutout); + + return toothCutout.RotateZDegrees(-this.angleToothToTooth / 2); + } + + private IVertexSource 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.CircularPitch / 2; + + var cutterDepth = this.addendum + this.clearance; + var cutterOutsideLength = 3 * this.addendum; + + var sinPressureAngle = Math.Sin(this.pressureAngle * Math.PI / 180.0); + var cosPressureAngle = Math.Cos(this.pressureAngle * Math.PI / 180.0); + + // 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 (vertical to cutter faces). + var dx = this.backlash / 2 / cosPressureAngle; + + var lowerRightCorner = new Vector2(toothWidth / 2 + dx - cutterDepth * sinPressureAngle, this.pitchRadius + this.profileShift * this.addendum - cutterDepth); + var upperRightCorner = new Vector2(toothWidth / 2 + dx + cutterOutsideLength * sinPressureAngle, this.pitchRadius + this.profileShift * this.addendum + cutterOutsideLength); + var upperLeftCorner = new Vector2(-upperRightCorner[0], upperRightCorner[1]); + var lowerLeftCorner = new Vector2(-lowerRightCorner[0], lowerRightCorner[1]); + + var cutterPath = new VertexStorage(); + cutterPath.MoveTo(lowerLeftCorner); + cutterPath.LineTo(upperLeftCorner); + cutterPath.LineTo(upperRightCorner); + cutterPath.LineTo(lowerRightCorner); + + return cutterPath; + } + + private IVertexSource _smoothConvexCorners(IVertexSource corners) + { + // removes single convex corners located between concave corners + return this._smoothCorners(corners, true); // removeSingleConvex + } + + private IVertexSource _createInternalToothCutter() + { + // To cut the internal gear teeth, the actual pinion comes close but we need to enlarge it so properly caters for clearance and backlash + var pinion = this.connectedGear; + + var enlargedPinion = new Gear2D() + { + CircularPitch = pinion.CircularPitch, + // pressureAngle: pinion.pressureAngle, + // clearance: -pinion.clearance, + // backlash: -pinion.backlash, + // toothCount: pinion.toothCount, + // centerHoleDiameter: 0, + // profileShift: pinion.profileShift, + // stepsPerToothAngle: pinion.stepsPerToothAngle + }; + + var tooth = enlargedPinion.CreateSingleTooth(); + return tooth.tooth.RotateZDegrees(90 + 180 / enlargedPinion.ToothCount); // we need a tooth pointing to the left + } + + private IVertexSource _createInternalToothProfile() + { + var radius = this.pitchRadius + (1 - this.profileShift) * this.addendum + this.clearance; + var angleToothToTooth = 360 / this.ToothCount; + var sin = Math.Sin(angleToothToTooth / 2 * Math.PI / 180); + var cos = Math.Cos(angleToothToTooth / 2 * Math.PI / 180); + + var fullSector = new VertexStorage(); + + fullSector.MoveTo(0, 0); + fullSector.LineTo(-(radius * cos), radius * sin); + fullSector.LineTo(-radius, 0); + fullSector.LineTo(-(radius * cos), -radius * sin); + + var innerRadius = radius - (2 * this.addendum + this.clearance); + var innerCircle = new Ellipse(this.center, innerRadius) + { + ResolutionScale = 10 + }; + + var sector = fullSector.Subtract(innerCircle); + + var cutterTemplate = this._createInternalToothCutter(); + + var pinion = this.connectedGear; + var stepsPerTooth = this.stepsPerToothAngle; + var angleStepSize = angleToothToTooth / stepsPerTooth; + var toothShape = sector; + var cutter = cutterTemplate.Translate(-this.pitchRadius + this.connectedGear.pitchRadius, 0); + toothShape = toothShape.Subtract(cutter); + + for (var i = 1; i < stepsPerTooth; i++) + { + var pinionRotationAngle = i * angleStepSize; + var pinionCenterRayAngle = -pinionRotationAngle * pinion.ToothCount / this.ToothCount; + + // var cutter = cutterTemplate; + cutter = cutterTemplate.RotateZDegrees(pinionRotationAngle); + cutter = cutter.Translate(-this.pitchRadius + this.connectedGear.pitchRadius, 0); + cutter = cutter.RotateZDegrees(pinionCenterRayAngle); + + toothShape = toothShape.Subtract(cutter); + + cutter = cutterTemplate.RotateZDegrees(-pinionRotationAngle); + cutter = cutter.Translate(-this.pitchRadius + this.connectedGear.pitchRadius, 0); + cutter = cutter.RotateZDegrees(-pinionCenterRayAngle); + + toothShape = toothShape.Subtract(cutter); + } + + return toothShape; + } + + + private IVertexSource _smoothConcaveCorners(IVertexSource corners) + { + // removes single concave corners located between convex corners + return this._smoothCorners(corners, false); // removeSingleConvex + } + + private IVertexSource _smoothCorners(IVertexSource corners_in, bool removeSingleConvex) + { + var corners = corners_in as VertexStorage; + var isConvex = new List(); + var previousCorner = corners[corners.Count - 1]; + var currentCorner = corners[0]; + for (var i = 0; i < corners.Count; i++) + { + var nextCorner = corners[(i + 1) % corners.Count]; + + var v1 = previousCorner.position - currentCorner.position; + var v2 = nextCorner.position - currentCorner.position; + var crossProduct = v1.Cross(v2); + isConvex.Add(crossProduct < 0); + + previousCorner = currentCorner; + currentCorner = nextCorner; + } + + // we want to remove any concave corners that are located between two convex corners + var cleanedUpCorners = new VertexStorage(); + var previousIndex = corners.Count - 1; + var currentIndex = 0; + for (var i = 0; i < corners.Count; i++) + { + var corner = corners[currentIndex]; + var nextIndex = (i + 1) % corners.Count; + + var isSingleConcave = !isConvex[currentIndex] && isConvex[previousIndex] && isConvex[nextIndex]; + var isSingleConvex = isConvex[currentIndex] && !isConvex[previousIndex] && !isConvex[nextIndex]; + + previousIndex = currentIndex; + currentIndex = nextIndex; + + if (removeSingleConvex && isSingleConvex) + { + continue; + } + + if (!removeSingleConvex && isSingleConcave) + { + continue; + } + + cleanedUpCorners.Add(corner.X, corner.Y, corner.command); + } + + return cleanedUpCorners; + } + } +} + +public static class Extensions +{ + public static IVertexSource Subtract(this IVertexSource a, IVertexSource b) + { + return a.Minus(b); + } + + public static IVertexSource Union(this IVertexSource a, IVertexSource b) + { + return a.Plus(b); + } + + public static IVertexSource RotateZDegrees(this IVertexSource a, double angle) + { + return new VertexSourceApplyTransform(a, Affine.NewRotation(MathHelper.DegreesToRadians(angle))); + } + + public static IVertexSource Translate(this IVertexSource a, Vector2 delta) + { + return new VertexSourceApplyTransform(a, Affine.NewTranslation(delta)); + } + + public static IVertexSource Minus(this IVertexSource a, IVertexSource b) + { + return CombinePaths(a, b, ClipType.ctDifference); + } + + public static IVertexSource Plus(this IVertexSource a, IVertexSource b) + { + return CombinePaths(a, b, ClipType.ctUnion); + } + + private static VertexStorage CombinePaths(IVertexSource a, IVertexSource b, ClipType clipType) + { + List> aPolys = a.CreatePolygons(); + List> bPolys = b.CreatePolygons(); + + var clipper = new Clipper(); + + clipper.AddPaths(aPolys, PolyType.ptSubject, true); + clipper.AddPaths(bPolys, PolyType.ptClip, true); + + var outputPolys = new List>(); + clipper.Execute(clipType, outputPolys); + + Clipper.CleanPolygons(outputPolys); + + VertexStorage output = outputPolys.CreateVertexStorage(); + + output.Add(0, 0, ShapePath.FlagsAndCommand.Stop); + + return output; + } +} \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Primitives/TextPathObject3D.cs b/MatterControlLib/DesignTools/Primitives/TextPathObject3D.cs index 0cfb17f1f..4245dcf71 100644 --- a/MatterControlLib/DesignTools/Primitives/TextPathObject3D.cs +++ b/MatterControlLib/DesignTools/Primitives/TextPathObject3D.cs @@ -85,6 +85,7 @@ namespace MatterHackers.MatterControl.DesignTools { newContainer.Children.Add(child.Clone()); } + undoBuffer.AddAndDo(new ReplaceCommand(new[] { this }, new[] { newContainer })); } diff --git a/MatterControlLib/PartPreviewWindow/MainViewWidget.cs b/MatterControlLib/PartPreviewWindow/MainViewWidget.cs index 80b5a3550..8effdbc58 100644 --- a/MatterControlLib/PartPreviewWindow/MainViewWidget.cs +++ b/MatterControlLib/PartPreviewWindow/MainViewWidget.cs @@ -38,7 +38,6 @@ using MatterHackers.Agg.Platform; using MatterHackers.Agg.UI; using MatterHackers.DataConverters3D; using MatterHackers.Localizations; -using MatterHackers.MatterControl.CustomWidgets; using MatterHackers.MatterControl.Library; using MatterHackers.MatterControl.PartPreviewWindow.PlusTab; using MatterHackers.MatterControl.PrintLibrary; diff --git a/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs b/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs index 555a11537..baf444c00 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs @@ -1620,6 +1620,12 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private void Scene_Invalidated(object sender, InvalidateArgs e) { + if (Scene.Descendants().Count() != lastSceneDescendantsCount) + { + rebuildTreePending = true; + UiThread.RunOnIdle(this.RebuildTree); + } + if (e.InvalidateType.HasFlag(InvalidateType.Children) && !rebuildTreePending) { @@ -1636,10 +1642,13 @@ namespace MatterHackers.MatterControl.PartPreviewWindow rebuildTreePending = true; UiThread.RunOnIdle(this.RebuildTree); } + Scene.SelectedItem = null; Scene.SelectedItem = lastSelectedItem; } + lastSceneDescendantsCount = Scene.Descendants().Count(); + // Invalidate widget on scene invalidate this.Invalidate(); } @@ -1704,6 +1713,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private bool assigningTreeNode; private FlowLayoutWidget treeNodeContainer; + private int lastSceneDescendantsCount; private InlineStringEdit workspaceName; public InteractiveScene Scene => sceneContext.Scene; diff --git a/Submodules/MatterSlice b/Submodules/MatterSlice index 09299400c..8551764ee 160000 --- a/Submodules/MatterSlice +++ b/Submodules/MatterSlice @@ -1 +1 @@ -Subproject commit 09299400c077dcf7ee494dd95823daaa7c156bd9 +Subproject commit 8551764ee5116ad0d479442bfcba36385bc16594 diff --git a/Submodules/agg-sharp b/Submodules/agg-sharp index 494c44d11..24360177b 160000 --- a/Submodules/agg-sharp +++ b/Submodules/agg-sharp @@ -1 +1 @@ -Subproject commit 494c44d1184e4cf30223bc8fa30348d287462349 +Subproject commit 24360177b57b10f7d3cdc6dd97e57e76e8f95ade