mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-12 14:51:09 -05:00
feat(clone): choose Shift+click push order and add box-only Ctrl+click
Shift+click in Clone Parts always kept whichever axis order landed the copy nearest the origin, which can park it in a pocket the user didn't want. Options > Clone push placement now picks Closest to origin (the old behavior, still the default), Push left then down, or Push down then left. Ctrl+click settles the copy on bounding boxes alone, so it stops square to its neighbors' boxes instead of nesting into their shapes. Shift+click keeps its box pass then geometry settle; both follow the chosen order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
beccbae106
commit
cafb34b510
10 files changed
+216
-25
No files matched your search
@@ -390,29 +390,43 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settles a copied placement against the plate in both axis orders, choosing
|
||||
/// the group closest to the quadrant's work-area corner. A coarse box pass
|
||||
/// runs only when no moving box starts inside an existing part's box.
|
||||
/// Settles a copied placement against the plate. <see cref="SettleOrder.ClosestToCorner"/>
|
||||
/// tries both axis orders and keeps the group closest to the quadrant's work-area
|
||||
/// corner; the other orders push along the named axis first. A coarse box pass
|
||||
/// runs only when no moving box starts inside an existing part's box. With
|
||||
/// <paramref name="boundingBoxesOnly"/> every push uses bounding boxes alone, so the
|
||||
/// group stops at the part spacing from obstacles' boxes, never nesting into their shapes.
|
||||
/// </summary>
|
||||
public static void SettlePlacement(
|
||||
List<Part> movingParts,
|
||||
Plate plate,
|
||||
PushDirection horizontal,
|
||||
PushDirection vertical,
|
||||
int maxIterations = 20
|
||||
int maxIterations = 20,
|
||||
SettleOrder order = SettleOrder.ClosestToCorner,
|
||||
bool boundingBoxesOnly = false
|
||||
)
|
||||
{
|
||||
if (movingParts.Count == 0)
|
||||
return;
|
||||
|
||||
var workArea = plate.WorkArea();
|
||||
var skipBoxes = movingParts.Any(moving =>
|
||||
var skipBoxes = boundingBoxesOnly || movingParts.Any(moving =>
|
||||
plate.Parts.Any(obstacle => moving.BoundingBox.Intersects(obstacle.BoundingBox))
|
||||
);
|
||||
double PushOnce(List<Part> trial, PushDirection direction) => boundingBoxesOnly
|
||||
? PushBoundingBox(trial, plate, direction)
|
||||
: Push(trial, plate, direction);
|
||||
var bestScore = double.MaxValue;
|
||||
Vector[] best = null;
|
||||
var firstAxes = order switch
|
||||
{
|
||||
SettleOrder.HorizontalFirst => new[] { horizontal },
|
||||
SettleOrder.VerticalFirst => new[] { vertical },
|
||||
_ => new[] { horizontal, vertical },
|
||||
};
|
||||
|
||||
foreach (var first in new[] { horizontal, vertical })
|
||||
foreach (var first in firstAxes)
|
||||
{
|
||||
var second = first == horizontal ? vertical : horizontal;
|
||||
var trial = movingParts.Select(p => (Part)p.Clone()).ToList();
|
||||
@@ -424,7 +438,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
for (var i = 0; i < maxIterations; i++)
|
||||
{
|
||||
var moved = Push(trial, plate, first) + Push(trial, plate, second);
|
||||
var moved = PushOnce(trial, first) + PushOnce(trial, second);
|
||||
if (moved < 0.01)
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// How <see cref="Compactor.SettlePlacement"/> orders its pushes. Directions follow
|
||||
/// the plate's origin quadrant: "horizontal" is toward the origin's side, as is "vertical".
|
||||
/// </summary>
|
||||
public enum SettleOrder
|
||||
{
|
||||
/// <summary>Try both orders and keep the placement closest to the origin corner.</summary>
|
||||
ClosestToCorner,
|
||||
|
||||
/// <summary>Push horizontally first, then vertically, repeating until settled.</summary>
|
||||
HorizontalFirst,
|
||||
|
||||
/// <summary>Push vertically first, then horizontally, repeating until settled.</summary>
|
||||
VerticalFirst,
|
||||
}
|
||||
}
|
||||
@@ -644,6 +644,58 @@ namespace OpenNest.Tests.Fill
|
||||
Assert.Equal(start, moving.Location);
|
||||
}
|
||||
|
||||
// Two pockets: pushing left first parks the part on the tall block at (20, 55);
|
||||
// pushing down first parks it on the low floor at (55, 10), nearer the origin.
|
||||
private static Plate TwoPocketPlate()
|
||||
{
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(MakeRectPart(0, 0, 20, 100));
|
||||
plate.Parts.Add(MakeRectPart(20, 0, 35, 55));
|
||||
plate.Parts.Add(MakeRectPart(55, 0, 45, 10));
|
||||
return plate;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SettleOrder.ClosestToCorner, 55, 10)]
|
||||
[InlineData(SettleOrder.HorizontalFirst, 20, 55)]
|
||||
[InlineData(SettleOrder.VerticalFirst, 55, 10)]
|
||||
public void SettlePlacement_OrderChoosesThePocket(SettleOrder order, double left, double bottom)
|
||||
{
|
||||
var plate = TwoPocketPlate();
|
||||
var moving = MakeRectPart(60, 60, 10, 10);
|
||||
|
||||
Compactor.SettlePlacement(new List<Part> { moving }, plate,
|
||||
PushDirection.Left, PushDirection.Down, order: order);
|
||||
|
||||
Assert.Equal(left, moving.BoundingBox.Left, 3);
|
||||
Assert.Equal(bottom, moving.BoundingBox.Bottom, 3);
|
||||
Assert.All(plate.Parts, obstacle => Assert.False(moving.Intersects(obstacle, out _)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettlePlacement_BoundingBoxesOnlyStopsAtTheObstacleBox()
|
||||
{
|
||||
// Triangle material is below the diagonal; geometry lets the square slide in
|
||||
// over the hypotenuse, boxes stop it square to the triangle's bounds. The floor
|
||||
// keeps both above y = 5.
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(MakeTrianglePart(new Vector(0, 0),
|
||||
new Vector(40, 0), new Vector(0, 40)));
|
||||
plate.Parts.Add(MakeRectPart(40, 0, 60, 5));
|
||||
var geometry = MakeRectPart(50, 6, 5, 5);
|
||||
var boxes = MakeRectPart(50, 6, 5, 5);
|
||||
|
||||
Compactor.SettlePlacement(new List<Part> { geometry }, plate,
|
||||
PushDirection.Left, PushDirection.Down, order: SettleOrder.HorizontalFirst);
|
||||
Compactor.SettlePlacement(new List<Part> { boxes }, plate,
|
||||
PushDirection.Left, PushDirection.Down, order: SettleOrder.HorizontalFirst,
|
||||
boundingBoxesOnly: true);
|
||||
|
||||
Assert.True(geometry.BoundingBox.Left < 39, $"geometry left {geometry.BoundingBox.Left}");
|
||||
Assert.InRange(boxes.BoundingBox.Left, 40, 40.01);
|
||||
Assert.InRange(boxes.BoundingBox.Bottom, 5, 5.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushBoundingBox_Left_MovesPartTowardEdge()
|
||||
{
|
||||
|
||||
@@ -92,6 +92,28 @@ public class ActionCloneTests
|
||||
Assert.Single(plate.Parts);
|
||||
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
|
||||
|
||||
[Theory]
|
||||
[InlineData(Keys.Shift)]
|
||||
[InlineData(Keys.Control)]
|
||||
public void ModifiedClickPushesTheCopyAgainstPlacedParts(Keys modifiers) => StaTestThread.Run(() =>
|
||||
{
|
||||
var plate = new Plate(48, 96) { PartSpacing = 0 };
|
||||
plate.Parts.Add(new Part(Square("placed")));
|
||||
using var view = new PlateView { Plate = plate, Size = new System.Drawing.Size(600, 600) };
|
||||
view.ZoomToFit();
|
||||
view.SetAction(typeof(ActionClone), Square("new"));
|
||||
var action = Assert.IsType<ActionClone>(view.Actions.CurrentAction);
|
||||
action.PlacementOrder = OpenNest.Engine.Fill.SettleOrder.VerticalFirst;
|
||||
var preview = Assert.Single(view.SelectedParts);
|
||||
preview.BasePart.Location = new Vector(20, 1);
|
||||
|
||||
action.Apply(modifiers);
|
||||
|
||||
var copy = plate.Parts[1].BoundingBox;
|
||||
Assert.InRange(copy.Left, 4, 4.01);
|
||||
Assert.Equal(0, copy.Bottom, 6);
|
||||
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
|
||||
|
||||
[Fact]
|
||||
public void ADialogsFakeMouseMoveLeavesNudgedPreviewsInPlace() => StaTestThread.Run(() =>
|
||||
{
|
||||
|
||||
@@ -200,11 +200,30 @@ namespace OpenNest.Actions
|
||||
change.Complete();
|
||||
}
|
||||
|
||||
public void Apply()
|
||||
/// <summary>
|
||||
/// Which way Shift+click and Ctrl+click push the copy; null reads the user's Options
|
||||
/// setting at each click, so a change there applies without restarting the action.
|
||||
/// </summary>
|
||||
public SettleOrder? PlacementOrder { get; set; }
|
||||
|
||||
// An unknown stored name falls back to the closest-to-origin search.
|
||||
internal static SettleOrder ReadPlacementOrder() =>
|
||||
System.Enum.TryParse<SettleOrder>(Properties.Settings.Default.ClonePlacementOrder, out var order)
|
||||
&& System.Enum.IsDefined(order)
|
||||
? order
|
||||
: SettleOrder.ClosestToCorner;
|
||||
|
||||
public void Apply() => Apply(Control.ModifierKeys);
|
||||
|
||||
// Shift settles on the parts' geometry (a box pass first when clear); Ctrl settles on
|
||||
// bounding boxes alone, so the copy stops square to its neighbors' boxes.
|
||||
internal void Apply(Keys modifiers)
|
||||
{
|
||||
using var change = plateView.BeginChange(parts.Count == 1 ? "Add Part" : "Add Parts");
|
||||
change.MarkChanged();
|
||||
if ((Control.ModifierKeys & Keys.Shift) == Keys.Shift)
|
||||
var shift = (modifiers & Keys.Shift) == Keys.Shift;
|
||||
var boxesOnly = !shift && (modifiers & Keys.Control) == Keys.Control;
|
||||
if (shift || boxesOnly)
|
||||
{
|
||||
var movingParts = parts.Select(p => p.BasePart).ToList();
|
||||
|
||||
@@ -234,7 +253,8 @@ namespace OpenNest.Actions
|
||||
break;
|
||||
}
|
||||
|
||||
Compactor.SettlePlacement(movingParts, plateView.Plate, hDir, vDir);
|
||||
Compactor.SettlePlacement(movingParts, plateView.Plate, hDir, vDir,
|
||||
order: PlacementOrder ?? ReadPlacementOrder(), boundingBoxesOnly: boxesOnly);
|
||||
|
||||
parts.ForEach(p => p.IsDirty = true);
|
||||
plateView.Invalidate();
|
||||
|
||||
Generated
+42
-14
@@ -43,6 +43,8 @@
|
||||
this.strategyGroupBox = new System.Windows.Forms.GroupBox();
|
||||
this.colorSchemeLabel = new System.Windows.Forms.Label();
|
||||
this.colorSchemeCombo = new System.Windows.Forms.ComboBox();
|
||||
this.clonePlacementLabel = new System.Windows.Forms.Label();
|
||||
this.clonePlacementCombo = new System.Windows.Forms.ComboBox();
|
||||
this.serverGroupBox = new System.Windows.Forms.GroupBox();
|
||||
this.serverUrlBox = new System.Windows.Forms.TextBox();
|
||||
this.setServerButton = new System.Windows.Forms.Button();
|
||||
@@ -71,7 +73,7 @@
|
||||
this.checkBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.checkBox1.AutoSize = true;
|
||||
this.tableLayoutPanel1.SetColumnSpan(this.checkBox1, 2);
|
||||
this.checkBox1.Location = new System.Drawing.Point(3, 165);
|
||||
this.checkBox1.Location = new System.Drawing.Point(3, 213);
|
||||
this.checkBox1.Name = "checkBox1";
|
||||
this.checkBox1.Size = new System.Drawing.Size(281, 20);
|
||||
this.checkBox1.TabIndex = 7;
|
||||
@@ -143,16 +145,19 @@
|
||||
this.tableLayoutPanel1.Controls.Add(this.colorSchemeCombo, 1, 1);
|
||||
this.tableLayoutPanel1.Controls.Add(this.planningEffortLabel, 0, 2);
|
||||
this.tableLayoutPanel1.Controls.Add(this.planningEffortBox, 1, 2);
|
||||
this.tableLayoutPanel1.Controls.Add(this.checkBox1, 0, 3);
|
||||
this.tableLayoutPanel1.Controls.Add(this.clonePlacementLabel, 0, 3);
|
||||
this.tableLayoutPanel1.Controls.Add(this.clonePlacementCombo, 1, 3);
|
||||
this.tableLayoutPanel1.Controls.Add(this.checkBox1, 0, 4);
|
||||
this.tableLayoutPanel1.Controls.Add(this.numericUpDown1, 1, 0);
|
||||
this.tableLayoutPanel1.Location = new System.Drawing.Point(12, 12);
|
||||
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
this.tableLayoutPanel1.RowCount = 4;
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 25F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 25F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 25F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 25F));
|
||||
this.tableLayoutPanel1.Size = new System.Drawing.Size(684, 200);
|
||||
this.tableLayoutPanel1.RowCount = 5;
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
|
||||
this.tableLayoutPanel1.Size = new System.Drawing.Size(684, 240);
|
||||
this.tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// saveButton
|
||||
@@ -185,7 +190,7 @@
|
||||
this.bottomPanel1.Controls.Add(this.cancelButton);
|
||||
this.bottomPanel1.Controls.Add(this.saveButton);
|
||||
this.bottomPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.bottomPanel1.Location = new System.Drawing.Point(0, 647);
|
||||
this.bottomPanel1.Location = new System.Drawing.Point(0, 715);
|
||||
this.bottomPanel1.Name = "bottomPanel1";
|
||||
this.bottomPanel1.Size = new System.Drawing.Size(708, 50);
|
||||
this.bottomPanel1.TabIndex = 1;
|
||||
@@ -204,7 +209,7 @@
|
||||
// strategyGroupBox
|
||||
//
|
||||
this.strategyGroupBox.Controls.Add(this.strategyGrid);
|
||||
this.strategyGroupBox.Location = new System.Drawing.Point(12, 485);
|
||||
this.strategyGroupBox.Location = new System.Drawing.Point(12, 525);
|
||||
this.strategyGroupBox.Name = "strategyGroupBox";
|
||||
this.strategyGroupBox.Size = new System.Drawing.Size(684, 180);
|
||||
this.strategyGroupBox.TabIndex = 2;
|
||||
@@ -230,11 +235,32 @@
|
||||
this.colorSchemeCombo.Size = new System.Drawing.Size(130, 24);
|
||||
this.colorSchemeCombo.TabIndex = 11;
|
||||
//
|
||||
// clonePlacementLabel
|
||||
//
|
||||
this.clonePlacementLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.clonePlacementLabel.AutoSize = true;
|
||||
this.clonePlacementLabel.Location = new System.Drawing.Point(3, 172);
|
||||
this.clonePlacementLabel.Name = "clonePlacementLabel";
|
||||
this.clonePlacementLabel.Size = new System.Drawing.Size(145, 16);
|
||||
this.clonePlacementLabel.TabIndex = 14;
|
||||
this.clonePlacementLabel.Text = "Clone push placement:";
|
||||
//
|
||||
// clonePlacementCombo
|
||||
//
|
||||
this.clonePlacementCombo.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.clonePlacementCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.clonePlacementCombo.Location = new System.Drawing.Point(154, 168);
|
||||
this.clonePlacementCombo.Name = "clonePlacementCombo";
|
||||
this.clonePlacementCombo.Size = new System.Drawing.Size(130, 24);
|
||||
this.clonePlacementCombo.TabIndex = 15;
|
||||
this.toolTip1.SetToolTip(this.clonePlacementCombo, "Where Clone Parts pushes the copy on Shift+click (part geometry) "
|
||||
+ "or Ctrl+click (bounding boxes). Left and down mirror for plates whose origin is in another corner.");
|
||||
//
|
||||
// serverGroupBox
|
||||
//
|
||||
this.serverGroupBox.Controls.Add(this.serverUrlBox);
|
||||
this.serverGroupBox.Controls.Add(this.setServerButton);
|
||||
this.serverGroupBox.Location = new System.Drawing.Point(12, 218);
|
||||
this.serverGroupBox.Location = new System.Drawing.Point(12, 258);
|
||||
this.serverGroupBox.Name = "serverGroupBox";
|
||||
this.serverGroupBox.Size = new System.Drawing.Size(684, 69);
|
||||
this.serverGroupBox.TabIndex = 1;
|
||||
@@ -264,7 +290,7 @@
|
||||
this.updateGroupBox.Controls.Add(this.updateFeedUrlBox);
|
||||
this.updateGroupBox.Controls.Add(this.setUpdateFeedButton);
|
||||
this.updateGroupBox.Controls.Add(this.includeDevBuildsBox);
|
||||
this.updateGroupBox.Location = new System.Drawing.Point(12, 391);
|
||||
this.updateGroupBox.Location = new System.Drawing.Point(12, 431);
|
||||
this.updateGroupBox.Name = "updateGroupBox";
|
||||
this.updateGroupBox.Size = new System.Drawing.Size(684, 88);
|
||||
this.updateGroupBox.TabIndex = 3;
|
||||
@@ -307,7 +333,7 @@
|
||||
this.ledgerGroupBox.Controls.Add(this.ledgerKeyBox);
|
||||
this.ledgerGroupBox.Controls.Add(this.testLedgerButton);
|
||||
this.ledgerGroupBox.Controls.Add(this.saveLedgerButton);
|
||||
this.ledgerGroupBox.Location = new System.Drawing.Point(12, 293);
|
||||
this.ledgerGroupBox.Location = new System.Drawing.Point(12, 333);
|
||||
this.ledgerGroupBox.Name = "ledgerGroupBox";
|
||||
this.ledgerGroupBox.Size = new System.Drawing.Size(684, 92);
|
||||
this.ledgerGroupBox.TabIndex = 4;
|
||||
@@ -374,7 +400,7 @@
|
||||
this.AcceptButton = this.saveButton;
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
this.CancelButton = this.cancelButton;
|
||||
this.ClientSize = new System.Drawing.Size(708, 725);
|
||||
this.ClientSize = new System.Drawing.Size(708, 765);
|
||||
this.Controls.Add(this.strategyGroupBox);
|
||||
this.Controls.Add(this.updateGroupBox);
|
||||
this.Controls.Add(this.ledgerGroupBox);
|
||||
@@ -421,6 +447,8 @@
|
||||
private System.Windows.Forms.GroupBox strategyGroupBox;
|
||||
private System.Windows.Forms.Label colorSchemeLabel;
|
||||
private System.Windows.Forms.ComboBox colorSchemeCombo;
|
||||
private System.Windows.Forms.Label clonePlacementLabel;
|
||||
private System.Windows.Forms.ComboBox clonePlacementCombo;
|
||||
private System.Windows.Forms.GroupBox serverGroupBox;
|
||||
private System.Windows.Forms.TextBox serverUrlBox;
|
||||
private System.Windows.Forms.Button setServerButton;
|
||||
|
||||
@@ -3,10 +3,12 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Actions;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Data.MaterialLedger;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Properties;
|
||||
|
||||
@@ -14,6 +16,14 @@ namespace OpenNest.Forms
|
||||
{
|
||||
public partial class OptionsForm : Form
|
||||
{
|
||||
// Combo order matches these; the setting stores the enum name.
|
||||
private static readonly (SettleOrder Order, string Text)[] ClonePlacements =
|
||||
{
|
||||
(SettleOrder.ClosestToCorner, "Closest to origin"),
|
||||
(SettleOrder.HorizontalFirst, "Push left, then down"),
|
||||
(SettleOrder.VerticalFirst, "Push down, then left"),
|
||||
};
|
||||
|
||||
public OptionsForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -96,6 +106,13 @@ namespace OpenNest.Forms
|
||||
var idx = colorSchemeCombo.Items.IndexOf(active);
|
||||
colorSchemeCombo.SelectedIndex = idx >= 0 ? idx : 0;
|
||||
|
||||
clonePlacementCombo.Items.Clear();
|
||||
foreach (var (_, text) in ClonePlacements)
|
||||
clonePlacementCombo.Items.Add(text);
|
||||
var placement = ActionClone.ReadPlacementOrder();
|
||||
clonePlacementCombo.SelectedIndex = System.Math.Max(0,
|
||||
System.Array.FindIndex(ClonePlacements, p => p.Order == placement));
|
||||
|
||||
var disabledNames = ParseDisabledStrategies(Settings.Default.DisabledStrategies);
|
||||
foreach (DataGridViewRow row in strategyGrid.Rows)
|
||||
row.Cells["Enabled"].Value = !disabledNames.Contains(
|
||||
@@ -111,6 +128,8 @@ namespace OpenNest.Forms
|
||||
Settings.Default.CuttingPlanEffort = (double)planningEffortBox.Value;
|
||||
Settings.Default.ActiveColorScheme =
|
||||
colorSchemeCombo.SelectedItem as string ?? "Classic";
|
||||
Settings.Default.ClonePlacementOrder =
|
||||
ClonePlacements[System.Math.Max(0, clonePlacementCombo.SelectedIndex)].Order.ToString();
|
||||
|
||||
var disabledNames = new List<string>();
|
||||
foreach (DataGridViewRow row in strategyGrid.Rows)
|
||||
|
||||
Generated
+13
@@ -488,5 +488,18 @@ namespace OpenNest.Properties {
|
||||
this["MadeBy"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("ClosestToCorner")]
|
||||
// Where Shift+click in Clone Parts pushes the copy: an OpenNest.Engine.Fill.SettleOrder name.
|
||||
public string ClonePlacementOrder {
|
||||
get {
|
||||
return ((string)(this["ClonePlacementOrder"]));
|
||||
}
|
||||
set {
|
||||
this["ClonePlacementOrder"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,5 +116,8 @@
|
||||
<Setting Name="MadeBy" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="ClonePlacementOrder" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">ClosestToCorner</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
@@ -16,7 +16,9 @@ Open/incomplete chains and ambiguous contacts conservatively block. Loops are re
|
||||
- CPU best-fit batches prepare contact topology once and use all vertices plus curve/line interior and curve/curve tangency events. The old leading-half vertex filter cannot establish the next blocker after a skipped touch.
|
||||
- GPU kernels retain nearest-hit reduction and return unsnapped contact witnesses. The shared CPU classifier accepts a blocking witness or replays the full query after a nonblocking witness, preserving tied/later blockers. Both batch APIs honor active buffer lengths and refresh mutated/reused segment arrays. The GPU distance adapter sends only exact cardinal directions to the axis-only slide interface; arbitrary directions and native curves use the shared CPU path.
|
||||
|
||||
- Shift-click while cloning parts settles the copied group toward the plate quadrant. If its starting bounds overlap any placed part's bounds, it skips the coarse bounding-box pass and uses geometry directly; otherwise it tries coarse horizontal/vertical and vertical/horizontal orders. Each candidate then alternates geometry pushes until movement is negligible (at most 20 iterations), and the group nearest the quadrant's work-area corner wins. The coarse pass remains useful for avoiding sawtooth/rung traps when the starting boxes do not overlap.
|
||||
- Shift-click while cloning parts settles the copied group toward the plate quadrant. If its starting bounds overlap any placed part's bounds, it skips the coarse bounding-box pass and uses geometry directly; otherwise each axis order starts with a coarse box pass. Each candidate then alternates geometry pushes until movement is negligible (at most 20 iterations). The coarse pass remains useful for avoiding sawtooth/rung traps when the starting boxes do not overlap.
|
||||
- Ctrl-click settles the same way on bounding boxes alone (`SettlePlacement(boundingBoxesOnly: true)`): every push stops at the part spacing from obstacles' boxes, so the copy never nests into a neighbor's shape. A group whose box already overlaps an obstacle's box does not move.
|
||||
- Options > Clone push placement (`ClonePlacementOrder`, a `SettleOrder` name) picks the axis order for both: *Closest to origin* (default) tries horizontal-first and vertical-first and keeps the group nearest the quadrant's work-area corner; *Push left, then down* and *Push down, then left* run only that order. Left and down mirror with the plate quadrant.
|
||||
|
||||
## Regression coverage
|
||||
`SlideContactTests` exercises cardinal line, translated line, reused edge-array, arbitrary-vector, native-entity, and both CPU batch paths. Cases include winding reversal, nonzero origins, rotated hooks, holes, separating circles, positive-distance grazing followed by a blocker, full-circle arc seams, concave/straight junctions, thin rings, and circle/line interior contact.
|
||||
|
||||
Reference in new issue
Block a user