diff --git a/.gitignore b/.gitignore index 7d2dff6a1..f4531ab50 100644 --- a/.gitignore +++ b/.gitignore @@ -116,4 +116,7 @@ Backup*/ UpgradeLog*.XML MatterControl.userprefs -.vs/ \ No newline at end of file +.vs/ + +# JetBrains Rider user configuration directory +/.idea/ diff --git a/Tests/MatterControl.AutomationTests/PartPreviewTests.cs b/Tests/MatterControl.AutomationTests/PartPreviewTests.cs index cf11006d6..fd2292e78 100644 --- a/Tests/MatterControl.AutomationTests/PartPreviewTests.cs +++ b/Tests/MatterControl.AutomationTests/PartPreviewTests.cs @@ -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().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(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(scene.SelectedItem, "Scene selection should be group"); + Assert.IsInstanceOf(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(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(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(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(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(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(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(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(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(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(scene.SelectedItem, "Should have a selection group after selecting half wedge"); + Assert.IsTrue( + new HashSet(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(scene.SelectedItem, "Selection in scene should be selection group after adding sphere"); + Assert.IsTrue( + new HashSet(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 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() { diff --git a/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs b/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs index a14bde438..b6e793687 100644 --- a/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs +++ b/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs @@ -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); + } + + /// + /// 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. + /// + /// The AutomationRunner in use + /// Object control layer from a View3DWidget + /// Names of the parts to select + /// The AutomationRunner + public static AutomationRunner RectangleSelectParts(this AutomationRunner testRunner, Object3DControlsLayer controlLayer, IEnumerable partNames) + { + var topWindow = controlLayer.Parents().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 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)); } } -} \ No newline at end of file +}