Create automation test for control-click in design tree view

This commit is contained in:
Eric Horch 2022-02-23 03:27:44 -07:00
parent 37751ebeb8
commit 5734368188
3 changed files with 432 additions and 19 deletions

5
.gitignore vendored
View file

@ -116,4 +116,7 @@ Backup*/
UpgradeLog*.XML
MatterControl.userprefs
.vs/
.vs/
# JetBrains Rider user configuration directory
/.idea/

View file

@ -27,13 +27,21 @@ of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the FreeBSD Project.
*/
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MatterHackers.Agg;
using MatterHackers.Agg.UI;
using MatterHackers.DataConverters3D;
using MatterHackers.GuiAutomation;
using MatterHackers.MatterControl.CustomWidgets;
using MatterHackers.MatterControl.DataStorage;
using MatterHackers.MatterControl.DesignTools.Operations;
using MatterHackers.MatterControl.PartPreviewWindow;
using MatterHackers.MatterControl.PrintQueue;
using MatterHackers.VectorMath;
using NUnit.Framework;
namespace MatterHackers.MatterControl.Tests.Automation
@ -101,6 +109,296 @@ namespace MatterHackers.MatterControl.Tests.Automation
}, overrideWidth: 1300, maxTimeToRun: 60);
}
[Test]
public static async Task ControlClickInDesignTreeView()
{
await MatterControlUtilities.RunTest((testRunner) =>
{
testRunner.OpenPartTab();
var parts = new[]
{
"Row Item Cube",
"Row Item Half Cylinder",
"Row Item Half Wedge",
"Row Item Pyramid"
};
testRunner.AddPrimitivePartsToBed(parts, multiSelect: false);
var view3D = testRunner.GetWidgetByName("View3DWidget", out _, 3) as View3DWidget;
var scene = view3D.Object3DControlLayer.Scene;
var designTree = testRunner.GetWidgetByName("DesignTree", out _, 3) as TreeView;
Assert.AreEqual(scene.Children.Count, FetchTreeNodes().Count, "Scene part count should equal tree node count");
// Open up some room in the design tree view panel for adding group and selection nodes.
var splitter = designTree.Parents<Splitter>().First();
var splitterBar = splitter.Children.Where(child => child.GetType().Name == "SplitterBar").First();
var treeNodes = FetchTreeNodes();
var cubeNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Cube").Single();
var expandBy = cubeNode.Size.Y * 4d;
testRunner.DragWidget(splitterBar, new Point2D(0, expandBy)).Drop();
//===========================================================================================//
// Verify control-click isn't broken in library view.
var moreParts = new[]
{
"Row Item Sphere",
"Row Item Wedge"
};
testRunner.AddPrimitivePartsToBed(moreParts, multiSelect: true);
var partCount = parts.Length + moreParts.Length;
Assert.IsTrue(scene.Children.Any(child => child is SelectionGroupObject3D), "Scene should have a selection child");
treeNodes = FetchTreeNodes();
Assert.IsFalse(treeNodes.Where(node => node.Tag is SelectionGroupObject3D).Any());
Assert.AreEqual(treeNodes.Count, partCount, "Design tree should show all parts");
Assert.AreEqual(
scene.Children.Sum(child =>
{
if (child is SelectionGroupObject3D selection)
{
return selection.Children.Count;
}
return 1;
}),
treeNodes.Count,
"Number of parts in scene should equal number of nodes in design view");
//===========================================================================================//
// Verify rectangle drag select on bed creates a selection group in scene only.
//
// Rotate bed to top-down view so it's easier to select parts.
var top = Matrix4X4.LookAt(Vector3.Zero, new Vector3(0, 0, -1), new Vector3(0, 1, 0));
view3D.TrackballTumbleWidget.AnimateRotation(top);
testRunner.Delay()
.ClickByName("Pyramid")
.Delay()
.RectangleSelectParts(view3D.Object3DControlLayer, new[] { "Cube", "Pyramid" })
.Delay();
Assert.AreEqual(partCount - 3, scene.Children.Count, "Scene should have {0} children after drag rectangle select", partCount - 3);
Assert.IsTrue(scene.Children.Any(child => child is SelectionGroupObject3D), "Scene should have a selection child after drag rectangle select");
Assert.AreEqual(4, scene.SelectedItem.Children.Count, "4 parts should be selected");
Assert.IsTrue(
new HashSet<string>(scene.SelectedItem.Children.Select(child => child.Name)).SetEquals(new[] { "Cube", "Half Wedge", "Half Cylinder", "Pyramid" }),
"Cube, Half Cylinder, Half Wedge, Pyramid should be selected");
treeNodes = FetchTreeNodes();
Assert.IsFalse(treeNodes.Where(node => node.Tag is SelectionGroupObject3D).Any());
Assert.AreEqual(
scene.Children.Sum(child =>
{
if (child is SelectionGroupObject3D selection)
{
return selection.Children.Count;
}
return 1;
}),
treeNodes.Count,
"Number of parts in scene should equal number of nodes in design view afte drag rectangle select");
//===========================================================================================//
// Verify shift-clicking on parts on bed creates a selection group.
testRunner
.ClickByName("Sphere")
.ClickByName("Half Cylinder")
.PressModifierKeys(AutomationRunner.ModifierKeys.Shift)
.ClickByName("Pyramid")
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Shift);
Assert.AreEqual(partCount - 1, scene.Children.Count, "Should have {0} children after selection", partCount - 1);
Assert.IsTrue(scene.Children.Any(child => child is SelectionGroupObject3D), "Selection group should be child of scene");
Assert.IsFalse(scene.Children.Any(child => child.Name == "Half Cylinder" || child.Name == "Pyramid"), "Half Cylinder and Pyramid should be removed as direct children of scene");
Assert.IsNull(designTree.SelectedNode, "Design tree shouldn't have a selected node when multiple parts are selected");
//===========================================================================================//
// Verify grouping parts creates a group.
testRunner.ClickByName("Group Button");
Assert.AreEqual(partCount - 1, scene.Children.Count, "Should have {0} parts after group", partCount - 1);
Assert.IsInstanceOf<GroupObject3D>(scene.SelectedItem, "Scene selection should be group");
Assert.IsInstanceOf<GroupObject3D>(designTree.SelectedNode.Tag, "Group should be selected in design tree");
Assert.AreSame(scene.SelectedItem, designTree.SelectedNode.Tag, "Same group object should be selected in scene and design tree");
treeNodes = FetchTreeNodes();
Assert.AreEqual(scene.Children.Count, treeNodes.Count, "Scene part count should equal tree node count after group");
Assert.IsTrue(treeNodes.Any(node => node.Tag is GroupObject3D), "Design tree should have node for group");
Assert.AreSame(designTree.SelectedNode.Tag, treeNodes.Single(node => node.Tag is GroupObject3D).Tag, "Selected node in design tree should be group node");
var groupNode = treeNodes.Where(node => node.Tag is GroupObject3D).Single();
Assert.AreEqual(2, groupNode.Nodes.Count, "Group should have 2 parts");
Assert.IsTrue(
new HashSet<string>(groupNode.Nodes.Select(node => ((IObject3D)node.Tag).Name)).SetEquals(new[] {"Half Cylinder", "Pyramid"}),
"Half Cylinder and Pyramind should be grouped");
var singleItemNodes = treeNodes
.Where(node => !(node.Tag is GroupObject3D))
.Where(node => !(node.Tag is SelectionGroupObject3D))
.ToList();
var singleItemNames = new HashSet<string>(singleItemNodes.Select(item => ((IObject3D)item.Tag).Name));
Assert.AreEqual(partCount - 2, singleItemNodes.Count, "There should be {0} single item nodes in the design tree", partCount - 2);
Assert.IsTrue(singleItemNames.SetEquals(new[] {"Cube", "Half Wedge", "Sphere", "Wedge"}), "Cube, Half Wedge, Sphere, Wedge should be single items");
//===========================================================================================//
// Verify using the design tree to create a selection group.
var halfWedgeNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Half Wedge").Single();
var sphereNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Sphere").Single();
testRunner.ClickWidget(halfWedgeNode)
.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(sphereNode)
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
Assert.AreEqual(partCount - 2, scene.Children.Count, "Should have {0} parts after selection", partCount - 2);
Assert.IsNull(designTree.SelectedNode, "Design tree shouldn't have a selected node after creating selection in design tree");
//===========================================================================================//
// Verify control-clicking a part in the group does not get added to the selection group. Only top-level nodes can be
// selected.
treeNodes = FetchTreeNodes();
groupNode = treeNodes.Where(node => node.Tag is GroupObject3D).Single();
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(groupNode.Nodes.Last())
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
Assert.AreEqual(
scene.Children.Sum(child =>
{
if (child is SelectionGroupObject3D selection)
{
return selection.Children.Count;
}
return 1;
}),
treeNodes.Count,
"Scene part count should equal design tree node count after control-click on group child");
Assert.IsInstanceOf<SelectionGroupObject3D>(scene.SelectedItem, "Selection shouldn't change after control-click on group child");
Assert.AreEqual(2, scene.SelectedItem.Children.Count, "Selection should have 2 parts after control-click on group child");
//===========================================================================================//
// Verify adding group to selection.
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(groupNode.TitleBar)
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
Assert.AreEqual(partCount - 3, scene.Children.Count, "Scene should have {0} children after control-clicking group", partCount - 3);
Assert.IsInstanceOf<SelectionGroupObject3D>(scene.SelectedItem, "Selected item should be a selection group after control-clicking on group");
Assert.AreEqual(3, scene.SelectedItem.Children.Count, "Selection should have 3 items after control-clicking on group");
Assert.IsTrue(
new HashSet<string>(scene.SelectedItem.Children.Select(child => child.Name)).SetEquals(new[] {"Half Wedge", "Sphere", "Group"}),
"Selection should have Group, Half Wedge, Sphere");
//===========================================================================================//
// Verify control-clicking on a part in the selection removes it from the selection.
treeNodes = FetchTreeNodes();
halfWedgeNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Half Wedge").Single();
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(halfWedgeNode)
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
Assert.IsInstanceOf<SelectionGroupObject3D>(scene.SelectedItem, "Selection group should exist after removing a child");
Assert.AreEqual(2, scene.SelectedItem.Children.Count, "Selection should have 2 parts after removing a child");
Assert.IsTrue(
new HashSet<string>(scene.SelectedItem.Children.Select(child => child.Name)).SetEquals(new[] {"Group", "Sphere"}),
"Group and Sphere should be in selection after removing a child");
//===========================================================================================//
// Verify control-clicking on second-to-last part in the selection removes it from the selection
// and destroys selection group.
treeNodes = FetchTreeNodes();
groupNode = treeNodes.Where(node => node.Tag is GroupObject3D).Single();
sphereNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Sphere").Single();
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(sphereNode)
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
treeNodes = FetchTreeNodes();
Assert.AreEqual(scene.Children.Count, treeNodes.Count, "Scene part count should equal design tree node count after removing penultimate child");
Assert.IsNotInstanceOf<SelectionGroupObject3D>(scene.SelectedItem, "Selection group shouldn't exist after removing penultimate child");
Assert.AreSame(groupNode.Tag, scene.SelectedItem, "Selection should be group after removing penultimate child");
//===========================================================================================//
// Verify control-clicking on a part in the group that's part of the selection doesn't change the selection.
halfWedgeNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Half Wedge").Single();
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(halfWedgeNode)
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
treeNodes = FetchTreeNodes();
sphereNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Sphere").Single();
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(sphereNode)
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
treeNodes = FetchTreeNodes();
groupNode = treeNodes.Where(node => node.Tag is GroupObject3D).Single();
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(groupNode.Nodes.Last())
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
Assert.IsInstanceOf<SelectionGroupObject3D>(scene.SelectedItem, "Selection shouldn't change after control-click on selection group child");
Assert.AreEqual(3, scene.SelectedItem.Children.Count, "Selection should have 3 parts after control-click on selection group child");
//===========================================================================================//
// Verify clicking on a top-level node that's not in the selection group unselects all the parts in the group
// and selects the part associated with the clicked node.
treeNodes = FetchTreeNodes();
var wedgeNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Wedge").Single();
testRunner.ClickWidget(wedgeNode);
Assert.AreEqual(partCount - 1, scene.Children.Count, "Should be {0} parts in the scene after selecting wedge", partCount - 1);
Assert.AreSame(scene.SelectedItem, wedgeNode.Tag, "Wedge should be selected");
Assert.IsFalse(scene.Children.Any(child => child is SelectionGroupObject3D), "Selection group should go away when another part is selected");
Assert.AreSame(scene.SelectedItem, designTree.SelectedNode.Tag, "The same part should be selected in the scene and design tree");
treeNodes = FetchTreeNodes();
wedgeNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Wedge").Single();
Assert.AreSame(designTree.SelectedNode, wedgeNode, "Wedge node should be selected in design tree");
Assert.IsFalse(treeNodes.Any(node => node.Tag is SelectionGroupObject3D), "Selection group shouldn't exist in design tree after selecting wedge");
//===========================================================================================//
// Verify that shift-clicking a part on the bed makes a selection group with a part that's been selected through
// the design tree.
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Shift)
.ClickByName("Half Wedge")
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Shift);
Assert.AreEqual(partCount - 2, scene.Children.Count, "Scene should have {0} children after selecting half wedge", partCount - 2);
Assert.IsNull(designTree.SelectedNode, "Selected node in design tree should be null after selecting half wedge");
Assert.IsInstanceOf<SelectionGroupObject3D>(scene.SelectedItem, "Should have a selection group after selecting half wedge");
Assert.IsTrue(
new HashSet<string>(scene.SelectedItem.Children.Select(child => child.Name)).SetEquals(new [] {"Wedge", "Half Wedge"}),
"Half Wedge and Wedge should be in selection");
//===========================================================================================//
// Verify that control-click on a top-level part adds to an existing selection.
treeNodes = FetchTreeNodes();
sphereNode = treeNodes.Where(node => ((IObject3D)node.Tag).Name == "Sphere").Single();
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickWidget(sphereNode)
.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
Assert.AreEqual(partCount - 3, scene.Children.Count, "Scene should have {0} children after selecting sphere", partCount - 3);
Assert.IsInstanceOf<SelectionGroupObject3D>(scene.SelectedItem, "Selection in scene should be selection group after adding sphere");
Assert.IsTrue(
new HashSet<string>(scene.SelectedItem.Children.Select(child => child.Name)).SetEquals(new [] {"Wedge", "Half Wedge", "Sphere"}),
"Half Wedge, Sphere, Wedge should be in selection");
//===========================================================================================//
// Done
return Task.CompletedTask;
// The nodes in the design tree are regenerated after certain events and must
// be fetched anew.
List<TreeNode> FetchTreeNodes() =>
designTree.Children
.Where(child => child is ScrollingArea)
.First()
.Children
.Where(child => child is FlowLayoutWidget)
.First()
.Children
.Select(child => (TreeNode)child)
.ToList();
}, overrideWidth: 1300, maxTimeToRun: 110);
}
[Test]
public async Task DesignTabFileOpperations()
{

View file

@ -39,9 +39,11 @@ using MatterHackers.Agg;
using MatterHackers.Agg.Image;
using MatterHackers.Agg.Platform;
using MatterHackers.Agg.UI;
using MatterHackers.DataConverters3D;
using MatterHackers.GuiAutomation;
using MatterHackers.MatterControl.CustomWidgets;
using MatterHackers.MatterControl.DataStorage;
using MatterHackers.MatterControl.DesignTools.Operations;
using MatterHackers.MatterControl.Library;
using MatterHackers.MatterControl.PartPreviewWindow;
using MatterHackers.MatterControl.PrinterCommunication;
@ -124,10 +126,14 @@ namespace MatterHackers.MatterControl.Tests.Automation
const string containerName = "Primitives Row Item Collection";
testRunner.NavigateToFolder(containerName);
if (multiSelect)
{
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control);
}
var partCount = 0;
foreach (var partName in partNames)
{
Keyboard.SetKeyDownState(Keys.ControlKey, multiSelect);
foreach (var result in testRunner.GetWidgetsByName(partName))
{
// Opening the primitive parts library folder causes a second set of primitive part widgets to be created.
@ -144,34 +150,35 @@ namespace MatterHackers.MatterControl.Tests.Automation
}
if (!partWidget.IsSelected)
{
testRunner.ClickWidget(partWidget);
if (multiSelect)
{
testRunner.ClickWidget(partWidget);
}
else
{
testRunner.RightClickWidget(partWidget)
.ClickByName("Add to Bed Menu Item");
}
}
partCount += 1;
break;
}
}
if (multiSelect)
{
// Release control key so additional operations work normally.
Keyboard.SetKeyDownState(Keys.ControlKey, false);
testRunner.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control)
.ClickByName("Print Library Overflow Menu")
.ClickByName("Add to Bed Menu Item");
}
testRunner.ClickByName("Print Library Overflow Menu");
var view3D = testRunner.GetWidgetByName("View3DWidget", out _) as View3DWidget;
var scene = view3D.Object3DControlLayer.Scene;
var preAddCount = scene.Children.Count;
var postAddCount = preAddCount + (multiSelect ? 1 : partCount);
testRunner.ClickByName("Add to Bed Menu Item")
// wait for the objects to be added
.WaitFor(() => scene.Children.Count == postAddCount);
// wait for the objects to be done loading
var insertionGroup = scene.Children.LastOrDefault() as InsertionGroupObject3D;
if (insertionGroup != null)
{
testRunner.WaitFor(() => scene.Children.LastOrDefault() as InsertionGroupObject3D != null, 10);
}
// wait for the objects to be added
testRunner.WaitFor(() => scene.Children.Count == postAddCount, 1);
return testRunner;
}
@ -950,6 +957,8 @@ namespace MatterHackers.MatterControl.Tests.Automation
}
UserSettings.Instance.set(UserSettingsKey.ThumbnailRenderingMode, "orthographic");
// The EULA popup throws off the tests on Linux.
UserSettings.Instance.set(UserSettingsKey.SoftwareLicenseAccepted, "true");
// GL.HardwareAvailable = false;
var config = TestAutomationConfig.Load();
@ -1483,13 +1492,116 @@ namespace MatterHackers.MatterControl.Tests.Automation
public static void SelectListItems(this AutomationRunner testRunner, params string[] widgetNames)
{
// Control click all items
Keyboard.SetKeyDownState(Keys.ControlKey, down: true);
testRunner.PressModifierKeys(AutomationRunner.ModifierKeys.Control);
foreach (var widgetName in widgetNames)
{
testRunner.ClickByName(widgetName);
}
Keyboard.SetKeyDownState(Keys.ControlKey, down: false);
testRunner.ReleaseModifierKeys(AutomationRunner.ModifierKeys.Control);
}
/// <summary>
/// Uses the drag rectangle on the bed to select parts. Assumes the bed has been rotated to a
/// bird's eye view (top down). That makes it easier to select the correct parts because the
/// drag rectangle will be parallel to the XY plane.
/// </summary>
/// <param name="testRunner">The AutomationRunner in use</param>
/// <param name="controlLayer">Object control layer from a View3DWidget</param>
/// <param name="partNames">Names of the parts to select</param>
/// <returns>The AutomationRunner</returns>
public static AutomationRunner RectangleSelectParts(this AutomationRunner testRunner, Object3DControlsLayer controlLayer, IEnumerable<string> partNames)
{
var topWindow = controlLayer.Parents<SystemWindow>().First();
var widgets = partNames
.Select(name => ResolveName(controlLayer.Scene.Children, name))
.Where(x => x.Ok)
.Select(x =>
{
var widget = testRunner.GetWidgetByName(x.Name, out var containingWindow, 1);
return new
{
Widget = widget ?? controlLayer,
ContainingWindow = widget != null ? containingWindow : topWindow,
x.Bounds
};
})
.ToList();
if (!widgets.Any())
{
return testRunner;
}
var minPosition = widgets.Aggregate((double.MaxValue, double.MaxValue), (acc, wi) =>
{
var bounds = wi.Widget.TransformToParentSpace(wi.ContainingWindow, wi.Bounds);
var x = bounds.Left - 1;
var y = bounds.Bottom - 1;
return (x < acc.Item1 ? x : acc.Item1, y < acc.Item2 ? y : acc.Item2);
});
var maxPosition = widgets.Aggregate((0d, 0d), (acc, wi) =>
{
var bounds = wi.Widget.TransformToParentSpace(wi.ContainingWindow, wi.Bounds);
var x = bounds.Right + 1;
var y = bounds.Top + 1;
return (x > acc.Item1 ? x : acc.Item1, y > acc.Item2 ? y : acc.Item2);
});
var systemWindow = widgets.First().ContainingWindow;
testRunner.SetMouseCursorPosition(systemWindow, (int)minPosition.Item1, (int)minPosition.Item2);
testRunner.DragToPosition(systemWindow, (int)maxPosition.Item1, (int)maxPosition.Item2).Drop();
return testRunner;
RectangleDouble GetBoundingBox(IObject3D part)
{
var screenBoundsOfObject3D = RectangleDouble.ZeroIntersection;
var bounds = part.GetBVHData().GetAxisAlignedBoundingBox();
for (var i = 0; i < 4; i += 1)
{
screenBoundsOfObject3D.ExpandToInclude(controlLayer.World.GetScreenPosition(bounds.GetTopCorner(i)));
screenBoundsOfObject3D.ExpandToInclude(controlLayer.World.GetScreenPosition(bounds.GetBottomCorner(i)));
}
return screenBoundsOfObject3D;
}
(bool Ok, string Name, RectangleDouble Bounds)
ResolveName(IEnumerable<IObject3D> parts, string name)
{
foreach (var part in parts)
{
if (part.Name == name)
{
return (true, name, GetBoundingBox(part));
}
if (part is GroupObject3D group)
{
var (ok, _, bounds) = ResolveName(group.Children, name);
if (ok)
{
// WARNING the position of a part changes when it's added to a group.
// Not sure if there's some sort of offset that needs to be applied or
// if this is a bug. It is restored to its correct position when the
// part is ungrouped.
return (true, name, bounds);
}
}
if (part is SelectionGroupObject3D selection)
{
var (ok, _, bounds) = ResolveName(selection.Children, name);
if (ok)
{
return (true, name, bounds);
}
}
}
return (false, null, RectangleDouble.ZeroIntersection);
}
}
}
@ -1563,4 +1675,4 @@ namespace MatterHackers.MatterControl.Tests.Automation
File.WriteAllText(ConfigPath, JsonConvert.SerializeObject(this, Formatting.Indented));
}
}
}
}